v1.1.0: Alpine 轻量级 Docker 开发环境 + 敏感凭证清理
This commit is contained in:
+173
@@ -0,0 +1,173 @@
|
||||
const http = require('http');
|
||||
const net = require('net');
|
||||
const fs = require('fs');
|
||||
|
||||
// 配置
|
||||
const PORT = 8080;
|
||||
const TOKEN_PREFIX = 'wk_';
|
||||
// 用户口令从环境变量读取(JSON),未设置时用弱默认——生产环境务必通过 WORKPOD_AUTH_USERS 注入强口令
|
||||
const VALID_PASSWORDS = JSON.parse(process.env.WORKPOD_AUTH_USERS || '{"wk":"1234567","admin":"admin123"}');
|
||||
const WORKSPACES = [
|
||||
{ path: '/', name: '默认工作区', desc: '/workspace', port: 7681 },
|
||||
{ path: '/hszd', name: '华商智地', desc: '/workspace/wk-hszd', port: 7682 },
|
||||
];
|
||||
|
||||
// Token 存储 (内存)
|
||||
const tokens = new Set();
|
||||
|
||||
// 生成 token
|
||||
function createToken(user) {
|
||||
const t = TOKEN_PREFIX + user + '_' + Date.now();
|
||||
tokens.add(t);
|
||||
// 1小时过期清理
|
||||
setTimeout(() => tokens.delete(t), 3600000);
|
||||
return t;
|
||||
}
|
||||
|
||||
// 验证 token
|
||||
function validateToken(token) {
|
||||
return token && tokens.has(token);
|
||||
}
|
||||
|
||||
// 解析 URL 路径,匹配工作区
|
||||
function matchWorkspace(pathname) {
|
||||
for (const ws of WORKSPACES) {
|
||||
if (pathname === ws.path || pathname.startsWith(ws.path + '/')) {
|
||||
const proxyPath = ws.path === '/' ? pathname : pathname.slice(ws.path.length) || '/';
|
||||
return { ws, proxyPath };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 代理请求到 ttyd
|
||||
function proxyToTtyd(req, res, port, path) {
|
||||
const options = {
|
||||
hostname: '127.0.0.1',
|
||||
port: port,
|
||||
path: path,
|
||||
method: req.method,
|
||||
headers: { ...req.headers, host: '127.0.0.1:' + port, 'X-WorkPod-Auth': '1' }
|
||||
};
|
||||
const proxyReq = http.request(options, (proxyRes) => {
|
||||
res.writeHead(proxyRes.statusCode, proxyRes.headers);
|
||||
proxyRes.pipe(res);
|
||||
});
|
||||
proxyReq.on('error', () => {
|
||||
if (!res.headersSent) res.writeHead(502);
|
||||
res.end('Bad Gateway');
|
||||
});
|
||||
req.pipe(proxyReq);
|
||||
}
|
||||
|
||||
// 登录页 HTML
|
||||
const LOGIN_HTML = fs.readFileSync('/var/www/workpod/login.html', 'utf-8');
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
const url = new URL(req.url, 'http://' + req.headers.host);
|
||||
const token = url.searchParams.get('token') || '';
|
||||
|
||||
// 登录页
|
||||
if (url.pathname === '/login') {
|
||||
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
||||
return res.end(LOGIN_HTML);
|
||||
}
|
||||
|
||||
// 认证 API
|
||||
if (url.pathname === '/auth/check') {
|
||||
const auth = req.headers.authorization;
|
||||
if (!auth || !auth.startsWith('Basic ')) {
|
||||
res.writeHead(401);
|
||||
return res.end('Unauthorized');
|
||||
}
|
||||
const decoded = Buffer.from(auth.slice(6), 'base64').toString();
|
||||
const [user, pass] = decoded.split(':');
|
||||
if (!VALID_PASSWORDS[user] || VALID_PASSWORDS[user] !== pass) {
|
||||
res.writeHead(401);
|
||||
return res.end('Unauthorized');
|
||||
}
|
||||
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
||||
return res.end(createToken(user));
|
||||
}
|
||||
|
||||
// 工作空间列表
|
||||
if (url.pathname === '/api/workspaces') {
|
||||
if (!validateToken(token)) {
|
||||
res.writeHead(401);
|
||||
return res.end('Unauthorized');
|
||||
}
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
return res.end(JSON.stringify(WORKSPACES));
|
||||
}
|
||||
|
||||
// ttyd 内部端点 (/token, /ws 等) — 直接代理到默认工作区
|
||||
if (url.pathname === '/token' || url.pathname.startsWith('/ttyd/')) {
|
||||
proxyToTtyd(req, res, WORKSPACES[0].port, url.pathname + url.search);
|
||||
return;
|
||||
}
|
||||
|
||||
// Token 验证
|
||||
if (!validateToken(token)) {
|
||||
res.writeHead(302, { 'Location': '/login' });
|
||||
return res.end();
|
||||
}
|
||||
|
||||
// 路由到对应工作区 (普通 HTTP)
|
||||
const matched = matchWorkspace(url.pathname);
|
||||
if (matched) {
|
||||
const { ws, proxyPath } = matched;
|
||||
proxyToTtyd(req, res, ws.port, proxyPath + url.search);
|
||||
return;
|
||||
}
|
||||
|
||||
// 未匹配路径 -> 登录页
|
||||
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
||||
res.end(LOGIN_HTML);
|
||||
});
|
||||
|
||||
// WebSocket 代理 (upgrade 事件)
|
||||
server.on('upgrade', (req, socket, head) => {
|
||||
const url = new URL(req.url, 'http://' + req.headers.host);
|
||||
const token = url.searchParams.get('token') || '';
|
||||
|
||||
// Token 验证
|
||||
if (!validateToken(token)) {
|
||||
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
// 匹配工作区
|
||||
const matched = matchWorkspace(url.pathname);
|
||||
if (!matched) {
|
||||
socket.write('HTTP/1.1 404 Not Found\r\n\r\n');
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
const { ws, proxyPath } = matched;
|
||||
const proxyUrl = new URL(proxyPath + url.search, 'http://127.0.0.1:' + ws.port);
|
||||
|
||||
// 建立到 ttyd 的 TCP 连接
|
||||
const target = net.connect(ws.port, '127.0.0.1', () => {
|
||||
// 构造 upgrade 请求头
|
||||
const headers = { ...req.headers, host: '127.0.0.1:' + ws.port, 'X-WorkPod-Auth': '1' };
|
||||
const requestLine = 'GET ' + proxyUrl.pathname + proxyUrl.search + ' HTTP/1.1\r\n';
|
||||
const headerLines = Object.entries(headers).map(([k, v]) => k + ': ' + v).join('\r\n');
|
||||
target.write(requestLine + headerLines + '\r\n\r\n');
|
||||
|
||||
// 如果有缓冲数据(head),转发给 ttyd
|
||||
if (head.length > 0) target.write(head);
|
||||
|
||||
// 双向管道: 浏览器 <-> ttyd
|
||||
target.pipe(socket);
|
||||
socket.pipe(target);
|
||||
});
|
||||
|
||||
target.on('error', () => socket.destroy());
|
||||
socket.on('error', () => target.destroy());
|
||||
});
|
||||
|
||||
server.listen(PORT, () => {
|
||||
console.log('[auth-proxy] 认证代理已启动,端口: ' + PORT);
|
||||
});
|
||||
Reference in New Issue
Block a user