@chidchanun/bcp 0.1.9 → 0.1.10
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/CHANGELOG.md +41 -0
- package/README.md +141 -8
- package/docs/releasing.md +34 -19
- package/docs/route-guards.md +240 -0
- package/docs/updating.md +130 -0
- package/package.json +1 -1
- package/packages/bundler/src/server-production-guards.ts +497 -0
- package/packages/bundler/src/server-production-middleware.ts +33 -8
- package/packages/cli/src/args.ts +58 -0
- package/packages/cli/src/index.ts +41 -13
- package/packages/cli/src/update.ts +860 -0
- package/packages/client/src/index.tsx +1 -0
- package/packages/client/src/loader-data.tsx +171 -41
- package/packages/server/src/page-guard.ts +529 -0
- package/packages/server/src/page-loader.ts +317 -20
- package/packages/server/src/standalone-production-runtime-v2-guard.ts +815 -0
- package/packages/server/src/standalone-production-runtime-v3.ts +1 -1
|
@@ -0,0 +1,815 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import * as http from "node:http";
|
|
3
|
+
import * as net from "node:net";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import {
|
|
6
|
+
pathToFileURL,
|
|
7
|
+
} from "node:url";
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
GUARD_DATA_HEADER,
|
|
11
|
+
type PageLoader,
|
|
12
|
+
} from "./page-loader.js";
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
createNavigationRedirectResponse,
|
|
16
|
+
sendNavigationWebResponse,
|
|
17
|
+
} from "./navigation-response.js";
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
createStandaloneProductionServer as createBaseStandaloneProductionServer,
|
|
21
|
+
type StandaloneApiRoute,
|
|
22
|
+
type StandalonePageRoute,
|
|
23
|
+
type StandaloneProductionServerOptions as BaseStandaloneProductionServerOptions,
|
|
24
|
+
} from "./standalone-production-runtime-v2-navigation.js";
|
|
25
|
+
|
|
26
|
+
export type {
|
|
27
|
+
StandaloneApiRoute,
|
|
28
|
+
StandalonePageRoute,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export interface StandaloneProductionServerOptions
|
|
32
|
+
extends BaseStandaloneProductionServerOptions {}
|
|
33
|
+
|
|
34
|
+
interface GuardAllowResult {
|
|
35
|
+
kind: "allow";
|
|
36
|
+
data: Record<string, unknown>;
|
|
37
|
+
setCookies: string[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface GuardResponseResult {
|
|
41
|
+
kind: "response";
|
|
42
|
+
response: Response;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
type GuardResult =
|
|
46
|
+
| GuardAllowResult
|
|
47
|
+
| GuardResponseResult;
|
|
48
|
+
|
|
49
|
+
interface ProductionGuardModule {
|
|
50
|
+
guardedPathnames: string[];
|
|
51
|
+
evaluateRouteGuards(
|
|
52
|
+
input: {
|
|
53
|
+
url: string;
|
|
54
|
+
method: string;
|
|
55
|
+
headers: Array<[
|
|
56
|
+
string,
|
|
57
|
+
string
|
|
58
|
+
]>;
|
|
59
|
+
remoteAddress: string | null;
|
|
60
|
+
}
|
|
61
|
+
): Promise<GuardResult | null>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const MAX_GUARD_HEADER_BYTES =
|
|
65
|
+
16 * 1024;
|
|
66
|
+
|
|
67
|
+
export function createStandaloneProductionServer(
|
|
68
|
+
options: StandaloneProductionServerOptions
|
|
69
|
+
) {
|
|
70
|
+
const guardFile =
|
|
71
|
+
path.join(
|
|
72
|
+
options.buildDirectory,
|
|
73
|
+
"server",
|
|
74
|
+
"guards.mjs"
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
if (!fs.existsSync(guardFile)) {
|
|
78
|
+
return createBaseStandaloneProductionServer(
|
|
79
|
+
options
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
let baseServer:
|
|
84
|
+
ReturnType<
|
|
85
|
+
typeof createBaseStandaloneProductionServer
|
|
86
|
+
> | null =
|
|
87
|
+
null;
|
|
88
|
+
let gateway:
|
|
89
|
+
http.Server | null =
|
|
90
|
+
null;
|
|
91
|
+
let started =
|
|
92
|
+
false;
|
|
93
|
+
let guardModule:
|
|
94
|
+
ProductionGuardModule | null =
|
|
95
|
+
null;
|
|
96
|
+
|
|
97
|
+
async function start(): Promise<void> {
|
|
98
|
+
if (started) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
guardModule =
|
|
103
|
+
await import(
|
|
104
|
+
pathToFileURL(
|
|
105
|
+
guardFile
|
|
106
|
+
).href
|
|
107
|
+
) as ProductionGuardModule;
|
|
108
|
+
|
|
109
|
+
assertGuardModule(
|
|
110
|
+
guardModule,
|
|
111
|
+
guardFile
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const guardedPathnames =
|
|
115
|
+
new Set(
|
|
116
|
+
guardModule.guardedPathnames
|
|
117
|
+
);
|
|
118
|
+
/*
|
|
119
|
+
* Guard-only routes still need the loader/navigation runtime so the
|
|
120
|
+
* internal guard-data header is decoded and exposed to React. Return
|
|
121
|
+
* null instead of undefined because loader results must remain JSON
|
|
122
|
+
* serializable in the production runtime.
|
|
123
|
+
*/
|
|
124
|
+
const guardPassthroughLoader:
|
|
125
|
+
PageLoader =
|
|
126
|
+
async () =>
|
|
127
|
+
null;
|
|
128
|
+
const routes =
|
|
129
|
+
options.routes.map(
|
|
130
|
+
(route) =>
|
|
131
|
+
guardedPathnames.has(
|
|
132
|
+
route.pathname
|
|
133
|
+
) &&
|
|
134
|
+
route.loader ===
|
|
135
|
+
null
|
|
136
|
+
? {
|
|
137
|
+
...route,
|
|
138
|
+
loader:
|
|
139
|
+
guardPassthroughLoader,
|
|
140
|
+
}
|
|
141
|
+
: route
|
|
142
|
+
);
|
|
143
|
+
const internalPort =
|
|
144
|
+
await findFreePort();
|
|
145
|
+
|
|
146
|
+
baseServer =
|
|
147
|
+
createBaseStandaloneProductionServer({
|
|
148
|
+
...options,
|
|
149
|
+
routes,
|
|
150
|
+
port:
|
|
151
|
+
internalPort,
|
|
152
|
+
hostname:
|
|
153
|
+
"127.0.0.1",
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
await baseServer.start();
|
|
157
|
+
|
|
158
|
+
gateway =
|
|
159
|
+
http.createServer(
|
|
160
|
+
async (
|
|
161
|
+
req,
|
|
162
|
+
res
|
|
163
|
+
) => {
|
|
164
|
+
try {
|
|
165
|
+
const requestUrl =
|
|
166
|
+
new URL(
|
|
167
|
+
req.url ??
|
|
168
|
+
"/",
|
|
169
|
+
`http://${
|
|
170
|
+
req.headers.host ??
|
|
171
|
+
`${options.hostname}:${options.port}`
|
|
172
|
+
}`
|
|
173
|
+
);
|
|
174
|
+
const target =
|
|
175
|
+
resolveGuardTarget(
|
|
176
|
+
requestUrl
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
if (
|
|
180
|
+
!target ||
|
|
181
|
+
!guardModule
|
|
182
|
+
) {
|
|
183
|
+
proxyRequest(
|
|
184
|
+
req,
|
|
185
|
+
res,
|
|
186
|
+
internalPort,
|
|
187
|
+
null,
|
|
188
|
+
[]
|
|
189
|
+
);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const result =
|
|
194
|
+
await guardModule
|
|
195
|
+
.evaluateRouteGuards({
|
|
196
|
+
url:
|
|
197
|
+
target.href,
|
|
198
|
+
method:
|
|
199
|
+
requestUrl.pathname ===
|
|
200
|
+
"/_bcp/navigation"
|
|
201
|
+
? "GET"
|
|
202
|
+
: req.method ??
|
|
203
|
+
"GET",
|
|
204
|
+
headers:
|
|
205
|
+
collectRequestHeaders(
|
|
206
|
+
req
|
|
207
|
+
),
|
|
208
|
+
remoteAddress:
|
|
209
|
+
req.socket
|
|
210
|
+
.remoteAddress ??
|
|
211
|
+
null,
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
if (!result) {
|
|
215
|
+
proxyRequest(
|
|
216
|
+
req,
|
|
217
|
+
res,
|
|
218
|
+
internalPort,
|
|
219
|
+
null,
|
|
220
|
+
[]
|
|
221
|
+
);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (
|
|
226
|
+
result.kind ===
|
|
227
|
+
"response"
|
|
228
|
+
) {
|
|
229
|
+
const response =
|
|
230
|
+
result.response;
|
|
231
|
+
|
|
232
|
+
if (
|
|
233
|
+
requestUrl.pathname ===
|
|
234
|
+
"/_bcp/navigation"
|
|
235
|
+
) {
|
|
236
|
+
const navigationRedirect =
|
|
237
|
+
createNavigationRedirectResponse(
|
|
238
|
+
response
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
if (
|
|
242
|
+
navigationRedirect
|
|
243
|
+
) {
|
|
244
|
+
await sendNavigationWebResponse(
|
|
245
|
+
req,
|
|
246
|
+
res,
|
|
247
|
+
navigationRedirect
|
|
248
|
+
);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
await sendWebResponse(
|
|
254
|
+
req,
|
|
255
|
+
res,
|
|
256
|
+
response
|
|
257
|
+
);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const encodedData =
|
|
262
|
+
encodeGuardData(
|
|
263
|
+
result.data
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
proxyRequest(
|
|
267
|
+
req,
|
|
268
|
+
res,
|
|
269
|
+
internalPort,
|
|
270
|
+
encodedData,
|
|
271
|
+
result.setCookies
|
|
272
|
+
);
|
|
273
|
+
} catch (error) {
|
|
274
|
+
console.error(
|
|
275
|
+
"[BCP Guard Error]",
|
|
276
|
+
error
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
if (
|
|
280
|
+
res.headersSent
|
|
281
|
+
) {
|
|
282
|
+
res.destroy();
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
res.writeHead(
|
|
287
|
+
500,
|
|
288
|
+
{
|
|
289
|
+
"Content-Type":
|
|
290
|
+
"text/plain; charset=utf-8",
|
|
291
|
+
"Cache-Control":
|
|
292
|
+
"no-store",
|
|
293
|
+
}
|
|
294
|
+
);
|
|
295
|
+
res.end(
|
|
296
|
+
"Internal Server Error"
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
gateway.keepAliveTimeout =
|
|
303
|
+
65_000;
|
|
304
|
+
gateway.headersTimeout =
|
|
305
|
+
66_000;
|
|
306
|
+
|
|
307
|
+
try {
|
|
308
|
+
await listen(
|
|
309
|
+
gateway,
|
|
310
|
+
options.port,
|
|
311
|
+
options.hostname
|
|
312
|
+
);
|
|
313
|
+
} catch (error) {
|
|
314
|
+
await baseServer.stop();
|
|
315
|
+
baseServer =
|
|
316
|
+
null;
|
|
317
|
+
gateway =
|
|
318
|
+
null;
|
|
319
|
+
guardModule =
|
|
320
|
+
null;
|
|
321
|
+
throw error;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
started =
|
|
325
|
+
true;
|
|
326
|
+
|
|
327
|
+
console.log(
|
|
328
|
+
`[BCP Guard] Protected routes: ${guardedPathnames.size}`
|
|
329
|
+
);
|
|
330
|
+
console.log(
|
|
331
|
+
`[BCP Guard] Gateway: http://${options.hostname}:${options.port}`
|
|
332
|
+
);
|
|
333
|
+
console.log("");
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async function stop(): Promise<void> {
|
|
337
|
+
if (gateway) {
|
|
338
|
+
await closeServer(
|
|
339
|
+
gateway
|
|
340
|
+
);
|
|
341
|
+
gateway =
|
|
342
|
+
null;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (baseServer) {
|
|
346
|
+
await baseServer.stop();
|
|
347
|
+
baseServer =
|
|
348
|
+
null;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
guardModule =
|
|
352
|
+
null;
|
|
353
|
+
started =
|
|
354
|
+
false;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return {
|
|
358
|
+
start,
|
|
359
|
+
stop,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function assertGuardModule(
|
|
364
|
+
value: ProductionGuardModule,
|
|
365
|
+
filePath: string
|
|
366
|
+
): void {
|
|
367
|
+
if (
|
|
368
|
+
!Array.isArray(
|
|
369
|
+
value.guardedPathnames
|
|
370
|
+
) ||
|
|
371
|
+
typeof value.evaluateRouteGuards !==
|
|
372
|
+
"function"
|
|
373
|
+
) {
|
|
374
|
+
throw new Error(
|
|
375
|
+
`BCP Framework: invalid standalone guard module at "${filePath}".`
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function resolveGuardTarget(
|
|
381
|
+
requestUrl: URL
|
|
382
|
+
): URL | null {
|
|
383
|
+
if (
|
|
384
|
+
requestUrl.pathname !==
|
|
385
|
+
"/_bcp/navigation"
|
|
386
|
+
) {
|
|
387
|
+
return requestUrl;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const targetPath =
|
|
391
|
+
requestUrl.searchParams.get(
|
|
392
|
+
"url"
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
if (!targetPath) {
|
|
396
|
+
return null;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
try {
|
|
400
|
+
const target =
|
|
401
|
+
new URL(
|
|
402
|
+
targetPath,
|
|
403
|
+
requestUrl.origin
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
return target.origin ===
|
|
407
|
+
requestUrl.origin
|
|
408
|
+
? target
|
|
409
|
+
: null;
|
|
410
|
+
} catch {
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function collectRequestHeaders(
|
|
416
|
+
req: http.IncomingMessage
|
|
417
|
+
): Array<[
|
|
418
|
+
string,
|
|
419
|
+
string
|
|
420
|
+
]> {
|
|
421
|
+
const result:
|
|
422
|
+
Array<[
|
|
423
|
+
string,
|
|
424
|
+
string
|
|
425
|
+
]> = [];
|
|
426
|
+
|
|
427
|
+
for (
|
|
428
|
+
const [
|
|
429
|
+
key,
|
|
430
|
+
value,
|
|
431
|
+
]
|
|
432
|
+
of Object.entries(
|
|
433
|
+
req.headers
|
|
434
|
+
)
|
|
435
|
+
) {
|
|
436
|
+
if (
|
|
437
|
+
key.toLowerCase() ===
|
|
438
|
+
GUARD_DATA_HEADER ||
|
|
439
|
+
value ===
|
|
440
|
+
undefined
|
|
441
|
+
) {
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (Array.isArray(value)) {
|
|
446
|
+
for (const item of value) {
|
|
447
|
+
result.push([
|
|
448
|
+
key,
|
|
449
|
+
item,
|
|
450
|
+
]);
|
|
451
|
+
}
|
|
452
|
+
} else {
|
|
453
|
+
result.push([
|
|
454
|
+
key,
|
|
455
|
+
value,
|
|
456
|
+
]);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return result;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function encodeGuardData(
|
|
464
|
+
data: Record<string, unknown>
|
|
465
|
+
): string {
|
|
466
|
+
const content =
|
|
467
|
+
Buffer.from(
|
|
468
|
+
JSON.stringify(
|
|
469
|
+
data
|
|
470
|
+
),
|
|
471
|
+
"utf8"
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
if (
|
|
475
|
+
content.length >
|
|
476
|
+
MAX_GUARD_HEADER_BYTES
|
|
477
|
+
) {
|
|
478
|
+
throw new Error(
|
|
479
|
+
`BCP Framework: serialized guard data exceeds ${MAX_GUARD_HEADER_BYTES} bytes.`
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return content.toString(
|
|
484
|
+
"base64url"
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function proxyRequest(
|
|
489
|
+
req: http.IncomingMessage,
|
|
490
|
+
res: http.ServerResponse,
|
|
491
|
+
internalPort: number,
|
|
492
|
+
encodedGuardData: string | null,
|
|
493
|
+
guardCookies: string[]
|
|
494
|
+
): void {
|
|
495
|
+
const headers:
|
|
496
|
+
http.OutgoingHttpHeaders = {
|
|
497
|
+
...req.headers,
|
|
498
|
+
host:
|
|
499
|
+
req.headers.host,
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
delete headers[
|
|
503
|
+
GUARD_DATA_HEADER
|
|
504
|
+
];
|
|
505
|
+
|
|
506
|
+
if (encodedGuardData) {
|
|
507
|
+
headers[
|
|
508
|
+
GUARD_DATA_HEADER
|
|
509
|
+
] =
|
|
510
|
+
encodedGuardData;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const upstream =
|
|
514
|
+
http.request({
|
|
515
|
+
hostname:
|
|
516
|
+
"127.0.0.1",
|
|
517
|
+
port:
|
|
518
|
+
internalPort,
|
|
519
|
+
method:
|
|
520
|
+
req.method ??
|
|
521
|
+
"GET",
|
|
522
|
+
path:
|
|
523
|
+
req.url ??
|
|
524
|
+
"/",
|
|
525
|
+
headers,
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
upstream.on(
|
|
529
|
+
"response",
|
|
530
|
+
(upstreamResponse) => {
|
|
531
|
+
const responseHeaders:
|
|
532
|
+
http.OutgoingHttpHeaders = {
|
|
533
|
+
...upstreamResponse.headers,
|
|
534
|
+
};
|
|
535
|
+
const upstreamCookies =
|
|
536
|
+
normalizeSetCookieHeader(
|
|
537
|
+
responseHeaders[
|
|
538
|
+
"set-cookie"
|
|
539
|
+
]
|
|
540
|
+
);
|
|
541
|
+
const combinedCookies = [
|
|
542
|
+
...guardCookies,
|
|
543
|
+
...upstreamCookies,
|
|
544
|
+
];
|
|
545
|
+
|
|
546
|
+
if (
|
|
547
|
+
combinedCookies.length >
|
|
548
|
+
0
|
|
549
|
+
) {
|
|
550
|
+
responseHeaders[
|
|
551
|
+
"set-cookie"
|
|
552
|
+
] =
|
|
553
|
+
combinedCookies;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const statusCode =
|
|
557
|
+
upstreamResponse.statusCode ??
|
|
558
|
+
502;
|
|
559
|
+
|
|
560
|
+
if (
|
|
561
|
+
upstreamResponse.statusMessage
|
|
562
|
+
) {
|
|
563
|
+
res.writeHead(
|
|
564
|
+
statusCode,
|
|
565
|
+
upstreamResponse.statusMessage,
|
|
566
|
+
responseHeaders
|
|
567
|
+
);
|
|
568
|
+
} else {
|
|
569
|
+
res.writeHead(
|
|
570
|
+
statusCode,
|
|
571
|
+
responseHeaders
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
upstreamResponse.pipe(
|
|
576
|
+
res
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
);
|
|
580
|
+
|
|
581
|
+
upstream.on(
|
|
582
|
+
"error",
|
|
583
|
+
(error) => {
|
|
584
|
+
console.error(
|
|
585
|
+
"[BCP Guard Proxy Error]",
|
|
586
|
+
error
|
|
587
|
+
);
|
|
588
|
+
|
|
589
|
+
if (
|
|
590
|
+
res.headersSent
|
|
591
|
+
) {
|
|
592
|
+
res.destroy(
|
|
593
|
+
error
|
|
594
|
+
);
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
res.writeHead(
|
|
599
|
+
502,
|
|
600
|
+
{
|
|
601
|
+
"Content-Type":
|
|
602
|
+
"text/plain; charset=utf-8",
|
|
603
|
+
"Cache-Control":
|
|
604
|
+
"no-store",
|
|
605
|
+
}
|
|
606
|
+
);
|
|
607
|
+
res.end(
|
|
608
|
+
"Bad Gateway"
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
);
|
|
612
|
+
|
|
613
|
+
req.pipe(
|
|
614
|
+
upstream
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
async function sendWebResponse(
|
|
619
|
+
req: http.IncomingMessage,
|
|
620
|
+
res: http.ServerResponse,
|
|
621
|
+
response: Response
|
|
622
|
+
): Promise<void> {
|
|
623
|
+
const headers:
|
|
624
|
+
http.OutgoingHttpHeaders = {};
|
|
625
|
+
|
|
626
|
+
response.headers.forEach(
|
|
627
|
+
(value, key) => {
|
|
628
|
+
if (
|
|
629
|
+
key.toLowerCase() !==
|
|
630
|
+
"set-cookie"
|
|
631
|
+
) {
|
|
632
|
+
headers[key] =
|
|
633
|
+
value;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
);
|
|
637
|
+
|
|
638
|
+
const setCookies =
|
|
639
|
+
response.headers
|
|
640
|
+
.getSetCookie();
|
|
641
|
+
|
|
642
|
+
if (
|
|
643
|
+
setCookies.length > 0
|
|
644
|
+
) {
|
|
645
|
+
headers[
|
|
646
|
+
"set-cookie"
|
|
647
|
+
] =
|
|
648
|
+
setCookies;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
res.writeHead(
|
|
652
|
+
response.status,
|
|
653
|
+
response.statusText,
|
|
654
|
+
headers
|
|
655
|
+
);
|
|
656
|
+
|
|
657
|
+
if (
|
|
658
|
+
(
|
|
659
|
+
req.method ??
|
|
660
|
+
"GET"
|
|
661
|
+
).toUpperCase() ===
|
|
662
|
+
"HEAD" ||
|
|
663
|
+
!response.body
|
|
664
|
+
) {
|
|
665
|
+
res.end();
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
const reader =
|
|
670
|
+
response.body.getReader();
|
|
671
|
+
|
|
672
|
+
try {
|
|
673
|
+
while (true) {
|
|
674
|
+
const {
|
|
675
|
+
done,
|
|
676
|
+
value,
|
|
677
|
+
} =
|
|
678
|
+
await reader.read();
|
|
679
|
+
|
|
680
|
+
if (done) {
|
|
681
|
+
break;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
res.write(
|
|
685
|
+
Buffer.from(
|
|
686
|
+
value
|
|
687
|
+
)
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
res.end();
|
|
692
|
+
} finally {
|
|
693
|
+
reader.releaseLock();
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function normalizeSetCookieHeader(
|
|
698
|
+
value:
|
|
699
|
+
string |
|
|
700
|
+
string[] |
|
|
701
|
+
undefined
|
|
702
|
+
): string[] {
|
|
703
|
+
if (!value) {
|
|
704
|
+
return [];
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
return Array.isArray(value)
|
|
708
|
+
? value
|
|
709
|
+
: [
|
|
710
|
+
value,
|
|
711
|
+
];
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
async function findFreePort(): Promise<number> {
|
|
715
|
+
const server =
|
|
716
|
+
net.createServer();
|
|
717
|
+
|
|
718
|
+
try {
|
|
719
|
+
await listen(
|
|
720
|
+
server,
|
|
721
|
+
0,
|
|
722
|
+
"127.0.0.1"
|
|
723
|
+
);
|
|
724
|
+
|
|
725
|
+
const address =
|
|
726
|
+
server.address();
|
|
727
|
+
|
|
728
|
+
if (
|
|
729
|
+
!address ||
|
|
730
|
+
typeof address ===
|
|
731
|
+
"string"
|
|
732
|
+
) {
|
|
733
|
+
throw new Error(
|
|
734
|
+
"BCP Framework: could not allocate guard production port."
|
|
735
|
+
);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
return address.port;
|
|
739
|
+
} finally {
|
|
740
|
+
if (server.listening) {
|
|
741
|
+
await closeServer(
|
|
742
|
+
server
|
|
743
|
+
);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function listen(
|
|
749
|
+
server: net.Server,
|
|
750
|
+
port: number,
|
|
751
|
+
hostname: string
|
|
752
|
+
): Promise<void> {
|
|
753
|
+
return new Promise<void>(
|
|
754
|
+
(
|
|
755
|
+
resolve,
|
|
756
|
+
reject
|
|
757
|
+
) => {
|
|
758
|
+
const onError =
|
|
759
|
+
(error: Error) => {
|
|
760
|
+
server.off(
|
|
761
|
+
"listening",
|
|
762
|
+
onListening
|
|
763
|
+
);
|
|
764
|
+
reject(
|
|
765
|
+
error
|
|
766
|
+
);
|
|
767
|
+
};
|
|
768
|
+
const onListening =
|
|
769
|
+
() => {
|
|
770
|
+
server.off(
|
|
771
|
+
"error",
|
|
772
|
+
onError
|
|
773
|
+
);
|
|
774
|
+
resolve();
|
|
775
|
+
};
|
|
776
|
+
|
|
777
|
+
server.once(
|
|
778
|
+
"error",
|
|
779
|
+
onError
|
|
780
|
+
);
|
|
781
|
+
server.once(
|
|
782
|
+
"listening",
|
|
783
|
+
onListening
|
|
784
|
+
);
|
|
785
|
+
server.listen(
|
|
786
|
+
port,
|
|
787
|
+
hostname
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
);
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
function closeServer(
|
|
794
|
+
server: net.Server
|
|
795
|
+
): Promise<void> {
|
|
796
|
+
return new Promise<void>(
|
|
797
|
+
(
|
|
798
|
+
resolve,
|
|
799
|
+
reject
|
|
800
|
+
) => {
|
|
801
|
+
server.close(
|
|
802
|
+
(error) => {
|
|
803
|
+
if (error) {
|
|
804
|
+
reject(
|
|
805
|
+
error
|
|
806
|
+
);
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
resolve();
|
|
811
|
+
}
|
|
812
|
+
);
|
|
813
|
+
}
|
|
814
|
+
);
|
|
815
|
+
}
|