Merge feature/add_skin into master: resolve all conflicts

- GameGlobal.js: keep upstream SERVER_URL with /ws suffix
- en.js/zh.js: merge both settings.nickname and settings.profile keys
- SettingsScene.js: keep both nickname row and profile button
- server/index.js: merge express app + content security proxy with
  noServer WebSocket mode and path validation
- Add .gitignore for node_modules and .codebuddy
This commit is contained in:
jakciehan
2026-05-12 07:05:20 +08:00
parent 38294c040c
commit d263c7bf48
48 changed files with 10480 additions and 25 deletions
+89 -24
View File
@@ -10,8 +10,17 @@
*/
const { WebSocketServer } = require('ws');
const express = require('express');
const http = require('http');
// ============================================================
// Content Security Proxy Configuration// The content security service is now deployed as an independent
// microservice in the "content-security" K8s namespace.
// This proxy forwards /api/content/* requests to that service.
// ============================================================
const CONTENT_SECURITY_SERVICE_URL = process.env.CONTENT_SECURITY_SERVICE_URL
|| 'http://content-security-service.content-security.svc.cluster.local:3000';
const GAME_ID = process.env.GAME_ID || 'tankwar';
// ============================================================
// Configuration
// ============================================================
@@ -22,26 +31,80 @@ const HEARTBEAT_INTERVAL = 10000; // ms
const ROOM_TIMEOUT = 300000; // 5 minutes room idle timeout
// ============================================================
// HTTP Health Check Server
// Express HTTP Server + Content Security API
// ============================================================
const healthServer = http.createServer((req, res) => {
if (req.url === '/health' || req.url === '/tankwar/health') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
status: 'healthy',
timestamp: new Date().toISOString(),
activeConnections: players.size,
activeRooms: rooms.size,
activeTeamRooms: teamRooms.size
}));
} else {
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('Not Found');
}
const app = express();
const server = http.createServer(app);
// Parse JSON bodies
app.use(express.json({ limit: '2mb' }));
// Health check endpoints (for K8s livenessProbe/readinessProbe)
app.get('/health', (req, res) => {
res.json({
status: 'healthy',
timestamp: new Date().toISOString(),
activeConnections: players.size,
activeRooms: rooms.size,
activeTeamRooms: teamRooms.size
});
});
app.get('/tankwar/health', (req, res) => {
res.json({
status: 'healthy',
timestamp: new Date().toISOString(),
activeConnections: players.size,
activeRooms: rooms.size,
activeTeamRooms: teamRooms.size
});
});
app.get('/api/health', (req, res) => {
res.json({ status: 'ok', timestamp: Date.now() });
});
healthServer.listen(PORT, HOST, () => {
console.log(`[Health Server] Running on ${HOST}:${PORT}`);
// ============================================================
// Content Security Proxy
// Forward /api/content/* requests to the independent
// content-security-service in the content-security namespace.
// This allows the tankwar-server to delegate all content
// moderation to the shared microservice.
// ============================================================
app.use('/api/content', (req, res, next) => {
// Parse target host and port
const urlObj = new URL(CONTENT_SECURITY_SERVICE_URL);
const targetPath = `/api/content${req.path}`;
const qs = req.url.includes('?') ? req.url.substring(req.url.indexOf('?')) : '';
const finalPath = qs ? `${targetPath}${qs}` : targetPath;
const proxyReq = http.request({
hostname: urlObj.hostname,
port: urlObj.port || 80,
path: finalPath,
method: req.method,
headers: {
...req.headers,
host: `${urlObj.hostname}:${urlObj.port || 80}`,
'X-Game-Id': GAME_ID,
'X-Forwarded-For': req.ip || req.socket.remoteAddress,
},
}, (proxyRes) => {
res.writeHead(proxyRes.statusCode, proxyRes.headers);
proxyRes.pipe(res, { end: true });
});
proxyReq.on('error', (err) => {
console.error('[TankWar Proxy] Failed to forward to content-security-service:', err.message);
if (!res.headersSent) {
res.status(502).json({
errcode: -1,
errmsg: '内容安全服务暂时不可用,请稍后再试',
});
}
});
// Pipe request body to proxy
req.pipe(proxyReq, { end: true });
});
// ============================================================
@@ -1594,15 +1657,14 @@ setInterval(() => {
}, 300000); // Every 5 minutes
// ============================================================
// WebSocket Server (noServer mode, shares HTTP server with health check)
// ============================================================
// WebSocket Server (noServer mode, shares HTTP server with express)
// ============================================================
// Use noServer mode so the WS upgrade only fires on the configured path.
// This lets /health stay as plain HTTP on the same port.
const wss = new WebSocketServer({ noServer: true });
healthServer.on('upgrade', (req, socket, head) => {
server.on('upgrade', (req, socket, head) => {
// Only upgrade on the configured WebSocket path; reject any other path.
// We compare by pathname so query strings are tolerated.
const pathname = (req.url || '').split('?')[0];
if (pathname !== WS_PATH) {
console.warn(`[Server] Rejected WebSocket upgrade on unexpected path: ${req.url}`);
@@ -1676,6 +1738,9 @@ setInterval(() => {
// ============================================================
// Startup
// ============================================================
console.log(`[Tank War Server] Running on ${HOST}:${PORT}`);
console.log(`[Tank War Server] WebSocket path: ${WS_PATH}`);
console.log(`[Tank War Server] Health check paths: /health, /tankwar/health`);
server.listen(PORT, HOST, () => {
console.log(`[Tank War Server] Running on ${HOST}:${PORT}`);
console.log(`[Tank War Server] WebSocket path: ${WS_PATH}`);
console.log(`[Tank War Server] Health check paths: /health, /tankwar/health`);
console.log(`[Tank War Server] HTTP API URL: http://${HOST}:${PORT}`);
});