mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-29 14:06:36 +08:00
feat(cache): 开启缓存默认仅缓存标准静态资源
路由缓存策略新增 static(内置扩展名,不含 HTML)与 all; 存量 url 规范为 all。OpenResty 渲染与代理路由 UI 同步。
This commit is contained in:
@@ -23,6 +23,7 @@ sidebar: false
|
||||
|
||||
### 变更
|
||||
|
||||
- 边缘缓存默认策略调整为「标准静态资源」:开启站点缓存后默认仅缓存 css/js/图片/字体等扩展名(不含 HTML);原「按 URL」行为保留为「所有可缓存 GET」。
|
||||
- 优化访问日志概览及其他图表全局排行榜 (RankChart) 的样式布局:将每一项改为单行横向排布(左侧标签、中间进度条、右侧数值),数值支持自动格式化为 Compact 形式(如 39.57k、1.2M),同时调整默认高度为 320px 并隐藏滚动条。
|
||||
- 访问日志页面重构为「概览」与「日志明细」两个标签页:概览展示请求量、访问量、带宽趋势与 Top Paths/Hosts/IPs,明细列表保留检索能力;已移除时间折叠与 IP 汇总视图。
|
||||
- 边缘访问日志现支持上报并存储 User-Agent;概览新增设备类型饼图,以及浏览器、操作系统、User-Agent 排行。
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# 边缘缓存默认 static 策略 — 实现计划
|
||||
|
||||
对应设计:[edge-cache-design.md](../design/edge-cache-design.md)
|
||||
|
||||
## 目标
|
||||
|
||||
路由开启缓存后默认仅缓存标准静态扩展名(`static`);存量 `url` 映射为 `all`。
|
||||
|
||||
## 修改清单
|
||||
|
||||
1. **渲染** `pkg/render/openresty/render.go`:`static` 内置扩展名;`url`/`all` 无路径限制
|
||||
2. **校验** `internal/apps/openflare/proxy_route/helpers.go`:策略枚举与规范化
|
||||
3. **前端** `cache-section.tsx` + helpers:默认 `static`,选项文案
|
||||
4. **测试** render + helpers
|
||||
5. **changelog**
|
||||
|
||||
## 验证
|
||||
|
||||
```bash
|
||||
go test ./pkg/render/openresty/ ./internal/apps/openflare/proxy_route/
|
||||
make code-check # 或至少 go test + frontend tsc
|
||||
```
|
||||
@@ -17,6 +17,7 @@
|
||||
* [WAF 可编排规则](./20260713-waf-orchestration.md):使用 React Flow 编辑 DAG 规则,发布时编译并由 OpenResty 纯内存执行。
|
||||
* [边缘可观测与业务流量统计重构](./20260717-observability-redesign.md):访问日志为业务唯一真相;Agent 只上报明细与主机读数;收敛「出站/已提供」双字段。
|
||||
* [访问日志 cache_status 明细可见](./20260718-access-log-cache-status.md):上报 `$upstream_cache_status`,明细展示命中/回源/未缓存三态。
|
||||
* [边缘缓存默认 static 策略](./20260718-edge-cache-static-default.md):开启缓存默认仅静态扩展名;存量 url→all。
|
||||
|
||||
## 使用建议
|
||||
|
||||
|
||||
@@ -278,10 +278,10 @@ export function normalizeLimitRate(value: string) {
|
||||
}
|
||||
|
||||
export function validateCacheRules(
|
||||
policy: 'url' | 'suffix' | 'path_prefix' | 'path_exact',
|
||||
policy: 'static' | 'all' | 'url' | 'suffix' | 'path_prefix' | 'path_exact',
|
||||
rules: string[],
|
||||
) {
|
||||
if (policy === 'url') {
|
||||
if (policy === 'static' || policy === 'all' || policy === 'url') {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -335,7 +335,12 @@ export function buildPayloadFromRoute(
|
||||
limit_conn_per_ip: route.limit_conn_per_ip,
|
||||
limit_rate: route.limit_rate,
|
||||
cache_enabled: route.cache_enabled,
|
||||
cache_policy: route.cache_policy || 'url',
|
||||
cache_policy:
|
||||
!route.cache_policy || route.cache_policy === 'url'
|
||||
? route.cache_policy === 'url'
|
||||
? 'all'
|
||||
: 'static'
|
||||
: route.cache_policy,
|
||||
cache_rules: route.cache_rule_list ?? [],
|
||||
custom_headers: route.custom_header_list ?? [],
|
||||
basic_auth_enabled: route.basic_auth_enabled,
|
||||
|
||||
@@ -229,7 +229,7 @@ export function ProxyRouteCreateSheet({
|
||||
limit_conn_per_ip: 0,
|
||||
limit_rate: '',
|
||||
cache_enabled: false,
|
||||
cache_policy: 'url',
|
||||
cache_policy: 'static',
|
||||
cache_rules: [],
|
||||
custom_headers: [],
|
||||
basic_auth_enabled: false,
|
||||
|
||||
@@ -36,7 +36,14 @@ import { SectionShell } from './section-shell';
|
||||
const cacheSchema = z
|
||||
.object({
|
||||
cache_enabled: z.boolean(),
|
||||
cache_policy: z.enum(['url', 'suffix', 'path_prefix', 'path_exact']),
|
||||
cache_policy: z.enum([
|
||||
'static',
|
||||
'all',
|
||||
'url',
|
||||
'suffix',
|
||||
'path_prefix',
|
||||
'path_exact',
|
||||
]),
|
||||
cache_rules_text: z.string(),
|
||||
})
|
||||
.superRefine((value, context) => {
|
||||
@@ -63,6 +70,22 @@ interface CacheSectionProps {
|
||||
onSavingChange?: (saving: boolean) => void;
|
||||
}
|
||||
|
||||
function normalizeCachePolicyValue(policy: string | undefined | null) {
|
||||
const value = (policy || '').trim();
|
||||
if (!value || value === 'static') return 'static';
|
||||
if (value === 'url' || value === 'all') return 'all';
|
||||
if (value === 'suffix' || value === 'path_prefix' || value === 'path_exact') {
|
||||
return value;
|
||||
}
|
||||
return 'static';
|
||||
}
|
||||
|
||||
function needsRulesForPolicy(policy: string) {
|
||||
return (
|
||||
policy === 'suffix' || policy === 'path_prefix' || policy === 'path_exact'
|
||||
);
|
||||
}
|
||||
|
||||
export function CacheSection({
|
||||
route,
|
||||
onRouteUpdate,
|
||||
@@ -78,8 +101,9 @@ export function CacheSection({
|
||||
resolver: zodResolver(cacheSchema),
|
||||
defaultValues: {
|
||||
cache_enabled: route.cache_enabled,
|
||||
cache_policy: (route.cache_policy ||
|
||||
'url') as CacheValues['cache_policy'],
|
||||
cache_policy: normalizeCachePolicyValue(
|
||||
route.cache_policy,
|
||||
) as CacheValues['cache_policy'],
|
||||
cache_rules_text: route.cache_rule_list.join('\n'),
|
||||
},
|
||||
});
|
||||
@@ -87,14 +111,19 @@ export function CacheSection({
|
||||
useEffect(() => {
|
||||
form.reset({
|
||||
cache_enabled: route.cache_enabled,
|
||||
cache_policy: (route.cache_policy ||
|
||||
'url') as CacheValues['cache_policy'],
|
||||
cache_policy: normalizeCachePolicyValue(
|
||||
route.cache_policy,
|
||||
) as CacheValues['cache_policy'],
|
||||
cache_rules_text: route.cache_rule_list.join('\n'),
|
||||
});
|
||||
}, [form, route]);
|
||||
|
||||
const watchedEnabled = form.watch('cache_enabled');
|
||||
const watchedPolicy = form.watch('cache_policy');
|
||||
const needsRules =
|
||||
watchedPolicy === 'suffix' ||
|
||||
watchedPolicy === 'path_prefix' ||
|
||||
watchedPolicy === 'path_exact';
|
||||
|
||||
const rulesHint =
|
||||
watchedPolicy === 'suffix'
|
||||
@@ -103,7 +132,9 @@ export function CacheSection({
|
||||
? '每行一个路径前缀,例如 /assets、/static。'
|
||||
: watchedPolicy === 'path_exact'
|
||||
? '每行一个精确路径,例如 /robots.txt。'
|
||||
: '按 URL 缓存时无需额外规则。';
|
||||
: watchedPolicy === 'static'
|
||||
? '标准静态资源使用内置扩展名列表(不含 HTML),无需填写规则。'
|
||||
: '所有可缓存 GET 无需额外规则(仍会绕过登录态与 Authorization)。';
|
||||
|
||||
const rulesPlaceholder =
|
||||
watchedPolicy === 'suffix'
|
||||
@@ -112,7 +143,7 @@ export function CacheSection({
|
||||
? '/assets\n/static'
|
||||
: watchedPolicy === 'path_exact'
|
||||
? '/robots.txt\n/manifest.json'
|
||||
: '按 URL 缓存时无需额外规则';
|
||||
: '当前策略无需额外规则';
|
||||
|
||||
return (
|
||||
<SectionShell
|
||||
@@ -131,10 +162,11 @@ export function CacheSection({
|
||||
{
|
||||
cache_enabled: values.cache_enabled,
|
||||
cache_policy: values.cache_enabled
|
||||
? values.cache_policy
|
||||
: 'url',
|
||||
? normalizeCachePolicyValue(values.cache_policy)
|
||||
: 'static',
|
||||
cache_rules:
|
||||
values.cache_enabled && values.cache_policy !== 'url'
|
||||
values.cache_enabled &&
|
||||
needsRulesForPolicy(values.cache_policy)
|
||||
? rules
|
||||
: [],
|
||||
},
|
||||
@@ -150,8 +182,8 @@ export function CacheSection({
|
||||
<div className='space-y-0.5'>
|
||||
<FormLabel>启用站点缓存</FormLabel>
|
||||
<FormDescription>
|
||||
系统仍会自动绕过非 GET、带 Authorization 或常见登录态 Cookie
|
||||
的请求。
|
||||
开启后默认仅缓存标准静态扩展名(不含 HTML)。仍会自动绕过非
|
||||
GET、Authorization 与常见登录态 Cookie。
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
@@ -181,12 +213,17 @@ export function CacheSection({
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value='url'>按 URL 缓存</SelectItem>
|
||||
<SelectItem value='suffix'>按后缀缓存</SelectItem>
|
||||
<SelectItem value='path_prefix'>按路径前缀缓存</SelectItem>
|
||||
<SelectItem value='path_exact'>按精确路径缓存</SelectItem>
|
||||
<SelectItem value='static'>标准静态资源(推荐)</SelectItem>
|
||||
<SelectItem value='all'>所有可缓存 GET(高级)</SelectItem>
|
||||
<SelectItem value='suffix'>自定义后缀</SelectItem>
|
||||
<SelectItem value='path_prefix'>路径前缀</SelectItem>
|
||||
<SelectItem value='path_exact'>精确路径</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormDescription>
|
||||
标准静态资源含 css/js/图片/字体/媒体等,默认不缓存 HTML
|
||||
与接口路径。
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
@@ -201,7 +238,7 @@ export function CacheSection({
|
||||
<FormControl>
|
||||
<Textarea
|
||||
className='min-h-32'
|
||||
disabled={!watchedEnabled || watchedPolicy === 'url'}
|
||||
disabled={!watchedEnabled || !needsRules}
|
||||
placeholder={rulesPlaceholder}
|
||||
{...field}
|
||||
/>
|
||||
|
||||
@@ -25,7 +25,9 @@ var proxyHeaderKeyPattern = regexp.MustCompile(`^[A-Za-z0-9_-]+$`)
|
||||
var proxyRouteLimitRatePattern = regexp.MustCompile(`^\d+[kKmM]?$`)
|
||||
|
||||
const (
|
||||
proxyRouteCachePolicyURL = "url"
|
||||
proxyRouteCachePolicyStatic = "static"
|
||||
proxyRouteCachePolicyAll = "all"
|
||||
proxyRouteCachePolicyURL = "url" // legacy alias of all
|
||||
proxyRouteCachePolicySuffix = "suffix"
|
||||
proxyRouteCachePolicyPathPrefix = "path_prefix"
|
||||
proxyRouteCachePolicyPathExact = "path_exact"
|
||||
@@ -438,11 +440,18 @@ func normalizeCachePolicy(enabled bool, raw string) string {
|
||||
if !enabled {
|
||||
return ""
|
||||
}
|
||||
policy := strings.TrimSpace(raw)
|
||||
if policy == "" {
|
||||
return proxyRouteCachePolicyURL
|
||||
policy := strings.TrimSpace(strings.ToLower(raw))
|
||||
switch policy {
|
||||
case "", proxyRouteCachePolicyStatic:
|
||||
return proxyRouteCachePolicyStatic
|
||||
case proxyRouteCachePolicyURL, proxyRouteCachePolicyAll:
|
||||
// Legacy url is normalized to all (full GET allow after security bypass).
|
||||
return proxyRouteCachePolicyAll
|
||||
case proxyRouteCachePolicySuffix, proxyRouteCachePolicyPathPrefix, proxyRouteCachePolicyPathExact:
|
||||
return policy
|
||||
default:
|
||||
return policy
|
||||
}
|
||||
return policy
|
||||
}
|
||||
|
||||
func normalizeCacheRules(enabled bool, rawPolicy string, rules []string) ([]string, error) {
|
||||
@@ -451,7 +460,7 @@ func normalizeCacheRules(enabled bool, rawPolicy string, rules []string) ([]stri
|
||||
}
|
||||
policy := normalizeCachePolicy(enabled, rawPolicy)
|
||||
switch policy {
|
||||
case proxyRouteCachePolicyURL:
|
||||
case proxyRouteCachePolicyStatic, proxyRouteCachePolicyAll, proxyRouteCachePolicyURL:
|
||||
return []string{}, nil
|
||||
case proxyRouteCachePolicySuffix:
|
||||
return normalizeCacheSuffixRules(rules)
|
||||
|
||||
@@ -80,3 +80,23 @@ func TestCreateProxyRouteHTTPSRequiresCoveringCertificate(t *testing.T) {
|
||||
_, err := CreateProxyRoute(ctx, Input{SiteName: "api", ZoneDomainIDs: []uint{domain.ID}, OriginURL: "http://origin.example.com:8080", EnableHTTPS: true})
|
||||
require.EqualError(t, err, errProxyRouteCertRequired)
|
||||
}
|
||||
|
||||
func TestNormalizeCachePolicyDefaultsAndLegacy(t *testing.T) {
|
||||
assert.Equal(t, "", normalizeCachePolicy(false, "static"))
|
||||
assert.Equal(t, proxyRouteCachePolicyStatic, normalizeCachePolicy(true, ""))
|
||||
assert.Equal(t, proxyRouteCachePolicyStatic, normalizeCachePolicy(true, "static"))
|
||||
assert.Equal(t, proxyRouteCachePolicyAll, normalizeCachePolicy(true, "url"))
|
||||
assert.Equal(t, proxyRouteCachePolicyAll, normalizeCachePolicy(true, "all"))
|
||||
assert.Equal(t, proxyRouteCachePolicySuffix, normalizeCachePolicy(true, "suffix"))
|
||||
|
||||
rules, err := normalizeCacheRules(true, "url", []string{"css"})
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, rules)
|
||||
|
||||
rules, err = normalizeCacheRules(true, "static", nil)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, rules)
|
||||
|
||||
_, err = normalizeCacheRules(true, "suffix", nil)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
@@ -479,15 +479,35 @@ func renderRouteLimitBlock(limitConfig routeLimitConfig) string {
|
||||
}
|
||||
|
||||
func renderRouteCachePolicyCondition(cacheConfig routeCacheConfig) string {
|
||||
switch cacheConfig.Policy {
|
||||
policy := normalizeRenderCachePolicy(cacheConfig.Policy)
|
||||
switch policy {
|
||||
case cachePolicyStatic:
|
||||
return fmt.Sprintf(" if ($uri !~* %s) {\n set $openflare_skip_cache 1;\n }\n", quoteNginxStringLiteral(buildSuffixMatchPattern(DefaultStaticCacheExtensions)))
|
||||
case cachePolicySuffix:
|
||||
return fmt.Sprintf(" if ($uri !~* %s) {\n set $openflare_skip_cache 1;\n }\n", quoteNginxStringLiteral(buildSuffixMatchPattern(cacheConfig.Rules)))
|
||||
case cachePolicyPathPrefix:
|
||||
return fmt.Sprintf(" if ($uri !~ %s) {\n set $openflare_skip_cache 1;\n }\n", quoteNginxStringLiteral(buildPathPrefixMatchPattern(cacheConfig.Rules)))
|
||||
case cachePolicyPathExact:
|
||||
return fmt.Sprintf(" if ($uri !~ %s) {\n set $openflare_skip_cache 1;\n }\n", quoteNginxStringLiteral(buildPathExactMatchPattern(cacheConfig.Rules)))
|
||||
default:
|
||||
case cachePolicyAll, cachePolicyURL:
|
||||
return ""
|
||||
default:
|
||||
// Unknown policy: treat as static for safety (do not cache everything).
|
||||
return fmt.Sprintf(" if ($uri !~* %s) {\n set $openflare_skip_cache 1;\n }\n", quoteNginxStringLiteral(buildSuffixMatchPattern(DefaultStaticCacheExtensions)))
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeRenderCachePolicy(raw string) string {
|
||||
policy := strings.TrimSpace(strings.ToLower(raw))
|
||||
switch policy {
|
||||
case "", cachePolicyStatic:
|
||||
return cachePolicyStatic
|
||||
case cachePolicyURL, cachePolicyAll:
|
||||
return cachePolicyAll
|
||||
case cachePolicySuffix, cachePolicyPathPrefix, cachePolicyPathExact:
|
||||
return policy
|
||||
default:
|
||||
return policy
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -398,3 +398,46 @@ func TestRenderRouteConfigPagesWithSPAFallbackServesRoot(t *testing.T) {
|
||||
t.Fatalf("expected spa fallback try_files in location /, got:\n%s", routeConfig)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderRouteCachePolicyConditionStaticDefault(t *testing.T) {
|
||||
staticBlock := renderRouteCachePolicyCondition(routeCacheConfig{Enabled: true, Policy: "static"})
|
||||
if staticBlock == "" {
|
||||
t.Fatal("static policy should emit a path condition")
|
||||
}
|
||||
if !strings.Contains(staticBlock, "css") || !strings.Contains(staticBlock, "woff2") {
|
||||
t.Fatalf("static policy should include default extensions, got:\n%s", staticBlock)
|
||||
}
|
||||
if strings.Contains(staticBlock, "html") {
|
||||
t.Fatalf("static policy must not include html, got:\n%s", staticBlock)
|
||||
}
|
||||
|
||||
emptyPolicy := renderRouteCachePolicyCondition(routeCacheConfig{Enabled: true, Policy: ""})
|
||||
if emptyPolicy == "" {
|
||||
t.Fatal("empty policy should default to static condition")
|
||||
}
|
||||
|
||||
allBlock := renderRouteCachePolicyCondition(routeCacheConfig{Enabled: true, Policy: "all"})
|
||||
if allBlock != "" {
|
||||
t.Fatalf("all policy should not add path condition, got %q", allBlock)
|
||||
}
|
||||
urlBlock := renderRouteCachePolicyCondition(routeCacheConfig{Enabled: true, Policy: "url"})
|
||||
if urlBlock != "" {
|
||||
t.Fatalf("legacy url policy should map to all, got %q", urlBlock)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderRouteCacheBlockIncludesStaticWhenEnabled(t *testing.T) {
|
||||
block := renderRouteCacheBlock(
|
||||
routeCacheConfig{Enabled: true, Policy: "static"},
|
||||
ConfigSnapshot{CacheEnabled: true},
|
||||
)
|
||||
if !strings.Contains(block, "proxy_cache openflare_cache") {
|
||||
t.Fatalf("expected proxy_cache, got:\n%s", block)
|
||||
}
|
||||
if !strings.Contains(block, "\\.(?:") {
|
||||
t.Fatalf("expected static suffix pattern, got:\n%s", block)
|
||||
}
|
||||
if !strings.Contains(block, "request_method != GET") {
|
||||
t.Fatalf("expected security bypass for non-GET, got:\n%s", block)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,9 @@ const (
|
||||
)
|
||||
|
||||
const (
|
||||
cachePolicyStatic = "static"
|
||||
cachePolicyAll = "all"
|
||||
cachePolicyURL = "url" // legacy alias of all
|
||||
cachePolicySuffix = "suffix"
|
||||
cachePolicyPathPrefix = "path_prefix"
|
||||
cachePolicyPathExact = "path_exact"
|
||||
@@ -33,6 +36,17 @@ const (
|
||||
anubisAPIPrefix = "/.within.website/x/cmd/anubis/api/"
|
||||
)
|
||||
|
||||
// DefaultStaticCacheExtensions is the built-in suffix allowlist for cache_policy=static.
|
||||
// HTML is intentionally excluded (Cloudflare-like default).
|
||||
var DefaultStaticCacheExtensions = []string{
|
||||
"css", "js", "mjs", "map", "json",
|
||||
"ico", "cur", "gif", "jpg", "jpeg", "png", "webp", "avif", "svg", "svgz",
|
||||
"ttf", "otf", "woff", "woff2", "eot",
|
||||
"mp3", "mp4", "webm", "ogg", "flac",
|
||||
"wasm", "pdf",
|
||||
"zip", "7z", "gz", "tar",
|
||||
}
|
||||
|
||||
// OpenFlareRuntimeUser is the dedicated service account shared by the agent
|
||||
// process and OpenResty worker processes.
|
||||
const OpenFlareRuntimeUser = "openflare"
|
||||
|
||||
Reference in New Issue
Block a user