perf(cache): 边缘缓存对齐 Cloudflare 默认模型

This commit is contained in:
ryan
2026-07-23 23:39:15 +08:00
parent 5b1e27d0a3
commit f94767fbc7
14 changed files with 465 additions and 94 deletions
+24
View File
@@ -111,3 +111,27 @@ cross-build:
. .
@echo "==> Done. Binaries written to ./bin/" @echo "==> Done. Binaries written to ./bin/"
@ls -lh bin/ @ls -lh bin/
dev-f:
@echo "==> Starting frontend development server..."
cd frontend && pnpm dev
dev-b:
@echo "==> Starting backend development server..."
go run main.go all
dev:
@echo "==> Starting frontend and backend development servers in parallel..."
@PIDS=""; \
STATUS=0; \
( cd frontend && pnpm dev 2>&1 | sed 's/^/[frontend] /' ) & PIDS="$$PIDS $$!"; \
( go run main.go all 2>&1 | sed 's/^/[backend] /' ) & PIDS="$$PIDS $$!"; \
for PID in $$PIDS; do \
wait $$PID || STATUS=1; \
done; \
if [ $$STATUS -eq 0 ]; then \
echo "==> All development servers exited successfully."; \
else \
echo "==> Development servers exited with errors." >&2; \
exit 1; \
fi
+1
View File
@@ -28,6 +28,7 @@ sidebar: false
### 改进 ### 改进
- 边缘缓存对齐 Cloudflare 默认模型:不再因请求会话 Cookie、Authorization 或客户端 Cache-Control 一律跳过缓存;响应带 Set-Cookie 时不写入边缘;无源站缓存头时按状态码使用默认 Edge TTL;标准静态扩展名默认不再包含 JSON。生效需重新发布节点配置。
- IP 组自动规则中的 `StatusCount` / `StatusRatio` 支持状态码类写法(如 `"2xx"`、`"4xx"`、`"5xx"`),便于按整类错误率匹配。 - IP 组自动规则中的 `StatusCount` / `StatusRatio` 支持状态码类写法(如 `"2xx"`、`"4xx"`、`"5xx"`),便于按整类错误率匹配。
- IP 组同步间隔下限由 5 分钟调整为 1 分钟,便于更频繁同步自动/订阅名单。 - IP 组同步间隔下限由 5 分钟调整为 1 分钟,便于更频繁同步自动/订阅名单。
- 自动 IP 组回看窗口字段由 `lookback_minutes` 调整为 `lookback`,支持 `60m`、`1h` 等时长写法,并移除最小 5 分钟限制(兼容旧字段)。 - 自动 IP 组回看窗口字段由 `lookback_minutes` 调整为 `lookback`,支持 `60m`、`1h` 等时长写法,并移除最小 5 分钟限制(兼容旧字段)。
+167 -67
View File
@@ -1,6 +1,6 @@
# 边缘缓存策略设计(对标 Cloudflare 默认可缓存范围) # 边缘缓存策略设计
你会学到:OpenFlare 边缘 `proxy_cache` 的产品边界、默认可缓存范围如何对齐 Cloudflare「静态资源默认可缓存」、策略枚举与渲染规则、兼容迁移,以及本阶段明确不做的能力。 你会学到:OpenFlare 边缘 `proxy_cache` 如何在「该缓存」与「不该缓存」之间对齐 Cloudflare 默认闭环:请求 eligible(扩展名/策略)× 响应可共享缓存(源站 `Cache-Control` / `Expires` / `Set-Cookie`),以及与过往过严请求旁路的差异。
本设计是 [系统架构](./architecture.md) 中「基础缓存」的产品化专章;访问日志中的缓存结果见 [观测数据模型 §3.5.1](./observability-data-model.md)。 本设计是 [系统架构](./architecture.md) 中「基础缓存」的产品化专章;访问日志中的缓存结果见 [观测数据模型 §3.5.1](./observability-data-model.md)。
@@ -8,35 +8,61 @@
## 1. 目标与非目标 ## 1. 目标与非目标
### 1.1 目标(第一期) ### 1.1 目标
* **开箱接近 CF 默认**:路由开启缓存后,**默认只缓存静态扩展名**,不默认缓存 HTML/无扩展名动态路径。 * **开箱接近 CF 默认**:路由开启缓存后,**默认只缓存静态扩展名**,不默认缓存 HTML;**不因请求会话 Cookie / Authorization / 客户端 Cache-Control 一律 BYPASS**。
* **行为可解释**:与现有安全旁路(非 GET、Authorization、会话 Cookie、请求 `Cache-Control`)叠加,不削弱安全。 * **该缓存的能命中**:带登录 Cookie 的用户访问 `/_app/**/*.js` 等静态资源可出现 `MISS` → `HIT`。
* **不该缓存的仍挡住**:策略不 eligible(等价 CF `DYNAMIC`);源站 `private` / `no-store`;响应带 **`Set-Cookie` 不入库**(对齐 CF OCC 默认);`all` 为高级选项并文档警示。
* **无源站 freshness 时有默认 Edge TTL**:对齐 CF 按状态码的默认 TTL(见 §3.5)。
* **可观测一致**:继续依赖 `$upstream_cache_status` → `cache_status` 明细三态。 * **可观测一致**:继续依赖 `$upstream_cache_status` → `cache_status` 明细三态。
* **兼容存量**:旧路由 `cache_policy=url`(近似「过旁路即可缓存」)迁移为显式策略 `all`,行为不变。 * **兼容存量**:旧路由 `cache_policy=url` 映射为 `all`;策略枚举与迁移规则保持 [§5](#5-兼容与迁移)。
### 1.2 非目标(后续迭代) ### 1.2 非目标(后续迭代)
* Cache Rules 表达式引擎 * Cache Rules 表达式引擎
* Edge TTL / `proxy_cache_valid` / 忽略源站 `Cache-Control` * 忽略源站 `Cache-Control` 的强制 Edge TTL(CF Cache Rules「Ignore cache-control」)
* 可配置 Cookie 旁路列表、Query 忽略列表
* Purge(按 URL/前缀/全站) * Purge(按 URL/前缀/全站)
* 浏览器 TTL 改写、客户端 `CF-Cache-Status` 响应头 * 浏览器 TTL 改写、客户端 `CF-Cache-Status` 响应头
* 完整 RFC 条件:`Authorization` 仅当响应含 `public`/`s-maxage`/`must-revalidate` 才缓存(需 Lua;本期删除请求侧一律旁路,依赖策略 + 源站头)
* HEAD 转 GET 再缓存
* 命中率看板 * 命中率看板
--- ---
## 2. 现状摘要 ## 2. Cloudflare 判定闭环(对齐基准)
| 层 | 现状 | CF 默认是 **两段决策**,**不是**「请求带 Cookie 就不缓存」。
### 2.1 阶段 A — 请求时 Eligible
| 条件 | CF 结果 |
| --- | --- | | --- | --- |
| 全局 | `proxy_cache_path` / key / lock / stale(Performance 部分字段) | | 非 GET | 默认不缓存 |
| 路由 | `cache_enabled` + `cache_policy`:`url` \| `suffix` \| `path_prefix` \| `path_exact` | | 扩展名不在默认可缓存表,且无 Rules 强制 Eligible | **`DYNAMIC`**(不查缓存) |
| 旁路 | 渲染器硬编码:非 GET、Authorization、会话 Cookie、请求 Cache-Control | | 扩展名在默认表,或 Rules Eligible | 继续阶段 B |
| TTL | **无** `proxy_cache_valid`;存多久主要看源站头 + `inactive` | | **请求 Cookie** | **默认不影响** |
| 观测 | 已上报 `cache_status`,UI 三态:命中 / 回源 / 未缓存 | | Cache Rules Bypass | `DYNAMIC` |
问题:默认策略 `url` 对「过旁路的 GET」范围过宽,与 CF「默认主要缓存静态扩展名、默认不缓存 HTML」不一致。 CF 默认可缓存扩展名按 **扩展名** 而非 MIME;**默认不缓存 HTML / JSON**。
### 2.2 阶段 B — 响应是否可入库(OCC on,Free/Pro/Biz 默认)
| 条件 | 结果 |
| --- | --- |
| `Cache-Control: no-store` / `private` | 不入库 |
| `public` + `max-age>0`,或未来 `Expires` | 可缓存 |
| 无 Cache-Control / Expires | 按状态码 **默认 Edge TTL** 仍可缓存(如 200 → 120m) |
| 响应 **`Set-Cookie`**(默认缓存级别 + OCC) | **不入库**,状态倾向 **BYPASS** |
| 请求 `Authorization` | 仅当响应另有 `public` / `s-maxage` / `must-revalidate` 才可缓存(完整条件本期用 Nginx 简化,见 §3.4) |
### 2.3 状态语义(对照观测)
| CF | 含义 | OpenFlare `cache_status` |
| --- | --- | --- |
| HIT / STALE / UPDATING / REVALIDATED | 命中类 | 同名或等价 |
| MISS / EXPIRED | 回源取内容 | 同名 |
| BYPASS | 请求时 eligible,响应不可缓存 | `BYPASS` → UI「未缓存」 |
| DYNAMIC | 请求时不 eligible | 策略 skip 多为 `BYPASS` 或空 → UI「未缓存」 |
--- ---
@@ -49,24 +75,24 @@
两者均开启时才进入缓存逻辑。 两者均开启时才进入缓存逻辑。
### 3.2 策略枚举(第一期) ### 3.2 策略枚举
| `cache_policy` | 含义 | 新建默认 | 旧值兼容 | | `cache_policy` | 含义 | 新建默认 | 旧值兼容 |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| **`static`** | 仅 URI 匹配**标准静态扩展名**(内置表)才允许缓存 | **是** | — | | **`static`** | 仅 URI 匹配**标准静态扩展名**才 eligible | **是** | — |
| **`all`** | 过安全旁路后,不限制路径/扩展名(等同今日 `url`) | 否 | 存量 `url` → `all` | | **`all`** | 过方法旁路后,不限制路径/扩展名(高级,风险类似 CF Cache Everything) | 否 | 存量 `url` → `all` |
| **`suffix`** | 自定义扩展名列表(`cache_rules`) | 否 | 保持 | | **`suffix`** | 自定义扩展名列表(`cache_rules`) | 否 | 保持 |
| **`path_prefix`** | 自定义路径前缀 | 否 | 保持 | | **`path_prefix`** | 自定义路径前缀 | 否 | 保持 |
| **`path_exact`** | 自定义精确路径 | 否 | 保持 | | **`path_exact`** | 自定义精确路径 | 否 | 保持 |
> 渲染层:读到历史值 `url` 时按 `all` 处理,避免未迁移数据行为突变;API 校验与 UI 只暴露上表枚举(写入时可将 `url` 规范为 `all`)。 渲染层:历史值 `url` 按 `all` 处理;API/UI 只暴露上表枚举。
### 3.3 标准静态扩展名(内置,V1 硬编码) ### 3.3 标准静态扩展名(内置)
对齐 Cloudflare 常见「默认可缓存静态」集合,**默认不包含** `html` / `htm`: 对齐 CF 默认「不缓存 HTML/JSON」;保留现代前端常用增强项:
```text ```text
css js mjs map json css js mjs map
ico cur gif jpg jpeg png webp avif svg svgz ico cur gif jpg jpeg png webp avif svg svgz
ttf otf woff woff2 eot ttf otf woff woff2 eot
mp3 mp4 webm ogg flac mp3 mp4 webm ogg flac
@@ -74,26 +100,68 @@ wasm pdf
zip 7z gz tar zip 7z gz tar
``` ```
* 匹配对象:`$uri` 的扩展名(大小写不敏感),实现上与现有 `suffix` 策略相同: * **不含** `html` / `htm` / **`json`**(对齐 CF 默认不缓存 JSON)。
* **含** `map` / `mjs` / `wasm`(有意增强,提高 sourcemap / ES module / WASM 命中)。
* 匹配:`$uri` 扩展名,大小写不敏感:
`if ($uri !~* \.(?:css|js|…)$) { set $openflare_skip_cache 1; }` `if ($uri !~* \.(?:css|js|…)$) { set $openflare_skip_cache 1; }`
* **V1.1(可选)**:全局配置项覆盖该列表;第一期不强制。
### 3.4 安全旁路(保持硬编码) ### 3.4 请求侧旁路(对齐 CF 后)
在策略匹配之前/之外,仍设置 `$openflare_skip_cache=1`: 仅保留:
1. `$request_method != GET`(含 HEAD,与现网一致) 1. `$request_method != GET`(含 HEAD,与现网一致;不做 CF 的 HEAD→GET)
2. `$http_authorization != ""`
3. 会话类 Cookie 正则(现网列表)
4. 请求 `$http_cache_control` 匹配 `no-cache|no-store|private`
`proxy_cache_bypass` / `proxy_no_cache` 均绑定 `$openflare_skip_cache`。 **删除(过往过严,导致缓存率过低):**
### 3.5 与源站头的关系(本阶段不改) * 会话类 Cookie 正则
* `$http_authorization != ""`
* 请求 `$http_cache_control` 匹配 `no-cache|no-store|private`
* 仍不输出 `proxy_cache_valid`。 **安全如何仍成立:**
* 对象**是否进入缓存流程**由策略 + 旁路决定;**存多久**继续依赖源站 `Cache-Control` / `Expires` 等及全局 `inactive`。
* Edge TTL / 强制忽略源站头 → 后续专项。 | 威胁 | 闸门 |
| --- | --- |
| 误缓存 HTML/API | 默认 `static` 扩展名(不含 html/json) |
| 个性化内容 | 源站 `private` / `no-store`(Nginx 尊重) |
| 响应写会话 | **`Set-Cookie` → 不入库**(§3.6) |
| `all` 过宽 | UI/文档警告:需源站正确 Cache-Control |
| 带 Bearer 的 API | 依赖策略(勿对 API 用 `all`)+ 源站头;完整 Auth 条件缓存为后续 |
### 3.5 默认 Edge TTL(无源站 freshness 时)
对齐 CF 无 `Cache-Control`/`Expires` 时的状态码默认 TTL,在启用缓存的 location 输出:
| 状态码 | TTL |
| --- | --- |
| 200, 206, 301 | 120m |
| 302, 303 | 20m |
| 404, 410 | 3m |
```nginx
proxy_cache_valid 200 206 301 120m;
proxy_cache_valid 302 303 20m;
proxy_cache_valid 404 410 3m;
```
* 源站提供合法 `Cache-Control` / `Expires` 时,仍以源站 freshness 为准(不 `proxy_ignore_headers`)。
* **不做**强制忽略源站头的 Edge TTL 覆盖。
### 3.6 响应侧:Set-Cookie 不入库
对齐 CF OCC 默认:eligible 请求若源站返回 **`Set-Cookie`**,**不写入** `proxy_cache`(可读路径仍可能 MISS/BYPASS 语义)。
```nginx
proxy_no_cache $openflare_skip_cache $upstream_http_set_cookie;
```
(`proxy_no_cache` 多参数:任一非空且非 `"0"` 则不写入。)
`proxy_cache_bypass` 仍仅绑定 `$openflare_skip_cache`(请求侧 skip);响应侧只影响**写入**,与 CF「eligible 但响应不可缓存」一致。
### 3.7 与源站头的关系
* **是否 eligible**:策略 + 方法旁路。
* **是否入库 / 存多久**:源站 `Cache-Control` / `Expires` + 默认 `proxy_cache_valid` + Set-Cookie 闸门 + 全局 `inactive`。
--- ---
@@ -107,11 +175,13 @@ zip 7z gz tar
│ no → location 无 proxy_cache │ no → location 无 proxy_cache
▼ yes ▼ yes
set $openflare_skip_cache 0 set $openflare_skip_cache 0
→ 安全旁路 if → 置 1 → 非 GET → 置 1
→ 策略 if(static/all/suffix/…)→ 可置 1 → 策略 if(static/all/suffix/…)→ 可置 1
proxy_cache openflare_cache proxy_cache openflare_cache
proxy_cache_methods GET proxy_cache_methods GET
proxy_cache_bypass / proxy_no_cache $openflare_skip_cache proxy_cache_bypass $openflare_skip_cache
proxy_no_cache $openflare_skip_cache $upstream_http_set_cookie
proxy_cache_valid …
→ →
access.log cache_status=$upstream_cache_status access.log cache_status=$upstream_cache_status
``` ```
@@ -125,16 +195,16 @@ access.log cache_status=$upstream_cache_status
| `suffix` | 不匹配 `cache_rules` 扩展名 → skip | | `suffix` | 不匹配 `cache_rules` 扩展名 → skip |
| `path_prefix` / `path_exact` | 同现实现 | | `path_prefix` / `path_exact` | 同现实现 |
### 4.2 涉及代码面(实现时) ### 4.2 涉及代码面
| 区域 | 路径 | | 区域 | 路径 |
| --- | --- | | --- | --- |
| 渲染 | `pkg/render/openresty/render.go`(策略分支 + 内置扩展名常量) | | 渲染 | `pkg/render/openresty/render.go`(旁路、Set-Cookie、`proxy_cache_valid`、扩展名常量) |
| 校验 | `internal/apps/openflare/proxy_route/helpers.go` | | 校验 | `internal/apps/openflare/proxy_route/helpers.go` |
| 模型/默认 | 创建路由默认 `cache_policy=static`;读写时 `url`→`all` | | 模型/默认 | 创建路由默认 `cache_policy=static`;读写时 `url`→`all` |
| 快照 | `config_version/snapshot.go` | | 快照 | `config_version` 快照规范化 |
| UI | `proxy-routes/detail/components/cache-section.tsx` | | UI | `proxy-routes/detail/components/cache-section.tsx` |
| 测试 | `pkg/render/openresty/render_test.go`、proxy_route helpers 测试 | | 测试 | `pkg/render/openresty/render_test.go` 等 |
--- ---
@@ -142,49 +212,79 @@ access.log cache_status=$upstream_cache_status
| 数据 | 处理 | | 数据 | 处理 |
| --- | --- | | --- | --- |
| DB 中 `cache_policy=''` 或 `url`(且已启用缓存) | 读取 / 快照 / 渲染均规范为 **`all`**,保证存量「宽缓存」不变 | | DB 中 `cache_policy=''` 或 `url`(且已启用缓存) | 读 / 快照 / 渲染 → **`all`** |
| API 写入时 `enabled` 且 policy 为空 | 规范为 **`all`**(兼容旧客户端);UI 新建开启时**显式提交** `static` | | API 写入 enabled 且 policy 为空 | 规范为 **`all`**;UI 新建开启时**显式提交** `static` |
| 新建路由 | 默认 `cache_enabled=false`;表单开启缓存时默认策略 **`static`** | | 新建路由 | 开启缓存时默认 **`static`** |
| 已开启且 `url` 的站点 | 显示与发布为 `all`,**缓存范围不变** | | 旁路行为变更 | **破坏性相对旧实现**:带 Cookie/Auth 的流量从「未缓存」变为可 HIT;需 **重新发布节点配置** 后生效 |
| 期望「只缓存静态」的旧站点 | 用户在 UI 改为 `static` 或自定义 `suffix` | | 默认扩展名 | 自表中 **移除 `json`**;已依赖缓存 `*.json` 的站点可改 `suffix` 自定义或 `all` |
**发布说明建议:** 说明默认策略变更仅影响**新配置**;存量 `url` 视为 `all`。 **发布说明:** 说明本次对齐 CF 默认模型;命中率预期上升;`all` 与错误源站头风险需运维自查。
--- ---
## 6. UI 文案要点(缓存 Tab) ## 6. UI 文案要点(缓存 Tab)
* 开启缓存后默认:**标准静态资源**(列出扩展名摘要,并写明不含 HTML)。 * 开启缓存后默认:**标准静态资源**(摘要扩展名,**不含 HTML/JSON**;含 map/mjs 等)。
* 选项:**标准静态资源** / **所有可缓存 GET(高级)** / 自定义后缀 / 路径前缀 / 精确路径。 * 选项:标准静态 / 所有可缓存 GET(高级)/ 自定义后缀 / 路径前缀 / 精确路径。
* 固定说明:非 GET、带 Authorization、常见登录 Cookie、请求禁止缓存头时跳过缓存。 * 说明对齐 CF:
* 提示:全局 Performance 中缓存总开关须开启,否则站点开关无效。 * 登录 Cookie **不会**单独跳过缓存;
* 源站 `private` / `no-store` / 响应 **`Set-Cookie`** 不会写入边缘缓存;
* 无源站缓存头时使用默认 Edge TTL。
* **高级 `all`**:警告「类似 Cache Everything,个性化页面必须由源站声明 private/no-store」。
* 全局 Performance 缓存总开关须开启。
--- ---
## 7. 验证要点 ## 7. 验证要点
* 渲染:`static` 生成扩展名 `if`;`all`/`url` 无路径限制;旁路四条仍在。 * 渲染:无 Cookie/Auth/请求 Cache-Control 旁路;含 `proxy_cache_valid` 三行;`proxy_no_cache` 含 `$upstream_http_set_cookie`。
* 单测:内置表含 `css`/`js`/`woff2`,不含 `html`。 * 单测:内置表含 `css`/`js`/`map`/`mjs`,**不含** `html`/`json`。
* 手动:开启 `static` 后请求 `/a.css` 可出现 HIT/MISS;`/index.html` 或 `/api` 多为未缓存/BYPASS。 * 手动:
* 观测:access log `cache_status` 与列表三态一致。 * 带 session Cookie 请求 `/a.js` → 第二次 `HIT`;
* `/index.html` + `static` → 未缓存;
* 源站对 eligible 路径返回 `Set-Cookie` → 不入库(持续 MISS/不 HIT);
* 源站 `Cache-Control: private` → 不入库。
* 观测:access log 三态与原始 `cache_status` 一致。
* 生效:配置版本发布并节点应用后验证。
--- ---
## 8. 后续路线图(非本设计交付) ## 8. 决策矩阵(防漏判)
1. **Edge TTL / 尊重源站开关**(`proxy_cache_valid`、`proxy_ignore_headers`) | 场景 | CF | OpenFlare(本设计) |
2. **可配置旁路**(Cookie/Query) | --- | --- | --- |
3. **Purge API** | GET 静态 + session Cookie + 源站 public max-age | HIT | HIT |
4. **Cache Rules**(有序规则 + 动作) | GET HTML + static 策略 | DYNAMIC | 策略 skip → 未缓存 |
5. **全局默认可缓存扩展名配置** | GET + all + 源站 private | 不入库 | 不入库 |
| GET 静态 + 响应 Set-Cookie | BYPASS(OCC) | 不入库 |
| GET + Authorization + 静态 public | 条件缓存 | 可缓存(简化;依赖源站勿对敏感 API 乱标 public) |
| GET + 无 CC 的 200 静态 | 默认 120m | `proxy_cache_valid` 120m |
| DevTools Disable cache(请求 no-cache) | 边缘默认可仍 HIT | 边缘默认可仍 HIT |
| POST | 不缓存 | 非 GET skip |
--- ---
## 9. 决策记录 ## 9. 后续路线图
1. Auth 完整 RFC/CF 条件缓存(Lua)
2. 强制 Edge TTL / `proxy_ignore_headers`(Cache Rules 级)
3. Purge API
4. Cache Rules(有序规则 + 动作)
5. 全局默认可缓存扩展名可配置;可选对齐 CF 更长扩展名表
6. HEAD→GET
---
## 10. 决策记录
| 决策 | 选择 | 原因 | | 决策 | 选择 | 原因 |
| --- | --- | --- | | --- | --- | --- |
| 默认可缓存范围 | 开启缓存默认 `static` 扩展名表 | 对标 CF 开箱行为,降低 HTML/API 被误缓存 | | 请求 Cookie 旁路 | **删除** | 对齐 CF;恢复登录用户静态命中率 |
| 旧 `url` | 映射为 `all` | 避免存量站点行为变化 | | 请求 Authorization / Cache-Control 旁路 | **删除** | 对齐 CF 请求 eligible 模型;响应闸门兜底 |
| HTML | 默认不在白名单 | 对齐 CF 默认不缓存 HTML | | Set-Cookie | **proxy_no_cache 绑定** | 对齐 CF OCC「响应 Set-Cookie 不入库」 |
| 第一期不做 Edge TTL/Purge | 明确 Out of Scope | 先收敛「谁可以进缓存」再优化「存多久/怎么清」 | | 默认 Edge TTL | **按状态码 proxy_cache_valid** | 对齐 CF 无头时默认 TTL,避免「永不入库」 |
| 默认表去掉 json | **是** | 对齐 CF 默认不缓存 JSON |
| 保留 map/mjs/wasm | **是** | 现代前端有用命中,有意增强 |
| 默认可缓存范围 | 开启缓存默认 `static` | 对标 CF,降低 HTML/API 误缓存 |
| 旧 `url` | 映射 `all` | 存量行为不收窄 |
| 完整 Auth 条件 / Purge / Rules | 后续 | 先闭合默认闭环再扩展 |
+1 -1
View File
@@ -22,7 +22,7 @@ OpenFlare 适合需要统一管理多台 OpenResty 代理节点的团队,具
| 能力 | 说明 | 详细设计/使用指南 | | 能力 | 说明 | 详细设计/使用指南 |
| --- | --- | --- | | --- | --- | --- |
| **反代配置管理** | 以网站规则(Proxy Route)为聚合边界,支持多域名与多上游负载均衡 | [新建反代配置](../guide/proxy-config.md) | | **反代配置管理** | 以网站规则(Proxy Route)为聚合边界,支持多域名与多上游负载均衡 | [新建反代配置](../guide/proxy-config.md) |
| **边缘缓存** | 单节点 OpenResty `proxy_cache`;开启后默认仅缓存标准静态扩展名(对标 CF 默认可缓存范围) | [边缘缓存策略设计](./edge-cache-design.md) | | **边缘缓存** | 单节点 OpenResty `proxy_cache`;默认 static 扩展名 + 源站头/Set-Cookie 闸门 + 默认 Edge TTL(对标 CF 默认模型) | [边缘缓存策略设计](./edge-cache-design.md) |
| **Zone 与域名管理** | 以可注册根域为管理入口,聚合明确域名、域名证书与反代路由 | [Zone 与域名资源设计](./zone-design.md) | | **Zone 与域名管理** | 以可注册根域为管理入口,聚合明确域名、域名证书与反代路由 | [Zone 与域名资源设计](./zone-design.md) |
| **配置版本控制** | 支持全局单一激活版本的预览、发布、不可变快照历史与秒级一键回滚 | [Agent 与发布模型](./agent-design.md) | | **配置版本控制** | 支持全局单一激活版本的预览、发布、不可变快照历史与秒级一键回滚 | [Agent 与发布模型](./agent-design.md) |
| **WAF 安全防护** | 支持可视化 DAG 编排规则、手动/自动/订阅型 IP 组、GeoIP 匹配与 PoW CC 防护 | [WAF 设计](./waf-design.md) / [WAF 可编排规则设计](./waf-orchestration-design.md) / [WAF 使用指南](../guide/waf-usage.md) | | **WAF 安全防护** | 支持可视化 DAG 编排规则、手动/自动/订阅型 IP 组、GeoIP 匹配与 PoW CC 防护 | [WAF 设计](./waf-design.md) / [WAF 可编排规则设计](./waf-orchestration-design.md) / [WAF 使用指南](../guide/waf-usage.md) |
+4 -3
View File
@@ -10,7 +10,7 @@ OpenFlare 是一套自托管的 OpenResty 控制面。它把反向代理网站
1. [快速开始](./quick-start.md):用 Docker Compose 启动 Server,登录管理端,并接入第一个 Agent。 1. [快速开始](./quick-start.md):用 Docker Compose 启动 Server,登录管理端,并接入第一个 Agent。
2. [发布第一份配置](./first-site.md):快速新建一条最基础的 HTTP 反代站点规则,并验证节点生效状态。 2. [发布第一份配置](./first-site.md):快速新建一条最基础的 HTTP 反代站点规则,并验证节点生效状态。
3. [新建反代配置](./proxy-config.md):一步一步了解如何从证书导入与申请开始,配置 HTTPS 加密与上游源站管理。 3. [新建反代配置](./proxy-config.md):一步一步了解如何从证书导入与申请开始,配置 HTTPS 加密、上游源站与边缘缓存。
4. [Zone 域名迁移](./zone-domain-migration.md):从旧托管域名/路由内嵌域名升级到 Zone 模型(goose 自动导入),含备份、验收与回滚说明。 4. [Zone 域名迁移](./zone-domain-migration.md):从旧托管域名/路由内嵌域名升级到 Zone 模型(goose 自动导入),含备份、验收与回滚说明。
5. [Pages 静态托管使用](./pages-usage.md):了解静态项目 ZIP 上传限制、SPA Fallback、以及内置 API 反向代理配置。 5. [Pages 静态托管使用](./pages-usage.md):了解静态项目 ZIP 上传限制、SPA Fallback、以及内置 API 反向代理配置。
6. [内网穿透与隧道使用](./tunnel-usage.md):部署 Relay 与 Client,实现安全、无公网 IP 反向穿透。 6. [内网穿透与隧道使用](./tunnel-usage.md):部署 Relay 与 Client,实现安全、无公网 IP 反向穿透。
@@ -18,7 +18,7 @@ OpenFlare 是一套自托管的 OpenResty 控制面。它把反向代理网站
8. [WAF 自动 IP 组语法](./waf-ip-group-expr.md):编写自动 IP 组 Expr 规则,了解关键字含义和预设规则。 8. [WAF 自动 IP 组语法](./waf-ip-group-expr.md):编写自动 IP 组 Expr 规则,了解关键字含义和预设规则。
9. [Uptime Kuma 监控同步](./uptime-kuma.md):配置并使用 Uptime Kuma 自动差分同步和监控范围控制。 9. [Uptime Kuma 监控同步](./uptime-kuma.md):配置并使用 Uptime Kuma 自动差分同步和监控范围控制。
10. [SSO 登录配置](./sso.md):配置 GitHub 或 OIDC 实现第三方单点登录 (SSO) 接入。 10. [SSO 登录配置](./sso.md):配置 GitHub 或 OIDC 实现第三方单点登录 (SSO) 接入。
11. [故障排查](./troubleshooting.md):按症状排查登录、数据库、节点同步、OpenResty 应用和前端构建问题。 11. [故障排查](./troubleshooting.md):按症状排查登录、数据库、节点同步、OpenResty、边缘缓存命中与前端构建问题。
12. [引用与致谢](./credits.md):查看系统依赖的优秀开源项目与社区致谢清单。 12. [引用与致谢](./credits.md):查看系统依赖的优秀开源项目与社区致谢清单。
## 按角色查找 ## 按角色查找
@@ -27,7 +27,8 @@ OpenFlare 是一套自托管的 OpenResty 控制面。它把反向代理网站
| --- | --- | | --- | --- |
| 5 分钟内跑起管理端 | [快速开始](./quick-start.md) | | 5 分钟内跑起管理端 | [快速开始](./quick-start.md) |
| 发布第一条反向代理配置 | [发布第一份配置](./first-site.md) | | 发布第一条反向代理配置 | [发布第一份配置](./first-site.md) |
| 配置域名证书与高级反代 | [新建反代配置](./proxy-config.md) | | 配置域名证书、反代与边缘缓存 | [新建反代配置](./proxy-config.md)(含缓存说明) |
| 静态资源不命中缓存 | [故障排查 · 边缘缓存](./troubleshooting.md#边缘缓存命中率异常) |
| 托管单页应用或静态网站 | [Pages 静态托管使用](./pages-usage.md) | | 托管单页应用或静态网站 | [Pages 静态托管使用](./pages-usage.md) |
| 配置内网穿透映射 | [内网穿透与隧道使用](./tunnel-usage.md) | | 配置内网穿透映射 | [内网穿透与隧道使用](./tunnel-usage.md) |
| 配置防 CC 与 IP 组拦截 | [WAF 安全防护使用](./waf-usage.md) | | 配置防 CC 与 IP 组拦截 | [WAF 安全防护使用](./waf-usage.md) |
+31
View File
@@ -84,3 +84,34 @@
2. 在历史列表中找到发布前的上一个稳定版本。 2. 在历史列表中找到发布前的上一个稳定版本。
3. 点击 **「激活此版本」**。 3. 点击 **「激活此版本」**。
4. 所有在线 Agent 节点将在秒级自动重载回历史配置,实现秒级避险。 4. 所有在线 Agent 节点将在秒级自动重载回历史配置,实现秒级避险。
---
## 边缘缓存(可选)
站点详情 **「缓存」** 页可开启边缘 `proxy_cache`(须同时开启 **性能设置 → 全局 OpenResty 缓存**)。行为对标 Cloudflare 默认模型,详见 [边缘缓存策略设计](../design/edge-cache-design.md)。
### 推荐设置
| 项 | 建议 |
| --- | --- |
| 策略 | **标准静态资源**(默认推荐):仅 css/js/map/图片/字体等,**不含 HTML/JSON** |
| 登录 Cookie | **不会**单独跳过缓存;带会话的用户仍可命中静态资源 |
| 源站 | 静态资源建议 `Cache-Control: public, max-age=…`;动态/个性化必须 `private` 或 `no-store` |
| 响应 Set-Cookie | 不会写入边缘缓存 |
| 无源站缓存头 | 按状态码使用默认 Edge TTL(如 200 约 120 分钟) |
### 高级策略「所有可缓存 GET」
类似 Cloudflare Cache Everything:路径不再限制扩展名。若源站对 HTML 未声明 `private`/`no-store`,**可能把个性化页面缓存并串用户**。仅在源站缓存头正确、或内容全局一致时使用。
### 生效方式
缓存开关与策略写在配置快照中。保存站点后须 **发布并激活配置版本**,Agent 应用后才生效。仅改 UI 不发布则节点仍用旧规则。
### 快速自检
1. 全局缓存已开,站点缓存已开,策略为「标准静态资源」。
2. 发布配置并确认节点应用成功。
3. 带登录 Cookie 连续两次请求同一 `/assets/app.js`(或带 hash 的 immutable 路径),访问日志中 `cache_status` 第二次应为 **HIT**(或 UI「命中」)。
4. 若仍为「未缓存」:确认策略是否匹配该路径扩展名、是否非 GET、源站是否返回 `Set-Cookie` / `private`,以及节点是否已应用新版本。更多见 [故障排查 · 边缘缓存](./troubleshooting.md#边缘缓存命中率异常)。
+31
View File
@@ -15,6 +15,7 @@
| 发布后节点未更新 | 激活版本、节点 heartbeat、应用记录 | | 发布后节点未更新 | 激活版本、节点 heartbeat、应用记录 |
| OpenResty 应用失败 | 应用记录、Agent 日志、证书、上游地址、端口占用 | | OpenResty 应用失败 | 应用记录、Agent 日志、证书、上游地址、端口占用 |
| 访问分析无数据 | OpenResty 容器状态、观测端口、Agent 补报日志 | | 访问分析无数据 | OpenResty 容器状态、观测端口、Agent 补报日志 |
| 静态资源总不命中缓存 | 全局/站点缓存开关、策略扩展名、配置是否已发布、访问日志 `cache_status`、源站 Set-Cookie / Cache-Control |
## Server 无法启动 ## Server 无法启动
@@ -238,6 +239,36 @@ pnpm build
| API 类型不一致 | 检查 `lib/api/` 和 `types/` 中的响应结构 | | API 类型不一致 | 检查 `lib/api/` 和 `types/` 中的响应结构 |
| E2E 失败 | 确认 Server 和前端开发服务器都已启动 | | E2E 失败 | 确认 Server 和前端开发服务器都已启动 |
## 边缘缓存命中率异常
访问日志中缓存三态:**命中**(HIT/STALE/REVALIDATED/UPDATING)、**回源**(MISS/EXPIRED)、**未缓存**(BYPASS 或空,请求时未进入可缓存路径或响应未入库)。设计说明见 [边缘缓存策略设计](../design/edge-cache-design.md)。
### 检查清单
1. **性能设置** 中全局 OpenResty 缓存已开启。
2. 站点 **缓存** 已启用,策略与路径匹配(「标准静态资源」只覆盖内置扩展名,**不含** HTML/JSON;`.js.map` 的扩展名是 `map`,在默认表内)。
3. 已 **发布并激活** 配置版本,对应节点应用记录成功(改缓存规则不发布则节点仍用旧旁路逻辑)。
4. 请求方法为 **GET**(非 GET 一律不缓存)。
5. 源站未对目标 URL 返回 **`Set-Cookie`**(有则不会写入边缘)。
6. 源站未声明 **`Cache-Control: private` / `no-store`**(共享缓存不会存)。
7. 浏览器 DevTools「禁用缓存」只影响浏览器;边缘是否 HIT 看访问日志 `cache_status`,不要只看 Network 面板。
### 常见误解
| 现象 | 说明 |
| --- | --- |
| 登录后全是「未缓存」且从未发布新版本 | 旧配置曾因会话 Cookie 旁路;升级后须重新发布节点配置 |
| `static` 下 `/api/foo` 或 `/index.html` 未缓存 | 预期行为(扩展名不在默认可缓存表) |
| 策略为 `all` 后 HTML 被串用户 | 源站未禁止共享缓存;改回 `static` 或给动态响应加 `private`/`no-store` |
| 带 `?v=` 的 URL 命中率低 | 默认缓存键含完整 `$request_uri`,query 不同即不同对象 |
| 第一次 MISS、第二次仍 MISS | 查源站是否每次 `Set-Cookie`、是否 `private`,或节点磁盘/缓存 inactive 过短 |
### 期望行为(对齐 Cloudflare 默认)
* 带登录 Cookie 的用户访问 `/_app/**/*.js` 等静态资源:**可以 HIT**。
* 响应带 `Set-Cookie` 或 `private`:**不入库**。
* 无源站缓存头的可缓存状态码:使用默认 Edge TTL(如 200 约 120 分钟)。
## 文档站构建失败 ## 文档站构建失败
```bash ```bash
+85
View File
@@ -0,0 +1,85 @@
# 边缘缓存对齐 Cloudflare 默认模型 — 实现计划
对应设计:[edge-cache-design.md](../design/edge-cache-design.md)
## 1. 目标与背景
* **需求背景**:现网对会话 Cookie / Authorization / 请求 Cache-Control 一律旁路,登录用户静态资源几乎全是「未缓存」,命中率远低于 Cloudflare 默认。需对齐 CF 两段闭环:请求 eligible × 响应可共享缓存。
* **Scope(必做)**
1. 删除请求侧 Cookie、Authorization、请求 Cache-Control 旁路
2. 响应侧:`proxy_no_cache` 绑定 `$upstream_http_set_cookie`
3. 默认 `proxy_cache_valid`(200/206/301→120m,302/303→20m,404/410→3m)
4. 默认静态扩展名移除 `json`;保留 `map`/`mjs`/`wasm`
5. 渲染单测 + UI 文案 + 设计/changelog
* **Out of Scope**:Purge、Cache Rules、强制忽略源站 CC、Auth RFC 条件缓存、HEAD→GET
## 2. 设计决策摘要
| 决策 | 选择 |
| --- | --- |
| 请求 Cookie | 不旁路(对齐 CF) |
| Set-Cookie | 不入库 |
| 无源站 CC | 状态码默认 Edge TTL |
| json | 默认表移除 |
| 兼容 | `url`→`all` 不变;行为变更需重新发布配置 |
## 3. 修改清单
### 边缘渲染
* #### [MODIFY] `pkg/render/openresty/render.go`
* `renderRouteCacheBlock`:仅保留非 GET 旁路;`proxy_no_cache $openflare_skip_cache $upstream_http_set_cookie`;追加三行 `proxy_cache_valid`
* #### [MODIFY] `pkg/render/openresty/types.go`
* `DefaultStaticCacheExtensions`:去掉 `json`
* #### [MODIFY] `pkg/render/openresty/render_test.go`
* 断言:无 cookie/auth/cache_control 旁路;含 set_cookie 与 proxy_cache_valid;表不含 json、含 map
### 前端
* #### [MODIFY] `frontend/app/(main)/proxy-routes/detail/components/cache-section.tsx`
* 去掉「绕过登录 Cookie / Authorization」类文案
* 改为 CF 对齐说明:源站 private/no-store、Set-Cookie 不入库、默认静态不含 HTML/JSON
### 文档
* #### [MODIFY] `docs/design/edge-cache-design.md`(已更新)
* #### [MODIFY] `docs/changelog/index.md` `[Unreleased]`
* #### [MODIFY] `docs/plan/index.md` 登记本计划
### 不改
* 无 DB 迁移
* 无 API 字段变更(策略枚举不变)
## 4. 验证计划
### 自动化
```bash
go test ./pkg/render/openresty/
make format
make code-check
```
### 数据面(配置发布后)
1. 站点 `cache_enabled` + `static`,全局缓存开
2. 带 session Cookie:`GET /static/app.js` 第二次应 HIT
3. `GET /index.html` 应为未缓存
4. 源站返回 `Set-Cookie` 的静态 URL 不应出现稳定 HIT
5. 访问日志 `cache_status` 与三态一致
## 5. 发布注意
* 节点需 **重新发布/拉取配置版本** 后旁路变更才生效
* 若站点依赖边缘缓存 `*.json`,改为 `suffix` 含 json 或 `all`
## 6. 状态
- [x] 设计定稿(用户确认:全量对齐 CF)
- [x] 渲染与单测
- [x] UI 文案
- [x] changelog / plan index
- [x] `make format` + `make code-check`
- [x] 复查补强:`all` 策略 UI 警告;proxy-config / troubleshooting 运维说明
- [ ] 用户确认后提交
+1
View File
@@ -20,6 +20,7 @@
* [边缘缓存默认 static 策略](./20260718-edge-cache-static-default.md):开启缓存默认仅静态扩展名;存量 url→all。 * [边缘缓存默认 static 策略](./20260718-edge-cache-static-default.md):开启缓存默认仅静态扩展名;存量 url→all。
* [访问日志 IP 明细 Tab](./20260719-access-log-ip-tab.md):第三 Tab 按 IP 聚合列表(时间窗/流量/2xx 比例);IP 情报迁入独立详情;日志详情仅请求字段。 * [访问日志 IP 明细 Tab](./20260719-access-log-ip-tab.md):第三 Tab 按 IP 聚合列表(时间窗/流量/2xx 比例);IP 情报迁入独立详情;日志详情仅请求字段。
* [边缘限流全局默认](./20260719-http-default-rate-limit.md):全局默认并发/带宽;站点 0 继承、-1 关闭;RenderRouteConfig 合并。 * [边缘限流全局默认](./20260719-http-default-rate-limit.md):全局默认并发/带宽;站点 0 继承、-1 关闭;RenderRouteConfig 合并。
* [边缘缓存对齐 Cloudflare 默认模型](./20260723-edge-cache-cf-align.md):删除过严请求旁路;Set-Cookie 不入库;默认 Edge TTL;扩展名去 json。
## 已完成的计划 ## 已完成的计划
@@ -2,9 +2,12 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { CircleHelp, TriangleAlert } from 'lucide-react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { z } from 'zod'; import { z } from 'zod';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Button } from '@/components/ui/button';
import { import {
Form, Form,
FormControl, FormControl,
@@ -14,6 +17,11 @@ import {
FormLabel, FormLabel,
FormMessage, FormMessage,
} from '@/components/ui/form'; } from '@/components/ui/form';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover';
import { import {
Select, Select,
SelectContent, SelectContent,
@@ -92,6 +100,55 @@ function needsRulesForPolicy(policy: string) {
); );
} }
function CacheHelpButton() {
return (
<Popover>
<PopoverTrigger asChild>
<Button
type='button'
variant='ghost'
size='icon-sm'
className='size-7 text-muted-foreground'
aria-label='缓存使用说明'
>
<CircleHelp />
</Button>
</PopoverTrigger>
<PopoverContent align='start' className='w-80 flex flex-col gap-3 p-4'>
<div className='text-sm font-medium'>缓存使用说明</div>
<div className='flex flex-col gap-2 text-xs text-muted-foreground leading-relaxed'>
<p>
扩展名/策略决定是否可缓存,源站头与Set-Cookie
决定是否入库。须同时开启性能设置中的全局 OpenResty 缓存。
</p>
<p>
<span className='font-medium text-foreground'>推荐策略:</span>
新建站点建议使用「标准静态资源」(含
css/js/map/图片/字体/媒体等,不含 HTML/JSON)。
</p>
<p>
<span className='font-medium text-foreground'>通用规则:</span>非
GET 不缓存;登录 Cookie 不会单独跳过缓存;源站 private/no-store
或响应 Set-Cookie 不会写入边缘。
</p>
<p>
<span className='font-medium text-foreground'>
所有可缓存 GET:
</span>
高级选项,类似 Cache Everything。个性化页面须由源站声明
private/no-store,否则 HTML 可能被边缘缓存并串给其他用户。
</p>
<p>
<span className='font-medium text-foreground'>自定义规则:</span>
后缀填 jpg/css/js;路径前缀填 /assets;精确路径填 /robots.txt。
保存后须重新发布配置版本才会在节点生效。
</p>
</div>
</PopoverContent>
</Popover>
);
}
export function CacheSection({ export function CacheSection({
route, route,
onRouteUpdate, onRouteUpdate,
@@ -141,8 +198,8 @@ export function CacheSection({
: watchedPolicy === 'path_exact' : watchedPolicy === 'path_exact'
? '每行一个精确路径,例如 /robots.txt。' ? '每行一个精确路径,例如 /robots.txt。'
: watchedPolicy === 'static' : watchedPolicy === 'static'
? '标准静态资源使用内置扩展名列表(不含 HTML),无需填写规则。' ? '标准静态资源使用内置扩展名列表,无需填写规则。'
: '所有可缓存 GET 无需额外规则(仍会绕过登录态与 Authorization)。'; : '当前策略无需额外路径规则。';
const rulesPlaceholder = const rulesPlaceholder =
watchedPolicy === 'suffix' watchedPolicy === 'suffix'
@@ -156,7 +213,8 @@ export function CacheSection({
return ( return (
<SectionShell <SectionShell
title='缓存' title='缓存'
description='保留现有安全绕过逻辑,只对当前站点生效。' description='配置站点边缘缓存策略。'
titleExtra={<CacheHelpButton />}
formId={proxyRouteFormIds.cache} formId={proxyRouteFormIds.cache}
saving={saving} saving={saving}
> >
@@ -187,11 +245,7 @@ export function CacheSection({
<FormItem className='flex items-center justify-between rounded-lg border p-3'> <FormItem className='flex items-center justify-between rounded-lg border p-3'>
<div className='space-y-0.5'> <div className='space-y-0.5'>
<FormLabel>启用站点缓存</FormLabel> <FormLabel>启用站点缓存</FormLabel>
<FormDescription> <FormDescription>新建推荐「标准静态资源」。</FormDescription>
新建推荐「标准静态资源」(不含
HTML)。须同时开启性能设置中的全局 OpenResty
缓存。仍会绕过非 GET、Authorization 与常见登录 Cookie。
</FormDescription>
</div> </div>
<FormControl> <FormControl>
<Switch <Switch
@@ -227,15 +281,22 @@ export function CacheSection({
<SelectItem value='path_exact'>精确路径</SelectItem> <SelectItem value='path_exact'>精确路径</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
<FormDescription>
标准静态资源含 css/js/图片/字体/媒体等,默认不缓存 HTML
与接口路径。
</FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
{watchedEnabled && watchedPolicy === 'all' ? (
<Alert>
<TriangleAlert />
<AlertTitle>高级策略风险</AlertTitle>
<AlertDescription>
个性化 HTML 在源站未声明 private / no-store
时可能被边缘缓存并串给其他用户。详情见标题旁帮助。
</AlertDescription>
</Alert>
) : null}
<FormField <FormField
control={form.control} control={form.control}
name='cache_rules_text' name='cache_rules_text'
@@ -14,6 +14,7 @@ import {
interface SectionShellProps { interface SectionShellProps {
title: string; title: string;
description: string; description: string;
titleExtra?: ReactNode;
formId: string; formId: string;
saving?: boolean; saving?: boolean;
children: ReactNode; children: ReactNode;
@@ -22,6 +23,7 @@ interface SectionShellProps {
export function SectionShell({ export function SectionShell({
title, title,
description, description,
titleExtra,
formId, formId,
saving = false, saving = false,
children, children,
@@ -30,7 +32,10 @@ export function SectionShell({
<Card> <Card>
<CardHeader className='flex flex-row items-start justify-between gap-4 space-y-0'> <CardHeader className='flex flex-row items-start justify-between gap-4 space-y-0'>
<div className='space-y-1'> <div className='space-y-1'>
<CardTitle className='text-sm font-semibold'>{title}</CardTitle> <div className='flex items-center gap-1.5'>
<CardTitle className='text-sm font-semibold'>{title}</CardTitle>
{titleExtra}
</div>
<CardDescription>{description}</CardDescription> <CardDescription>{description}</CardDescription>
</div> </div>
<Button <Button
+4 -4
View File
@@ -490,16 +490,16 @@ func renderRouteCacheBlock(cacheConfig routeCacheConfig, cfg ConfigSnapshot) str
var builder strings.Builder var builder strings.Builder
builder.WriteString(" set $openflare_skip_cache 0;\n") builder.WriteString(" set $openflare_skip_cache 0;\n")
builder.WriteString(" if ($request_method != GET) {\n set $openflare_skip_cache 1;\n }\n") builder.WriteString(" if ($request_method != GET) {\n set $openflare_skip_cache 1;\n }\n")
builder.WriteString(" if ($http_authorization != \"\") {\n set $openflare_skip_cache 1;\n }\n")
builder.WriteString(" if ($http_cookie ~* \"(session|sess|token|auth|jwt|logged_in|remember|laravel_session|connect\\\\.sid|_session)\") {\n set $openflare_skip_cache 1;\n }\n")
builder.WriteString(" if ($http_cache_control ~* \"(no-cache|no-store|private)\") {\n set $openflare_skip_cache 1;\n }\n")
if condition := renderRouteCachePolicyCondition(cacheConfig); condition != "" { if condition := renderRouteCachePolicyCondition(cacheConfig); condition != "" {
builder.WriteString(condition) builder.WriteString(condition)
} }
builder.WriteString(" proxy_cache openflare_cache;\n") builder.WriteString(" proxy_cache openflare_cache;\n")
builder.WriteString(" proxy_cache_methods GET;\n") builder.WriteString(" proxy_cache_methods GET;\n")
builder.WriteString(" proxy_cache_bypass $openflare_skip_cache;\n") builder.WriteString(" proxy_cache_bypass $openflare_skip_cache;\n")
builder.WriteString(" proxy_no_cache $openflare_skip_cache;\n") builder.WriteString(" proxy_no_cache $openflare_skip_cache $upstream_http_set_cookie;\n")
builder.WriteString(" proxy_cache_valid 200 206 301 120m;\n")
builder.WriteString(" proxy_cache_valid 302 303 20m;\n")
builder.WriteString(" proxy_cache_valid 404 410 3m;\n")
return builder.String() return builder.String()
} }
+34 -3
View File
@@ -407,11 +407,18 @@ func TestRenderRouteCachePolicyConditionStaticDefault(t *testing.T) {
if !strings.Contains(staticBlock, "css") || !strings.Contains(staticBlock, "woff2") { if !strings.Contains(staticBlock, "css") || !strings.Contains(staticBlock, "woff2") {
t.Fatalf("static policy should include default extensions, got:\n%s", staticBlock) t.Fatalf("static policy should include default extensions, got:\n%s", staticBlock)
} }
if !strings.Contains(staticBlock, "map") || !strings.Contains(staticBlock, "mjs") {
t.Fatalf("static policy should include map and mjs, got:\n%s", staticBlock)
}
if strings.Contains(staticBlock, "html") { if strings.Contains(staticBlock, "html") {
t.Fatalf("static policy must not include html, got:\n%s", staticBlock) t.Fatalf("static policy must not include html, got:\n%s", staticBlock)
} }
// Pattern is \.(?:css|js|...)$ — reject bare "json" as an alternation token.
if strings.Contains(staticBlock, "|json|") || strings.Contains(staticBlock, "|json)") || strings.Contains(staticBlock, "(?:json|") {
t.Fatalf("static policy must not include json (CF default), got:\n%s", staticBlock)
}
// Legacy empty/url = all (wide cache after security bypass). // Legacy empty/url = all (wide cache after method bypass).
emptyPolicy := renderRouteCachePolicyCondition(routeCacheConfig{Enabled: true, Policy: ""}) emptyPolicy := renderRouteCachePolicyCondition(routeCacheConfig{Enabled: true, Policy: ""})
if emptyPolicy != "" { if emptyPolicy != "" {
t.Fatalf("empty policy should map to all (no path filter), got %q", emptyPolicy) t.Fatalf("empty policy should map to all (no path filter), got %q", emptyPolicy)
@@ -427,7 +434,7 @@ func TestRenderRouteCachePolicyConditionStaticDefault(t *testing.T) {
} }
} }
func TestRenderRouteCacheBlockIncludesStaticWhenEnabled(t *testing.T) { func TestRenderRouteCacheBlockAlignsCloudflareDefaults(t *testing.T) {
block := renderRouteCacheBlock( block := renderRouteCacheBlock(
routeCacheConfig{Enabled: true, Policy: "static"}, routeCacheConfig{Enabled: true, Policy: "static"},
ConfigSnapshot{CacheEnabled: true}, ConfigSnapshot{CacheEnabled: true},
@@ -439,7 +446,31 @@ func TestRenderRouteCacheBlockIncludesStaticWhenEnabled(t *testing.T) {
t.Fatalf("expected static suffix pattern, got:\n%s", block) t.Fatalf("expected static suffix pattern, got:\n%s", block)
} }
if !strings.Contains(block, "request_method != GET") { if !strings.Contains(block, "request_method != GET") {
t.Fatalf("expected security bypass for non-GET, got:\n%s", block) t.Fatalf("expected method bypass for non-GET, got:\n%s", block)
}
if strings.Contains(block, "$http_authorization") {
t.Fatalf("must not bypass on Authorization (CF-aligned), got:\n%s", block)
}
if strings.Contains(block, "$http_cookie") {
t.Fatalf("must not bypass on Cookie (CF-aligned), got:\n%s", block)
}
if strings.Contains(block, "$http_cache_control") {
t.Fatalf("must not bypass on request Cache-Control (CF-aligned), got:\n%s", block)
}
if !strings.Contains(block, "proxy_no_cache $openflare_skip_cache $upstream_http_set_cookie") {
t.Fatalf("expected Set-Cookie no-cache gate, got:\n%s", block)
}
if !strings.Contains(block, "proxy_cache_valid 200 206 301 120m") {
t.Fatalf("expected default Edge TTL for 200/206/301, got:\n%s", block)
}
if !strings.Contains(block, "proxy_cache_valid 302 303 20m") {
t.Fatalf("expected default Edge TTL for 302/303, got:\n%s", block)
}
if !strings.Contains(block, "proxy_cache_valid 404 410 3m") {
t.Fatalf("expected default Edge TTL for 404/410, got:\n%s", block)
}
if !strings.Contains(block, "proxy_cache_bypass $openflare_skip_cache") {
t.Fatalf("expected proxy_cache_bypass on skip flag only, got:\n%s", block)
} }
} }
+2 -2
View File
@@ -37,9 +37,9 @@ const (
) )
// DefaultStaticCacheExtensions is the built-in suffix allowlist for cache_policy=static. // DefaultStaticCacheExtensions is the built-in suffix allowlist for cache_policy=static.
// HTML is intentionally excluded (Cloudflare-like default). // HTML and JSON are excluded (Cloudflare default). map/mjs/wasm are intentional extras.
var DefaultStaticCacheExtensions = []string{ var DefaultStaticCacheExtensions = []string{
"css", "js", "mjs", "map", "json", "css", "js", "mjs", "map",
"ico", "cur", "gif", "jpg", "jpeg", "png", "webp", "avif", "svg", "svgz", "ico", "cur", "gif", "jpg", "jpeg", "png", "webp", "avif", "svg", "svgz",
"ttf", "otf", "woff", "woff2", "eot", "ttf", "otf", "woff", "woff2", "eot",
"mp3", "mp4", "webm", "ogg", "flac", "mp3", "mp4", "webm", "ogg", "flac",