Vendor node-static (#11295)

Also includes a decent amount of refactoring to bring it in line with Showdown code standards.

---------

Co-authored-by: Slayer95 <ivojulca@hotmail.com>
This commit is contained in:
Guangcong Luo 2025-07-23 21:19:55 -07:00 committed by GitHub
parent 1487d52db0
commit 7a9e535e35
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 486 additions and 173 deletions

439
lib/static-server.ts Normal file
View File

@ -0,0 +1,439 @@
/**
* Static server
*
* API resembles node-static, but with some differences:
*
* - `serve`'s callback needs to return `true` to suppress the default error page
* - everything is Promises
* - no customizing cache time by filename
* - no index.json directory streaming (it was undocumented; you weren't using it)
*
* Forked from node-static @
* https://github.com/cloudhead/node-static/blob/e49fbd728e93294c225f52103962e56aab86cb1a/lib/node-static.js
*
* @author Guangcong Luo <guangcongluo@gmail.com>, Alexis Sellier, Brett Zamir
* @license MIT
*/
import fs from 'node:fs';
import fsP from 'node:fs/promises';
import http from 'node:http';
import path from 'node:path';
const DEBUG = false;
export const SERVER_INFO = 'node-static-vendored/1.0';
export type Headers = Record<string, string>;
export type Options = {
/** Root directory to serve files from. */
root?: string,
/** Index file when serving a directory. */
indexFile?: string,
/** Default extension to append to files if not found. */
defaultExtension?: string,
/** Cache time in seconds. null = no cache header. undefined = default (3600). 0 = no cache. */
cacheTime?: number | null,
/** Serve `.gz` files if available. */
gzip?: boolean | RegExp,
/** Custom headers for success responses (not sent on errors). */
headers?: Headers,
/** Server header. `null` to disable. */
serverInfo?: string | null,
};
export type Result = {
status: number,
headers: Record<string, string>,
message: string | undefined,
/** Have we already responded? */
alreadySent: boolean,
};
/** Return true to suppress default error page */
export type ErrorCallback = (result: Result) => boolean | void;
export const mimeTypes: { [key: string]: string } = {
'.html': 'text/html;charset=utf-8',
'.htm': 'text/html;charset=utf-8',
'.css': 'text/css;charset=utf-8',
'.js': 'application/javascript;charset=utf-8',
'.jsx': 'application/javascript;charset=utf-8',
'.cjs': 'application/javascript;charset=utf-8',
'.mjs': 'application/javascript;charset=utf-8',
'.json': 'application/json;charset=utf-8',
'.ts': 'application/typescript;charset=utf-8',
'.xml': 'application/xml;charset=utf-8',
'.txt': 'text/plain;charset=utf-8',
'.md': 'text/markdown;charset=utf-8',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml;charset=utf-8',
'.ico': 'image/x-icon',
'.bmp': 'image/bmp',
'.webp': 'image/webp',
'.woff': 'font/woff',
'.woff2': 'font/woff2',
'.ttf': 'font/ttf',
'.eot': 'application/vnd.ms-fontobject',
'.zip': 'application/zip',
'.tar': 'application/x-tar',
'.gz': 'application/gzip',
'.mp3': 'audio/mpeg',
'.wav': 'audio/wav',
'.ogg': 'audio/ogg',
'.mp4': 'video/mp4',
'.webm': 'video/webm',
};
export class StaticServer {
root: string;
options: Options;
cacheTime: number | null = 3600;
defaultHeaders: Headers = {};
/** Contains the `.`, unlike options.defaultExtension */
defaultExtension = '';
constructor(root: string, options?: Options);
constructor(options?: Options);
constructor(root?: Options | string | null, options?: Options) {
if (root && typeof root === 'object') {
options = root;
root = null;
}
// resolve() doesn't normalize (to lowercase) drive letters on Windows
this.root = path.normalize(path.resolve(root || '.'));
this.options = options || {};
this.options.indexFile ||= 'index.html';
if (this.options.cacheTime !== undefined) {
this.cacheTime = this.options.cacheTime;
}
if (this.options.defaultExtension) {
this.defaultExtension = `.${this.options.defaultExtension}`;
}
if (this.options.serverInfo !== null) {
this.defaultHeaders['server'] = this.options.serverInfo || SERVER_INFO;
}
for (const k in this.options.headers) {
this.defaultHeaders[k] = this.options.headers[k];
}
}
async serveDir(
pathname: string, req: http.IncomingMessage, res: http.ServerResponse
): Promise<Result> {
const htmlIndex = path.join(pathname, this.options.indexFile!);
try {
const stat = await fsP.stat(htmlIndex);
const status = 200;
const headers = {};
const originalPathname = decodeURIComponent(new URL(req.url!, 'http://localhost').pathname);
if (originalPathname.length && !originalPathname.endsWith('/')) {
return this.getResult(301, { 'Location': originalPathname + '/' });
} else {
return this.respond(status, headers, htmlIndex, stat, req, res);
}
} catch {
return this.getResult(404);
}
}
async serveFile(
pathname: string, status: number, headers: Headers, req: http.IncomingMessage, res: http.ServerResponse,
errorCallback?: ErrorCallback
): Promise<Result> {
pathname = this.resolve(pathname);
const stat = await fsP.stat(pathname);
const result = await this.respond(status, headers, pathname, stat, req, res);
return this.finish(result, req, res, errorCallback);
}
getResult(status: number, headers: Headers = {}, alreadySent = false): Result {
if (this.defaultHeaders['server']) {
headers['server'] ||= this.defaultHeaders['server'];
}
return {
status,
headers,
message: http.STATUS_CODES[status],
alreadySent,
};
}
finish(
result: Result, req: http.IncomingMessage, res: http.ServerResponse, errorCallback?: ErrorCallback
): Result {
// If `alreadySent`, it's been taken care of in `this.stream`.
if (!result.alreadySent && !errorCallback?.(result)) {
res.writeHead(result.status, result.headers);
if (result.status >= 400 && req.method !== 'HEAD') {
res.write(`${result.status} ${result.message}`);
}
res.end();
}
return result;
}
async servePath(
pathname: string, status: number, headers: Headers, req: http.IncomingMessage, res: http.ServerResponse
): Promise<Result> {
pathname = this.resolve(pathname);
// Make sure we're not trying to access a
// file outside of the root.
if (!pathname.startsWith(this.root)) {
// Forbidden
return this.getResult(403);
}
try {
const stat = await fsP.stat(pathname);
if (stat.isFile()) { // Stream a single file.
return this.respond(status, headers, pathname, stat, req, res);
} else if (stat.isDirectory()) { // Stream a directory of files.
return this.serveDir(pathname, req, res);
} else {
return this.getResult(400);
}
} catch {
// possibly not found, check default extension
if (this.defaultExtension) {
try {
const stat = await fsP.stat(pathname + this.defaultExtension);
if (stat.isFile()) {
return this.respond(status, headers, pathname + this.defaultExtension, stat, req, res);
} else {
return this.getResult(400);
}
} catch {
// really not found
return this.getResult(404);
}
} else {
return this.getResult(404);
}
}
}
resolve(pathname: string) {
return path.resolve(path.join(this.root, pathname));
}
async serve(req: http.IncomingMessage, res: http.ServerResponse, errorCallback?: ErrorCallback): Promise<Result> {
let pathname;
try {
pathname = decodeURIComponent(new URL(req.url!, 'http://localhost').pathname);
} catch {
return this.finish(this.getResult(400), req, res, errorCallback);
}
const result = await this.servePath(pathname, 200, {}, req, res);
return this.finish(result, req, res, errorCallback);
}
/** Check if we should consider sending a gzip version of the file based on the
* file content type and client's Accept-Encoding header value. */
gzipOk(req: http.IncomingMessage, contentType: string) {
const enable = this.options.gzip;
if (enable === true || ((enable instanceof RegExp) && enable.test(contentType))) {
return req.headers['accept-encoding']?.includes('gzip');
}
return false;
}
/** Send a gzipped version of the file if the options and the client indicate gzip is enabled and
* we find a .gz file matching the static resource requested. */
respondGzip(
status: number, contentType: string, _headers: Headers, file: string, stat: fs.Stats,
req: http.IncomingMessage, res: http.ServerResponse
): Promise<Result> {
if (!this.gzipOk(req, contentType)) {
// Client doesn't want gzip
return this.respondNoGzip(status, contentType, _headers, file, stat, req, res);
}
const gzFile = `${file}.gz`;
return fsP.stat(gzFile).catch(() => null).then(gzStat => {
if (gzStat?.isFile()) {
const vary = _headers['Vary'];
_headers['Vary'] = (vary && vary !== 'Accept-Encoding' ? `${vary}, ` : '') + 'Accept-Encoding';
_headers['Content-Encoding'] = 'gzip';
stat.size = gzStat.size;
file = gzFile;
}
return this.respondNoGzip(status, contentType, _headers, file, stat, req, res);
});
}
parseByteRange(req: http.IncomingMessage, stat: fs.Stats) {
const byteRange = {
from: 0,
to: 0,
valid: false,
};
const rangeHeader = req.headers['range'];
const flavor = 'bytes=';
if (rangeHeader) {
if (rangeHeader.startsWith(flavor) && !rangeHeader.includes(',')) {
/* Parse */
const splitRangeHeader = rangeHeader.substr(flavor.length).split('-');
byteRange.from = parseInt(splitRangeHeader[0]);
byteRange.to = parseInt(splitRangeHeader[1]);
/* Replace empty fields of differential requests by absolute values */
if (isNaN(byteRange.from) && !isNaN(byteRange.to)) {
byteRange.from = stat.size - byteRange.to;
byteRange.to = stat.size ? stat.size - 1 : 0;
} else if (!isNaN(byteRange.from) && isNaN(byteRange.to)) {
byteRange.to = stat.size ? stat.size - 1 : 0;
}
/* General byte range validation */
if (!isNaN(byteRange.from) && !isNaN(byteRange.to) && 0 <= byteRange.from && byteRange.from <= byteRange.to) {
byteRange.valid = true;
} else {
if (DEBUG) console.warn('Request contains invalid range header: ', splitRangeHeader);
}
} else {
if (DEBUG) console.warn('Request contains unsupported range header: ', rangeHeader);
}
}
return byteRange;
}
async respondNoGzip(
status: number, contentType: string, _headers: Headers, file: string, stat: fs.Stats,
req: http.IncomingMessage, res: http.ServerResponse
): Promise<Result> {
const mtime = Date.parse(stat.mtime as any);
const headers: Headers = {};
const clientETag = req.headers['if-none-match'];
const clientMTime = Date.parse(req.headers['if-modified-since']!);
const byteRange = this.parseByteRange(req, stat);
let startByte = 0;
let length = stat.size;
/* Handle byte ranges */
if (byteRange.valid) {
if (byteRange.to < length) {
// Note: HTTP Range param is inclusive
startByte = byteRange.from;
length = byteRange.to - byteRange.from + 1;
status = 206;
// Set Content-Range response header (we advertise initial resource size on server here (stat.size))
headers['Content-Range'] = `bytes ${byteRange.from}-${byteRange.to}/${stat.size}`;
} else {
byteRange.valid = false;
if (DEBUG) {
console.warn('Range request exceeds file boundaries, goes until byte no', byteRange.to, 'against file size of', length, 'bytes');
}
}
}
/* In any case, check for unhandled byte range headers */
if (!byteRange.valid && req.headers['range']) {
if (DEBUG) console.error(new Error('Range request present but invalid, might serve whole file instead'));
}
// Copy default headers
for (const k in this.defaultHeaders) headers[k] = this.defaultHeaders[k];
headers['Etag'] = JSON.stringify([stat.ino, stat.size, mtime].join('-'));
headers['Date'] = new Date().toUTCString();
headers['Last-Modified'] = new Date(stat.mtime).toUTCString();
headers['Content-Type'] = contentType;
headers['Content-Length'] = length as any;
// Copy custom headers
for (const k in _headers) { headers[k] = _headers[k]; }
// Conditional GET
// If the "If-Modified-Since" or "If-None-Match" headers
// match the conditions, send a 304 Not Modified.
if ((clientMTime || clientETag) &&
(!clientETag || clientETag === headers['Etag']) &&
(!clientMTime || clientMTime >= mtime)) {
// 304 response should not contain entity headers
for (const entityHeader of [
'Content-Encoding', 'Content-Language', 'Content-Length', 'Content-Location', 'Content-MD5', 'Content-Range', 'Content-Type', 'Expires', 'Last-Modified',
]) {
delete headers[entityHeader];
}
return this.getResult(304, headers);
} else if (req.method === 'HEAD') {
return this.getResult(status, headers);
} else {
res.writeHead(status, headers);
try {
await this.stream(file, length, startByte, res);
return this.getResult(status, headers, true);
} catch {
// too late to actually send the 500 header
return this.getResult(500, {}, true);
}
}
}
respond(
status: number, _headers: Headers, file: string, stat: fs.Stats,
req: http.IncomingMessage, res: http.ServerResponse
): Promise<Result> {
const contentType = _headers['Content-Type'] ||
mimeTypes[path.extname(file)] ||
'application/octet-stream';
_headers = this.setCacheHeaders(_headers);
if (this.options.gzip) {
return this.respondGzip(status, contentType, _headers, file, stat, req, res);
} else {
return this.respondNoGzip(status, contentType, _headers, file, stat, req, res);
}
}
stream(file: string, length: number, startByte: number, res: http.ServerResponse): Promise<number> {
return new Promise<number>((resolve, reject) => {
let offset = 0;
// Stream the file to the client
fs.createReadStream(file, {
flags: 'r',
mode: 0o666,
start: startByte,
end: startByte + (length ? length - 1 : 0),
}).on('data', chunk => {
// Bounds check the incoming chunk and offset, as copying
// a buffer from an invalid offset will throw an error and crash
if (chunk.length && offset < length && offset >= 0) {
offset += chunk.length;
}
}).on('close', () => {
res.end();
resolve(offset);
}).on('error', err => {
reject(err);
console.error(err);
}).pipe(res, { end: false });
});
}
setCacheHeaders(_headers: Headers): Headers {
if (typeof this.cacheTime === 'number') {
_headers['cache-control'] = `max-age=${this.cacheTime}`;
}
return _headers;
}
}

195
package-lock.json generated
View File

@ -7,7 +7,6 @@
"": {
"name": "pokemon-showdown",
"version": "0.11.10",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"esbuild": "^0.25.0",
@ -25,7 +24,6 @@
"@types/better-sqlite3": "7.6.3",
"@types/cloud-env": "^0.2.2",
"@types/node": "^14.18.63",
"@types/node-static": "^0.7.7",
"@types/nodemailer": "^6.4.4",
"@types/pg": "^8.6.5",
"@types/sockjs": "^0.3.33",
@ -44,7 +42,6 @@
"better-sqlite3": "^11.8.1",
"cloud-env": "^0.2.3",
"githubhook": "^1.9.3",
"node-static": "^0.7.11",
"nodemailer": "^6.4.6",
"permessage-deflate": "^0.1.7",
"pg": "^8.11.3",
@ -921,12 +918,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/mime": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz",
"integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==",
"dev": true
},
"node_modules/@types/node": {
"version": "14.18.63",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz",
@ -934,16 +925,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/node-static": {
"version": "0.7.7",
"resolved": "https://registry.npmjs.org/@types/node-static/-/node-static-0.7.7.tgz",
"integrity": "sha512-Cq3c9lfC9zRrGxe7ox073219Mpy/kmWNsISG0yEG7aUEk33xv/g+uqz/+4b7hM4WN9LsGageBzGuvy09inaGhg==",
"dev": true,
"dependencies": {
"@types/mime": "^1",
"@types/node": "*"
}
},
"node_modules/@types/nodemailer": {
"version": "6.4.7",
"resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.7.tgz",
@ -1485,7 +1466,8 @@
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"optional": true
},
"node_modules/buffer-writer": {
"version": "2.0.0",
@ -1677,15 +1659,6 @@
"color-support": "bin.js"
}
},
"node_modules/colors": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz",
"integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==",
"optional": true,
"engines": {
"node": ">=0.1.90"
}
},
"node_modules/commander": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
@ -2551,6 +2524,7 @@
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"optional": true,
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3"
},
@ -2867,7 +2841,8 @@
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"devOptional": true
},
"node_modules/log-symbols": {
"version": "4.1.0",
@ -2956,18 +2931,6 @@
"node": ">=8.6"
}
},
"node_modules/mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
"optional": true,
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/mimic-response": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
@ -2992,12 +2955,6 @@
"node": "*"
}
},
"node_modules/minimist": {
"version": "0.0.10",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz",
"integrity": "sha512-iotkTvxc+TwOm5Ieim8VnSNvCDjCK9S8G3scJ50ZthspSxa7jx50jkhYduuAtAjvfDUwSgOwf8+If99AlOEhyw==",
"optional": true
},
"node_modules/minipass": {
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
@ -3239,6 +3196,7 @@
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"devOptional": true,
"license": "MIT"
},
"node_modules/multiline": {
@ -3337,6 +3295,7 @@
"version": "2.9.1",
"resolved": "https://registry.npmjs.org/needle/-/needle-2.9.1.tgz",
"integrity": "sha512-6R9fqJ5Zcmf+uYaFgdIHmLwNldn5HbK8L5ybn7Uz+ylX/rnOsSp1AHcvQSrCaFN+qNM1wpymHqD7mVasEOlHGQ==",
"optional": true,
"dependencies": {
"debug": "^3.2.6",
"iconv-lite": "^0.4.4",
@ -3353,6 +3312,7 @@
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
"optional": true,
"dependencies": {
"ms": "^2.1.1"
}
@ -3411,23 +3371,6 @@
"node": ">= 10.12.0"
}
},
"node_modules/node-static": {
"version": "0.7.11",
"resolved": "https://registry.npmjs.org/node-static/-/node-static-0.7.11.tgz",
"integrity": "sha512-zfWC/gICcqb74D9ndyvxZWaI1jzcoHmf4UTHWQchBNuNMxdBLJMDiUgZ1tjGLEIe/BMhj2DxKD8HOuc2062pDQ==",
"optional": true,
"dependencies": {
"colors": ">=0.6.0",
"mime": "^1.2.9",
"optimist": ">=0.3.4"
},
"bin": {
"static": "bin/cli.js"
},
"engines": {
"node": ">= 0.4.1"
}
},
"node_modules/nodemailer": {
"version": "6.8.0",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.8.0.tgz",
@ -3498,16 +3441,6 @@
"wrappy": "1"
}
},
"node_modules/optimist": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz",
"integrity": "sha512-snN4O4TkigujZphWLN0E//nQmm7790RYaE53DdL7ZYwee2D8DDo9/EyYiKUfN3rneWUjhJnueija3G9I2i0h3g==",
"optional": true,
"dependencies": {
"minimist": "~0.0.1",
"wordwrap": "~0.0.2"
}
},
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@ -3889,12 +3822,12 @@
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/probe-image-size/-/probe-image-size-7.2.3.tgz",
"integrity": "sha512-HubhG4Rb2UH8YtV4ba0Vp5bQ7L78RTONYu/ujmCu5nBI8wGv24s4E9xSKBi0N1MowRpxk76pFCpJtW0KPzOK0w==",
"optional": true,
"dependencies": {
"lodash.merge": "^4.6.2",
"needle": "^2.5.2",
"stream-parser": "~0.3.1"
},
"optional": true
}
},
"node_modules/promise-inflight": {
"version": "1.0.1",
@ -4138,7 +4071,8 @@
"node_modules/sax": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==",
"optional": true
},
"node_modules/semver": {
"version": "7.7.1",
@ -4319,6 +4253,7 @@
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"optional": true,
"engines": {
"node": ">=0.10.0"
}
@ -4327,6 +4262,7 @@
"version": "0.5.21",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
"optional": true,
"dependencies": {
"buffer-from": "^1.0.0",
"source-map": "^0.6.0"
@ -4414,6 +4350,7 @@
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/stream-parser/-/stream-parser-0.3.1.tgz",
"integrity": "sha512-bJ/HgKq41nlKvlhccD5kaCr/P+Hu0wPNKPJOH7en+YrJu/9EgqUF+88w5Jb6KNcjOFMhfX4B2asfeAtIGuHObQ==",
"optional": true,
"dependencies": {
"debug": "2"
}
@ -4422,6 +4359,7 @@
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"optional": true,
"dependencies": {
"ms": "2.0.0"
}
@ -4429,7 +4367,8 @@
"node_modules/stream-parser/node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"optional": true
},
"node_modules/string_decoder": {
"version": "1.1.1",
@ -4832,15 +4771,6 @@
"node": ">=0.10.0"
}
},
"node_modules/wordwrap": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz",
"integrity": "sha512-1tMA907+V4QmxV7dbRvb4/8MaRALK6q9Abid3ndMYnbyo8piisCmeONVqVSXqQA3KaP4SLt5b7ud6E2sqP8TFw==",
"optional": true,
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/workerpool": {
"version": "6.5.1",
"resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz",
@ -5429,28 +5359,12 @@
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
"dev": true
},
"@types/mime": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz",
"integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==",
"dev": true
},
"@types/node": {
"version": "14.18.63",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz",
"integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==",
"dev": true
},
"@types/node-static": {
"version": "0.7.7",
"resolved": "https://registry.npmjs.org/@types/node-static/-/node-static-0.7.7.tgz",
"integrity": "sha512-Cq3c9lfC9zRrGxe7ox073219Mpy/kmWNsISG0yEG7aUEk33xv/g+uqz/+4b7hM4WN9LsGageBzGuvy09inaGhg==",
"dev": true,
"requires": {
"@types/mime": "^1",
"@types/node": "*"
}
},
"@types/nodemailer": {
"version": "6.4.7",
"resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.7.tgz",
@ -5809,7 +5723,8 @@
"buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"optional": true
},
"buffer-writer": {
"version": "2.0.0",
@ -5950,12 +5865,6 @@
"integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==",
"optional": true
},
"colors": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz",
"integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==",
"optional": true
},
"commander": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
@ -6580,6 +6489,7 @@
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"optional": true,
"requires": {
"safer-buffer": ">= 2.1.2 < 3"
}
@ -6801,7 +6711,8 @@
"lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"devOptional": true
},
"log-symbols": {
"version": "4.1.0",
@ -6867,12 +6778,6 @@
"picomatch": "^2.3.1"
}
},
"mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
"optional": true
},
"mimic-response": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
@ -6888,12 +6793,6 @@
"brace-expansion": "^1.1.7"
}
},
"minimist": {
"version": "0.0.10",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz",
"integrity": "sha512-iotkTvxc+TwOm5Ieim8VnSNvCDjCK9S8G3scJ50ZthspSxa7jx50jkhYduuAtAjvfDUwSgOwf8+If99AlOEhyw==",
"optional": true
},
"minipass": {
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
@ -7064,7 +6963,8 @@
"ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"devOptional": true
},
"multiline": {
"version": "1.0.2",
@ -7147,6 +7047,7 @@
"version": "2.9.1",
"resolved": "https://registry.npmjs.org/needle/-/needle-2.9.1.tgz",
"integrity": "sha512-6R9fqJ5Zcmf+uYaFgdIHmLwNldn5HbK8L5ybn7Uz+ylX/rnOsSp1AHcvQSrCaFN+qNM1wpymHqD7mVasEOlHGQ==",
"optional": true,
"requires": {
"debug": "^3.2.6",
"iconv-lite": "^0.4.4",
@ -7157,6 +7058,7 @@
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
"optional": true,
"requires": {
"ms": "^2.1.1"
}
@ -7202,17 +7104,6 @@
"which": "^2.0.2"
}
},
"node-static": {
"version": "0.7.11",
"resolved": "https://registry.npmjs.org/node-static/-/node-static-0.7.11.tgz",
"integrity": "sha512-zfWC/gICcqb74D9ndyvxZWaI1jzcoHmf4UTHWQchBNuNMxdBLJMDiUgZ1tjGLEIe/BMhj2DxKD8HOuc2062pDQ==",
"optional": true,
"requires": {
"colors": ">=0.6.0",
"mime": "^1.2.9",
"optimist": ">=0.3.4"
}
},
"nodemailer": {
"version": "6.8.0",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.8.0.tgz",
@ -7261,16 +7152,6 @@
"wrappy": "1"
}
},
"optimist": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz",
"integrity": "sha512-snN4O4TkigujZphWLN0E//nQmm7790RYaE53DdL7ZYwee2D8DDo9/EyYiKUfN3rneWUjhJnueija3G9I2i0h3g==",
"optional": true,
"requires": {
"minimist": "~0.0.1",
"wordwrap": "~0.0.2"
}
},
"optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@ -7541,6 +7422,7 @@
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/probe-image-size/-/probe-image-size-7.2.3.tgz",
"integrity": "sha512-HubhG4Rb2UH8YtV4ba0Vp5bQ7L78RTONYu/ujmCu5nBI8wGv24s4E9xSKBi0N1MowRpxk76pFCpJtW0KPzOK0w==",
"optional": true,
"requires": {
"lodash.merge": "^4.6.2",
"needle": "^2.5.2",
@ -7695,7 +7577,8 @@
"sax": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==",
"optional": true
},
"semver": {
"version": "7.7.1",
@ -7814,17 +7697,18 @@
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"optional": true
},
"source-map-support": {
"version": "0.5.21",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
"optional": true,
"requires": {
"buffer-from": "^1.0.0",
"source-map": "^0.6.0"
},
"optional": true
}
},
"split2": {
"version": "4.1.0",
@ -7881,6 +7765,7 @@
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/stream-parser/-/stream-parser-0.3.1.tgz",
"integrity": "sha512-bJ/HgKq41nlKvlhccD5kaCr/P+Hu0wPNKPJOH7en+YrJu/9EgqUF+88w5Jb6KNcjOFMhfX4B2asfeAtIGuHObQ==",
"optional": true,
"requires": {
"debug": "2"
},
@ -7889,6 +7774,7 @@
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"optional": true,
"requires": {
"ms": "2.0.0"
}
@ -7896,7 +7782,8 @@
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"optional": true
}
}
},
@ -8191,12 +8078,6 @@
"integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
"dev": true
},
"wordwrap": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz",
"integrity": "sha512-1tMA907+V4QmxV7dbRvb4/8MaRALK6q9Abid3ndMYnbyo8piisCmeONVqVSXqQA3KaP4SLt5b7ud6E2sqP8TFw==",
"optional": true
},
"workerpool": {
"version": "6.5.1",
"resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz",

View File

@ -16,7 +16,6 @@
"better-sqlite3": "^11.8.1",
"cloud-env": "^0.2.3",
"githubhook": "^1.9.3",
"node-static": "^0.7.11",
"nodemailer": "^6.4.6",
"permessage-deflate": "^0.1.7",
"pg": "^8.11.3",
@ -72,7 +71,6 @@
"@types/better-sqlite3": "7.6.3",
"@types/cloud-env": "^0.2.2",
"@types/node": "^14.18.63",
"@types/node-static": "^0.7.7",
"@types/nodemailer": "^6.4.4",
"@types/pg": "^8.6.5",
"@types/sockjs": "^0.3.33",

View File

@ -18,6 +18,7 @@ import * as path from 'path';
import { crashlogger, ProcessManager, Streams, Repl } from '../lib';
import { IPTools } from './ip-tools';
import { type ChannelID, extractChannelMessages } from '../sim/battle';
import { StaticServer } from '../lib/static-server';
type StreamWorker = ProcessManager.StreamWorker;
@ -271,7 +272,6 @@ export class ServerStream extends Streams.ObjectReadWriteStream<string> {
wsdeflate?: typeof Config.wsdeflate,
proxyip?: typeof Config.proxyip,
customhttpresponse?: typeof Config.customhttpresponse,
disablenodestatic?: boolean,
}) {
super();
if (!config.bindaddress) config.bindaddress = '0.0.0.0';
@ -334,8 +334,6 @@ export class ServerStream extends Streams.ObjectReadWriteStream<string> {
// Static server
try {
if (config.disablenodestatic) throw new Error("disablenodestatic");
const StaticServer: typeof import('node-static').Server = require('node-static').Server;
const roomidRegex = /^\/(?:[A-Za-z0-9][A-Za-z0-9-]*)\/?$/;
const cssServer = new StaticServer('./config');
const avatarServer = new StaticServer('./config/avatars');
@ -350,19 +348,20 @@ export class ServerStream extends Streams.ObjectReadWriteStream<string> {
let server = staticServer;
if (req.url) {
if (req.url === '/custom.css') {
if (req.url === '/custom.css' || req.url.startsWith('/custom.css?')) {
server = cssServer;
} else if (req.url.startsWith('/avatars/')) {
req.url = req.url.substr(8);
req.url = req.url.slice(8);
server = avatarServer;
} else if (roomidRegex.test(req.url)) {
req.url = '/';
}
}
server.serve(req, res, e => {
if (e && (e as any).status === 404) {
staticServer.serveFile('404.html', 404, {}, req, res);
void server.serve(req, res, e => {
if (e.status === 404) {
void staticServer.serveFile('404.html', 404, {}, req, res);
return true;
}
});
});
@ -370,12 +369,8 @@ export class ServerStream extends Streams.ObjectReadWriteStream<string> {
this.server.on('request', staticRequestHandler);
if (this.serverSsl) this.serverSsl.on('request', staticRequestHandler);
} catch (e: any) {
if (e.message === 'disablenodestatic') {
console.log('node-static is disabled');
} else {
console.log('Could not start node-static - try `npm install` if you want to use it');
}
} catch {
console.log('Could not start static server');
}
// SockJS server