page-proxy/src/module/index.ts

247 lines
8.1 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 { getDNS, isLocalhost } from '@/utils/dns.ts';
import http from 'http';
import { UserApp } from './get-user-app.ts';
import { config, fileStore } from '../module/config.ts';
import path from 'path';
import fs from 'fs';
import { getContentType } from './get-content-type.ts';
import { createRefreshHtml } from './html/create-refresh-html.ts';
const api = config?.api || { host: 'kevisual.xiongxiao.me', path: '/api/router' };
const domain = config?.proxy?.domain || 'kevisual.xiongxiao.me';
const allowedOrigins = config?.proxy?.allowOrigin || [];
const home = config?.proxy?.home || '/ai/chat';
const noProxyUrl = ['/', '/favicon.ico'];
export const handleRequest = async (req: http.IncomingMessage, res: http.ServerResponse) => {
if (req.url === '/favicon.ico') {
res.writeHead(200, { 'Content-Type': 'image/x-icon' });
res.end('proxy no favicon.ico\n');
return;
}
if (req.url.startsWith('/api/proxy')) {
return;
}
if (req.url.startsWith('/api')) {
// 代理到 http://codeflow.xiongxiao.me/api
const _u = new URL(req.url, `http://${api.host}`);
// 设置代理请求的目标 URL 和请求头
let header: any = {};
if (req.headers?.['Authorization']) {
header.authorization = req.headers['Authorization'];
} else if (req.headers?.['authorization']) {
header.authorization = req.headers['authorization'];
}
if (req.headers?.['Content-Type']) {
header['Content-Type'] = req.headers?.['Content-Type'];
}
const options = {
host: _u.hostname,
path: req.url,
method: req.method,
headers: {
...header,
},
};
if (_u.port) {
// @ts-ignore
options.port = _u.port;
}
// 创建代理请求
const proxyReq = http.request(options, (proxyRes) => {
// 将代理服务器的响应头和状态码返回给客户端
res.writeHead(proxyRes.statusCode, proxyRes.headers);
// 将代理响应流写入客户端响应
proxyRes.pipe(res, { end: true });
});
// 处理代理请求的错误事件
proxyReq.on('error', (err) => {
console.error(`Proxy request error: ${err.message}`);
res.writeHead(500, { 'Content-Type': 'text/plain' });
res.write(`Proxy request error: ${err.message}`);
});
// 处理 POST 请求的请求体(传递数据到目标服务器)
req.pipe(proxyReq, { end: true });
return;
}
if (req.url.startsWith('/api')) {
res.end('not catch api');
return;
}
const dns = getDNS(req);
// 配置可以跨域
// 配置可以访问的域名 localhost, xiongxiao.me
const _orings = allowedOrigins || [];
const host = dns.hostName;
if (
_orings.some((item) => {
return host.includes(item);
})
) {
res.setHeader('Access-Control-Allow-Origin', '*');
}
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
let user, app;
let domainApp = false;
if (isLocalhost(dns.hostName)) {
// 本地开发环境 测试
// user = 'root';
// app = 'codeflow';
// domainApp = true;
} else {
// 验证域名
if (dns.hostName !== domain) {
// redis获取域名对应的用户和应用
domainApp = true;
const data = await UserApp.getDomainApp(dns.hostName);
if (!data) {
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('Invalid domain\n');
return;
}
if (!data.user || !data.app) {
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('Invalid domain config\n');
return;
}
user = data.user;
app = data.app;
}
}
// const url = req.url;
const pathname = new URL(req.url, `http://${dns.hostName}`).pathname;
const url = pathname;
if (!domainApp && noProxyUrl.includes(url)) {
if (url === '/') {
// 获取一下登陆用户如果没有登陆用户重定向到ai-chat页面
// 重定向到
res.writeHead(302, { Location: home });
return res.end();
}
// 不是域名代理且是在不代理的url当中
res.write('No proxy for this URL\n');
return res.end();
}
if (!domainApp) {
// 原始url地址
const urls = url.split('/');
if (urls.length < 3) {
console.log('urls errpr', urls);
res.writeHead(404, { 'Content-Type': 'text/html' });
res.write('Invalid Proxy URL\n');
return res.end();
}
const [_, _user, _app] = urls;
if (_app && urls.length === 3) {
// 重定向到
res.writeHead(302, { Location: `${url}/` });
return res.end();
}
if (!_user || !_app) {
res.write('Invalid URL\n');
return res.end();
}
user = _user;
app = _app;
}
const userApp = new UserApp({ user, app });
let isExist = await userApp.getExist();
if (!isExist) {
try {
const { code, loading } = await userApp.setCacheData();
if (loading || code === 20000) {
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(createRefreshHtml(user, app));
return;
}
res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
res.write('Not Found App\n');
res.end();
// 不存在就一定先返回loading状态。
return;
} catch (error) {
console.error('setCacheData error', error);
res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
res.write('Server Error\n');
res.end();
userApp.setLoaded('error', 'setCacheData error');
return;
}
}
const indexFile = isExist; // 已经必定存在了
try {
let appFileUrl: string;
if (domainApp) {
appFileUrl = (url + '').replace(`/`, '');
} else {
appFileUrl = (url + '').replace(`/${user}/${app}/`, '');
}
const appFile = await userApp.getFile(appFileUrl);
if (!appFile) {
const [indexFilePath, etag] = indexFile.split('||');
const contentType = getContentType(indexFilePath);
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('File expired, Not Found\n');
res.end();
await userApp.clearCacheData();
return;
}
// 如果 content是 'application/octet-stream' 会下载文件, 添加文件后缀
if (contentType === 'application/octet-stream') {
// 提取文件名,只保留文件名而不是整个路径
const fileName = path.basename(indexFilePath);
res.setHeader('Content-Disposition', `attachment; filename=${fileName}`);
}
// 不存在的文件,返回indexFile的文件
res.writeHead(200, { 'Content-Type': contentType, 'Cache-Control': isHTML ? 'no-cache' : 'public, max-age=3600' });
const readStream = fs.createReadStream(filePath);
readStream.pipe(res);
return;
} else {
const [appFilePath, eTag] = appFile.split('||');
// 检查 If-None-Match 头判断缓存是否有效
if (req.headers['if-none-match'] === eTag) {
res.statusCode = 304; // 内容未修改
res.end();
return;
}
const filePath = path.join(fileStore, appFilePath);
let contentType = getContentType(filePath);
const isHTML = contentType.includes('html');
// 如果 content是 'application/octet-stream' 会下载文件, 添加文件后缀
if (contentType === 'application/octet-stream') {
// 提取文件名,只保留文件名而不是整个路径
const fileName = path.basename(appFilePath);
res.setHeader('Content-Disposition', `attachment; filename=${fileName}`);
}
res.writeHead(200, {
'Content-Type': contentType,
'Cache-Control': isHTML ? 'no-cache' : 'public, max-age=3600', // 设置缓存时间为 1 小时
ETag: eTag,
});
if (!userApp.fileCheck(filePath)) {
console.error('File expired', filePath);
res.end('File expired\n');
await userApp.clearCacheData();
return;
}
const readStream = fs.createReadStream(filePath);
readStream.pipe(res);
return;
}
} catch (error) {
console.error('getFile error', error);
}
};