@lark-apaas/nestjs-http-forwarder 0.1.1-alpha.1 → 0.1.2
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/README.md +4 -4
- package/dist/index.cjs +2 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ NestJS 模块:通用内网转发能力。**自动注册 controller**,把请
|
|
|
9
9
|
## 自动注册的接口
|
|
10
10
|
|
|
11
11
|
```
|
|
12
|
-
ALL /anycross/forward?targetUrl=<内网 URL>
|
|
12
|
+
ALL /api/sdk_innerapi/anycross/forward?targetUrl=<内网 URL>
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
- **method**:取自请求自身(GET/POST/PUT/PATCH/DELETE/HEAD/OPTIONS)
|
|
@@ -47,17 +47,17 @@ export class AppModule {}
|
|
|
47
47
|
import { axiosForBackend } from '@lark-apaas/client-toolkit';
|
|
48
48
|
|
|
49
49
|
// GET 内网接口
|
|
50
|
-
const res = await axiosForBackend.get('/anycross/forward', {
|
|
50
|
+
const res = await axiosForBackend.get('/api/sdk_innerapi/anycross/forward', {
|
|
51
51
|
params: { targetUrl: 'http://api.corp.com/v1/users' },
|
|
52
52
|
});
|
|
53
53
|
|
|
54
54
|
// POST 内网接口
|
|
55
|
-
await axiosForBackend.post('/anycross/forward', { qty: 3 }, {
|
|
55
|
+
await axiosForBackend.post('/api/sdk_innerapi/anycross/forward', { qty: 3 }, {
|
|
56
56
|
params: { targetUrl: 'http://api.corp.com/v1/orders' },
|
|
57
57
|
});
|
|
58
58
|
|
|
59
59
|
// 其他 method 同理
|
|
60
|
-
await axiosForBackend.delete('/anycross/forward', {
|
|
60
|
+
await axiosForBackend.delete('/api/sdk_innerapi/anycross/forward', {
|
|
61
61
|
params: { targetUrl: 'http://api.corp.com/v1/orders/42' },
|
|
62
62
|
});
|
|
63
63
|
```
|
package/dist/index.cjs
CHANGED
|
@@ -41,7 +41,7 @@ var import_common3 = require("@nestjs/common");
|
|
|
41
41
|
|
|
42
42
|
// src/const.ts
|
|
43
43
|
var HTTP_FORWARDER_MODULE_OPTIONS = /* @__PURE__ */ Symbol("HTTP_FORWARDER_MODULE_OPTIONS");
|
|
44
|
-
var CONTROLLER_ROUTE = "anycross/forward";
|
|
44
|
+
var CONTROLLER_ROUTE = "api/sdk_innerapi/anycross/forward";
|
|
45
45
|
var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
|
|
46
46
|
var DEFAULT_MAX_RESPONSE_BYTES = 10 * 1024 * 1024;
|
|
47
47
|
var ALLOWED_PROTOCOLS = [
|
|
@@ -374,6 +374,7 @@ var HttpForwarderModule = class _HttpForwarderModule {
|
|
|
374
374
|
const resolved = resolveOptions(options);
|
|
375
375
|
return {
|
|
376
376
|
module: _HttpForwarderModule,
|
|
377
|
+
global: true,
|
|
377
378
|
controllers: [
|
|
378
379
|
HttpForwarderController
|
|
379
380
|
],
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/http-forwarder.module.ts","../src/const.ts","../src/controllers/http-forwarder.controller.ts","../src/services/http-forwarder.service.ts","../src/services/options.resolved.ts"],"sourcesContent":["/**\n * @lark-apaas/nestjs-http-forwarder\n *\n * NestJS 模块:通用内网转发能力。自动注册 controller `/anycross/forward`\n * 接收前端请求并通过 node fetch 透传到目标 URL;同时导出 `HttpForwarderService`\n * 供需要自定义 controller 的场景使用。\n *\n * 用途:把请求的出口从浏览器变成 node。客户内网访问的代理 / 鉴权 / 隧道由部署\n * 环境(如沙箱内的 mihomo 透明代理 + iptables 拦截)负责,SDK 不感知。\n */\n\n// 主模块\nexport { HttpForwarderModule } from './http-forwarder.module';\n\n// 控制器\nexport * from './controllers';\n\n// 服务\nexport * from './services';\n\n// 类型定义\nexport * from './types';\n\n// 常量\nexport {\n HTTP_FORWARDER_MODULE_OPTIONS,\n CONTROLLER_ROUTE,\n DEFAULT_REQUEST_TIMEOUT_MS,\n DEFAULT_MAX_RESPONSE_BYTES,\n ALLOWED_PROTOCOLS,\n HEADERS_NOT_FORWARDED_TO_UPSTREAM,\n HEADERS_NOT_RETURNED_TO_CLIENT,\n ERROR_CODES,\n} from './const';\n","import { DynamicModule, Module } from '@nestjs/common';\nimport { HTTP_FORWARDER_MODULE_OPTIONS } from './const';\nimport { HttpForwarderController } from './controllers/http-forwarder.controller';\nimport { HttpForwarderService } from './services/http-forwarder.service';\nimport { resolveOptions } from './services/options.resolved';\nimport type { HttpForwarderModuleOptions } from './types';\n\n/**\n * HttpForwarderModule\n *\n * 自动注册:\n * - `HttpForwarderController` —— 通用内网转发 endpoint `ALL /anycross/forward?targetUrl=...`\n * 支持所有 HTTP method,headers / body 自动透传,上游响应直接投射到客户端。\n * - `HttpForwarderService`(可注入到自定义 controller,编排特殊场景)\n *\n * **不内置鉴权**——消费方在 AppModule 层面给路由加 guard(如全局 NeedLoginGuard)。\n *\n * 使用示例:\n * ```ts\n * @Module({\n * imports: [\n * HttpForwarderModule.forRoot({\n * requestTimeoutMs: 30_000,\n * maxResponseBytes: 10 * 1024 * 1024,\n * }),\n * ],\n * })\n * export class AppModule {}\n * ```\n *\n * 前端调用:\n * ```ts\n * axiosForBackend.get('/anycross/forward', { params: { targetUrl: 'http://api.corp.com/v1/users' } });\n * axiosForBackend.post('/anycross/forward', body, { params: { targetUrl: 'http://api.corp.com/v1/orders' } });\n * ```\n *\n * **注**:实际代理 / 鉴权 / 隧道由部署环境处理(如沙箱内的 mihomo 透明代理 +\n * iptables 拦截)。SDK 不感知 anycross / proxy_group_id / jwtToken。\n */\n@Module({})\nexport class HttpForwarderModule {\n static forRoot(options?: HttpForwarderModuleOptions): DynamicModule {\n const resolved = resolveOptions(options);\n return {\n module: HttpForwarderModule,\n controllers: [HttpForwarderController],\n providers: [\n {\n provide: HTTP_FORWARDER_MODULE_OPTIONS,\n useValue: resolved,\n },\n HttpForwarderService,\n ],\n exports: [HttpForwarderService],\n };\n }\n}\n","/** HttpForwarder module options injection token */\nexport const HTTP_FORWARDER_MODULE_OPTIONS = Symbol('HTTP_FORWARDER_MODULE_OPTIONS');\n\n/** Controller route prefix (硬编码,不可配置以保持调用方式统一) */\nexport const CONTROLLER_ROUTE = 'anycross/forward';\n\n/** Default request timeout in milliseconds */\nexport const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;\n\n/** Default maximum response body size in bytes (10 MB) */\nexport const DEFAULT_MAX_RESPONSE_BYTES = 10 * 1024 * 1024;\n\n/** Allowed target URL protocols */\nexport const ALLOWED_PROTOCOLS = ['http:', 'https:'] as const;\n\n/**\n * 入站 headers 中不能透传给上游的 hop-by-hop / transport / encoding 字段。\n * RFC 7230 §6.1 hop-by-hop + accept-encoding(由 fetch / undici 接管)。\n *\n * 注:host / content-length 由 service.buildOutgoingHeaders 在写入 fetch 入参时\n * 单点处理(host 重写为 targetUrl.host;content-length 让 fetch 自算),此处不重复。\n */\nexport const HEADERS_NOT_FORWARDED_TO_UPSTREAM = new Set([\n 'connection',\n 'keep-alive',\n 'transfer-encoding',\n 'upgrade',\n 'proxy-connection',\n 'proxy-authenticate',\n 'proxy-authorization',\n 'te',\n 'trailer',\n 'accept-encoding',\n]);\n\n/**\n * 上游响应 headers 中不应回写到下游 response 的字段。\n * - content-encoding:fetch 已解压,原 encoding 不再适用\n * - content-length:node http 会根据真实 body 重算\n * - transfer-encoding / connection / keep-alive:hop-by-hop\n */\nexport const HEADERS_NOT_RETURNED_TO_CLIENT = new Set([\n 'content-encoding',\n 'content-length',\n 'transfer-encoding',\n 'connection',\n 'keep-alive',\n 'upgrade',\n]);\n\n/** Error codes returned to callers */\nexport const ERROR_CODES = {\n INVALID_REQUEST: 'INVALID_REQUEST',\n INVALID_TARGET_PROTOCOL: 'INVALID_TARGET_PROTOCOL',\n UPSTREAM_UNREACHABLE: 'UPSTREAM_UNREACHABLE',\n UPSTREAM_TIMEOUT: 'UPSTREAM_TIMEOUT',\n RESPONSE_TOO_LARGE: 'RESPONSE_TOO_LARGE',\n} as const;\n","import {\n All,\n Controller,\n HttpException,\n HttpStatus,\n Req,\n Res,\n} from '@nestjs/common';\nimport type { Request, Response } from 'express';\nimport {\n CONTROLLER_ROUTE,\n ERROR_CODES,\n HEADERS_NOT_FORWARDED_TO_UPSTREAM,\n HEADERS_NOT_RETURNED_TO_CLIENT,\n} from '../const';\nimport { HttpForwarderService } from '../services/http-forwarder.service';\n\n/**\n * 通用内网转发 controller。\n *\n * 路径:`/anycross/forward`(硬编码,所有消费方一致)。\n *\n * 接受所有 HTTP method(GET/POST/PUT/PATCH/DELETE/HEAD/OPTIONS)。\n * - method:直接取自入站请求自身\n * - targetUrl:取自 query `?targetUrl=...`\n * - headers:透传入站请求 headers(剔除 hop-by-hop / accept-encoding;\n * host / content-length 由 service 单点处理)\n * - body:透传入站请求 body(已经过 express body-parser,按情况序列化为字符串)\n *\n * 响应:上游 status / headers / body **直接投射到 HTTP response**,让前端 axios\n * 看到的就是真实上游响应(而非 wrap 的 JSON)。\n *\n * **SDK 不内置鉴权**——消费方在 AppModule 层面给本路由加 guard(如全局 NeedLoginGuard)。\n */\n@Controller()\nexport class HttpForwarderController {\n constructor(private readonly svc: HttpForwarderService) {}\n\n @All(CONTROLLER_ROUTE)\n async forward(@Req() req: Request, @Res() res: Response): Promise<void> {\n const targetUrl = req.query?.targetUrl;\n if (typeof targetUrl !== 'string' || !targetUrl) {\n throw new HttpException(\n {\n code: ERROR_CODES.INVALID_REQUEST,\n message: 'query parameter `targetUrl` is required',\n },\n HttpStatus.BAD_REQUEST,\n );\n }\n\n const out = await this.svc.forward({\n method: req.method.toUpperCase(),\n targetUrl,\n headers: HttpForwarderController.pickIncomingHeaders(req.headers),\n body: HttpForwarderController.serializeIncomingBody(req),\n });\n\n res.status(out.status);\n for (const [key, value] of Object.entries(out.headers)) {\n if (HEADERS_NOT_RETURNED_TO_CLIENT.has(key.toLowerCase())) continue;\n res.setHeader(key, value);\n }\n res.send(out.body);\n }\n\n /**\n * 从入站请求 headers 选出可透传给上游的字段。剔除 hop-by-hop / accept-encoding。\n * 保留 cookie / authorization / x-* 等业务头。\n * host / content-length 不在此处剥离——由 service.buildOutgoingHeaders 单点处理。\n */\n static pickIncomingHeaders(incoming: Request['headers']): Record<string, string> {\n const out: Record<string, string> = {};\n for (const [key, raw] of Object.entries(incoming)) {\n if (HEADERS_NOT_FORWARDED_TO_UPSTREAM.has(key.toLowerCase())) continue;\n if (raw === undefined) continue;\n out[key] = Array.isArray(raw) ? raw.join(', ') : String(raw);\n }\n return out;\n }\n\n /**\n * 把 express 已解析的 body 序列化回字符串供 service 透传。\n */\n static serializeIncomingBody(req: Request): string | null {\n const method = req.method.toUpperCase();\n if (method === 'GET' || method === 'HEAD') return null;\n const body = (req as { body?: unknown }).body;\n if (body === undefined || body === null) return null;\n if (typeof body === 'string') return body;\n if (Buffer.isBuffer(body)) return body.toString('utf-8');\n if (typeof body === 'object' && Object.keys(body as object).length === 0) {\n return null;\n }\n return JSON.stringify(body);\n }\n}\n","import { HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common';\nimport {\n ALLOWED_PROTOCOLS,\n ERROR_CODES,\n HTTP_FORWARDER_MODULE_OPTIONS,\n} from '../const';\nimport type { ForwardRequestDto, ForwardResponseDto } from '../types';\nimport type { HttpForwarderOptionsResolved } from './options.resolved';\n\n/**\n * 反向 HTTP 转发器:把入站请求\"原样\"转发到目标 URL。\n *\n * 用途:把请求的出口从浏览器变成 node。客户内网访问的代理 / 鉴权 / 隧道由\n * 部署环境(如沙箱内的 mihomo 透明代理 + iptables 拦截)负责,SDK 不感知。\n *\n * 只做 5 件核心事:\n * 1. 协议白名单(仅 http/https,防 SSRF)\n * 2. host 重写 + content-length 剥离(语义正确性,单点处理)\n * 3. 单次请求超时(AbortController,默认 30s)\n * 4. 响应体大小上限(流式累计字节,防 OOM,默认 10MB)\n * 5. 错误码映射(给调用方稳定的 error contract)\n */\n@Injectable()\nexport class HttpForwarderService {\n constructor(\n @Inject(HTTP_FORWARDER_MODULE_OPTIONS)\n private readonly options: HttpForwarderOptionsResolved,\n ) {}\n\n async forward(payload: ForwardRequestDto): Promise<ForwardResponseDto> {\n const targetUrl = this.assertValidTargetUrl(payload?.targetUrl);\n const headers = this.buildOutgoingHeaders(payload.headers, targetUrl);\n const controller = new AbortController();\n const timeoutTimer = setTimeout(\n () => controller.abort(),\n this.options.requestTimeoutMs,\n );\n if (typeof timeoutTimer.unref === 'function') timeoutTimer.unref();\n\n try {\n const response = await fetch(targetUrl.toString(), {\n method: payload.method,\n headers,\n body: payload.body ?? undefined,\n signal: controller.signal,\n });\n const body = await this.readBoundedBody(response, controller);\n return {\n status: response.status,\n headers: Object.fromEntries(response.headers),\n body,\n };\n } catch (err) {\n throw this.mapNetworkError(err, controller);\n } finally {\n clearTimeout(timeoutTimer);\n }\n }\n\n /**\n * URL 解析 + 协议白名单。其他入参校验交给 fetch 自身。\n */\n private assertValidTargetUrl(raw: unknown): URL {\n if (typeof raw !== 'string' || !raw) {\n throw new HttpException(\n { code: ERROR_CODES.INVALID_REQUEST, message: 'targetUrl is required' },\n HttpStatus.BAD_REQUEST,\n );\n }\n let parsed: URL;\n try {\n parsed = new URL(raw);\n } catch {\n throw new HttpException(\n { code: ERROR_CODES.INVALID_REQUEST, message: 'Invalid targetUrl' },\n HttpStatus.BAD_REQUEST,\n );\n }\n if (\n !ALLOWED_PROTOCOLS.includes(parsed.protocol as (typeof ALLOWED_PROTOCOLS)[number])\n ) {\n throw new HttpException(\n {\n code: ERROR_CODES.INVALID_TARGET_PROTOCOL,\n message: `Unsupported protocol: ${parsed.protocol}`,\n },\n HttpStatus.BAD_REQUEST,\n );\n }\n return parsed;\n }\n\n /**\n * host / content-length 的**单点处理点**:\n * - host:强制覆盖为 targetUrl.host(防上游误识别,无论调用方传什么)\n * - content-length:剥离(fetch 会按真实 body 自算)\n *\n * 其他 hop-by-hop / accept-encoding 等由 controller 已剥离,service 信任入参。\n */\n private buildOutgoingHeaders(\n incoming: Record<string, string> | undefined,\n targetUrl: URL,\n ): Record<string, string> {\n const out: Record<string, string> = {};\n if (incoming) {\n for (const [key, value] of Object.entries(incoming)) {\n const lower = key.toLowerCase();\n if (lower === 'host' || lower === 'content-length') continue;\n out[key] = value;\n }\n }\n out['host'] = targetUrl.host;\n return out;\n }\n\n /**\n * 流式读取响应体并累计字节数,超 `maxResponseBytes` 即 abort + 抛 502。\n */\n private async readBoundedBody(\n response: Response,\n controller: AbortController,\n ): Promise<string> {\n const limit = this.options.maxResponseBytes;\n if (!response.body) return '';\n const reader = response.body.getReader();\n const chunks: Uint8Array[] = [];\n let total = 0;\n try {\n // eslint-disable-next-line no-constant-condition\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n if (value) {\n total += value.byteLength;\n if (total > limit) {\n controller.abort();\n throw new HttpException(\n {\n code: ERROR_CODES.RESPONSE_TOO_LARGE,\n message: `response exceeds maxResponseBytes (${limit})`,\n },\n HttpStatus.BAD_GATEWAY,\n );\n }\n chunks.push(value);\n }\n }\n } finally {\n void reader.cancel().catch(() => undefined);\n }\n return Buffer.concat(chunks).toString('utf-8');\n }\n\n /**\n * 把 fetch 抛错 / 超时统一映射为 HttpException。\n *\n * 1. HttpException → 直接透传(含 INVALID_TARGET_PROTOCOL / RESPONSE_TOO_LARGE)\n * 2. controller.signal.aborted → UPSTREAM_TIMEOUT 504\n * 3. TypeError → INVALID_REQUEST 400(fetch 对配置错统一抛 TypeError)\n * 4. 兜底 → UPSTREAM_UNREACHABLE 502\n */\n private mapNetworkError(err: unknown, controller: AbortController): HttpException {\n if (err instanceof HttpException) return err;\n if (controller.signal.aborted) {\n return new HttpException(\n { code: ERROR_CODES.UPSTREAM_TIMEOUT, message: 'upstream timeout' },\n HttpStatus.GATEWAY_TIMEOUT,\n );\n }\n if (err instanceof TypeError) {\n return new HttpException(\n { code: ERROR_CODES.INVALID_REQUEST, message: 'invalid request configuration' },\n HttpStatus.BAD_REQUEST,\n );\n }\n return new HttpException(\n { code: ERROR_CODES.UPSTREAM_UNREACHABLE, message: 'upstream unreachable' },\n HttpStatus.BAD_GATEWAY,\n );\n }\n}\n","import type { HttpForwarderModuleOptions } from '../types';\nimport {\n DEFAULT_MAX_RESPONSE_BYTES,\n DEFAULT_REQUEST_TIMEOUT_MS,\n} from '../const';\n\n/**\n * forRoot 入参经过默认值合并后的形态。SDK 内部统一使用此类型。\n */\nexport interface HttpForwarderOptionsResolved\n extends Required<HttpForwarderModuleOptions> {}\n\n/**\n * 校验并补默认值。配置错误(非法字段值)在模块装配时抛出,启动期 fail-fast。\n */\nexport function resolveOptions(\n options: HttpForwarderModuleOptions | undefined,\n): HttpForwarderOptionsResolved {\n const opts = options ?? {};\n const requestTimeoutMs = opts.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;\n const maxResponseBytes = opts.maxResponseBytes ?? DEFAULT_MAX_RESPONSE_BYTES;\n\n if (!Number.isInteger(requestTimeoutMs) || requestTimeoutMs <= 0) {\n throw new Error(\n 'HttpForwarderModule.forRoot: `requestTimeoutMs` must be a positive integer',\n );\n }\n if (!Number.isInteger(maxResponseBytes) || maxResponseBytes <= 0) {\n throw new Error(\n 'HttpForwarderModule.forRoot: `maxResponseBytes` must be a positive integer',\n );\n }\n return { requestTimeoutMs, maxResponseBytes };\n}\n"],"mappings":"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","names":["import_common","HTTP_FORWARDER_MODULE_OPTIONS","Symbol","CONTROLLER_ROUTE","DEFAULT_REQUEST_TIMEOUT_MS","DEFAULT_MAX_RESPONSE_BYTES","ALLOWED_PROTOCOLS","HEADERS_NOT_FORWARDED_TO_UPSTREAM","Set","HEADERS_NOT_RETURNED_TO_CLIENT","ERROR_CODES","INVALID_REQUEST","INVALID_TARGET_PROTOCOL","UPSTREAM_UNREACHABLE","UPSTREAM_TIMEOUT","RESPONSE_TOO_LARGE","import_common","HttpForwarderService","options","forward","payload","targetUrl","assertValidTargetUrl","headers","buildOutgoingHeaders","controller","AbortController","timeoutTimer","setTimeout","abort","requestTimeoutMs","unref","response","fetch","toString","method","body","undefined","signal","readBoundedBody","status","Object","fromEntries","err","mapNetworkError","clearTimeout","raw","HttpException","code","ERROR_CODES","INVALID_REQUEST","message","HttpStatus","BAD_REQUEST","parsed","URL","ALLOWED_PROTOCOLS","includes","protocol","INVALID_TARGET_PROTOCOL","incoming","out","key","value","entries","lower","toLowerCase","host","limit","maxResponseBytes","reader","getReader","chunks","total","done","read","byteLength","RESPONSE_TOO_LARGE","BAD_GATEWAY","push","cancel","catch","Buffer","concat","aborted","UPSTREAM_TIMEOUT","GATEWAY_TIMEOUT","TypeError","UPSTREAM_UNREACHABLE","HttpForwarderController","svc","forward","req","res","targetUrl","query","HttpException","code","ERROR_CODES","INVALID_REQUEST","message","HttpStatus","BAD_REQUEST","out","method","toUpperCase","headers","pickIncomingHeaders","body","serializeIncomingBody","status","key","value","Object","entries","HEADERS_NOT_RETURNED_TO_CLIENT","has","toLowerCase","setHeader","send","incoming","raw","HEADERS_NOT_FORWARDED_TO_UPSTREAM","undefined","Array","isArray","join","String","Buffer","isBuffer","toString","keys","length","JSON","stringify","resolveOptions","options","opts","requestTimeoutMs","DEFAULT_REQUEST_TIMEOUT_MS","maxResponseBytes","DEFAULT_MAX_RESPONSE_BYTES","Number","isInteger","Error","HttpForwarderModule","forRoot","options","resolved","resolveOptions","module","controllers","HttpForwarderController","providers","provide","HTTP_FORWARDER_MODULE_OPTIONS","useValue","HttpForwarderService","exports"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/http-forwarder.module.ts","../src/const.ts","../src/controllers/http-forwarder.controller.ts","../src/services/http-forwarder.service.ts","../src/services/options.resolved.ts"],"sourcesContent":["/**\n * @lark-apaas/nestjs-http-forwarder\n *\n * NestJS 模块:通用内网转发能力。自动注册 controller `/api/sdk_innerapi/anycross/forward`\n * 接收前端请求并通过 node fetch 透传到目标 URL;同时导出 `HttpForwarderService`\n * 供需要自定义 controller 的场景使用。\n *\n * 用途:把请求的出口从浏览器变成 node。客户内网访问的代理 / 鉴权 / 隧道由部署\n * 环境(如沙箱内的 mihomo 透明代理 + iptables 拦截)负责,SDK 不感知。\n */\n\n// 主模块\nexport { HttpForwarderModule } from './http-forwarder.module';\n\n// 控制器\nexport * from './controllers';\n\n// 服务\nexport * from './services';\n\n// 类型定义\nexport * from './types';\n\n// 常量\nexport {\n HTTP_FORWARDER_MODULE_OPTIONS,\n CONTROLLER_ROUTE,\n DEFAULT_REQUEST_TIMEOUT_MS,\n DEFAULT_MAX_RESPONSE_BYTES,\n ALLOWED_PROTOCOLS,\n HEADERS_NOT_FORWARDED_TO_UPSTREAM,\n HEADERS_NOT_RETURNED_TO_CLIENT,\n ERROR_CODES,\n} from './const';\n","import { DynamicModule, Module } from '@nestjs/common';\nimport { HTTP_FORWARDER_MODULE_OPTIONS } from './const';\nimport { HttpForwarderController } from './controllers/http-forwarder.controller';\nimport { HttpForwarderService } from './services/http-forwarder.service';\nimport { resolveOptions } from './services/options.resolved';\nimport type { HttpForwarderModuleOptions } from './types';\n\n/**\n * HttpForwarderModule\n *\n * 自动注册:\n * - `HttpForwarderController` —— 通用内网转发 endpoint `ALL /api/sdk_innerapi/anycross/forward?targetUrl=...`\n * 支持所有 HTTP method,headers / body 自动透传,上游响应直接投射到客户端。\n * - `HttpForwarderService`(可注入到自定义 controller,编排特殊场景)\n *\n * **不内置鉴权**——消费方在 AppModule 层面给路由加 guard(如全局 NeedLoginGuard)。\n *\n * 使用示例:\n * ```ts\n * @Module({\n * imports: [\n * HttpForwarderModule.forRoot({\n * requestTimeoutMs: 30_000,\n * maxResponseBytes: 10 * 1024 * 1024,\n * }),\n * ],\n * })\n * export class AppModule {}\n * ```\n *\n * 前端调用:\n * ```ts\n * axiosForBackend.get('/api/sdk_innerapi/anycross/forward', { params: { targetUrl: 'http://api.corp.com/v1/users' } });\n * axiosForBackend.post('/api/sdk_innerapi/anycross/forward', body, { params: { targetUrl: 'http://api.corp.com/v1/orders' } });\n * ```\n *\n * **注**:实际代理 / 鉴权 / 隧道由部署环境处理(如沙箱内的 mihomo 透明代理 +\n * iptables 拦截)。SDK 不感知 anycross / proxy_group_id / jwtToken。\n */\n@Module({})\nexport class HttpForwarderModule {\n static forRoot(options?: HttpForwarderModuleOptions): DynamicModule {\n const resolved = resolveOptions(options);\n return {\n module: HttpForwarderModule,\n global: true,\n controllers: [HttpForwarderController],\n providers: [\n {\n provide: HTTP_FORWARDER_MODULE_OPTIONS,\n useValue: resolved,\n },\n HttpForwarderService,\n ],\n exports: [HttpForwarderService],\n };\n }\n}\n","/** HttpForwarder module options injection token */\nexport const HTTP_FORWARDER_MODULE_OPTIONS = Symbol('HTTP_FORWARDER_MODULE_OPTIONS');\n\n/** Controller route prefix (硬编码,不可配置以保持调用方式统一) */\nexport const CONTROLLER_ROUTE = 'api/sdk_innerapi/anycross/forward';\n\n/** Default request timeout in milliseconds */\nexport const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;\n\n/** Default maximum response body size in bytes (10 MB) */\nexport const DEFAULT_MAX_RESPONSE_BYTES = 10 * 1024 * 1024;\n\n/** Allowed target URL protocols */\nexport const ALLOWED_PROTOCOLS = ['http:', 'https:'] as const;\n\n/**\n * 入站 headers 中不能透传给上游的 hop-by-hop / transport / encoding 字段。\n * RFC 7230 §6.1 hop-by-hop + accept-encoding(由 fetch / undici 接管)。\n *\n * 注:host / content-length 由 service.buildOutgoingHeaders 在写入 fetch 入参时\n * 单点处理(host 重写为 targetUrl.host;content-length 让 fetch 自算),此处不重复。\n */\nexport const HEADERS_NOT_FORWARDED_TO_UPSTREAM = new Set([\n 'connection',\n 'keep-alive',\n 'transfer-encoding',\n 'upgrade',\n 'proxy-connection',\n 'proxy-authenticate',\n 'proxy-authorization',\n 'te',\n 'trailer',\n 'accept-encoding',\n]);\n\n/**\n * 上游响应 headers 中不应回写到下游 response 的字段。\n * - content-encoding:fetch 已解压,原 encoding 不再适用\n * - content-length:node http 会根据真实 body 重算\n * - transfer-encoding / connection / keep-alive:hop-by-hop\n */\nexport const HEADERS_NOT_RETURNED_TO_CLIENT = new Set([\n 'content-encoding',\n 'content-length',\n 'transfer-encoding',\n 'connection',\n 'keep-alive',\n 'upgrade',\n]);\n\n/** Error codes returned to callers */\nexport const ERROR_CODES = {\n INVALID_REQUEST: 'INVALID_REQUEST',\n INVALID_TARGET_PROTOCOL: 'INVALID_TARGET_PROTOCOL',\n UPSTREAM_UNREACHABLE: 'UPSTREAM_UNREACHABLE',\n UPSTREAM_TIMEOUT: 'UPSTREAM_TIMEOUT',\n RESPONSE_TOO_LARGE: 'RESPONSE_TOO_LARGE',\n} as const;\n","import {\n All,\n Controller,\n HttpException,\n HttpStatus,\n Req,\n Res,\n} from '@nestjs/common';\nimport type { Request, Response } from 'express';\nimport {\n CONTROLLER_ROUTE,\n ERROR_CODES,\n HEADERS_NOT_FORWARDED_TO_UPSTREAM,\n HEADERS_NOT_RETURNED_TO_CLIENT,\n} from '../const';\nimport { HttpForwarderService } from '../services/http-forwarder.service';\n\n/**\n * 通用内网转发 controller。\n *\n * 路径:`/api/sdk_innerapi/anycross/forward`(硬编码,所有消费方一致)。\n *\n * 接受所有 HTTP method(GET/POST/PUT/PATCH/DELETE/HEAD/OPTIONS)。\n * - method:直接取自入站请求自身\n * - targetUrl:取自 query `?targetUrl=...`\n * - headers:透传入站请求 headers(剔除 hop-by-hop / accept-encoding;\n * host / content-length 由 service 单点处理)\n * - body:透传入站请求 body(已经过 express body-parser,按情况序列化为字符串)\n *\n * 响应:上游 status / headers / body **直接投射到 HTTP response**,让前端 axios\n * 看到的就是真实上游响应(而非 wrap 的 JSON)。\n *\n * **SDK 不内置鉴权**——消费方在 AppModule 层面给本路由加 guard(如全局 NeedLoginGuard)。\n */\n@Controller()\nexport class HttpForwarderController {\n constructor(private readonly svc: HttpForwarderService) {}\n\n @All(CONTROLLER_ROUTE)\n async forward(@Req() req: Request, @Res() res: Response): Promise<void> {\n const targetUrl = req.query?.targetUrl;\n if (typeof targetUrl !== 'string' || !targetUrl) {\n throw new HttpException(\n {\n code: ERROR_CODES.INVALID_REQUEST,\n message: 'query parameter `targetUrl` is required',\n },\n HttpStatus.BAD_REQUEST,\n );\n }\n\n const out = await this.svc.forward({\n method: req.method.toUpperCase(),\n targetUrl,\n headers: HttpForwarderController.pickIncomingHeaders(req.headers),\n body: HttpForwarderController.serializeIncomingBody(req),\n });\n\n res.status(out.status);\n for (const [key, value] of Object.entries(out.headers)) {\n if (HEADERS_NOT_RETURNED_TO_CLIENT.has(key.toLowerCase())) continue;\n res.setHeader(key, value);\n }\n res.send(out.body);\n }\n\n /**\n * 从入站请求 headers 选出可透传给上游的字段。剔除 hop-by-hop / accept-encoding。\n * 保留 cookie / authorization / x-* 等业务头。\n * host / content-length 不在此处剥离——由 service.buildOutgoingHeaders 单点处理。\n */\n static pickIncomingHeaders(incoming: Request['headers']): Record<string, string> {\n const out: Record<string, string> = {};\n for (const [key, raw] of Object.entries(incoming)) {\n if (HEADERS_NOT_FORWARDED_TO_UPSTREAM.has(key.toLowerCase())) continue;\n if (raw === undefined) continue;\n out[key] = Array.isArray(raw) ? raw.join(', ') : String(raw);\n }\n return out;\n }\n\n /**\n * 把 express 已解析的 body 序列化回字符串供 service 透传。\n */\n static serializeIncomingBody(req: Request): string | null {\n const method = req.method.toUpperCase();\n if (method === 'GET' || method === 'HEAD') return null;\n const body = (req as { body?: unknown }).body;\n if (body === undefined || body === null) return null;\n if (typeof body === 'string') return body;\n if (Buffer.isBuffer(body)) return body.toString('utf-8');\n if (typeof body === 'object' && Object.keys(body as object).length === 0) {\n return null;\n }\n return JSON.stringify(body);\n }\n}\n","import { HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common';\nimport {\n ALLOWED_PROTOCOLS,\n ERROR_CODES,\n HTTP_FORWARDER_MODULE_OPTIONS,\n} from '../const';\nimport type { ForwardRequestDto, ForwardResponseDto } from '../types';\nimport type { HttpForwarderOptionsResolved } from './options.resolved';\n\n/**\n * 反向 HTTP 转发器:把入站请求\"原样\"转发到目标 URL。\n *\n * 用途:把请求的出口从浏览器变成 node。客户内网访问的代理 / 鉴权 / 隧道由\n * 部署环境(如沙箱内的 mihomo 透明代理 + iptables 拦截)负责,SDK 不感知。\n *\n * 只做 5 件核心事:\n * 1. 协议白名单(仅 http/https,防 SSRF)\n * 2. host 重写 + content-length 剥离(语义正确性,单点处理)\n * 3. 单次请求超时(AbortController,默认 30s)\n * 4. 响应体大小上限(流式累计字节,防 OOM,默认 10MB)\n * 5. 错误码映射(给调用方稳定的 error contract)\n */\n@Injectable()\nexport class HttpForwarderService {\n constructor(\n @Inject(HTTP_FORWARDER_MODULE_OPTIONS)\n private readonly options: HttpForwarderOptionsResolved,\n ) {}\n\n async forward(payload: ForwardRequestDto): Promise<ForwardResponseDto> {\n const targetUrl = this.assertValidTargetUrl(payload?.targetUrl);\n const headers = this.buildOutgoingHeaders(payload.headers, targetUrl);\n const controller = new AbortController();\n const timeoutTimer = setTimeout(\n () => controller.abort(),\n this.options.requestTimeoutMs,\n );\n if (typeof timeoutTimer.unref === 'function') timeoutTimer.unref();\n\n try {\n const response = await fetch(targetUrl.toString(), {\n method: payload.method,\n headers,\n body: payload.body ?? undefined,\n signal: controller.signal,\n });\n const body = await this.readBoundedBody(response, controller);\n return {\n status: response.status,\n headers: Object.fromEntries(response.headers),\n body,\n };\n } catch (err) {\n throw this.mapNetworkError(err, controller);\n } finally {\n clearTimeout(timeoutTimer);\n }\n }\n\n /**\n * URL 解析 + 协议白名单。其他入参校验交给 fetch 自身。\n */\n private assertValidTargetUrl(raw: unknown): URL {\n if (typeof raw !== 'string' || !raw) {\n throw new HttpException(\n { code: ERROR_CODES.INVALID_REQUEST, message: 'targetUrl is required' },\n HttpStatus.BAD_REQUEST,\n );\n }\n let parsed: URL;\n try {\n parsed = new URL(raw);\n } catch {\n throw new HttpException(\n { code: ERROR_CODES.INVALID_REQUEST, message: 'Invalid targetUrl' },\n HttpStatus.BAD_REQUEST,\n );\n }\n if (\n !ALLOWED_PROTOCOLS.includes(parsed.protocol as (typeof ALLOWED_PROTOCOLS)[number])\n ) {\n throw new HttpException(\n {\n code: ERROR_CODES.INVALID_TARGET_PROTOCOL,\n message: `Unsupported protocol: ${parsed.protocol}`,\n },\n HttpStatus.BAD_REQUEST,\n );\n }\n return parsed;\n }\n\n /**\n * host / content-length 的**单点处理点**:\n * - host:强制覆盖为 targetUrl.host(防上游误识别,无论调用方传什么)\n * - content-length:剥离(fetch 会按真实 body 自算)\n *\n * 其他 hop-by-hop / accept-encoding 等由 controller 已剥离,service 信任入参。\n */\n private buildOutgoingHeaders(\n incoming: Record<string, string> | undefined,\n targetUrl: URL,\n ): Record<string, string> {\n const out: Record<string, string> = {};\n if (incoming) {\n for (const [key, value] of Object.entries(incoming)) {\n const lower = key.toLowerCase();\n if (lower === 'host' || lower === 'content-length') continue;\n out[key] = value;\n }\n }\n out['host'] = targetUrl.host;\n return out;\n }\n\n /**\n * 流式读取响应体并累计字节数,超 `maxResponseBytes` 即 abort + 抛 502。\n */\n private async readBoundedBody(\n response: Response,\n controller: AbortController,\n ): Promise<string> {\n const limit = this.options.maxResponseBytes;\n if (!response.body) return '';\n const reader = response.body.getReader();\n const chunks: Uint8Array[] = [];\n let total = 0;\n try {\n // eslint-disable-next-line no-constant-condition\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n if (value) {\n total += value.byteLength;\n if (total > limit) {\n controller.abort();\n throw new HttpException(\n {\n code: ERROR_CODES.RESPONSE_TOO_LARGE,\n message: `response exceeds maxResponseBytes (${limit})`,\n },\n HttpStatus.BAD_GATEWAY,\n );\n }\n chunks.push(value);\n }\n }\n } finally {\n void reader.cancel().catch(() => undefined);\n }\n return Buffer.concat(chunks).toString('utf-8');\n }\n\n /**\n * 把 fetch 抛错 / 超时统一映射为 HttpException。\n *\n * 1. HttpException → 直接透传(含 INVALID_TARGET_PROTOCOL / RESPONSE_TOO_LARGE)\n * 2. controller.signal.aborted → UPSTREAM_TIMEOUT 504\n * 3. TypeError → INVALID_REQUEST 400(fetch 对配置错统一抛 TypeError)\n * 4. 兜底 → UPSTREAM_UNREACHABLE 502\n */\n private mapNetworkError(err: unknown, controller: AbortController): HttpException {\n if (err instanceof HttpException) return err;\n if (controller.signal.aborted) {\n return new HttpException(\n { code: ERROR_CODES.UPSTREAM_TIMEOUT, message: 'upstream timeout' },\n HttpStatus.GATEWAY_TIMEOUT,\n );\n }\n if (err instanceof TypeError) {\n return new HttpException(\n { code: ERROR_CODES.INVALID_REQUEST, message: 'invalid request configuration' },\n HttpStatus.BAD_REQUEST,\n );\n }\n return new HttpException(\n { code: ERROR_CODES.UPSTREAM_UNREACHABLE, message: 'upstream unreachable' },\n HttpStatus.BAD_GATEWAY,\n );\n }\n}\n","import type { HttpForwarderModuleOptions } from '../types';\nimport {\n DEFAULT_MAX_RESPONSE_BYTES,\n DEFAULT_REQUEST_TIMEOUT_MS,\n} from '../const';\n\n/**\n * forRoot 入参经过默认值合并后的形态。SDK 内部统一使用此类型。\n */\nexport interface HttpForwarderOptionsResolved\n extends Required<HttpForwarderModuleOptions> {}\n\n/**\n * 校验并补默认值。配置错误(非法字段值)在模块装配时抛出,启动期 fail-fast。\n */\nexport function resolveOptions(\n options: HttpForwarderModuleOptions | undefined,\n): HttpForwarderOptionsResolved {\n const opts = options ?? {};\n const requestTimeoutMs = opts.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;\n const maxResponseBytes = opts.maxResponseBytes ?? DEFAULT_MAX_RESPONSE_BYTES;\n\n if (!Number.isInteger(requestTimeoutMs) || requestTimeoutMs <= 0) {\n throw new Error(\n 'HttpForwarderModule.forRoot: `requestTimeoutMs` must be a positive integer',\n );\n }\n if (!Number.isInteger(maxResponseBytes) || maxResponseBytes <= 0) {\n throw new Error(\n 'HttpForwarderModule.forRoot: `maxResponseBytes` must be a positive integer',\n );\n }\n return { requestTimeoutMs, maxResponseBytes };\n}\n"],"mappings":"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","names":["import_common","HTTP_FORWARDER_MODULE_OPTIONS","Symbol","CONTROLLER_ROUTE","DEFAULT_REQUEST_TIMEOUT_MS","DEFAULT_MAX_RESPONSE_BYTES","ALLOWED_PROTOCOLS","HEADERS_NOT_FORWARDED_TO_UPSTREAM","Set","HEADERS_NOT_RETURNED_TO_CLIENT","ERROR_CODES","INVALID_REQUEST","INVALID_TARGET_PROTOCOL","UPSTREAM_UNREACHABLE","UPSTREAM_TIMEOUT","RESPONSE_TOO_LARGE","import_common","HttpForwarderService","options","forward","payload","targetUrl","assertValidTargetUrl","headers","buildOutgoingHeaders","controller","AbortController","timeoutTimer","setTimeout","abort","requestTimeoutMs","unref","response","fetch","toString","method","body","undefined","signal","readBoundedBody","status","Object","fromEntries","err","mapNetworkError","clearTimeout","raw","HttpException","code","ERROR_CODES","INVALID_REQUEST","message","HttpStatus","BAD_REQUEST","parsed","URL","ALLOWED_PROTOCOLS","includes","protocol","INVALID_TARGET_PROTOCOL","incoming","out","key","value","entries","lower","toLowerCase","host","limit","maxResponseBytes","reader","getReader","chunks","total","done","read","byteLength","RESPONSE_TOO_LARGE","BAD_GATEWAY","push","cancel","catch","Buffer","concat","aborted","UPSTREAM_TIMEOUT","GATEWAY_TIMEOUT","TypeError","UPSTREAM_UNREACHABLE","HttpForwarderController","svc","forward","req","res","targetUrl","query","HttpException","code","ERROR_CODES","INVALID_REQUEST","message","HttpStatus","BAD_REQUEST","out","method","toUpperCase","headers","pickIncomingHeaders","body","serializeIncomingBody","status","key","value","Object","entries","HEADERS_NOT_RETURNED_TO_CLIENT","has","toLowerCase","setHeader","send","incoming","raw","HEADERS_NOT_FORWARDED_TO_UPSTREAM","undefined","Array","isArray","join","String","Buffer","isBuffer","toString","keys","length","JSON","stringify","resolveOptions","options","opts","requestTimeoutMs","DEFAULT_REQUEST_TIMEOUT_MS","maxResponseBytes","DEFAULT_MAX_RESPONSE_BYTES","Number","isInteger","Error","HttpForwarderModule","forRoot","options","resolved","resolveOptions","module","global","controllers","HttpForwarderController","providers","provide","HTTP_FORWARDER_MODULE_OPTIONS","useValue","HttpForwarderService","exports"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -36,7 +36,7 @@ interface HttpForwarderModuleOptions {
|
|
|
36
36
|
* HttpForwarderModule
|
|
37
37
|
*
|
|
38
38
|
* 自动注册:
|
|
39
|
-
* - `HttpForwarderController` —— 通用内网转发 endpoint `ALL /anycross/forward?targetUrl=...`
|
|
39
|
+
* - `HttpForwarderController` —— 通用内网转发 endpoint `ALL /api/sdk_innerapi/anycross/forward?targetUrl=...`
|
|
40
40
|
* 支持所有 HTTP method,headers / body 自动透传,上游响应直接投射到客户端。
|
|
41
41
|
* - `HttpForwarderService`(可注入到自定义 controller,编排特殊场景)
|
|
42
42
|
*
|
|
@@ -57,8 +57,8 @@ interface HttpForwarderModuleOptions {
|
|
|
57
57
|
*
|
|
58
58
|
* 前端调用:
|
|
59
59
|
* ```ts
|
|
60
|
-
* axiosForBackend.get('/anycross/forward', { params: { targetUrl: 'http://api.corp.com/v1/users' } });
|
|
61
|
-
* axiosForBackend.post('/anycross/forward', body, { params: { targetUrl: 'http://api.corp.com/v1/orders' } });
|
|
60
|
+
* axiosForBackend.get('/api/sdk_innerapi/anycross/forward', { params: { targetUrl: 'http://api.corp.com/v1/users' } });
|
|
61
|
+
* axiosForBackend.post('/api/sdk_innerapi/anycross/forward', body, { params: { targetUrl: 'http://api.corp.com/v1/orders' } });
|
|
62
62
|
* ```
|
|
63
63
|
*
|
|
64
64
|
* **注**:实际代理 / 鉴权 / 隧道由部署环境处理(如沙箱内的 mihomo 透明代理 +
|
|
@@ -125,7 +125,7 @@ declare class HttpForwarderService {
|
|
|
125
125
|
/**
|
|
126
126
|
* 通用内网转发 controller。
|
|
127
127
|
*
|
|
128
|
-
* 路径:`/anycross/forward`(硬编码,所有消费方一致)。
|
|
128
|
+
* 路径:`/api/sdk_innerapi/anycross/forward`(硬编码,所有消费方一致)。
|
|
129
129
|
*
|
|
130
130
|
* 接受所有 HTTP method(GET/POST/PUT/PATCH/DELETE/HEAD/OPTIONS)。
|
|
131
131
|
* - method:直接取自入站请求自身
|
|
@@ -158,7 +158,7 @@ declare class HttpForwarderController {
|
|
|
158
158
|
/** HttpForwarder module options injection token */
|
|
159
159
|
declare const HTTP_FORWARDER_MODULE_OPTIONS: unique symbol;
|
|
160
160
|
/** Controller route prefix (硬编码,不可配置以保持调用方式统一) */
|
|
161
|
-
declare const CONTROLLER_ROUTE = "anycross/forward";
|
|
161
|
+
declare const CONTROLLER_ROUTE = "api/sdk_innerapi/anycross/forward";
|
|
162
162
|
/** Default request timeout in milliseconds */
|
|
163
163
|
declare const DEFAULT_REQUEST_TIMEOUT_MS = 30000;
|
|
164
164
|
/** Default maximum response body size in bytes (10 MB) */
|
package/dist/index.d.ts
CHANGED
|
@@ -36,7 +36,7 @@ interface HttpForwarderModuleOptions {
|
|
|
36
36
|
* HttpForwarderModule
|
|
37
37
|
*
|
|
38
38
|
* 自动注册:
|
|
39
|
-
* - `HttpForwarderController` —— 通用内网转发 endpoint `ALL /anycross/forward?targetUrl=...`
|
|
39
|
+
* - `HttpForwarderController` —— 通用内网转发 endpoint `ALL /api/sdk_innerapi/anycross/forward?targetUrl=...`
|
|
40
40
|
* 支持所有 HTTP method,headers / body 自动透传,上游响应直接投射到客户端。
|
|
41
41
|
* - `HttpForwarderService`(可注入到自定义 controller,编排特殊场景)
|
|
42
42
|
*
|
|
@@ -57,8 +57,8 @@ interface HttpForwarderModuleOptions {
|
|
|
57
57
|
*
|
|
58
58
|
* 前端调用:
|
|
59
59
|
* ```ts
|
|
60
|
-
* axiosForBackend.get('/anycross/forward', { params: { targetUrl: 'http://api.corp.com/v1/users' } });
|
|
61
|
-
* axiosForBackend.post('/anycross/forward', body, { params: { targetUrl: 'http://api.corp.com/v1/orders' } });
|
|
60
|
+
* axiosForBackend.get('/api/sdk_innerapi/anycross/forward', { params: { targetUrl: 'http://api.corp.com/v1/users' } });
|
|
61
|
+
* axiosForBackend.post('/api/sdk_innerapi/anycross/forward', body, { params: { targetUrl: 'http://api.corp.com/v1/orders' } });
|
|
62
62
|
* ```
|
|
63
63
|
*
|
|
64
64
|
* **注**:实际代理 / 鉴权 / 隧道由部署环境处理(如沙箱内的 mihomo 透明代理 +
|
|
@@ -125,7 +125,7 @@ declare class HttpForwarderService {
|
|
|
125
125
|
/**
|
|
126
126
|
* 通用内网转发 controller。
|
|
127
127
|
*
|
|
128
|
-
* 路径:`/anycross/forward`(硬编码,所有消费方一致)。
|
|
128
|
+
* 路径:`/api/sdk_innerapi/anycross/forward`(硬编码,所有消费方一致)。
|
|
129
129
|
*
|
|
130
130
|
* 接受所有 HTTP method(GET/POST/PUT/PATCH/DELETE/HEAD/OPTIONS)。
|
|
131
131
|
* - method:直接取自入站请求自身
|
|
@@ -158,7 +158,7 @@ declare class HttpForwarderController {
|
|
|
158
158
|
/** HttpForwarder module options injection token */
|
|
159
159
|
declare const HTTP_FORWARDER_MODULE_OPTIONS: unique symbol;
|
|
160
160
|
/** Controller route prefix (硬编码,不可配置以保持调用方式统一) */
|
|
161
|
-
declare const CONTROLLER_ROUTE = "anycross/forward";
|
|
161
|
+
declare const CONTROLLER_ROUTE = "api/sdk_innerapi/anycross/forward";
|
|
162
162
|
/** Default request timeout in milliseconds */
|
|
163
163
|
declare const DEFAULT_REQUEST_TIMEOUT_MS = 30000;
|
|
164
164
|
/** Default maximum response body size in bytes (10 MB) */
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import { Module } from "@nestjs/common";
|
|
|
6
6
|
|
|
7
7
|
// src/const.ts
|
|
8
8
|
var HTTP_FORWARDER_MODULE_OPTIONS = /* @__PURE__ */ Symbol("HTTP_FORWARDER_MODULE_OPTIONS");
|
|
9
|
-
var CONTROLLER_ROUTE = "anycross/forward";
|
|
9
|
+
var CONTROLLER_ROUTE = "api/sdk_innerapi/anycross/forward";
|
|
10
10
|
var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
|
|
11
11
|
var DEFAULT_MAX_RESPONSE_BYTES = 10 * 1024 * 1024;
|
|
12
12
|
var ALLOWED_PROTOCOLS = [
|
|
@@ -339,6 +339,7 @@ var HttpForwarderModule = class _HttpForwarderModule {
|
|
|
339
339
|
const resolved = resolveOptions(options);
|
|
340
340
|
return {
|
|
341
341
|
module: _HttpForwarderModule,
|
|
342
|
+
global: true,
|
|
342
343
|
controllers: [
|
|
343
344
|
HttpForwarderController
|
|
344
345
|
],
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/http-forwarder.module.ts","../src/const.ts","../src/controllers/http-forwarder.controller.ts","../src/services/http-forwarder.service.ts","../src/services/options.resolved.ts"],"sourcesContent":["import { DynamicModule, Module } from '@nestjs/common';\nimport { HTTP_FORWARDER_MODULE_OPTIONS } from './const';\nimport { HttpForwarderController } from './controllers/http-forwarder.controller';\nimport { HttpForwarderService } from './services/http-forwarder.service';\nimport { resolveOptions } from './services/options.resolved';\nimport type { HttpForwarderModuleOptions } from './types';\n\n/**\n * HttpForwarderModule\n *\n * 自动注册:\n * - `HttpForwarderController` —— 通用内网转发 endpoint `ALL /anycross/forward?targetUrl=...`\n * 支持所有 HTTP method,headers / body 自动透传,上游响应直接投射到客户端。\n * - `HttpForwarderService`(可注入到自定义 controller,编排特殊场景)\n *\n * **不内置鉴权**——消费方在 AppModule 层面给路由加 guard(如全局 NeedLoginGuard)。\n *\n * 使用示例:\n * ```ts\n * @Module({\n * imports: [\n * HttpForwarderModule.forRoot({\n * requestTimeoutMs: 30_000,\n * maxResponseBytes: 10 * 1024 * 1024,\n * }),\n * ],\n * })\n * export class AppModule {}\n * ```\n *\n * 前端调用:\n * ```ts\n * axiosForBackend.get('/anycross/forward', { params: { targetUrl: 'http://api.corp.com/v1/users' } });\n * axiosForBackend.post('/anycross/forward', body, { params: { targetUrl: 'http://api.corp.com/v1/orders' } });\n * ```\n *\n * **注**:实际代理 / 鉴权 / 隧道由部署环境处理(如沙箱内的 mihomo 透明代理 +\n * iptables 拦截)。SDK 不感知 anycross / proxy_group_id / jwtToken。\n */\n@Module({})\nexport class HttpForwarderModule {\n static forRoot(options?: HttpForwarderModuleOptions): DynamicModule {\n const resolved = resolveOptions(options);\n return {\n module: HttpForwarderModule,\n controllers: [HttpForwarderController],\n providers: [\n {\n provide: HTTP_FORWARDER_MODULE_OPTIONS,\n useValue: resolved,\n },\n HttpForwarderService,\n ],\n exports: [HttpForwarderService],\n };\n }\n}\n","/** HttpForwarder module options injection token */\nexport const HTTP_FORWARDER_MODULE_OPTIONS = Symbol('HTTP_FORWARDER_MODULE_OPTIONS');\n\n/** Controller route prefix (硬编码,不可配置以保持调用方式统一) */\nexport const CONTROLLER_ROUTE = 'anycross/forward';\n\n/** Default request timeout in milliseconds */\nexport const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;\n\n/** Default maximum response body size in bytes (10 MB) */\nexport const DEFAULT_MAX_RESPONSE_BYTES = 10 * 1024 * 1024;\n\n/** Allowed target URL protocols */\nexport const ALLOWED_PROTOCOLS = ['http:', 'https:'] as const;\n\n/**\n * 入站 headers 中不能透传给上游的 hop-by-hop / transport / encoding 字段。\n * RFC 7230 §6.1 hop-by-hop + accept-encoding(由 fetch / undici 接管)。\n *\n * 注:host / content-length 由 service.buildOutgoingHeaders 在写入 fetch 入参时\n * 单点处理(host 重写为 targetUrl.host;content-length 让 fetch 自算),此处不重复。\n */\nexport const HEADERS_NOT_FORWARDED_TO_UPSTREAM = new Set([\n 'connection',\n 'keep-alive',\n 'transfer-encoding',\n 'upgrade',\n 'proxy-connection',\n 'proxy-authenticate',\n 'proxy-authorization',\n 'te',\n 'trailer',\n 'accept-encoding',\n]);\n\n/**\n * 上游响应 headers 中不应回写到下游 response 的字段。\n * - content-encoding:fetch 已解压,原 encoding 不再适用\n * - content-length:node http 会根据真实 body 重算\n * - transfer-encoding / connection / keep-alive:hop-by-hop\n */\nexport const HEADERS_NOT_RETURNED_TO_CLIENT = new Set([\n 'content-encoding',\n 'content-length',\n 'transfer-encoding',\n 'connection',\n 'keep-alive',\n 'upgrade',\n]);\n\n/** Error codes returned to callers */\nexport const ERROR_CODES = {\n INVALID_REQUEST: 'INVALID_REQUEST',\n INVALID_TARGET_PROTOCOL: 'INVALID_TARGET_PROTOCOL',\n UPSTREAM_UNREACHABLE: 'UPSTREAM_UNREACHABLE',\n UPSTREAM_TIMEOUT: 'UPSTREAM_TIMEOUT',\n RESPONSE_TOO_LARGE: 'RESPONSE_TOO_LARGE',\n} as const;\n","import {\n All,\n Controller,\n HttpException,\n HttpStatus,\n Req,\n Res,\n} from '@nestjs/common';\nimport type { Request, Response } from 'express';\nimport {\n CONTROLLER_ROUTE,\n ERROR_CODES,\n HEADERS_NOT_FORWARDED_TO_UPSTREAM,\n HEADERS_NOT_RETURNED_TO_CLIENT,\n} from '../const';\nimport { HttpForwarderService } from '../services/http-forwarder.service';\n\n/**\n * 通用内网转发 controller。\n *\n * 路径:`/anycross/forward`(硬编码,所有消费方一致)。\n *\n * 接受所有 HTTP method(GET/POST/PUT/PATCH/DELETE/HEAD/OPTIONS)。\n * - method:直接取自入站请求自身\n * - targetUrl:取自 query `?targetUrl=...`\n * - headers:透传入站请求 headers(剔除 hop-by-hop / accept-encoding;\n * host / content-length 由 service 单点处理)\n * - body:透传入站请求 body(已经过 express body-parser,按情况序列化为字符串)\n *\n * 响应:上游 status / headers / body **直接投射到 HTTP response**,让前端 axios\n * 看到的就是真实上游响应(而非 wrap 的 JSON)。\n *\n * **SDK 不内置鉴权**——消费方在 AppModule 层面给本路由加 guard(如全局 NeedLoginGuard)。\n */\n@Controller()\nexport class HttpForwarderController {\n constructor(private readonly svc: HttpForwarderService) {}\n\n @All(CONTROLLER_ROUTE)\n async forward(@Req() req: Request, @Res() res: Response): Promise<void> {\n const targetUrl = req.query?.targetUrl;\n if (typeof targetUrl !== 'string' || !targetUrl) {\n throw new HttpException(\n {\n code: ERROR_CODES.INVALID_REQUEST,\n message: 'query parameter `targetUrl` is required',\n },\n HttpStatus.BAD_REQUEST,\n );\n }\n\n const out = await this.svc.forward({\n method: req.method.toUpperCase(),\n targetUrl,\n headers: HttpForwarderController.pickIncomingHeaders(req.headers),\n body: HttpForwarderController.serializeIncomingBody(req),\n });\n\n res.status(out.status);\n for (const [key, value] of Object.entries(out.headers)) {\n if (HEADERS_NOT_RETURNED_TO_CLIENT.has(key.toLowerCase())) continue;\n res.setHeader(key, value);\n }\n res.send(out.body);\n }\n\n /**\n * 从入站请求 headers 选出可透传给上游的字段。剔除 hop-by-hop / accept-encoding。\n * 保留 cookie / authorization / x-* 等业务头。\n * host / content-length 不在此处剥离——由 service.buildOutgoingHeaders 单点处理。\n */\n static pickIncomingHeaders(incoming: Request['headers']): Record<string, string> {\n const out: Record<string, string> = {};\n for (const [key, raw] of Object.entries(incoming)) {\n if (HEADERS_NOT_FORWARDED_TO_UPSTREAM.has(key.toLowerCase())) continue;\n if (raw === undefined) continue;\n out[key] = Array.isArray(raw) ? raw.join(', ') : String(raw);\n }\n return out;\n }\n\n /**\n * 把 express 已解析的 body 序列化回字符串供 service 透传。\n */\n static serializeIncomingBody(req: Request): string | null {\n const method = req.method.toUpperCase();\n if (method === 'GET' || method === 'HEAD') return null;\n const body = (req as { body?: unknown }).body;\n if (body === undefined || body === null) return null;\n if (typeof body === 'string') return body;\n if (Buffer.isBuffer(body)) return body.toString('utf-8');\n if (typeof body === 'object' && Object.keys(body as object).length === 0) {\n return null;\n }\n return JSON.stringify(body);\n }\n}\n","import { HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common';\nimport {\n ALLOWED_PROTOCOLS,\n ERROR_CODES,\n HTTP_FORWARDER_MODULE_OPTIONS,\n} from '../const';\nimport type { ForwardRequestDto, ForwardResponseDto } from '../types';\nimport type { HttpForwarderOptionsResolved } from './options.resolved';\n\n/**\n * 反向 HTTP 转发器:把入站请求\"原样\"转发到目标 URL。\n *\n * 用途:把请求的出口从浏览器变成 node。客户内网访问的代理 / 鉴权 / 隧道由\n * 部署环境(如沙箱内的 mihomo 透明代理 + iptables 拦截)负责,SDK 不感知。\n *\n * 只做 5 件核心事:\n * 1. 协议白名单(仅 http/https,防 SSRF)\n * 2. host 重写 + content-length 剥离(语义正确性,单点处理)\n * 3. 单次请求超时(AbortController,默认 30s)\n * 4. 响应体大小上限(流式累计字节,防 OOM,默认 10MB)\n * 5. 错误码映射(给调用方稳定的 error contract)\n */\n@Injectable()\nexport class HttpForwarderService {\n constructor(\n @Inject(HTTP_FORWARDER_MODULE_OPTIONS)\n private readonly options: HttpForwarderOptionsResolved,\n ) {}\n\n async forward(payload: ForwardRequestDto): Promise<ForwardResponseDto> {\n const targetUrl = this.assertValidTargetUrl(payload?.targetUrl);\n const headers = this.buildOutgoingHeaders(payload.headers, targetUrl);\n const controller = new AbortController();\n const timeoutTimer = setTimeout(\n () => controller.abort(),\n this.options.requestTimeoutMs,\n );\n if (typeof timeoutTimer.unref === 'function') timeoutTimer.unref();\n\n try {\n const response = await fetch(targetUrl.toString(), {\n method: payload.method,\n headers,\n body: payload.body ?? undefined,\n signal: controller.signal,\n });\n const body = await this.readBoundedBody(response, controller);\n return {\n status: response.status,\n headers: Object.fromEntries(response.headers),\n body,\n };\n } catch (err) {\n throw this.mapNetworkError(err, controller);\n } finally {\n clearTimeout(timeoutTimer);\n }\n }\n\n /**\n * URL 解析 + 协议白名单。其他入参校验交给 fetch 自身。\n */\n private assertValidTargetUrl(raw: unknown): URL {\n if (typeof raw !== 'string' || !raw) {\n throw new HttpException(\n { code: ERROR_CODES.INVALID_REQUEST, message: 'targetUrl is required' },\n HttpStatus.BAD_REQUEST,\n );\n }\n let parsed: URL;\n try {\n parsed = new URL(raw);\n } catch {\n throw new HttpException(\n { code: ERROR_CODES.INVALID_REQUEST, message: 'Invalid targetUrl' },\n HttpStatus.BAD_REQUEST,\n );\n }\n if (\n !ALLOWED_PROTOCOLS.includes(parsed.protocol as (typeof ALLOWED_PROTOCOLS)[number])\n ) {\n throw new HttpException(\n {\n code: ERROR_CODES.INVALID_TARGET_PROTOCOL,\n message: `Unsupported protocol: ${parsed.protocol}`,\n },\n HttpStatus.BAD_REQUEST,\n );\n }\n return parsed;\n }\n\n /**\n * host / content-length 的**单点处理点**:\n * - host:强制覆盖为 targetUrl.host(防上游误识别,无论调用方传什么)\n * - content-length:剥离(fetch 会按真实 body 自算)\n *\n * 其他 hop-by-hop / accept-encoding 等由 controller 已剥离,service 信任入参。\n */\n private buildOutgoingHeaders(\n incoming: Record<string, string> | undefined,\n targetUrl: URL,\n ): Record<string, string> {\n const out: Record<string, string> = {};\n if (incoming) {\n for (const [key, value] of Object.entries(incoming)) {\n const lower = key.toLowerCase();\n if (lower === 'host' || lower === 'content-length') continue;\n out[key] = value;\n }\n }\n out['host'] = targetUrl.host;\n return out;\n }\n\n /**\n * 流式读取响应体并累计字节数,超 `maxResponseBytes` 即 abort + 抛 502。\n */\n private async readBoundedBody(\n response: Response,\n controller: AbortController,\n ): Promise<string> {\n const limit = this.options.maxResponseBytes;\n if (!response.body) return '';\n const reader = response.body.getReader();\n const chunks: Uint8Array[] = [];\n let total = 0;\n try {\n // eslint-disable-next-line no-constant-condition\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n if (value) {\n total += value.byteLength;\n if (total > limit) {\n controller.abort();\n throw new HttpException(\n {\n code: ERROR_CODES.RESPONSE_TOO_LARGE,\n message: `response exceeds maxResponseBytes (${limit})`,\n },\n HttpStatus.BAD_GATEWAY,\n );\n }\n chunks.push(value);\n }\n }\n } finally {\n void reader.cancel().catch(() => undefined);\n }\n return Buffer.concat(chunks).toString('utf-8');\n }\n\n /**\n * 把 fetch 抛错 / 超时统一映射为 HttpException。\n *\n * 1. HttpException → 直接透传(含 INVALID_TARGET_PROTOCOL / RESPONSE_TOO_LARGE)\n * 2. controller.signal.aborted → UPSTREAM_TIMEOUT 504\n * 3. TypeError → INVALID_REQUEST 400(fetch 对配置错统一抛 TypeError)\n * 4. 兜底 → UPSTREAM_UNREACHABLE 502\n */\n private mapNetworkError(err: unknown, controller: AbortController): HttpException {\n if (err instanceof HttpException) return err;\n if (controller.signal.aborted) {\n return new HttpException(\n { code: ERROR_CODES.UPSTREAM_TIMEOUT, message: 'upstream timeout' },\n HttpStatus.GATEWAY_TIMEOUT,\n );\n }\n if (err instanceof TypeError) {\n return new HttpException(\n { code: ERROR_CODES.INVALID_REQUEST, message: 'invalid request configuration' },\n HttpStatus.BAD_REQUEST,\n );\n }\n return new HttpException(\n { code: ERROR_CODES.UPSTREAM_UNREACHABLE, message: 'upstream unreachable' },\n HttpStatus.BAD_GATEWAY,\n );\n }\n}\n","import type { HttpForwarderModuleOptions } from '../types';\nimport {\n DEFAULT_MAX_RESPONSE_BYTES,\n DEFAULT_REQUEST_TIMEOUT_MS,\n} from '../const';\n\n/**\n * forRoot 入参经过默认值合并后的形态。SDK 内部统一使用此类型。\n */\nexport interface HttpForwarderOptionsResolved\n extends Required<HttpForwarderModuleOptions> {}\n\n/**\n * 校验并补默认值。配置错误(非法字段值)在模块装配时抛出,启动期 fail-fast。\n */\nexport function resolveOptions(\n options: HttpForwarderModuleOptions | undefined,\n): HttpForwarderOptionsResolved {\n const opts = options ?? {};\n const requestTimeoutMs = opts.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;\n const maxResponseBytes = opts.maxResponseBytes ?? DEFAULT_MAX_RESPONSE_BYTES;\n\n if (!Number.isInteger(requestTimeoutMs) || requestTimeoutMs <= 0) {\n throw new Error(\n 'HttpForwarderModule.forRoot: `requestTimeoutMs` must be a positive integer',\n );\n }\n if (!Number.isInteger(maxResponseBytes) || maxResponseBytes <= 0) {\n throw new Error(\n 'HttpForwarderModule.forRoot: `maxResponseBytes` must be a positive integer',\n );\n }\n return { requestTimeoutMs, maxResponseBytes };\n}\n"],"mappings":"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","names":["Module","HTTP_FORWARDER_MODULE_OPTIONS","Symbol","CONTROLLER_ROUTE","DEFAULT_REQUEST_TIMEOUT_MS","DEFAULT_MAX_RESPONSE_BYTES","ALLOWED_PROTOCOLS","HEADERS_NOT_FORWARDED_TO_UPSTREAM","Set","HEADERS_NOT_RETURNED_TO_CLIENT","ERROR_CODES","INVALID_REQUEST","INVALID_TARGET_PROTOCOL","UPSTREAM_UNREACHABLE","UPSTREAM_TIMEOUT","RESPONSE_TOO_LARGE","All","Controller","HttpException","HttpStatus","Req","Res","HttpException","HttpStatus","Inject","Injectable","HttpForwarderService","options","forward","payload","targetUrl","assertValidTargetUrl","headers","buildOutgoingHeaders","controller","AbortController","timeoutTimer","setTimeout","abort","requestTimeoutMs","unref","response","fetch","toString","method","body","undefined","signal","readBoundedBody","status","Object","fromEntries","err","mapNetworkError","clearTimeout","raw","HttpException","code","ERROR_CODES","INVALID_REQUEST","message","HttpStatus","BAD_REQUEST","parsed","URL","ALLOWED_PROTOCOLS","includes","protocol","INVALID_TARGET_PROTOCOL","incoming","out","key","value","entries","lower","toLowerCase","host","limit","maxResponseBytes","reader","getReader","chunks","total","done","read","byteLength","RESPONSE_TOO_LARGE","BAD_GATEWAY","push","cancel","catch","Buffer","concat","aborted","UPSTREAM_TIMEOUT","GATEWAY_TIMEOUT","TypeError","UPSTREAM_UNREACHABLE","HttpForwarderController","svc","forward","req","res","targetUrl","query","HttpException","code","ERROR_CODES","INVALID_REQUEST","message","HttpStatus","BAD_REQUEST","out","method","toUpperCase","headers","pickIncomingHeaders","body","serializeIncomingBody","status","key","value","Object","entries","HEADERS_NOT_RETURNED_TO_CLIENT","has","toLowerCase","setHeader","send","incoming","raw","HEADERS_NOT_FORWARDED_TO_UPSTREAM","undefined","Array","isArray","join","String","Buffer","isBuffer","toString","keys","length","JSON","stringify","resolveOptions","options","opts","requestTimeoutMs","DEFAULT_REQUEST_TIMEOUT_MS","maxResponseBytes","DEFAULT_MAX_RESPONSE_BYTES","Number","isInteger","Error","HttpForwarderModule","forRoot","options","resolved","resolveOptions","module","controllers","HttpForwarderController","providers","provide","HTTP_FORWARDER_MODULE_OPTIONS","useValue","HttpForwarderService","exports"]}
|
|
1
|
+
{"version":3,"sources":["../src/http-forwarder.module.ts","../src/const.ts","../src/controllers/http-forwarder.controller.ts","../src/services/http-forwarder.service.ts","../src/services/options.resolved.ts"],"sourcesContent":["import { DynamicModule, Module } from '@nestjs/common';\nimport { HTTP_FORWARDER_MODULE_OPTIONS } from './const';\nimport { HttpForwarderController } from './controllers/http-forwarder.controller';\nimport { HttpForwarderService } from './services/http-forwarder.service';\nimport { resolveOptions } from './services/options.resolved';\nimport type { HttpForwarderModuleOptions } from './types';\n\n/**\n * HttpForwarderModule\n *\n * 自动注册:\n * - `HttpForwarderController` —— 通用内网转发 endpoint `ALL /api/sdk_innerapi/anycross/forward?targetUrl=...`\n * 支持所有 HTTP method,headers / body 自动透传,上游响应直接投射到客户端。\n * - `HttpForwarderService`(可注入到自定义 controller,编排特殊场景)\n *\n * **不内置鉴权**——消费方在 AppModule 层面给路由加 guard(如全局 NeedLoginGuard)。\n *\n * 使用示例:\n * ```ts\n * @Module({\n * imports: [\n * HttpForwarderModule.forRoot({\n * requestTimeoutMs: 30_000,\n * maxResponseBytes: 10 * 1024 * 1024,\n * }),\n * ],\n * })\n * export class AppModule {}\n * ```\n *\n * 前端调用:\n * ```ts\n * axiosForBackend.get('/api/sdk_innerapi/anycross/forward', { params: { targetUrl: 'http://api.corp.com/v1/users' } });\n * axiosForBackend.post('/api/sdk_innerapi/anycross/forward', body, { params: { targetUrl: 'http://api.corp.com/v1/orders' } });\n * ```\n *\n * **注**:实际代理 / 鉴权 / 隧道由部署环境处理(如沙箱内的 mihomo 透明代理 +\n * iptables 拦截)。SDK 不感知 anycross / proxy_group_id / jwtToken。\n */\n@Module({})\nexport class HttpForwarderModule {\n static forRoot(options?: HttpForwarderModuleOptions): DynamicModule {\n const resolved = resolveOptions(options);\n return {\n module: HttpForwarderModule,\n global: true,\n controllers: [HttpForwarderController],\n providers: [\n {\n provide: HTTP_FORWARDER_MODULE_OPTIONS,\n useValue: resolved,\n },\n HttpForwarderService,\n ],\n exports: [HttpForwarderService],\n };\n }\n}\n","/** HttpForwarder module options injection token */\nexport const HTTP_FORWARDER_MODULE_OPTIONS = Symbol('HTTP_FORWARDER_MODULE_OPTIONS');\n\n/** Controller route prefix (硬编码,不可配置以保持调用方式统一) */\nexport const CONTROLLER_ROUTE = 'api/sdk_innerapi/anycross/forward';\n\n/** Default request timeout in milliseconds */\nexport const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;\n\n/** Default maximum response body size in bytes (10 MB) */\nexport const DEFAULT_MAX_RESPONSE_BYTES = 10 * 1024 * 1024;\n\n/** Allowed target URL protocols */\nexport const ALLOWED_PROTOCOLS = ['http:', 'https:'] as const;\n\n/**\n * 入站 headers 中不能透传给上游的 hop-by-hop / transport / encoding 字段。\n * RFC 7230 §6.1 hop-by-hop + accept-encoding(由 fetch / undici 接管)。\n *\n * 注:host / content-length 由 service.buildOutgoingHeaders 在写入 fetch 入参时\n * 单点处理(host 重写为 targetUrl.host;content-length 让 fetch 自算),此处不重复。\n */\nexport const HEADERS_NOT_FORWARDED_TO_UPSTREAM = new Set([\n 'connection',\n 'keep-alive',\n 'transfer-encoding',\n 'upgrade',\n 'proxy-connection',\n 'proxy-authenticate',\n 'proxy-authorization',\n 'te',\n 'trailer',\n 'accept-encoding',\n]);\n\n/**\n * 上游响应 headers 中不应回写到下游 response 的字段。\n * - content-encoding:fetch 已解压,原 encoding 不再适用\n * - content-length:node http 会根据真实 body 重算\n * - transfer-encoding / connection / keep-alive:hop-by-hop\n */\nexport const HEADERS_NOT_RETURNED_TO_CLIENT = new Set([\n 'content-encoding',\n 'content-length',\n 'transfer-encoding',\n 'connection',\n 'keep-alive',\n 'upgrade',\n]);\n\n/** Error codes returned to callers */\nexport const ERROR_CODES = {\n INVALID_REQUEST: 'INVALID_REQUEST',\n INVALID_TARGET_PROTOCOL: 'INVALID_TARGET_PROTOCOL',\n UPSTREAM_UNREACHABLE: 'UPSTREAM_UNREACHABLE',\n UPSTREAM_TIMEOUT: 'UPSTREAM_TIMEOUT',\n RESPONSE_TOO_LARGE: 'RESPONSE_TOO_LARGE',\n} as const;\n","import {\n All,\n Controller,\n HttpException,\n HttpStatus,\n Req,\n Res,\n} from '@nestjs/common';\nimport type { Request, Response } from 'express';\nimport {\n CONTROLLER_ROUTE,\n ERROR_CODES,\n HEADERS_NOT_FORWARDED_TO_UPSTREAM,\n HEADERS_NOT_RETURNED_TO_CLIENT,\n} from '../const';\nimport { HttpForwarderService } from '../services/http-forwarder.service';\n\n/**\n * 通用内网转发 controller。\n *\n * 路径:`/api/sdk_innerapi/anycross/forward`(硬编码,所有消费方一致)。\n *\n * 接受所有 HTTP method(GET/POST/PUT/PATCH/DELETE/HEAD/OPTIONS)。\n * - method:直接取自入站请求自身\n * - targetUrl:取自 query `?targetUrl=...`\n * - headers:透传入站请求 headers(剔除 hop-by-hop / accept-encoding;\n * host / content-length 由 service 单点处理)\n * - body:透传入站请求 body(已经过 express body-parser,按情况序列化为字符串)\n *\n * 响应:上游 status / headers / body **直接投射到 HTTP response**,让前端 axios\n * 看到的就是真实上游响应(而非 wrap 的 JSON)。\n *\n * **SDK 不内置鉴权**——消费方在 AppModule 层面给本路由加 guard(如全局 NeedLoginGuard)。\n */\n@Controller()\nexport class HttpForwarderController {\n constructor(private readonly svc: HttpForwarderService) {}\n\n @All(CONTROLLER_ROUTE)\n async forward(@Req() req: Request, @Res() res: Response): Promise<void> {\n const targetUrl = req.query?.targetUrl;\n if (typeof targetUrl !== 'string' || !targetUrl) {\n throw new HttpException(\n {\n code: ERROR_CODES.INVALID_REQUEST,\n message: 'query parameter `targetUrl` is required',\n },\n HttpStatus.BAD_REQUEST,\n );\n }\n\n const out = await this.svc.forward({\n method: req.method.toUpperCase(),\n targetUrl,\n headers: HttpForwarderController.pickIncomingHeaders(req.headers),\n body: HttpForwarderController.serializeIncomingBody(req),\n });\n\n res.status(out.status);\n for (const [key, value] of Object.entries(out.headers)) {\n if (HEADERS_NOT_RETURNED_TO_CLIENT.has(key.toLowerCase())) continue;\n res.setHeader(key, value);\n }\n res.send(out.body);\n }\n\n /**\n * 从入站请求 headers 选出可透传给上游的字段。剔除 hop-by-hop / accept-encoding。\n * 保留 cookie / authorization / x-* 等业务头。\n * host / content-length 不在此处剥离——由 service.buildOutgoingHeaders 单点处理。\n */\n static pickIncomingHeaders(incoming: Request['headers']): Record<string, string> {\n const out: Record<string, string> = {};\n for (const [key, raw] of Object.entries(incoming)) {\n if (HEADERS_NOT_FORWARDED_TO_UPSTREAM.has(key.toLowerCase())) continue;\n if (raw === undefined) continue;\n out[key] = Array.isArray(raw) ? raw.join(', ') : String(raw);\n }\n return out;\n }\n\n /**\n * 把 express 已解析的 body 序列化回字符串供 service 透传。\n */\n static serializeIncomingBody(req: Request): string | null {\n const method = req.method.toUpperCase();\n if (method === 'GET' || method === 'HEAD') return null;\n const body = (req as { body?: unknown }).body;\n if (body === undefined || body === null) return null;\n if (typeof body === 'string') return body;\n if (Buffer.isBuffer(body)) return body.toString('utf-8');\n if (typeof body === 'object' && Object.keys(body as object).length === 0) {\n return null;\n }\n return JSON.stringify(body);\n }\n}\n","import { HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common';\nimport {\n ALLOWED_PROTOCOLS,\n ERROR_CODES,\n HTTP_FORWARDER_MODULE_OPTIONS,\n} from '../const';\nimport type { ForwardRequestDto, ForwardResponseDto } from '../types';\nimport type { HttpForwarderOptionsResolved } from './options.resolved';\n\n/**\n * 反向 HTTP 转发器:把入站请求\"原样\"转发到目标 URL。\n *\n * 用途:把请求的出口从浏览器变成 node。客户内网访问的代理 / 鉴权 / 隧道由\n * 部署环境(如沙箱内的 mihomo 透明代理 + iptables 拦截)负责,SDK 不感知。\n *\n * 只做 5 件核心事:\n * 1. 协议白名单(仅 http/https,防 SSRF)\n * 2. host 重写 + content-length 剥离(语义正确性,单点处理)\n * 3. 单次请求超时(AbortController,默认 30s)\n * 4. 响应体大小上限(流式累计字节,防 OOM,默认 10MB)\n * 5. 错误码映射(给调用方稳定的 error contract)\n */\n@Injectable()\nexport class HttpForwarderService {\n constructor(\n @Inject(HTTP_FORWARDER_MODULE_OPTIONS)\n private readonly options: HttpForwarderOptionsResolved,\n ) {}\n\n async forward(payload: ForwardRequestDto): Promise<ForwardResponseDto> {\n const targetUrl = this.assertValidTargetUrl(payload?.targetUrl);\n const headers = this.buildOutgoingHeaders(payload.headers, targetUrl);\n const controller = new AbortController();\n const timeoutTimer = setTimeout(\n () => controller.abort(),\n this.options.requestTimeoutMs,\n );\n if (typeof timeoutTimer.unref === 'function') timeoutTimer.unref();\n\n try {\n const response = await fetch(targetUrl.toString(), {\n method: payload.method,\n headers,\n body: payload.body ?? undefined,\n signal: controller.signal,\n });\n const body = await this.readBoundedBody(response, controller);\n return {\n status: response.status,\n headers: Object.fromEntries(response.headers),\n body,\n };\n } catch (err) {\n throw this.mapNetworkError(err, controller);\n } finally {\n clearTimeout(timeoutTimer);\n }\n }\n\n /**\n * URL 解析 + 协议白名单。其他入参校验交给 fetch 自身。\n */\n private assertValidTargetUrl(raw: unknown): URL {\n if (typeof raw !== 'string' || !raw) {\n throw new HttpException(\n { code: ERROR_CODES.INVALID_REQUEST, message: 'targetUrl is required' },\n HttpStatus.BAD_REQUEST,\n );\n }\n let parsed: URL;\n try {\n parsed = new URL(raw);\n } catch {\n throw new HttpException(\n { code: ERROR_CODES.INVALID_REQUEST, message: 'Invalid targetUrl' },\n HttpStatus.BAD_REQUEST,\n );\n }\n if (\n !ALLOWED_PROTOCOLS.includes(parsed.protocol as (typeof ALLOWED_PROTOCOLS)[number])\n ) {\n throw new HttpException(\n {\n code: ERROR_CODES.INVALID_TARGET_PROTOCOL,\n message: `Unsupported protocol: ${parsed.protocol}`,\n },\n HttpStatus.BAD_REQUEST,\n );\n }\n return parsed;\n }\n\n /**\n * host / content-length 的**单点处理点**:\n * - host:强制覆盖为 targetUrl.host(防上游误识别,无论调用方传什么)\n * - content-length:剥离(fetch 会按真实 body 自算)\n *\n * 其他 hop-by-hop / accept-encoding 等由 controller 已剥离,service 信任入参。\n */\n private buildOutgoingHeaders(\n incoming: Record<string, string> | undefined,\n targetUrl: URL,\n ): Record<string, string> {\n const out: Record<string, string> = {};\n if (incoming) {\n for (const [key, value] of Object.entries(incoming)) {\n const lower = key.toLowerCase();\n if (lower === 'host' || lower === 'content-length') continue;\n out[key] = value;\n }\n }\n out['host'] = targetUrl.host;\n return out;\n }\n\n /**\n * 流式读取响应体并累计字节数,超 `maxResponseBytes` 即 abort + 抛 502。\n */\n private async readBoundedBody(\n response: Response,\n controller: AbortController,\n ): Promise<string> {\n const limit = this.options.maxResponseBytes;\n if (!response.body) return '';\n const reader = response.body.getReader();\n const chunks: Uint8Array[] = [];\n let total = 0;\n try {\n // eslint-disable-next-line no-constant-condition\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n if (value) {\n total += value.byteLength;\n if (total > limit) {\n controller.abort();\n throw new HttpException(\n {\n code: ERROR_CODES.RESPONSE_TOO_LARGE,\n message: `response exceeds maxResponseBytes (${limit})`,\n },\n HttpStatus.BAD_GATEWAY,\n );\n }\n chunks.push(value);\n }\n }\n } finally {\n void reader.cancel().catch(() => undefined);\n }\n return Buffer.concat(chunks).toString('utf-8');\n }\n\n /**\n * 把 fetch 抛错 / 超时统一映射为 HttpException。\n *\n * 1. HttpException → 直接透传(含 INVALID_TARGET_PROTOCOL / RESPONSE_TOO_LARGE)\n * 2. controller.signal.aborted → UPSTREAM_TIMEOUT 504\n * 3. TypeError → INVALID_REQUEST 400(fetch 对配置错统一抛 TypeError)\n * 4. 兜底 → UPSTREAM_UNREACHABLE 502\n */\n private mapNetworkError(err: unknown, controller: AbortController): HttpException {\n if (err instanceof HttpException) return err;\n if (controller.signal.aborted) {\n return new HttpException(\n { code: ERROR_CODES.UPSTREAM_TIMEOUT, message: 'upstream timeout' },\n HttpStatus.GATEWAY_TIMEOUT,\n );\n }\n if (err instanceof TypeError) {\n return new HttpException(\n { code: ERROR_CODES.INVALID_REQUEST, message: 'invalid request configuration' },\n HttpStatus.BAD_REQUEST,\n );\n }\n return new HttpException(\n { code: ERROR_CODES.UPSTREAM_UNREACHABLE, message: 'upstream unreachable' },\n HttpStatus.BAD_GATEWAY,\n );\n }\n}\n","import type { HttpForwarderModuleOptions } from '../types';\nimport {\n DEFAULT_MAX_RESPONSE_BYTES,\n DEFAULT_REQUEST_TIMEOUT_MS,\n} from '../const';\n\n/**\n * forRoot 入参经过默认值合并后的形态。SDK 内部统一使用此类型。\n */\nexport interface HttpForwarderOptionsResolved\n extends Required<HttpForwarderModuleOptions> {}\n\n/**\n * 校验并补默认值。配置错误(非法字段值)在模块装配时抛出,启动期 fail-fast。\n */\nexport function resolveOptions(\n options: HttpForwarderModuleOptions | undefined,\n): HttpForwarderOptionsResolved {\n const opts = options ?? {};\n const requestTimeoutMs = opts.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;\n const maxResponseBytes = opts.maxResponseBytes ?? DEFAULT_MAX_RESPONSE_BYTES;\n\n if (!Number.isInteger(requestTimeoutMs) || requestTimeoutMs <= 0) {\n throw new Error(\n 'HttpForwarderModule.forRoot: `requestTimeoutMs` must be a positive integer',\n );\n }\n if (!Number.isInteger(maxResponseBytes) || maxResponseBytes <= 0) {\n throw new Error(\n 'HttpForwarderModule.forRoot: `maxResponseBytes` must be a positive integer',\n );\n }\n return { requestTimeoutMs, maxResponseBytes };\n}\n"],"mappings":"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","names":["Module","HTTP_FORWARDER_MODULE_OPTIONS","Symbol","CONTROLLER_ROUTE","DEFAULT_REQUEST_TIMEOUT_MS","DEFAULT_MAX_RESPONSE_BYTES","ALLOWED_PROTOCOLS","HEADERS_NOT_FORWARDED_TO_UPSTREAM","Set","HEADERS_NOT_RETURNED_TO_CLIENT","ERROR_CODES","INVALID_REQUEST","INVALID_TARGET_PROTOCOL","UPSTREAM_UNREACHABLE","UPSTREAM_TIMEOUT","RESPONSE_TOO_LARGE","All","Controller","HttpException","HttpStatus","Req","Res","HttpException","HttpStatus","Inject","Injectable","HttpForwarderService","options","forward","payload","targetUrl","assertValidTargetUrl","headers","buildOutgoingHeaders","controller","AbortController","timeoutTimer","setTimeout","abort","requestTimeoutMs","unref","response","fetch","toString","method","body","undefined","signal","readBoundedBody","status","Object","fromEntries","err","mapNetworkError","clearTimeout","raw","HttpException","code","ERROR_CODES","INVALID_REQUEST","message","HttpStatus","BAD_REQUEST","parsed","URL","ALLOWED_PROTOCOLS","includes","protocol","INVALID_TARGET_PROTOCOL","incoming","out","key","value","entries","lower","toLowerCase","host","limit","maxResponseBytes","reader","getReader","chunks","total","done","read","byteLength","RESPONSE_TOO_LARGE","BAD_GATEWAY","push","cancel","catch","Buffer","concat","aborted","UPSTREAM_TIMEOUT","GATEWAY_TIMEOUT","TypeError","UPSTREAM_UNREACHABLE","HttpForwarderController","svc","forward","req","res","targetUrl","query","HttpException","code","ERROR_CODES","INVALID_REQUEST","message","HttpStatus","BAD_REQUEST","out","method","toUpperCase","headers","pickIncomingHeaders","body","serializeIncomingBody","status","key","value","Object","entries","HEADERS_NOT_RETURNED_TO_CLIENT","has","toLowerCase","setHeader","send","incoming","raw","HEADERS_NOT_FORWARDED_TO_UPSTREAM","undefined","Array","isArray","join","String","Buffer","isBuffer","toString","keys","length","JSON","stringify","resolveOptions","options","opts","requestTimeoutMs","DEFAULT_REQUEST_TIMEOUT_MS","maxResponseBytes","DEFAULT_MAX_RESPONSE_BYTES","Number","isInteger","Error","HttpForwarderModule","forRoot","options","resolved","resolveOptions","module","global","controllers","HttpForwarderController","providers","provide","HTTP_FORWARDER_MODULE_OPTIONS","useValue","HttpForwarderService","exports"]}
|
package/package.json
CHANGED