-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathserver.mjs
More file actions
163 lines (146 loc) · 4.81 KB
/
Copy pathserver.mjs
File metadata and controls
163 lines (146 loc) · 4.81 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import url from "node:url";
import { createRequestHandler } from "@react-router/express";
import compression from "compression";
import express from "express";
import morgan from "morgan";
import sourceMapSupport from "source-map-support";
import getPort from "get-port";
process.env.NODE_ENV = process.env.NODE_ENV ?? "production";
// Last-resort safety net: a single unhandled promise rejection (e.g. a Mongo
// "write conflict" from concurrent pool.update during a Baileys reconnect) used
// to take down the WHOLE app and crash-loop it out of restarts. A web server
// must never die from one stray async error — log it loudly and keep serving.
// Targeted fixes still belong at the source; this only prevents total outages.
process.on("unhandledRejection", (reason) => {
console.error("[unhandledRejection]", reason instanceof Error ? reason.stack : reason);
});
process.on("uncaughtException", (err) => {
console.error("[uncaughtException]", err instanceof Error ? err.stack : err);
});
sourceMapSupport.install({
retrieveSourceMap(source) {
if (source.startsWith("file://")) {
const filePath = url.fileURLToPath(source);
const sourceMapPath = `${filePath}.map`;
if (fs.existsSync(sourceMapPath)) {
return { url: source, map: fs.readFileSync(sourceMapPath, "utf8") };
}
}
return null;
},
});
// Paths commonly probed by vulnerability scanners and bots.
// Requests matching these are rejected early with a 404 to avoid
// polluting logs with "No route matches URL" errors from React Router.
const BOT_PROBE_PATTERNS = [
/^\/__debug/,
/^\/__cve/,
/^\/wp-/,
/\/wp-includes\//,
/\/wp-content\//,
/\/wp-json\//,
/wlwmanifest\.xml/,
/^\/wordpress/i,
/^\/\.env/,
/^\/\.git/,
/^\/phpmyadmin/i,
/^\/admin\/?$/,
/^\/administrator/i,
/^\/xmlrpc\.php/,
/^\/wp-login/,
/^\/wp-admin/,
/^\/cgi-bin/,
/^\/vendor/,
/^\/telescope/,
/^\/config\.(json|yml|yaml|php|bak)/,
/^\/backup/i,
/^\/debug/,
/^\/console/,
/^\/solr/,
/^\/actuator/,
/^\/_ignition/,
// Extensiones que ninguna ruta de la app sirve — puro scanner. El `/1.php`,
// `/index.php`, `/shell.php` que ahogaron el CPU en el outage 2026-07-09 caían a
// React Router (404 con stack trace + symbolication = 600-1200ms c/u). Cortarlas
// aquí = 404 seco O(1) antes de morgan/RR.
/\.php(\?|$)/i,
/\.(asp|aspx|jsp|cgi)(\?|$)/i,
/\.(bak|old|sql|zip|tar|gz|env)(\?|$)/i,
];
async function run() {
const port =
parseNumber(process.env.PORT) ?? (await getPort({ port: 3000 }));
const buildPathArg = process.argv[2];
if (!buildPathArg) {
console.error(
"\n Usage: node server.mjs <server-build-path> - e.g. node server.mjs build/server/index.js"
);
process.exit(1);
}
const buildPath = path.resolve(buildPathArg);
const build = await import(url.pathToFileURL(buildPath).href);
const app = express();
app.disable("x-powered-by");
app.use(compression());
// Static assets
app.use(
path.posix.join(build.publicPath, "assets"),
express.static(path.join(build.assetsBuildDirectory, "assets"), {
immutable: true,
maxAge: "1y",
})
);
app.use(build.publicPath, express.static(build.assetsBuildDirectory));
app.use(express.static("public", { maxAge: "1h" }));
// Block bot/scanner probes before logging or React Router
app.use((req, res, next) => {
if (BOT_PROBE_PATTERNS.some((p) => p.test(req.path))) {
res.status(404).end();
return;
}
next();
});
// CORS preflight for public API endpoints
app.options("/api/v2/public/*", (req, res) => {
res.set({
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "POST, GET, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
}).status(204).end();
});
// Logging (after bot filter so probes don't pollute logs)
app.use(morgan("tiny"));
// React Router handler
app.all("*", createRequestHandler({ build, mode: process.env.NODE_ENV }));
const onListen = () => {
const address =
process.env.HOST ||
Object.values(os.networkInterfaces())
.flat()
.find((ip) => String(ip?.family).includes("4") && !ip?.internal)
?.address;
if (!address) {
console.log(`[easybits] http://localhost:${port}`);
} else {
console.log(
`[easybits] http://localhost:${port} (http://${address}:${port})`
);
}
};
const server = process.env.HOST
? app.listen(port, process.env.HOST, onListen)
: app.listen(port, onListen);
["SIGTERM", "SIGINT"].forEach((signal) => {
process.once(signal, () => server?.close(console.error));
});
}
function parseNumber(raw) {
if (raw === undefined) return undefined;
const maybe = Number(raw);
if (Number.isNaN(maybe)) return maybe;
return maybe;
}
run();