-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathproxy-server.js
More file actions
147 lines (132 loc) · 4.2 KB
/
Copy pathproxy-server.js
File metadata and controls
147 lines (132 loc) · 4.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
/**
* proxyflow 前端代理服务器
* - 监听端口 3100
* - /api/* 和 /ws/* 转发到后端 9000
* - 其余请求服务 frontend/dist 静态文件(SPA 回退到 index.html)
*/
const http = require('http');
const fs = require('fs');
const path = require('path');
const LISTEN_PORT = parseInt(process.env.FRONTEND_PORT || '3100');
const BACKEND_URL = process.env.BACKEND_URL || 'http://127.0.0.1:9000';
const STATIC_DIR = path.join(__dirname, 'frontend/dist');
// MIME 类型映射
const MIME_TYPES = {
'.html': 'text/html; charset=utf-8',
'.js': 'application/javascript; charset=utf-8',
'.css': 'text/css; charset=utf-8',
'.json': 'application/json; charset=utf-8',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.ico': 'image/x-icon',
'.woff': 'font/woff',
'.woff2':'font/woff2',
'.ttf': 'font/ttf',
'.map': 'application/json',
};
// 静态文件处理
function serveStatic(req, res) {
let urlPath = req.url.split('?')[0];
let filePath = path.join(STATIC_DIR, urlPath);
// 安全检查:防止路径穿越
if (!filePath.startsWith(STATIC_DIR)) {
res.writeHead(403);
res.end('Forbidden');
return;
}
// 尝试读取文件
function tryFile(fp, fallbackToIndex) {
fs.stat(fp, (err, stat) => {
if (!err && stat.isDirectory()) {
tryFile(path.join(fp, 'index.html'), fallbackToIndex);
return;
}
if (err || !stat.isFile()) {
if (fallbackToIndex) {
// SPA 回退:返回 index.html
fs.readFile(path.join(STATIC_DIR, 'index.html'), (e, data) => {
if (e) {
res.writeHead(404);
res.end('Not Found');
} else {
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(data);
}
});
} else {
res.writeHead(404);
res.end('Not Found');
}
return;
}
const ext = path.extname(fp).toLowerCase();
const contentType = MIME_TYPES[ext] || 'application/octet-stream';
res.writeHead(200, {
'Content-Type': contentType,
'Cache-Control': ext === '.html' ? 'no-cache' : 'public, max-age=31536000',
});
fs.createReadStream(fp).pipe(res);
});
}
tryFile(filePath, true);
}
// 创建 HTTP 服务器
const server = http.createServer((req, res) => {
const url = req.url || '/';
// /api/* → 后端
if (url.startsWith('/api/') || url.startsWith('/health')) {
const options = {
hostname: '127.0.0.1',
port: 9000,
path: url,
method: req.method,
headers: req.headers,
};
const proxyReq = http.request(options, (proxyRes) => {
res.writeHead(proxyRes.statusCode, proxyRes.headers);
proxyRes.pipe(res);
});
proxyReq.on('error', (err) => {
console.error('[proxy] backend error:', err.message);
res.writeHead(502, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Backend unavailable', detail: err.message }));
});
req.pipe(proxyReq);
return;
}
// 静态文件服务
serveStatic(req, res);
});
// WebSocket 升级代理
server.on('upgrade', (req, socket, head) => {
const url = req.url || '';
if (url.startsWith('/ws')) {
const net = require('net');
const backendSocket = net.connect(9000, '127.0.0.1', () => {
backendSocket.write(
`${req.method} ${req.url} HTTP/1.1\r\n` +
Object.entries(req.headers).map(([k, v]) => `${k}: ${v}`).join('\r\n') +
'\r\n\r\n'
);
backendSocket.write(head);
socket.pipe(backendSocket);
backendSocket.pipe(socket);
});
backendSocket.on('error', (err) => {
console.error('[ws-proxy] error:', err.message);
socket.destroy();
});
socket.on('error', () => backendSocket.destroy());
} else {
socket.destroy();
}
});
server.listen(LISTEN_PORT, () => {
console.log(`✅ proxyflow 前端代理服务器启动 http://localhost:${LISTEN_PORT}`);
console.log(` 静态文件: ${STATIC_DIR}`);
console.log(` API 代理: /api/* → ${BACKEND_URL}`);
console.log(` WS 代理: /ws/* → ${BACKEND_URL.replace('http', 'ws')}`);
});