@chidchanun/bcp 0.1.16 → 0.1.18

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.
@@ -1,11 +1,10 @@
1
1
  import * as http from "node:http";
2
2
 
3
3
  import {
4
- applyMiddlewareRequestHeaders,
5
- applyMiddlewareResponseHeaders,
6
- executeMiddleware,
7
- sendMiddlewareRedirect,
4
+ executeMiddlewarePipeline,
5
+ getMiddlewareRedirectTarget,
8
6
  sendMiddlewareResponse,
7
+ type MiddlewareExecutionInput,
9
8
  type MiddlewareModule,
10
9
  } from "./middleware.js";
11
10
 
@@ -81,8 +80,8 @@ export function createMiddlewareProxy(
81
80
  await options
82
81
  .getMiddleware();
83
82
 
84
- const result =
85
- await executeMiddleware(
83
+ const response =
84
+ await executeMiddlewarePipeline(
86
85
  module,
87
86
  {
88
87
  url:
@@ -94,91 +93,53 @@ export function createMiddlewareProxy(
94
93
  incomingHeadersToHeaders(
95
94
  req.headers
96
95
  ),
97
- }
96
+ },
97
+ (
98
+ pipelineInput
99
+ ) =>
100
+ requestUpstreamAsResponse(
101
+ req,
102
+ pipelineInput,
103
+ upstreamHostname,
104
+ options.upstreamPort,
105
+ host,
106
+ incomingUrl,
107
+ navigationTarget,
108
+ targetUrl
109
+ )
98
110
  );
99
111
 
100
- if (
101
- result.type ===
102
- "redirect"
103
- ) {
104
- if (
105
- navigationTarget
106
- ) {
107
- applyMiddlewareResponseHeaders(
108
- res,
109
- result.headers
110
- );
111
- res.statusCode =
112
- 204;
113
- res.setHeader(
114
- NAVIGATION_REDIRECT_HEADER,
115
- result.url.href
116
- );
117
- res.setHeader(
118
- "Cache-Control",
119
- "no-store"
120
- );
121
- res.end();
122
- return;
123
- }
124
-
125
- sendMiddlewareRedirect(
126
- req,
127
- res,
128
- result
112
+ const middlewareRedirect =
113
+ getMiddlewareRedirectTarget(
114
+ response
129
115
  );
130
- return;
131
- }
132
116
 
133
117
  if (
134
- result.type ===
135
- "response"
118
+ navigationTarget &&
119
+ middlewareRedirect
136
120
  ) {
137
- await sendMiddlewareResponse(
138
- req,
121
+ applyNavigationRedirectHeaders(
139
122
  res,
140
- result.response
123
+ response.headers
141
124
  );
125
+ res.statusCode =
126
+ 204;
127
+ res.setHeader(
128
+ NAVIGATION_REDIRECT_HEADER,
129
+ middlewareRedirect.href
130
+ );
131
+ res.setHeader(
132
+ "Cache-Control",
133
+ "no-store"
134
+ );
135
+ res.end();
142
136
  return;
143
137
  }
144
138
 
145
- applyMiddlewareRequestHeaders(
146
- req,
147
- result.requestHeaders
148
- );
149
- applyMiddlewareResponseHeaders(
150
- res,
151
- result.responseHeaders
152
- );
153
-
154
- if (
155
- result.type ===
156
- "rewrite"
157
- ) {
158
- if (
159
- navigationTarget
160
- ) {
161
- incomingUrl
162
- .searchParams
163
- .set(
164
- "url",
165
- `${result.url.pathname}${result.url.search}${result.url.hash}`
166
- );
167
-
168
- req.url =
169
- `${incomingUrl.pathname}${incomingUrl.search}`;
170
- } else {
171
- req.url =
172
- `${result.url.pathname}${result.url.search}`;
173
- }
174
- }
175
-
176
- proxyRequest(
139
+ await sendMiddlewareResponse(
177
140
  req,
178
141
  res,
179
- upstreamHostname,
180
- options.upstreamPort,
181
- host
142
+ response
182
143
  );
183
144
  } catch (error) {
184
145
  console.error(
@@ -282,114 +243,319 @@ function resolveNavigationTarget(
282
243
  }
283
244
  }
284
245
 
285
- function proxyRequest(
246
+ function requestUpstreamAsResponse(
286
247
  req: http.IncomingMessage,
287
- res: http.ServerResponse,
248
+ input: MiddlewareExecutionInput,
288
249
  upstreamHostname: string,
289
250
  upstreamPort: number,
290
- originalHost: string
291
- ): void {
292
- const upstream =
293
- http.request(
294
- {
295
- hostname:
296
- upstreamHostname,
297
- port:
298
- upstreamPort,
299
- method:
300
- req.method ??
301
- "GET",
302
- path:
303
- req.url ??
304
- "/",
305
- headers: {
306
- ...req.headers,
307
- host:
308
- originalHost,
309
- "x-forwarded-host":
310
- originalHost,
311
- },
312
- }
251
+ originalHost: string,
252
+ incomingUrl: URL,
253
+ navigationTarget: URL | null,
254
+ originalTargetUrl: URL
255
+ ): Promise<Response> {
256
+ const target =
257
+ input.url instanceof URL
258
+ ? new URL(
259
+ input.url.href
260
+ )
261
+ : new URL(
262
+ input.url
263
+ );
264
+ const requestHeaders =
265
+ headersToNodeHeaders(
266
+ new Headers(
267
+ input.headers
268
+ )
313
269
  );
314
270
 
315
- upstream.on(
316
- "response",
317
- (upstreamResponse) => {
318
- const headers = {
319
- ...upstreamResponse.headers,
320
- };
271
+ requestHeaders.host =
272
+ originalHost;
273
+ requestHeaders[
274
+ "x-forwarded-host"
275
+ ] = originalHost;
276
+
277
+ const path =
278
+ resolveUpstreamPath(
279
+ target,
280
+ incomingUrl,
281
+ navigationTarget,
282
+ originalTargetUrl
283
+ );
321
284
 
322
- for (
323
- const [
324
- name,
325
- value,
326
- ]
327
- of Object.entries(
328
- headers
329
- )
330
- ) {
331
- if (
332
- value !== undefined
333
- ) {
334
- res.setHeader(
335
- name,
336
- value
285
+ return new Promise<Response>(
286
+ (
287
+ resolve,
288
+ reject
289
+ ) => {
290
+ const upstream =
291
+ http.request(
292
+ {
293
+ hostname:
294
+ upstreamHostname,
295
+ port:
296
+ upstreamPort,
297
+ method:
298
+ input.method ??
299
+ req.method ??
300
+ "GET",
301
+ path,
302
+ headers:
303
+ requestHeaders,
304
+ }
305
+ );
306
+
307
+ upstream.once(
308
+ "response",
309
+ (
310
+ upstreamResponse
311
+ ) => {
312
+ const chunks:
313
+ Buffer[] = [];
314
+
315
+ upstreamResponse.on(
316
+ "data",
317
+ (
318
+ chunk:
319
+ Buffer |
320
+ string
321
+ ) => {
322
+ chunks.push(
323
+ Buffer.isBuffer(
324
+ chunk
325
+ )
326
+ ? chunk
327
+ : Buffer.from(
328
+ chunk
329
+ )
330
+ );
331
+ }
332
+ );
333
+ upstreamResponse.once(
334
+ "error",
335
+ reject
336
+ );
337
+ upstreamResponse.once(
338
+ "end",
339
+ () => {
340
+ const headers =
341
+ responseHeadersFromNode(
342
+ upstreamResponse.headers
343
+ );
344
+ const status =
345
+ upstreamResponse.statusCode ??
346
+ 502;
347
+ const body =
348
+ req.method ===
349
+ "HEAD" ||
350
+ status === 204 ||
351
+ status === 304
352
+ ? null
353
+ : Buffer.concat(
354
+ chunks
355
+ );
356
+
357
+ resolve(
358
+ new Response(
359
+ body,
360
+ {
361
+ status,
362
+ statusText:
363
+ upstreamResponse.statusMessage ??
364
+ "",
365
+ headers,
366
+ }
367
+ )
368
+ );
369
+ }
337
370
  );
338
371
  }
372
+ );
373
+
374
+ upstream.once(
375
+ "error",
376
+ reject
377
+ );
378
+
379
+ req.pipe(
380
+ upstream
381
+ );
382
+ }
383
+ );
384
+ }
385
+
386
+ function resolveUpstreamPath(
387
+ target: URL,
388
+ incomingUrl: URL,
389
+ navigationTarget: URL | null,
390
+ originalTargetUrl: URL
391
+ ): string {
392
+ if (!navigationTarget) {
393
+ return `${target.pathname}${target.search}`;
394
+ }
395
+
396
+ const envelope =
397
+ new URL(
398
+ incomingUrl.href
399
+ );
400
+
401
+ if (
402
+ target.href !==
403
+ originalTargetUrl.href
404
+ ) {
405
+ envelope.searchParams.set(
406
+ "url",
407
+ `${target.pathname}${target.search}${target.hash}`
408
+ );
409
+ }
410
+
411
+ return `${envelope.pathname}${envelope.search}`;
412
+ }
413
+
414
+ function responseHeadersFromNode(
415
+ input: http.IncomingHttpHeaders
416
+ ): Headers {
417
+ const headers =
418
+ new Headers();
419
+
420
+ for (
421
+ const [
422
+ key,
423
+ value,
424
+ ]
425
+ of Object.entries(
426
+ input
427
+ )
428
+ ) {
429
+ if (
430
+ value === undefined ||
431
+ isHopByHopHeader(
432
+ key
433
+ )
434
+ ) {
435
+ continue;
436
+ }
437
+
438
+ if (
439
+ Array.isArray(
440
+ value
441
+ )
442
+ ) {
443
+ for (
444
+ const item
445
+ of value
446
+ ) {
447
+ headers.append(
448
+ key,
449
+ item
450
+ );
339
451
  }
452
+ continue;
453
+ }
340
454
 
341
- res.statusCode =
342
- upstreamResponse.statusCode ??
343
- 502;
455
+ headers.set(
456
+ key,
457
+ value
458
+ );
459
+ }
460
+
461
+ return headers;
462
+ }
344
463
 
464
+ function headersToNodeHeaders(
465
+ input: Headers
466
+ ): http.OutgoingHttpHeaders {
467
+ const headers:
468
+ http.OutgoingHttpHeaders = {};
469
+
470
+ input.forEach(
471
+ (value, key) => {
345
472
  if (
346
- upstreamResponse.statusMessage
473
+ !isHopByHopHeader(
474
+ key
475
+ )
347
476
  ) {
348
- res.statusMessage =
349
- upstreamResponse.statusMessage;
477
+ headers[key] =
478
+ value;
350
479
  }
351
-
352
- upstreamResponse.pipe(
353
- res
354
- );
355
480
  }
356
481
  );
357
482
 
358
- upstream.on(
359
- "error",
360
- (error) => {
361
- console.error(
362
- "[BCP Middleware] upstream request failed:",
363
- error
364
- );
483
+ return headers;
484
+ }
485
+
486
+ function applyNavigationRedirectHeaders(
487
+ res: http.ServerResponse,
488
+ headers: Headers
489
+ ): void {
490
+ headers.forEach(
491
+ (value, key) => {
492
+ const normalized =
493
+ key.toLowerCase();
365
494
 
366
495
  if (
367
- res.headersSent
496
+ normalized ===
497
+ "location" ||
498
+ normalized ===
499
+ "content-length" ||
500
+ isHopByHopHeader(
501
+ normalized
502
+ )
368
503
  ) {
369
- res.destroy(
370
- error
371
- );
372
504
  return;
373
505
  }
374
506
 
375
- res.writeHead(
376
- 502,
377
- {
378
- "Content-Type":
379
- "text/plain; charset=utf-8",
380
- "Cache-Control":
381
- "no-store",
382
- }
383
- );
384
- res.end(
385
- "Bad Gateway"
507
+ res.setHeader(
508
+ key,
509
+ value
386
510
  );
387
511
  }
388
512
  );
389
513
 
390
- req.pipe(
391
- upstream
392
- );
514
+ const getSetCookie =
515
+ (
516
+ headers as Headers & {
517
+ getSetCookie?: () => string[];
518
+ }
519
+ ).getSetCookie;
520
+
521
+ if (
522
+ typeof getSetCookie ===
523
+ "function"
524
+ ) {
525
+ const cookies =
526
+ getSetCookie.call(
527
+ headers
528
+ );
529
+
530
+ if (
531
+ cookies.length > 0
532
+ ) {
533
+ res.setHeader(
534
+ "Set-Cookie",
535
+ cookies
536
+ );
537
+ }
538
+ }
539
+ }
540
+
541
+ function isHopByHopHeader(
542
+ name: string
543
+ ): boolean {
544
+ switch (
545
+ name.toLowerCase()
546
+ ) {
547
+ case "connection":
548
+ case "keep-alive":
549
+ case "proxy-authenticate":
550
+ case "proxy-authorization":
551
+ case "te":
552
+ case "trailer":
553
+ case "transfer-encoding":
554
+ case "upgrade":
555
+ return true;
556
+ default:
557
+ return false;
558
+ }
393
559
  }
394
560
 
395
561
  function incomingHeadersToHeaders(