Repository navigation
Expand file tree
/
Copy pathserver.js
More file actions
179 lines (157 loc) · 6.67 KB
/
Copy pathserver.js
File metadata and controls
179 lines (157 loc) · 6.67 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
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
#!/usr/bin/env node
'use strict';
const http = require('node:http');
const path = require('node:path');
const crypto = require('node:crypto');
const express = require('express');
const config = require('./src/config');
const store = require('./src/store');
const runtime = require('./src/runtime');
const qr = require('./src/qr');
const mdns = require('./src/mdns');
const { getShareBase } = require('./src/baseUrl');
const { createHub } = require('./src/ws-hub');
const createFilesRouter = require('./src/routes/files');
const createNotesRouter = require('./src/routes/notes');
const createAdminRouter = require('./src/routes/admin');
const createUpdateRouter = require('./src/routes/update');
const { requestGuard } = require('./src/request-guard');
const SWEEP_INTERVAL_MS = 60 * 1000;
const IDLE_TIMEOUT_MS = 2 * 60 * 1000;
/**
* "Any size" means an upload may take as long as it needs. Node's default
* requestTimeout (5 minutes) cut off large videos sent from phones over Wi-Fi
* with a 408. Instead, drop a connection only when it goes silent for two
* minutes; headers must still arrive within the default headersTimeout.
*/
function tuneServer(server) {
server.requestTimeout = 0;
server.timeout = IDLE_TIMEOUT_MS;
return server;
}
/**
* Build the Express app. `hub` only needs a `.broadcast(type, payload)` method,
* so tests can pass a stub instead of a real WebSocket hub.
* Returns { app, files } — `files` exposes purge helpers for the expiry sweep.
*/
function buildApp(hub) {
const app = express();
app.disable('x-powered-by');
app.enable('trust proxy'); // correct req.protocol/host behind a reverse proxy / VPN
// First, before any route or static file: refuse DNS rebinding everywhere and
// cross-site writes (CSRF). See src/request-guard.js.
app.use(requestGuard);
app.use(express.json({ limit: '16kb' }));
const files = createFilesRouter(hub);
const notes = createNotesRouter(hub);
const admin = createAdminRouter({ hub, files });
const update = createUpdateRouter();
app.use(files.router);
app.use(notes.router);
app.use(admin.router);
app.use(update.router);
app.get('/api/health', (req, res) => res.json({ ok: true, version: config.version }));
// Host console page. no-store so the browser never serves a stale console.
app.get('/admin', (req, res) => {
res.setHeader('Cache-Control', 'no-store');
res.sendFile(path.join(config.publicDir, 'admin.html'));
});
// Static dashboard. express.static never lists directories, and uploads/ is
// never served here — files leave only through GET /d/:token.
// `no-store` = never cache the UI assets, so the browser can't serve a stale
// dashboard/console (this was the real cause of the "Host only" ghost page).
app.use(express.static(config.publicDir, {
index: 'index.html',
setHeaders: (res) => res.setHeader('Cache-Control', 'no-store'),
}));
// Upload-size + fallback error handler (must be last).
app.use((err, req, res, next) => {
if (err && err.code === 'LIMIT_FILE_SIZE') {
return res.status(413).json({ error: `File too large (max ${config.maxUploadMb} MB)` });
}
console.error(err);
if (res.headersSent) return next(err);
res.status(500).json({ error: 'Server error' });
});
return { app, files };
}
function printBanner() {
const shareUrl = getShareBase();
console.log('');
console.log(' ┌─────────────────────────────────────────────');
console.log(' │ MeghXL is running');
console.log(' │');
console.log(` │ On this computer: http://localhost:${config.port}`);
console.log(` │ On your network: http://${config.lanIp}:${config.port}`);
if (config.mdnsEnabled) {
console.log(` │ Friendly name: http://${config.mdnsName}.local:${config.port}`);
}
if (config.publicBaseUrl) {
console.log(` │ Public URL: ${config.publicBaseUrl}`);
}
console.log(' │');
const cap = config.maxUploadMb ? `${config.maxUploadMb} MB` : 'unlimited';
console.log(` │ Max upload ${cap} · files in ${config.uploadDir}`);
console.log(' └─────────────────────────────────────────────');
console.log('');
console.log(` Host console: http://localhost:${config.port}/admin (opens automatically on this PC)`);
if (runtime.adminKey) console.log(` remote admin key: ${runtime.adminKey} (enter it on the console's unlock form)`);
console.log('');
console.log(' Scan to open the dashboard on your phone:');
console.log('');
return qr.printTerminal(shareUrl).then(() => {
console.log(' Same Wi-Fi required. LAN-only by design — for remote access, put it');
console.log(' behind your own VPN / reverse proxy and set PUBLIC_BASE_URL.');
console.log(' Stop with Ctrl+C.\n');
});
}
function main() {
store.load();
let key = process.env.ADMIN_KEY || store.getAdminKey();
if (!key) {
key = crypto.randomBytes(6).toString('hex');
store.setAdminKey(key);
store.flushSync();
}
runtime.adminKey = key;
const server = tuneServer(http.createServer());
const hub = createHub(server);
const { app, files } = buildApp(hub);
server.on('request', app);
const sweep = setInterval(() => {
const n = files.purgeExpired();
if (n) console.log(`[sweep] removed ${n} expired file(s)`);
}, SWEEP_INTERVAL_MS);
if (sweep.unref) sweep.unref();
server.on('error', (err) => {
if (err.code === 'EADDRINUSE') {
console.error(`\n Port ${config.port} is already in use. Try: PORT=8080 npm start\n`);
process.exit(1);
}
throw err;
});
server.listen(config.port, config.host, () => {
if (config.mdnsEnabled) mdns.start({ name: config.mdnsName, port: config.port });
printBanner();
});
const shutdown = () => {
console.log('\n Shutting down…');
store.flushSync();
mdns.stop();
server.close(() => process.exit(0));
setTimeout(() => process.exit(0), 1000).unref();
};
process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);
// Launched as the desktop app's sidecar: stdin is a pipe from the app, so
// EOF means the app is gone — shut down with it. Without this, force-quitting
// the app orphans this process; it keeps port 3000, and every later launch
// attaches to it and silently serves the old build. Opt-in so the CLI, whose
// stdin is a terminal, is unaffected.
if (process.env.MEGHXL_EXIT_WITH_PARENT === '1') {
process.stdin.resume();
for (const ev of ['end', 'close', 'error']) process.stdin.on(ev, shutdown);
}
}
if (require.main === module) main();
module.exports = { buildApp, tuneServer };