From f2cc76a8eaf17e992e81108214aa0039b99863cc Mon Sep 17 00:00:00 2001 From: xion Date: Thu, 8 May 2025 00:12:14 +0800 Subject: [PATCH] add ai proxy --- src/module/get-user-app.ts | 4 +- src/module/index.ts | 94 ++++++++++++++++++---------------- src/module/proxy/ai-proxy.ts | 64 +++++++++++++++++++++++ src/module/proxy/http-proxy.ts | 17 +++--- src/utils/get-user.ts | 11 ++++ 5 files changed, 137 insertions(+), 53 deletions(-) create mode 100644 src/module/proxy/ai-proxy.ts create mode 100644 src/utils/get-user.ts diff --git a/src/module/get-user-app.ts b/src/module/get-user-app.ts index 7bc4647..349dc00 100644 --- a/src/module/get-user-app.ts +++ b/src/module/get-user-app.ts @@ -7,8 +7,8 @@ import { nanoid } from 'nanoid'; import { pipeline } from 'stream'; import { promisify } from 'util'; import { fetchApp, fetchDomain, fetchTest } from './query/get-router.ts'; -import { getAppLoadStatus, setAppLoadStatus, AppLoadStatus } from './redis/get-app-status.ts'; -import { bucketName, minioClient, minioResources } from './minio.ts'; +import { getAppLoadStatus, setAppLoadStatus } from './redis/get-app-status.ts'; +import { minioResources } from './minio.ts'; import { downloadFileFromMinio } from './proxy/http-proxy.ts'; const pipelineAsync = promisify(pipeline); diff --git a/src/module/index.ts b/src/module/index.ts index 4ccd450..a9b6a53 100644 --- a/src/module/index.ts +++ b/src/module/index.ts @@ -11,11 +11,48 @@ import { getTextFromStreamAndAddStat, httpProxy } from './proxy/http-proxy.ts'; import { UserPermission } from '@kevisual/permission'; import { getLoginUser } from '@/middleware/auth.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 domain = config?.proxy?.domain || 'kevisual.xiongxiao.me'; const allowedOrigins = config?.proxy?.allowedOrigin || []; 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) => { const querySearch = new URL(req.url, `http://${req.headers.host}`).searchParams; const password = querySearch.get('p'); @@ -176,10 +213,7 @@ export const handleRequest = async (req: http.IncomingMessage, res: http.ServerR user = _user; app = _app; } - - const userApp = new UserApp({ user, app }); - let isExist = await userApp.getExist(); - const createRefreshPage = () => { + const createRefreshPage = (user, app) => { res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); 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.end(); }; + if (app === 'ai') { + return aiProxy(req, res, { + createNotFoundPage, + }); + } + + const userApp = new UserApp({ user, app }); + let isExist = await userApp.getExist(); + if (!isExist) { try { const { code, loading, message } = await userApp.setCacheData(); if (loading || code === 20000) { - return createRefreshPage(); + return createRefreshPage(user, app); } else if (code === 500) { return createNotFoundPage(message || 'Not Found App\n'); } else if (code !== 200) { @@ -214,42 +257,7 @@ export const handleRequest = async (req: http.IncomingMessage, res: http.ServerR if (!isExist) { 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)) { const { permission } = isExist; const permissionInstance = new UserPermission({ permission, owner: user }); @@ -283,7 +291,6 @@ export const handleRequest = async (req: http.IncomingMessage, res: http.ServerR userApp, createNotFoundPage, }); - // userApp.clearCacheData() return; } 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 filePath = path.join(fileStore, indexFilePath); 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.write(createRefreshHtml(user, app)); res.end(); diff --git a/src/module/proxy/ai-proxy.ts b/src/module/proxy/ai-proxy.ts new file mode 100644 index 0000000..c0e3566 --- /dev/null +++ b/src/module/proxy/ai-proxy.ts @@ -0,0 +1,64 @@ +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'); + const version = params.get('version') || '1.0.0'; + const { user, app } = getUserFromRequest(req); + const objectName = pathname.replace(`/${user}/${app}/`, `${user}/${app}/${version}/`); + 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: user }); + 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; + } +}; diff --git a/src/module/proxy/http-proxy.ts b/src/module/proxy/http-proxy.ts index ca826ba..3476bba 100644 --- a/src/module/proxy/http-proxy.ts +++ b/src/module/proxy/http-proxy.ts @@ -32,18 +32,18 @@ export async function minioProxy( res: ServerResponse, opts: { proxyUrl: string; - userApp: UserApp; createNotFoundPage: (msg?: string) => any; isDownload?: boolean; }, ) { const fileUrl = opts.proxyUrl; - const { userApp, createNotFoundPage, isDownload = false } = opts; + const { createNotFoundPage, isDownload = false } = opts; const objectName = fileUrl.replace(minioResources + '/', ''); try { const stat = await minioClient.statObject(bucketName, objectName); 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 contentLength = stat.size; @@ -72,10 +72,11 @@ export async function minioProxy( }); objectStream.pipe(res, { end: true }); } + return true; } catch (error) { console.error(`Proxy request error: ${error.message}`); - userApp.clearCacheData(); - return createNotFoundPage('Invalid proxy url'); + createNotFoundPage('Invalid proxy url'); + return false; } } @@ -114,7 +115,11 @@ export const httpProxy = async ( const params = _u.searchParams; const isDownload = params.get('download') === 'true'; 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; // 代理 diff --git a/src/utils/get-user.ts b/src/utils/get-user.ts new file mode 100644 index 0000000..44ba731 --- /dev/null +++ b/src/utils/get-user.ts @@ -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, + }; +};