10. HTTP Engine#
HTTP Engine 是 Link1 的七层处理模块,用于 HTTP/HTTPS 调试、兼容修补、请求/响应改写、Mock、Capture 和 Replay。普通代理不需要开启它;只有当你明确需要观察或修改 HTTP 内容时再使用。
工作位置#
入站连接
-> 路由/出站选择
-> HTTP Engine 判断是否接管 HTTP 流
-> URL/Header/Body/JSON/JQ/Script/Mock/Route 规则
-> Capture 记录
-> 上游或客户端
HTTPS 内容默认是加密的。要改写 HTTPS 请求/响应,需要 MITM:
- 生成或导入 CA。
- 客户端信任该 CA。
http-engine.mitm.hosts命中目标域名。
- 连接经过 Link1。
最小启用#
http-engine:
enabled: true
defaults:
body-max-size: 1 MiB
on-error: fail-open
字段影响:
| 字段 | 含义 | 实际影响 |
|---|---|---|
enabled | 开启 HTTP Engine | 关闭时所有 HTTP Engine 规则不运行 |
defaults | 默认限制和错误策略 | 控制 body/JQ/script 默认行为 |
mitm | HTTPS MITM | 解密指定 HTTPS 域名 |
force-http-engine | 强制进入 HTTP Engine 的 host/pattern | 对特定流量启用 L7 处理 |
downstream-h3-proxy | 下游 HTTP/3 proxy 支持 | 影响客户端到 Link1 的 H3 行为 |
upstream-h3 | 上游 HTTP/3 策略 | off、hinted、aggressive |
capture | 捕获流量 | 记录请求/响应元数据和 body |
scripts | QuickJS 脚本源 | 供 script 规则引用 |
rules | 规则集合 | URL/Header/Body/JSON/JQ/Script/Mock/Route |
defaults#
http-engine:
defaults:
body-max-size: 1 MiB
jq-timeout: 100ms
script-timeout: 50ms
script-memory-limit: 8 MiB
on-error: fail-open
| 字段 | 影响 |
|---|---|
body-max-size | 默认允许读取/改写的 body 大小 |
jq-timeout | JQ 默认执行超时 |
script-timeout | QuickJS 默认执行超时 |
script-memory-limit | QuickJS 默认内存限制 |
on-error | fail-open 出错放行;fail-closed 出错阻断 |
MITM#
使用 App 自动管理的 CA#
managed: ca-managed 表示使用 Link1 App 创建和保存的本地 CA;普通用户不需要手写 PEM,只需要在 App 中生成/信任 CA,再在配置中引用这个 CA ID。
http-engine:
enabled: true
mitm:
enabled: true
ca-cert:
managed: ca-managed
ca-key:
managed: ca-managed
hosts:
- service.example.com
- '*.debug.example.com'
h2: true
leaf-cache-max-entries: 1024
使用 PEM 文件#
http-engine:
enabled: true
mitm:
enabled: true
ca-cert:
file: ./certs/ca.pem
ca-key:
file: ./certs/ca.key
hosts:
- service.example.com
使用内联 PEM#
ca-cert:
inline-pem: |
-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----
ca-key:
inline-pem: |
-----BEGIN PRIVATE KEY-----
...
-----END PRIVATE KEY-----
字段影响:
| 字段 | 影响 |
|---|---|
enabled | 开启 MITM |
ca-cert / ca-key | CA 证书和私钥来源,必须能配对 |
hosts | 只对命中的主机 MITM |
h2 | 是否支持 HTTP/2 leaf |
leaf-cache-max-entries | 叶子证书缓存数量 |
安全提示:只 MITM 你需要调试的域名,不要对全网开启。
Capture#
http-engine:
enabled: true
capture:
enabled: true
max-flows: 1000
body-preview-bytes: 4 KiB
store-full-body: true
full-body-max-bytes: 1 MiB
spool-dir: ./http-capture
字段影响:
| 字段 | 影响 |
|---|---|
enabled | 开启捕获 |
max-flows | 保留最近多少条 flow |
body-preview-bytes | 列表里展示的 body 预览大小 |
store-full-body | 是否保存完整 body |
full-body-max-bytes | 完整 body 最大保存大小 |
spool-dir | body 落盘目录 |
Link1 App 展示:
- flow 列表:最近捕获的请求/响应摘要。
- flow 详情:方法、host、path、状态码、命中规则、耗时。
- body 视图:可查看原始请求、改写后请求、原始响应、改写后响应。
- 落盘提示:当 body 超过预览大小或启用完整保存时,App 会显示保存位置或截断状态。
这些内容只在 Capture 开启且流量命中 HTTP Engine 时出现。
Match 条件#
所有 HTTP Engine 规则都有 match。字段:
| 字段 | 含义 |
|---|---|
view | 匹配视图/阶段 |
url / url-regex | 完整 URL 精确/正则 |
scheme | http、https |
host | Host 精确/模式匹配 |
path / path-regex | 路径匹配 |
query / query-regex | Query 匹配 |
method | HTTP 方法 |
content-type | Content-Type |
user-agent / user-agent-regex | User-Agent |
header / header-regex | 请求/响应头 |
cookie / cookie-regex | Cookie |
protocol | HTTP 协议版本/类型 |
entry-point | 入站入口 |
示例:
match:
scheme: [https]
host: service.example.com
path-regex: '^/old/'
method: [GET, POST]
header:
X-Client: app
URL rewrite#
http-engine:
rules:
url-rewrite:
- name: rewrite-service
match:
host: old.example.com
path: /old/items
action:
type: rewrite-url
replacement: https://new.example.com/new/items
Action 类型:
type | 影响 |
|---|---|
rewrite-url | 改写请求 URL 后继续转发 |
redirect | 直接返回重定向,配合 status/location |
reject | 拒绝请求 |
reject-200 | 返回 200 空响应 |
reject-img | 返回图片占位 |
reject-dict | 返回空对象 |
reject-array | 返回空数组 |
Header rewrite#
http-engine:
rules:
header-rewrite:
- name: add-debug-header
direction: request
match:
host: service.example.com
operations:
- op: set
key: X-Debug
value: '1'
- op: del
key: X-Remove-Me
方向:request 或 response。
操作:
op | 影响 |
|---|---|
set | 设置为指定值 |
replace | 替换现有值 |
add | 追加 header 值 |
del / delete | 删除 header |
replace-regex | 用正则替换 header 值 |
Body rewrite#
http-engine:
rules:
body-rewrite:
- name: patch-text
direction: response
match:
host: service.example.com
content-type: [text/plain]
require-body: true
max-size: 512 KiB
on-error: fail-open
operations:
- op: replace
from: old
to: new
- op: replace-regex
pattern: 'token=[^&]+'
replacement: 'token=redacted'
字段影响:
| 字段 | 影响 |
|---|---|
require-body | 无 body 时是否视为不匹配/错误 |
max-size | 超过大小不处理 |
on-error | 出错后放行或阻断 |
operations | 顺序执行文本替换 |
JSON transform#
http-engine:
rules:
json-transform:
- name: normalize-json
direction: response
match:
host: service.example.com
content-type: [application/json]
require-body: true
max-size: 1 MiB
when:
- path: $.ok
eq: true
ops:
- op: set
path: $.source
value: link1
- op: set-if-missing
path: $.items
value: []
- op: del
path: $.debug
规则字段:
| 字段 | 影响 |
|---|---|
when | JSON 谓词,全部满足才执行 |
ops | JSON 操作列表 |
when 支持:path、eq、neq、in、not-in、exists。
操作支持:
op | 影响 |
|---|---|
set | 设置路径值 |
set-if-missing | 路径不存在时设置 |
del | 删除路径 |
append | 向数组追加 |
merge | 合并对象 |
replace-if-eq | 当前值等于 equals 时替换 |
filter-array | 过滤数组元素 |
update-array-items | 更新数组元素 |
数组过滤条件 where 支持 field、eq、neq、all。
JQ#
http-engine:
rules:
jq:
- name: jq-response
direction: response
match:
host: service.example.com
content-type: [application/json]
require-body: true
max-size: 1 MiB
timeout: 100ms
on-error: fail-open
expression: '.items |= map(. + {source: $source})'
variables:
source: link1
字段影响:
| 字段 | 影响 |
|---|---|
expression | JQ 表达式 |
variables | 传给 JQ 的变量 |
timeout | 单次执行超时 |
max-size | body 大小上限 |
on-error | 出错策略 |
QuickJS script#
QuickJS script 适合处理“内置规则表达不出来,但又不值得写独立服务”的 HTTP 改写逻辑,例如:按响应 JSON 动态补字段、把某些请求直接 Mock 掉、根据 Header 临时标记路由。它运行在 Link1 内核里,不是浏览器插件,也不是 Node.js 环境。
脚本源#
先在 http-engine.scripts 中声明脚本源,再在规则里引用它。脚本名必须唯一。
http-engine:
scripts:
- name: patcher
source:
file: ./http-engine/scripts/patcher.js
脚本也可以内联,适合很短的规则:
http-engine:
scripts:
- name: inline-script
source:
inline: |
function onResponse(payload) {
return {
headers: {
"X-Link1": "1"
}
};
}
注意:脚本函数应直接声明为全局函数 function onRequest(...) / function onResponse(...)。不要写 export function,也不要假设有模块系统。
规则引用脚本#
http-engine:
rules:
script:
- name: run-patcher
direction: response
match:
host: service.example.com
engine: quickjs
script: patcher
require-body: true
max-size: 1 MiB
timeout: 50ms
memory-limit: 8 MiB
on-error: fail-open
arguments:
env: prod
字段影响:
| 字段 | 影响 |
|---|---|
direction | request 调用 onRequest(payload);response 调用 onResponse(payload)。 |
engine | 使用 quickjs;不写时也按 QuickJS 处理。 |
script | 引用 http-engine.scripts[].name。引用不存在会在配置编译阶段报错。 |
require-body | 要求脚本能读取 body。无 body、body 仍是流式、或超过 max-size 时按 on-error 处理。 |
max-size | 脚本最多读取多少 body。超过后,require-body=false 时脚本仍运行但看不到 body;require-body=true 时触发错误策略。 |
binary-body-mode | 把可读取的 body 作为 Uint8Array 放入 bodyBytes,适合图片、压缩包、protobuf 等二进制内容。 |
arguments | 传给脚本的只读字符串参数,适合把环境名、开关、固定值放进 YAML,而不是写死在 JS 里。 |
timeout | 单次脚本执行超时。循环或复杂计算超过时间会中断。 |
memory-limit | 单次脚本 VM 的内存上限。body 越大、JSON 越深,需要的内存越多。 |
on-error | fail-open 跳过本规则继续放行;fail-closed 把错误暴露给连接,适合调试和强策略。 |
Hook 与 payload 上下文#
direction 决定调用哪个 Hook:
function onRequest(payload) {
// 只处理请求阶段
}
function onResponse(payload) {
// 能看到请求与响应
}
payload 是 Link1 为当前 HTTP flow 构造的对象。脚本里修改 payload 本身不会自动生效;应该返回一个“改写结果 envelope”。不要为了“原样返回”而 return payload,否则在响应阶段可能把当前响应当成合成响应重新写一遍。
| 路径 | 方向 | 类型 | 可写入返回值吗 | 含义 |
|---|---|---|---|---|
payload.request.url | request/response | string | 不能直接改,需返回 body/header/route 等 envelope | 当前请求完整 URL。 |
payload.request.method | request/response | string | 不能直接改 | HTTP 方法,例如 GET、POST。 |
payload.request.headers | request/response | object | 返回 headers 才会生效 | 请求 Header。单值 header 是字符串,多值 header 是字符串数组。 |
payload.request.body | request/response | any | 返回 body 才会生效 | 当 body 能读取且能解析成 JSON 时出现。 |
payload.request.bodyText | request/response | string | 不能作为输出字段 | 当 body 能读取但不是 JSON 时出现。 |
payload.request.bodyBytes | request/response | Uint8Array | 返回 bodyBytes 才会生效 | binary-body-mode: true 且 body 可读取时出现。 |
payload.response.status | response | number | 响应规则可返回 status | HTTP 状态码。 |
payload.response.headers | response | object | 返回 headers 才会生效 | 响应 Header。 |
payload.response.body | response | any | 返回 body 才会生效 | 响应 body 解析成 JSON 后的值。 |
payload.response.bodyText | response | string | 不能作为输出字段 | 响应 body 不是 JSON 时的文本。 |
payload.response.bodyBytes | response | Uint8Array | 返回 bodyBytes 才会生效 | 二进制模式下的响应 body。 |
payload.arguments | request/response | object | 只读 | 来自规则 arguments 的字符串键值。 |
Body 字段不会总是出现:
- 没有 body 时,不会出现
body、bodyText或bodyBytes。
require-body: false且 body 是流式传输时,Link1 不会为了脚本强行读完流,脚本看不到 body。
- body 超过
max-size时,require-body: false会让脚本继续运行但不带 body;require-body: true会触发错误策略。
- 非二进制模式下,JSON body 进入
body;非 JSON 文本进入bodyText。
- 二进制模式下,body 进入
bodyBytes,脚本里看到的是Uint8Array。
返回值 envelope#
脚本的返回值决定是否改写当前 flow。
| 返回值 | 影响 |
|---|---|
undefined / null | 不做任何修改,继续处理后续规则。 |
{headers: {"X-A": "1"}} | 设置当前请求或响应 Header;方向由规则 direction 决定。 |
{status: 299} | 只对响应规则有意义,修改响应状态码。 |
{body: {...}} | 把当前请求或响应 body 改成 JSON 序列化后的内容。 |
{bodyBytes: [1,2,3]} 或 {bodyBytes: Uint8Array} | 把当前请求或响应 body 改成精确字节;适合文本原样输出或二进制内容。 |
{response: {status, headers, body/bodyBytes}} | 在请求阶段直接返回合成响应,不再访问上游;在响应阶段整体替换当前响应。 |
{route: {outbound: "PROXY", freeze: true}} | 只在请求阶段可用,把当前 HTTP flow 标记到指定出站。freeze=false 表示只是候选,后续 route 规则仍可覆盖。 |
{outbound: "PROXY"} | route 的简写,等价于冻结到指定出站。 |
输出 Header 建议写字符串值。输入 Header 可能是字符串数组,但输出数组会被当成普通值格式化,不适合表达多值 Header。
示例:按环境添加 Header#
http-engine:
scripts:
- name: add-env-header
source:
inline: |
function onRequest(payload) {
return {
headers: {
"X-Link1-Env": payload.arguments.env || "dev"
}
};
}
rules:
script:
- name: add-env-header
direction: request
match:
host: api.example.com
script: add-env-header
arguments:
env: prod
效果:发往 api.example.com 的请求会被加上 X-Link1-Env: prod。这个例子不需要读取 body,因此不要写 require-body: true。
示例:修改 JSON 响应#
http-engine:
scripts:
- name: normalize-profile
source:
inline: |
function onResponse(payload) {
const body = payload.response.body;
if (!body || typeof body !== "object") return;
body.source = "link1";
if (!Array.isArray(body.items)) body.items = [];
delete body.debug;
return {
headers: {
"Content-Type": "application/json"
},
body: body
};
}
rules:
script:
- name: normalize-profile
direction: response
match:
host: api.example.com
path: /profile
content-type: [application/json]
script: normalize-profile
require-body: true
max-size: 1 MiB
timeout: 50ms
on-error: fail-open
效果:只有响应 body 能作为 JSON 读取时才修改。返回 body 后,Link1 会把它序列化成 JSON;如果你要保持原始缩进或非 JSON 文本,请使用 bodyBytes。
示例:返回合成响应#
http-engine:
scripts:
- name: mock-maintenance
source:
inline: |
function onRequest(payload) {
if (payload.arguments.maintenance !== "on") return;
return {
response: {
status: 503,
headers: {
"Content-Type": "application/json",
"Retry-After": "60"
},
body: {
ok: false,
message: "maintenance"
}
}
};
}
rules:
script:
- name: maintenance-page
direction: request
match:
host: api.example.com
path: /v1/order
script: mock-maintenance
arguments:
maintenance: "on"
on-error: fail-closed
效果:命中后 Link1 直接给客户端返回 503,不再连接 api.example.com。这种规则适合测试和临时降级,不适合长期替代服务端逻辑。
示例:二进制 body#
如果要精确输出文本字节或处理图片、压缩包、protobuf,请打开 binary-body-mode。QuickJS 环境没有 TextEncoder,如果只是调试 ASCII 文本,可以用一个小函数生成字节数组:
http-engine:
scripts:
- name: binary-debug
source:
inline: |
function ascii(text) {
const out = new Uint8Array(text.length);
for (let i = 0; i < text.length; i++) out[i] = text.charCodeAt(i) & 255;
return out;
}
function onResponse(payload) {
const input = payload.response.bodyBytes;
if (!(input instanceof Uint8Array)) return;
return {
headers: {
"Content-Type": "text/plain"
},
bodyBytes: ascii("bytes=" + input.length)
};
}
rules:
script:
- name: binary-debug
direction: response
match:
host: download.example.com
script: binary-debug
binary-body-mode: true
require-body: true
max-size: 64 KiB
效果:响应 body 被替换为 bytes=<长度> 这段原始文本字节。真实处理二进制时,应尽量缩小 match 和 max-size,避免把大文件读进脚本。
调试方法#
QuickJS 脚本调试建议按这个顺序做:
- 先缩小 match:只匹配一个测试域名、路径和方法,避免影响真实业务。
- 先不用 body:先确认 Header 改写生效,再打开
require-body。
- 调试时用
fail-closed:脚本抛错时,连接会失败,App 日志或连接错误里能看到规则名和错误信息;确认无误后再改回fail-open。
- 用
throw new Error(...)做断言:例如if (!payload.response.body) throw new Error("missing json body")。
- 用临时 Header 输出状态:例如返回
headers: {"X-Debug-Status": String(payload.response.status)},再在 Capture 或浏览器开发者工具里查看。
- 用合成响应输出调试信息:请求阶段可以临时返回
{response: {status: 200, body: {...}}},确认 payload 中到底有什么。
- 打开 Capture 看前后差异:比较原始请求/响应与改写后请求/响应,确认是 match 没命中、body 没读到,还是返回 envelope 写错。
QuickJS 环境不提供 console.log。如果脚本里写 console.log(...),会得到 ReferenceError: 'console' is not defined。也不要依赖 fetch、文件系统、DOM、浏览器对象、Node.js 模块、setTimeout、TextEncoder 或异步事件循环。
运行约束与安全边界#
- 每次脚本执行都会使用独立 VM;不要依赖全局变量在不同请求之间保存状态。
- 脚本是同步执行的;不要写等待网络、等待定时器或异步回调的逻辑。
- body 进入脚本前需要被读取到内存;大 body 应使用
max-size限制。
memory-limit限制的是脚本 VM 内存,不包括整个 Link1 进程的所有开销。
timeout不是性能优化工具,而是兜底保护;脚本本身仍应保持简单。
- QuickJS 规则运行在代理层,能看到和改写 HTTP 内容。不要在脚本和 Capture 中长期保留密码、Token、Cookie、私钥或企业数据。
Mock#
http-engine:
rules:
mock:
- name: mock-health
match:
host: service.example.com
path: /health
response:
status: 200
headers:
Content-Type: application/json
body: '{"ok":true}'
响应 body 来源:
| 字段 | 影响 |
|---|---|
body | 直接内联文本 |
body-file | 从文件读取 |
body-base64 | base64 body |
tiny-gif | 返回 tiny gif |
Route 规则#
http-engine:
rules:
route:
- name: mark-service
match:
host: service.example.com
outbound: PROXY
HTTP Engine route 用于在七层处理阶段给当前 HTTP flow 标记业务出口。例如,你可以根据 HTTP path、Header 或 JSON/JQ/QuickJS 的结果,把某个请求标记到 PROXY。它只影响已经进入 HTTP Engine 的 HTTP flow;全局、通用的出站分流仍建议写在顶层 rules,这样 DNS、TUN、非 HTTP 流量和普通连接都能得到一致处理。
Replay#
Replay 用于对捕获的 HTTP flow 做重放测试。典型流程:
- 在 App 的 HTTP 捕获列表中选择一条 flow。
- 查看原始请求和改写后请求,确认不会泄露敏感内容。
- 选择重放目标:使用原始目标,或临时改成测试目标。
- 执行重放,观察状态码、响应头、响应 body 和命中的 HTTP Engine 规则。
Replay 适合验证 URL rewrite、Header rewrite、JSON/JQ/QuickJS 改写和 Mock。它可能再次访问真实服务,包含登录态或业务数据的请求不要随意重放。
使用建议#
- 只对需要调试的 host 开 MITM。
- 给 body/JQ/script 设置合理
max-size和timeout。
- 默认
on-error: fail-open更适合生产代理,避免改写规则故障导致全站不可用。
fail-closed适合强策略或测试场景。
- Capture 全量 body 会占磁盘,生产环境要限制
max-flows和full-body-max-bytes。