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

249 lines
7.8 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 { 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
}
}