local-traffic 0.0.39 → 0.0.42

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/index.ts CHANGED
@@ -42,20 +42,20 @@ enum LogLevel {
42
42
  }
43
43
 
44
44
  enum EMOJIS {
45
- INBOUND = '↘️ ',
46
- PORT = '☎️ ',
47
- OUTBOUND = '↗️ ',
48
- RULES = '🔗',
49
- BODY_REPLACEMENT = '✒️ ',
50
- WEBSOCKET = '☄️ ',
51
- COLORED = '',
52
- NO = '',
53
- ERROR_1 = '',
54
- ERROR_2 = '⛈️ ',
55
- ERROR_3 = '☢️ ',
56
- ERROR_4 = '⁉️ ',
57
- ERROR_5 = '',
58
- ERROR_6 = '☠️ ',
45
+ INBOUND = "↘️ ",
46
+ PORT = "☎️ ",
47
+ OUTBOUND = "↗️ ",
48
+ RULES = "🔗",
49
+ BODY_REPLACEMENT = "✒️ ",
50
+ WEBSOCKET = "☄️ ",
51
+ COLORED = "",
52
+ NO = "",
53
+ ERROR_1 = "",
54
+ ERROR_2 = "⛈️ ",
55
+ ERROR_3 = "☢️ ",
56
+ ERROR_4 = "⁉️ ",
57
+ ERROR_5 = "",
58
+ ERROR_6 = "☠️ ",
59
59
  }
60
60
 
61
61
  interface LocalConfiguration {
@@ -73,7 +73,7 @@ const filename = resolve(
73
73
  process.cwd(),
74
74
  process.argv.slice(-1)[0].endsWith(".json")
75
75
  ? process.argv.slice(-1)[0]
76
- : userHomeConfigFile
76
+ : userHomeConfigFile,
77
77
  );
78
78
  const defaultConfig: LocalConfiguration = {
79
79
  mapping: {},
@@ -90,7 +90,7 @@ const getCurrentTime = (simpleLogs?: boolean) => {
90
90
  const date = new Date();
91
91
  return `${simpleLogs ? "" : "\u001b[36m"}${`${date.getHours()}`.padStart(
92
92
  2,
93
- "0"
93
+ "0",
94
94
  )}${
95
95
  simpleLogs ? ":" : "\u001b[33m:\u001b[36m"
96
96
  }${`${date.getMinutes()}`.padStart(2, "0")}${
@@ -99,8 +99,8 @@ const getCurrentTime = (simpleLogs?: boolean) => {
99
99
  };
100
100
  const log = (text: string, level?: LogLevel, emoji?: string) => {
101
101
  console.log(
102
- `${getCurrentTime(config.simpleLogs)} ${
103
- config.simpleLogs
102
+ `${getCurrentTime(config?.simpleLogs)} ${
103
+ config?.simpleLogs
104
104
  ? text
105
105
  .replace(/⎸/g, "|")
106
106
  .replace(/⎹/g, "|")
@@ -108,40 +108,75 @@ const log = (text: string, level?: LogLevel, emoji?: string) => {
108
108
  .replace(new RegExp(EMOJIS.INBOUND, "g"), "inbound:")
109
109
  .replace(new RegExp(EMOJIS.PORT, "g"), "port:")
110
110
  .replace(new RegExp(EMOJIS.OUTBOUND, "g"), "outbound:")
111
- .replace(new RegExp(EMOJIS.RULES, "g"), 'rules:')
112
- .replace(new RegExp(EMOJIS.NO, "g"), '')
113
- .replace(new RegExp(EMOJIS.BODY_REPLACEMENT, "g"), 'body replacement')
114
- .replace(new RegExp(EMOJIS.WEBSOCKET, "g"), 'websocket')
115
- .replace(/\|+/g, '|')
111
+ .replace(new RegExp(EMOJIS.RULES, "g"), "rules:")
112
+ .replace(new RegExp(EMOJIS.NO, "g"), "")
113
+ .replace(
114
+ new RegExp(EMOJIS.BODY_REPLACEMENT, "g"),
115
+ "body replacement",
116
+ )
117
+ .replace(new RegExp(EMOJIS.WEBSOCKET, "g"), "websocket")
118
+ .replace(/\|+/g, "|")
116
119
  : level
117
120
  ? `\u001b[48;5;${level}m⎸ ${
118
121
  !process.stdout.isTTY ? "" : emoji || ""
119
122
  } ${text.padEnd(36)} ⎹\u001b[0m`
120
123
  : text
121
- }`
124
+ }`,
125
+ );
126
+ };
127
+
128
+ const quickStatus = (thisConfig: LocalConfiguration) => {
129
+ log(
130
+ `\u001b[48;5;52m⎸${EMOJIS.PORT} ${thisConfig.port
131
+ .toString()
132
+ .padStart(5)} \u001b[48;5;53m⎸${EMOJIS.INBOUND} ${
133
+ thisConfig.ssl ? "H/2 " : "H1.1"
134
+ } \u001b[48;5;54m⎸${EMOJIS.OUTBOUND} ${
135
+ thisConfig.dontUseHttp2Downstream ? "H1.1" : "H/2 "
136
+ }⎹\u001b[48;5;55m⎸${EMOJIS.RULES}${Object.keys(config.mapping)
137
+ .length.toString()
138
+ .padStart(3)}⎹\u001b[48;5;56m⎸${
139
+ config.replaceResponseBodyUrls ? EMOJIS.BODY_REPLACEMENT : EMOJIS.NO
140
+ }⎹\u001b[48;5;57m⎸${
141
+ config.websocket ? EMOJIS.WEBSOCKET : EMOJIS.NO
142
+ }⎹\u001b[48;5;93m⎸${
143
+ !config.simpleLogs ? EMOJIS.COLORED : EMOJIS.NO
144
+ }⎹\u001b[0m`,
122
145
  );
123
146
  };
124
147
 
125
148
  const load = async (firstTime: boolean = true) =>
126
- new Promise((resolve) =>
149
+ new Promise(resolve =>
127
150
  readFile(filename, (error, data) => {
128
151
  if (error && !firstTime) {
129
- log("config error. Using default value", LogLevel.ERROR, EMOJIS.ERROR_1);
152
+ log(
153
+ "config error. Using default value",
154
+ LogLevel.ERROR,
155
+ EMOJIS.ERROR_1,
156
+ );
130
157
  }
131
158
  try {
132
159
  config = Object.assign(
133
160
  {},
134
161
  defaultConfig,
135
- JSON.parse((data || "{}").toString())
162
+ JSON.parse((data || "{}").toString()),
136
163
  );
137
164
  } catch (e) {
138
- log("config syntax incorrect, aborting", LogLevel.ERROR, EMOJIS.ERROR_2);
165
+ log(
166
+ "config syntax incorrect, aborting",
167
+ LogLevel.ERROR,
168
+ EMOJIS.ERROR_2,
169
+ );
139
170
  config = config || { ...defaultConfig };
140
171
  resolve(config);
141
172
  return;
142
173
  }
143
174
  if (!config.mapping[""]) {
144
- log('default mapping "" not provided.', LogLevel.WARNING, EMOJIS.ERROR_3);
175
+ log(
176
+ 'default mapping "" not provided.',
177
+ LogLevel.WARNING,
178
+ EMOJIS.ERROR_3,
179
+ );
145
180
  }
146
181
  if (
147
182
  error &&
@@ -149,36 +184,18 @@ const load = async (firstTime: boolean = true) =>
149
184
  firstTime &&
150
185
  filename === userHomeConfigFile
151
186
  ) {
152
- writeFile(filename, JSON.stringify(defaultConfig), (fileWriteErr) => {
187
+ writeFile(filename, JSON.stringify(defaultConfig), fileWriteErr => {
153
188
  if (fileWriteErr)
154
189
  log("config file NOT created", LogLevel.ERROR, EMOJIS.ERROR_4);
155
190
  else log("config file created", LogLevel.INFO, EMOJIS.COLORED);
156
191
  resolve(config);
157
192
  });
158
193
  } else resolve(config);
159
- })
194
+ }),
160
195
  ).then(() => {
161
196
  if (firstTime) watchFile(filename, onWatch);
162
197
  });
163
198
 
164
- const quickStatus = (thisConfig: LocalConfiguration) => {
165
- log(
166
- `\u001b[48;5;52m⎸${EMOJIS.PORT} ${thisConfig.port
167
- .toString()
168
- .padStart(5)} \u001b[48;5;53m⎸${EMOJIS.INBOUND} ${
169
- thisConfig.ssl ? "H/2 " : "H1.1"
170
- } \u001b[48;5;54m⎸${EMOJIS.OUTBOUND} ${
171
- thisConfig.dontUseHttp2Downstream ? "H1.1" : "H/2 "
172
- }⎹\u001b[48;5;55m⎸${EMOJIS.RULES}${Object.keys(config.mapping)
173
- .length.toString()
174
- .padStart(3)}⎹\u001b[48;5;56m⎸${
175
- config.replaceResponseBodyUrls ?
176
- EMOJIS.BODY_REPLACEMENT : EMOJIS.NO}⎹\u001b[48;5;57m⎸${
177
- config.websocket ? EMOJIS.WEBSOCKET : EMOJIS.NO}⎹\u001b[48;5;93m⎸${
178
- !config.simpleLogs ? EMOJIS.COLORED : EMOJIS.NO}⎹\u001b[0m`
179
- );
180
- };
181
-
182
199
  const onWatch = async () => {
183
200
  const previousConfig = { ...config };
184
201
  await load(false);
@@ -189,47 +206,63 @@ const onWatch = async () => {
189
206
  }
190
207
  if (typeof config.mapping !== "object") {
191
208
  config = previousConfig;
192
- log("mapping should be an object. Aborting", LogLevel.ERROR, EMOJIS.ERROR_5);
209
+ log(
210
+ "mapping should be an object. Aborting",
211
+ LogLevel.ERROR,
212
+ EMOJIS.ERROR_5,
213
+ );
193
214
  return;
194
215
  }
195
216
  if (
196
217
  config.replaceResponseBodyUrls !== previousConfig.replaceResponseBodyUrls
197
218
  ) {
198
- log(`response body url ${
199
- !config.replaceResponseBodyUrls ? 'NO ' : ''
200
- }replacement`, LogLevel.INFO, EMOJIS.BODY_REPLACEMENT);
219
+ log(
220
+ `response body url ${
221
+ !config.replaceResponseBodyUrls ? "NO " : ""
222
+ }replacement`,
223
+ LogLevel.INFO,
224
+ EMOJIS.BODY_REPLACEMENT,
225
+ );
201
226
  }
202
- if (
203
- config.dontUseHttp2Downstream !== previousConfig.dontUseHttp2Downstream
204
- ) {
205
- log(`http/2 ${
206
- config.dontUseHttp2Downstream ? 'de' : ''}activated downstream`, LogLevel.INFO, EMOJIS.OUTBOUND);
227
+ if (config.dontUseHttp2Downstream !== previousConfig.dontUseHttp2Downstream) {
228
+ log(
229
+ `http/2 ${config.dontUseHttp2Downstream ? "de" : ""}activated downstream`,
230
+ LogLevel.INFO,
231
+ EMOJIS.OUTBOUND,
232
+ );
207
233
  }
208
- if (
209
- config.websocket !== previousConfig.websocket
210
- ) {
211
- log(`websocket ${
212
- !config.websocket ? 'de' : ''}activated`, LogLevel.INFO, EMOJIS.WEBSOCKET);
234
+ if (config.websocket !== previousConfig.websocket) {
235
+ log(
236
+ `websocket ${!config.websocket ? "de" : ""}activated`,
237
+ LogLevel.INFO,
238
+ EMOJIS.WEBSOCKET,
239
+ );
240
+ }
241
+ if (config.simpleLogs !== previousConfig.simpleLogs) {
242
+ log(
243
+ `simple logs ${!config.simpleLogs ? "off" : "on"}`,
244
+ LogLevel.INFO,
245
+ EMOJIS.COLORED,
246
+ );
213
247
  }
214
248
  if (
215
- config.simpleLogs !== previousConfig.simpleLogs
249
+ Object.keys(config.mapping).join("\n") !==
250
+ Object.keys(previousConfig.mapping).join("\n")
216
251
  ) {
217
- log(`simple logs ${
218
- !config.simpleLogs ? 'off' : 'on'}`, LogLevel.INFO, EMOJIS.COLORED);
219
- }
220
- if (Object.keys(config.mapping).join('\n') !== Object.keys(previousConfig.mapping).join('\n')) {
221
252
  log(
222
253
  `${Object.keys(config.mapping)
223
254
  .length.toString()
224
255
  .padStart(5)} loaded mapping rules`,
225
256
  LogLevel.INFO,
226
- EMOJIS.RULES
257
+ EMOJIS.RULES,
227
258
  );
228
259
  }
229
- if (
230
- config.port !== previousConfig.port
231
- ) {
232
- log(`port changed from ${previousConfig.port} to ${config.port}`, LogLevel.INFO, EMOJIS.PORT);
260
+ if (config.port !== previousConfig.port) {
261
+ log(
262
+ `port changed from ${previousConfig.port} to ${config.port}`,
263
+ LogLevel.INFO,
264
+ EMOJIS.PORT,
265
+ );
233
266
  }
234
267
  if (config.ssl && !previousConfig.ssl) {
235
268
  log(`ssl configuration added`, LogLevel.INFO, EMOJIS.INBOUND);
@@ -241,8 +274,8 @@ const onWatch = async () => {
241
274
  config.port !== previousConfig.port ||
242
275
  JSON.stringify(config.ssl) !== JSON.stringify(previousConfig.ssl)
243
276
  ) {
244
- await new Promise((resolve) =>
245
- !server ? resolve(void 0) : server.close(resolve)
277
+ await new Promise(resolve =>
278
+ !server ? resolve(void 0) : server.close(resolve),
246
279
  );
247
280
  start();
248
281
  } else quickStatus(config);
@@ -255,7 +288,7 @@ const envs: () => { [prefix: string]: URL } = () => ({
255
288
  {},
256
289
  ...Object.entries(config.mapping).map(([key, value]) => ({
257
290
  [key]: new URL(unixNorm(value)),
258
- }))
291
+ })),
259
292
  ),
260
293
  });
261
294
 
@@ -266,117 +299,113 @@ const fileRequest = (url: URL): ClientHttp2Session => {
266
299
  ...url.pathname
267
300
  .replace(/[?#].*$/, "")
268
301
  .replace(/^\/+/, "")
269
- .split("/")
302
+ .split("/"),
270
303
  );
271
- const clientRequest = function () {
272
- return {
273
- error: null as Error,
274
- data: null as string | Buffer,
275
- hasRun: false,
276
- run: function () {
277
- return this.hasRun
278
- ? Promise.resolve()
279
- : new Promise((promiseResolve) =>
280
- readFile(file, (error, data) => {
281
- this.hasRun = true;
282
- if (!error || error.code !== "EISDIR") {
283
- this.error = error;
284
- this.data = data;
304
+ return {
305
+ error: null as Error,
306
+ data: null as string | Buffer,
307
+ hasRun: false,
308
+ run: function () {
309
+ return this.hasRun
310
+ ? Promise.resolve()
311
+ : new Promise(promiseResolve =>
312
+ readFile(file, (error, data) => {
313
+ this.hasRun = true;
314
+ if (!error || error.code !== "EISDIR") {
315
+ this.error = error;
316
+ this.data = data;
317
+ promiseResolve(void 0);
318
+ return;
319
+ }
320
+ readdir(file, (readDirError, filelist) => {
321
+ this.error = readDirError;
322
+ this.data = filelist;
323
+ if (readDirError) {
285
324
  promiseResolve(void 0);
286
325
  return;
287
326
  }
288
- readdir(file, (readDirError, filelist) => {
289
- this.error = readDirError;
290
- this.data = filelist;
291
- if (readDirError) {
292
- promiseResolve(void 0);
293
- return;
294
- }
295
- Promise.all(
296
- filelist.map(
297
- (file) =>
298
- new Promise((innerResolve) =>
299
- lstat(resolve(url.pathname, file), (err, stats) =>
300
- innerResolve([file, stats, err])
301
- )
302
- )
303
- )
304
- ).then((filesWithTypes) => {
305
- const entries = filesWithTypes
306
- .filter((entry) => !entry[2] && entry[1].isDirectory())
307
- .concat(
308
- filesWithTypes.filter(
309
- (entry) => !entry[2] && entry[1].isFile()
310
- )
311
- );
312
- this.data = `${header(0x1f4c2, "directory", url.href)}
313
- <p>Directory content of <i>${url.href.replace(
314
- /\//g,
315
- "&#x002F;"
316
- )}</i></p>
317
- <ul class="list-group">
318
- <li class="list-group-item">&#x1F4C1;<a href="${
319
- url.pathname.endsWith("/") ? ".." : "."
320
- }">&lt;parent&gt;</a></li>
321
- ${entries
322
- .filter((entry) => !entry[2])
323
- .map((entry) => {
324
- const type = entry[1].isDirectory()
325
- ? 0x1f4c1
326
- : 0x1f4c4;
327
- return `<li class="list-group-item">&#x${type.toString(
328
- 16
329
- )};<a href="${
330
- url.pathname.endsWith("/")
331
- ? ""
332
- : `${url.pathname.split("/").slice(-1)[0]}/`
333
- }${entry[0]}">${entry[0]}</a></li>`;
334
- })
335
- .join("\n")}
336
- </li>
337
- </ul>
338
- </body></html>`;
339
- promiseResolve(void 0);
340
- });
327
+ Promise.all(
328
+ filelist.map(
329
+ file =>
330
+ new Promise(innerResolve =>
331
+ lstat(resolve(url.pathname, file), (err, stats) =>
332
+ innerResolve([file, stats, err]),
333
+ ),
334
+ ),
335
+ ),
336
+ ).then(filesWithTypes => {
337
+ const entries = filesWithTypes
338
+ .filter(entry => !entry[2] && entry[1].isDirectory())
339
+ .concat(
340
+ filesWithTypes.filter(
341
+ entry => !entry[2] && entry[1].isFile(),
342
+ ),
343
+ );
344
+ this.data = `${header(
345
+ 0x1f4c2,
346
+ "directory",
347
+ url.href,
348
+ )}<p>Directory content of <i>${url.href.replace(
349
+ /\//g,
350
+ "&#x002F;",
351
+ )}</i></p><ul class="list-group"><li class="list-group-item">&#x1F4C1;<a href="${
352
+ url.pathname.endsWith("/") ? ".." : "."
353
+ }">&lt;parent&gt;</a></li>${entries
354
+ .filter(entry => !entry[2])
355
+ .map(entry => {
356
+ const type = entry[1].isDirectory() ? 0x1f4c1 : 0x1f4c4;
357
+ return `<li class="list-group-item">&#x${type.toString(
358
+ 16,
359
+ )};<a href="${
360
+ url.pathname.endsWith("/")
361
+ ? ""
362
+ : `${url.pathname.split("/").slice(-1)[0]}/`
363
+ }${entry[0]}">${entry[0]}</a></li>`;
364
+ })
365
+ .join("\n")}</li></ul></body></html>`;
366
+ promiseResolve(void 0);
341
367
  });
342
- })
343
- );
344
- },
345
- events: {} as { [name: string]: (...any: any) => any },
346
- on: function (name: string, action: (...any: any) => any) {
347
- this.events[name] = action;
348
- this.run().then(() => {
349
- if (name === "response")
350
- this.events["response"]({ Server: "local",
351
- 'Content-Type': file.endsWith('.svg') ? 'image/svg+xml' : null}, 0);
352
- if (name === "data" && this.data) {
353
- this.events["data"](this.data);
354
- this.events["end"]();
355
- }
356
- if (name === "error" && this.error) {
357
- this.events["error"](this.error);
358
- }
359
- });
360
- return this;
361
- },
362
- end: function () {
363
- return this;
364
- },
365
- request: function () {
366
- return this;
367
- },
368
- };
369
- };
370
-
371
- const newClientRequest = clientRequest();
372
-
373
- return (newClientRequest as any) as ClientHttp2Session;
368
+ });
369
+ }),
370
+ );
371
+ },
372
+ events: {} as { [name: string]: (...any: any) => any },
373
+ on: function (name: string, action: (...any: any) => any) {
374
+ this.events[name] = action;
375
+ this.run().then(() => {
376
+ if (name === "response")
377
+ this.events["response"](
378
+ file.endsWith(".svg")
379
+ ? {
380
+ Server: "local",
381
+ "Content-Type": "image/svg+xml",
382
+ }
383
+ : { Server: "local" },
384
+ 0,
385
+ );
386
+ if (name === "data" && this.data) {
387
+ this.events["data"](this.data);
388
+ this.events["end"]();
389
+ }
390
+ if (name === "error" && this.error) {
391
+ this.events["error"](this.error);
392
+ }
393
+ });
394
+ return this;
395
+ },
396
+ end: function () {
397
+ return this;
398
+ },
399
+ request: function () {
400
+ return this;
401
+ },
402
+ } as unknown as ClientHttp2Session;
374
403
  };
375
404
 
376
405
  const header = (
377
406
  icon: number,
378
407
  category: string,
379
- pageTitle: string
408
+ pageTitle: string,
380
409
  ) => `<!doctype html>
381
410
  <html lang="en">
382
411
  <head>
@@ -386,7 +415,7 @@ const header = (
386
415
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@latest/dist/js/bootstrap.bundle.min.js"></script>
387
416
  </head>
388
417
  <body><div class="container"><h1>&#x${icon.toString(
389
- 16
418
+ 16,
390
419
  )}; local-traffic ${category}</h1>
391
420
  <br/>`;
392
421
 
@@ -394,7 +423,7 @@ const errorPage = (
394
423
  thrown: Error,
395
424
  phase: string,
396
425
  requestedURL: URL,
397
- downstreamURL?: URL
426
+ downstreamURL?: URL,
398
427
  ) => `${header(0x1f4a3, "error", thrown.message)}
399
428
  <p>An error happened while trying to proxy a remote exchange</p>
400
429
  <div class="alert alert-warning" role="alert">
@@ -402,7 +431,9 @@ const errorPage = (
402
431
  </div>
403
432
  <div class="alert alert-danger" role="alert">
404
433
  <pre><code>${thrown.stack || `<i>${thrown.name} : ${thrown.message}</i>`}${
405
- (thrown as ErrorWithErrno).errno ? `<br/>(code : ${(thrown as ErrorWithErrno).errno})` : ""
434
+ (thrown as ErrorWithErrno).errno
435
+ ? `<br/>(code : ${(thrown as ErrorWithErrno).errno})`
436
+ : ""
406
437
  }</code></pre>
407
438
  </div>
408
439
  More information about the request :
@@ -427,7 +458,7 @@ More information about the request :
427
458
  const send = (
428
459
  code: number,
429
460
  inboundResponse: Http2ServerResponse | ServerResponse,
430
- errorBuffer: Buffer
461
+ errorBuffer: Buffer,
431
462
  ) => {
432
463
  inboundResponse.writeHead(
433
464
  code,
@@ -435,528 +466,589 @@ const send = (
435
466
  {
436
467
  "content-type": "text/html",
437
468
  "content-length": errorBuffer.length,
438
- }
469
+ },
439
470
  );
440
471
  inboundResponse.end(errorBuffer);
441
472
  };
442
473
 
443
- const determineMapping = (inboundRequest: Http2ServerRequest | IncomingMessage): {
444
- proxyHostname: string,
445
- proxyHostnameAndPort: string,
446
- url: URL,
447
- path: string,
448
- key: string,
449
- target: URL
474
+ const determineMapping = (
475
+ inboundRequest: Http2ServerRequest | IncomingMessage,
476
+ ): {
477
+ proxyHostname: string;
478
+ proxyHostnameAndPort: string;
479
+ url: URL;
480
+ path: string;
481
+ key: string;
482
+ target: URL;
450
483
  } => {
451
-
452
- const proxyHostname =
453
- (inboundRequest.headers[":authority"]?.toString() ??
454
- inboundRequest.headers.host ?? 'localhost').replace(/:.*/, '');
484
+ const proxyHostname = (
485
+ inboundRequest.headers[":authority"]?.toString() ??
486
+ inboundRequest.headers.host ??
487
+ "localhost"
488
+ ).replace(/:.*/, "");
455
489
  const proxyHostnameAndPort =
456
- inboundRequest.headers[":authority"] as string ||
457
- `${inboundRequest.headers.host}${inboundRequest.headers.host.match(/:[0-9]+$/)
458
- ? ""
459
- : config.port === 80 && !config.ssl
490
+ (inboundRequest.headers[":authority"] as string) ||
491
+ `${inboundRequest.headers.host}${
492
+ inboundRequest.headers.host.match(/:[0-9]+$/)
493
+ ? ""
494
+ : config.port === 80 && !config.ssl
460
495
  ? ""
461
496
  : config.port === 443 && config.ssl
462
- ? ""
463
- : `:${config.port}`
497
+ ? ""
498
+ : `:${config.port}`
464
499
  }`;
465
500
  const url = new URL(
466
- `http${config.ssl ? "s" : ""}://${proxyHostnameAndPort}${inboundRequest.url}`
501
+ `http${config.ssl ? "s" : ""}://${proxyHostnameAndPort}${
502
+ inboundRequest.url
503
+ }`,
467
504
  );
468
505
  const path = url.href.substring(url.origin.length);
469
506
  const [key, target] =
470
- Object.entries(envs()).find(([key]) => path.match(RegExp(key))) || [];
507
+ Object.entries(envs()).find(([key]) =>
508
+ path.match(RegExp(key.replace(/^\//, "^/"))),
509
+ ) || [];
471
510
  return { proxyHostname, proxyHostnameAndPort, url, path, key, target };
472
- }
511
+ };
473
512
 
474
513
  const start = () => {
475
- server = ((config.ssl
476
- ? createSecureServer.bind(null, { ...config.ssl, allowHTTP1: true })
477
- : createServer)(
478
- async (
479
- inboundRequest: Http2ServerRequest | IncomingMessage,
480
- inboundResponse: Http2ServerResponse | ServerResponse
481
- ) => {
482
- // phase: mapping
483
- if (!inboundRequest.headers.host && !inboundRequest.headers[":authority"]) {
484
- send(
485
- 400,
486
- inboundResponse,
487
- Buffer.from(
488
- errorPage(
489
- new Error(`client must supply a 'host' header`),
490
- "proxy",
491
- new URL(`http${config.ssl ? "s" : ""}://unknowndomain${inboundRequest.url}`)
492
- )
493
- )
514
+ server = (
515
+ (config.ssl
516
+ ? createSecureServer.bind(null, { ...config.ssl, allowHTTP1: true })
517
+ : createServer)(
518
+ async (
519
+ inboundRequest: Http2ServerRequest | IncomingMessage,
520
+ inboundResponse: Http2ServerResponse | ServerResponse,
521
+ ) => {
522
+ // phase: mapping
523
+ if (
524
+ !inboundRequest.headers.host &&
525
+ !inboundRequest.headers[":authority"]
526
+ ) {
527
+ send(
528
+ 400,
529
+ inboundResponse,
530
+ Buffer.from(
531
+ errorPage(
532
+ new Error(`client must supply a 'host' header`),
533
+ "proxy",
534
+ new URL(
535
+ `http${config.ssl ? "s" : ""}://unknowndomain${
536
+ inboundRequest.url
537
+ }`,
538
+ ),
539
+ ),
540
+ ),
541
+ );
542
+ return;
543
+ }
544
+ const { proxyHostname, proxyHostnameAndPort, url, path, key, target } =
545
+ determineMapping(inboundRequest);
546
+ if (!target) {
547
+ send(
548
+ 502,
549
+ inboundResponse,
550
+ Buffer.from(
551
+ errorPage(
552
+ new Error(`No mapping found in config file ${filename}`),
553
+ "proxy",
554
+ url,
555
+ ),
556
+ ),
557
+ );
558
+ return;
559
+ }
560
+ const targetHost = target.host.replace(RegExp(/\/+$/), "");
561
+ const targetPrefix = target.href.substring(
562
+ "https://".length + target.host.length,
494
563
  );
495
- return;
496
- }
497
- const { proxyHostname, proxyHostnameAndPort, url, path, key, target } =
498
- determineMapping(inboundRequest);
499
- if (!target) {
500
- send(
501
- 502,
502
- inboundResponse,
503
- Buffer.from(
504
- errorPage(
505
- new Error(`No mapping found in config file ${filename}`),
506
- "proxy",
507
- url
508
- )
509
- )
564
+ const fullPath = `${targetPrefix}${unixNorm(
565
+ path.replace(RegExp(unixNorm(key)), ""),
566
+ )}`.replace(/^\/*/, "/");
567
+ const targetUrl = new URL(
568
+ `${target.protocol}//${targetHost}${fullPath}`,
510
569
  );
511
- return;
512
- }
513
- const targetHost = target.host.replace(RegExp(/\/+$/), "");
514
- const targetPrefix = target.href.substring(
515
- "https://".length + target.host.length
516
- );
517
- const fullPath = `${targetPrefix}${unixNorm(
518
- path.replace(RegExp(unixNorm(key)), "")
519
- )}`.replace(/^\/*/, "/");
520
- const targetUrl = new URL(`${target.protocol}//${targetHost}${fullPath}`);
521
-
522
- // phase: connection
523
- let error: Buffer = null;
524
- let http2IsSupported = !config.dontUseHttp2Downstream;
525
- const outboundRequest: ClientHttp2Session =
526
- target.protocol === "file:"
527
- ? fileRequest(targetUrl)
528
- : !http2IsSupported
529
- ? null
530
- : await Promise.race([
531
- new Promise<ClientHttp2Session>((resolve) => {
532
- const result = connect(
570
+
571
+ // phase: connection
572
+ let error: Buffer = null;
573
+ let http2IsSupported = !config.dontUseHttp2Downstream;
574
+ const outboundRequest: ClientHttp2Session =
575
+ target.protocol === "file:"
576
+ ? fileRequest(targetUrl)
577
+ : !http2IsSupported
578
+ ? null
579
+ : await Promise.race([
580
+ new Promise<ClientHttp2Session>(resolve => {
581
+ const result = connect(
582
+ targetUrl,
583
+ {
584
+ rejectUnauthorized: false,
585
+ protocol: target.protocol,
586
+ } as SecureClientSessionOptions,
587
+ (_, socketPath) => {
588
+ http2IsSupported =
589
+ http2IsSupported && !!(socketPath as any).alpnProtocol;
590
+ resolve(!http2IsSupported ? null : result);
591
+ },
592
+ );
593
+ (result as unknown as Http2Session).on(
594
+ "error",
595
+ (thrown: Error) => {
596
+ error =
597
+ http2IsSupported &&
598
+ Buffer.from(
599
+ errorPage(thrown, "connection", url, targetUrl),
600
+ );
601
+ },
602
+ );
603
+ }),
604
+ new Promise<ClientHttp2Session>(resolve =>
605
+ setTimeout(() => {
606
+ http2IsSupported = false;
607
+ resolve(null);
608
+ }, 3000),
609
+ ),
610
+ ]);
611
+ if (!(error instanceof Buffer)) error = null;
612
+
613
+ const outboundHeaders: OutgoingHttpHeaders = {
614
+ ...[...Object.entries(inboundRequest.headers)]
615
+ // host and connection are forbidden in http/2
616
+ .filter(
617
+ ([key]) => !["host", "connection"].includes(key.toLowerCase()),
618
+ )
619
+ .reduce((acc: any, [key, value]) => {
620
+ acc[key] =
621
+ (acc[key] || "") +
622
+ (!Array.isArray(value) ? [value] : value)
623
+ .map(oneValue => oneValue.replace(url.hostname, targetHost))
624
+ .join(", ");
625
+ return acc;
626
+ }, {}),
627
+ origin: target.href,
628
+ referer: targetUrl.toString(),
629
+ ":authority": targetHost,
630
+ ":method": inboundRequest.method,
631
+ ":path": fullPath,
632
+ ":scheme": target.protocol.replace(":", ""),
633
+ };
634
+
635
+ const outboundExchange =
636
+ outboundRequest &&
637
+ !error &&
638
+ outboundRequest.request(outboundHeaders, {
639
+ endStream: config.ssl
640
+ ? !(
641
+ (inboundRequest as Http2ServerRequest)?.stream
642
+ ?.readableLength ?? true
643
+ )
644
+ : !(inboundRequest as IncomingMessage).readableLength,
645
+ });
646
+
647
+ outboundExchange &&
648
+ (outboundExchange as unknown as Http2Stream).on(
649
+ "error",
650
+ (thrown: Error) => {
651
+ const httpVersionSupported =
652
+ (thrown as ErrorWithErrno).errno === -505;
653
+ error = Buffer.from(
654
+ errorPage(
655
+ thrown,
656
+ "stream" +
657
+ (httpVersionSupported
658
+ ? " (error -505 usually means that the downstream service " +
659
+ "does not support this http version)"
660
+ : ""),
661
+ url,
533
662
  targetUrl,
534
- {
535
- rejectUnauthorized: false,
536
- protocol: target.protocol,
537
- } as SecureClientSessionOptions,
538
- (_, socketPath) => {
539
- http2IsSupported =
540
- http2IsSupported && !!(socketPath as any).alpnProtocol;
541
- resolve(!http2IsSupported ? null : result);
542
- }
543
- );
544
- ((result as unknown) as Http2Session).on(
545
- "error",
546
- (thrown: Error) => {
547
- error =
548
- http2IsSupported &&
549
- Buffer.from(
550
- errorPage(thrown, "connection", url, targetUrl)
551
- );
552
- }
553
- );
554
- }),
555
- new Promise<ClientHttp2Session>((resolve) =>
556
- setTimeout(() => {
557
- http2IsSupported = false;
558
- resolve(null);
559
- }, 3000)
560
- ),
561
- ]);
562
- if (!(error instanceof Buffer)) error = null;
563
-
564
- const outboundHeaders: OutgoingHttpHeaders = {
565
- ...[...Object.entries(inboundRequest.headers)]
566
- // host and connection are forbidden in http/2
567
- .filter(
568
- ([key]) => !["host", "connection"].includes(key.toLowerCase())
569
- )
570
- .reduce((acc: any, [key, value]) => {
571
- acc[key] =
572
- (acc[key] || "") +
573
- (!Array.isArray(value) ? [value] : value)
574
- .map((oneValue) => oneValue.replace(url.hostname, targetHost))
575
- .join(", ");
576
- return acc;
577
- }, {}),
578
- origin: target.href,
579
- referer: targetUrl.toString(),
580
- ":authority": targetHost,
581
- ":method": inboundRequest.method,
582
- ":path": fullPath,
583
- ":scheme": target.protocol.replace(":", ""),
584
- };
663
+ ),
664
+ );
665
+ },
666
+ );
585
667
 
586
- const outboundExchange =
587
- outboundRequest &&
588
- !error &&
589
- outboundRequest.request(outboundHeaders, {
590
- endStream: config.ssl
591
- ? !((inboundRequest as Http2ServerRequest)?.stream?.readableLength ?? true)
592
- : !(inboundRequest as IncomingMessage).readableLength,
593
- });
668
+ const http1RequestOptions: RequestOptions = {
669
+ hostname: target.hostname,
670
+ path: fullPath,
671
+ port: target.port,
672
+ protocol: target.protocol,
673
+ rejectUnauthorized: false,
674
+ method: inboundRequest.method,
675
+ headers: {
676
+ ...Object.assign(
677
+ {},
678
+ ...Object.entries(outboundHeaders)
679
+ .filter(
680
+ ([h]) =>
681
+ !h.startsWith(":") &&
682
+ h.toLowerCase() !== "transfer-encoding",
683
+ )
684
+ .map(([key, value]) => ({ [key]: value })),
685
+ ),
686
+ host: target.hostname,
687
+ },
688
+ };
689
+ const outboundHttp1Response: IncomingMessage =
690
+ !error &&
691
+ !http2IsSupported &&
692
+ target.protocol !== "file:" &&
693
+ (await new Promise(resolve => {
694
+ const outboundHttp1Request: ClientRequest =
695
+ target.protocol === "https:"
696
+ ? httpsRequest(http1RequestOptions, resolve)
697
+ : httpRequest(http1RequestOptions, resolve);
594
698
 
595
- outboundExchange &&
596
- ((outboundExchange as unknown) as Http2Stream).on(
597
- "error",
598
- (thrown: Error) => {
599
- const httpVersionSupported = (thrown as ErrorWithErrno).errno === -505;
600
- error = Buffer.from(
601
- errorPage(
602
- thrown,
603
- "stream" +
604
- (httpVersionSupported
605
- ? " (error -505 usually means that the downstream service " +
606
- "does not support this http version)"
607
- : ""),
608
- url,
609
- targetUrl
610
- )
699
+ outboundHttp1Request.on("error", thrown => {
700
+ error = Buffer.from(errorPage(thrown, "request", url, targetUrl));
701
+ resolve(null as IncomingMessage);
702
+ });
703
+ inboundRequest.on("data", chunk =>
704
+ outboundHttp1Request.write(chunk),
611
705
  );
612
- }
613
- );
706
+ inboundRequest.on("end", () => outboundHttp1Request.end());
707
+ }));
708
+ // intriguingly, error is reset to "false" at this point, even if it was null
709
+ if (error) {
710
+ send(502, inboundResponse, error);
711
+ return;
712
+ } else error = null;
614
713
 
615
- const http1RequestOptions: RequestOptions = {
616
- hostname: target.hostname,
617
- path: fullPath,
618
- port: target.port,
619
- protocol: target.protocol,
620
- rejectUnauthorized: false,
621
- method: inboundRequest.method,
622
- headers: {
623
- ...Object.assign(
624
- {},
625
- ...Object.entries(outboundHeaders)
626
- .filter(
627
- ([h]) =>
628
- !h.startsWith(":") && h.toLowerCase() !== "transfer-encoding"
629
- )
630
- .map(([key, value]) => ({ [key]: value }))
631
- ),
632
- host: target.hostname,
633
- },
634
- };
635
- const outboundHttp1Response: IncomingMessage =
636
- !error &&
637
- !http2IsSupported &&
638
- target.protocol !== "file:" &&
639
- (await new Promise((resolve) => {
640
- const outboundHttp1Request: ClientRequest =
641
- target.protocol === "https:"
642
- ? httpsRequest(http1RequestOptions, resolve)
643
- : httpRequest(http1RequestOptions, resolve);
644
-
645
- outboundHttp1Request.on("error", (thrown) => {
646
- error = Buffer.from(errorPage(thrown, "request", url, targetUrl));
647
- resolve(null as IncomingMessage);
648
- });
649
- inboundRequest.on("data", (chunk) =>
650
- outboundHttp1Request.write(chunk)
714
+ // phase : request body
715
+ if (
716
+ config.ssl && // http/2
717
+ (inboundRequest as Http2ServerRequest).stream &&
718
+ (inboundRequest as Http2ServerRequest).stream.readableLength &&
719
+ outboundExchange
720
+ ) {
721
+ (inboundRequest as Http2ServerRequest).stream.on("data", chunk =>
722
+ outboundExchange.write(chunk),
651
723
  );
652
- inboundRequest.on("end", () => outboundHttp1Request.end());
653
- }));
654
- // intriguingly, error is reset to "false" at this point, even if it was null
655
- if (error) {
656
- send(502, inboundResponse, error);
657
- return;
658
- } else error = null;
724
+ (inboundRequest as Http2ServerRequest).stream.on("end", () =>
725
+ outboundExchange.end(),
726
+ );
727
+ }
659
728
 
660
- // phase : request body
661
- if (
662
- config.ssl && // http/2
663
- (inboundRequest as Http2ServerRequest).stream &&
664
- (inboundRequest as Http2ServerRequest).stream.readableLength &&
665
- outboundExchange
666
- ) {
667
- (inboundRequest as Http2ServerRequest).stream.on("data", (chunk) =>
668
- outboundExchange.write(chunk)
669
- );
670
- (inboundRequest as Http2ServerRequest).stream.on("end", () =>
671
- outboundExchange.end()
672
- );
673
- }
729
+ if (
730
+ !config.ssl && // http1.1
731
+ (inboundRequest as IncomingMessage).readableLength &&
732
+ outboundExchange
733
+ ) {
734
+ (inboundRequest as IncomingMessage).on("data", chunk =>
735
+ outboundExchange.write(chunk),
736
+ );
737
+ (inboundRequest as IncomingMessage).on("end", () =>
738
+ outboundExchange.end(),
739
+ );
740
+ }
674
741
 
675
- if (
676
- !config.ssl && // http1.1
677
- (inboundRequest as IncomingMessage).readableLength &&
678
- outboundExchange
679
- ) {
680
- (inboundRequest as IncomingMessage).on("data", (chunk) =>
681
- outboundExchange.write(chunk)
682
- );
683
- (inboundRequest as IncomingMessage).on("end", () =>
684
- outboundExchange.end()
742
+ // phase : response headers
743
+ const { outboundResponseHeaders } = await new Promise(resolve =>
744
+ outboundExchange
745
+ ? outboundExchange.on("response", headers => {
746
+ resolve({
747
+ outboundResponseHeaders: headers,
748
+ });
749
+ })
750
+ : !outboundExchange && outboundHttp1Response
751
+ ? resolve({
752
+ outboundResponseHeaders: outboundHttp1Response.headers,
753
+ })
754
+ : resolve({
755
+ outboundResponseHeaders: {},
756
+ }),
685
757
  );
686
- }
687
758
 
688
- // phase : response headers
689
- const { outboundResponseHeaders } = await new Promise((resolve) =>
690
- outboundExchange
691
- ? outboundExchange.on("response", (headers) => {
692
- resolve({
693
- outboundResponseHeaders: headers,
694
- });
695
- })
696
- : !outboundExchange && outboundHttp1Response
697
- ? resolve({
698
- outboundResponseHeaders: outboundHttp1Response.headers,
699
- })
700
- : resolve({
701
- outboundResponseHeaders: {},
702
- })
703
- );
759
+ const newUrl = !outboundResponseHeaders["location"]
760
+ ? null
761
+ : new URL(
762
+ outboundResponseHeaders["location"].startsWith("/")
763
+ ? `${target.href}${outboundResponseHeaders["location"].replace(
764
+ /^\/+/,
765
+ ``,
766
+ )}`
767
+ : outboundResponseHeaders["location"],
768
+ );
769
+ const newPath = !newUrl
770
+ ? null
771
+ : newUrl.href.substring(newUrl.origin.length);
772
+ const newTarget = url.origin;
773
+ const newTargetUrl = !newUrl ? null : `${newTarget}${newPath}`;
704
774
 
705
- const newUrl = !outboundResponseHeaders["location"]
706
- ? null
707
- : new URL(
708
- outboundResponseHeaders["location"].startsWith("/")
709
- ? `${target.href}${outboundResponseHeaders["location"].replace(
710
- /^\/+/,
711
- ``
712
- )}`
713
- : outboundResponseHeaders["location"]
714
- );
715
- const newPath = !newUrl
716
- ? null
717
- : newUrl.href.substring(newUrl.origin.length);
718
- const newTarget = url.origin;
719
- const newTargetUrl = !newUrl ? null : `${newTarget}${newPath}`;
720
-
721
- // phase : response body
722
- const payloadSource = outboundExchange || outboundHttp1Response;
723
- const payload =
724
- error ??
725
- (await new Promise((resolve) => {
726
- let partialBody = Buffer.alloc(0);
727
- if (!payloadSource) {
728
- resolve(partialBody);
729
- return;
730
- }
731
- (payloadSource as ClientHttp2Stream | Duplex).on(
732
- "data",
733
- (chunk: Buffer | string) =>
734
- (partialBody = Buffer.concat([
735
- partialBody,
736
- typeof chunk === "string"
737
- ? Buffer.from(chunk as string)
738
- : (chunk as Buffer),
739
- ]))
775
+ // phase : response body
776
+ const payloadSource = outboundExchange || outboundHttp1Response;
777
+ const payload =
778
+ error ??
779
+ (await new Promise(resolve => {
780
+ let partialBody = Buffer.alloc(0);
781
+ if (!payloadSource) {
782
+ resolve(partialBody);
783
+ return;
784
+ }
785
+ (payloadSource as ClientHttp2Stream | Duplex).on(
786
+ "data",
787
+ (chunk: Buffer | string) =>
788
+ (partialBody = Buffer.concat([
789
+ partialBody,
790
+ typeof chunk === "string"
791
+ ? Buffer.from(chunk as string)
792
+ : (chunk as Buffer),
793
+ ])),
740
794
  );
741
- (payloadSource as any).on("end", () => {
742
- resolve(partialBody);
743
- });
744
- }).then((payloadBuffer: Buffer) => {
745
- if (!config.replaceResponseBodyUrls) return payloadBuffer;
746
- if (!payloadBuffer.length) return payloadBuffer;
747
-
748
- return (outboundResponseHeaders["content-encoding"] || "")
749
- .split(",")
750
- .reduce(async (buffer: Promise<Buffer>, formatNotTrimed: string) => {
751
- const format = formatNotTrimed.trim().toLowerCase();
752
- const method =
753
- format === "gzip" || format === "x-gzip"
754
- ? gunzip
755
- : format === "deflate"
756
- ? inflate
757
- : format === "br"
758
- ? brotliDecompress
759
- : format === "identity" || format === ""
760
- ? (
761
- input: Buffer,
762
- callback: (err?: Error, data?: Buffer) => void
763
- ) => {
764
- callback(null, input);
765
- }
766
- : null;
767
- if (method === null) {
768
- send(
769
- 502,
770
- inboundResponse,
771
- Buffer.from(
772
- errorPage(
773
- new Error(
774
- `${format} compression not supported by the proxy`
775
- ),
776
- "stream",
777
- url,
778
- targetUrl
779
- )
780
- )
781
- );
782
- return;
783
- }
795
+ (payloadSource as any).on("end", () => {
796
+ resolve(partialBody);
797
+ });
798
+ }).then((payloadBuffer: Buffer) => {
799
+ if (!config.replaceResponseBodyUrls) return payloadBuffer;
800
+ if (!payloadBuffer.length) return payloadBuffer;
784
801
 
785
- const openedBuffer = await buffer;
786
- return await new Promise((resolve) =>
787
- method(openedBuffer, (err_1, data_1) => {
788
- if (err_1) {
789
- send(
790
- 502,
791
- inboundResponse,
792
- Buffer.from(errorPage(err_1, "stream", url, targetUrl))
793
- );
794
- resolve("");
795
- return;
796
- }
797
- resolve(data_1);
798
- })
799
- );
800
- }, Promise.resolve(payloadBuffer))
801
- .then((uncompressedBuffer: Buffer) => {
802
- const fileTooBig = uncompressedBuffer.length > 1E7;
803
- const fileHasSpecialChars = () => /[^\x00-\x7F]/.test(uncompressedBuffer.toString());
804
- const contentTypeCanBeProcessed =
805
- ['text/html', 'application/javascript', 'application/json'].some(allowedContentType =>
806
- (outboundResponseHeaders["content-type"] ?? "").includes(allowedContentType));
807
- const willReplace = !fileTooBig && (contentTypeCanBeProcessed || !fileHasSpecialChars());
808
- return !willReplace ?
809
- uncompressedBuffer :
810
- !config.replaceResponseBodyUrls
811
- ? uncompressedBuffer.toString()
812
- : Object.entries(config.mapping)
813
- .reduce(
814
- (inProgress, [path, mapping]) =>
815
- path !== '' && !path.match(/^[-a-zA-Z0-9()@:%_\+.~#?&//=]*$/)
816
- ? inProgress
817
- : inProgress.replace(
818
- new RegExp(
819
- mapping
820
- .replace(/^file:\/\//, "")
821
- .replace(/[*+?^${}()|[\]\\]/g, "")
822
- .replace(/^https/, 'https?') + '/*',
823
- "ig"
824
- ),
825
- `https://${proxyHostnameAndPort}${path.replace(
826
- /\/+$/,
827
- ""
828
- )}/`
829
- ),
830
- uncompressedBuffer.toString()
831
- )
832
- .split(`${proxyHostnameAndPort}/:`)
833
- .join(`${proxyHostnameAndPort}:`)
834
- .replace(/\?protocol=wss?%3A&hostname=[^&]+&port=[0-9]+&pathname=/g,
835
- `?protocol=ws${config.ssl ?
836
- "s" : ""}%3A&hostname=${proxyHostname}&port=${config.port}&pathname=${
837
- encodeURIComponent(key.replace(/\/+$/, ''))}`)
838
- })
839
- .then((updatedBody: Buffer | string) =>
840
- (outboundResponseHeaders["content-encoding"] || "")
841
- .split(",")
842
- .reduce((buffer: Promise<Buffer>, formatNotTrimed: string) => {
802
+ return (outboundResponseHeaders["content-encoding"] || "")
803
+ .split(",")
804
+ .reduce(
805
+ async (buffer: Promise<Buffer>, formatNotTrimed: string) => {
843
806
  const format = formatNotTrimed.trim().toLowerCase();
844
807
  const method =
845
808
  format === "gzip" || format === "x-gzip"
846
- ? gzip
809
+ ? gunzip
847
810
  : format === "deflate"
848
- ? deflate
811
+ ? inflate
849
812
  : format === "br"
850
- ? brotliCompress
813
+ ? brotliDecompress
851
814
  : format === "identity" || format === ""
852
815
  ? (
853
816
  input: Buffer,
854
- callback: (err?: Error, data?: Buffer) => void
817
+ callback: (err?: Error, data?: Buffer) => void,
855
818
  ) => {
856
819
  callback(null, input);
857
820
  }
858
821
  : null;
859
- if (method === null)
860
- throw new Error(
861
- `${format} compression not supported by the proxy`
822
+ if (method === null) {
823
+ send(
824
+ 502,
825
+ inboundResponse,
826
+ Buffer.from(
827
+ errorPage(
828
+ new Error(
829
+ `${format} compression not supported by the proxy`,
830
+ ),
831
+ "stream",
832
+ url,
833
+ targetUrl,
834
+ ),
835
+ ),
862
836
  );
837
+ return;
838
+ }
863
839
 
864
- return buffer.then(
865
- (data) =>
866
- new Promise((resolve) =>
867
- method(data, (err, data) => {
868
- if (err) throw err;
869
- resolve(data);
870
- })
871
- )
840
+ const openedBuffer = await buffer;
841
+ return await new Promise(resolve =>
842
+ method(openedBuffer, (err_1, data_1) => {
843
+ if (err_1) {
844
+ send(
845
+ 502,
846
+ inboundResponse,
847
+ Buffer.from(
848
+ errorPage(err_1, "stream", url, targetUrl),
849
+ ),
850
+ );
851
+ resolve("");
852
+ return;
853
+ }
854
+ resolve(data_1);
855
+ }),
872
856
  );
873
- }, Promise.resolve(Buffer.from(updatedBody)))
874
- );
875
- }));
876
-
877
- // phase : inbound response
878
- const responseHeaders = {
879
- ...Object.entries({
880
- ...outboundResponseHeaders,
881
- ...(config.replaceResponseBodyUrls
882
- ? { ["content-length"]: `${payload.byteLength}` }
883
- : {}),
884
- })
885
- .filter(
886
- ([h]) =>
887
- !h.startsWith(":") &&
888
- h.toLowerCase() !== "transfer-encoding" &&
889
- h.toLowerCase() !== "connection"
890
- )
891
- .reduce((acc: any, [key, value]: [string, string | string[]]) => {
892
- const allSubdomains = targetHost
893
- .split("")
894
- .map(
895
- (_, i) =>
896
- targetHost.substring(i).startsWith(".") &&
897
- targetHost.substring(i)
857
+ },
858
+ Promise.resolve(payloadBuffer),
898
859
  )
899
- .filter((subdomain) => subdomain) as string[];
900
- const transformedValue = [targetHost].concat(allSubdomains).reduce(
901
- (acc1, subDomain) =>
902
- (!Array.isArray(acc1) ? [acc1] : (acc1 as string[])).map(
903
- (oneElement) => {
904
- return typeof oneElement === "string"
905
- ? oneElement.replace(
906
- `Domain=${subDomain}`,
907
- `Domain=${url.hostname}`
908
- )
909
- : oneElement;
910
- }
911
- ),
912
- value
913
- );
860
+ .then((uncompressedBuffer: Buffer) => {
861
+ const fileTooBig = uncompressedBuffer.length > 1e7;
862
+ const fileHasSpecialChars = () =>
863
+ /[^\x00-\x7F]/.test(uncompressedBuffer.toString());
864
+ const contentTypeCanBeProcessed = [
865
+ "text/html",
866
+ "application/javascript",
867
+ "application/json",
868
+ ].some(allowedContentType =>
869
+ (outboundResponseHeaders["content-type"] ?? "").includes(
870
+ allowedContentType,
871
+ ),
872
+ );
873
+ const willReplace =
874
+ !fileTooBig &&
875
+ (contentTypeCanBeProcessed || !fileHasSpecialChars());
876
+ return !willReplace
877
+ ? uncompressedBuffer
878
+ : !config.replaceResponseBodyUrls
879
+ ? uncompressedBuffer.toString()
880
+ : Object.entries(config.mapping)
881
+ .reduce(
882
+ (inProgress, [path, mapping]) =>
883
+ path !== "" &&
884
+ !path.match(/^[-a-zA-Z0-9()@:%_\+.~#?&//=]*$/)
885
+ ? inProgress
886
+ : inProgress.replace(
887
+ new RegExp(
888
+ mapping
889
+ .replace(/^file:\/\//, "")
890
+ .replace(/[*+?^${}()|[\]\\]/g, "")
891
+ .replace(/^https/, "https?") + "/*",
892
+ "ig",
893
+ ),
894
+ `https://${proxyHostnameAndPort}${path.replace(
895
+ /\/+$/,
896
+ "",
897
+ )}/`,
898
+ ),
899
+ uncompressedBuffer.toString(),
900
+ )
901
+ .split(`${proxyHostnameAndPort}/:`)
902
+ .join(`${proxyHostnameAndPort}:`)
903
+ .replace(
904
+ /\?protocol=wss?%3A&hostname=[^&]+&port=[0-9]+&pathname=/g,
905
+ `?protocol=ws${
906
+ config.ssl ? "s" : ""
907
+ }%3A&hostname=${proxyHostname}&port=${
908
+ config.port
909
+ }&pathname=${encodeURIComponent(
910
+ key.replace(/\/+$/, ""),
911
+ )}`,
912
+ );
913
+ })
914
+ .then((updatedBody: Buffer | string) =>
915
+ (outboundResponseHeaders["content-encoding"] || "")
916
+ .split(",")
917
+ .reduce(
918
+ (buffer: Promise<Buffer>, formatNotTrimed: string) => {
919
+ const format = formatNotTrimed.trim().toLowerCase();
920
+ const method =
921
+ format === "gzip" || format === "x-gzip"
922
+ ? gzip
923
+ : format === "deflate"
924
+ ? deflate
925
+ : format === "br"
926
+ ? brotliCompress
927
+ : format === "identity" || format === ""
928
+ ? (
929
+ input: Buffer,
930
+ callback: (err?: Error, data?: Buffer) => void,
931
+ ) => {
932
+ callback(null, input);
933
+ }
934
+ : null;
935
+ if (method === null)
936
+ throw new Error(
937
+ `${format} compression not supported by the proxy`,
938
+ );
914
939
 
915
- acc[key] = (acc[key] || []).concat(transformedValue);
916
- return acc;
917
- }, {}),
918
- ...(newTargetUrl ? { location: [newTargetUrl] } : {}),
919
- };
920
- try {
921
- Object.entries(responseHeaders).forEach(([headerName, headerValue]) =>
922
- headerValue && inboundResponse.setHeader(headerName, headerValue as string)
940
+ return buffer.then(
941
+ data =>
942
+ new Promise(resolve =>
943
+ method(data, (err, data) => {
944
+ if (err) throw err;
945
+ resolve(data);
946
+ }),
947
+ ),
948
+ );
949
+ },
950
+ Promise.resolve(Buffer.from(updatedBody)),
951
+ ),
952
+ );
953
+ }));
954
+
955
+ // phase : inbound response
956
+ const responseHeaders = {
957
+ ...Object.entries({
958
+ ...outboundResponseHeaders,
959
+ ...(config.replaceResponseBodyUrls
960
+ ? { ["content-length"]: `${payload.byteLength}` }
961
+ : {}),
962
+ })
963
+ .filter(
964
+ ([h]) =>
965
+ !h.startsWith(":") &&
966
+ h.toLowerCase() !== "transfer-encoding" &&
967
+ h.toLowerCase() !== "connection",
968
+ )
969
+ .reduce((acc: any, [key, value]: [string, string | string[]]) => {
970
+ const allSubdomains = targetHost
971
+ .split("")
972
+ .map(
973
+ (_, i) =>
974
+ targetHost.substring(i).startsWith(".") &&
975
+ targetHost.substring(i),
976
+ )
977
+ .filter(subdomain => subdomain) as string[];
978
+ const transformedValue = [targetHost]
979
+ .concat(allSubdomains)
980
+ .reduce(
981
+ (acc1, subDomain) =>
982
+ (!Array.isArray(acc1) ? [acc1] : (acc1 as string[])).map(
983
+ oneElement => {
984
+ return typeof oneElement === "string"
985
+ ? oneElement.replace(
986
+ `Domain=${subDomain}`,
987
+ `Domain=${url.hostname}`,
988
+ )
989
+ : oneElement;
990
+ },
991
+ ),
992
+ value,
993
+ );
994
+
995
+ acc[key] = (acc[key] || []).concat(transformedValue);
996
+ return acc;
997
+ }, {}),
998
+ ...(newTargetUrl ? { location: [newTargetUrl] } : {}),
999
+ };
1000
+ try {
1001
+ Object.entries(responseHeaders).forEach(
1002
+ ([headerName, headerValue]) =>
1003
+ headerValue &&
1004
+ inboundResponse.setHeader(headerName, headerValue as string),
1005
+ );
1006
+ } catch (e) {
1007
+ // ERR_HTTP2_HEADERS_SENT
1008
+ }
1009
+ inboundResponse.writeHead(
1010
+ outboundResponseHeaders[":status"] ||
1011
+ outboundHttp1Response.statusCode ||
1012
+ 200,
1013
+ config.ssl
1014
+ ? undefined // statusMessage is discarded in http/2
1015
+ : outboundHttp1Response.statusMessage || "Status read from http/2",
1016
+ responseHeaders,
923
1017
  );
924
- } catch(e) {
925
- // ERR_HTTP2_HEADERS_SENT
926
- }
927
- inboundResponse.writeHead(
928
- outboundResponseHeaders[":status"] ||
929
- outboundHttp1Response.statusCode ||
930
- 200,
931
- config.ssl
932
- ? undefined // statusMessage is discarded in http/2
933
- : outboundHttp1Response.statusMessage || "Status read from http/2",
934
- responseHeaders
935
- );
936
- if (payload) inboundResponse.end(payload);
937
- else inboundResponse.end();
938
- }
939
- ) as Server)
1018
+ if (payload) inboundResponse.end(payload);
1019
+ else inboundResponse.end();
1020
+ },
1021
+ ) as Server
1022
+ )
940
1023
  .addListener("error", (err: Error) => {
941
1024
  if ((err as ErrorWithErrno).code === "EACCES")
942
1025
  log(`permission denied for this port`, LogLevel.ERROR, EMOJIS.NO);
943
1026
  if ((err as ErrorWithErrno).code === "EADDRINUSE")
944
- log(`port is already used. NOT started`, LogLevel.ERROR, EMOJIS.ERROR_6);
1027
+ log(
1028
+ `port is already used. NOT started`,
1029
+ LogLevel.ERROR,
1030
+ EMOJIS.ERROR_6,
1031
+ );
945
1032
  })
946
1033
  .addListener("listening", () => {
947
1034
  quickStatus(config);
948
1035
  })
949
1036
  .on("upgrade", (request: IncomingMessage, upstreamSocket: Duplex) => {
950
1037
  if (!config.websocket) {
951
- upstreamSocket.end(`HTTP/1.1 503 Service Unavailable\r\n\r\n`)
1038
+ upstreamSocket.end(`HTTP/1.1 503 Service Unavailable\r\n\r\n`);
952
1039
  return;
953
1040
  }
954
1041
 
955
1042
  const { key, target: targetWithForcedPrefix } = determineMapping(request);
956
- const target = new URL(`${targetWithForcedPrefix.protocol}//${
957
- targetWithForcedPrefix.host}${request.url.endsWith('/_next/webpack-hmr')
958
- ? request.url
959
- : request.url.replace( new RegExp(`^${key}`, 'g'), '').replace(/^\/*/, '/')}`);
1043
+ const target = new URL(
1044
+ `${targetWithForcedPrefix.protocol}//${targetWithForcedPrefix.host}${
1045
+ request.url.endsWith("/_next/webpack-hmr")
1046
+ ? request.url
1047
+ : request.url
1048
+ .replace(new RegExp(`^${key}`, "g"), "")
1049
+ .replace(/^\/*/, "/")
1050
+ }`,
1051
+ );
960
1052
  const downstreamRequestOptions: RequestOptions = {
961
1053
  hostname: target.hostname,
962
1054
  path: target.pathname,
@@ -968,40 +1060,61 @@ const start = () => {
968
1060
  host: target.hostname,
969
1061
  };
970
1062
 
971
- const downstreamRequest = target.protocol === "https:"
972
- ? httpsRequest(downstreamRequestOptions)
973
- : httpRequest(downstreamRequestOptions);
1063
+ const downstreamRequest =
1064
+ target.protocol === "https:"
1065
+ ? httpsRequest(downstreamRequestOptions)
1066
+ : httpRequest(downstreamRequestOptions);
974
1067
  downstreamRequest.end();
975
- downstreamRequest.on('error', (error) => {
976
- log(`websocket request has errored ${
977
- (error as ErrorWithErrno).errno ?
978
- `(${(error as ErrorWithErrno).errno})` : ''}`,
979
- LogLevel.WARNING, EMOJIS.WEBSOCKET)
980
- });
981
- downstreamRequest.on('upgrade', (response, downstreamSocket) => {
982
- const upgradeResponse = `HTTP/${response.httpVersion} ${response.statusCode} ${
983
- response.statusMessage}\r\n${Object.entries(response.headers)
984
- .flatMap(([key, value]) => (!Array.isArray(value) ? [value] : value)
985
- .map(oneValue => [key, oneValue]))
986
- .map(([key, value]) =>
987
- `${key}: ${value}\r\n`).join('')}\r\n`;
1068
+ downstreamRequest.on("error", error => {
1069
+ log(
1070
+ `websocket request has errored ${
1071
+ (error as ErrorWithErrno).errno
1072
+ ? `(${(error as ErrorWithErrno).errno})`
1073
+ : ""
1074
+ }`,
1075
+ LogLevel.WARNING,
1076
+ EMOJIS.WEBSOCKET,
1077
+ );
1078
+ });
1079
+ downstreamRequest.on("upgrade", (response, downstreamSocket) => {
1080
+ const upgradeResponse = `HTTP/${response.httpVersion} ${
1081
+ response.statusCode
1082
+ } ${response.statusMessage}\r\n${Object.entries(response.headers)
1083
+ .flatMap(([key, value]) =>
1084
+ (!Array.isArray(value) ? [value] : value).map(oneValue => [
1085
+ key,
1086
+ oneValue,
1087
+ ]),
1088
+ )
1089
+ .map(([key, value]) => `${key}: ${value}\r\n`)
1090
+ .join("")}\r\n`;
988
1091
  upstreamSocket.write(upgradeResponse);
989
1092
  upstreamSocket.allowHalfOpen = true;
990
1093
  downstreamSocket.allowHalfOpen = true;
991
- downstreamSocket.on('data', (data) => upstreamSocket.write(data));
992
- upstreamSocket.on('data', (data) => downstreamSocket.write(data));
993
- downstreamSocket.on('error', (error) => {
994
- log(`downstream socket has errored ${
995
- (error as ErrorWithErrno).errno ?
996
- `(${(error as ErrorWithErrno).errno})` : ''}`,
997
- LogLevel.WARNING, EMOJIS.WEBSOCKET)
998
- })
999
- upstreamSocket.on('error', (error) => {
1000
- log(`upstream socket has errored ${
1001
- (error as ErrorWithErrno).errno ?
1002
- `(${(error as ErrorWithErrno).errno})` : ''}`,
1003
- LogLevel.WARNING, EMOJIS.WEBSOCKET)
1004
- })
1094
+ downstreamSocket.on("data", data => upstreamSocket.write(data));
1095
+ upstreamSocket.on("data", data => downstreamSocket.write(data));
1096
+ downstreamSocket.on("error", error => {
1097
+ log(
1098
+ `downstream socket has errored ${
1099
+ (error as ErrorWithErrno).errno
1100
+ ? `(${(error as ErrorWithErrno).errno})`
1101
+ : ""
1102
+ }`,
1103
+ LogLevel.WARNING,
1104
+ EMOJIS.WEBSOCKET,
1105
+ );
1106
+ });
1107
+ upstreamSocket.on("error", error => {
1108
+ log(
1109
+ `upstream socket has errored ${
1110
+ (error as ErrorWithErrno).errno
1111
+ ? `(${(error as ErrorWithErrno).errno})`
1112
+ : ""
1113
+ }`,
1114
+ LogLevel.WARNING,
1115
+ EMOJIS.WEBSOCKET,
1116
+ );
1117
+ });
1005
1118
  });
1006
1119
  })
1007
1120
  .listen(config.port);