wslog-proxy 0.0.0-stage → 0.1.1

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 (109) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +110 -2
  3. package/dist/cli/args.d.ts +14 -0
  4. package/dist/cli/args.js +98 -0
  5. package/dist/cli/args.js.map +1 -0
  6. package/dist/cli/index.d.ts +2 -0
  7. package/dist/cli/index.js +78 -0
  8. package/dist/cli/index.js.map +1 -0
  9. package/dist/index.d.ts +12 -0
  10. package/dist/index.js +10 -0
  11. package/dist/index.js.map +1 -0
  12. package/dist/server/app.d.ts +19 -0
  13. package/dist/server/app.js +96 -0
  14. package/dist/server/app.js.map +1 -0
  15. package/dist/server/config.d.ts +51 -0
  16. package/dist/server/config.js +62 -0
  17. package/dist/server/config.js.map +1 -0
  18. package/dist/server/connections.d.ts +21 -0
  19. package/dist/server/connections.js +81 -0
  20. package/dist/server/connections.js.map +1 -0
  21. package/dist/server/context.d.ts +37 -0
  22. package/dist/server/context.js +64 -0
  23. package/dist/server/context.js.map +1 -0
  24. package/dist/server/demo/bots.d.ts +4 -0
  25. package/dist/server/demo/bots.js +90 -0
  26. package/dist/server/demo/bots.js.map +1 -0
  27. package/dist/server/demo/chat-server.d.ts +9 -0
  28. package/dist/server/demo/chat-server.js +90 -0
  29. package/dist/server/demo/chat-server.js.map +1 -0
  30. package/dist/server/demo/controller.d.ts +21 -0
  31. package/dist/server/demo/controller.js +37 -0
  32. package/dist/server/demo/controller.js.map +1 -0
  33. package/dist/server/events.d.ts +19 -0
  34. package/dist/server/events.js +19 -0
  35. package/dist/server/events.js.map +1 -0
  36. package/dist/server/http/api.d.ts +6 -0
  37. package/dist/server/http/api.js +89 -0
  38. package/dist/server/http/api.js.map +1 -0
  39. package/dist/server/http/auth.d.ts +26 -0
  40. package/dist/server/http/auth.js +78 -0
  41. package/dist/server/http/auth.js.map +1 -0
  42. package/dist/server/http/live.d.ts +26 -0
  43. package/dist/server/http/live.js +117 -0
  44. package/dist/server/http/live.js.map +1 -0
  45. package/dist/server/http/router.d.ts +31 -0
  46. package/dist/server/http/router.js +89 -0
  47. package/dist/server/http/router.js.map +1 -0
  48. package/dist/server/http/server.d.ts +17 -0
  49. package/dist/server/http/server.js +82 -0
  50. package/dist/server/http/server.js.map +1 -0
  51. package/dist/server/http/static.d.ts +3 -0
  52. package/dist/server/http/static.js +56 -0
  53. package/dist/server/http/static.js.map +1 -0
  54. package/dist/server/http/upgrade.d.ts +3 -0
  55. package/dist/server/http/upgrade.js +13 -0
  56. package/dist/server/http/upgrade.js.map +1 -0
  57. package/dist/server/masking.d.ts +11 -0
  58. package/dist/server/masking.js +104 -0
  59. package/dist/server/masking.js.map +1 -0
  60. package/dist/server/paths.d.ts +12 -0
  61. package/dist/server/paths.js +30 -0
  62. package/dist/server/paths.js.map +1 -0
  63. package/dist/server/plugins.d.ts +20 -0
  64. package/dist/server/plugins.js +4 -0
  65. package/dist/server/plugins.js.map +1 -0
  66. package/dist/server/proxy/headers.d.ts +4 -0
  67. package/dist/server/proxy/headers.js +39 -0
  68. package/dist/server/proxy/headers.js.map +1 -0
  69. package/dist/server/proxy/proxy.d.ts +41 -0
  70. package/dist/server/proxy/proxy.js +188 -0
  71. package/dist/server/proxy/proxy.js.map +1 -0
  72. package/dist/server/recorder.d.ts +34 -0
  73. package/dist/server/recorder.js +65 -0
  74. package/dist/server/recorder.js.map +1 -0
  75. package/dist/server/storage/index.d.ts +14 -0
  76. package/dist/server/storage/index.js +40 -0
  77. package/dist/server/storage/index.js.map +1 -0
  78. package/dist/server/storage/jsonl.d.ts +30 -0
  79. package/dist/server/storage/jsonl.js +107 -0
  80. package/dist/server/storage/jsonl.js.map +1 -0
  81. package/dist/server/storage/memory.d.ts +24 -0
  82. package/dist/server/storage/memory.js +74 -0
  83. package/dist/server/storage/memory.js.map +1 -0
  84. package/dist/server/storage/sqlite.d.ts +23 -0
  85. package/dist/server/storage/sqlite.js +125 -0
  86. package/dist/server/storage/sqlite.js.map +1 -0
  87. package/dist/server/storage/types.d.ts +30 -0
  88. package/dist/server/storage/types.js +2 -0
  89. package/dist/server/storage/types.js.map +1 -0
  90. package/dist/server/targets.d.ts +41 -0
  91. package/dist/server/targets.js +156 -0
  92. package/dist/server/targets.js.map +1 -0
  93. package/dist/server/util/logger.d.ts +16 -0
  94. package/dist/server/util/logger.js +19 -0
  95. package/dist/server/util/logger.js.map +1 -0
  96. package/dist/shared/filter.d.ts +14 -0
  97. package/dist/shared/filter.js +41 -0
  98. package/dist/shared/filter.js.map +1 -0
  99. package/dist/shared/index.d.ts +2 -0
  100. package/dist/shared/index.js +3 -0
  101. package/dist/shared/index.js.map +1 -0
  102. package/dist/shared/protocol.d.ts +124 -0
  103. package/dist/shared/protocol.js +9 -0
  104. package/dist/shared/protocol.js.map +1 -0
  105. package/dist/web/assets/index-C3OwozRd.css +1 -0
  106. package/dist/web/assets/index-D2oUxHhH.js +11 -0
  107. package/dist/web/favicon.svg +1 -0
  108. package/dist/web/index.html +15 -0
  109. package/package.json +84 -4
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.js","sourceRoot":"","sources":["../../../src/server/http/api.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ import type { IncomingMessage } from 'node:http';
2
+ import type { Role } from '../../shared/protocol.js';
3
+ import { type WslogConfig } from '../config.js';
4
+ export declare class HttpError extends Error {
5
+ readonly status: number;
6
+ constructor(status: number, message: string);
7
+ }
8
+ /**
9
+ * Who is calling?
10
+ * - the share token → viewer (read-only)
11
+ * - the admin token → admin
12
+ * - no token, from loopback → admin (the person running wslog), but only
13
+ * when the Host header is a local name too. Otherwise a website that
14
+ * rebinds its DNS to 127.0.0.1 would be admin when bound to 0.0.0.0.
15
+ * - anything else → rejected
16
+ */
17
+ export declare function resolveRole(req: IncomingMessage, url: URL, config: WslogConfig): Role | null;
18
+ /**
19
+ * Browser-safety checks for the dashboard and API (not the proxy endpoint):
20
+ * - Origin must match Host, so other websites cannot read traffic through
21
+ * the live WebSocket or call the API (cross-site WebSocket hijacking).
22
+ * `Origin: null` (sandboxed iframes, local files) is blocked too: the
23
+ * dashboard never sends it.
24
+ * - When bound to loopback, Host must be a local name (DNS rebinding).
25
+ */
26
+ export declare function checkBrowserSafety(req: IncomingMessage, config: WslogConfig): void;
@@ -0,0 +1,78 @@
1
+ import { timingSafeEqual } from 'node:crypto';
2
+ import { isLoopbackHost } from '../config.js';
3
+ export class HttpError extends Error {
4
+ status;
5
+ constructor(status, message) {
6
+ super(message);
7
+ this.status = status;
8
+ }
9
+ }
10
+ /**
11
+ * Who is calling?
12
+ * - the share token → viewer (read-only)
13
+ * - the admin token → admin
14
+ * - no token, from loopback → admin (the person running wslog), but only
15
+ * when the Host header is a local name too. Otherwise a website that
16
+ * rebinds its DNS to 127.0.0.1 would be admin when bound to 0.0.0.0.
17
+ * - anything else → rejected
18
+ */
19
+ export function resolveRole(req, url, config) {
20
+ const token = readToken(req, url);
21
+ if (token) {
22
+ if (safeEqual(token, config.adminToken))
23
+ return 'admin';
24
+ if (safeEqual(token, config.shareToken))
25
+ return 'viewer';
26
+ return null;
27
+ }
28
+ const fromLoopback = isLoopbackHost(normalizeAddress(req.socket.remoteAddress ?? ''));
29
+ return fromLoopback && isLocalHostHeader(req.headers.host ?? '') ? 'admin' : null;
30
+ }
31
+ function readToken(req, url) {
32
+ const auth = req.headers.authorization;
33
+ if (auth?.startsWith('Bearer '))
34
+ return auth.slice(7).trim();
35
+ const header = req.headers['x-wslog-token'];
36
+ if (typeof header === 'string' && header)
37
+ return header;
38
+ return url.searchParams.get('token') ?? undefined;
39
+ }
40
+ function safeEqual(a, b) {
41
+ const left = Buffer.from(a);
42
+ const right = Buffer.from(b);
43
+ return left.length === right.length && timingSafeEqual(left, right);
44
+ }
45
+ function normalizeAddress(address) {
46
+ return address.startsWith('::ffff:') ? address.slice(7) : address;
47
+ }
48
+ /**
49
+ * Browser-safety checks for the dashboard and API (not the proxy endpoint):
50
+ * - Origin must match Host, so other websites cannot read traffic through
51
+ * the live WebSocket or call the API (cross-site WebSocket hijacking).
52
+ * `Origin: null` (sandboxed iframes, local files) is blocked too: the
53
+ * dashboard never sends it.
54
+ * - When bound to loopback, Host must be a local name (DNS rebinding).
55
+ */
56
+ export function checkBrowserSafety(req, config) {
57
+ const host = req.headers.host ?? '';
58
+ if (isLoopbackHost(config.host) && !isLocalHostHeader(host)) {
59
+ throw new HttpError(403, `Host "${host}" is not allowed. wslog only answers to localhost.`);
60
+ }
61
+ const origin = req.headers.origin;
62
+ if (origin !== undefined && originHost(origin) !== host) {
63
+ throw new HttpError(403, `Cross-origin request from ${origin || '(empty origin)'} blocked`);
64
+ }
65
+ }
66
+ function originHost(origin) {
67
+ try {
68
+ return new URL(origin).host;
69
+ }
70
+ catch {
71
+ return undefined; // includes the opaque origin "null"
72
+ }
73
+ }
74
+ function isLocalHostHeader(host) {
75
+ const hostname = host.replace(/:\d+$/, '').replace(/^\[|\]$/g, '').toLowerCase();
76
+ return hostname === 'localhost' || hostname.endsWith('.localhost') || isLoopbackHost(hostname);
77
+ }
78
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.js","sourceRoot":"","sources":["../../../src/server/http/auth.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ import type { IncomingMessage } from 'node:http';
2
+ import type { Duplex } from 'node:stream';
3
+ import type { Role } from '../../shared/protocol.js';
4
+ import { type WslogContext } from '../context.js';
5
+ /**
6
+ * Streams wslog events to dashboards over `/api/live`.
7
+ * Messages and connection updates are batched every 50 ms so a busy
8
+ * proxy does not flood the browser with tiny frames.
9
+ */
10
+ export declare class LiveHub {
11
+ private readonly ctx;
12
+ private readonly wss;
13
+ private readonly clients;
14
+ private pendingMessages;
15
+ private readonly pendingConnections;
16
+ private readonly timers;
17
+ private readonly unsubscribe;
18
+ constructor(ctx: WslogContext);
19
+ handleUpgrade(req: IncomingMessage, socket: Duplex, head: Buffer, role: Role): void;
20
+ close(): void;
21
+ private sendHello;
22
+ private flush;
23
+ private tickRates;
24
+ private broadcast;
25
+ private send;
26
+ }
@@ -0,0 +1,117 @@
1
+ import WebSocket, { WebSocketServer } from 'ws';
2
+ import { appInfo, baseInfo, publicTargets } from '../context.js';
3
+ import { toPartial } from './api.js';
4
+ const FLUSH_MS = 50;
5
+ const RATE_MS = 1000;
6
+ const HELLO_MESSAGES = 1000;
7
+ /** Stop streaming to a dashboard that cannot keep up; resync it once it drains. */
8
+ const MAX_BUFFERED_BYTES = 8 * 1024 * 1024;
9
+ /**
10
+ * Streams wslog events to dashboards over `/api/live`.
11
+ * Messages and connection updates are batched every 50 ms so a busy
12
+ * proxy does not flood the browser with tiny frames.
13
+ */
14
+ export class LiveHub {
15
+ ctx;
16
+ wss = new WebSocketServer({ noServer: true, perMessageDeflate: true });
17
+ clients = new Set();
18
+ pendingMessages = [];
19
+ pendingConnections = new Map();
20
+ timers = [];
21
+ unsubscribe = [];
22
+ constructor(ctx) {
23
+ this.ctx = ctx;
24
+ const { bus } = ctx;
25
+ this.unsubscribe.push(bus.on('message', (m) => this.pendingMessages.push(toPartial(m))), bus.on('connection', (c) => this.pendingConnections.set(c.id, { ...c, counts: { ...c.counts } })), bus.on('targets', (targets) => this.broadcast({ type: 'targets', targets: publicTargets(ctx, targets) })), bus.on('cleared', () => {
26
+ this.pendingMessages = [];
27
+ this.pendingConnections.clear();
28
+ this.broadcast({ type: 'cleared', connections: ctx.connections.list() });
29
+ }),
30
+ // Per client: the proxy base URL depends on how each dashboard reached wslog.
31
+ bus.on('info', () => {
32
+ for (const client of this.clients)
33
+ this.send(client, { type: 'info', info: baseInfo(ctx, client.req) });
34
+ }));
35
+ this.timers.push(setInterval(() => this.flush(), FLUSH_MS), setInterval(() => this.tickRates(), RATE_MS));
36
+ for (const t of this.timers)
37
+ t.unref();
38
+ }
39
+ handleUpgrade(req, socket, head, role) {
40
+ this.wss.handleUpgrade(req, socket, head, (ws) => {
41
+ // Batched messages are already in the store, so they will be in the hello
42
+ // snapshot. Send them to existing dashboards now, before this one joins,
43
+ // or the next flush would deliver them to it a second time.
44
+ this.flush();
45
+ const client = { ws, role, req, lagging: false };
46
+ this.clients.add(client);
47
+ ws.on('close', () => this.clients.delete(client));
48
+ ws.on('error', () => ws.terminate());
49
+ this.sendHello(client);
50
+ });
51
+ }
52
+ close() {
53
+ for (const t of this.timers)
54
+ clearInterval(t);
55
+ for (const off of this.unsubscribe)
56
+ off();
57
+ for (const { ws } of this.clients)
58
+ ws.close(1001, 'wslog shutting down');
59
+ this.clients.clear();
60
+ }
61
+ sendHello(client) {
62
+ const { ctx } = this;
63
+ this.send(client, {
64
+ type: 'hello',
65
+ info: appInfo(ctx, client.role, client.req),
66
+ targets: publicTargets(ctx),
67
+ connections: ctx.connections.list(),
68
+ messages: ctx.store.query({ limit: HELLO_MESSAGES }).map(toPartial),
69
+ });
70
+ }
71
+ flush() {
72
+ if (this.pendingConnections.size) {
73
+ for (const connection of this.pendingConnections.values())
74
+ this.broadcast({ type: 'connection', connection });
75
+ this.pendingConnections.clear();
76
+ }
77
+ if (this.pendingMessages.length) {
78
+ const items = this.pendingMessages;
79
+ this.pendingMessages = [];
80
+ this.broadcast({ type: 'messages', items });
81
+ }
82
+ }
83
+ tickRates() {
84
+ const rates = this.ctx.connections.drainRates();
85
+ // Counters change without events; push fresh totals for active connections.
86
+ for (const id of Object.keys(rates)) {
87
+ const connection = this.ctx.connections.get(id);
88
+ if (connection && rates[id])
89
+ this.pendingConnections.set(id, { ...connection, counts: { ...connection.counts } });
90
+ }
91
+ this.broadcast({ type: 'rates', time: Date.now(), rates });
92
+ }
93
+ broadcast(event) {
94
+ if (!this.clients.size)
95
+ return;
96
+ const data = JSON.stringify(event);
97
+ for (const client of this.clients) {
98
+ if (client.ws.readyState !== WebSocket.OPEN)
99
+ continue;
100
+ if (client.ws.bufferedAmount > MAX_BUFFERED_BYTES) {
101
+ client.lagging = true;
102
+ continue;
103
+ }
104
+ if (client.lagging) {
105
+ client.lagging = false;
106
+ this.sendHello(client);
107
+ continue;
108
+ }
109
+ client.ws.send(data);
110
+ }
111
+ }
112
+ send(client, event) {
113
+ if (client.ws.readyState === WebSocket.OPEN)
114
+ client.ws.send(JSON.stringify(event));
115
+ }
116
+ }
117
+ //# sourceMappingURL=live.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"live.js","sourceRoot":"","sources":["../../../src/server/http/live.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ import type { IncomingMessage, ServerResponse } from 'node:http';
2
+ import type { Role } from '../../shared/protocol.js';
3
+ export interface RouteContext {
4
+ req: IncomingMessage;
5
+ res: ServerResponse;
6
+ url: URL;
7
+ params: Record<string, string>;
8
+ role: Role;
9
+ body<T>(): Promise<T>;
10
+ }
11
+ export type RouteHandler = (ctx: RouteContext) => unknown;
12
+ export interface RouteOptions {
13
+ /** Minimum role. Defaults to `viewer` (anyone with access). */
14
+ role?: Role;
15
+ }
16
+ /**
17
+ * Tiny method + path router. Handlers return a value to send as JSON,
18
+ * or write to `res` themselves and return `undefined`.
19
+ */
20
+ export declare class Router {
21
+ private readonly routes;
22
+ get(path: string, handler: RouteHandler, options?: RouteOptions): this;
23
+ post(path: string, handler: RouteHandler, options?: RouteOptions): this;
24
+ delete(path: string, handler: RouteHandler, options?: RouteOptions): this;
25
+ add(method: string, path: string, handler: RouteHandler, options?: RouteOptions): this;
26
+ /** Returns false when no route matches the path. */
27
+ handle(req: IncomingMessage, res: ServerResponse, url: URL, role: Role): Promise<boolean>;
28
+ }
29
+ /** `decodeURIComponent` that answers 400 instead of throwing on bad `%` escapes. */
30
+ export declare function decodePathSegment(segment: string): string;
31
+ export declare function sendJson(res: ServerResponse, status: number, body: unknown): void;
@@ -0,0 +1,89 @@
1
+ import { HttpError } from './auth.js';
2
+ const MAX_BODY_BYTES = 64 * 1024;
3
+ /**
4
+ * Tiny method + path router. Handlers return a value to send as JSON,
5
+ * or write to `res` themselves and return `undefined`.
6
+ */
7
+ export class Router {
8
+ routes = [];
9
+ get(path, handler, options) {
10
+ return this.add('GET', path, handler, options);
11
+ }
12
+ post(path, handler, options) {
13
+ return this.add('POST', path, handler, options);
14
+ }
15
+ delete(path, handler, options) {
16
+ return this.add('DELETE', path, handler, options);
17
+ }
18
+ add(method, path, handler, options = {}) {
19
+ const keys = [];
20
+ const source = path.replace(/\/:(\w+)/g, (_m, key) => {
21
+ keys.push(key);
22
+ return '/([^/]+)';
23
+ });
24
+ this.routes.push({ method, pattern: new RegExp(`^${source}/?$`), keys, handler, role: options.role ?? 'viewer' });
25
+ return this;
26
+ }
27
+ /** Returns false when no route matches the path. */
28
+ async handle(req, res, url, role) {
29
+ let pathMatched = false;
30
+ for (const route of this.routes) {
31
+ const match = route.pattern.exec(url.pathname);
32
+ if (!match)
33
+ continue;
34
+ pathMatched = true;
35
+ if (route.method !== req.method && !(route.method === 'GET' && req.method === 'HEAD'))
36
+ continue;
37
+ if (route.role === 'admin' && role !== 'admin')
38
+ throw new HttpError(403, 'This link is read-only');
39
+ const params = Object.fromEntries(route.keys.map((k, i) => [k, decodePathSegment(match[i + 1])]));
40
+ const result = await route.handler({ req, res, url, params, role, body: () => readJson(req) });
41
+ if (!res.writableEnded && result !== undefined)
42
+ sendJson(res, 200, result);
43
+ else if (!res.writableEnded)
44
+ res.writeHead(204).end();
45
+ return true;
46
+ }
47
+ if (pathMatched)
48
+ throw new HttpError(405, 'Method not allowed');
49
+ return false;
50
+ }
51
+ }
52
+ /** `decodeURIComponent` that answers 400 instead of throwing on bad `%` escapes. */
53
+ export function decodePathSegment(segment) {
54
+ try {
55
+ return decodeURIComponent(segment);
56
+ }
57
+ catch {
58
+ throw new HttpError(400, 'Malformed URL encoding');
59
+ }
60
+ }
61
+ export function sendJson(res, status, body) {
62
+ const json = JSON.stringify(body);
63
+ res.writeHead(status, {
64
+ 'Content-Type': 'application/json; charset=utf-8',
65
+ 'Content-Length': Buffer.byteLength(json),
66
+ 'Cache-Control': 'no-store',
67
+ });
68
+ res.end(json);
69
+ }
70
+ async function readJson(req) {
71
+ if (!req.headers['content-type']?.includes('application/json')) {
72
+ throw new HttpError(415, 'Expected Content-Type: application/json');
73
+ }
74
+ const chunks = [];
75
+ let size = 0;
76
+ for await (const chunk of req) {
77
+ size += chunk.length;
78
+ if (size > MAX_BODY_BYTES)
79
+ throw new HttpError(413, 'Request body too large');
80
+ chunks.push(chunk);
81
+ }
82
+ try {
83
+ return JSON.parse(Buffer.concat(chunks).toString('utf8'));
84
+ }
85
+ catch {
86
+ throw new HttpError(400, 'Invalid JSON body');
87
+ }
88
+ }
89
+ //# sourceMappingURL=router.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"router.js","sourceRoot":"","sources":["../../../src/server/http/router.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ import { type Server } from 'node:http';
2
+ import type { WslogContext } from '../context.js';
3
+ import type { WebSocketProxy } from '../proxy/proxy.js';
4
+ import type { LiveHub } from './live.js';
5
+ export interface HttpServerDeps {
6
+ ctx: WslogContext;
7
+ proxy: WebSocketProxy;
8
+ live: LiveHub;
9
+ webRoot: string;
10
+ }
11
+ /**
12
+ * One HTTP server, three jobs:
13
+ * /p/<target>/... WebSocket proxy (what your app connects to)
14
+ * /api/... REST API + /api/live stream for the dashboard
15
+ * everything else the dashboard itself
16
+ */
17
+ export declare function createHttpServer({ ctx, proxy, live, webRoot }: HttpServerDeps): Server;
@@ -0,0 +1,82 @@
1
+ import { createServer } from 'node:http';
2
+ import { LIVE_PATH, PROXY_PREFIX } from '../../shared/protocol.js';
3
+ import { checkBrowserSafety, HttpError, resolveRole } from './auth.js';
4
+ import { sendJson } from './router.js';
5
+ import { createStaticHandler } from './static.js';
6
+ import { rejectUpgrade } from './upgrade.js';
7
+ /**
8
+ * One HTTP server, three jobs:
9
+ * /p/<target>/... WebSocket proxy (what your app connects to)
10
+ * /api/... REST API + /api/live stream for the dashboard
11
+ * everything else the dashboard itself
12
+ */
13
+ export function createHttpServer({ ctx, proxy, live, webRoot }) {
14
+ const serveStatic = createStaticHandler(webRoot);
15
+ const server = createServer((req, res) => {
16
+ handleRequest(req, res).catch((err) => sendError(res, err, ctx));
17
+ });
18
+ async function handleRequest(req, res) {
19
+ const url = parseUrl(req);
20
+ if (url.pathname.startsWith('/api/')) {
21
+ checkBrowserSafety(req, ctx.config);
22
+ const role = resolveRole(req, url, ctx.config);
23
+ if (!role)
24
+ throw new HttpError(401, 'Unauthorized. Open the dashboard with the link that includes ?token=…');
25
+ if (!(await ctx.router.handle(req, res, url, role)))
26
+ throw new HttpError(404, 'Not found');
27
+ return;
28
+ }
29
+ if (url.pathname.startsWith(PROXY_PREFIX)) {
30
+ res.writeHead(426, { 'Content-Type': 'text/plain; charset=utf-8', Upgrade: 'websocket' });
31
+ res.end('This is a wslog WebSocket proxy URL. Connect to it with a WebSocket client (ws://...).\n');
32
+ return;
33
+ }
34
+ serveStatic(req, res, url.pathname);
35
+ }
36
+ server.on('upgrade', (req, socket, head) => {
37
+ socket.on('error', (err) => ctx.logger.debug(`upgrade socket error: ${err.message}`));
38
+ try {
39
+ const url = parseUrl(req);
40
+ if (url.pathname.startsWith(PROXY_PREFIX)) {
41
+ const id = url.pathname.slice(PROXY_PREFIX.length).split('/')[0] ?? '';
42
+ const target = ctx.targets.get(id);
43
+ if (!target)
44
+ return proxy.rejectUnknownTarget(req, socket, id);
45
+ return proxy.handleUpgrade(req, socket, head, target);
46
+ }
47
+ if (url.pathname === LIVE_PATH) {
48
+ checkBrowserSafety(req, ctx.config);
49
+ const role = resolveRole(req, url, ctx.config);
50
+ if (!role)
51
+ return rejectUpgrade(socket, 401);
52
+ return live.handleUpgrade(req, socket, head, role);
53
+ }
54
+ rejectUpgrade(socket, 404);
55
+ }
56
+ catch (err) {
57
+ rejectUpgrade(socket, err instanceof HttpError ? err.status : 500, err instanceof Error ? err.message : undefined);
58
+ }
59
+ });
60
+ return server;
61
+ }
62
+ function parseUrl(req) {
63
+ try {
64
+ return new URL(req.url ?? '/', 'http://wslog.local');
65
+ }
66
+ catch {
67
+ // e.g. an absolute-form target like `GET http://[x/` — must not crash the process.
68
+ throw new HttpError(400, 'Invalid request URL');
69
+ }
70
+ }
71
+ function sendError(res, err, ctx) {
72
+ const status = err instanceof HttpError ? err.status : 500;
73
+ const message = err instanceof Error ? err.message : 'Internal error';
74
+ if (status >= 500)
75
+ ctx.logger.error(message, err);
76
+ if (res.headersSent) {
77
+ res.end();
78
+ return;
79
+ }
80
+ sendJson(res, status, { error: status >= 500 ? 'Internal error' : message });
81
+ }
82
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../../../src/server/http/server.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { IncomingMessage, ServerResponse } from 'node:http';
2
+ /** Serves the built dashboard with an SPA fallback to index.html. */
3
+ export declare function createStaticHandler(root: string): (req: IncomingMessage, res: ServerResponse, pathname: string) => void;
@@ -0,0 +1,56 @@
1
+ import { createReadStream, existsSync, statSync } from 'node:fs';
2
+ import { extname, join, normalize, sep } from 'node:path';
3
+ import { decodePathSegment } from './router.js';
4
+ const MIME = {
5
+ '.html': 'text/html; charset=utf-8',
6
+ '.js': 'text/javascript; charset=utf-8',
7
+ '.css': 'text/css; charset=utf-8',
8
+ '.json': 'application/json; charset=utf-8',
9
+ '.svg': 'image/svg+xml',
10
+ '.png': 'image/png',
11
+ '.ico': 'image/x-icon',
12
+ '.woff2': 'font/woff2',
13
+ '.map': 'application/json; charset=utf-8',
14
+ };
15
+ const NOT_BUILT = `<!doctype html><meta charset="utf-8"><title>wslog</title>
16
+ <body style="font:15px system-ui;padding:40px;max-width:640px">
17
+ <h1>wslog is running</h1>
18
+ <p>The dashboard has not been built yet. Run <code>npm run build:web</code>,
19
+ or use <code>npm run dev</code> and open the Vite URL.</p>
20
+ <p>The proxy and API work without it.</p></body>`;
21
+ /** Serves the built dashboard with an SPA fallback to index.html. */
22
+ export function createStaticHandler(root) {
23
+ const index = join(root, 'index.html');
24
+ return (req, res, pathname) => {
25
+ if (req.method !== 'GET' && req.method !== 'HEAD') {
26
+ res.writeHead(405).end();
27
+ return;
28
+ }
29
+ if (!existsSync(index)) {
30
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }).end(NOT_BUILT);
31
+ return;
32
+ }
33
+ let file = normalize(join(root, decodePathSegment(pathname)));
34
+ if (!file.startsWith(root + sep) && file !== root) {
35
+ res.writeHead(403).end();
36
+ return;
37
+ }
38
+ if (!existsSync(file) || statSync(file).isDirectory())
39
+ file = index;
40
+ const isAsset = file.includes(`${sep}assets${sep}`);
41
+ res.writeHead(200, {
42
+ 'Content-Type': MIME[extname(file)] ?? 'application/octet-stream',
43
+ 'Cache-Control': isAsset ? 'public, max-age=31536000, immutable' : 'no-cache',
44
+ 'X-Content-Type-Options': 'nosniff',
45
+ 'Referrer-Policy': 'no-referrer',
46
+ // Other sites may not frame the dashboard (clickjacking on Clear, Remove, ...).
47
+ 'X-Frame-Options': 'DENY',
48
+ 'Content-Security-Policy': "frame-ancestors 'none'",
49
+ });
50
+ if (req.method === 'HEAD')
51
+ res.end();
52
+ else
53
+ createReadStream(file).pipe(res);
54
+ };
55
+ }
56
+ //# sourceMappingURL=static.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"static.js","sourceRoot":"","sources":["../../../src/server/http/static.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEjE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAC1D,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAEhD,MAAM,IAAI,GAA2B;IACnC,OAAO,EAAE,0BAA0B;IACnC,KAAK,EAAE,gCAAgC;IACvC,MAAM,EAAE,yBAAyB;IACjC,OAAO,EAAE,iCAAiC;IAC1C,MAAM,EAAE,eAAe;IACvB,MAAM,EAAE,WAAW;IACnB,MAAM,EAAE,cAAc;IACtB,QAAQ,EAAE,YAAY;IACtB,MAAM,EAAE,iCAAiC;CAC1C,CAAC;AAEF,MAAM,SAAS,GAAG;;;;;iDAK+B,CAAC;AAElD,qEAAqE;AACrE,MAAM,UAAU,mBAAmB,CAAC,IAAY;IAC9C,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,EAAE,YAAY,CAAC,CAAC;IAEvC,OAAO,CAAC,GAAoB,EAAE,GAAmB,EAAE,QAAgB,EAAQ,EAAE;QAC3E,IAAI,GAAG,CAAC,MAAM,KAAK,KAAK,IAAI,GAAG,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC;YAClD,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,CAAC;YACzB,OAAO;QACT,CAAC;QACD,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,EAAE,CAAC;YACvB,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,0BAA0B,EAAE,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;YAClF,OAAO;QACT,CAAC;QAED,IAAI,IAAI,GAAG,SAAS,CAAC,IAAI,CAAC,IAAI,EAAE,iBAAiB,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;QAC9D,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,GAAG,GAAG,CAAC,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;YAClD,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,CAAC;YACzB,OAAO;QACT,CAAC;QACD,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE;YAAE,IAAI,GAAG,KAAK,CAAC;QAEpE,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,GAAG,SAAS,GAAG,EAAE,CAAC,CAAC;QACpD,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE;YACjB,cAAc,EAAE,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,IAAI,0BAA0B;YACjE,eAAe,EAAE,OAAO,CAAC,CAAC,CAAC,qCAAqC,CAAC,CAAC,CAAC,UAAU;YAC7E,wBAAwB,EAAE,SAAS;YACnC,iBAAiB,EAAE,aAAa;YAChC,gFAAgF;YAChF,iBAAiB,EAAE,MAAM;YACzB,yBAAyB,EAAE,wBAAwB;SACpD,CAAC,CAAC;QACH,IAAI,GAAG,CAAC,MAAM,KAAK,MAAM;YAAE,GAAG,CAAC,GAAG,EAAE,CAAC;;YAChC,gBAAgB,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACxC,CAAC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { Duplex } from 'node:stream';
2
+ /** Answer a WebSocket upgrade request with a plain HTTP error and close the socket. */
3
+ export declare function rejectUpgrade(socket: Duplex, status: number, message?: string): void;
@@ -0,0 +1,13 @@
1
+ import { STATUS_CODES } from 'node:http';
2
+ /** Answer a WebSocket upgrade request with a plain HTTP error and close the socket. */
3
+ export function rejectUpgrade(socket, status, message = STATUS_CODES[status] ?? '') {
4
+ if (socket.destroyed)
5
+ return;
6
+ const body = `${message}\n`;
7
+ socket.end(`HTTP/1.1 ${status} ${STATUS_CODES[status] ?? 'Error'}\r\n` +
8
+ 'Connection: close\r\n' +
9
+ 'Content-Type: text/plain; charset=utf-8\r\n' +
10
+ `Content-Length: ${Buffer.byteLength(body)}\r\n\r\n` +
11
+ body);
12
+ }
13
+ //# sourceMappingURL=upgrade.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upgrade.js","sourceRoot":"","sources":["../../../src/server/http/upgrade.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAGzC,uFAAuF;AACvF,MAAM,UAAU,aAAa,CAAC,MAAc,EAAE,MAAc,EAAE,OAAO,GAAG,YAAY,CAAC,MAAM,CAAC,IAAI,EAAE;IAChG,IAAI,MAAM,CAAC,SAAS;QAAE,OAAO;IAC7B,MAAM,IAAI,GAAG,GAAG,OAAO,IAAI,CAAC;IAC5B,MAAM,CAAC,GAAG,CACR,YAAY,MAAM,IAAI,YAAY,CAAC,MAAM,CAAC,IAAI,OAAO,MAAM;QACzD,uBAAuB;QACvB,6CAA6C;QAC7C,mBAAmB,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,UAAU;QACpD,IAAI,CACP,CAAC;AACJ,CAAC"}
@@ -0,0 +1,11 @@
1
+ export declare const MASK = "***";
2
+ export interface Masker {
3
+ readonly fields: readonly string[];
4
+ /** Mask a text payload. JSON is masked structurally; other text by `key=value` / `"key": "value"` patterns. */
5
+ text(input: string): string;
6
+ /** Mask query parameters in a URL or path. A password in the URL (`ws://user:pw@host`) is always hidden. */
7
+ url(input: string): string;
8
+ }
9
+ export declare function createMasker(fields: readonly string[]): Masker;
10
+ /** `ws://user:secret@host/x` → `ws://user:***@host/x`. */
11
+ export declare function redactUrlPassword(input: string): string;