Compare commits
2 Commits
b4fc1a545c
...
5de857aca8
Author | SHA1 | Date | |
---|---|---|---|
5de857aca8 | |||
f2cc76a8ea |
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "page-proxy",
|
"name": "page-proxy",
|
||||||
"version": "0.0.4",
|
"version": "0.0.5",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
@ -7,21 +7,26 @@ import { nanoid } from 'nanoid';
|
|||||||
import { pipeline } from 'stream';
|
import { pipeline } from 'stream';
|
||||||
import { promisify } from 'util';
|
import { promisify } from 'util';
|
||||||
import { fetchApp, fetchDomain, fetchTest } from './query/get-router.ts';
|
import { fetchApp, fetchDomain, fetchTest } from './query/get-router.ts';
|
||||||
import { getAppLoadStatus, setAppLoadStatus, AppLoadStatus } from './redis/get-app-status.ts';
|
import { getAppLoadStatus, setAppLoadStatus } from './redis/get-app-status.ts';
|
||||||
import { bucketName, minioClient, minioResources } from './minio.ts';
|
import { minioResources } from './minio.ts';
|
||||||
import { downloadFileFromMinio } from './proxy/http-proxy.ts';
|
import { downloadFileFromMinio } from './proxy/http-proxy.ts';
|
||||||
|
|
||||||
const pipelineAsync = promisify(pipeline);
|
const pipelineAsync = promisify(pipeline);
|
||||||
|
|
||||||
const { resources } = config?.proxy || { resources: 'https://minio.xiongxiao.me/resources' };
|
const { resources } = config?.proxy || { resources: 'https://minio.xiongxiao.me/resources' };
|
||||||
|
const wrapperResources = (resources: string, urlpath: string) => {
|
||||||
|
if (urlpath.startsWith('http')) {
|
||||||
|
return urlpath;
|
||||||
|
}
|
||||||
|
return `${resources}/${urlpath}`;
|
||||||
|
};
|
||||||
const demoData = {
|
const demoData = {
|
||||||
user: 'root',
|
user: 'root',
|
||||||
key: 'codeflow',
|
key: 'codeflow',
|
||||||
appType: 'web-single', //
|
appType: 'web-single', //
|
||||||
version: '1.0.0',
|
version: '1.0.0',
|
||||||
domain: null,
|
domain: null,
|
||||||
type: 'local', // local, oss, 默认是oss
|
type: 'oss', // oss, 默认是oss
|
||||||
data: {
|
data: {
|
||||||
files: [
|
files: [
|
||||||
{
|
{
|
||||||
@ -221,9 +226,9 @@ export class UserApp {
|
|||||||
// 将文件名和路径添加到 `data` 对象中
|
// 将文件名和路径添加到 `data` 对象中
|
||||||
files.forEach((file) => {
|
files.forEach((file) => {
|
||||||
if (file.name === 'index.html') {
|
if (file.name === 'index.html') {
|
||||||
indexHtml = resources + '/' + file.path;
|
indexHtml = wrapperResources(resources, file.path);
|
||||||
}
|
}
|
||||||
data[file.name] = resources + '/' + file.path;
|
data[file.name] = wrapperResources(resources, file.path);
|
||||||
});
|
});
|
||||||
await redis.set('user:app:exist:' + app + ':' + user, indexHtml + '||etag||true', 'EX', 60 * 60 * 24 * 7); // 7天
|
await redis.set('user:app:exist:' + app + ':' + user, indexHtml + '||etag||true', 'EX', 60 * 60 * 24 * 7); // 7天
|
||||||
await redis.set('user:app:permission:' + app + ':' + user, JSON.stringify(permission), 'EX', 60 * 60 * 24 * 7); // 7天
|
await redis.set('user:app:permission:' + app + ':' + user, JSON.stringify(permission), 'EX', 60 * 60 * 24 * 7); // 7天
|
||||||
@ -326,38 +331,9 @@ export const downloadUserAppFiles = async (user: string, app: string, data: type
|
|||||||
fs.mkdirSync(uploadFiles, { recursive: true });
|
fs.mkdirSync(uploadFiles, { recursive: true });
|
||||||
}
|
}
|
||||||
const newFiles = [];
|
const newFiles = [];
|
||||||
try {
|
|
||||||
if (data.type === 'local') {
|
|
||||||
// local copy file
|
|
||||||
for (let i = 0; i < files.length; i++) {
|
|
||||||
const file = files[i];
|
|
||||||
const copyFile = path.join(fileStore, file.path);
|
|
||||||
const destFile = path.join(uploadFiles, file.name);
|
|
||||||
const destDir = path.dirname(destFile); // 获取目标文件所在的目录路径
|
|
||||||
// 检查目录是否存在,如果不存在则创建
|
|
||||||
if (!checkFileExistsSync(destDir)) {
|
|
||||||
fs.mkdirSync(destDir, { recursive: true }); // 递归创建目录
|
|
||||||
}
|
|
||||||
fs.copyFileSync(copyFile, destFile);
|
|
||||||
// const etag = await setEtag(fs.readFileSync(destFile, 'utf-8'));
|
|
||||||
const etag = nanoid();
|
|
||||||
newFiles.push({
|
|
||||||
name: file.name,
|
|
||||||
path: destFile.replace(fileStore, '') + '||' + etag,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
const userApp = new UserApp({ user, app });
|
|
||||||
userApp.clearCacheData();
|
|
||||||
return {
|
|
||||||
data: {
|
|
||||||
files: [],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (data.type === 'oss') {
|
if (data.type === 'oss') {
|
||||||
let serverPath = new URL(resources).href + '/';
|
let serverPath = new URL(resources).href;
|
||||||
let hasIndexHtml = false;
|
let hasIndexHtml = false;
|
||||||
// server download file
|
// server download file
|
||||||
for (let i = 0; i < files.length; i++) {
|
for (let i = 0; i < files.length; i++) {
|
||||||
@ -371,8 +347,9 @@ export const downloadUserAppFiles = async (user: string, app: string, data: type
|
|||||||
if (!checkFileExistsSync(destDir)) {
|
if (!checkFileExistsSync(destDir)) {
|
||||||
fs.mkdirSync(destDir, { recursive: true }); // 递归创建目录
|
fs.mkdirSync(destDir, { recursive: true }); // 递归创建目录
|
||||||
}
|
}
|
||||||
|
const downloadURL = wrapperResources(serverPath, file.path);
|
||||||
// 下载文件到 destFile
|
// 下载文件到 destFile
|
||||||
await downloadFile(serverPath + file.path, destFile);
|
await downloadFile(downloadURL, destFile);
|
||||||
const etag = nanoid();
|
const etag = nanoid();
|
||||||
newFiles.push({
|
newFiles.push({
|
||||||
name: file.name,
|
name: file.name,
|
||||||
|
@ -11,11 +11,48 @@ import { getTextFromStreamAndAddStat, httpProxy } from './proxy/http-proxy.ts';
|
|||||||
import { UserPermission } from '@kevisual/permission';
|
import { UserPermission } from '@kevisual/permission';
|
||||||
import { getLoginUser } from '@/middleware/auth.ts';
|
import { getLoginUser } from '@/middleware/auth.ts';
|
||||||
import { rediretHome } from './user-home/index.ts';
|
import { rediretHome } from './user-home/index.ts';
|
||||||
|
import { aiProxy } from './proxy/ai-proxy.ts';
|
||||||
const api = config?.api || { host: 'kevisual.xiongxiao.me', path: '/api/router' };
|
const api = config?.api || { host: 'kevisual.xiongxiao.me', path: '/api/router' };
|
||||||
const domain = config?.proxy?.domain || 'kevisual.xiongxiao.me';
|
const domain = config?.proxy?.domain || 'kevisual.xiongxiao.me';
|
||||||
const allowedOrigins = config?.proxy?.allowedOrigin || [];
|
const allowedOrigins = config?.proxy?.allowedOrigin || [];
|
||||||
|
|
||||||
const noProxyUrl = ['/', '/favicon.ico'];
|
const noProxyUrl = ['/', '/favicon.ico'];
|
||||||
|
const notAuthPathList = [
|
||||||
|
{
|
||||||
|
user: 'root',
|
||||||
|
paths: ['center'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
user: 'admin',
|
||||||
|
paths: ['center'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
user: 'user',
|
||||||
|
paths: ['login'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
user: 'public',
|
||||||
|
paths: ['center'],
|
||||||
|
all: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
user: 'test',
|
||||||
|
paths: ['center'],
|
||||||
|
all: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const checkNotAuthPath = (user, app) => {
|
||||||
|
const notAuthPath = notAuthPathList.find((item) => {
|
||||||
|
if (item.user === user) {
|
||||||
|
if (item.all) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return item.paths?.includes?.(app);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
return notAuthPath;
|
||||||
|
};
|
||||||
export const handleRequest = async (req: http.IncomingMessage, res: http.ServerResponse) => {
|
export const handleRequest = async (req: http.IncomingMessage, res: http.ServerResponse) => {
|
||||||
const querySearch = new URL(req.url, `http://${req.headers.host}`).searchParams;
|
const querySearch = new URL(req.url, `http://${req.headers.host}`).searchParams;
|
||||||
const password = querySearch.get('p');
|
const password = querySearch.get('p');
|
||||||
@ -176,10 +213,7 @@ export const handleRequest = async (req: http.IncomingMessage, res: http.ServerR
|
|||||||
user = _user;
|
user = _user;
|
||||||
app = _app;
|
app = _app;
|
||||||
}
|
}
|
||||||
|
const createRefreshPage = (user, app) => {
|
||||||
const userApp = new UserApp({ user, app });
|
|
||||||
let isExist = await userApp.getExist();
|
|
||||||
const createRefreshPage = () => {
|
|
||||||
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
||||||
res.end(createRefreshHtml(user, app));
|
res.end(createRefreshHtml(user, app));
|
||||||
};
|
};
|
||||||
@ -193,11 +227,20 @@ export const handleRequest = async (req: http.IncomingMessage, res: http.ServerR
|
|||||||
res.write(msg || 'Not Found App\n');
|
res.write(msg || 'Not Found App\n');
|
||||||
res.end();
|
res.end();
|
||||||
};
|
};
|
||||||
|
if (app === 'ai' || user === 'resources') {
|
||||||
|
return aiProxy(req, res, {
|
||||||
|
createNotFoundPage,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const userApp = new UserApp({ user, app });
|
||||||
|
let isExist = await userApp.getExist();
|
||||||
|
|
||||||
if (!isExist) {
|
if (!isExist) {
|
||||||
try {
|
try {
|
||||||
const { code, loading, message } = await userApp.setCacheData();
|
const { code, loading, message } = await userApp.setCacheData();
|
||||||
if (loading || code === 20000) {
|
if (loading || code === 20000) {
|
||||||
return createRefreshPage();
|
return createRefreshPage(user, app);
|
||||||
} else if (code === 500) {
|
} else if (code === 500) {
|
||||||
return createNotFoundPage(message || 'Not Found App\n');
|
return createNotFoundPage(message || 'Not Found App\n');
|
||||||
} else if (code !== 200) {
|
} else if (code !== 200) {
|
||||||
@ -214,42 +257,7 @@ export const handleRequest = async (req: http.IncomingMessage, res: http.ServerR
|
|||||||
if (!isExist) {
|
if (!isExist) {
|
||||||
return createNotFoundPage();
|
return createNotFoundPage();
|
||||||
}
|
}
|
||||||
const notAuthPathList = [
|
|
||||||
{
|
|
||||||
user: 'root',
|
|
||||||
paths: ['center'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
user: 'admin',
|
|
||||||
paths: ['center'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
user: 'user',
|
|
||||||
paths: ['login'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
user: 'public',
|
|
||||||
paths: ['center'],
|
|
||||||
all: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
user: 'test',
|
|
||||||
paths: ['center'],
|
|
||||||
all: true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const checkNotAuthPath = (user, app) => {
|
|
||||||
const notAuthPath = notAuthPathList.find((item) => {
|
|
||||||
if (item.user === user) {
|
|
||||||
if (item.all) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return item.paths?.includes?.(app);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
return notAuthPath;
|
|
||||||
};
|
|
||||||
if (!checkNotAuthPath(user, app)) {
|
if (!checkNotAuthPath(user, app)) {
|
||||||
const { permission } = isExist;
|
const { permission } = isExist;
|
||||||
const permissionInstance = new UserPermission({ permission, owner: user });
|
const permissionInstance = new UserPermission({ permission, owner: user });
|
||||||
@ -283,7 +291,6 @@ export const handleRequest = async (req: http.IncomingMessage, res: http.ServerR
|
|||||||
userApp,
|
userApp,
|
||||||
createNotFoundPage,
|
createNotFoundPage,
|
||||||
});
|
});
|
||||||
// userApp.clearCacheData()
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log('appFile', appFile, appFileUrl, isExist);
|
console.log('appFile', appFile, appFileUrl, isExist);
|
||||||
@ -295,9 +302,6 @@ export const handleRequest = async (req: http.IncomingMessage, res: http.ServerR
|
|||||||
const isHTML = contentType.includes('html');
|
const isHTML = contentType.includes('html');
|
||||||
const filePath = path.join(fileStore, indexFilePath);
|
const filePath = path.join(fileStore, indexFilePath);
|
||||||
if (!userApp.fileCheck(filePath)) {
|
if (!userApp.fileCheck(filePath)) {
|
||||||
// 动态删除文件
|
|
||||||
// res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
||||||
// res.write('App Cache expired, Please refresh\n');
|
|
||||||
res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8', tips: 'App Cache expired, Please refresh' });
|
res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8', tips: 'App Cache expired, Please refresh' });
|
||||||
res.write(createRefreshHtml(user, app));
|
res.write(createRefreshHtml(user, app));
|
||||||
res.end();
|
res.end();
|
||||||
|
72
src/module/proxy/ai-proxy.ts
Normal file
72
src/module/proxy/ai-proxy.ts
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import { bucketName, minioClient } from '../minio.ts';
|
||||||
|
import { IncomingMessage, ServerResponse } from 'http';
|
||||||
|
import { filterKeys } from './http-proxy.ts';
|
||||||
|
import { getUserFromRequest } from '@/utils/get-user.ts';
|
||||||
|
import { UserPermission, Permission } from '@kevisual/permission';
|
||||||
|
import { getLoginUser } from '@/middleware/auth.ts';
|
||||||
|
|
||||||
|
export const aiProxy = async (
|
||||||
|
req: IncomingMessage,
|
||||||
|
res: ServerResponse,
|
||||||
|
opts: {
|
||||||
|
createNotFoundPage: (msg?: string) => any;
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
const { createNotFoundPage } = opts;
|
||||||
|
const _u = new URL(req.url, 'http://localhost');
|
||||||
|
|
||||||
|
const pathname = _u.pathname;
|
||||||
|
const params = _u.searchParams;
|
||||||
|
const password = params.get('p');
|
||||||
|
let objectName = '';
|
||||||
|
let owner = '';
|
||||||
|
const { user, app } = getUserFromRequest(req);
|
||||||
|
if (user === 'ai') {
|
||||||
|
const version = params.get('version') || '1.0.0'; // root/ai
|
||||||
|
objectName = pathname.replace(`/${user}/${app}/`, `${user}/${app}/${version}/`);
|
||||||
|
owner = user;
|
||||||
|
} else {
|
||||||
|
objectName = pathname.replace(`/${user}`, ``); // resources/root/
|
||||||
|
owner = app;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const stat = await minioClient.statObject(bucketName, objectName);
|
||||||
|
if (stat.size === 0) {
|
||||||
|
createNotFoundPage('Invalid proxy url');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const permissionInstance = new UserPermission({ permission: stat.metaData as Permission, owner: owner });
|
||||||
|
const loginUser = await getLoginUser(req);
|
||||||
|
const checkPermission = permissionInstance.checkPermissionSuccess({
|
||||||
|
username: loginUser?.tokenUser?.username || '',
|
||||||
|
password: password,
|
||||||
|
});
|
||||||
|
if (!checkPermission.success) {
|
||||||
|
return createNotFoundPage('no permission');
|
||||||
|
}
|
||||||
|
const filterMetaData = filterKeys(stat.metaData, ['size', 'etag', 'last-modified']);
|
||||||
|
const contentLength = stat.size;
|
||||||
|
const etag = stat.etag;
|
||||||
|
const lastModified = stat.lastModified.toISOString();
|
||||||
|
const fileName = objectName.split('/').pop();
|
||||||
|
|
||||||
|
const objectStream = await minioClient.getObject(bucketName, objectName);
|
||||||
|
const headers = {
|
||||||
|
'Content-Length': contentLength,
|
||||||
|
etag,
|
||||||
|
'last-modified': lastModified,
|
||||||
|
'file-name': fileName,
|
||||||
|
...filterMetaData,
|
||||||
|
};
|
||||||
|
|
||||||
|
res.writeHead(200, {
|
||||||
|
...headers,
|
||||||
|
});
|
||||||
|
objectStream.pipe(res, { end: true });
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Proxy request error: ${error.message}`);
|
||||||
|
createNotFoundPage('Invalid ai proxy url');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
@ -32,18 +32,18 @@ export async function minioProxy(
|
|||||||
res: ServerResponse,
|
res: ServerResponse,
|
||||||
opts: {
|
opts: {
|
||||||
proxyUrl: string;
|
proxyUrl: string;
|
||||||
userApp: UserApp;
|
|
||||||
createNotFoundPage: (msg?: string) => any;
|
createNotFoundPage: (msg?: string) => any;
|
||||||
isDownload?: boolean;
|
isDownload?: boolean;
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
const fileUrl = opts.proxyUrl;
|
const fileUrl = opts.proxyUrl;
|
||||||
const { userApp, createNotFoundPage, isDownload = false } = opts;
|
const { createNotFoundPage, isDownload = false } = opts;
|
||||||
const objectName = fileUrl.replace(minioResources + '/', '');
|
const objectName = fileUrl.replace(minioResources + '/', '');
|
||||||
try {
|
try {
|
||||||
const stat = await minioClient.statObject(bucketName, objectName);
|
const stat = await minioClient.statObject(bucketName, objectName);
|
||||||
if (stat.size === 0) {
|
if (stat.size === 0) {
|
||||||
return createNotFoundPage('Invalid proxy url');
|
createNotFoundPage('Invalid proxy url');
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
const filterMetaData = filterKeys(stat.metaData, ['size', 'etag', 'last-modified']);
|
const filterMetaData = filterKeys(stat.metaData, ['size', 'etag', 'last-modified']);
|
||||||
const contentLength = stat.size;
|
const contentLength = stat.size;
|
||||||
@ -72,10 +72,11 @@ export async function minioProxy(
|
|||||||
});
|
});
|
||||||
objectStream.pipe(res, { end: true });
|
objectStream.pipe(res, { end: true });
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Proxy request error: ${error.message}`);
|
console.error(`Proxy request error: ${error.message}`);
|
||||||
userApp.clearCacheData();
|
createNotFoundPage('Invalid proxy url');
|
||||||
return createNotFoundPage('Invalid proxy url');
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,7 +115,11 @@ export const httpProxy = async (
|
|||||||
const params = _u.searchParams;
|
const params = _u.searchParams;
|
||||||
const isDownload = params.get('download') === 'true';
|
const isDownload = params.get('download') === 'true';
|
||||||
if (proxyUrl.startsWith(minioResources)) {
|
if (proxyUrl.startsWith(minioResources)) {
|
||||||
return minioProxy(req, res, { ...opts, isDownload });
|
const isOk = await minioProxy(req, res, { ...opts, isDownload });
|
||||||
|
if (!isOk) {
|
||||||
|
userApp.clearCacheData();
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
let protocol = proxyUrl.startsWith('https') ? https : http;
|
let protocol = proxyUrl.startsWith('https') ? https : http;
|
||||||
// 代理
|
// 代理
|
||||||
|
11
src/utils/get-user.ts
Normal file
11
src/utils/get-user.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { IncomingMessage, ServerResponse } from 'http';
|
||||||
|
export const getUserFromRequest = (req: IncomingMessage) => {
|
||||||
|
const url = new URL(req.url, `http://${req.headers.host}`);
|
||||||
|
const pathname = url.pathname;
|
||||||
|
const keys = pathname.split('/');
|
||||||
|
const [_, user, app] = keys;
|
||||||
|
return {
|
||||||
|
user,
|
||||||
|
app,
|
||||||
|
};
|
||||||
|
};
|
Loading…
x
Reference in New Issue
Block a user