Files

146 lines
4.9 KiB
JavaScript

// res_proxy.js —— 本地资源代理(res.3dtank.com → 本地缓存)
//
// 为什么需要:官方 3DS 贴图(BG1 1024.PNG / SKY1.PNG 等)没有 CORS 头,
// 浏览器从 localhost 直接请求会被拦截;同时每次开发都走公网很慢。
//
// 用法:node tools/res_proxy.js [--port 8082] [--cache build/res-cache]
// GET /res/<path> → https://res.3dtank.com/<path>,落盘缓存后返回
// GET /play/<path> → 3DTank/web/play/<path>(本地镜像,可选)
// GET /health → ok
//
// 前端通过 webpack devServer 把 /res 代理到本服务(webpack.config.d/res-proxy.js)。
'use strict';
const http = require('http');
const fs = require('fs');
const path = require('path');
const { URL } = require('url');
const Argv = process.argv.slice(2);
const Arg = (Name, Def) => {
const I = Argv.indexOf('--' + Name);
return I >= 0 && Argv[I + 1] ? Argv[I + 1] : Def;
};
const Root = path.resolve(__dirname, '..');
const Port = Number(Arg('port', '8082'));
const CacheDir = path.resolve(Root, Arg('cache', 'build/res-cache'));
const MirrorPlay = path.resolve(Root, '..', '3DTank', 'web', 'play');
const Upstream = 'https://res.3dtank.com';
const Mime = {
'.3ds': 'application/octet-stream',
'.a3d': 'application/octet-stream',
'.webp': 'image/webp',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.svg': 'image/svg+xml',
'.json': 'application/json',
'.info': 'application/octet-stream',
'.mp3': 'audio/mpeg',
'.ktx': 'application/octet-stream',
};
fs.mkdirSync(CacheDir, { recursive: true });
function CachePath(Key) {
const Clean = Key.replace(/^\/+/, '').replace(/[^a-zA-Z0-9._/-]/g, '_').replace(/\.\./g, '_');
return path.join(CacheDir, Clean);
}
function Send(Res, Status, Type, Body) {
Res.writeHead(Status, {
'Content-Type': Type || 'application/octet-stream',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, OPTIONS',
'Access-Control-Allow-Headers': '*',
'Cache-Control': 'public, max-age=31536000, immutable',
});
Res.end(Body);
}
async function FetchUpstream(PathAndQuery) {
const Decoded = decodeURIComponent(PathAndQuery);
const Ext = path.extname(Decoded);
const NoExt = Decoded.slice(0, Decoded.length - Ext.length);
const Candidates = [
PathAndQuery,
Decoded.toLowerCase(),
(NoExt + '.webp').toLowerCase(),
(NoExt + '.png').toLowerCase(),
(NoExt + '.webp'),
(NoExt + '.png'),
];
for (const PathTry of [...new Set(Candidates)]) {
const R = await fetch(Upstream + (PathTry.startsWith('/') ? PathTry : '/' + PathTry), {
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) TankiOffline/0.1',
Accept: '*/*',
},
});
if (R.ok) {
const Body = Buffer.from(await R.arrayBuffer());
return {
Status: 200,
Type: R.headers.get('content-type') || 'application/octet-stream',
Body,
Resolved: PathTry,
};
}
}
return { Status: 404, Type: 'text/plain', Body: Buffer.from('upstream 404') };
}
const Server = http.createServer(async (Req, Res) => {
if (Req.method === 'OPTIONS') {
Send(Res, 204, 'text/plain', Buffer.alloc(0));
return;
}
const U = new URL(Req.url, 'http://localhost');
if (U.pathname === '/health') {
Send(Res, 200, 'text/plain', Buffer.from('ok'));
return;
}
// 本地镜像:/play/... → 3DTank/web/play/...
if (U.pathname.startsWith('/play/')) {
const File = path.join(MirrorPlay, U.pathname.slice('/play/'.length));
if (File.startsWith(MirrorPlay) && fs.existsSync(File) && fs.statSync(File).isFile()) {
Send(Res, 200, Mime[path.extname(File).toLowerCase()], fs.readFileSync(File));
return;
}
Send(Res, 404, 'text/plain', Buffer.from('mirror miss'));
return;
}
// 线上资源:/res/<path> → res.3dtank.com/<path>
if (!U.pathname.startsWith('/res/')) {
Send(Res, 404, 'text/plain', Buffer.from('not found'));
return;
}
const Key = U.pathname.slice('/res'.length);
const Cache = CachePath(Key);
if (fs.existsSync(Cache) && fs.statSync(Cache).isFile()) {
Send(Res, 200, Mime[path.extname(Cache).toLowerCase()] || 'application/octet-stream', fs.readFileSync(Cache));
return;
}
try {
const Result = await FetchUpstream(Key);
if (Result.Status === 200) {
fs.mkdirSync(path.dirname(Cache), { recursive: true });
fs.writeFileSync(Cache, Result.Body);
}
Send(Res, Result.Status, Result.Type, Result.Body);
console.log(`${Result.Status === 200 ? 'cache' : 'fail '} ${Key} (${Result.Body.length}B)`);
} catch (E) {
Send(Res, 502, 'text/plain', Buffer.from('proxy error: ' + E.message));
console.log('error', Key, E.message);
}
});
Server.listen(Port, '127.0.0.1', () => {
console.log(`res proxy: http://127.0.0.1:${Port}/res/... (cache: ${CacheDir})`);
console.log(`play mirror: http://127.0.0.1:${Port}/play/... (${MirrorPlay})`);
});