mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-06 15:46:37 +08:00
[功能] 添加缓存策略支持,优化代理路由配置和验证逻辑
This commit is contained in:
@@ -84,6 +84,7 @@ OpenResty 性能参数与缓存参数继续统一保存在 `Option` 表。当前
|
|||||||
这类参数必须以结构化方式校验、保存并参与版本渲染。
|
这类参数必须以结构化方式校验、保存并参与版本渲染。
|
||||||
|
|
||||||
* `OpenRestyResolvers` 由管理端性能页面维护,支持填写多个 DNS 服务器 IP;留空时不额外生成 `resolver` 指令。
|
* `OpenRestyResolvers` 由管理端性能页面维护,支持填写多个 DNS 服务器 IP;留空时不额外生成 `resolver` 指令。
|
||||||
|
* `OpenRestyCacheEnabled` 用于启用缓存基础设施与全局默认参数;实际是否缓存、按 URL / 后缀 / 路径等命中策略由各条 `proxy_routes` 单独决定,不再默认对所有规则开启缓存。
|
||||||
### 1.5 前端构建环境变量
|
### 1.5 前端构建环境变量
|
||||||
|
|
||||||
| 环境变量 | 作用 | 默认值 |
|
| 环境变量 | 作用 | 默认值 |
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ type ProxyRoute struct {
|
|||||||
EnableHTTPS bool `json:"enable_https" gorm:"column:enable_https;not null;default:false"`
|
EnableHTTPS bool `json:"enable_https" gorm:"column:enable_https;not null;default:false"`
|
||||||
CertID *uint `json:"cert_id"`
|
CertID *uint `json:"cert_id"`
|
||||||
RedirectHTTP bool `json:"redirect_http" gorm:"not null;default:false"`
|
RedirectHTTP bool `json:"redirect_http" gorm:"not null;default:false"`
|
||||||
|
CacheEnabled bool `json:"cache_enabled" gorm:"not null;default:false"`
|
||||||
|
CachePolicy string `json:"cache_policy" gorm:"size:32;not null;default:''"`
|
||||||
|
CacheRules string `json:"cache_rules" gorm:"type:text;not null;default:'[]'"`
|
||||||
CustomHeaders string `json:"custom_headers" gorm:"type:text;not null;default:'[]'"`
|
CustomHeaders string `json:"custom_headers" gorm:"type:text;not null;default:'[]'"`
|
||||||
Remark string `json:"remark" gorm:"size:255"`
|
Remark string `json:"remark" gorm:"size:255"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
@@ -46,6 +49,9 @@ func (route *ProxyRoute) Update() error {
|
|||||||
"enable_https": route.EnableHTTPS,
|
"enable_https": route.EnableHTTPS,
|
||||||
"cert_id": route.CertID,
|
"cert_id": route.CertID,
|
||||||
"redirect_http": route.RedirectHTTP,
|
"redirect_http": route.RedirectHTTP,
|
||||||
|
"cache_enabled": route.CacheEnabled,
|
||||||
|
"cache_policy": route.CachePolicy,
|
||||||
|
"cache_rules": route.CacheRules,
|
||||||
"custom_headers": route.CustomHeaders,
|
"custom_headers": route.CustomHeaders,
|
||||||
"remark": route.Remark,
|
"remark": route.Remark,
|
||||||
}).Error
|
}).Error
|
||||||
|
|||||||
@@ -44,11 +44,14 @@ func TestPhase1PublishLifecycle(t *testing.T) {
|
|||||||
token := prepareRootToken(t)
|
token := prepareRootToken(t)
|
||||||
|
|
||||||
createBody := map[string]any{
|
createBody := map[string]any{
|
||||||
"domain": "app.example.com",
|
"domain": "app.example.com",
|
||||||
"origin_url": "https://origin-a.internal",
|
"origin_url": "https://origin-a.internal",
|
||||||
"origin_host": "origin-a.internal",
|
"origin_host": "origin-a.internal",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"remark": "primary route",
|
"cache_enabled": true,
|
||||||
|
"cache_policy": "path_prefix",
|
||||||
|
"cache_rules": []string{"/assets", "/static"},
|
||||||
|
"remark": "primary route",
|
||||||
}
|
}
|
||||||
resp := performJSONRequest(t, engine, token, http.MethodPost, "/api/proxy-routes/", createBody)
|
resp := performJSONRequest(t, engine, token, http.MethodPost, "/api/proxy-routes/", createBody)
|
||||||
var createdRoute model.ProxyRoute
|
var createdRoute model.ProxyRoute
|
||||||
@@ -59,6 +62,12 @@ func TestPhase1PublishLifecycle(t *testing.T) {
|
|||||||
if createdRoute.OriginHost != "origin-a.internal" {
|
if createdRoute.OriginHost != "origin-a.internal" {
|
||||||
t.Fatalf("unexpected created route origin host: %s", createdRoute.OriginHost)
|
t.Fatalf("unexpected created route origin host: %s", createdRoute.OriginHost)
|
||||||
}
|
}
|
||||||
|
if !createdRoute.CacheEnabled || createdRoute.CachePolicy != "path_prefix" {
|
||||||
|
t.Fatalf("expected route cache settings to persist, got %+v", createdRoute)
|
||||||
|
}
|
||||||
|
if !strings.Contains(createdRoute.CacheRules, "/assets") {
|
||||||
|
t.Fatalf("expected route cache rules to persist, got %s", createdRoute.CacheRules)
|
||||||
|
}
|
||||||
|
|
||||||
resp = performJSONRequest(t, engine, token, http.MethodGet, "/api/proxy-routes/", nil)
|
resp = performJSONRequest(t, engine, token, http.MethodGet, "/api/proxy-routes/", nil)
|
||||||
var routes []model.ProxyRoute
|
var routes []model.ProxyRoute
|
||||||
@@ -103,11 +112,14 @@ func TestPhase1PublishLifecycle(t *testing.T) {
|
|||||||
initialRendered := version1.RenderedConfig
|
initialRendered := version1.RenderedConfig
|
||||||
|
|
||||||
updateBody := map[string]any{
|
updateBody := map[string]any{
|
||||||
"domain": "app.example.com",
|
"domain": "app.example.com",
|
||||||
"origin_url": "https://origin-b.internal",
|
"origin_url": "https://origin-b.internal",
|
||||||
"origin_host": "origin-b.internal",
|
"origin_host": "origin-b.internal",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"remark": "updated route",
|
"cache_enabled": true,
|
||||||
|
"cache_policy": "path_exact",
|
||||||
|
"cache_rules": []string{"/robots.txt"},
|
||||||
|
"remark": "updated route",
|
||||||
}
|
}
|
||||||
routePath := "/api/proxy-routes/" + toString(createdRoute.ID)
|
routePath := "/api/proxy-routes/" + toString(createdRoute.ID)
|
||||||
resp = performJSONRequest(t, engine, token, http.MethodPost, routePath+"/update", updateBody)
|
resp = performJSONRequest(t, engine, token, http.MethodPost, routePath+"/update", updateBody)
|
||||||
@@ -118,6 +130,9 @@ func TestPhase1PublishLifecycle(t *testing.T) {
|
|||||||
if createdRoute.OriginHost != "origin-b.internal" {
|
if createdRoute.OriginHost != "origin-b.internal" {
|
||||||
t.Fatalf("unexpected updated route origin host: %s", createdRoute.OriginHost)
|
t.Fatalf("unexpected updated route origin host: %s", createdRoute.OriginHost)
|
||||||
}
|
}
|
||||||
|
if createdRoute.CachePolicy != "path_exact" || !strings.Contains(createdRoute.CacheRules, "/robots.txt") {
|
||||||
|
t.Fatalf("expected updated route cache rules to persist, got %+v", createdRoute)
|
||||||
|
}
|
||||||
|
|
||||||
resp = performJSONRequest(t, engine, token, http.MethodPost, "/api/config-versions/publish", nil)
|
resp = performJSONRequest(t, engine, token, http.MethodPost, "/api/config-versions/publish", nil)
|
||||||
var version2 model.ConfigVersion
|
var version2 model.ConfigVersion
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"openflare/common"
|
"openflare/common"
|
||||||
"openflare/model"
|
"openflare/model"
|
||||||
|
"regexp"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -65,10 +66,19 @@ type snapshotRoute struct {
|
|||||||
EnableHTTPS bool `json:"enable_https"`
|
EnableHTTPS bool `json:"enable_https"`
|
||||||
CertID *uint `json:"cert_id,omitempty"`
|
CertID *uint `json:"cert_id,omitempty"`
|
||||||
RedirectHTTP bool `json:"redirect_http"`
|
RedirectHTTP bool `json:"redirect_http"`
|
||||||
|
CacheEnabled bool `json:"cache_enabled"`
|
||||||
|
CachePolicy string `json:"cache_policy,omitempty"`
|
||||||
|
CacheRules []string `json:"cache_rules,omitempty"`
|
||||||
CustomHeaders []ProxyRouteCustomHeaderInput `json:"custom_headers,omitempty"`
|
CustomHeaders []ProxyRouteCustomHeaderInput `json:"custom_headers,omitempty"`
|
||||||
Remark string `json:"remark,omitempty"`
|
Remark string `json:"remark,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type routeCacheConfig struct {
|
||||||
|
Enabled bool
|
||||||
|
Policy string
|
||||||
|
Rules []string
|
||||||
|
}
|
||||||
|
|
||||||
type openRestyConfigSnapshot struct {
|
type openRestyConfigSnapshot struct {
|
||||||
WorkerProcesses string `json:"worker_processes"`
|
WorkerProcesses string `json:"worker_processes"`
|
||||||
WorkerConnections int `json:"worker_connections"`
|
WorkerConnections int `json:"worker_connections"`
|
||||||
@@ -391,6 +401,10 @@ func buildSnapshotRoutes(routes []*model.ProxyRoute) ([]snapshotRoute, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("路由 %s 自定义请求头无效", route.Domain)
|
return nil, fmt.Errorf("路由 %s 自定义请求头无效", route.Domain)
|
||||||
}
|
}
|
||||||
|
cacheRules, err := decodeStoredCacheRules(route.CacheRules)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("路由 %s 缓存规则无效", route.Domain)
|
||||||
|
}
|
||||||
items = append(items, snapshotRoute{
|
items = append(items, snapshotRoute{
|
||||||
Domain: route.Domain,
|
Domain: route.Domain,
|
||||||
OriginURL: route.OriginURL,
|
OriginURL: route.OriginURL,
|
||||||
@@ -399,6 +413,9 @@ func buildSnapshotRoutes(routes []*model.ProxyRoute) ([]snapshotRoute, error) {
|
|||||||
EnableHTTPS: route.EnableHTTPS,
|
EnableHTTPS: route.EnableHTTPS,
|
||||||
CertID: route.CertID,
|
CertID: route.CertID,
|
||||||
RedirectHTTP: route.RedirectHTTP,
|
RedirectHTTP: route.RedirectHTTP,
|
||||||
|
CacheEnabled: route.CacheEnabled,
|
||||||
|
CachePolicy: route.CachePolicy,
|
||||||
|
CacheRules: cacheRules,
|
||||||
CustomHeaders: customHeaders,
|
CustomHeaders: customHeaders,
|
||||||
Remark: route.Remark,
|
Remark: route.Remark,
|
||||||
})
|
})
|
||||||
@@ -435,14 +452,27 @@ func normalizeSnapshotRoutes(routes []snapshotRoute) []snapshotRoute {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
routes[index].CustomHeaders = normalizedHeaders
|
routes[index].CustomHeaders = normalizedHeaders
|
||||||
}
|
}
|
||||||
|
normalizedCacheRules, err := normalizeCacheRules(routes[index].CacheEnabled, routes[index].CachePolicy, routes[index].CacheRules)
|
||||||
|
if err == nil {
|
||||||
|
routes[index].CachePolicy = normalizeCachePolicy(routes[index].CacheEnabled, routes[index].CachePolicy)
|
||||||
|
routes[index].CacheRules = normalizedCacheRules
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return routes
|
return routes
|
||||||
}
|
}
|
||||||
|
|
||||||
func snapshotRouteConfigEqual(left snapshotRoute, right snapshotRoute) bool {
|
func snapshotRouteConfigEqual(left snapshotRoute, right snapshotRoute) bool {
|
||||||
if left.Domain != right.Domain || left.OriginURL != right.OriginURL || left.OriginHost != right.OriginHost || left.EnableHTTPS != right.EnableHTTPS || left.RedirectHTTP != right.RedirectHTTP || !uintPointerEqual(left.CertID, right.CertID) {
|
if left.Domain != right.Domain || left.OriginURL != right.OriginURL || left.OriginHost != right.OriginHost || left.EnableHTTPS != right.EnableHTTPS || left.RedirectHTTP != right.RedirectHTTP || left.CacheEnabled != right.CacheEnabled || left.CachePolicy != right.CachePolicy || !uintPointerEqual(left.CertID, right.CertID) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if len(left.CacheRules) != len(right.CacheRules) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for index := range left.CacheRules {
|
||||||
|
if left.CacheRules[index] != right.CacheRules[index] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
if len(left.CustomHeaders) != len(right.CustomHeaders) {
|
if len(left.CustomHeaders) != len(right.CustomHeaders) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -611,8 +641,17 @@ func renderRouteConfig(routes []*model.ProxyRoute, cfg openRestyConfigSnapshot)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil, fmt.Errorf("路由 %s 自定义请求头无效", route.Domain)
|
return "", nil, fmt.Errorf("路由 %s 自定义请求头无效", route.Domain)
|
||||||
}
|
}
|
||||||
|
cacheRules, err := decodeStoredCacheRules(route.CacheRules)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, fmt.Errorf("路由 %s 缓存规则无效", route.Domain)
|
||||||
|
}
|
||||||
|
cacheConfig := routeCacheConfig{
|
||||||
|
Enabled: route.CacheEnabled,
|
||||||
|
Policy: route.CachePolicy,
|
||||||
|
Rules: cacheRules,
|
||||||
|
}
|
||||||
if !route.EnableHTTPS {
|
if !route.EnableHTTPS {
|
||||||
builder.WriteString(renderHTTPProxyServer(route.Domain, route.OriginURL, route.OriginHost, customHeaders, cfg))
|
builder.WriteString(renderHTTPProxyServer(route.Domain, route.OriginURL, route.OriginHost, customHeaders, cacheConfig, cfg))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if route.CertID == nil || *route.CertID == 0 {
|
if route.CertID == nil || *route.CertID == 0 {
|
||||||
@@ -629,9 +668,9 @@ func renderRouteConfig(routes []*model.ProxyRoute, cfg openRestyConfigSnapshot)
|
|||||||
if route.RedirectHTTP {
|
if route.RedirectHTTP {
|
||||||
builder.WriteString(renderHTTPRedirectServer(route.Domain))
|
builder.WriteString(renderHTTPRedirectServer(route.Domain))
|
||||||
} else {
|
} else {
|
||||||
builder.WriteString(renderHTTPProxyServer(route.Domain, route.OriginURL, route.OriginHost, customHeaders, cfg))
|
builder.WriteString(renderHTTPProxyServer(route.Domain, route.OriginURL, route.OriginHost, customHeaders, cacheConfig, cfg))
|
||||||
}
|
}
|
||||||
builder.WriteString(renderHTTPSServer(route.Domain, route.OriginURL, route.OriginHost, certificate.ID, customHeaders, cfg))
|
builder.WriteString(renderHTTPSServer(route.Domain, route.OriginURL, route.OriginHost, certificate.ID, customHeaders, cacheConfig, cfg))
|
||||||
}
|
}
|
||||||
return builder.String(), dedupeSupportFiles(supportFiles), nil
|
return builder.String(), dedupeSupportFiles(supportFiles), nil
|
||||||
}
|
}
|
||||||
@@ -768,18 +807,18 @@ func nextVersionNumber(now time.Time) (string, error) {
|
|||||||
return fmt.Sprintf("%s-%03d", prefix, count+1), nil
|
return fmt.Sprintf("%s-%03d", prefix, count+1), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderHTTPProxyServer(domain string, originURL string, originHost string, customHeaders []ProxyRouteCustomHeaderInput, cfg openRestyConfigSnapshot) string {
|
func renderHTTPProxyServer(domain string, originURL string, originHost string, customHeaders []ProxyRouteCustomHeaderInput, cacheConfig routeCacheConfig, cfg openRestyConfigSnapshot) string {
|
||||||
return fmt.Sprintf("server {\n listen 80;\n server_name %s;\n\n location / {\n%s%s }\n}\n\n", domain, renderProxyHeaderBlock(originURL, originHost, customHeaders), renderProxyPassBlock(originURL, cfg))
|
return fmt.Sprintf("server {\n listen 80;\n server_name %s;\n\n location / {\n%s%s%s }\n}\n\n", domain, renderProxyHeaderBlock(originURL, originHost, customHeaders), renderRouteCacheBlock(cacheConfig, cfg), renderProxyPassBlock(originURL, cfg))
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderHTTPRedirectServer(domain string) string {
|
func renderHTTPRedirectServer(domain string) string {
|
||||||
return fmt.Sprintf("server {\n listen 80;\n server_name %s;\n\n return 301 https://$host$request_uri;\n}\n\n", domain)
|
return fmt.Sprintf("server {\n listen 80;\n server_name %s;\n\n return 301 https://$host$request_uri;\n}\n\n", domain)
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderHTTPSServer(domain string, originURL string, originHost string, certificateID uint, customHeaders []ProxyRouteCustomHeaderInput, cfg openRestyConfigSnapshot) string {
|
func renderHTTPSServer(domain string, originURL string, originHost string, certificateID uint, customHeaders []ProxyRouteCustomHeaderInput, cacheConfig routeCacheConfig, cfg openRestyConfigSnapshot) string {
|
||||||
certPath := fmt.Sprintf("%s/%s", nginxCertDirPlaceholder, certificateCertFileName(certificateID))
|
certPath := fmt.Sprintf("%s/%s", nginxCertDirPlaceholder, certificateCertFileName(certificateID))
|
||||||
keyPath := fmt.Sprintf("%s/%s", nginxCertDirPlaceholder, certificateKeyFileName(certificateID))
|
keyPath := fmt.Sprintf("%s/%s", nginxCertDirPlaceholder, certificateKeyFileName(certificateID))
|
||||||
return fmt.Sprintf("server {\n listen 443 ssl http2;\n server_name %s;\n ssl_certificate %s;\n ssl_certificate_key %s;\n\n location / {\n%s%s }\n}\n\n", domain, certPath, keyPath, renderProxyHeaderBlock(originURL, originHost, customHeaders), renderProxyPassBlock(originURL, cfg))
|
return fmt.Sprintf("server {\n listen 443 ssl http2;\n server_name %s;\n ssl_certificate %s;\n ssl_certificate_key %s;\n\n location / {\n%s%s%s }\n}\n\n", domain, certPath, keyPath, renderProxyHeaderBlock(originURL, originHost, customHeaders), renderRouteCacheBlock(cacheConfig, cfg), renderProxyPassBlock(originURL, cfg))
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderConnectionUpgradeMap() string {
|
func renderConnectionUpgradeMap() string {
|
||||||
@@ -812,12 +851,74 @@ func renderProxyHeaderBlock(originURL string, originHost string, customHeaders [
|
|||||||
for _, header := range customHeaders {
|
for _, header := range customHeaders {
|
||||||
builder.WriteString(fmt.Sprintf(" proxy_set_header %s %s;\n", header.Key, quoteNginxHeaderValue(header.Value)))
|
builder.WriteString(fmt.Sprintf(" proxy_set_header %s %s;\n", header.Key, quoteNginxHeaderValue(header.Value)))
|
||||||
}
|
}
|
||||||
if common.OpenRestyCacheEnabled {
|
|
||||||
builder.WriteString(" proxy_cache openflare_cache;\n")
|
|
||||||
}
|
|
||||||
return builder.String()
|
return builder.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func renderRouteCacheBlock(cacheConfig routeCacheConfig, cfg openRestyConfigSnapshot) string {
|
||||||
|
if !cfg.CacheEnabled || !cacheConfig.Enabled {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
var builder strings.Builder
|
||||||
|
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 ($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 policyCondition := renderRouteCachePolicyCondition(cacheConfig); policyCondition != "" {
|
||||||
|
builder.WriteString(policyCondition)
|
||||||
|
}
|
||||||
|
builder.WriteString(" proxy_cache openflare_cache;\n")
|
||||||
|
builder.WriteString(" proxy_cache_methods GET;\n")
|
||||||
|
builder.WriteString(" proxy_cache_bypass $openflare_skip_cache;\n")
|
||||||
|
builder.WriteString(" proxy_no_cache $openflare_skip_cache;\n")
|
||||||
|
return builder.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderRouteCachePolicyCondition(cacheConfig routeCacheConfig) string {
|
||||||
|
switch cacheConfig.Policy {
|
||||||
|
case proxyRouteCachePolicySuffix:
|
||||||
|
return fmt.Sprintf(" if ($uri !~* %s) {\n set $openflare_skip_cache 1;\n }\n", quoteNginxStringLiteral(buildSuffixMatchPattern(cacheConfig.Rules)))
|
||||||
|
case proxyRouteCachePolicyPathPrefix:
|
||||||
|
return fmt.Sprintf(" if ($uri !~ %s) {\n set $openflare_skip_cache 1;\n }\n", quoteNginxStringLiteral(buildPathPrefixMatchPattern(cacheConfig.Rules)))
|
||||||
|
case proxyRouteCachePolicyPathExact:
|
||||||
|
return fmt.Sprintf(" if ($uri !~ %s) {\n set $openflare_skip_cache 1;\n }\n", quoteNginxStringLiteral(buildPathExactMatchPattern(cacheConfig.Rules)))
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildSuffixMatchPattern(rules []string) string {
|
||||||
|
parts := make([]string, 0, len(rules))
|
||||||
|
for _, rule := range rules {
|
||||||
|
parts = append(parts, regexp.QuoteMeta(rule))
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("\\.(?:%s)$", strings.Join(parts, "|"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildPathPrefixMatchPattern(rules []string) string {
|
||||||
|
parts := make([]string, 0, len(rules))
|
||||||
|
for _, rule := range rules {
|
||||||
|
trimmed := strings.TrimRight(rule, "/")
|
||||||
|
if trimmed == "" {
|
||||||
|
trimmed = "/"
|
||||||
|
}
|
||||||
|
if trimmed == "/" {
|
||||||
|
parts = append(parts, "/")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
parts = append(parts, fmt.Sprintf("%s(?:/|$)", regexp.QuoteMeta(trimmed)))
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("^(?:%s)", strings.Join(parts, "|"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildPathExactMatchPattern(rules []string) string {
|
||||||
|
parts := make([]string, 0, len(rules))
|
||||||
|
for _, rule := range rules {
|
||||||
|
parts = append(parts, regexp.QuoteMeta(rule))
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("^(?:%s)$", strings.Join(parts, "|"))
|
||||||
|
}
|
||||||
|
|
||||||
func renderProxyPassBlock(originURL string, cfg openRestyConfigSnapshot) string {
|
func renderProxyPassBlock(originURL string, cfg openRestyConfigSnapshot) string {
|
||||||
parsed, err := url.Parse(originURL)
|
parsed, err := url.Parse(originURL)
|
||||||
if err != nil || parsed.Host == "" || parsed.Scheme == "" {
|
if err != nil || parsed.Host == "" || parsed.Scheme == "" {
|
||||||
|
|||||||
@@ -149,6 +149,86 @@ func TestPublishConfigVersionRendersCustomHeaders(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCreateProxyRouteRejectsCachePolicyWithoutRules(t *testing.T) {
|
||||||
|
setupServiceTestDB(t)
|
||||||
|
|
||||||
|
_, err := CreateProxyRoute(ProxyRouteInput{
|
||||||
|
Domain: "cache.example.com",
|
||||||
|
OriginURL: "https://origin.internal",
|
||||||
|
Enabled: true,
|
||||||
|
CacheEnabled: true,
|
||||||
|
CachePolicy: proxyRouteCachePolicySuffix,
|
||||||
|
})
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "至少填写一个后缀") {
|
||||||
|
t.Fatalf("expected cache rule validation error, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPublishConfigVersionRendersRouteLevelCachePolicy(t *testing.T) {
|
||||||
|
setupServiceTestDB(t)
|
||||||
|
if err := model.UpdateOption("OpenRestyCacheEnabled", "true"); err != nil {
|
||||||
|
t.Fatalf("UpdateOption OpenRestyCacheEnabled failed: %v", err)
|
||||||
|
}
|
||||||
|
if err := model.UpdateOption("OpenRestyCachePath", "/var/cache/openresty/openflare"); err != nil {
|
||||||
|
t.Fatalf("UpdateOption OpenRestyCachePath failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := CreateProxyRoute(ProxyRouteInput{
|
||||||
|
Domain: "static.example.com",
|
||||||
|
OriginURL: "https://origin.internal",
|
||||||
|
Enabled: true,
|
||||||
|
CacheEnabled: true,
|
||||||
|
CachePolicy: proxyRouteCachePolicySuffix,
|
||||||
|
CacheRules: []string{"jpg", ".css", "js"},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreateProxyRoute cached failed: %v", err)
|
||||||
|
}
|
||||||
|
_, err = CreateProxyRoute(ProxyRouteInput{
|
||||||
|
Domain: "nocache.example.com",
|
||||||
|
OriginURL: "https://origin.internal",
|
||||||
|
Enabled: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreateProxyRoute uncached failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := PublishConfigVersion("root")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("PublishConfigVersion failed: %v", err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(result.Version.MainConfig, "proxy_cache_path /var/cache/openresty/openflare") {
|
||||||
|
t.Fatal("expected main config to include cache zone when cache infra is enabled")
|
||||||
|
}
|
||||||
|
if !strings.Contains(result.Version.RenderedConfig, "proxy_cache_methods GET;") {
|
||||||
|
t.Fatal("expected rendered config to only cache GET requests")
|
||||||
|
}
|
||||||
|
if !strings.Contains(result.Version.RenderedConfig, "proxy_cache_bypass $openflare_skip_cache;") {
|
||||||
|
t.Fatal("expected rendered config to bypass cache when request is unsafe")
|
||||||
|
}
|
||||||
|
if !strings.Contains(result.Version.RenderedConfig, "proxy_no_cache $openflare_skip_cache;") {
|
||||||
|
t.Fatal("expected rendered config to avoid storing unsafe requests in cache")
|
||||||
|
}
|
||||||
|
if !strings.Contains(result.Version.RenderedConfig, "if ($http_authorization != \"\")") {
|
||||||
|
t.Fatal("expected rendered config to bypass authenticated requests")
|
||||||
|
}
|
||||||
|
if !strings.Contains(result.Version.RenderedConfig, "if ($request_method != GET)") {
|
||||||
|
t.Fatal("expected rendered config to bypass non-GET requests")
|
||||||
|
}
|
||||||
|
if !strings.Contains(result.Version.RenderedConfig, "if ($uri !~* \"\\\\.(?:jpg|css|js)$\")") {
|
||||||
|
t.Fatal("expected rendered config to render suffix cache matching rule")
|
||||||
|
}
|
||||||
|
if strings.Count(result.Version.RenderedConfig, "proxy_cache openflare_cache;") != 1 {
|
||||||
|
t.Fatal("expected only cache-enabled route to include proxy_cache directive")
|
||||||
|
}
|
||||||
|
if !strings.Contains(result.Version.SnapshotJSON, `"cache_enabled":true`) {
|
||||||
|
t.Fatal("expected snapshot to include route cache toggle")
|
||||||
|
}
|
||||||
|
if !strings.Contains(result.Version.SnapshotJSON, `"cache_policy":"suffix"`) {
|
||||||
|
t.Fatal("expected snapshot to include route cache policy")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestPublishConfigVersionOverridesOriginHostHeader(t *testing.T) {
|
func TestPublishConfigVersionOverridesOriginHostHeader(t *testing.T) {
|
||||||
setupServiceTestDB(t)
|
setupServiceTestDB(t)
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,13 @@ import (
|
|||||||
|
|
||||||
var proxyHeaderKeyPattern = regexp.MustCompile(`^[A-Za-z0-9_-]+$`)
|
var proxyHeaderKeyPattern = regexp.MustCompile(`^[A-Za-z0-9_-]+$`)
|
||||||
|
|
||||||
|
const (
|
||||||
|
proxyRouteCachePolicyURL = "url"
|
||||||
|
proxyRouteCachePolicySuffix = "suffix"
|
||||||
|
proxyRouteCachePolicyPathPrefix = "path_prefix"
|
||||||
|
proxyRouteCachePolicyPathExact = "path_exact"
|
||||||
|
)
|
||||||
|
|
||||||
type ProxyRouteCustomHeaderInput struct {
|
type ProxyRouteCustomHeaderInput struct {
|
||||||
Key string `json:"key"`
|
Key string `json:"key"`
|
||||||
Value string `json:"value"`
|
Value string `json:"value"`
|
||||||
@@ -24,6 +31,9 @@ type ProxyRouteInput struct {
|
|||||||
EnableHTTPS bool `json:"enable_https"`
|
EnableHTTPS bool `json:"enable_https"`
|
||||||
CertID *uint `json:"cert_id"`
|
CertID *uint `json:"cert_id"`
|
||||||
RedirectHTTP bool `json:"redirect_http"`
|
RedirectHTTP bool `json:"redirect_http"`
|
||||||
|
CacheEnabled bool `json:"cache_enabled"`
|
||||||
|
CachePolicy string `json:"cache_policy"`
|
||||||
|
CacheRules []string `json:"cache_rules"`
|
||||||
CustomHeaders []ProxyRouteCustomHeaderInput `json:"custom_headers"`
|
CustomHeaders []ProxyRouteCustomHeaderInput `json:"custom_headers"`
|
||||||
Remark string `json:"remark"`
|
Remark string `json:"remark"`
|
||||||
}
|
}
|
||||||
@@ -77,10 +87,19 @@ func buildProxyRoute(route *model.ProxyRoute, input ProxyRouteInput) (*model.Pro
|
|||||||
originURL := strings.TrimSpace(input.OriginURL)
|
originURL := strings.TrimSpace(input.OriginURL)
|
||||||
originHost := strings.TrimSpace(input.OriginHost)
|
originHost := strings.TrimSpace(input.OriginHost)
|
||||||
remark := strings.TrimSpace(input.Remark)
|
remark := strings.TrimSpace(input.Remark)
|
||||||
|
cachePolicy := strings.TrimSpace(input.CachePolicy)
|
||||||
|
cacheRules, err := normalizeCacheRules(input.CacheEnabled, cachePolicy, input.CacheRules)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
customHeaders, err := normalizeCustomHeaders(input.CustomHeaders)
|
customHeaders, err := normalizeCustomHeaders(input.CustomHeaders)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
cacheRulesJSON, err := json.Marshal(cacheRules)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
customHeadersJSON, err := json.Marshal(customHeaders)
|
customHeadersJSON, err := json.Marshal(customHeaders)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -122,6 +141,9 @@ func buildProxyRoute(route *model.ProxyRoute, input ProxyRouteInput) (*model.Pro
|
|||||||
route.EnableHTTPS = input.EnableHTTPS
|
route.EnableHTTPS = input.EnableHTTPS
|
||||||
route.CertID = input.CertID
|
route.CertID = input.CertID
|
||||||
route.RedirectHTTP = input.RedirectHTTP
|
route.RedirectHTTP = input.RedirectHTTP
|
||||||
|
route.CacheEnabled = input.CacheEnabled
|
||||||
|
route.CachePolicy = normalizeCachePolicy(input.CacheEnabled, cachePolicy)
|
||||||
|
route.CacheRules = string(cacheRulesJSON)
|
||||||
route.CustomHeaders = string(customHeadersJSON)
|
route.CustomHeaders = string(customHeadersJSON)
|
||||||
route.Remark = remark
|
route.Remark = remark
|
||||||
return route, nil
|
return route, nil
|
||||||
@@ -167,6 +189,108 @@ func decodeStoredCustomHeaders(raw string) ([]ProxyRouteCustomHeaderInput, error
|
|||||||
return normalizeCustomHeaders(headers)
|
return normalizeCustomHeaders(headers)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func normalizeCachePolicy(enabled bool, raw string) string {
|
||||||
|
if !enabled {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
policy := strings.TrimSpace(raw)
|
||||||
|
if policy == "" {
|
||||||
|
return proxyRouteCachePolicyURL
|
||||||
|
}
|
||||||
|
return policy
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeCacheRules(enabled bool, rawPolicy string, rules []string) ([]string, error) {
|
||||||
|
if !enabled {
|
||||||
|
return []string{}, nil
|
||||||
|
}
|
||||||
|
policy := normalizeCachePolicy(enabled, rawPolicy)
|
||||||
|
switch policy {
|
||||||
|
case proxyRouteCachePolicyURL:
|
||||||
|
return []string{}, nil
|
||||||
|
case proxyRouteCachePolicySuffix:
|
||||||
|
return normalizeCacheSuffixRules(rules)
|
||||||
|
case proxyRouteCachePolicyPathPrefix:
|
||||||
|
return normalizeCachePathRules(rules, true)
|
||||||
|
case proxyRouteCachePolicyPathExact:
|
||||||
|
return normalizeCachePathRules(rules, false)
|
||||||
|
default:
|
||||||
|
return nil, errors.New("缓存策略不支持")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeCacheSuffixRules(rules []string) ([]string, error) {
|
||||||
|
normalized := make([]string, 0, len(rules))
|
||||||
|
seen := make(map[string]struct{}, len(rules))
|
||||||
|
for _, rule := range rules {
|
||||||
|
item := strings.TrimSpace(strings.TrimPrefix(rule, "."))
|
||||||
|
if item == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.ContainsAny(item, "/\\ \t\r\n") {
|
||||||
|
return nil, errors.New("缓存后缀格式不合法")
|
||||||
|
}
|
||||||
|
if _, ok := seen[item]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[item] = struct{}{}
|
||||||
|
normalized = append(normalized, item)
|
||||||
|
}
|
||||||
|
if len(normalized) == 0 {
|
||||||
|
return nil, errors.New("按后缀缓存时至少填写一个后缀")
|
||||||
|
}
|
||||||
|
return normalized, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeCachePathRules(rules []string, allowPrefix bool) ([]string, error) {
|
||||||
|
normalized := make([]string, 0, len(rules))
|
||||||
|
seen := make(map[string]struct{}, len(rules))
|
||||||
|
for _, rule := range rules {
|
||||||
|
item := strings.TrimSpace(rule)
|
||||||
|
if item == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(item, "/") || strings.Contains(item, "://") || strings.ContainsAny(item, " \t\r\n") {
|
||||||
|
return nil, errors.New("缓存路径规则格式不合法")
|
||||||
|
}
|
||||||
|
if !allowPrefix && strings.HasSuffix(item, "/") && len(item) > 1 {
|
||||||
|
item = strings.TrimRight(item, "/")
|
||||||
|
}
|
||||||
|
if _, ok := seen[item]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[item] = struct{}{}
|
||||||
|
normalized = append(normalized, item)
|
||||||
|
}
|
||||||
|
if len(normalized) == 0 {
|
||||||
|
if allowPrefix {
|
||||||
|
return nil, errors.New("按路径前缀缓存时至少填写一个路径")
|
||||||
|
}
|
||||||
|
return nil, errors.New("按精确路径缓存时至少填写一个路径")
|
||||||
|
}
|
||||||
|
return normalized, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeStoredCacheRules(raw string) ([]string, error) {
|
||||||
|
text := strings.TrimSpace(raw)
|
||||||
|
if text == "" {
|
||||||
|
return []string{}, nil
|
||||||
|
}
|
||||||
|
var rules []string
|
||||||
|
if err := json.Unmarshal([]byte(text), &rules); err != nil {
|
||||||
|
return nil, errors.New("缓存规则格式不合法")
|
||||||
|
}
|
||||||
|
normalized := make([]string, 0, len(rules))
|
||||||
|
for _, rule := range rules {
|
||||||
|
item := strings.TrimSpace(rule)
|
||||||
|
if item == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
normalized = append(normalized, item)
|
||||||
|
}
|
||||||
|
return normalized, nil
|
||||||
|
}
|
||||||
|
|
||||||
func validateOriginURL(raw string) error {
|
func validateOriginURL(raw string) error {
|
||||||
if raw == "" {
|
if raw == "" {
|
||||||
return errors.New("源站地址不能为空")
|
return errors.New("源站地址不能为空")
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ const customHeaderSchema = z.object({
|
|||||||
value: z.string(),
|
value: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const cachePolicyValues = ['url', 'suffix', 'path_prefix', 'path_exact'] as const;
|
||||||
|
|
||||||
const proxyRouteSchema = z
|
const proxyRouteSchema = z
|
||||||
.object({
|
.object({
|
||||||
domain: z.string().trim().min(1, '请输入域名'),
|
domain: z.string().trim().min(1, '请输入域名'),
|
||||||
@@ -91,6 +93,9 @@ const proxyRouteSchema = z
|
|||||||
enable_https: z.boolean(),
|
enable_https: z.boolean(),
|
||||||
cert_id: z.string(),
|
cert_id: z.string(),
|
||||||
redirect_http: z.boolean(),
|
redirect_http: z.boolean(),
|
||||||
|
cache_enabled: z.boolean(),
|
||||||
|
cache_policy: z.enum(cachePolicyValues),
|
||||||
|
cache_rules_text: z.string(),
|
||||||
custom_headers: z.array(customHeaderSchema).min(1),
|
custom_headers: z.array(customHeaderSchema).min(1),
|
||||||
remark: z.string().max(255, '备注不能超过 255 个字符'),
|
remark: z.string().max(255, '备注不能超过 255 个字符'),
|
||||||
})
|
})
|
||||||
@@ -103,6 +108,17 @@ const proxyRouteSchema = z
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (value.cache_enabled) {
|
||||||
|
const cacheRules = parseCacheRulesText(value.cache_rules_text);
|
||||||
|
if (value.cache_policy !== 'url' && cacheRules.length === 0) {
|
||||||
|
context.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
path: ['cache_rules_text'],
|
||||||
|
message: '当前缓存策略至少需要填写一条规则',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
value.custom_headers.forEach((header, index) => {
|
value.custom_headers.forEach((header, index) => {
|
||||||
const key = header.key.trim();
|
const key = header.key.trim();
|
||||||
const headerValue = header.value.trim();
|
const headerValue = header.value.trim();
|
||||||
@@ -152,6 +168,9 @@ const defaultValues: ProxyRouteFormValues = {
|
|||||||
enable_https: false,
|
enable_https: false,
|
||||||
cert_id: '',
|
cert_id: '',
|
||||||
redirect_http: false,
|
redirect_http: false,
|
||||||
|
cache_enabled: false,
|
||||||
|
cache_policy: 'url',
|
||||||
|
cache_rules_text: '',
|
||||||
custom_headers: [{ key: '', value: '' }],
|
custom_headers: [{ key: '', value: '' }],
|
||||||
remark: '',
|
remark: '',
|
||||||
};
|
};
|
||||||
@@ -195,6 +214,52 @@ function parseCustomHeaders(rawValue: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseCacheRules(rawValue: string) {
|
||||||
|
if (!rawValue) {
|
||||||
|
return [] as string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(rawValue) as string[];
|
||||||
|
return Array.isArray(parsed) ? parsed.filter(Boolean) : [];
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseCacheRulesText(value: string) {
|
||||||
|
return value
|
||||||
|
.split(/\r?\n/)
|
||||||
|
.map((item) => item.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCachePolicyLabel(policy: string) {
|
||||||
|
switch (policy) {
|
||||||
|
case 'suffix':
|
||||||
|
return '按后缀';
|
||||||
|
case 'path_prefix':
|
||||||
|
return '按前缀';
|
||||||
|
case 'path_exact':
|
||||||
|
return '按路径';
|
||||||
|
default:
|
||||||
|
return '按 URL';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCacheRulesHint(policy: string) {
|
||||||
|
switch (policy) {
|
||||||
|
case 'suffix':
|
||||||
|
return '每行一个后缀,例如:jpg、css、js。';
|
||||||
|
case 'path_prefix':
|
||||||
|
return '每行一个路径前缀,例如:/assets、/static/images。';
|
||||||
|
case 'path_exact':
|
||||||
|
return '每行一个精确路径,例如:/robots.txt、/manifest.json。';
|
||||||
|
default:
|
||||||
|
return '按 URL 缓存时无需额外规则,系统会按请求 URL 粒度缓存。';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function buildCertificateLabel(certificate: TlsCertificateItem) {
|
function buildCertificateLabel(certificate: TlsCertificateItem) {
|
||||||
return certificate.not_after
|
return certificate.not_after
|
||||||
? `${certificate.name}(到期:${formatDateTime(certificate.not_after)})`
|
? `${certificate.name}(到期:${formatDateTime(certificate.not_after)})`
|
||||||
@@ -211,6 +276,11 @@ function toPayload(values: ProxyRouteFormValues): ProxyRouteMutationPayload {
|
|||||||
cert_id:
|
cert_id:
|
||||||
values.enable_https && values.cert_id ? Number(values.cert_id) : null,
|
values.enable_https && values.cert_id ? Number(values.cert_id) : null,
|
||||||
redirect_http: values.enable_https ? values.redirect_http : false,
|
redirect_http: values.enable_https ? values.redirect_http : false,
|
||||||
|
cache_enabled: values.cache_enabled,
|
||||||
|
cache_policy: values.cache_enabled ? values.cache_policy : 'url',
|
||||||
|
cache_rules: values.cache_enabled
|
||||||
|
? parseCacheRulesText(values.cache_rules_text)
|
||||||
|
: [],
|
||||||
custom_headers: values.custom_headers
|
custom_headers: values.custom_headers
|
||||||
.map((item) => ({ key: item.key.trim(), value: item.value.trim() }))
|
.map((item) => ({ key: item.key.trim(), value: item.value.trim() }))
|
||||||
.filter((item) => item.key || item.value),
|
.filter((item) => item.key || item.value),
|
||||||
@@ -220,6 +290,7 @@ function toPayload(values: ProxyRouteFormValues): ProxyRouteMutationPayload {
|
|||||||
|
|
||||||
function toFormValues(route: ProxyRouteItem): ProxyRouteFormValues {
|
function toFormValues(route: ProxyRouteItem): ProxyRouteFormValues {
|
||||||
const headers = parseCustomHeaders(route.custom_headers);
|
const headers = parseCustomHeaders(route.custom_headers);
|
||||||
|
const cacheRules = parseCacheRules(route.cache_rules);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
domain: route.domain,
|
domain: route.domain,
|
||||||
@@ -229,6 +300,9 @@ function toFormValues(route: ProxyRouteItem): ProxyRouteFormValues {
|
|||||||
enable_https: route.enable_https,
|
enable_https: route.enable_https,
|
||||||
cert_id: route.cert_id ? String(route.cert_id) : '',
|
cert_id: route.cert_id ? String(route.cert_id) : '',
|
||||||
redirect_http: route.redirect_http,
|
redirect_http: route.redirect_http,
|
||||||
|
cache_enabled: route.cache_enabled,
|
||||||
|
cache_policy: (route.cache_policy || 'url') as ProxyRouteFormValues['cache_policy'],
|
||||||
|
cache_rules_text: cacheRules.join('\n'),
|
||||||
custom_headers: headers.length > 0 ? headers : [{ key: '', value: '' }],
|
custom_headers: headers.length > 0 ? headers : [{ key: '', value: '' }],
|
||||||
remark: route.remark || '',
|
remark: route.remark || '',
|
||||||
};
|
};
|
||||||
@@ -288,6 +362,14 @@ export function ProxyRoutesPage() {
|
|||||||
control: form.control,
|
control: form.control,
|
||||||
name: 'redirect_http',
|
name: 'redirect_http',
|
||||||
});
|
});
|
||||||
|
const watchedCacheEnabled = useWatch({
|
||||||
|
control: form.control,
|
||||||
|
name: 'cache_enabled',
|
||||||
|
});
|
||||||
|
const watchedCachePolicy = useWatch({
|
||||||
|
control: form.control,
|
||||||
|
name: 'cache_policy',
|
||||||
|
});
|
||||||
const watchedCertId = useWatch({ control: form.control, name: 'cert_id' });
|
const watchedCertId = useWatch({ control: form.control, name: 'cert_id' });
|
||||||
|
|
||||||
const routesQuery = useQuery({
|
const routesQuery = useQuery({
|
||||||
@@ -514,6 +596,7 @@ export function ProxyRoutesPage() {
|
|||||||
<th className="px-3 py-3 font-medium">域名</th>
|
<th className="px-3 py-3 font-medium">域名</th>
|
||||||
<th className="px-3 py-3 font-medium">源站地址</th>
|
<th className="px-3 py-3 font-medium">源站地址</th>
|
||||||
<th className="px-3 py-3 font-medium">HTTPS</th>
|
<th className="px-3 py-3 font-medium">HTTPS</th>
|
||||||
|
<th className="px-3 py-3 font-medium">缓存</th>
|
||||||
<th className="px-3 py-3 font-medium">请求头</th>
|
<th className="px-3 py-3 font-medium">请求头</th>
|
||||||
<th className="px-3 py-3 font-medium">状态</th>
|
<th className="px-3 py-3 font-medium">状态</th>
|
||||||
<th className="px-3 py-3 font-medium">备注</th>
|
<th className="px-3 py-3 font-medium">备注</th>
|
||||||
@@ -524,6 +607,7 @@ export function ProxyRoutesPage() {
|
|||||||
<tbody className="divide-y divide-[var(--border-default)]">
|
<tbody className="divide-y divide-[var(--border-default)]">
|
||||||
{routes.map((route) => {
|
{routes.map((route) => {
|
||||||
const headers = parseCustomHeaders(route.custom_headers);
|
const headers = parseCustomHeaders(route.custom_headers);
|
||||||
|
const cacheRules = parseCacheRules(route.cache_rules);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr key={route.id} className="align-top">
|
<tr key={route.id} className="align-top">
|
||||||
@@ -554,6 +638,23 @@ export function ProxyRoutesPage() {
|
|||||||
<StatusBadge label="HTTP" variant="warning" />
|
<StatusBadge label="HTTP" variant="warning" />
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
|
<td className="px-3 py-4">
|
||||||
|
{route.cache_enabled ? (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<StatusBadge
|
||||||
|
label={buildCachePolicyLabel(route.cache_policy)}
|
||||||
|
variant="success"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-[var(--foreground-muted)]">
|
||||||
|
{cacheRules.length > 0
|
||||||
|
? `${cacheRules.length} 条规则`
|
||||||
|
: '按 URL 粒度缓存'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<StatusBadge label="关闭" variant="warning" />
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
<td className="px-3 py-4">
|
<td className="px-3 py-4">
|
||||||
<StatusBadge
|
<StatusBadge
|
||||||
label={
|
label={
|
||||||
@@ -739,6 +840,74 @@ export function ProxyRoutesPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-4 lg:grid-cols-[0.8fr_1.2fr]">
|
||||||
|
<ToggleField
|
||||||
|
label="启用规则缓存"
|
||||||
|
description="仅对当前规则生效;系统会自动绕过非 GET、Authorization 和常见登录态 Cookie 请求。"
|
||||||
|
checked={watchedCacheEnabled}
|
||||||
|
onChange={(checked) => {
|
||||||
|
form.setValue('cache_enabled', checked, {
|
||||||
|
shouldDirty: true,
|
||||||
|
shouldValidate: true,
|
||||||
|
});
|
||||||
|
if (!checked) {
|
||||||
|
form.setValue('cache_policy', 'url', {
|
||||||
|
shouldDirty: true,
|
||||||
|
shouldValidate: true,
|
||||||
|
});
|
||||||
|
form.setValue('cache_rules_text', '', {
|
||||||
|
shouldDirty: true,
|
||||||
|
shouldValidate: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ResourceField
|
||||||
|
label="缓存策略"
|
||||||
|
hint="按 URL 会缓存所有符合安全条件的 URL;其余策略会先匹配规则再决定是否缓存。"
|
||||||
|
>
|
||||||
|
<ResourceSelect
|
||||||
|
value={watchedCachePolicy}
|
||||||
|
disabled={!watchedCacheEnabled}
|
||||||
|
onChange={(event) =>
|
||||||
|
form.setValue(
|
||||||
|
'cache_policy',
|
||||||
|
event.target.value as ProxyRouteFormValues['cache_policy'],
|
||||||
|
{
|
||||||
|
shouldDirty: true,
|
||||||
|
shouldValidate: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<option value="url">按 URL 缓存</option>
|
||||||
|
<option value="suffix">按后缀匹配缓存</option>
|
||||||
|
<option value="path_prefix">按路径前缀缓存</option>
|
||||||
|
<option value="path_exact">按精确路径缓存</option>
|
||||||
|
</ResourceSelect>
|
||||||
|
</ResourceField>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ResourceField
|
||||||
|
label="缓存规则"
|
||||||
|
hint={getCacheRulesHint(watchedCachePolicy)}
|
||||||
|
error={form.formState.errors.cache_rules_text?.message}
|
||||||
|
>
|
||||||
|
<ResourceTextarea
|
||||||
|
placeholder={
|
||||||
|
watchedCachePolicy === 'suffix'
|
||||||
|
? 'jpg\ncss\njs'
|
||||||
|
: watchedCachePolicy === 'path_prefix'
|
||||||
|
? '/assets\n/static/images'
|
||||||
|
: watchedCachePolicy === 'path_exact'
|
||||||
|
? '/robots.txt\n/manifest.json'
|
||||||
|
: '按 URL 缓存无需填写规则'
|
||||||
|
}
|
||||||
|
disabled={!watchedCacheEnabled || watchedCachePolicy === 'url'}
|
||||||
|
{...form.register('cache_rules_text')}
|
||||||
|
/>
|
||||||
|
</ResourceField>
|
||||||
|
|
||||||
<div className="rounded-2xl border border-[var(--border-default)] bg-[var(--surface-elevated)] px-4 py-4">
|
<div className="rounded-2xl border border-[var(--border-default)] bg-[var(--surface-elevated)] px-4 py-4">
|
||||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ export interface ProxyRouteItem {
|
|||||||
enable_https: boolean;
|
enable_https: boolean;
|
||||||
cert_id: number | null;
|
cert_id: number | null;
|
||||||
redirect_http: boolean;
|
redirect_http: boolean;
|
||||||
|
cache_enabled: boolean;
|
||||||
|
cache_policy: string;
|
||||||
|
cache_rules: string;
|
||||||
custom_headers: string;
|
custom_headers: string;
|
||||||
remark: string;
|
remark: string;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
@@ -26,6 +29,9 @@ export interface ProxyRouteMutationPayload {
|
|||||||
enable_https: boolean;
|
enable_https: boolean;
|
||||||
cert_id: number | null;
|
cert_id: number | null;
|
||||||
redirect_http: boolean;
|
redirect_http: boolean;
|
||||||
|
cache_enabled: boolean;
|
||||||
|
cache_policy: string;
|
||||||
|
cache_rules: string[];
|
||||||
custom_headers: ProxyRouteCustomHeader[];
|
custom_headers: ProxyRouteCustomHeader[];
|
||||||
remark: string;
|
remark: string;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user