v0.2.7: 修复Redis连接 + 启动管理后台
- 修复Redis认证 (配置密码) - 启动Python管理后台 (端口9531, 15个功能开关) - 统一版本号 0.2.7 - 更新docker-compose.yml (镜像版本/Redis URL/Admin服务)
This commit is contained in:
137
source_clean/src/proxy/pansou-web.ts
Executable file
137
source_clean/src/proxy/pansou-web.ts
Executable file
@@ -0,0 +1,137 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
// Native fetch available in Node 20+
|
||||
import { getSystemConfig } from '../admin/system-config.service';
|
||||
|
||||
const PANSOU_UPSTREAM = 'http://pansou:80';
|
||||
|
||||
// Content types that need sub_filter path rewriting
|
||||
const TEXT_TYPES = ['text/html', 'application/javascript', 'text/javascript'];
|
||||
|
||||
// Hop-by-hop headers that should not be forwarded
|
||||
const HOP_HEADERS = new Set([
|
||||
'host', 'connection', 'content-length', 'transfer-encoding',
|
||||
'keep-alive', 'proxy-authenticate', 'proxy-authorization',
|
||||
'te', 'trailer', 'upgrade',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Apply sub_filter string replacements to HTML/JS content.
|
||||
* This matches what the nginx pansou.conf sub_filter does.
|
||||
*/
|
||||
function applySubFilter(text: string): string {
|
||||
return text
|
||||
// Replace HTML/JS path references: /api/ -> /pansou/api/
|
||||
.replace(/\/api\//g, '/pansou/api/')
|
||||
// baseURL rewrite (Vue SPA config)
|
||||
.replace(/baseURL:"\/api"/g, 'baseURL:"/pansou/api"')
|
||||
.replace(/baseURL:'\/api'/g, "baseURL:'/pansou/api'")
|
||||
// Static asset path rewrites
|
||||
.replace(/src="\/assets\//g, 'src="/pansou/assets/')
|
||||
.replace(/src='\/assets\//g, "src='/pansou/assets/")
|
||||
.replace(/href="\/assets\//g, 'href="/pansou/assets/')
|
||||
.replace(/href='\/assets\//g, "href='/pansou/assets/")
|
||||
// Favicon path rewrite
|
||||
.replace(/href="\/favicon\.ico/g, 'href="/pansou/favicon.ico')
|
||||
.replace(/href='\/favicon\.ico/g, "href='/pansou/favicon.ico");
|
||||
}
|
||||
|
||||
/**
|
||||
* Express middleware that proxies /pansou/* requests to the PanSou web container.
|
||||
*
|
||||
* How it works:
|
||||
* 1. Strips the /pansou prefix from the request path
|
||||
* 2. Forwards the request to http://pansou:80/{path}
|
||||
* 3. For HTML/JS responses, applies sub_filter path rewriting
|
||||
* so that /api/ becomes /pansou/api/ and /assets/ becomes /pansou/assets/
|
||||
* 4. For static assets (CSS, images, fonts), pipes through as-is
|
||||
*
|
||||
* Controlled by system config key 'pansou_web_enabled' (true/false).
|
||||
*/
|
||||
export async function pansouWebProxy(req: Request, res: Response, _next: NextFunction): Promise<void> {
|
||||
try {
|
||||
// Check if PanSou web is enabled
|
||||
const enabled = getSystemConfig('pansou_web_enabled');
|
||||
if (enabled !== 'true') {
|
||||
res.status(404).send('PanSou Web UI is disabled by administrator');
|
||||
return;
|
||||
}
|
||||
|
||||
// Build upstream URL: strip /pansou prefix
|
||||
let targetPath = req.path;
|
||||
targetPath = targetPath.replace(/^\/pansou/, '') || '/';
|
||||
|
||||
// Preserve query string
|
||||
const queryIndex = req.url.indexOf('?');
|
||||
const query = queryIndex >= 0 ? req.url.substring(queryIndex) : '';
|
||||
const upstreamUrl = `${PANSOU_UPSTREAM}${targetPath}${query}`;
|
||||
|
||||
// Build forwarded headers (filter out hop-by-hop headers)
|
||||
const forwardHeaders: Record<string, string> = {};
|
||||
for (const [key, value] of Object.entries(req.headers)) {
|
||||
if (!HOP_HEADERS.has(key.toLowerCase()) && value !== undefined) {
|
||||
forwardHeaders[key] = Array.isArray(value) ? value.join(', ') : value;
|
||||
}
|
||||
}
|
||||
// Override Host header to target the upstream
|
||||
forwardHeaders['Host'] = 'pansou';
|
||||
// Remove Accept-Encoding so we get uncompressed content for text rewriting
|
||||
forwardHeaders['accept-encoding'] = '';
|
||||
|
||||
// Forward the request
|
||||
const response = await fetch(upstreamUrl, {
|
||||
method: req.method as any,
|
||||
headers: forwardHeaders,
|
||||
body: ['GET', 'HEAD'].includes(req.method) ? undefined : JSON.stringify(req.body),
|
||||
redirect: 'manual',
|
||||
signal: AbortSignal.timeout(30000),
|
||||
});
|
||||
|
||||
const contentType = response.headers.get('content-type') || '';
|
||||
|
||||
// Set response status
|
||||
res.status(response.status);
|
||||
|
||||
// Handle redirects - rewrite Location header to include /pansou prefix
|
||||
if (response.status >= 300 && response.status < 400) {
|
||||
const location = response.headers.get('location');
|
||||
if (location) {
|
||||
if (location.startsWith('/')) {
|
||||
res.setHeader('location', '/pansou' + location);
|
||||
} else {
|
||||
res.setHeader('location', location);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For HTML/JS content, apply sub_filter string replacements
|
||||
if (TEXT_TYPES.some(t => contentType.includes(t))) {
|
||||
const text = await response.text();
|
||||
const modified = applySubFilter(text);
|
||||
res.setHeader('content-type', contentType);
|
||||
// Remove content-encoding since we decompressed
|
||||
res.setHeader('content-length', Buffer.byteLength(modified, 'utf-8').toString());
|
||||
res.send(modified);
|
||||
return;
|
||||
}
|
||||
|
||||
// For other content (CSS, images, fonts, etc.), pipe through as-is
|
||||
const excludedHeaders = new Set([
|
||||
'content-encoding', 'content-length', 'transfer-encoding',
|
||||
'keep-alive', 'connection',
|
||||
]);
|
||||
response.headers.forEach((value, key) => {
|
||||
if (!excludedHeaders.has(key.toLowerCase())) {
|
||||
res.setHeader(key, value);
|
||||
}
|
||||
});
|
||||
|
||||
// Use buffer for reliability
|
||||
const buffer = await response.arrayBuffer().then(buf => Buffer.from(buf));
|
||||
res.end(buffer);
|
||||
} catch (err: any) {
|
||||
console.error(`[PanSou Web Proxy] Error proxying ${req.path}:`, err.message);
|
||||
if (!res.headersSent) {
|
||||
res.status(502).send(`PanSou Web proxy error: ${err.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user