import { createApiProxy, ProxyInfo, proxy } from '@/module/assistant/index.ts'; import http from 'node:http'; import { LocalProxy } from './local-proxy.ts'; import { assistantConfig, simpleRouter, app } from '@/app.ts'; import { log, logger } from '@/module/logger.ts'; import { getToken } from '@/module/http-token.ts'; import { getTokenUserCache } from '@/routes/index.ts'; import type { WebSocketListenerFun } from "@kevisual/router"; import WebSocket from 'ws'; import { renderNoAuthAndLogin } from '@/module/assistant/html/login.ts'; import { LiveCode } from '@/module/livecode/index.ts'; const localProxy = new LocalProxy({}); localProxy.initFromAssistantConfig(assistantConfig); const isOpenPath = (pathname: string): boolean => { const openPaths = ['/root/home', '/root/cli', '/root/login']; for (const openPath of openPaths) { if (pathname.startsWith(openPath)) { return true; } } return false; } /** * 过滤访问的资源,允许谁访问 * @param req * @param res * @returns */ const authFilter = async (req: http.IncomingMessage, res: http.ServerResponse) => { const _assistantConfig = assistantConfig.getCacheAssistantConfig(); const auth = _assistantConfig?.auth || {}; const share = auth.share || 'protected'; const noAdmin = !auth.username; if (noAdmin) return { code: 200, message: '没有管理员, 直接放过, 让管理登录和自己设置' }; const admin = auth.username; const admins = auth.admin || []; if (admin) { admins.push(admin); } const url = new URL(req.url, 'http://localhost'); const pathname = decodeURIComponent(url.pathname); // 放开 / if (pathname === '/' || pathname === '/favicon.ico') { return { code: 200, message: '允许访问根路径' }; } // 放开首页 if (pathname.startsWith('/root/home') || pathname === '/root/cli-center/') { return { code: 200, message: '允许访问首页' }; } // 放开api, 以 /api, /v1, /client, /serve 开头的请求 const openApiPaths = ['/api', '/v1', '/client', '/serve', '/proxy', '/root']; for (const openPath of openApiPaths) { if (pathname.startsWith(openPath)) { return { code: 200, message: '允许公共访问模块' }; } } if (share === 'public') { return { code: 200, message: '公开模式允许访问' }; } const { token } = await getToken(req) if (!token) { // no token 转到登录页面 res.writeHead(302, { Location: `/root/home/` }); res.end(); return { code: 500, message: '未登录' }; } const tokenUser = await getTokenUserCache(token); if (share === 'protected' && tokenUser?.code === 200) { return { code: 200, message: '受保护模式已登录允许访问' }; } if (share === 'private') { if (tokenUser?.code === 200) { const username = tokenUser?.data?.username; if (admins.includes(username)) { return { code: 200, message: '私有模式管理员允许访问' }; } } } return { code: 500, message: '没有权限访问' }; } export const proxyRoute = async (req: http.IncomingMessage, res: http.ServerResponse) => { const _assistantConfig = assistantConfig.getCacheAssistantConfig(); const home = _assistantConfig?.home || '/root/home'; const auth = _assistantConfig?.auth || {}; let noAdmin = !auth.username; const toSetting = () => { res.writeHead(302, { Location: `/root/cli-center/` }); res.end(); return true; } const url = new URL(req.url, 'http://localhost'); const pathname = decodeURIComponent(url.pathname); if (pathname === '/') { if (noAdmin) { return toSetting(); } res.writeHead(302, { Location: `${home}/` }); return res.end(); } if (pathname.startsWith('/favicon.ico')) { res.statusCode = 404; res.end('Not Found Favicon'); return; } if (pathname.startsWith('/client/upload')) { simpleRouter.parse(req, res); return; } if (pathname.startsWith('/client')) { logger.debug('handle by router', { url: req.url }); return; } if (pathname.startsWith('/router') || pathname.startsWith('/opencode')) { logger.debug('handle by router (opencode/router)', { url: req.url }); return; } // client, api, v1, serve 开头的拦截 const apiProxy = _assistantConfig?.api?.proxy || []; const defaultApiProxy = createApiProxy(_assistantConfig?.app?.url || 'https://kevisual.cn'); const allProxy = [...apiProxy, ...defaultApiProxy]; const apiBackendProxy = allProxy.find((item) => pathname.startsWith(item.path)); const proxyFn = async (req: http.IncomingMessage, res: http.ServerResponse, proxyApi: ProxyInfo) => { log.debug('proxyApi', { proxyApi, url: req.url }); // 设置 CORS 头 // res.setHeader('Access-Control-Allow-Origin', '*'); // res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); // res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With'); if (proxyApi.s3?.id) { const storage = _assistantConfig?.storage || [] const storageConfig = storage.find((item) => item.id === proxyApi.s3?.id); proxyApi.s3 = { ...storageConfig, ...proxyApi.s3, } } if (proxyApi.file?.id) { const storage = _assistantConfig?.storage || [] const storageConfig = storage.find((item) => item.id === proxyApi.file?.id); proxyApi.file = { ...storageConfig, ...proxyApi.file, } } return proxy(req, res, { path: proxyApi.path, target: proxyApi.target, ...proxyApi, }); } if (apiBackendProxy) { return proxyFn(req, res, apiBackendProxy); } logger.debug('proxyRoute handle by router', { url: req.url }, noAdmin); const urls = pathname.split('/'); const [_, _user, _app] = urls; if (!_app) { res.statusCode = 404; res.end('Not Found Proxy'); return; } const isOpen = isOpenPath(pathname) logger.debug('proxyRoute', { _user, _app, pathname, noAdmin, isOpen }); if (noAdmin && !isOpen) { return toSetting(); } if (_app && urls.length === 3) { // 重定向到 res.writeHead(302, { Location: `${req.url}/` }); return res.end(); } const proxyApiList = _assistantConfig?.proxy || []; const proxyApi = proxyApiList.find((item) => pathname.startsWith(item.path)); if (proxyApi) { logger.debug('proxyPage', { proxyApi, pathname }); return proxyFn(req, res, proxyApi); } const filter = await authFilter(req, res); if (filter.code !== 200) { logger.debug('auth filter deny', filter); res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); // TODO: 这里可以做成可配置的登录页面 return res.end(renderNoAuthAndLogin('Not Authorized Proxy')); } const localProxyProxyList = localProxy.getLocalProxyList(); const localProxyProxy = localProxyProxyList.find((item) => pathname.startsWith(item.path)); if (localProxyProxy) { logger.debug('localProxyProxy', { localProxyProxy, url: req.url }); return proxyFn(req, res, { path: localProxyProxy.path, "type": 'file', file: { rootPath: localProxy.pagesDir, indexPath: localProxyProxy.file.indexPath, } }); } const creatCenterProxy = createApiProxy(_assistantConfig?.app?.url || 'https://kevisual.cn', ['/root', '/' + _user]); const centerProxy = creatCenterProxy.find((item) => pathname.startsWith(item.path)); if (centerProxy) { return proxyFn(req, res, { path: centerProxy.path, target: centerProxy.target, type: 'http', }); } logger.debug('handle by router 404', req.url); res.statusCode = 404; res.end('Not Found Proxy'); }; export const proxyWs = () => { const apiProxy = assistantConfig.getCacheAssistantConfig()?.api?.proxy || []; const registry = assistantConfig.getRegistry() const proxy = assistantConfig.getCacheAssistantConfig()?.proxy || []; const proxyApi = [...apiProxy, ...proxy].filter((item) => item.ws); const demoProxy = [ { path: '/api/ws/demo', target: 'https://kevisual.xiongxiao.me', pathname: '/api/router', ws: true, } ] const pathRouter = proxyApi.find((item) => item.path === '/api/router'); if (!pathRouter) { proxyApi.push({ path: '/api/router', target: registry || 'https://kevisual.cn', pathname: '/api/router', ws: true, }); } return proxyApi.map(createProxyInfo); }; const liveCode = new LiveCode(app) export const proxyLivecodeWs = () => { const livecode = assistantConfig.getCacheAssistantConfig()?.router?.livecode ?? true; if (!livecode) { return []; } const fun: WebSocketListenerFun = async (req, res) => { const { ws, emitter, id, data } = req; // if (!id) { // ws.send(JSON.stringify({ type: 'error', message: 'not found id' })); // ws.close(); // return; // } liveCode.conn(req) } return [{ path: '/livecode/ws', io: true, func: fun }] } export const createProxyInfo = (proxyApiItem: ProxyInfo) => { const func: WebSocketListenerFun = async (req, res) => { const { ws, emitter, id, data } = req; if (!id) { ws.send(JSON.stringify({ type: 'error', message: 'not found id' })); ws.close(); return; } // @ts-ignore let _proxySocket = ws.data.proxySocket; if (!_proxySocket) { const _u = new URL(proxyApiItem.path, `${proxyApiItem.target}`); if (proxyApiItem.pathname) { _u.pathname = proxyApiItem.pathname; } const isHttps = _u.protocol === 'https:'; const wsProtocol = isHttps ? 'wss' : 'ws'; const wsUrl = `${wsProtocol}://${_u.host}${_u.pathname}`; console.log('WebSocket proxy URL', { wsUrl }); const proxySocket = new WebSocket(wsUrl); proxySocket.on('open', () => { proxySocket.on('message', (message) => { ws.send(message); }); }); proxySocket.on('error', (err) => { console.error(`WebSocket proxy error: ${err.message}`); }); proxySocket.on('close', () => { console.log('WebSocket proxy closed'); }); emitter.once('close--' + id, () => { console.log('WebSocket client closed'); proxySocket?.close?.(); }); // @ts-ignore ws.data.proxySocket = proxySocket; return; } console.log('ws.data', data); _proxySocket.send(JSON.stringify(data)) } return { path: proxyApiItem.path, io: true, func: func } }