update
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
import { useFileStore } from '@kevisual/use-config/file-store';
|
||||
import http from 'http';
|
||||
import fs, { rm } from 'fs';
|
||||
import path from 'path';
|
||||
import http from 'node:http';
|
||||
import fs from 'fs';
|
||||
import { IncomingForm } from 'formidable';
|
||||
import { app, minioClient } from '@/app.ts';
|
||||
|
||||
@@ -9,16 +8,10 @@ import { bucketName } from '@/modules/minio.ts';
|
||||
import { getContentType } from '@/utils/get-content-type.ts';
|
||||
import { User } from '@/models/user.ts';
|
||||
import { getContainerById } from '@/routes/container/module/get-container-file.ts';
|
||||
import { router, error, checkAuth, clients, writeEvents } from './router.ts';
|
||||
import { router, error, checkAuth, writeEvents } from './router.ts';
|
||||
import './index.ts';
|
||||
|
||||
import { handleRequest as PageProxy } from './page-proxy.ts';
|
||||
const cacheFilePath = useFileStore('cache-file', { needExists: true });
|
||||
// curl -X POST http://localhost:4000/api/upload -F "file=@readme.md"
|
||||
// curl -X POST http://localhost:4000/api/upload \
|
||||
// -F "file=@readme.md" \
|
||||
// -F "file=@types/index.d.ts" \
|
||||
// -F "description=This is a test upload" \
|
||||
// -F "username=testuser"
|
||||
|
||||
router.get('/api/app/upload', async (req, res) => {
|
||||
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
||||
@@ -177,21 +170,28 @@ router.get('/api/container/file/:id', async (req, res) => {
|
||||
res.end(JSON.stringify(container));
|
||||
});
|
||||
|
||||
// router.get('/api/code/version', async (req, res) => {
|
||||
// const version = VERSION;
|
||||
// res.writeHead(200, {
|
||||
// 'Content-Type': 'application/json',
|
||||
// });
|
||||
// res.end(JSON.stringify({ code: 200, data: { version } }));
|
||||
// });
|
||||
const simpleAppsPrefixs = [
|
||||
"/api/app/",
|
||||
"/api/micro-app/",
|
||||
"/api/events",
|
||||
"/api/s1/",
|
||||
"/api/container/",
|
||||
"/api/resource/",
|
||||
];
|
||||
|
||||
export const uploadMiddleware = async (req: http.IncomingMessage, res: http.ServerResponse) => {
|
||||
export const handleRequest = async (req: http.IncomingMessage, res: http.ServerResponse) => {
|
||||
if (req.url?.startsWith('/api/router')) {
|
||||
// router自己管理
|
||||
return;
|
||||
}
|
||||
// 设置跨域
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
||||
return router.parse(req, res);
|
||||
if (req.url && simpleAppsPrefixs.some(prefix => req.url!.startsWith(prefix))) {
|
||||
// 简单应用路由处理
|
||||
// 设置跨域
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
||||
return router.parse(req, res);
|
||||
}
|
||||
// 其他请求交给页面代理处理
|
||||
return PageProxy(req, res);
|
||||
};
|
||||
386
src/routes-simple/page-proxy.ts
Normal file
386
src/routes-simple/page-proxy.ts
Normal file
@@ -0,0 +1,386 @@
|
||||
import { getDNS, isIpv4OrIpv6, isLocalhost } from '../modules/fm-manager/index.ts';
|
||||
import http from 'node:http';
|
||||
import https from 'node:https';
|
||||
import { UserApp } from '../modules/user-app/index.ts';
|
||||
import { config, fileStore } from '../modules/config.ts';
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs';
|
||||
import { getContentType } from '../modules/fm-manager/index.ts';
|
||||
import { createRefreshHtml } from '../modules/html/create-refresh-html.ts';
|
||||
import { fileProxy, getTextFromStreamAndAddStat, httpProxy, aiProxy } from '../modules/fm-manager/index.ts';
|
||||
import { UserPermission } from '@kevisual/permission';
|
||||
import { getLoginUser } from '../modules/auth.ts';
|
||||
import { rediretHome } from '../modules/user-app/index.ts';
|
||||
import { logger } from '../modules/logger.ts';
|
||||
import { UserV1Proxy } from '../modules/ws-proxy/proxy.ts';
|
||||
const domain = config?.proxy?.domain;
|
||||
const allowedOrigins = config?.proxy?.allowedOrigin || [];
|
||||
|
||||
const noProxyUrl = ['/', '/favicon.ico'];
|
||||
const notAuthPathList = [
|
||||
{
|
||||
user: 'root',
|
||||
paths: ['center'],
|
||||
},
|
||||
{
|
||||
user: 'admin',
|
||||
paths: ['center'],
|
||||
},
|
||||
{
|
||||
user: 'user',
|
||||
paths: ['login'],
|
||||
},
|
||||
{
|
||||
user: 'public',
|
||||
paths: ['center'],
|
||||
all: true,
|
||||
},
|
||||
{
|
||||
user: 'test',
|
||||
paths: ['center'],
|
||||
all: true,
|
||||
},
|
||||
];
|
||||
const checkNotAuthPath = (user, app) => {
|
||||
const notAuthPath = notAuthPathList.find((item) => {
|
||||
if (item.user === user) {
|
||||
if (item.all) {
|
||||
return true;
|
||||
}
|
||||
return item.paths?.includes?.(app);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
return notAuthPath;
|
||||
};
|
||||
export const handleRequest = async (req: http.IncomingMessage, res: http.ServerResponse) => {
|
||||
const querySearch = new URL(req.url, `http://${req.headers.host}`).searchParams;
|
||||
const password = querySearch.get('p');
|
||||
if (req.url === '/favicon.ico') {
|
||||
res.writeHead(200, { 'Content-Type': 'image/x-icon' });
|
||||
res.end('proxy no favicon.ico\n');
|
||||
return;
|
||||
}
|
||||
const proxyApiList = config?.apiList || [];
|
||||
const proxyApi = proxyApiList.find((item) => req.url.startsWith(item.path));
|
||||
if (proxyApi && proxyApi?.type === 'static') {
|
||||
return fileProxy(req, res, proxyApi);
|
||||
}
|
||||
if (proxyApi) {
|
||||
const _u = new URL(req.url, `${proxyApi.target}`);
|
||||
// 设置代理请求的目标 URL 和请求头
|
||||
let header: any = {};
|
||||
if (req.headers?.['Authorization']) {
|
||||
header.authorization = req.headers['Authorization'];
|
||||
} else if (req.headers?.['authorization']) {
|
||||
header.authorization = req.headers['authorization'];
|
||||
}
|
||||
// 提取req的headers中的非HOST的header
|
||||
const headers = Object.keys(req.headers).filter((item) => item && item.toLowerCase() !== 'host');
|
||||
const host = req.headers['host'];
|
||||
logger.info('proxy host', host);
|
||||
logger.info('headers', headers);
|
||||
|
||||
headers.forEach((item) => {
|
||||
header[item] = req.headers[item];
|
||||
});
|
||||
const options = {
|
||||
host: _u.hostname,
|
||||
path: req.url,
|
||||
method: req.method,
|
||||
headers: {
|
||||
...header,
|
||||
},
|
||||
};
|
||||
if (_u.port) {
|
||||
// @ts-ignore
|
||||
options.port = _u.port;
|
||||
}
|
||||
logger.info('proxy options', options);
|
||||
const isHttps = _u.protocol === 'https:';
|
||||
const protocol = isHttps ? https : http;
|
||||
if (isHttps) {
|
||||
// 不验证https
|
||||
// @ts-ignore
|
||||
options.rejectUnauthorized = false;
|
||||
}
|
||||
// 创建代理请求
|
||||
const proxyReq = protocol.request(options, (proxyRes) => {
|
||||
// 将代理服务器的响应头和状态码返回给客户端
|
||||
res.writeHead(proxyRes.statusCode, proxyRes.headers);
|
||||
// 将代理响应流写入客户端响应
|
||||
proxyRes.pipe(res, { end: true });
|
||||
});
|
||||
// 处理代理请求的错误事件
|
||||
proxyReq.on('error', (err) => {
|
||||
logger.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') || req.url.startsWith('/v1')) {
|
||||
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, DELETE, PUT');
|
||||
|
||||
let user, app;
|
||||
let domainApp = false;
|
||||
if (isLocalhost(dns.hostName)) {
|
||||
// 本地开发环境 测试
|
||||
// user = 'root';
|
||||
// app = 'codeflow';
|
||||
// domainApp = true;
|
||||
} else {
|
||||
if (isIpv4OrIpv6(dns.hostName)) {
|
||||
// 打印出 req.url 和错误信息
|
||||
console.error('Invalid domain: ', req.url, dns.hostName);
|
||||
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
||||
res.end('Invalid domain\n');
|
||||
return res.end();
|
||||
}
|
||||
// 验证域名
|
||||
if (domain && 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;
|
||||
|
||||
/**
|
||||
* url是pathname的路径
|
||||
*/
|
||||
const url = pathname;
|
||||
if (!domainApp && noProxyUrl.includes(url)) {
|
||||
if (url === '/') {
|
||||
// TODO: 获取一下登陆用户,如果没有登陆用户,重定向到ai-chat页面
|
||||
// 重定向到
|
||||
// res.writeHead(302, { Location: home });
|
||||
// return res.end();
|
||||
rediretHome(req, res);
|
||||
return;
|
||||
}
|
||||
// 不是域名代理,且是在不代理的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 createRefreshPage = (user, app) => {
|
||||
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
||||
res.end(createRefreshHtml(user, app));
|
||||
};
|
||||
const createErrorPage = () => {
|
||||
res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
|
||||
res.write('Server Error\n');
|
||||
res.end();
|
||||
};
|
||||
const createNotFoundPage = async (msg?: string, code = 404) => {
|
||||
res.writeHead(code, { 'Content-Type': 'text/html; charset=utf-8' });
|
||||
res.write(msg || 'Not Found App\n');
|
||||
res.end();
|
||||
};
|
||||
if (app === 'ai' || app === 'resources' || app === 'r') {
|
||||
return aiProxy(req, res, {
|
||||
createNotFoundPage,
|
||||
});
|
||||
}
|
||||
if (user !== 'api' && app === 'v1') {
|
||||
return UserV1Proxy(req, res, {
|
||||
createNotFoundPage,
|
||||
});
|
||||
}
|
||||
|
||||
const userApp = new UserApp({ user, app });
|
||||
let isExist = await userApp.getExist();
|
||||
logger.debug('userApp', userApp, isExist);
|
||||
if (!isExist) {
|
||||
try {
|
||||
const { code, loading, message } = await userApp.setCacheData();
|
||||
if (loading || code === 20000) {
|
||||
return createRefreshPage(user, app);
|
||||
} else if (code === 500) {
|
||||
return createNotFoundPage(message || 'Not Found App\n');
|
||||
} else if (code !== 200) {
|
||||
return createErrorPage();
|
||||
}
|
||||
isExist = await userApp.getExist();
|
||||
} catch (error) {
|
||||
console.error('setCacheData error', error);
|
||||
createErrorPage();
|
||||
userApp.setLoaded('error', 'setCacheData error');
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!isExist) {
|
||||
return createNotFoundPage();
|
||||
}
|
||||
|
||||
if (!checkNotAuthPath(user, app)) {
|
||||
const { permission } = isExist;
|
||||
const permissionInstance = new UserPermission({ permission, owner: user });
|
||||
const loginUser = await getLoginUser(req);
|
||||
const checkPermission = permissionInstance.checkPermissionSuccess({
|
||||
username: loginUser?.tokenUser?.username || '',
|
||||
password: password,
|
||||
});
|
||||
if (!checkPermission.success) {
|
||||
return createNotFoundPage('no permission');
|
||||
}
|
||||
}
|
||||
const indexFile = isExist.indexFilePath; // 已经必定存在了
|
||||
try {
|
||||
let appFileUrl: string;
|
||||
if (domainApp) {
|
||||
appFileUrl = (url + '').replace(`/`, '');
|
||||
} else {
|
||||
appFileUrl = (url + '').replace(`/${user}/${app}/`, '');
|
||||
}
|
||||
appFileUrl = decodeURIComponent(appFileUrl); // Decode URL components
|
||||
let appFile = await userApp.getFile(appFileUrl);
|
||||
if (!appFile && url.endsWith('/')) {
|
||||
appFile = await userApp.getFile(appFileUrl + 'index.html');
|
||||
}
|
||||
if (isExist.proxy) {
|
||||
let proxyUrl = appFile || isExist.indexFilePath;
|
||||
if (!proxyUrl.startsWith('http')) {
|
||||
return createNotFoundPage('Invalid proxy url');
|
||||
}
|
||||
console.log('proxyUrl', appFileUrl, proxyUrl);
|
||||
httpProxy(req, res, {
|
||||
proxyUrl,
|
||||
userApp,
|
||||
createNotFoundPage,
|
||||
});
|
||||
return;
|
||||
}
|
||||
console.log('appFile', appFile, appFileUrl, isExist);
|
||||
// console.log('isExist', isExist);
|
||||
if (!appFile) {
|
||||
const [indexFilePath, etag] = indexFile.split('||');
|
||||
// console.log('indexFilePath', indexFile, path.join(fileStore, indexFilePath));
|
||||
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', tips: 'App Cache expired, Please refresh' });
|
||||
res.write(createRefreshHtml(user, app));
|
||||
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' });
|
||||
if (isHTML) {
|
||||
const newHtml = await getTextFromStreamAndAddStat(fs.createReadStream(filePath));
|
||||
res.end(newHtml.html);
|
||||
} else {
|
||||
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}`);
|
||||
}
|
||||
|
||||
if (!userApp.fileCheck(filePath)) {
|
||||
console.error('File expired', filePath);
|
||||
res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
|
||||
res.end('File expired\n');
|
||||
await userApp.clearCacheData();
|
||||
return;
|
||||
}
|
||||
let resContent = '';
|
||||
const headers = new Map<string, string>();
|
||||
headers.set('Content-Type', contentType);
|
||||
headers.set('Cache-Control', isHTML ? 'no-cache' : 'public, max-age=3600'); // 设置缓存时间为 1 小时
|
||||
headers.set('ETag', eTag);
|
||||
res?.setHeaders?.(headers);
|
||||
if (isHTML) {
|
||||
const newHtml = await getTextFromStreamAndAddStat(fs.createReadStream(filePath));
|
||||
resContent = newHtml.html;
|
||||
headers.set('Content-Length', newHtml.contentLength.toString());
|
||||
res.writeHead(200);
|
||||
res.end(resContent);
|
||||
} else {
|
||||
res.writeHead(200);
|
||||
const readStream = fs.createReadStream(filePath);
|
||||
readStream.pipe(res);
|
||||
}
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('getFile error', error);
|
||||
}
|
||||
};
|
||||
@@ -11,7 +11,7 @@ import { ConfigModel } from '@/routes/config/models/model.ts';
|
||||
import { validateDirectory } from './util.ts';
|
||||
import { pick } from 'lodash-es';
|
||||
import { getFileStat } from '@/routes/file/index.ts';
|
||||
import { logger } from '@/logger/index.ts';
|
||||
import { logger } from '@/modules/logger.ts';
|
||||
|
||||
const cacheFilePath = useFileStore('cache-file', { needExists: true });
|
||||
|
||||
|
||||
Reference in New Issue
Block a user