Files
cli/assistant/src/services/proxy/proxy-page-index.ts

323 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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';
import { isCnb } from '@/routes/cnb-board/modules/is-cnb.ts';
const localProxy = new LocalProxy({});
localProxy.initFromAssistantConfig(assistantConfig);
const isOpenPath = (pathname: string): boolean => {
const openPaths = ['/root/home', '/root/cli', '/root/login', '/root/cli-center'];
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();
let home = _assistantConfig?.home
const auth = _assistantConfig?.auth || {};
// 没有管理员,需要去登陆
let noAdmin = !auth.username;
if (!home) {
if (isCnb()) {
home = '/root/cli-center/cnb-board'
} else {
home = '/root/cli-center/';
}
} else {
if (!home.startsWith('/')) {
home = '/' + home;
}
}
const toLogin = (redirect?: string) => {
res.writeHead(302, { Location: `/root/login/` + (redirect ? `?redirect=${encodeURIComponent(redirect)}` : '') });
res.end();
return true;
}
const url = new URL(req.url, 'http://localhost');
const pathname = decodeURIComponent(url.pathname);
if (pathname === '/') {
if (noAdmin) {
return toLogin(home + '/');
}
res.writeHead(302, { Location: home });
res.end();
return
}
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 toLogin();
}
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
}
}