@vpalmisano/webrtcperf 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +296 -0
  3. package/app.min.js +2 -0
  4. package/build/src/app.d.ts +6 -0
  5. package/build/src/app.js +207 -0
  6. package/build/src/app.js.map +1 -0
  7. package/build/src/config.d.ts +104 -0
  8. package/build/src/config.js +880 -0
  9. package/build/src/config.js.map +1 -0
  10. package/build/src/generate-config-docs.d.ts +1 -0
  11. package/build/src/generate-config-docs.js +41 -0
  12. package/build/src/generate-config-docs.js.map +1 -0
  13. package/build/src/index.d.ts +9 -0
  14. package/build/src/index.js +26 -0
  15. package/build/src/index.js.map +1 -0
  16. package/build/src/media.d.ts +33 -0
  17. package/build/src/media.js +113 -0
  18. package/build/src/media.js.map +1 -0
  19. package/build/src/rtcstats.d.ts +302 -0
  20. package/build/src/rtcstats.js +418 -0
  21. package/build/src/rtcstats.js.map +1 -0
  22. package/build/src/server.d.ts +173 -0
  23. package/build/src/server.js +639 -0
  24. package/build/src/server.js.map +1 -0
  25. package/build/src/session.d.ts +277 -0
  26. package/build/src/session.js +1552 -0
  27. package/build/src/session.js.map +1 -0
  28. package/build/src/stats.d.ts +243 -0
  29. package/build/src/stats.js +1383 -0
  30. package/build/src/stats.js.map +1 -0
  31. package/build/src/utils.d.ts +249 -0
  32. package/build/src/utils.js +1220 -0
  33. package/build/src/utils.js.map +1 -0
  34. package/build/src/visqol.d.ts +6 -0
  35. package/build/src/visqol.js +61 -0
  36. package/build/src/visqol.js.map +1 -0
  37. package/build/src/vmaf.d.ts +83 -0
  38. package/build/src/vmaf.js +624 -0
  39. package/build/src/vmaf.js.map +1 -0
  40. package/build/tsconfig.tsbuildinfo +1 -0
  41. package/package.json +129 -0
  42. package/src/app.ts +241 -0
  43. package/src/config.ts +852 -0
  44. package/src/generate-config-docs.ts +47 -0
  45. package/src/index.ts +9 -0
  46. package/src/media.ts +151 -0
  47. package/src/rtcstats.ts +507 -0
  48. package/src/server.ts +645 -0
  49. package/src/session.ts +1908 -0
  50. package/src/stats.ts +1668 -0
  51. package/src/utils.ts +1295 -0
  52. package/src/visqol.ts +62 -0
  53. package/src/vmaf.ts +771 -0
@@ -0,0 +1,639 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.Server = void 0;
40
+ const compression_1 = __importDefault(require("compression"));
41
+ const crypto_1 = require("crypto");
42
+ const express_1 = __importStar(require("express"));
43
+ const fs_1 = __importDefault(require("fs"));
44
+ const http_1 = require("http");
45
+ const https_1 = require("https");
46
+ const os_1 = __importDefault(require("os"));
47
+ const path_1 = __importDefault(require("path"));
48
+ const tar_fs_1 = __importDefault(require("tar-fs"));
49
+ const ws_1 = require("ws");
50
+ const zlib_1 = __importDefault(require("zlib"));
51
+ const basic_auth_1 = __importDefault(require("basic-auth"));
52
+ const config_1 = require("./config");
53
+ const session_1 = require("./session");
54
+ const utils_1 = require("./utils");
55
+ const log = (0, utils_1.logger)('webrtcperf:server');
56
+ /**
57
+ * An HTTP server instance that allows to control the tool using a REST
58
+ * interface. Moreover, it allows to aggregate stats data coming from multiple
59
+ * running tool instances.
60
+ */
61
+ class Server {
62
+ /** The server listening port. */
63
+ serverPort;
64
+ /** The basic auth secret. */
65
+ serverSecret;
66
+ /** If HTTPS protocol should be used. */
67
+ serverUseHttps;
68
+ /** An optional path that the HTTP server will expose with the /data endpoint. */
69
+ serverData;
70
+ /** The file path that will be used to serve the \`/view/page.log\` requests. */
71
+ pageLogPath;
72
+ /** The path that will be used to serve the \`/cache\` requests. */
73
+ videoCachePath;
74
+ /** A {@link Stats} class instance. */
75
+ stats;
76
+ app;
77
+ server = null;
78
+ wss = null;
79
+ /**
80
+ * Server instance.
81
+ * All the HTTP endpoints are protected by basic authentication with user
82
+ * `admin` and password {@link Server.serverSecret}.
83
+ * @param serverPort The server listening port.
84
+ * @param serverSecret The basic auth secret.
85
+ * @param serverUseHttps If HTTPS protocol should be used.
86
+ * @param serverData An optional path that the HTTP server will expose with the /data endpoint.
87
+ * @param pageLogPath The file path that will be used to serve the \`/view/page.log\` requests.
88
+ * @param videoCachePath The path that will be used to serve the \`/cache\` requests.
89
+ * @param stats A {@link Stats} class instance.
90
+ */
91
+ constructor({ serverPort = 5000, serverSecret = 'secret', serverUseHttps = false, serverData = '', pageLogPath = '', videoCachePath = '', } = {}, stats) {
92
+ this.serverPort = serverPort;
93
+ this.serverSecret = serverSecret;
94
+ this.serverUseHttps = serverUseHttps;
95
+ this.serverData = serverData;
96
+ this.pageLogPath = pageLogPath;
97
+ this.videoCachePath = videoCachePath;
98
+ this.stats = stats;
99
+ //
100
+ this.app = (0, express_1.default)();
101
+ this.app.use((0, compression_1.default)());
102
+ this.app.use((0, express_1.json)({
103
+ limit: '10mb',
104
+ }));
105
+ this.app.use((req, res, next) => {
106
+ if (req.query.auth === this.serverSecret) {
107
+ return next();
108
+ }
109
+ const credentials = (0, basic_auth_1.default)(req);
110
+ if (!credentials || credentials.name !== 'admin' || credentials.pass !== this.serverSecret) {
111
+ res.setHeader('WWW-Authenticate', 'Basic realm="Restricted Area"');
112
+ res.status(401).send('Unauthorized');
113
+ return;
114
+ }
115
+ next();
116
+ });
117
+ this.app.get('/', (_req, res) => {
118
+ res.send('');
119
+ });
120
+ this.app.get('/stats', this.getStats.bind(this));
121
+ this.app.get('/collected-stats', this.getCollectedStats.bind(this));
122
+ this.app.get('/screenshot/:sessionId', this.getScreenshot.bind(this));
123
+ this.app.put('/collected-stats', this.putCollectedStats.bind(this));
124
+ this.app.put('/session', this.putSession.bind(this));
125
+ this.app.put('/sessions', this.putSessions.bind(this));
126
+ this.app.delete('/session', this.deleteSession.bind(this));
127
+ this.app.delete('/sessions', this.deleteSessions.bind(this));
128
+ this.app.get('/view/page.log', this.getPageLog.bind(this));
129
+ this.app.get('/view/docker.log', this.getDockerLog.bind(this));
130
+ this.app.get('/download/alert-rules', this.getAlertRules.bind(this));
131
+ this.app.get('/download/stats', this.getStatsFile.bind(this));
132
+ this.app.get('/download/detailed-stats', this.getDetailedStatsFile.bind(this));
133
+ this.app.get('/empty-page', this.getEmptyPage.bind(this));
134
+ if (this.serverData) {
135
+ log.debug(`using serverData: ${this.serverData}`);
136
+ fs_1.default.promises.mkdir(this.serverData, { recursive: true }).catch(err => {
137
+ log.error(`mkdir ${this.serverData} error: ${err.message}`);
138
+ });
139
+ this.app.get('/data', this.getDataArchive.bind(this));
140
+ this.app.get('/data/*', this.getData.bind(this));
141
+ }
142
+ if (this.videoCachePath) {
143
+ log.debug(`using videoCachePath: ${this.videoCachePath}`);
144
+ fs_1.default.promises.mkdir(this.videoCachePath, { recursive: true }).catch(err => {
145
+ log.error(`mkdir ${this.videoCachePath} error: ${err.message}`);
146
+ });
147
+ this.app.get('/cache/*', this.getCache.bind(this));
148
+ }
149
+ this.app.use((err, req, res, next) => {
150
+ log.error(`request path=${req.path} error:`, err.stack);
151
+ if (res.headersSent) {
152
+ return next(err);
153
+ }
154
+ res.status(500).send(err.message);
155
+ });
156
+ }
157
+ /*
158
+ * onConnection
159
+ * @param {Socket} socket
160
+ */
161
+ /* onConnection(socket) {
162
+ log.debug('onConnection', socket);
163
+
164
+ socket.on('disconnect', () => {
165
+ log.debug('io socket disconnected');
166
+ });
167
+
168
+ socket.on('message', (msg) => {
169
+ log.debug('message', msg);
170
+ });
171
+ } */
172
+ /**
173
+ * GET /stats endpoint.
174
+ *
175
+ * Returns a JSON array of the last statistics for each running Session.
176
+ */
177
+ async getStats(req, res, next) {
178
+ log.debug(`GET /stats`);
179
+ const stats = [];
180
+ try {
181
+ for (const session of this.stats.sessions.values()) {
182
+ stats.push(session.stats);
183
+ }
184
+ res.json(stats);
185
+ }
186
+ catch (err) {
187
+ next(err);
188
+ }
189
+ }
190
+ /**
191
+ * GET /download/stats endpoint.
192
+ *
193
+ * Returns the {@link Stats.statsWriter} file content.
194
+ */
195
+ getStatsFile(req, res, next) {
196
+ log.debug(`/download/stats`, req.query);
197
+ if (!this.stats.statsWriter) {
198
+ return next(new Error('statsPath not set'));
199
+ }
200
+ res.download(this.stats.statsPath);
201
+ }
202
+ /**
203
+ * GET /download/detailed-stats endpoint.
204
+ *
205
+ * Returns the {@link Stats.detailedStatsWriter} file content.
206
+ */
207
+ getDetailedStatsFile(req, res, next) {
208
+ log.debug(`/download/detailed-stats`, req.query);
209
+ if (!this.stats.detailedStatsWriter) {
210
+ return next(new Error('detailedStatsPath not set'));
211
+ }
212
+ res.download(this.stats.detailedStatsPath);
213
+ }
214
+ /**
215
+ * GET /collected-stats endpoint.
216
+ *
217
+ * Returns a JSON array of the last statistics collected from external running
218
+ * tools.
219
+ */
220
+ getCollectedStats(req, res, next) {
221
+ log.debug(`GET /collected-stats`);
222
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
223
+ const stats = {};
224
+ try {
225
+ for (const [key, stat] of Object.entries(this.stats.collectedStats)) {
226
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
227
+ stats[key] = stat.data;
228
+ }
229
+ res.json(stats);
230
+ }
231
+ catch (err) {
232
+ next(err);
233
+ }
234
+ }
235
+ /**
236
+ * GET /screenshot/<sessionID> endpoint.
237
+ *
238
+ * Returns the page screenshot running inside the {@link Session} identified
239
+ * by `sessionID`.
240
+ * Additional query params:
241
+ * - `page`: the page number (starting from `0`) running inside the {@link Session}.
242
+ * - `format`: the image format (`jpeg`, `png`, `webp`). Default: `webp`.
243
+ */
244
+ async getScreenshot(req, res, next) {
245
+ const sessionId = parseInt(req.params.sessionId);
246
+ const pageId = parseInt(req.query.page || '0');
247
+ const format = req.query.format || 'webp';
248
+ log.debug(`GET /screenshot/${sessionId} page=${pageId} format=${format}`);
249
+ try {
250
+ const session = this.stats.sessions.get(sessionId);
251
+ if (!session) {
252
+ throw new Error(`Session not found: "${sessionId}"`);
253
+ }
254
+ const filePath = await session.pageScreenshot(pageId, format);
255
+ res.sendFile(path_1.default.resolve(filePath));
256
+ }
257
+ catch (err) {
258
+ next(err);
259
+ }
260
+ }
261
+ /**
262
+ * PUT /collected-stats endpoint.
263
+ *
264
+ * Allows to inject {@link Stats} metrics coming from an external tool.
265
+ */
266
+ putCollectedStats(req, res, next) {
267
+ log.debug(`PUT /collected-stats`);
268
+ const { id, stats, config } = req.body;
269
+ try {
270
+ this.stats.addExternalCollectedStats(id, stats, config);
271
+ res.json({
272
+ message: `Collected stats added`,
273
+ });
274
+ }
275
+ catch (err) {
276
+ next(err);
277
+ }
278
+ }
279
+ /**
280
+ * PUT /session endpoint.
281
+ *
282
+ * Starts a new {@link Session}.
283
+ * The request body format will be parsed as a {@link SessionParams} object.
284
+ */
285
+ async putSession(req, res, next) {
286
+ log.debug(`PUT /session`, req.body);
287
+ try {
288
+ const id = this.stats.consumeSessionId();
289
+ await this.startLocalSession(id, req.body);
290
+ res.json({
291
+ message: `Session created`,
292
+ data: { id },
293
+ });
294
+ }
295
+ catch (err) {
296
+ next(err);
297
+ }
298
+ }
299
+ /**
300
+ * PUT /sessions endpoint.
301
+ *
302
+ * Starts multiple {@link Session} instances as specified into the
303
+ * `body.sessions` value.
304
+ * The request body will be parsed as a {@link SessionParams} object.
305
+ */
306
+ async putSessions(req, res, next) {
307
+ log.debug(`PUT /sessions`, req.body);
308
+ try {
309
+ const { sessions } = req.body;
310
+ const sessionsIds = [];
311
+ for (let i = 0; i < sessions; i++) {
312
+ const id = this.stats.sessions.size;
313
+ await this.startLocalSession(id, req.body);
314
+ sessionsIds.push(id);
315
+ }
316
+ res.json({
317
+ message: `${sessions} sessions created`,
318
+ data: { ids: sessionsIds },
319
+ });
320
+ }
321
+ catch (err) {
322
+ next(err);
323
+ }
324
+ }
325
+ /**
326
+ * DELETE /session endpoint.
327
+ *
328
+ * Delete the {@link Session} instance identified by the `body.id` param.
329
+ */
330
+ async deleteSession(req, res, next) {
331
+ log.debug(`DELETE /session`, req.body);
332
+ try {
333
+ const { id } = req.body;
334
+ await this.stopLocalSession(id);
335
+ res.json({
336
+ message: `Session deleted`,
337
+ data: { id },
338
+ });
339
+ }
340
+ catch (err) {
341
+ next(err);
342
+ }
343
+ }
344
+ /**
345
+ * DELETE /sessions endpoint.
346
+ *
347
+ * Delete the {@link Session} instances specified by the `body.ids` array.
348
+ */
349
+ async deleteSessions(req, res, next) {
350
+ log.debug(`DELETE /sessions`, req.body);
351
+ try {
352
+ const { ids } = req.body;
353
+ for (const id of ids) {
354
+ await this.stopLocalSession(id);
355
+ }
356
+ res.json({
357
+ message: `${ids.length} sessions deleted`,
358
+ data: { ids },
359
+ });
360
+ }
361
+ catch (err) {
362
+ next(err);
363
+ }
364
+ }
365
+ /**
366
+ * GET /view/page.log endpoint.
367
+ *
368
+ * Returns the page log file content as specified in {@link Config} `pageLogPath`.
369
+ */
370
+ getPageLog(req, res, next) {
371
+ log.debug(`GET /view/page.log`, req.query);
372
+ if (!this.pageLogPath) {
373
+ return next(new Error('pageLogPath not set'));
374
+ }
375
+ if (req.query.range && !req.headers.range) {
376
+ req.headers.range = `bytes=${req.query.range}`;
377
+ }
378
+ res.sendFile(path_1.default.resolve(this.pageLogPath));
379
+ }
380
+ /**
381
+ * GET /view/docker.log endpoint.
382
+ *
383
+ * Returns the Docker logs related to the container running the tool.
384
+ * It requires to run the Docker container with the following options:
385
+ * ```
386
+ --cidfile /tmp/docker.id
387
+ -v /tmp/docker.id:/root/.webrtcperf/docker.id:ro
388
+ -v /var/lib/docker:/var/lib/docker:ro
389
+ * ```
390
+ */
391
+ async getDockerLog(req, res, next) {
392
+ log.debug(`GET /view/docker.log`, req.query);
393
+ try {
394
+ const logPath = await (0, utils_1.getDockerLogsPath)();
395
+ if (req.query.range && !req.headers.range) {
396
+ req.headers.range = `bytes=${req.query.range}`;
397
+ }
398
+ res.sendFile(path_1.default.resolve(logPath));
399
+ }
400
+ catch (err) {
401
+ next(err);
402
+ }
403
+ }
404
+ /**
405
+ * GET /download/alert-rules endpoint.
406
+ *
407
+ * Downloads the alert rules report stored into the {@link Stats.alertRulesFilename}.
408
+ */
409
+ getAlertRules(req, res, next) {
410
+ log.debug(`GET /download/alert-rules`, req.query);
411
+ if (!this.stats.alertRulesFilename) {
412
+ return next(new Error('Stats alertRulesFilename not set'));
413
+ }
414
+ res.download(this.stats.alertRulesFilename);
415
+ }
416
+ /**
417
+ * GET /empty-page endpoint.
418
+ *
419
+ * Returns an empty HTML page. Useful for running tests with raw Javascript
420
+ * content without any DOM rendering.
421
+ */
422
+ getEmptyPage(req, res) {
423
+ log.debug(`GET /empty-page`, req.query);
424
+ const title = req.query.title || 'EmptyPage';
425
+ res.send(`<html lang="en">
426
+ <head>
427
+ <meta charset="UTF-8">
428
+ <meta name="viewport" content="width=device-width, initial-scale=1">
429
+ <title>${title}</title>
430
+ </head>
431
+ <body></body>
432
+ </html>`);
433
+ }
434
+ /**
435
+ * GET /data/* endpoint.
436
+ *
437
+ * Returns the file content relative to the {@link Config} `serverData` path.
438
+ * If the requested path points to a directory, it returns the directory
439
+ * content in tar.gz format.
440
+ */
441
+ getData(req, res, next) {
442
+ const paramPath = path_1.default.normalize(req.params[0]).replace(/^(\.\.(\/|\\|$))+/, '');
443
+ log.debug(`GET /data/${paramPath}`, req.query);
444
+ const fpath = path_1.default.resolve(this.serverData, paramPath);
445
+ if (!fs_1.default.existsSync(fpath)) {
446
+ return next(new Error(`${paramPath} not found`));
447
+ }
448
+ if (req.query.range && !req.headers.range) {
449
+ req.headers.range = `bytes=${req.query.range}`;
450
+ }
451
+ res.sendFile(fpath);
452
+ }
453
+ getDataArchive(req, res, next) {
454
+ log.debug(`GET /data`, req.query);
455
+ const fpath = path_1.default.resolve(this.serverData);
456
+ if (!fs_1.default.lstatSync(fpath).isDirectory()) {
457
+ return next(new Error(`${fpath} is not a directory`));
458
+ }
459
+ res.header('Content-Disposition', `attachment; filename="${path_1.default.basename(fpath)}.tar.gz"`);
460
+ res.setHeader('content-type', 'application/gzip');
461
+ tar_fs_1.default.pack(fpath).pipe(zlib_1.default.createGzip()).pipe(res);
462
+ }
463
+ getCache(req, res, next) {
464
+ const paramPath = path_1.default.normalize(req.params[0]).replace(/^(\.\.(\/|\\|$))+/, '');
465
+ log.debug(`GET /cache/${paramPath}`, req.query);
466
+ const fpath = path_1.default.resolve(this.videoCachePath, paramPath);
467
+ if (!fs_1.default.existsSync(fpath)) {
468
+ return next(new Error(`${paramPath} not found`));
469
+ }
470
+ if (req.query.range && !req.headers.range) {
471
+ req.headers.range = `bytes=${req.query.range}`;
472
+ }
473
+ res.sendFile(fpath);
474
+ }
475
+ /**
476
+ * Starts a new {@link Session} instance.
477
+ * @param id The session unique id.
478
+ * @param config The session configuration.
479
+ */
480
+ async startLocalSession(id, config) {
481
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
482
+ const sessionConfig = (0, config_1.loadConfig)(undefined, config);
483
+ const session = new session_1.Session({ ...sessionConfig, id });
484
+ session.once('stop', () => {
485
+ console.warn(`Session ${id} stopped, reloading...`);
486
+ setTimeout(this.startLocalSession.bind(this), sessionConfig.spawnPeriod, id, config);
487
+ });
488
+ this.stats.addSession(session);
489
+ try {
490
+ await session.start();
491
+ }
492
+ catch (err) {
493
+ this.stats.removeSession(session.id);
494
+ throw err;
495
+ }
496
+ return session;
497
+ }
498
+ /**
499
+ * Stops a new {@link Session} instance.
500
+ * @param {number} id The session unique id.
501
+ */
502
+ async stopLocalSession(id) {
503
+ const session = this.stats.sessions.get(id);
504
+ if (!session) {
505
+ log.warn(`stopLocalSession session ${id} not found`);
506
+ return;
507
+ }
508
+ session.removeAllListeners();
509
+ this.stats.removeSession(id);
510
+ await session.stop();
511
+ }
512
+ /**
513
+ * Starts the {@link Server} instance.
514
+ */
515
+ async start() {
516
+ log.debug('start');
517
+ if (this.serverUseHttps) {
518
+ const destDir = path_1.default.join(os_1.default.homedir(), '.webrtcperf/ssl');
519
+ await (0, utils_1.runShellCommand)(`mkdir -p ${destDir} && openssl req -newkey rsa:2048 -nodes -keyout ${destDir}/domain.key -x509 -days 365 -out ${destDir}/domain.crt -subj "/C=EU/ST=London/L=London/O=Global Security/OU=IT Department/CN=example.com"`);
520
+ this.server = (0, https_1.createServer)({
521
+ key: fs_1.default.readFileSync(`${destDir}/domain.key`),
522
+ cert: fs_1.default.readFileSync(`${destDir}/domain.crt`),
523
+ }, this.app);
524
+ }
525
+ else {
526
+ this.server = (0, http_1.createServer)(this.app);
527
+ }
528
+ // WebSocket endpoint.
529
+ const wss = new ws_1.WebSocketServer({ noServer: true });
530
+ wss.on('connection', (ws, request) => {
531
+ try {
532
+ const query = new URLSearchParams(request.url?.split('?')[1] || '');
533
+ const action = query.get('action') || '';
534
+ log.debug(`ws connection from ${request.socket.remoteAddress} action: ${action}`);
535
+ switch (action) {
536
+ case 'write-stream': {
537
+ if (!this.serverData) {
538
+ throw new Error('serverData option not set');
539
+ }
540
+ const filename = query.get('filename') || '';
541
+ if (!filename) {
542
+ throw new Error('filename not set');
543
+ }
544
+ const paramPath = path_1.default.normalize(filename).replace(/^(\.\.(\/|\\|$))+/, '');
545
+ log.debug(`ws write-stream ${paramPath}`);
546
+ const fpath = path_1.default.resolve(this.serverData, paramPath);
547
+ if (fs_1.default.existsSync(fpath)) {
548
+ throw new Error(`file already exists: ${fpath}`);
549
+ }
550
+ const stream = fs_1.default.createWriteStream(fpath);
551
+ let headerWritten = false;
552
+ let framesWritten = 0;
553
+ const close = async () => {
554
+ stream.close();
555
+ ws.close();
556
+ try {
557
+ if (!framesWritten) {
558
+ await fs_1.default.promises.unlink(fpath);
559
+ }
560
+ }
561
+ catch (err) {
562
+ log.error(`ws write-stream close error: ${err.message}`);
563
+ }
564
+ };
565
+ stream.on('error', (err) => {
566
+ log.error(`ws write-stream error: ${err.message}`);
567
+ void close();
568
+ });
569
+ ws.on('error', (err) => {
570
+ log.error(`ws write-stream error: ${err.message}`);
571
+ void close();
572
+ });
573
+ ws.on('close', () => {
574
+ log.debug(`ws write-stream close`);
575
+ void close();
576
+ });
577
+ ws.on('message', (data) => {
578
+ if (!data?.byteLength)
579
+ return;
580
+ if (!headerWritten) {
581
+ stream.write(data);
582
+ headerWritten = true;
583
+ return;
584
+ }
585
+ stream.write(data);
586
+ framesWritten++;
587
+ });
588
+ break;
589
+ }
590
+ default:
591
+ throw new Error(`invalid action: ${action}`);
592
+ }
593
+ }
594
+ catch (err) {
595
+ log.error(`ws connection error: ${err.message}`);
596
+ ws.close();
597
+ }
598
+ });
599
+ this.wss = wss;
600
+ this.server.on('upgrade', (request, socket, head) => {
601
+ log.debug(`ws upgrade ${request.url}`);
602
+ try {
603
+ const query = new URLSearchParams(request.url?.split('?')[1] || '');
604
+ const auth = query.get('auth');
605
+ if (!auth || !(0, crypto_1.timingSafeEqual)(Buffer.from(auth), Buffer.from(this.serverSecret))) {
606
+ throw new Error('invalid auth');
607
+ }
608
+ }
609
+ catch (err) {
610
+ log.error(`ws upgrade error: ${err.message}`);
611
+ socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
612
+ socket.destroy();
613
+ return;
614
+ }
615
+ wss.handleUpgrade(request, socket, head, ws => {
616
+ wss.emit('connection', ws, request);
617
+ });
618
+ });
619
+ this.server.listen(this.serverPort, () => {
620
+ log.debug(`HTTPS server listening on port ${this.serverPort}`);
621
+ });
622
+ }
623
+ /**
624
+ * Stops the {@link Server} instance.
625
+ */
626
+ stop() {
627
+ if (this.wss) {
628
+ this.wss.close();
629
+ this.wss = null;
630
+ }
631
+ if (this.server) {
632
+ log.debug('stop');
633
+ this.server.close();
634
+ this.server = null;
635
+ }
636
+ }
637
+ }
638
+ exports.Server = Server;
639
+ //# sourceMappingURL=server.js.map