@mug-lab/cookie-auth-proxy 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,671 @@
1
+ const http = require("node:http");
2
+ const https = require("node:https");
3
+ const net = require("node:net");
4
+ const {
5
+ addHistory,
6
+ applyJsonOverrides,
7
+ findJsonOverrides,
8
+ findStub,
9
+ historyBodyFromBuffer,
10
+ historyBodyFromStoredBuffer,
11
+ parseRequestBody,
12
+ queryParamsObject,
13
+ stubBodyBuffer,
14
+ } = require("../runtime");
15
+ const { getActiveApiCookie } = require("../config");
16
+ const { log } = require("../logger");
17
+ const {
18
+ rewriteBody,
19
+ rewriteRequestHeaders,
20
+ rewriteResponseHeaders,
21
+ shouldApplyJsonOverrides,
22
+ shouldRewriteBody,
23
+ } = require("./rewrite");
24
+
25
+ /**
26
+ * ブラウザからアクセスされるプロキシサーバを起動する。
27
+ * リクエストはパス判定によりclientまたはリモートAPIへ振り分ける。
28
+ *
29
+ * @param {object} config runtime状態を含むマージ済みプロキシ設定。
30
+ * @returns {import("node:http").Server} listen済みのHTTPサーバ。
31
+ */
32
+ function startProxyServer(config) {
33
+ const server = http.createServer((req, res) => {
34
+ const useApi = shouldUseApi(req, config.matchers);
35
+ if (useApi && isCrossSiteBrowserRequest(req, config.publicOriginUrl.origin)) {
36
+ sendForbiddenCrossSiteApiRequest(req, res, config);
37
+ return;
38
+ }
39
+
40
+ if (useApi && !config.apiOriginUrl) {
41
+ sendMissingApiOrigin(req, res, config);
42
+ return;
43
+ }
44
+
45
+ const target = useApi ? config.apiOriginUrl : config.clientOriginUrl;
46
+ proxyHttp(
47
+ req,
48
+ res,
49
+ target,
50
+ config.publicOriginUrl,
51
+ config,
52
+ useApi ? "api" : "client",
53
+ ).catch((error) => {
54
+ log(config, "error", req.method, req.url, error.message);
55
+ if (!res.headersSent) {
56
+ res.writeHead(error.statusCode || 500, {
57
+ "content-type": "text/plain; charset=utf-8",
58
+ });
59
+ }
60
+ res.end(error.message);
61
+ });
62
+ });
63
+
64
+ server.on("upgrade", (req, socket, head) => {
65
+ proxyUpgrade(
66
+ req,
67
+ socket,
68
+ head,
69
+ config.clientOriginUrl,
70
+ config.publicOriginUrl,
71
+ config,
72
+ );
73
+ });
74
+
75
+ server.listen(config.listenPort, config.listenHost, () => {
76
+ log(config, "info", `Proxy listening on ${config.publicOriginUrl.origin}`);
77
+ log(config, "info", `Client -> ${config.clientOriginUrl.origin}`);
78
+ log(
79
+ config,
80
+ "info",
81
+ `API -> ${config.apiOriginUrl ? config.apiOriginUrl.origin : "(not set)"}`,
82
+ );
83
+ });
84
+
85
+ return server;
86
+ }
87
+
88
+ /**
89
+ * ブラウザからのcross-site APIリクエストか判定する。
90
+ */
91
+ function isCrossSiteBrowserRequest(req, expectedOrigin) {
92
+ const origin = req.headers.origin;
93
+ if (origin && origin !== expectedOrigin) return true;
94
+
95
+ const fetchSite = String(req.headers["sec-fetch-site"] || "").toLowerCase();
96
+ return fetchSite === "cross-site";
97
+ }
98
+
99
+ /**
100
+ * cross-site APIリクエストを拒否して履歴へ残す。
101
+ */
102
+ function sendForbiddenCrossSiteApiRequest(req, res, config) {
103
+ const requestUrl = new URL(req.url, "http://local.proxy");
104
+ const body = Buffer.from(
105
+ "Cross-site API requests are not allowed.",
106
+ "utf8",
107
+ );
108
+ const headers = {
109
+ "content-type": "text/plain; charset=utf-8",
110
+ "content-length": body.length,
111
+ };
112
+ res.writeHead(403, headers);
113
+ res.end(body);
114
+ addHistory(config, {
115
+ type: "error",
116
+ method: req.method,
117
+ path: `${requestUrl.pathname}${requestUrl.search}`,
118
+ origin: config.apiOriginUrl ? config.apiOriginUrl.origin : "",
119
+ status: 403,
120
+ durationMs: 0,
121
+ responseHeaders: headers,
122
+ requestQuery: queryParamsObject(requestUrl.searchParams),
123
+ requestBody: null,
124
+ responseBody: body.toString("utf8"),
125
+ });
126
+ }
127
+
128
+ /**
129
+ * API Origin未設定時のAPI向けリクエストへ、設定を促すエラーを返す。
130
+ */
131
+ function sendMissingApiOrigin(req, res, config) {
132
+ const requestUrl = new URL(req.url, "http://local.proxy");
133
+ const body = Buffer.from(
134
+ "API origin is not configured. Set it in the admin app.",
135
+ "utf8",
136
+ );
137
+ const headers = {
138
+ "content-type": "text/plain; charset=utf-8",
139
+ "content-length": body.length,
140
+ };
141
+ res.writeHead(502, headers);
142
+ res.end(body);
143
+ addHistory(config, {
144
+ type: "error",
145
+ method: req.method,
146
+ path: `${requestUrl.pathname}${requestUrl.search}`,
147
+ origin: "",
148
+ status: 502,
149
+ durationMs: 0,
150
+ responseHeaders: headers,
151
+ requestQuery: queryParamsObject(requestUrl.searchParams),
152
+ requestBody: null,
153
+ responseBody: body.toString("utf8"),
154
+ });
155
+ }
156
+
157
+ /**
158
+ * リクエストパスをclientではなくAPIへ送るか判定する。
159
+ */
160
+ function shouldUseApi(req, matchers) {
161
+ const requestUrl = new URL(req.url, "http://local.proxy");
162
+ const pathname = requestUrl.pathname;
163
+
164
+ if (matchers.apiExcludes.some((pattern) => pattern.test(pathname))) {
165
+ return false;
166
+ }
167
+
168
+ return matchers.apiIncludes.some((pattern) => pattern.test(pathname));
169
+ }
170
+
171
+ /**
172
+ * Locationヘッダーを通常書き換えから保護するパスか判定する。
173
+ */
174
+ function shouldPreserveLocationHeader(pathname, matchers) {
175
+ return matchers.preserveLocation.some((pattern) => pattern.test(pathname));
176
+ }
177
+
178
+ /**
179
+ * HTTPリクエスト本文をBufferとして読み込む。
180
+ */
181
+ function readRequestBuffer(req) {
182
+ return new Promise((resolve, reject) => {
183
+ const chunks = [];
184
+ req.on("data", (chunk) => {
185
+ chunks.push(chunk);
186
+ });
187
+ req.on("end", () => resolve(Buffer.concat(chunks)));
188
+ req.on("error", reject);
189
+ });
190
+ }
191
+
192
+ /**
193
+ * リクエストBodyをStub/Override条件判定用に読み込むか判定する。
194
+ */
195
+ function shouldInspectRequestBody(req, maxBytes) {
196
+ const contentLength = Number(req.headers["content-length"]);
197
+ return (
198
+ Number.isFinite(contentLength) &&
199
+ contentLength > 0 &&
200
+ contentLength <= maxBytes
201
+ );
202
+ }
203
+
204
+ /**
205
+ * 数値として信用できるContent-Lengthを返す。
206
+ */
207
+ function getContentLength(headers) {
208
+ const contentLength = Number(headers["content-length"]);
209
+ return Number.isFinite(contentLength) && contentLength >= 0
210
+ ? contentLength
211
+ : null;
212
+ }
213
+
214
+ /**
215
+ * レスポンス本文を書き換え対象としてバッファリングしてよいサイズか判定する。
216
+ */
217
+ function canBufferForRewrite(headers, maxBytes) {
218
+ const contentLength = getContentLength(headers);
219
+ return contentLength === null || contentLength <= maxBytes;
220
+ }
221
+
222
+ /**
223
+ * ストリーミング中に履歴表示用の末尾バイトだけ保持する。
224
+ */
225
+ function createTailRecorder(maxBytes) {
226
+ const chunks = [];
227
+ let storedBytes = 0;
228
+ let totalBytes = 0;
229
+
230
+ return {
231
+ write(chunk) {
232
+ totalBytes += chunk.length;
233
+ if (maxBytes <= 0) return;
234
+
235
+ chunks.push(chunk);
236
+ storedBytes += chunk.length;
237
+ while (storedBytes > maxBytes && chunks.length > 0) {
238
+ const excess = storedBytes - maxBytes;
239
+ const first = chunks[0];
240
+ if (first.length <= excess) {
241
+ chunks.shift();
242
+ storedBytes -= first.length;
243
+ } else {
244
+ chunks[0] = first.subarray(excess);
245
+ storedBytes -= excess;
246
+ }
247
+ }
248
+ },
249
+ toHistory(encoding) {
250
+ return historyBodyFromStoredBuffer(
251
+ Buffer.concat(chunks, storedBytes),
252
+ totalBytes,
253
+ encoding,
254
+ );
255
+ },
256
+ };
257
+ }
258
+
259
+ /**
260
+ * Bodyを書き換えずにブラウザへストリーミングし、履歴用本文だけ上限付きで残す。
261
+ */
262
+ function streamOriginalResponse(proxyRes, res, responseHeaders, options) {
263
+ const { statusCode, statusMessage, historyEncoding, maxHistoryBodyBytes } = options;
264
+ const contentLength = getContentLength(proxyRes.headers);
265
+ const tail = createTailRecorder(maxHistoryBodyBytes);
266
+
267
+ if (contentLength !== null) {
268
+ responseHeaders["content-length"] = contentLength;
269
+ }
270
+
271
+ res.writeHead(statusCode, statusMessage, responseHeaders);
272
+
273
+ return new Promise((resolve, reject) => {
274
+ proxyRes.on("data", (chunk) => {
275
+ tail.write(chunk);
276
+ });
277
+ proxyRes.on("end", () => {
278
+ resolve(tail.toHistory(historyEncoding));
279
+ });
280
+ proxyRes.on("error", (error) => {
281
+ res.destroy(error);
282
+ reject(error);
283
+ });
284
+ proxyRes.pipe(res);
285
+ });
286
+ }
287
+
288
+ /**
289
+ * 上限内ならBody書き換え用にバッファし、超えたら未加工Bodyをストリーミングする。
290
+ */
291
+ function bufferOrStreamOriginalResponse(proxyRes, res, responseHeaders, options) {
292
+ const {
293
+ statusCode,
294
+ statusMessage,
295
+ historyEncoding,
296
+ maxHistoryBodyBytes,
297
+ maxRewriteBytes,
298
+ } = options;
299
+
300
+ if (!canBufferForRewrite(proxyRes.headers, maxRewriteBytes)) {
301
+ return streamOriginalResponse(proxyRes, res, responseHeaders, {
302
+ statusCode,
303
+ statusMessage,
304
+ historyEncoding,
305
+ maxHistoryBodyBytes,
306
+ }).then((historyBody) => ({ streamed: true, historyBody }));
307
+ }
308
+
309
+ return new Promise((resolve, reject) => {
310
+ const chunks = [];
311
+ const tail = createTailRecorder(maxHistoryBodyBytes);
312
+ let total = 0;
313
+ let streaming = false;
314
+
315
+ proxyRes.on("data", (chunk) => {
316
+ tail.write(chunk);
317
+
318
+ if (streaming) {
319
+ return;
320
+ }
321
+
322
+ total += chunk.length;
323
+ if (total <= maxRewriteBytes) {
324
+ chunks.push(chunk);
325
+ return;
326
+ }
327
+
328
+ streaming = true;
329
+ res.writeHead(statusCode, statusMessage, responseHeaders);
330
+ chunks.forEach((entry) => res.write(entry));
331
+ res.write(chunk);
332
+ proxyRes.pipe(res, { end: false });
333
+ chunks.length = 0;
334
+ });
335
+
336
+ proxyRes.on("end", () => {
337
+ if (streaming) {
338
+ res.end();
339
+ resolve({ streamed: true, historyBody: tail.toHistory(historyEncoding) });
340
+ return;
341
+ }
342
+
343
+ resolve({ streamed: false, rawBody: Buffer.concat(chunks, total) });
344
+ });
345
+
346
+ proxyRes.on("error", (error) => {
347
+ if (streaming) {
348
+ res.destroy(error);
349
+ }
350
+ reject(error);
351
+ });
352
+ });
353
+ }
354
+
355
+ /**
356
+ * 上流レスポンス読み込み失敗をブラウザへ返す。
357
+ */
358
+ function sendProxyReadError(res, error) {
359
+ if (!res.headersSent) {
360
+ res.writeHead(error.statusCode || 502, {
361
+ "content-type": "text/plain; charset=utf-8",
362
+ });
363
+ }
364
+ res.end(error.message);
365
+ }
366
+
367
+ /**
368
+ * ブラウザからのHTTPリクエスト1件をclientまたはリモートAPIへ中継する。
369
+ * API向けリクエストではStub、JSON Override、Cookie注入、履歴記録も扱う。
370
+ *
371
+ * @param {import("node:http").IncomingMessage} req ブラウザからのリクエスト。
372
+ * @param {import("node:http").ServerResponse} res ブラウザへ返すレスポンス。
373
+ * @param {URL} targetOrigin 選択された上流Origin。
374
+ * @param {URL} publicOrigin ブラウザから見えるローカルプロキシOrigin。
375
+ * @param {object} config runtime状態を含むマージ済みプロキシ設定。
376
+ * @param {"api"|"client"} label 挙動分岐とログに使う転送先ラベル。
377
+ */
378
+ async function proxyHttp(req, res, targetOrigin, publicOrigin, config, label) {
379
+ const requestUrl = new URL(req.url, targetOrigin);
380
+ const requestPath = `${requestUrl.pathname}${requestUrl.search}`;
381
+ const transport = requestUrl.protocol === "https:" ? https : http;
382
+ const started = Date.now();
383
+ const isApi = label === "api";
384
+ const inspectRequestBody =
385
+ isApi && shouldInspectRequestBody(req, config.maxRequestBodyInspectionBytes);
386
+ const requestBodyBuffer = inspectRequestBody ? await readRequestBuffer(req) : null;
387
+ const requestMatch = {
388
+ pathname: requestUrl.pathname,
389
+ queryParams: queryParamsObject(requestUrl.searchParams),
390
+ requestBody: parseRequestBody(req.headers, requestBodyBuffer),
391
+ };
392
+ const preserveLocationHeader = shouldPreserveLocationHeader(
393
+ requestUrl.pathname,
394
+ config.matchers,
395
+ );
396
+ const stub = isApi ? findStub(config, req.method, requestMatch) : null;
397
+
398
+ if (stub) {
399
+ const body = stubBodyBuffer(stub);
400
+ const headers = {
401
+ ...stub.headers,
402
+ "content-length": Buffer.byteLength(body),
403
+ };
404
+ res.writeHead(stub.status, headers);
405
+ res.end(body);
406
+ addHistory(config, {
407
+ type: "stub",
408
+ method: req.method,
409
+ path: requestPath,
410
+ origin: targetOrigin.origin,
411
+ status: stub.status,
412
+ durationMs: Date.now() - started,
413
+ responseHeaders: headers,
414
+ requestQuery: requestMatch.queryParams,
415
+ requestBody: requestMatch.requestBody,
416
+ ...historyBodyFromBuffer(body, stub.bodyEncoding),
417
+ stubId: stub.id,
418
+ stub: ruleHistorySummary(stub),
419
+ });
420
+ return;
421
+ }
422
+
423
+ const proxyReq = transport.request(
424
+ {
425
+ protocol: requestUrl.protocol,
426
+ hostname: requestUrl.hostname,
427
+ port: requestUrl.port,
428
+ method: req.method,
429
+ path: requestPath,
430
+ headers: rewriteRequestHeaders(req, targetOrigin, publicOrigin, {
431
+ apiCookie: isApi ? getActiveApiCookie(config) : "",
432
+ }),
433
+ timeout: config.requestTimeoutMs,
434
+ },
435
+ (proxyRes) => {
436
+ const responseHeaders = rewriteResponseHeaders(
437
+ proxyRes.headers,
438
+ targetOrigin,
439
+ publicOrigin,
440
+ {
441
+ preserveLocationHeader,
442
+ clientOrigin: config.clientOriginUrl,
443
+ requestPathname: requestUrl.pathname,
444
+ locationHeaderQueryOverrideRules:
445
+ config.locationHeaderQueryOverrideRules,
446
+ },
447
+ );
448
+
449
+ if (isApi && shouldRewriteBody(proxyRes.headers)) {
450
+ const matchedJsonOverrides = shouldApplyJsonOverrides(proxyRes.headers)
451
+ ? findJsonOverrides(config, req.method, requestMatch)
452
+ : [];
453
+ bufferOrStreamOriginalResponse(proxyRes, res, responseHeaders, {
454
+ statusCode: proxyRes.statusCode || 502,
455
+ statusMessage: proxyRes.statusMessage,
456
+ historyEncoding: "text",
457
+ maxHistoryBodyBytes: config.maxHistoryBodyBytes,
458
+ maxRewriteBytes: config.maxResponseBodyRewriteBytes,
459
+ })
460
+ .then((result) => {
461
+ if (result.streamed) {
462
+ addHistory(config, {
463
+ type: "remote",
464
+ method: req.method,
465
+ path: requestPath,
466
+ origin: targetOrigin.origin,
467
+ status: proxyRes.statusCode || 502,
468
+ durationMs: Date.now() - started,
469
+ responseHeaders,
470
+ appliedOverrideIds: [],
471
+ skippedOverrides: matchedJsonOverrides.map(ruleHistorySummary),
472
+ requestQuery: requestMatch.queryParams,
473
+ requestBody: requestMatch.requestBody,
474
+ responseBodyRewriteSkipped: true,
475
+ jsonOverrideSkipped: matchedJsonOverrides.length > 0,
476
+ skippedOverrideIds: matchedJsonOverrides.map(
477
+ (override) => override.id,
478
+ ),
479
+ ...result.historyBody,
480
+ });
481
+ log(
482
+ config,
483
+ "debug",
484
+ label,
485
+ req.method,
486
+ req.url,
487
+ proxyRes.statusCode,
488
+ `${Date.now() - started}ms`,
489
+ "body rewrite skipped by size limit",
490
+ );
491
+ return;
492
+ }
493
+
494
+ const rawBody = result.rawBody;
495
+ let body = rewriteBody(rawBody, targetOrigin, publicOrigin);
496
+ let appliedOverrideIds = [];
497
+ if (isApi && shouldApplyJsonOverrides(proxyRes.headers)) {
498
+ const result = applyJsonOverrides(
499
+ body,
500
+ matchedJsonOverrides,
501
+ );
502
+ body = result.body;
503
+ appliedOverrideIds = result.appliedOverrideIds;
504
+ }
505
+ responseHeaders["content-length"] = Buffer.byteLength(body);
506
+ res.writeHead(
507
+ proxyRes.statusCode || 502,
508
+ proxyRes.statusMessage,
509
+ responseHeaders,
510
+ );
511
+ res.end(body);
512
+ if (isApi) {
513
+ addHistory(config, {
514
+ type: "remote",
515
+ method: req.method,
516
+ path: requestPath,
517
+ origin: targetOrigin.origin,
518
+ status: proxyRes.statusCode || 502,
519
+ durationMs: Date.now() - started,
520
+ responseHeaders,
521
+ appliedOverrideIds,
522
+ appliedOverrides: matchedJsonOverrides
523
+ .filter((override) => appliedOverrideIds.includes(override.id))
524
+ .map(ruleHistorySummary),
525
+ requestQuery: requestMatch.queryParams,
526
+ requestBody: requestMatch.requestBody,
527
+ ...historyBodyFromBuffer(
528
+ body,
529
+ "text",
530
+ config.maxHistoryBodyBytes,
531
+ ),
532
+ });
533
+ }
534
+ log(
535
+ config,
536
+ "debug",
537
+ label,
538
+ req.method,
539
+ req.url,
540
+ proxyRes.statusCode,
541
+ `${Date.now() - started}ms`,
542
+ );
543
+ })
544
+ .catch((error) => {
545
+ sendProxyReadError(res, error);
546
+ log(config, "error", label, req.method, req.url, error.message);
547
+ });
548
+ return;
549
+ }
550
+
551
+ streamOriginalResponse(proxyRes, res, responseHeaders, {
552
+ statusCode: proxyRes.statusCode || 502,
553
+ statusMessage: proxyRes.statusMessage,
554
+ historyEncoding: "base64",
555
+ maxHistoryBodyBytes: config.maxHistoryBodyBytes,
556
+ })
557
+ .then((historyBody) => {
558
+ if (isApi) {
559
+ addHistory(config, {
560
+ type: "remote",
561
+ method: req.method,
562
+ path: requestPath,
563
+ origin: targetOrigin.origin,
564
+ status: proxyRes.statusCode || 502,
565
+ durationMs: Date.now() - started,
566
+ responseHeaders,
567
+ requestQuery: requestMatch.queryParams,
568
+ requestBody: requestMatch.requestBody,
569
+ ...historyBody,
570
+ });
571
+ }
572
+ log(
573
+ config,
574
+ "debug",
575
+ label,
576
+ req.method,
577
+ req.url,
578
+ proxyRes.statusCode,
579
+ `${Date.now() - started}ms`,
580
+ );
581
+ })
582
+ .catch((error) => {
583
+ sendProxyReadError(res, error);
584
+ log(config, "error", label, req.method, req.url, error.message);
585
+ });
586
+ },
587
+ );
588
+
589
+ proxyReq.on("timeout", () => {
590
+ proxyReq.destroy(
591
+ new Error(`Proxy request timed out after ${config.requestTimeoutMs}ms.`),
592
+ );
593
+ });
594
+
595
+ proxyReq.on("error", (error) => {
596
+ if (!res.headersSent) {
597
+ res.writeHead(502, { "content-type": "text/plain; charset=utf-8" });
598
+ }
599
+
600
+ res.end(
601
+ `Proxy error while connecting to ${targetOrigin.origin}: ${error.message}`,
602
+ );
603
+ if (isApi) {
604
+ addHistory(config, {
605
+ type: "error",
606
+ method: req.method,
607
+ path: requestPath,
608
+ origin: targetOrigin.origin,
609
+ status: 502,
610
+ durationMs: Date.now() - started,
611
+ responseHeaders: { "content-type": "text/plain; charset=utf-8" },
612
+ requestQuery: requestMatch.queryParams,
613
+ requestBody: requestMatch.requestBody,
614
+ responseBody: error.message,
615
+ });
616
+ }
617
+ log(config, "error", label, req.method, req.url, error.message);
618
+ });
619
+
620
+ if (isApi && requestBodyBuffer !== null) {
621
+ proxyReq.end(requestBodyBuffer);
622
+ } else {
623
+ req.pipe(proxyReq);
624
+ }
625
+ }
626
+
627
+ /**
628
+ * WebSocket upgradeリクエストをclient dev serverへ中継する。
629
+ */
630
+ function proxyUpgrade(req, socket, head, targetOrigin, publicOrigin, config) {
631
+ const targetSocket = net.connect(
632
+ Number(targetOrigin.port || 80),
633
+ targetOrigin.hostname,
634
+ () => {
635
+ const headers = rewriteRequestHeaders(req, targetOrigin, publicOrigin);
636
+ const headerLines = Object.entries(headers).map(
637
+ ([key, value]) => `${key}: ${value}`,
638
+ );
639
+
640
+ targetSocket.write(
641
+ `${req.method} ${req.url} HTTP/${req.httpVersion}\r\n`,
642
+ );
643
+ targetSocket.write(`${headerLines.join("\r\n")}\r\n\r\n`);
644
+ if (head.length) targetSocket.write(head);
645
+
646
+ targetSocket.pipe(socket);
647
+ socket.pipe(targetSocket);
648
+ },
649
+ );
650
+
651
+ targetSocket.on("error", (error) => {
652
+ socket.end(
653
+ `HTTP/1.1 502 Bad Gateway\r\nContent-Type: text/plain\r\n\r\n${error.message}`,
654
+ );
655
+ log(config, "error", "ws", req.url, error.message);
656
+ });
657
+ }
658
+
659
+ /**
660
+ * 履歴詳細に残すルール概要を作る。
661
+ */
662
+ function ruleHistorySummary(rule) {
663
+ return {
664
+ id: rule.id,
665
+ name: rule.name,
666
+ method: rule.method,
667
+ pathPattern: rule.pathPattern,
668
+ };
669
+ }
670
+
671
+ module.exports = { startProxyServer };