@responsedotok/shufffle 0.1.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 (190) hide show
  1. package/LICENSE.md +9 -0
  2. package/README.md +91 -0
  3. package/dist/cjs/package.json +3 -0
  4. package/dist/cjs/src/cli/help.d.ts +1 -0
  5. package/dist/cjs/src/cli/help.js +54 -0
  6. package/dist/cjs/src/cli/index.d.ts +1 -0
  7. package/dist/cjs/src/cli/index.js +96 -0
  8. package/dist/cjs/src/cli/parse-args.d.ts +9 -0
  9. package/dist/cjs/src/cli/parse-args.js +68 -0
  10. package/dist/cjs/src/config/config.d.ts +16 -0
  11. package/dist/cjs/src/config/config.js +184 -0
  12. package/dist/cjs/src/env-overrides.d.ts +7 -0
  13. package/dist/cjs/src/env-overrides.js +37 -0
  14. package/dist/cjs/src/index.d.ts +10 -0
  15. package/dist/cjs/src/index.js +10 -0
  16. package/dist/cjs/src/lib/constants/hop-by-hop-headers.d.ts +1 -0
  17. package/dist/cjs/src/lib/constants/hop-by-hop-headers.js +13 -0
  18. package/dist/cjs/src/lib/services/handlers/http-handler.d.ts +32 -0
  19. package/dist/cjs/src/lib/services/handlers/http-handler.js +226 -0
  20. package/dist/cjs/src/lib/services/handlers/websocket-handler.d.ts +11 -0
  21. package/dist/cjs/src/lib/services/handlers/websocket-handler.js +100 -0
  22. package/dist/cjs/src/lib/services/headers/headers-service.d.ts +18 -0
  23. package/dist/cjs/src/lib/services/headers/headers-service.js +94 -0
  24. package/dist/cjs/src/lib/services/health/health-service.d.ts +15 -0
  25. package/dist/cjs/src/lib/services/health/health-service.js +65 -0
  26. package/dist/cjs/src/lib/services/load-balancers/create-balancer.d.ts +3 -0
  27. package/dist/cjs/src/lib/services/load-balancers/create-balancer.js +21 -0
  28. package/dist/cjs/src/lib/services/load-balancers/load-balancer.d.ts +4 -0
  29. package/dist/cjs/src/lib/services/load-balancers/load-balancer.js +6 -0
  30. package/dist/cjs/src/lib/services/load-balancers/random-balancer.d.ts +5 -0
  31. package/dist/cjs/src/lib/services/load-balancers/random-balancer.js +14 -0
  32. package/dist/cjs/src/lib/services/load-balancers/round-robin-balancer.d.ts +6 -0
  33. package/dist/cjs/src/lib/services/load-balancers/round-robin-balancer.js +16 -0
  34. package/dist/cjs/src/lib/services/load-balancers/weighted-balancer.d.ts +5 -0
  35. package/dist/cjs/src/lib/services/load-balancers/weighted-balancer.js +27 -0
  36. package/dist/cjs/src/lib/services/proxy/proxy-server.d.ts +16 -0
  37. package/dist/cjs/src/lib/services/proxy/proxy-server.js +118 -0
  38. package/dist/cjs/src/lib/types/config.d.ts +17 -0
  39. package/dist/cjs/src/lib/types/config.js +2 -0
  40. package/dist/cjs/src/lib/types/context.d.ts +10 -0
  41. package/dist/cjs/src/lib/types/context.js +2 -0
  42. package/dist/cjs/src/lib/types/header-rules.d.ts +6 -0
  43. package/dist/cjs/src/lib/types/header-rules.js +2 -0
  44. package/dist/cjs/src/lib/types/hooks.d.ts +6 -0
  45. package/dist/cjs/src/lib/types/hooks.js +2 -0
  46. package/dist/cjs/src/lib/types/load-balancer-strategy.d.ts +5 -0
  47. package/dist/cjs/src/lib/types/load-balancer-strategy.js +9 -0
  48. package/dist/cjs/src/lib/types/log.d.ts +8 -0
  49. package/dist/cjs/src/lib/types/log.js +10 -0
  50. package/dist/cjs/src/lib/types/route-match.d.ts +1 -0
  51. package/dist/cjs/src/lib/types/route-match.js +2 -0
  52. package/dist/cjs/src/lib/types/route-rewrite.d.ts +5 -0
  53. package/dist/cjs/src/lib/types/route-rewrite.js +2 -0
  54. package/dist/cjs/src/lib/types/route.d.ts +14 -0
  55. package/dist/cjs/src/lib/types/route.js +2 -0
  56. package/dist/cjs/src/lib/types/upstream.d.ts +6 -0
  57. package/dist/cjs/src/lib/types/upstream.js +2 -0
  58. package/dist/cjs/src/logger/logger.d.ts +10 -0
  59. package/dist/cjs/src/logger/logger.js +39 -0
  60. package/dist/cjs/src/utils/is-match.d.ts +2 -0
  61. package/dist/cjs/src/utils/is-match.js +14 -0
  62. package/dist/cjs/src/utils/match-route.d.ts +2 -0
  63. package/dist/cjs/src/utils/match-route.js +11 -0
  64. package/dist/cjs/src/utils/rewrite-path.d.ts +2 -0
  65. package/dist/cjs/src/utils/rewrite-path.js +23 -0
  66. package/dist/src/cli/help.d.ts +2 -0
  67. package/dist/src/cli/help.d.ts.map +1 -0
  68. package/dist/src/cli/help.js.map +1 -0
  69. package/dist/src/cli/help.mjs +52 -0
  70. package/dist/src/cli/index.d.ts +2 -0
  71. package/dist/src/cli/index.d.ts.map +1 -0
  72. package/dist/src/cli/index.js.map +1 -0
  73. package/dist/src/cli/index.mjs +91 -0
  74. package/dist/src/cli/parse-args.d.ts +10 -0
  75. package/dist/src/cli/parse-args.d.ts.map +1 -0
  76. package/dist/src/cli/parse-args.js.map +1 -0
  77. package/dist/src/cli/parse-args.mjs +66 -0
  78. package/dist/src/config/config.d.ts +17 -0
  79. package/dist/src/config/config.d.ts.map +1 -0
  80. package/dist/src/config/config.js.map +1 -0
  81. package/dist/src/config/config.mjs +145 -0
  82. package/dist/src/env-overrides.d.ts +8 -0
  83. package/dist/src/env-overrides.d.ts.map +1 -0
  84. package/dist/src/env-overrides.js.map +1 -0
  85. package/dist/src/env-overrides.mjs +34 -0
  86. package/dist/src/index.d.ts +11 -0
  87. package/dist/src/index.d.ts.map +1 -0
  88. package/dist/src/index.js.map +1 -0
  89. package/dist/src/index.mjs +4 -0
  90. package/dist/src/lib/constants/hop-by-hop-headers.d.ts +2 -0
  91. package/dist/src/lib/constants/hop-by-hop-headers.d.ts.map +1 -0
  92. package/dist/src/lib/constants/hop-by-hop-headers.js.map +1 -0
  93. package/dist/src/lib/constants/hop-by-hop-headers.mjs +11 -0
  94. package/dist/src/lib/services/handlers/http-handler.d.ts +33 -0
  95. package/dist/src/lib/services/handlers/http-handler.d.ts.map +1 -0
  96. package/dist/src/lib/services/handlers/http-handler.js.map +1 -0
  97. package/dist/src/lib/services/handlers/http-handler.mjs +220 -0
  98. package/dist/src/lib/services/handlers/websocket-handler.d.ts +12 -0
  99. package/dist/src/lib/services/handlers/websocket-handler.d.ts.map +1 -0
  100. package/dist/src/lib/services/handlers/websocket-handler.js.map +1 -0
  101. package/dist/src/lib/services/handlers/websocket-handler.mjs +94 -0
  102. package/dist/src/lib/services/headers/headers-service.d.ts +19 -0
  103. package/dist/src/lib/services/headers/headers-service.d.ts.map +1 -0
  104. package/dist/src/lib/services/headers/headers-service.js.map +1 -0
  105. package/dist/src/lib/services/headers/headers-service.mjs +91 -0
  106. package/dist/src/lib/services/health/health-service.d.ts +16 -0
  107. package/dist/src/lib/services/health/health-service.d.ts.map +1 -0
  108. package/dist/src/lib/services/health/health-service.js.map +1 -0
  109. package/dist/src/lib/services/health/health-service.mjs +59 -0
  110. package/dist/src/lib/services/load-balancers/create-balancer.d.ts +4 -0
  111. package/dist/src/lib/services/load-balancers/create-balancer.d.ts.map +1 -0
  112. package/dist/src/lib/services/load-balancers/create-balancer.js.map +1 -0
  113. package/dist/src/lib/services/load-balancers/create-balancer.mjs +19 -0
  114. package/dist/src/lib/services/load-balancers/load-balancer.d.ts +5 -0
  115. package/dist/src/lib/services/load-balancers/load-balancer.d.ts.map +1 -0
  116. package/dist/src/lib/services/load-balancers/load-balancer.js.map +1 -0
  117. package/dist/src/lib/services/load-balancers/load-balancer.mjs +3 -0
  118. package/dist/src/lib/services/load-balancers/random-balancer.d.ts +6 -0
  119. package/dist/src/lib/services/load-balancers/random-balancer.d.ts.map +1 -0
  120. package/dist/src/lib/services/load-balancers/random-balancer.js.map +1 -0
  121. package/dist/src/lib/services/load-balancers/random-balancer.mjs +11 -0
  122. package/dist/src/lib/services/load-balancers/round-robin-balancer.d.ts +7 -0
  123. package/dist/src/lib/services/load-balancers/round-robin-balancer.d.ts.map +1 -0
  124. package/dist/src/lib/services/load-balancers/round-robin-balancer.js.map +1 -0
  125. package/dist/src/lib/services/load-balancers/round-robin-balancer.mjs +13 -0
  126. package/dist/src/lib/services/load-balancers/weighted-balancer.d.ts +6 -0
  127. package/dist/src/lib/services/load-balancers/weighted-balancer.d.ts.map +1 -0
  128. package/dist/src/lib/services/load-balancers/weighted-balancer.js.map +1 -0
  129. package/dist/src/lib/services/load-balancers/weighted-balancer.mjs +24 -0
  130. package/dist/src/lib/services/proxy/proxy-server.d.ts +17 -0
  131. package/dist/src/lib/services/proxy/proxy-server.d.ts.map +1 -0
  132. package/dist/src/lib/services/proxy/proxy-server.js.map +1 -0
  133. package/dist/src/lib/services/proxy/proxy-server.mjs +111 -0
  134. package/dist/src/lib/types/config.d.ts +18 -0
  135. package/dist/src/lib/types/config.d.ts.map +1 -0
  136. package/dist/src/lib/types/config.js.map +1 -0
  137. package/dist/src/lib/types/config.mjs +2 -0
  138. package/dist/src/lib/types/context.d.ts +11 -0
  139. package/dist/src/lib/types/context.d.ts.map +1 -0
  140. package/dist/src/lib/types/context.js.map +1 -0
  141. package/dist/src/lib/types/context.mjs +2 -0
  142. package/dist/src/lib/types/header-rules.d.ts +7 -0
  143. package/dist/src/lib/types/header-rules.d.ts.map +1 -0
  144. package/dist/src/lib/types/header-rules.js.map +1 -0
  145. package/dist/src/lib/types/header-rules.mjs +2 -0
  146. package/dist/src/lib/types/hooks.d.ts +7 -0
  147. package/dist/src/lib/types/hooks.d.ts.map +1 -0
  148. package/dist/src/lib/types/hooks.js.map +1 -0
  149. package/dist/src/lib/types/hooks.mjs +2 -0
  150. package/dist/src/lib/types/load-balancer-strategy.d.ts +6 -0
  151. package/dist/src/lib/types/load-balancer-strategy.d.ts.map +1 -0
  152. package/dist/src/lib/types/load-balancer-strategy.js.map +1 -0
  153. package/dist/src/lib/types/load-balancer-strategy.mjs +7 -0
  154. package/dist/src/lib/types/log.d.ts +9 -0
  155. package/dist/src/lib/types/log.d.ts.map +1 -0
  156. package/dist/src/lib/types/log.js.map +1 -0
  157. package/dist/src/lib/types/log.mjs +8 -0
  158. package/dist/src/lib/types/route-match.d.ts +2 -0
  159. package/dist/src/lib/types/route-match.d.ts.map +1 -0
  160. package/dist/src/lib/types/route-match.js.map +1 -0
  161. package/dist/src/lib/types/route-match.mjs +2 -0
  162. package/dist/src/lib/types/route-rewrite.d.ts +6 -0
  163. package/dist/src/lib/types/route-rewrite.d.ts.map +1 -0
  164. package/dist/src/lib/types/route-rewrite.js.map +1 -0
  165. package/dist/src/lib/types/route-rewrite.mjs +2 -0
  166. package/dist/src/lib/types/route.d.ts +15 -0
  167. package/dist/src/lib/types/route.d.ts.map +1 -0
  168. package/dist/src/lib/types/route.js.map +1 -0
  169. package/dist/src/lib/types/route.mjs +2 -0
  170. package/dist/src/lib/types/upstream.d.ts +7 -0
  171. package/dist/src/lib/types/upstream.d.ts.map +1 -0
  172. package/dist/src/lib/types/upstream.js.map +1 -0
  173. package/dist/src/lib/types/upstream.mjs +2 -0
  174. package/dist/src/logger/logger.d.ts +11 -0
  175. package/dist/src/logger/logger.d.ts.map +1 -0
  176. package/dist/src/logger/logger.js.map +1 -0
  177. package/dist/src/logger/logger.mjs +36 -0
  178. package/dist/src/utils/is-match.d.ts +3 -0
  179. package/dist/src/utils/is-match.d.ts.map +1 -0
  180. package/dist/src/utils/is-match.js.map +1 -0
  181. package/dist/src/utils/is-match.mjs +12 -0
  182. package/dist/src/utils/match-route.d.ts +3 -0
  183. package/dist/src/utils/match-route.d.ts.map +1 -0
  184. package/dist/src/utils/match-route.js.map +1 -0
  185. package/dist/src/utils/match-route.mjs +9 -0
  186. package/dist/src/utils/rewrite-path.d.ts +3 -0
  187. package/dist/src/utils/rewrite-path.d.ts.map +1 -0
  188. package/dist/src/utils/rewrite-path.js.map +1 -0
  189. package/dist/src/utils/rewrite-path.mjs +21 -0
  190. package/package.json +63 -0
@@ -0,0 +1,226 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.HttpHandler = void 0;
7
+ const node_http_1 = __importDefault(require("node:http"));
8
+ const node_https_1 = __importDefault(require("node:https"));
9
+ const logger_js_1 = require("../../../logger/logger.js");
10
+ const match_route_js_1 = require("../../../utils/match-route.js");
11
+ const rewrite_path_js_1 = require("../../../utils/rewrite-path.js");
12
+ const load_balancer_strategy_js_1 = require("../../types/load-balancer-strategy.js");
13
+ const headers_service_js_1 = require("../headers/headers-service.js");
14
+ const health_service_js_1 = require("../health/health-service.js");
15
+ const create_balancer_js_1 = require("../load-balancers/create-balancer.js");
16
+ const RETRY_SAFE_METHODS = new Set(['GET', 'HEAD', 'OPTIONS']);
17
+ class HttpHandler {
18
+ config;
19
+ hooks;
20
+ balancers = new Map();
21
+ globalBalancer;
22
+ httpAgent = new node_http_1.default.Agent({ keepAlive: true });
23
+ httpsAgent = new node_https_1.default.Agent({ keepAlive: true });
24
+ healthService;
25
+ headersService;
26
+ logger;
27
+ constructor(config, hooks = {}, headersService, logger) {
28
+ this.config = config;
29
+ this.hooks = hooks;
30
+ this.logger = logger ?? new logger_js_1.Logger();
31
+ this.globalBalancer = (0, create_balancer_js_1.createBalancer)(config.balancer ?? load_balancer_strategy_js_1.LoadBalancerStrategy.RoundRobin);
32
+ this.headersService =
33
+ headersService ??
34
+ new headers_service_js_1.HeadersService(config.headers, config.forwardIp ?? true);
35
+ const allUpstreams = [
36
+ ...new Map(config.routes
37
+ .flatMap((r) => r.upstreams)
38
+ .map((u) => [`${u.host}:${u.port}`, u])).values(),
39
+ ];
40
+ this.healthService = new health_service_js_1.HealthService(allUpstreams, config.healthCheck?.interval, config.healthCheck?.timeout);
41
+ }
42
+ start() {
43
+ this.healthService.start();
44
+ }
45
+ stop() {
46
+ this.healthService.stop();
47
+ }
48
+ async handler(req, res) {
49
+ const url = new URL(req.url ?? '/', 'http://localhost');
50
+ const pathname = url.pathname;
51
+ const route = (0, match_route_js_1.matchRoute)(this.config.routes, pathname);
52
+ if (!route) {
53
+ this.sendError(res, 502, 'Bad Gateway');
54
+ return;
55
+ }
56
+ const maxBodySize = route.maxBodySize ?? this.config.maxBodySize;
57
+ if (maxBodySize !== undefined) {
58
+ const contentLength = Number(req.headers['content-length']);
59
+ if (!Number.isNaN(contentLength) && contentLength > maxBodySize) {
60
+ req.resume();
61
+ this.sendError(res, 413, 'Payload too large.');
62
+ return;
63
+ }
64
+ }
65
+ const candidates = this.healthyCandidates(route.upstreams);
66
+ if (candidates.length === 0) {
67
+ await this.handleError(new Error(`No upstreams configured for route '${pathname}'`), { req, route }, res);
68
+ return;
69
+ }
70
+ const balancer = this.getBalancer(route);
71
+ const upstream = balancer.pick(candidates);
72
+ const targetPath = (0, rewrite_path_js_1.rewritePath)(pathname, route.rewrite) + (url.search ?? '');
73
+ const ctx = { req, res, route, upstream, targetPath };
74
+ if (this.hooks.onRequest) {
75
+ try {
76
+ const proceed = await this.hooks.onRequest({
77
+ req,
78
+ route,
79
+ upstream,
80
+ targetPath,
81
+ });
82
+ if (!proceed) {
83
+ this.sendError(res, 403, 'Forbidden');
84
+ return;
85
+ }
86
+ }
87
+ catch (err) {
88
+ await this.handleError(err instanceof Error ? err : new Error(String(err)), ctx, res);
89
+ return;
90
+ }
91
+ }
92
+ await this.forward(ctx, new Set([upstream]));
93
+ }
94
+ forward(ctx, tried) {
95
+ return new Promise((resolve) => {
96
+ const { req, res, upstream, targetPath, route } = ctx;
97
+ const protocol = upstream.protocol ?? 'http';
98
+ const transport = protocol === 'https' ? node_https_1.default : node_http_1.default;
99
+ const forwardHeaders = this.headersService.buildRequestHeaders(req, route.headers, upstream);
100
+ const options = {
101
+ hostname: upstream.host,
102
+ port: upstream.port,
103
+ method: req.method,
104
+ path: targetPath,
105
+ headers: forwardHeaders,
106
+ timeout: route.timeout ?? this.config.timeout ?? 30000,
107
+ agent: protocol === 'https' ? this.httpsAgent : this.httpAgent,
108
+ };
109
+ const proxyReq = transport.request(options, (proxyRes) => {
110
+ res.statusCode = proxyRes.statusCode ?? 502;
111
+ for (const [k, v] of Object.entries(proxyRes.headers)) {
112
+ if (v !== undefined)
113
+ res.setHeader(k, v);
114
+ }
115
+ this.headersService.applyResponseHeaders(res, route.headers);
116
+ proxyRes.on('error', (err) => {
117
+ this.handleError(err, ctx, res).then(resolve);
118
+ });
119
+ if (this.hooks.onResponse) {
120
+ Promise.resolve(this.hooks.onResponse(ctx, res.statusCode)).then(() => {
121
+ proxyRes.pipe(res, { end: true });
122
+ proxyRes.on('end', resolve);
123
+ }, (err) => this.handleError(err instanceof Error ? err : new Error(String(err)), ctx, res).then(resolve));
124
+ }
125
+ else {
126
+ proxyRes.pipe(res, { end: true });
127
+ proxyRes.on('end', resolve);
128
+ }
129
+ });
130
+ proxyReq.on('timeout', () => {
131
+ proxyReq.destroy();
132
+ this.handleError(new Error(`Upstream request timed out ${upstream.host}:${upstream.port}`), ctx, res).then(resolve);
133
+ });
134
+ let bodyTooLarge = false;
135
+ let bodyConsumed = false;
136
+ proxyReq.on('error', (err) => {
137
+ if (bodyTooLarge) {
138
+ if (!res.headersSent)
139
+ this.sendError(res, 413, 'Payload Too Large');
140
+ resolve();
141
+ return;
142
+ }
143
+ if (!res.headersSent && this.canRetry(req, bodyConsumed)) {
144
+ tried.add(upstream);
145
+ const remaining = route.upstreams.filter((u) => !tried.has(u));
146
+ const healthyRemaining = remaining.filter((u) => this.healthService.isHealthy(u));
147
+ const candidates = healthyRemaining.length > 0 ? healthyRemaining : remaining;
148
+ if (candidates.length > 0) {
149
+ const balancer = this.getBalancer(route);
150
+ const nextUpstream = balancer.pick(candidates);
151
+ const nextCtx = { ...ctx, upstream: nextUpstream };
152
+ this.forward(nextCtx, tried).then(resolve);
153
+ return;
154
+ }
155
+ }
156
+ this.handleError(err, ctx, res).then(resolve);
157
+ });
158
+ req.on('error', (err) => {
159
+ this.handleError(err, ctx, res).then(resolve);
160
+ });
161
+ const maxBodySize = route.maxBodySize ?? this.config.maxBodySize;
162
+ let bodyBytes = 0;
163
+ req.on('data', (chunk) => {
164
+ bodyConsumed = true;
165
+ if (maxBodySize === undefined)
166
+ return;
167
+ bodyBytes += chunk.length;
168
+ if (!bodyTooLarge && bodyBytes > maxBodySize) {
169
+ bodyTooLarge = true;
170
+ req.resume();
171
+ proxyReq.destroy();
172
+ }
173
+ });
174
+ req.pipe(proxyReq, { end: true });
175
+ });
176
+ }
177
+ canRetry(req, bodyConsumed) {
178
+ if (bodyConsumed)
179
+ return false;
180
+ const method = (req.method ?? 'GET').toUpperCase();
181
+ if (!RETRY_SAFE_METHODS.has(method))
182
+ return false;
183
+ const clRaw = req.headers['content-length'];
184
+ const clStr = Array.isArray(clRaw) ? clRaw[0] : clRaw;
185
+ if (clStr !== undefined) {
186
+ const clNum = Number(clStr);
187
+ if (!Number.isFinite(clNum) || clNum !== 0)
188
+ return false;
189
+ }
190
+ if (req.headers['transfer-encoding'])
191
+ return false;
192
+ return true;
193
+ }
194
+ healthyCandidates(upstreams) {
195
+ const healthy = upstreams.filter((u) => this.healthService.isHealthy(u));
196
+ return healthy.length > 0 ? healthy : upstreams;
197
+ }
198
+ getBalancer(route) {
199
+ if (!route.balancer)
200
+ return this.globalBalancer;
201
+ if (!this.balancers.has(route)) {
202
+ this.balancers.set(route, (0, create_balancer_js_1.createBalancer)(route.balancer));
203
+ }
204
+ return this.balancers.get(route) ?? this.globalBalancer;
205
+ }
206
+ async handleError(err, ctx, res) {
207
+ if (!res.headersSent) {
208
+ this.sendError(res, 502, 'Bad Gateway');
209
+ }
210
+ try {
211
+ await this.hooks.onError?.(err, ctx);
212
+ }
213
+ catch (hookErr) {
214
+ this.logger.error('hooks.onError threw', {
215
+ message: String(hookErr),
216
+ });
217
+ }
218
+ }
219
+ sendError(res, status, message) {
220
+ if (res.headersSent)
221
+ return;
222
+ res.writeHead(status, { 'content-type': 'text/plain' });
223
+ res.end(`${status} ${message}`);
224
+ }
225
+ }
226
+ exports.HttpHandler = HttpHandler;
@@ -0,0 +1,11 @@
1
+ import type { IncomingMessage } from 'node:http';
2
+ import net from 'node:net';
3
+ import type { HeadersService } from '../headers/headers-service.js';
4
+ import type { HttpHandler } from './http-handler.js';
5
+ export declare class WebSocketHandler {
6
+ private readonly httpHandler;
7
+ private readonly headersService;
8
+ constructor(httpHandler: HttpHandler, headersService: HeadersService);
9
+ upgrade(req: IncomingMessage, socket: net.Socket, head: Buffer): Promise<void>;
10
+ private fail;
11
+ }
@@ -0,0 +1,100 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.WebSocketHandler = void 0;
7
+ const node_net_1 = __importDefault(require("node:net"));
8
+ const node_tls_1 = __importDefault(require("node:tls"));
9
+ const match_route_js_1 = require("../../../utils/match-route.js");
10
+ const rewrite_path_js_1 = require("../../../utils/rewrite-path.js");
11
+ class WebSocketHandler {
12
+ httpHandler;
13
+ headersService;
14
+ constructor(httpHandler, headersService) {
15
+ this.httpHandler = httpHandler;
16
+ this.headersService = headersService;
17
+ }
18
+ upgrade(req, socket, head) {
19
+ return new Promise((resolve, reject) => {
20
+ const { config } = this.httpHandler;
21
+ const url = new URL(req.url ?? '/', 'http://localhost');
22
+ const route = (0, match_route_js_1.matchRoute)(config.routes, url.pathname);
23
+ if (!route || route.upstreams.length === 0) {
24
+ this.fail(socket, 502, 'Bad Gateway');
25
+ reject(new Error(route
26
+ ? `No upstreams configured for route '${url.pathname}'`
27
+ : `No matching route for WebSocket upgrade '${url.pathname}'`));
28
+ return;
29
+ }
30
+ const candidates = this.httpHandler.healthyCandidates(route.upstreams);
31
+ const upstream = this.httpHandler.getBalancer(route).pick(candidates);
32
+ const targetPath = (0, rewrite_path_js_1.rewritePath)(url.pathname, route.rewrite) + (url.search ?? '');
33
+ const upgradeHeaders = this.headersService.buildUpgradeHeaders(req, route.headers, upstream);
34
+ const requestLine = `${req.method ?? 'GET'} ${targetPath} HTTP/1.1\r\n`;
35
+ const headerBlock = `${Object.entries(upgradeHeaders)
36
+ .filter(([, v]) => v !== undefined)
37
+ .map(([k, v]) => `${k}: ${Array.isArray(v) ? v.join(', ') : v}`)
38
+ .join('\r\n')}\r\n\r\n`;
39
+ const useTls = upstream.protocol === 'https';
40
+ const connectOptions = { host: upstream.host, port: upstream.port };
41
+ let settled = false;
42
+ let connected = false;
43
+ const teardown = () => {
44
+ upstreamSocket.setTimeout(0);
45
+ if (!socket.destroyed)
46
+ socket.destroy();
47
+ if (!upstreamSocket.destroyed)
48
+ upstreamSocket.destroy();
49
+ };
50
+ const settleResolve = () => {
51
+ if (settled)
52
+ return;
53
+ settled = true;
54
+ resolve();
55
+ };
56
+ const fail = (err) => {
57
+ if (settled) {
58
+ teardown();
59
+ return;
60
+ }
61
+ settled = true;
62
+ if (!connected) {
63
+ this.fail(socket, 502, 'Bad Gateway');
64
+ upstreamSocket.setTimeout(0);
65
+ if (!upstreamSocket.destroyed)
66
+ upstreamSocket.destroy();
67
+ }
68
+ else {
69
+ teardown();
70
+ }
71
+ reject(err);
72
+ };
73
+ const onConnect = () => {
74
+ connected = true;
75
+ upstreamSocket.setTimeout(0);
76
+ upstreamSocket.write(requestLine + headerBlock);
77
+ if (head.length > 0)
78
+ upstreamSocket.write(head);
79
+ socket.pipe(upstreamSocket);
80
+ upstreamSocket.pipe(socket);
81
+ settleResolve();
82
+ };
83
+ const upstreamSocket = useTls
84
+ ? node_tls_1.default.connect(connectOptions, onConnect)
85
+ : node_net_1.default.createConnection(connectOptions, onConnect);
86
+ upstreamSocket.on('error', (err) => fail(err instanceof Error ? err : new Error(String(err))));
87
+ upstreamSocket.setTimeout(route.timeout ?? config.timeout ?? 30000, () => fail(new Error(`Upstream WebSocket connection timed out ${upstream.host}:${upstream.port}`)));
88
+ socket.on('error', (err) => fail(err instanceof Error ? err : new Error(String(err))));
89
+ socket.on('close', teardown);
90
+ upstreamSocket.on('close', teardown);
91
+ });
92
+ }
93
+ fail(socket, status, message) {
94
+ if (!socket.destroyed && socket.writable) {
95
+ socket.write(`HTTP/1.1 ${status} ${message}\r\n\r\n`);
96
+ }
97
+ socket.destroy();
98
+ }
99
+ }
100
+ exports.WebSocketHandler = WebSocketHandler;
@@ -0,0 +1,18 @@
1
+ import type { IncomingHttpHeaders, IncomingMessage, ServerResponse } from 'node:http';
2
+ import type { HeaderRules } from '../../types/header-rules.js';
3
+ export declare class HeadersService {
4
+ private readonly globalRules;
5
+ private readonly forwardIp;
6
+ constructor(globalRules: HeaderRules | undefined, forwardIp: boolean);
7
+ buildRequestHeaders(req: IncomingMessage, routeRules: HeaderRules | undefined, upstream: {
8
+ host: string;
9
+ port: number;
10
+ }): IncomingHttpHeaders;
11
+ buildUpgradeHeaders(req: IncomingMessage, routeRules: HeaderRules | undefined, upstream: {
12
+ host: string;
13
+ port: number;
14
+ }): IncomingHttpHeaders;
15
+ applyResponseHeaders(res: ServerResponse, routeRules: HeaderRules | undefined): void;
16
+ private stripHopByHop;
17
+ private applyRules;
18
+ }
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HeadersService = void 0;
4
+ const hop_by_hop_headers_js_1 = require("../../constants/hop-by-hop-headers.js");
5
+ class HeadersService {
6
+ globalRules;
7
+ forwardIp;
8
+ constructor(globalRules, forwardIp) {
9
+ this.globalRules = globalRules;
10
+ this.forwardIp = forwardIp;
11
+ }
12
+ buildRequestHeaders(req, routeRules, upstream) {
13
+ const headers = { ...req.headers };
14
+ this.stripHopByHop(headers);
15
+ headers.host = `${upstream.host}:${upstream.port}`;
16
+ if (this.forwardIp) {
17
+ const existing = headers['x-forwarded-for'];
18
+ const clientIp = req.socket.remoteAddress ?? 'unknown';
19
+ const existingStr = Array.isArray(existing)
20
+ ? existing.join(', ')
21
+ : existing;
22
+ headers['x-forwarded-for'] = existingStr
23
+ ? `${existingStr}, ${clientIp}`
24
+ : clientIp;
25
+ headers['x-forwarded-proto'] ??= req.socket
26
+ .encrypted
27
+ ? 'https'
28
+ : 'http';
29
+ headers['x-forwarded-host'] ??= req.headers.host ?? '';
30
+ }
31
+ this.applyRules(headers, this.globalRules, 'request');
32
+ this.applyRules(headers, routeRules, 'request');
33
+ return headers;
34
+ }
35
+ buildUpgradeHeaders(req, routeRules, upstream) {
36
+ const headers = this.buildRequestHeaders(req, routeRules, upstream);
37
+ if (req.headers.upgrade)
38
+ headers.upgrade = req.headers.upgrade;
39
+ headers.connection = req.headers.connection ?? 'Upgrade';
40
+ if (req.headers['sec-websocket-key']) {
41
+ headers['sec-websocket-key'] = req.headers['sec-websocket-key'];
42
+ }
43
+ if (req.headers['sec-websocket-version']) {
44
+ headers['sec-websocket-version'] = req.headers['sec-websocket-version'];
45
+ }
46
+ if (req.headers['sec-websocket-protocol']) {
47
+ headers['sec-websocket-protocol'] = req.headers['sec-websocket-protocol'];
48
+ }
49
+ if (req.headers['sec-websocket-extensions']) {
50
+ headers['sec-websocket-extensions'] =
51
+ req.headers['sec-websocket-extensions'];
52
+ }
53
+ return headers;
54
+ }
55
+ applyResponseHeaders(res, routeRules) {
56
+ for (const key of [
57
+ ...(this.globalRules?.removeResponse ?? []),
58
+ ...(routeRules?.removeResponse ?? []),
59
+ ]) {
60
+ res.removeHeader(key.toLowerCase());
61
+ }
62
+ for (const rules of [this.globalRules?.response, routeRules?.response]) {
63
+ if (!rules)
64
+ continue;
65
+ for (const [key, value] of Object.entries(rules)) {
66
+ res.setHeader(key.toLowerCase(), value);
67
+ }
68
+ }
69
+ }
70
+ stripHopByHop(headers) {
71
+ const cxn = headers.connection;
72
+ if (typeof cxn === 'string') {
73
+ for (const name of cxn.split(',')) {
74
+ delete headers[name.trim().toLowerCase()];
75
+ }
76
+ }
77
+ for (const name of hop_by_hop_headers_js_1.HOP_BY_HOP_HEADERS) {
78
+ delete headers[name];
79
+ }
80
+ }
81
+ applyRules(headers, rules, direction) {
82
+ if (!rules)
83
+ return;
84
+ const removeKey = direction === 'request' ? 'removeRequest' : 'removeResponse';
85
+ const addKey = direction;
86
+ for (const key of rules[removeKey] ?? []) {
87
+ delete headers[key.toLowerCase()];
88
+ }
89
+ for (const [key, value] of Object.entries(rules[addKey] ?? {})) {
90
+ headers[key.toLowerCase()] = value;
91
+ }
92
+ }
93
+ }
94
+ exports.HeadersService = HeadersService;
@@ -0,0 +1,15 @@
1
+ import type { Upstream } from '../../types/upstream.js';
2
+ export declare class HealthService {
3
+ private readonly upstreams;
4
+ private readonly interval;
5
+ private readonly timeout;
6
+ private readonly unhealthy;
7
+ private readonly timers;
8
+ private started;
9
+ constructor(upstreams: Upstream[], interval?: number, timeout?: number);
10
+ start(): void;
11
+ stop(): void;
12
+ isHealthy(u: Upstream): boolean;
13
+ private key;
14
+ private probe;
15
+ }
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.HealthService = void 0;
7
+ const node_net_1 = __importDefault(require("node:net"));
8
+ class HealthService {
9
+ upstreams;
10
+ interval;
11
+ timeout;
12
+ unhealthy = new Set();
13
+ timers = [];
14
+ started = false;
15
+ constructor(upstreams, interval = 30000, timeout = 5000) {
16
+ this.upstreams = upstreams;
17
+ this.interval = interval;
18
+ this.timeout = timeout;
19
+ }
20
+ start() {
21
+ if (this.started)
22
+ return;
23
+ this.started = true;
24
+ for (const u of this.upstreams) {
25
+ this.probe(u);
26
+ const timer = setInterval(() => this.probe(u), this.interval);
27
+ timer.unref();
28
+ this.timers.push(timer);
29
+ }
30
+ }
31
+ stop() {
32
+ for (const t of this.timers) {
33
+ clearInterval(t);
34
+ }
35
+ this.timers.length = 0;
36
+ this.started = false;
37
+ }
38
+ isHealthy(u) {
39
+ return !this.unhealthy.has(this.key(u));
40
+ }
41
+ key(u) {
42
+ return `${u.host}:${u.port}`;
43
+ }
44
+ probe(u) {
45
+ const key = this.key(u);
46
+ const socket = node_net_1.default.createConnection({
47
+ host: u.host,
48
+ port: u.port,
49
+ });
50
+ socket.setTimeout(this.timeout);
51
+ socket.on('connect', () => {
52
+ this.unhealthy.delete(key);
53
+ socket.destroy();
54
+ });
55
+ socket.on('error', () => {
56
+ this.unhealthy.add(key);
57
+ socket.destroy();
58
+ });
59
+ socket.on('timeout', () => {
60
+ this.unhealthy.add(key);
61
+ socket.destroy();
62
+ });
63
+ }
64
+ }
65
+ exports.HealthService = HealthService;
@@ -0,0 +1,3 @@
1
+ import { LoadBalancerStrategy } from '../../types/load-balancer-strategy.js';
2
+ import type { LoadBalancer } from './load-balancer.js';
3
+ export declare function createBalancer(strategy: LoadBalancerStrategy): LoadBalancer;
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createBalancer = createBalancer;
4
+ const load_balancer_strategy_js_1 = require("../../types/load-balancer-strategy.js");
5
+ const random_balancer_js_1 = require("./random-balancer.js");
6
+ const round_robin_balancer_js_1 = require("./round-robin-balancer.js");
7
+ const weighted_balancer_js_1 = require("./weighted-balancer.js");
8
+ function createBalancer(strategy) {
9
+ switch (strategy) {
10
+ case load_balancer_strategy_js_1.LoadBalancerStrategy.RoundRobin:
11
+ return new round_robin_balancer_js_1.RoundRobinBalancer();
12
+ case load_balancer_strategy_js_1.LoadBalancerStrategy.Random:
13
+ return new random_balancer_js_1.RandomBalancer();
14
+ case load_balancer_strategy_js_1.LoadBalancerStrategy.Weighted:
15
+ return new weighted_balancer_js_1.WeightedBalancer();
16
+ default: {
17
+ const exhaustive = strategy;
18
+ throw new Error(`Unknown load balancer strategy: ${exhaustive}`);
19
+ }
20
+ }
21
+ }
@@ -0,0 +1,4 @@
1
+ import type { Upstream } from '../../types/upstream.js';
2
+ export declare abstract class LoadBalancer {
3
+ abstract pick(upstreams: Upstream[]): Upstream;
4
+ }
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LoadBalancer = void 0;
4
+ class LoadBalancer {
5
+ }
6
+ exports.LoadBalancer = LoadBalancer;
@@ -0,0 +1,5 @@
1
+ import type { Upstream } from '../../types/upstream.js';
2
+ import { LoadBalancer } from './load-balancer.js';
3
+ export declare class RandomBalancer extends LoadBalancer {
4
+ pick(upstreams: Upstream[]): Upstream;
5
+ }
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RandomBalancer = void 0;
4
+ const load_balancer_js_1 = require("./load-balancer.js");
5
+ class RandomBalancer extends load_balancer_js_1.LoadBalancer {
6
+ pick(upstreams) {
7
+ if (upstreams.length === 0) {
8
+ throw new Error('No upstreams available');
9
+ }
10
+ const randomIndex = Math.floor(Math.random() * upstreams.length);
11
+ return upstreams[randomIndex];
12
+ }
13
+ }
14
+ exports.RandomBalancer = RandomBalancer;
@@ -0,0 +1,6 @@
1
+ import type { Upstream } from '../../types/upstream.js';
2
+ import { LoadBalancer } from './load-balancer.js';
3
+ export declare class RoundRobinBalancer extends LoadBalancer {
4
+ private idx;
5
+ pick(upstreams: Upstream[]): Upstream;
6
+ }
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RoundRobinBalancer = void 0;
4
+ const load_balancer_js_1 = require("./load-balancer.js");
5
+ class RoundRobinBalancer extends load_balancer_js_1.LoadBalancer {
6
+ idx = 0;
7
+ pick(upstreams) {
8
+ if (upstreams.length === 0) {
9
+ throw new Error('No upstreams available');
10
+ }
11
+ const upstream = upstreams[this.idx % upstreams.length];
12
+ this.idx = (this.idx + 1) % upstreams.length;
13
+ return upstream;
14
+ }
15
+ }
16
+ exports.RoundRobinBalancer = RoundRobinBalancer;
@@ -0,0 +1,5 @@
1
+ import type { Upstream } from '../../types/upstream.js';
2
+ import { LoadBalancer } from './load-balancer.js';
3
+ export declare class WeightedBalancer extends LoadBalancer {
4
+ pick(upstreams: Upstream[]): Upstream;
5
+ }
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WeightedBalancer = void 0;
4
+ const load_balancer_js_1 = require("./load-balancer.js");
5
+ class WeightedBalancer extends load_balancer_js_1.LoadBalancer {
6
+ pick(upstreams) {
7
+ if (upstreams.length === 0) {
8
+ throw new Error('No upstreams available');
9
+ }
10
+ const weights = upstreams.map((upstream) => {
11
+ const weight = upstream.weight ?? 1;
12
+ if (!Number.isFinite(weight) || weight <= 0) {
13
+ throw new Error('WeightedBalancer.pick() requires all upstream weights to be positive finite numbers.');
14
+ }
15
+ return weight;
16
+ });
17
+ const totalWeight = weights.reduce((sum, weight) => sum + weight, 0);
18
+ let random = Math.random() * totalWeight;
19
+ for (let i = 0; i < upstreams.length; i++) {
20
+ random -= weights[i];
21
+ if (random <= 0)
22
+ return upstreams[i];
23
+ }
24
+ return upstreams[upstreams.length - 1];
25
+ }
26
+ }
27
+ exports.WeightedBalancer = WeightedBalancer;