@robiki/proxy 1.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.
package/dist/index.js ADDED
@@ -0,0 +1,542 @@
1
+ #!/usr/bin/env node
2
+ import net from 'node:net';
3
+ import { createServer as createServer$2, request as request$1 } from 'node:http';
4
+ import { createServer as createServer$1, request } from 'node:https';
5
+ import { constants, createSecureServer, createServer, connect } from 'node:http2';
6
+ import { exec } from 'node:child_process';
7
+ import { WebSocketServer, WebSocket } from 'ws';
8
+ import { getConfig, loadConfig } from './utils/config.js';
9
+ import 'node:fs';
10
+ import 'node:path';
11
+
12
+ const num = (seed) => {
13
+ const a = 1664525;
14
+ const c = 1013904223;
15
+ const m = 2 ** 32;
16
+ let state = Date.now();
17
+ state = (a * state + c) % m;
18
+ return state;
19
+ };
20
+
21
+ var RequestType = /* @__PURE__ */ ((RequestType2) => {
22
+ RequestType2["API"] = "api";
23
+ RequestType2["STREAM"] = "stream";
24
+ RequestType2["WEBSOCKET"] = "websocket";
25
+ return RequestType2;
26
+ })(RequestType || {});
27
+ const { HTTP2_HEADER_PATH, HTTP2_HEADER_METHOD, HTTP2_HEADER_SCHEME, HTTP2_HEADER_AUTHORITY } = constants;
28
+ const kill = (port) => {
29
+ return new Promise((resolve, reject) => {
30
+ console.log(`Attempting to kill process on port ${port}...`);
31
+ exec(`sleep 3 && kill -9 $(lsof -t -i:${port})`, (err) => {
32
+ if (err && err.message.indexOf("ENOENT") === -1) return reject(err);
33
+ return resolve();
34
+ });
35
+ });
36
+ };
37
+ const isPortTaken = (port) => {
38
+ return new Promise((resolve, reject) => {
39
+ const tester = net.createServer().once("error", (err) => {
40
+ if (err.code !== "EADDRINUSE") return reject(err);
41
+ resolve(true);
42
+ }).once("listening", () => {
43
+ tester.once("close", () => resolve(false)).close();
44
+ }).listen(port);
45
+ });
46
+ };
47
+ const allocatePort = (port) => {
48
+ return isPortTaken(port).then((isTaken) => {
49
+ if (!isTaken) return;
50
+ return kill(port);
51
+ });
52
+ };
53
+ const getCorsHeaders = (origin) => {
54
+ return {
55
+ "access-control-allow-origin": origin,
56
+ "access-control-allow-methods": "*",
57
+ "access-control-allow-headers": "*",
58
+ "access-control-allow-credentials": "true"
59
+ };
60
+ };
61
+ const http1ToHttp2Headers = (headers) => {
62
+ const http2Headers = {};
63
+ for (const [key, value] of Object.entries(headers)) {
64
+ switch (key.toLowerCase()) {
65
+ case "host":
66
+ http2Headers[":authority"] = value;
67
+ break;
68
+ /* HTTP/1.1-specific and WebSocket-specific headers */
69
+ case "connection":
70
+ case "upgrade":
71
+ case "http2-settings":
72
+ case "sec-websocket-key":
73
+ case "sec-websocket-version":
74
+ case "sec-websocket-protocol":
75
+ case "sec-websocket-extensions":
76
+ case "keep-alive":
77
+ case "transfer-encoding":
78
+ case "te":
79
+ break;
80
+ /* Other headers */
81
+ default:
82
+ http2Headers[key] = value;
83
+ }
84
+ }
85
+ return http2Headers;
86
+ };
87
+ const http2HeadersToHttp1Headers = (headers) => {
88
+ const http1Headers = {};
89
+ for (const [key, value] of Object.entries(headers)) {
90
+ switch (key) {
91
+ case ":authority":
92
+ http1Headers["host"] = value;
93
+ break;
94
+ /* HTTP/2 pseudo-headers */
95
+ case ":method":
96
+ case ":path":
97
+ case ":scheme":
98
+ case ":status":
99
+ break;
100
+ /* Other headers */
101
+ default:
102
+ http1Headers[key.replace(":", "")] = value;
103
+ }
104
+ }
105
+ return http1Headers;
106
+ };
107
+ const http = (routes, opts) => {
108
+ const port = opts?.port || 8080;
109
+ const sslOpts = opts && (opts.key || opts.cert) ? { key: opts.key, cert: opts.cert, ca: opts.ca } : void 0;
110
+ return (sslOpts ? createServer$1(sslOpts, routes) : createServer$2(routes)).listen(port, "0.0.0.0", () => {
111
+ console.log(`Server is listening on 0.0.0.0:${port}`);
112
+ }).on("error", async (err) => {
113
+ if (err && err.message.indexOf("EADDRINUSE") !== -1) {
114
+ console.log(`Port ${port} is already in use, attempting to kill process...`);
115
+ return allocatePort(port).then(() => http(routes, opts));
116
+ }
117
+ console.log("Server error: ", err);
118
+ throw err;
119
+ });
120
+ };
121
+ const http2 = (routes, streams, opts) => {
122
+ return (opts ? createSecureServer(opts, routes) : createServer(routes)).listen(opts?.port || 3e3, "0.0.0.0", () => {
123
+ console.log(`Server is listening on 0.0.0.0:${opts?.port || 3e3}`);
124
+ }).on("stream", (stream, headers, flags) => streams && streams(stream, headers, flags)).on("error", async (err) => {
125
+ if (err && err.message.indexOf("EADDRINUSE") !== -1) {
126
+ console.log(`Port ${opts?.port || 3e3} is already in use, attempting to kill process...`);
127
+ return allocatePort(opts?.port || 3e3).then(() => http2(routes, streams, opts));
128
+ }
129
+ console.log("Server error: ", err);
130
+ throw err;
131
+ });
132
+ };
133
+ const websocket = (server, routes, validate) => {
134
+ const wss = new WebSocketServer({
135
+ noServer: true,
136
+ verifyClient: ({ secure, req }, callback) => {
137
+ if (validate) {
138
+ const { headers, method, url, socket } = req;
139
+ const query = new URLSearchParams(req.url?.split("?")[1] || "");
140
+ const info = {
141
+ id: num(),
142
+ scheme: !!secure ? "https" : "http",
143
+ authority: headers.host || "",
144
+ origin: headers.origin || "",
145
+ method: method || "GET",
146
+ path: url || "/",
147
+ remoteAddress: socket.remoteAddress || "",
148
+ headers,
149
+ query,
150
+ type: "websocket" /* WEBSOCKET */,
151
+ respond: (status, headers2 = {}, body) => {
152
+ if (body) {
153
+ socket.push({ ":status": status, ...headers2, ...getCorsHeaders(info.origin) });
154
+ return socket.push(body);
155
+ }
156
+ socket.push({ ":status": status, ...headers2, ...getCorsHeaders(info.origin) });
157
+ return socket.push("");
158
+ },
159
+ end: (body) => {
160
+ if (body) return socket.end(body);
161
+ return socket.end();
162
+ }
163
+ };
164
+ return validate(info).then(({ status, message, code, headers: headers2 }) => {
165
+ console.log("Validated WebSocket request", status, code, message, headers2);
166
+ callback(status, code || 500, message || "Internal Server Error", headers2);
167
+ }).catch(() => callback(false, 500, "Internal Server Error"));
168
+ }
169
+ return callback(true);
170
+ }
171
+ });
172
+ server.on("upgrade", async (request, socket, upgradeHead) => {
173
+ return wss.handleUpgrade(
174
+ request,
175
+ socket,
176
+ upgradeHead,
177
+ (connection) => routes(request, connection, request.headers)
178
+ );
179
+ });
180
+ console.log("Websocket server started");
181
+ return wss;
182
+ };
183
+
184
+ const second = (num = 1) => {
185
+ return num * 1e3;
186
+ };
187
+ const minute = (num = 1) => {
188
+ return num * 60 * second();
189
+ };
190
+ const hour = (num = 1) => {
191
+ return num * 60 * minute();
192
+ };
193
+ const day = (num = 1) => {
194
+ return num * 24 * hour();
195
+ };
196
+
197
+ function isMediaFile(path) {
198
+ const mediaExtensions = [
199
+ ".png",
200
+ ".jpg",
201
+ ".jpeg",
202
+ ".gif",
203
+ ".svg",
204
+ ".webp",
205
+ ".mp4",
206
+ ".webm",
207
+ ".ogg",
208
+ ".mov",
209
+ ".avi",
210
+ ".mp3",
211
+ ".wav",
212
+ ".flac",
213
+ ".aac",
214
+ ".m4a",
215
+ ".ogg",
216
+ ".wav",
217
+ ".flac",
218
+ ".aac",
219
+ ".m4a",
220
+ ".woff2",
221
+ ".woff",
222
+ ".ttf",
223
+ ".eot",
224
+ ".otf",
225
+ ".ico"
226
+ ];
227
+ return mediaExtensions.some((ext) => {
228
+ return path.split("?")[0].split("#")[0].toLowerCase().endsWith(ext);
229
+ });
230
+ }
231
+
232
+ const restAPIProxyHandler = async (req, res) => {
233
+ const config = getConfig();
234
+ const { target, ssl, remap } = config.getTarget(req.headers.host || req.headers[":authority"]?.toString() || "");
235
+ if (req.httpVersion === "2.0" && ssl) return;
236
+ if (!target) {
237
+ res.writeHead(404);
238
+ res.end("Not Found");
239
+ return;
240
+ }
241
+ console.log("HTTP1 rest proxy", `${ssl ? "https" : "http"}://${target}${req.url}`, req.headers.host);
242
+ if (remap) req.url = remap(req.url || "");
243
+ const requestFn = ssl ? request : request$1;
244
+ const headers = req.httpVersion === "2.0" ? http2HeadersToHttp1Headers(req.headers) : req.headers;
245
+ const method = req.httpVersion === "2.0" ? req.headers[":method"]?.toString() : req.method;
246
+ console.log("Proxy Request::", req.url, method, headers);
247
+ const proxy = requestFn(
248
+ `${ssl ? "https" : "http"}://${target}${req.url || ""}`,
249
+ {
250
+ ...ssl ? { ...ssl, rejectUnauthorized: false } : {},
251
+ method,
252
+ headers
253
+ },
254
+ (proxyRes) => {
255
+ const responseHeaders = req.httpVersion === "2.0" ? http1ToHttp2Headers(proxyRes.headers) : proxyRes.headers;
256
+ if (req.url && isMediaFile(req.url)) {
257
+ responseHeaders["cache-control"] = `public, max-age=${day()}`;
258
+ }
259
+ res.writeHead(proxyRes.statusCode || 500, responseHeaders);
260
+ proxyRes.on("data", (chunk) => {
261
+ if (!res.writableEnded && !res.closed && !res.destroyed) {
262
+ res.write(chunk);
263
+ }
264
+ });
265
+ proxyRes.on("end", () => {
266
+ if (!res.writableEnded && !res.closed && !res.destroyed) {
267
+ res.end();
268
+ }
269
+ });
270
+ proxyRes.on("error", (error) => {
271
+ console.error("Proxy response error:", error);
272
+ if (!res.destroyed) res.destroy(error);
273
+ });
274
+ }
275
+ );
276
+ req.on("data", (chunk) => {
277
+ if (!proxy.writableEnded && !proxy.closed && !proxy.destroyed) {
278
+ proxy.write(chunk);
279
+ }
280
+ });
281
+ req.on("end", () => {
282
+ if (!proxy.writableEnded && !proxy.closed && !proxy.destroyed) {
283
+ proxy.end();
284
+ }
285
+ });
286
+ req.on("error", (error) => {
287
+ console.error("Client request error:", error);
288
+ if (!proxy.destroyed) proxy.destroy(error);
289
+ });
290
+ };
291
+
292
+ const streamAPIProxyHandler = async (stream, headers) => {
293
+ const config = getConfig();
294
+ const { target, ssl, remap } = config.getTarget(headers[":authority"] || "");
295
+ if (!ssl) return;
296
+ if (!target) {
297
+ stream.destroy(new Error("Not Found"));
298
+ return;
299
+ }
300
+ console.log("HTTP2 stream proxy", `${ssl ? "https" : "http"}://${target}${headers[":path"]}`, headers[":authority"]);
301
+ if (remap) headers[":path"] = remap(headers[":path"] || "");
302
+ console.log("Proxy Request::", headers[":path"]);
303
+ const proxy = connect(`https://${target}${headers[":path"]}`, {
304
+ ...ssl,
305
+ rejectUnauthorized: false
306
+ });
307
+ proxy.on("connect", () => {
308
+ const request = proxy.request(headers);
309
+ request.on("response", (headerResponse) => {
310
+ if (!stream.writableEnded && !stream.closed && !stream.destroyed) {
311
+ console.log("Proxy Response::", headerResponse[":status"], `for ${headers[":path"]}`);
312
+ if (headers[":path"] && isMediaFile(headers[":path"])) {
313
+ headerResponse["cache-control"] = `public, max-age=${day()}`;
314
+ }
315
+ stream.respond(headerResponse);
316
+ }
317
+ });
318
+ stream.on("data", (chunk) => {
319
+ if (!request.writableEnded && !request.closed && !request.destroyed) {
320
+ request.write(chunk);
321
+ }
322
+ });
323
+ stream.on("end", () => {
324
+ if (!request.writableEnded && !request.closed && !request.destroyed) {
325
+ request.end();
326
+ }
327
+ });
328
+ stream.on("close", () => {
329
+ if (!request.closed && !request.destroyed) request.close();
330
+ });
331
+ stream.on("goaway", (_, errorCode) => {
332
+ if (errorCode && !request.destroyed) {
333
+ request.destroy(new Error(`HTTP/2 connection closed with error code ${errorCode}`));
334
+ }
335
+ if (!stream.closed && !stream.destroyed) stream.close();
336
+ });
337
+ stream.on("error", (error) => {
338
+ console.error("HTTP2 stream proxy error:", error);
339
+ if (!request.destroyed) request.destroy(error);
340
+ if (!proxy.closed) proxy.close();
341
+ });
342
+ request.on("data", (chunk) => {
343
+ if (!stream.writableEnded && !stream.closed && !stream.destroyed) {
344
+ stream.write(chunk);
345
+ }
346
+ });
347
+ request.on("end", () => {
348
+ if (!stream.writableEnded && !stream.closed && !stream.destroyed) {
349
+ stream.end();
350
+ }
351
+ });
352
+ request.on("close", () => {
353
+ if (!stream.closed && !stream.destroyed) stream.close();
354
+ });
355
+ request.on("error", (error) => {
356
+ console.error("HTTP2 request proxy error:", error);
357
+ if (!stream.destroyed) stream.destroy(error);
358
+ return !proxy.closed && proxy.close();
359
+ });
360
+ proxy.on("timeout", () => {
361
+ console.error("HTTP/2 client timeout");
362
+ if (!stream.destroyed) stream.destroy(new Error("HTTP/2 client timeout"));
363
+ });
364
+ });
365
+ proxy.on("error", (error) => {
366
+ console.error("HTTP2 proxy connection error:", error);
367
+ if (!stream.destroyed) {
368
+ stream.destroy(error);
369
+ }
370
+ });
371
+ };
372
+
373
+ const websocketAPIProxyHandler = async (req, socket, headers) => {
374
+ const config = getConfig();
375
+ const { target, ssl, remap } = config.getTarget(req.headers.host || "");
376
+ if (!target) return socket.close();
377
+ console.log("HTTP2 websocket proxy", `${ssl ? "https" : "http"}://${target}${req.url}`, headers.host);
378
+ if (remap) req.url = remap(req.url || "");
379
+ const proxy = new WebSocket(
380
+ `${ssl ? "wss" : "ws"}://${target}${req.url || ""}`,
381
+ req.headers["sec-websocket-protocol"]?.split(",").map((p) => p.trim()),
382
+ {
383
+ ...ssl ? { ...ssl, rejectUnauthorized: false } : {},
384
+ headers: req.headers,
385
+ host: req.headers.host,
386
+ origin: req.headers.origin,
387
+ protocol: req.headers["sec-websocket-protocol"]
388
+ }
389
+ );
390
+ proxy.on("message", (message) => {
391
+ if (message.toString("utf8").startsWith("{")) {
392
+ socket.send(message.toString("utf8"));
393
+ } else {
394
+ socket.send(message);
395
+ }
396
+ });
397
+ socket.on("message", (message) => proxy.send(message));
398
+ proxy.on("close", () => socket.close());
399
+ socket.on("close", () => proxy.close());
400
+ proxy.on("error", (error) => {
401
+ console.error("WebSocket proxy error:", error);
402
+ socket.close();
403
+ });
404
+ };
405
+
406
+ class ProxyServer {
407
+ config;
408
+ servers = [];
409
+ constructor(config) {
410
+ this.config = config;
411
+ }
412
+ /**
413
+ * Start the proxy server
414
+ */
415
+ async start() {
416
+ const ssl = this.config.getSSL();
417
+ const ports = this.config.getPorts();
418
+ const logStartup = () => {
419
+ console.log("STARTING PROXY SERVER....");
420
+ console.log("Ports:", ports);
421
+ console.log("SSL:", !!ssl);
422
+ return { ssl, ports };
423
+ };
424
+ const createServers = ({ ssl: ssl2, ports: ports2 }) => {
425
+ for (const port of ports2) {
426
+ let server;
427
+ if (ssl2) {
428
+ server = http2(restAPIProxyHandler, streamAPIProxyHandler, { ...ssl2, port });
429
+ } else {
430
+ server = http(restAPIProxyHandler, { port });
431
+ }
432
+ websocket(server, websocketAPIProxyHandler, (info) => this.config.validate(info));
433
+ this.servers.push(server);
434
+ }
435
+ return this.servers;
436
+ };
437
+ const logSuccess = () => {
438
+ console.log("Proxy server started successfully");
439
+ };
440
+ const handleError = (error) => {
441
+ console.error("Failed to start proxy server:", error);
442
+ throw error;
443
+ };
444
+ return Promise.resolve().then(() => logStartup()).then((config) => createServers(config)).then(() => logSuccess()).catch((error) => handleError(error));
445
+ }
446
+ /**
447
+ * Stop the proxy server
448
+ */
449
+ async stop() {
450
+ console.log("Stopping proxy server...");
451
+ for (const server of this.servers) {
452
+ server.close();
453
+ }
454
+ this.servers = [];
455
+ console.log("Proxy server stopped");
456
+ }
457
+ /**
458
+ * Get the configuration
459
+ */
460
+ getConfig() {
461
+ return this.config;
462
+ }
463
+ }
464
+ function createProxy(config) {
465
+ const createProxyInstance = () => {
466
+ return new ProxyServer(loadConfig(config));
467
+ };
468
+ const startProxy = (proxy) => {
469
+ return proxy.start().then(() => proxy);
470
+ };
471
+ return Promise.resolve().then(() => createProxyInstance()).then((proxy) => startProxy(proxy));
472
+ }
473
+ function createCustomProxy(config, handlers) {
474
+ const servers = [];
475
+ const initializeConfig = () => {
476
+ const proxyConfig = loadConfig(config);
477
+ return {
478
+ ssl: proxyConfig.getSSL(),
479
+ ports: proxyConfig.getPorts(),
480
+ proxyConfig
481
+ };
482
+ };
483
+ const logStartup = (cfg) => {
484
+ console.log("STARTING CUSTOM PROXY SERVER....");
485
+ return cfg;
486
+ };
487
+ const createServers = ({ ssl, ports, proxyConfig }) => {
488
+ for (const port of ports) {
489
+ let server;
490
+ if (ssl) {
491
+ server = http2(handlers.rest || restAPIProxyHandler, handlers.stream || streamAPIProxyHandler, {
492
+ ...ssl,
493
+ port
494
+ });
495
+ } else {
496
+ server = http(handlers.rest || restAPIProxyHandler, { port });
497
+ }
498
+ websocket(server, handlers.websocket || websocketAPIProxyHandler);
499
+ servers.push(server);
500
+ }
501
+ return proxyConfig;
502
+ };
503
+ const createProxyInstance = (proxyConfig) => {
504
+ console.log("Custom proxy server started successfully");
505
+ return {
506
+ getConfig: () => proxyConfig,
507
+ start: async () => {
508
+ },
509
+ stop: async () => {
510
+ for (const server of servers) {
511
+ server.close();
512
+ }
513
+ }
514
+ };
515
+ };
516
+ const handleError = (error) => {
517
+ console.error("Failed to start custom proxy server:", error);
518
+ throw error;
519
+ };
520
+ return Promise.resolve().then(() => initializeConfig()).then((cfg) => logStartup(cfg)).then((cfg) => createServers(cfg)).then((proxyConfig) => createProxyInstance(proxyConfig)).catch((error) => handleError(error));
521
+ }
522
+ if (import.meta.url === `file://${process.argv[1]}`) {
523
+ const setupErrorHandlers = () => {
524
+ process.on("uncaughtException", function(error) {
525
+ console.log("UNCAUGHT EXCEPTION: ", error);
526
+ });
527
+ process.on("unhandledRejection", function(reason, promise) {
528
+ console.log("UNHANDLED REJECTION: ", reason, promise);
529
+ });
530
+ };
531
+ const startProxyServer = () => {
532
+ return createProxy();
533
+ };
534
+ const handleStartupError = (error) => {
535
+ console.error("Failed to start proxy server:", error);
536
+ process.exit(1);
537
+ };
538
+ setupErrorHandlers();
539
+ Promise.resolve().then(() => startProxyServer()).catch((error) => handleStartupError(error));
540
+ }
541
+
542
+ export { ProxyServer, RequestType, createCustomProxy, createProxy, getConfig, loadConfig, restAPIProxyHandler, streamAPIProxyHandler, websocketAPIProxyHandler };
@@ -0,0 +1,5 @@
1
+ import 'node:http';
2
+ export { c as CertificateConfig, C as CorsConfig, P as ProxyConfig, b as RouteConfig, S as ServerConfig, g as getConfig, i as initConfig, l as loadConfig, h as loadConfigFromEnv, f as loadConfigFromFile } from '../config-_6LOsppp.js';
3
+ import 'node:http2';
4
+ import 'node:tls';
5
+ import 'ws';