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/.prettierrc +12 -0
- package/dist/localTraffic.js +1 -1
- package/index.ts +783 -670
- package/package.json +4 -4
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
|
|
103
|
-
config
|
|
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"),
|
|
112
|
-
.replace(new RegExp(EMOJIS.NO, "g"),
|
|
113
|
-
.replace(
|
|
114
|
-
|
|
115
|
-
|
|
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(
|
|
149
|
+
new Promise(resolve =>
|
|
127
150
|
readFile(filename, (error, data) => {
|
|
128
151
|
if (error && !firstTime) {
|
|
129
|
-
log(
|
|
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(
|
|
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(
|
|
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),
|
|
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(
|
|
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(
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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.
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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(
|
|
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
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
)
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
.
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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
|
+
"/",
|
|
351
|
+
)}</i></p><ul class="list-group"><li class="list-group-item">📁<a href="${
|
|
352
|
+
url.pathname.endsWith("/") ? ".." : "."
|
|
353
|
+
}"><parent></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
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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
|
|
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 = (
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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}${
|
|
458
|
-
|
|
459
|
-
|
|
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
|
-
|
|
497
|
+
? ""
|
|
498
|
+
: `:${config.port}`
|
|
464
499
|
}`;
|
|
465
500
|
const url = new URL(
|
|
466
|
-
`http${config.ssl ? "s" : ""}://${proxyHostnameAndPort}${
|
|
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]) =>
|
|
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 = (
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
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
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
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
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
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
|
-
|
|
536
|
-
|
|
537
|
-
|
|
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
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
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
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
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
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
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", () =>
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
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
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
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
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
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
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
:
|
|
701
|
-
|
|
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
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
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
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
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
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
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
|
-
?
|
|
809
|
+
? gunzip
|
|
847
810
|
: format === "deflate"
|
|
848
|
-
?
|
|
811
|
+
? inflate
|
|
849
812
|
: format === "br"
|
|
850
|
-
?
|
|
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
|
-
|
|
861
|
-
|
|
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
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
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
|
-
},
|
|
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
|
-
.
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
)
|
|
912
|
-
|
|
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
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
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
|
-
|
|
925
|
-
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
|
|
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(
|
|
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(
|
|
957
|
-
targetWithForcedPrefix.host}${
|
|
958
|
-
|
|
959
|
-
|
|
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 =
|
|
972
|
-
|
|
973
|
-
|
|
1063
|
+
const downstreamRequest =
|
|
1064
|
+
target.protocol === "https:"
|
|
1065
|
+
? httpsRequest(downstreamRequestOptions)
|
|
1066
|
+
: httpRequest(downstreamRequestOptions);
|
|
974
1067
|
downstreamRequest.end();
|
|
975
|
-
downstreamRequest.on(
|
|
976
|
-
log(
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
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(
|
|
992
|
-
upstreamSocket.on(
|
|
993
|
-
downstreamSocket.on(
|
|
994
|
-
log(
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
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);
|