import { fileProxy, httpProxy, createApiProxy, ProxyInfo } from '@/module/assistant/index.ts'; import http from 'node:http'; import { LocalProxy } from './local-proxy.ts'; import { assistantConfig, 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'; const localProxy = new LocalProxy({}); localProxy.initFromAssistantConfig(assistantConfig); /** * 过滤访问的资源,允许谁访问 * @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 false; 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 false; } // 放开首页 if (pathname.startsWith('/root/home')) { return false; } // 放开api, 以 /api, /v1, /client, /serve 开头的请求 const openApiPaths = ['/api', '/v1', '/client', '/serve']; for (const openPath of openApiPaths) { if (pathname.startsWith(openPath)) { return false; } } if (share === 'public') { return false; } const { token } = await getToken(req, res) if (!token) { return false; } const tokenUser = await getTokenUserCache(token); if (share === 'protected' && tokenUser?.code === 200) { return false; } if (share === 'private') { if (tokenUser?.code === 200) { const username = tokenUser?.data?.username; if (admins.includes(username)) { return false; } } } return true; } 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/setting/` }); 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')) { logger.debug('handle by router', { url: req.url }); return; } // client, api, v1, serve 开头的拦截 const apiProxy = _assistantConfig?.api?.proxy || []; const defaultApiProxy = createApiProxy(_assistantConfig?.app?.url || 'https://kevisual.cn'); const apiBackendProxy = [...apiProxy, ...defaultApiProxy].find((item) => pathname.startsWith(item.path)); if (apiBackendProxy) { log.debug('apiBackendProxy', { apiBackendProxy, url: req.url }); return httpProxy(req, res, { path: apiBackendProxy.path, target: apiBackendProxy.target, }); } const urls = pathname.split('/'); const [_, _user, _app] = urls; if (!_app) { res.statusCode = 404; res.end('Not Found Proxy'); return; } if (noAdmin) { 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 && proxyApi.type === 'file') { log.debug('proxyApi', { proxyApi, pathname }); const _indexPath = proxyApi.indexPath || `${_user}/${_app}/index.html`; const _rootPath = proxyApi.rootPath; if (!_rootPath) { log.error('Not Found rootPath', { proxyApi, pathname }); return res.end(`Not Found [${proxyApi.path}] rootPath`); } return fileProxy(req, res, { path: proxyApi.path, // 代理路径, 比如/root/home rootPath: proxyApi.rootPath, ...proxyApi, indexPath: _indexPath, // 首页路径 }); } else if (proxyApi && proxyApi.type === 'http') { log.debug('proxyApi http', { proxyApi, pathname }); return httpProxy(req, res, { path: proxyApi.path, target: proxyApi.target, type: 'http', }); } const filter = await authFilter(req, res); if (filter) { return res.end('Not Authorized Proxy'); } const localProxyProxyList = localProxy.getLocalProxyList(); const localProxyProxy = localProxyProxyList.find((item) => pathname.startsWith(item.path)); if (localProxyProxy) { log.log('localProxyProxy', { localProxyProxy, url: req.url }); return fileProxy(req, res, { path: localProxyProxy.path, rootPath: localProxy.pagesDir, indexPath: localProxyProxy.indexPath, }); } const creatCenterProxy = createApiProxy(_assistantConfig?.app?.url || 'https://kevisual.cn', ['/root', '/' + _user]); const centerProxy = creatCenterProxy.find((item) => pathname.startsWith(item.path)); if (centerProxy) { return httpProxy(req, res, { path: centerProxy.path, target: centerProxy.target, type: 'http', }); } log.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); }; 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 } }