78 lines
3.8 KiB
JavaScript
78 lines
3.8 KiB
JavaScript
'use strict';
|
|
const http = require('node:http');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
|
|
const resourceRoot = path.resolve(
|
|
__dirname,
|
|
'../build/wechatgame-activities/remote'
|
|
);
|
|
|
|
function createServer(root = resourceRoot) {
|
|
fs.mkdirSync(root, { recursive: true });
|
|
const realRoot = fs.realpathSync(root);
|
|
const inside = file => {
|
|
const relative = path.relative(realRoot, file);
|
|
return relative && relative !== '..' && !relative.startsWith('..' + path.sep) && !path.isAbsolute(relative);
|
|
};
|
|
return http.createServer(async (req, res) => {
|
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
res.setHeader('Access-Control-Allow-Methods', 'GET, HEAD, OPTIONS');
|
|
res.setHeader('Cache-Control', 'no-store');
|
|
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
const reply = (code, message) => {
|
|
res.writeHead(code, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
res.end(req.method === 'HEAD' ? undefined : message);
|
|
};
|
|
if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; }
|
|
if (!['GET', 'HEAD'].includes(req.method)) {
|
|
res.setHeader('Allow', 'GET, HEAD, OPTIONS'); reply(405, 'Method not allowed'); return;
|
|
}
|
|
let pathname;
|
|
try { pathname = decodeURIComponent(new URL(req.url, 'http://localhost').pathname); }
|
|
catch (_) { reply(400, 'Invalid URL'); return; }
|
|
if (pathname === '/') {
|
|
reply(200, 'Activity resource server\nCopy manifest.json and bundle folders into:\n' + realRoot + '\n\nManifest: /remote/manifest.json\n'); return;
|
|
}
|
|
if (!pathname.startsWith('/remote/') || pathname.includes('\\') || pathname.includes('\0')) {
|
|
reply(404, 'Not found'); return;
|
|
}
|
|
const relative = pathname.slice('/remote/'.length);
|
|
if (relative.split('/').some(part => !part || part === '.' || part === '..') || !/\.(json|zip)$/.test(relative)) {
|
|
reply(404, 'Not found'); return;
|
|
}
|
|
try {
|
|
const file = await fs.promises.realpath(path.resolve(realRoot, relative));
|
|
if (!inside(file)) { reply(404, 'Not found'); return; }
|
|
const stat = await fs.promises.stat(file);
|
|
if (!stat.isFile()) { reply(404, 'Not found'); return; }
|
|
res.setHeader('Content-Type', file.endsWith('.zip') ? 'application/zip' : 'application/json; charset=utf-8');
|
|
res.setHeader('Content-Length', stat.size);
|
|
if (req.method === 'HEAD') { res.end(); return; }
|
|
const stream = fs.createReadStream(file);
|
|
stream.on('error', () => {
|
|
if (res.headersSent) res.destroy();
|
|
else { res.removeHeader('Content-Length'); reply(500, 'Resource read failed'); }
|
|
});
|
|
res.on('close', () => stream.destroy());
|
|
stream.pipe(res);
|
|
} catch (error) {
|
|
reply(error.code === 'ENOENT' || error.code === 'ENOTDIR' ? 404 : 500, 'Resource unavailable');
|
|
}
|
|
});
|
|
}
|
|
|
|
module.exports = { createServer, resourceRoot };
|
|
if (require.main === module) {
|
|
const port = Number(process.argv[2] || 8780);
|
|
if (!Number.isInteger(port) || port < 1 || port > 65535) throw Error('Port must be between 1 and 65535');
|
|
const server = createServer();
|
|
server.on('error', error => { console.error('Activity resource server failed:', error.message); process.exitCode = 1; });
|
|
server.listen(port, '127.0.0.1', () => {
|
|
console.log('Resource directory: ' + resourceRoot);
|
|
console.log('Resource baseUrl: http://127.0.0.1:' + port + '/remote/');
|
|
console.log('Manifest: http://127.0.0.1:' + port + '/remote/manifest.json');
|
|
console.log('Files are read on every request. Ctrl+C to stop.');
|
|
});
|
|
}
|