mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-11 01:36:37 +08:00
[功能] 添加支持多个上游地址,优化代理路由配置和负载均衡逻辑
This commit is contained in:
@@ -85,6 +85,8 @@ OpenResty 性能参数与缓存参数继续统一保存在 `Option` 表。当前
|
|||||||
|
|
||||||
* `OpenRestyResolvers` 由管理端性能页面维护,支持填写多个 DNS 服务器 IP;留空时不额外生成 `resolver` 指令。
|
* `OpenRestyResolvers` 由管理端性能页面维护,支持填写多个 DNS 服务器 IP;留空时不额外生成 `resolver` 指令。
|
||||||
* `OpenRestyCacheEnabled` 用于启用缓存基础设施与全局默认参数;实际是否缓存、按 URL / 后缀 / 路径等命中策略由各条 `proxy_routes` 单独决定,不再默认对所有规则开启缓存。
|
* `OpenRestyCacheEnabled` 用于启用缓存基础设施与全局默认参数;实际是否缓存、按 URL / 后缀 / 路径等命中策略由各条 `proxy_routes` 单独决定,不再默认对所有规则开启缓存。
|
||||||
|
* 默认缓存 Key 为 `$scheme$host$request_uri`,更贴近代理域名维度;如需按其他维度命中,可在性能页显式覆盖。
|
||||||
|
* 默认 `keepalive_timeout` 为 `20` 秒,默认 `proxy_connect_timeout` 为 `3` 秒,优先兼顾资源占用与回源失败切换速度。
|
||||||
* 默认事件模型为 `epoll`,并默认开启 `multi_accept`;HTTPS 监听默认附带 `reuseport`,以改善多 worker 下的连接分发。
|
* 默认事件模型为 `epoll`,并默认开启 `multi_accept`;HTTPS 监听默认附带 `reuseport`,以改善多 worker 下的连接分发。
|
||||||
### 1.5 前端构建环境变量
|
### 1.5 前端构建环境变量
|
||||||
|
|
||||||
|
|||||||
+3
-2
@@ -119,10 +119,11 @@ Origin
|
|||||||
|
|
||||||
稳定约束:
|
稳定约束:
|
||||||
|
|
||||||
* 一个域名只对应一个 `origin_url`
|
* 一个域名只对应一条 `proxy_routes` 规则
|
||||||
|
* `proxy_routes` 至少包含一个上游地址;为兼容历史数据保留 `origin_url` 主上游字段,也允许在同一规则内补充多个上游做负载均衡
|
||||||
* `proxy_routes.origin_host` 为可选字段,用于回源时覆盖 `Host` 请求头;未设置时默认透传访问域名
|
* `proxy_routes.origin_host` 为可选字段,用于回源时覆盖 `Host` 请求头;未设置时默认透传访问域名
|
||||||
* `proxy_routes.domain` 必须唯一
|
* `proxy_routes.domain` 必须唯一
|
||||||
* `origin_url` 必须为合法 `http://` 或 `https://`
|
* 所有上游地址都必须为合法 `http://` 或 `https://`
|
||||||
* `config_versions` 必须保存完整快照、渲染结果与 `checksum`
|
* `config_versions` 必须保存完整快照、渲染结果与 `checksum`
|
||||||
* 全局同时只能有一个激活版本
|
* 全局同时只能有一个激活版本
|
||||||
* 回滚通过重新激活旧版本实现
|
* 回滚通过重新激活旧版本实现
|
||||||
|
|||||||
@@ -115,7 +115,7 @@
|
|||||||
通用约束:
|
通用约束:
|
||||||
|
|
||||||
* 不新增平台化对象,除非设计文档明确要求
|
* 不新增平台化对象,除非设计文档明确要求
|
||||||
* `proxy_routes` 维持一条域名对应一个 `origin_url`
|
* `proxy_routes` 维持一条域名对应一条规则;规则内允许保存一个或多个上游地址用于负载均衡,但不引入独立 `origin_pool`
|
||||||
* `proxy_routes.origin_host` 为可选字段,仅用于覆盖回源 `Host` 请求头,不引入新的平台化对象
|
* `proxy_routes.origin_host` 为可选字段,仅用于覆盖回源 `Host` 请求头,不引入新的平台化对象
|
||||||
* `config_versions` 必须保存完整快照与渲染结果
|
* `config_versions` 必须保存完整快照与渲染结果
|
||||||
* 全局同时只能有一个激活版本
|
* 全局同时只能有一个激活版本
|
||||||
|
|||||||
@@ -62,14 +62,14 @@ var OpenRestyWorkerConnections = 4096
|
|||||||
var OpenRestyWorkerRlimitNofile = 65535
|
var OpenRestyWorkerRlimitNofile = 65535
|
||||||
var OpenRestyEventsUse = "epoll"
|
var OpenRestyEventsUse = "epoll"
|
||||||
var OpenRestyEventsMultiAcceptEnabled = true
|
var OpenRestyEventsMultiAcceptEnabled = true
|
||||||
var OpenRestyKeepaliveTimeout = 65
|
var OpenRestyKeepaliveTimeout = 20
|
||||||
var OpenRestyKeepaliveRequests = 1000
|
var OpenRestyKeepaliveRequests = 1000
|
||||||
var OpenRestyClientHeaderTimeout = 15
|
var OpenRestyClientHeaderTimeout = 15
|
||||||
var OpenRestyClientBodyTimeout = 15
|
var OpenRestyClientBodyTimeout = 15
|
||||||
var OpenRestyClientMaxBodySize = "64m"
|
var OpenRestyClientMaxBodySize = "64m"
|
||||||
var OpenRestyLargeClientHeaderBuffers = "4 16k"
|
var OpenRestyLargeClientHeaderBuffers = "4 16k"
|
||||||
var OpenRestySendTimeout = 30
|
var OpenRestySendTimeout = 30
|
||||||
var OpenRestyProxyConnectTimeout = 5
|
var OpenRestyProxyConnectTimeout = 3
|
||||||
var OpenRestyProxySendTimeout = 60
|
var OpenRestyProxySendTimeout = 60
|
||||||
var OpenRestyProxyReadTimeout = 60
|
var OpenRestyProxyReadTimeout = 60
|
||||||
var OpenRestyWebsocketEnabled = true
|
var OpenRestyWebsocketEnabled = true
|
||||||
@@ -87,7 +87,7 @@ var OpenRestyCachePath = ""
|
|||||||
var OpenRestyCacheLevels = "1:2"
|
var OpenRestyCacheLevels = "1:2"
|
||||||
var OpenRestyCacheInactive = "30m"
|
var OpenRestyCacheInactive = "30m"
|
||||||
var OpenRestyCacheMaxSize = "1g"
|
var OpenRestyCacheMaxSize = "1g"
|
||||||
var OpenRestyCacheKeyTemplate = "$scheme$proxy_host$request_uri"
|
var OpenRestyCacheKeyTemplate = "$scheme$host$request_uri"
|
||||||
var OpenRestyCacheLockEnabled = true
|
var OpenRestyCacheLockEnabled = true
|
||||||
var OpenRestyCacheLockTimeout = "5s"
|
var OpenRestyCacheLockTimeout = "5s"
|
||||||
var OpenRestyCacheUseStale = "error timeout updating http_500 http_502 http_503 http_504"
|
var OpenRestyCacheUseStale = "error timeout updating http_500 http_502 http_503 http_504"
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ type ProxyRoute struct {
|
|||||||
Domain string `json:"domain" gorm:"uniqueIndex;size:255;not null"`
|
Domain string `json:"domain" gorm:"uniqueIndex;size:255;not null"`
|
||||||
OriginURL string `json:"origin_url" gorm:"size:2048;not null"`
|
OriginURL string `json:"origin_url" gorm:"size:2048;not null"`
|
||||||
OriginHost string `json:"origin_host" gorm:"size:255"`
|
OriginHost string `json:"origin_host" gorm:"size:255"`
|
||||||
|
Upstreams string `json:"upstreams" gorm:"type:text;not null;default:'[]'"`
|
||||||
Enabled bool `json:"enabled" gorm:"not null;default:true"`
|
Enabled bool `json:"enabled" gorm:"not null;default:true"`
|
||||||
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"`
|
||||||
@@ -45,6 +46,7 @@ func (route *ProxyRoute) Update() error {
|
|||||||
"domain": route.Domain,
|
"domain": route.Domain,
|
||||||
"origin_url": route.OriginURL,
|
"origin_url": route.OriginURL,
|
||||||
"origin_host": route.OriginHost,
|
"origin_host": route.OriginHost,
|
||||||
|
"upstreams": route.Upstreams,
|
||||||
"enabled": route.Enabled,
|
"enabled": route.Enabled,
|
||||||
"enable_https": route.EnableHTTPS,
|
"enable_https": route.EnableHTTPS,
|
||||||
"cert_id": route.CertID,
|
"cert_id": route.CertID,
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ func TestPhase1PublishLifecycle(t *testing.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",
|
||||||
|
"upstreams": []string{"https://origin-a-backup.internal"},
|
||||||
"origin_host": "origin-a.internal",
|
"origin_host": "origin-a.internal",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"cache_enabled": true,
|
"cache_enabled": true,
|
||||||
@@ -65,6 +66,9 @@ func TestPhase1PublishLifecycle(t *testing.T) {
|
|||||||
if !createdRoute.CacheEnabled || createdRoute.CachePolicy != "path_prefix" {
|
if !createdRoute.CacheEnabled || createdRoute.CachePolicy != "path_prefix" {
|
||||||
t.Fatalf("expected route cache settings to persist, got %+v", createdRoute)
|
t.Fatalf("expected route cache settings to persist, got %+v", createdRoute)
|
||||||
}
|
}
|
||||||
|
if !strings.Contains(createdRoute.Upstreams, "origin-a-backup.internal") {
|
||||||
|
t.Fatalf("expected route upstream list to persist, got %s", createdRoute.Upstreams)
|
||||||
|
}
|
||||||
if !strings.Contains(createdRoute.CacheRules, "/assets") {
|
if !strings.Contains(createdRoute.CacheRules, "/assets") {
|
||||||
t.Fatalf("expected route cache rules to persist, got %s", createdRoute.CacheRules)
|
t.Fatalf("expected route cache rules to persist, got %s", createdRoute.CacheRules)
|
||||||
}
|
}
|
||||||
@@ -114,6 +118,7 @@ func TestPhase1PublishLifecycle(t *testing.T) {
|
|||||||
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",
|
||||||
|
"upstreams": []string{"https://origin-b-backup.internal"},
|
||||||
"origin_host": "origin-b.internal",
|
"origin_host": "origin-b.internal",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"cache_enabled": true,
|
"cache_enabled": true,
|
||||||
@@ -133,6 +138,9 @@ func TestPhase1PublishLifecycle(t *testing.T) {
|
|||||||
if createdRoute.CachePolicy != "path_exact" || !strings.Contains(createdRoute.CacheRules, "/robots.txt") {
|
if createdRoute.CachePolicy != "path_exact" || !strings.Contains(createdRoute.CacheRules, "/robots.txt") {
|
||||||
t.Fatalf("expected updated route cache rules to persist, got %+v", createdRoute)
|
t.Fatalf("expected updated route cache rules to persist, got %+v", createdRoute)
|
||||||
}
|
}
|
||||||
|
if !strings.Contains(createdRoute.Upstreams, "origin-b-backup.internal") {
|
||||||
|
t.Fatalf("expected updated route upstream list to persist, got %s", createdRoute.Upstreams)
|
||||||
|
}
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ type snapshotRoute struct {
|
|||||||
Domain string `json:"domain"`
|
Domain string `json:"domain"`
|
||||||
OriginURL string `json:"origin_url"`
|
OriginURL string `json:"origin_url"`
|
||||||
OriginHost string `json:"origin_host,omitempty"`
|
OriginHost string `json:"origin_host,omitempty"`
|
||||||
|
Upstreams []string `json:"upstreams,omitempty"`
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
EnableHTTPS bool `json:"enable_https"`
|
EnableHTTPS bool `json:"enable_https"`
|
||||||
CertID *uint `json:"cert_id,omitempty"`
|
CertID *uint `json:"cert_id,omitempty"`
|
||||||
@@ -82,7 +83,7 @@ type routeCacheConfig struct {
|
|||||||
type routeUpstreamConfig struct {
|
type routeUpstreamConfig struct {
|
||||||
Name string
|
Name string
|
||||||
Scheme string
|
Scheme string
|
||||||
Address string
|
Addresses []string
|
||||||
UsesNamedUpstream bool
|
UsesNamedUpstream bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -408,6 +409,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)
|
||||||
}
|
}
|
||||||
|
upstreams, err := decodeStoredUpstreams(route.Upstreams, route.OriginURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("路由 %s 上游配置无效", route.Domain)
|
||||||
|
}
|
||||||
cacheRules, err := decodeStoredCacheRules(route.CacheRules)
|
cacheRules, err := decodeStoredCacheRules(route.CacheRules)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("路由 %s 缓存规则无效", route.Domain)
|
return nil, fmt.Errorf("路由 %s 缓存规则无效", route.Domain)
|
||||||
@@ -416,6 +421,7 @@ func buildSnapshotRoutes(routes []*model.ProxyRoute) ([]snapshotRoute, error) {
|
|||||||
Domain: route.Domain,
|
Domain: route.Domain,
|
||||||
OriginURL: route.OriginURL,
|
OriginURL: route.OriginURL,
|
||||||
OriginHost: route.OriginHost,
|
OriginHost: route.OriginHost,
|
||||||
|
Upstreams: upstreams,
|
||||||
Enabled: route.Enabled,
|
Enabled: route.Enabled,
|
||||||
EnableHTTPS: route.EnableHTTPS,
|
EnableHTTPS: route.EnableHTTPS,
|
||||||
CertID: route.CertID,
|
CertID: route.CertID,
|
||||||
@@ -459,6 +465,11 @@ func normalizeSnapshotRoutes(routes []snapshotRoute) []snapshotRoute {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
routes[index].CustomHeaders = normalizedHeaders
|
routes[index].CustomHeaders = normalizedHeaders
|
||||||
}
|
}
|
||||||
|
normalizedUpstreams, err := normalizeUpstreams(routes[index].OriginURL, routes[index].Upstreams)
|
||||||
|
if err == nil {
|
||||||
|
routes[index].OriginURL = normalizedUpstreams[0]
|
||||||
|
routes[index].Upstreams = normalizedUpstreams
|
||||||
|
}
|
||||||
normalizedCacheRules, err := normalizeCacheRules(routes[index].CacheEnabled, routes[index].CachePolicy, routes[index].CacheRules)
|
normalizedCacheRules, err := normalizeCacheRules(routes[index].CacheEnabled, routes[index].CachePolicy, routes[index].CacheRules)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
routes[index].CachePolicy = normalizeCachePolicy(routes[index].CacheEnabled, routes[index].CachePolicy)
|
routes[index].CachePolicy = normalizeCachePolicy(routes[index].CacheEnabled, routes[index].CachePolicy)
|
||||||
@@ -472,6 +483,14 @@ 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 || left.CacheEnabled != right.CacheEnabled || left.CachePolicy != right.CachePolicy || !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.Upstreams) != len(right.Upstreams) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for index := range left.Upstreams {
|
||||||
|
if left.Upstreams[index] != right.Upstreams[index] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
if len(left.CacheRules) != len(right.CacheRules) {
|
if len(left.CacheRules) != len(right.CacheRules) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -648,6 +667,10 @@ 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)
|
||||||
}
|
}
|
||||||
|
upstreams, err := decodeStoredUpstreams(route.Upstreams, route.OriginURL)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, fmt.Errorf("路由 %s 上游配置无效", route.Domain)
|
||||||
|
}
|
||||||
cacheRules, err := decodeStoredCacheRules(route.CacheRules)
|
cacheRules, err := decodeStoredCacheRules(route.CacheRules)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil, fmt.Errorf("路由 %s 缓存规则无效", route.Domain)
|
return "", nil, fmt.Errorf("路由 %s 缓存规则无效", route.Domain)
|
||||||
@@ -657,7 +680,7 @@ func renderRouteConfig(routes []*model.ProxyRoute, cfg openRestyConfigSnapshot)
|
|||||||
Policy: route.CachePolicy,
|
Policy: route.CachePolicy,
|
||||||
Rules: cacheRules,
|
Rules: cacheRules,
|
||||||
}
|
}
|
||||||
upstreamConfig := buildRouteUpstreamConfig(route, cfg)
|
upstreamConfig := buildRouteUpstreamConfig(route, upstreams, cfg)
|
||||||
if upstreamConfig.UsesNamedUpstream {
|
if upstreamConfig.UsesNamedUpstream {
|
||||||
builder.WriteString(renderNamedUpstreamBlock(upstreamConfig))
|
builder.WriteString(renderNamedUpstreamBlock(upstreamConfig))
|
||||||
}
|
}
|
||||||
@@ -965,24 +988,55 @@ func renderProxyPassBlock(originURL string, upstreamConfig routeUpstreamConfig,
|
|||||||
return builder.String()
|
return builder.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildRouteUpstreamConfig(route *model.ProxyRoute, cfg openRestyConfigSnapshot) routeUpstreamConfig {
|
func buildRouteUpstreamConfig(route *model.ProxyRoute, upstreams []string, cfg openRestyConfigSnapshot) routeUpstreamConfig {
|
||||||
parsed, err := url.Parse(strings.TrimSpace(route.OriginURL))
|
if len(upstreams) == 0 {
|
||||||
if err != nil || parsed.Host == "" || parsed.Scheme == "" {
|
|
||||||
return routeUpstreamConfig{}
|
return routeUpstreamConfig{}
|
||||||
}
|
}
|
||||||
if shouldUseRuntimeResolver(route.OriginURL, cfg.Resolvers) {
|
if len(upstreams) == 1 {
|
||||||
return routeUpstreamConfig{}
|
parsed, err := url.Parse(strings.TrimSpace(upstreams[0]))
|
||||||
|
if err != nil || parsed.Host == "" || parsed.Scheme == "" {
|
||||||
|
return routeUpstreamConfig{}
|
||||||
|
}
|
||||||
|
if shouldUseRuntimeResolver(upstreams[0], cfg.Resolvers) {
|
||||||
|
return routeUpstreamConfig{}
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(parsed.EscapedPath()) != "" && strings.TrimSpace(parsed.EscapedPath()) != "/" {
|
||||||
|
return routeUpstreamConfig{}
|
||||||
|
}
|
||||||
|
if parsed.RawQuery != "" {
|
||||||
|
return routeUpstreamConfig{}
|
||||||
|
}
|
||||||
|
return routeUpstreamConfig{
|
||||||
|
Name: buildRouteUpstreamName(route),
|
||||||
|
Scheme: parsed.Scheme,
|
||||||
|
Addresses: []string{parsed.Host},
|
||||||
|
UsesNamedUpstream: true,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(parsed.EscapedPath()) != "" && strings.TrimSpace(parsed.EscapedPath()) != "/" {
|
addresses := make([]string, 0, len(upstreams))
|
||||||
return routeUpstreamConfig{}
|
var scheme string
|
||||||
}
|
for _, upstream := range upstreams {
|
||||||
if parsed.RawQuery != "" {
|
parsed, err := url.Parse(strings.TrimSpace(upstream))
|
||||||
return routeUpstreamConfig{}
|
if err != nil || parsed.Host == "" || parsed.Scheme == "" {
|
||||||
|
return routeUpstreamConfig{}
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(parsed.EscapedPath()) != "" && strings.TrimSpace(parsed.EscapedPath()) != "/" {
|
||||||
|
return routeUpstreamConfig{}
|
||||||
|
}
|
||||||
|
if parsed.RawQuery != "" {
|
||||||
|
return routeUpstreamConfig{}
|
||||||
|
}
|
||||||
|
if scheme == "" {
|
||||||
|
scheme = parsed.Scheme
|
||||||
|
} else if scheme != parsed.Scheme {
|
||||||
|
return routeUpstreamConfig{}
|
||||||
|
}
|
||||||
|
addresses = append(addresses, parsed.Host)
|
||||||
}
|
}
|
||||||
return routeUpstreamConfig{
|
return routeUpstreamConfig{
|
||||||
Name: buildRouteUpstreamName(route),
|
Name: buildRouteUpstreamName(route),
|
||||||
Scheme: parsed.Scheme,
|
Scheme: scheme,
|
||||||
Address: parsed.Host,
|
Addresses: addresses,
|
||||||
UsesNamedUpstream: true,
|
UsesNamedUpstream: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1008,7 +1062,13 @@ func buildRouteUpstreamName(route *model.ProxyRoute) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func renderNamedUpstreamBlock(upstreamConfig routeUpstreamConfig) string {
|
func renderNamedUpstreamBlock(upstreamConfig routeUpstreamConfig) string {
|
||||||
return fmt.Sprintf("upstream %s {\n server %s max_fails=3 fail_timeout=10s;\n keepalive 128;\n}\n\n", upstreamConfig.Name, upstreamConfig.Address)
|
var builder strings.Builder
|
||||||
|
builder.WriteString(fmt.Sprintf("upstream %s {\n", upstreamConfig.Name))
|
||||||
|
for _, address := range upstreamConfig.Addresses {
|
||||||
|
builder.WriteString(fmt.Sprintf(" server %s max_fails=3 fail_timeout=10s;\n", address))
|
||||||
|
}
|
||||||
|
builder.WriteString(" keepalive 128;\n}\n\n")
|
||||||
|
return builder.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func shouldUseRuntimeResolver(originURL string, resolvers string) bool {
|
func shouldUseRuntimeResolver(originURL string, resolvers string) bool {
|
||||||
|
|||||||
@@ -72,6 +72,12 @@ func TestCreateTLSCertificateAndRenderHTTPSConfig(t *testing.T) {
|
|||||||
if !strings.Contains(result.Version.MainConfig, "multi_accept on;") {
|
if !strings.Contains(result.Version.MainConfig, "multi_accept on;") {
|
||||||
t.Fatal("expected main config to default multi_accept to on")
|
t.Fatal("expected main config to default multi_accept to on")
|
||||||
}
|
}
|
||||||
|
if !strings.Contains(result.Version.MainConfig, "keepalive_timeout 20;") {
|
||||||
|
t.Fatal("expected main config to default keepalive_timeout to 20")
|
||||||
|
}
|
||||||
|
if !strings.Contains(result.Version.MainConfig, "proxy_connect_timeout 3;") {
|
||||||
|
t.Fatal("expected main config to default proxy_connect_timeout to 3")
|
||||||
|
}
|
||||||
if strings.Contains(result.Version.MainConfig, "allow 127.0.0.1;") {
|
if strings.Contains(result.Version.MainConfig, "allow 127.0.0.1;") {
|
||||||
t.Fatal("expected main config to avoid hard-coded allow rules on observability server")
|
t.Fatal("expected main config to avoid hard-coded allow rules on observability server")
|
||||||
}
|
}
|
||||||
@@ -212,6 +218,9 @@ func TestPublishConfigVersionRendersRouteLevelCachePolicy(t *testing.T) {
|
|||||||
if !strings.Contains(result.Version.MainConfig, "proxy_cache_path /var/cache/openresty/openflare") {
|
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")
|
t.Fatal("expected main config to include cache zone when cache infra is enabled")
|
||||||
}
|
}
|
||||||
|
if !strings.Contains(result.Version.MainConfig, `proxy_cache_key "$scheme$host$request_uri";`) {
|
||||||
|
t.Fatal("expected main config to default cache key to host dimension")
|
||||||
|
}
|
||||||
if !strings.Contains(result.Version.RenderedConfig, "proxy_cache_methods GET;") {
|
if !strings.Contains(result.Version.RenderedConfig, "proxy_cache_methods GET;") {
|
||||||
t.Fatal("expected rendered config to only cache GET requests")
|
t.Fatal("expected rendered config to only cache GET requests")
|
||||||
}
|
}
|
||||||
@@ -244,6 +253,50 @@ func TestPublishConfigVersionRendersRouteLevelCachePolicy(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPublishConfigVersionRendersMultipleUpstreams(t *testing.T) {
|
||||||
|
setupServiceTestDB(t)
|
||||||
|
|
||||||
|
route, err := CreateProxyRoute(ProxyRouteInput{
|
||||||
|
Domain: "lb.example.com",
|
||||||
|
OriginURL: "http://c1:39010",
|
||||||
|
Upstreams: []string{"http://c2:39010", "http://c3:39010"},
|
||||||
|
Enabled: true,
|
||||||
|
OriginHost: "lb.example.com",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreateProxyRoute failed: %v", err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(route.Upstreams, "c2:39010") {
|
||||||
|
t.Fatalf("expected route upstreams to persist, got %s", route.Upstreams)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := PublishConfigVersion("root")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("PublishConfigVersion failed: %v", err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(result.Version.RenderedConfig, "upstream backend_lb_example_com_1 {") {
|
||||||
|
t.Fatal("expected rendered config to define upstream block for load balancing route")
|
||||||
|
}
|
||||||
|
if strings.Count(result.Version.RenderedConfig, "server c") < 3 {
|
||||||
|
t.Fatal("expected rendered config to include every upstream server")
|
||||||
|
}
|
||||||
|
if !strings.Contains(result.Version.RenderedConfig, "server c1:39010 max_fails=3 fail_timeout=10s;") {
|
||||||
|
t.Fatal("expected rendered config to include primary upstream server")
|
||||||
|
}
|
||||||
|
if !strings.Contains(result.Version.RenderedConfig, "server c2:39010 max_fails=3 fail_timeout=10s;") {
|
||||||
|
t.Fatal("expected rendered config to include secondary upstream server")
|
||||||
|
}
|
||||||
|
if !strings.Contains(result.Version.RenderedConfig, "server c3:39010 max_fails=3 fail_timeout=10s;") {
|
||||||
|
t.Fatal("expected rendered config to include tertiary upstream server")
|
||||||
|
}
|
||||||
|
if !strings.Contains(result.Version.RenderedConfig, "proxy_pass http://backend_lb_example_com_1;") {
|
||||||
|
t.Fatal("expected rendered config to proxy through load balancing upstream")
|
||||||
|
}
|
||||||
|
if !strings.Contains(result.Version.SnapshotJSON, `"upstreams":["http://c1:39010","http://c2:39010","http://c3:39010"]`) {
|
||||||
|
t.Fatal("expected snapshot to include upstream list")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestPublishConfigVersionOverridesOriginHostHeader(t *testing.T) {
|
func TestPublishConfigVersionOverridesOriginHostHeader(t *testing.T) {
|
||||||
setupServiceTestDB(t)
|
setupServiceTestDB(t)
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ type ProxyRouteInput struct {
|
|||||||
Domain string `json:"domain"`
|
Domain string `json:"domain"`
|
||||||
OriginURL string `json:"origin_url"`
|
OriginURL string `json:"origin_url"`
|
||||||
OriginHost string `json:"origin_host"`
|
OriginHost string `json:"origin_host"`
|
||||||
|
Upstreams []string `json:"upstreams"`
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
EnableHTTPS bool `json:"enable_https"`
|
EnableHTTPS bool `json:"enable_https"`
|
||||||
CertID *uint `json:"cert_id"`
|
CertID *uint `json:"cert_id"`
|
||||||
@@ -87,6 +88,10 @@ 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)
|
||||||
|
upstreams, err := normalizeUpstreams(originURL, input.Upstreams)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
cachePolicy := strings.TrimSpace(input.CachePolicy)
|
cachePolicy := strings.TrimSpace(input.CachePolicy)
|
||||||
cacheRules, err := normalizeCacheRules(input.CacheEnabled, cachePolicy, input.CacheRules)
|
cacheRules, err := normalizeCacheRules(input.CacheEnabled, cachePolicy, input.CacheRules)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -100,6 +105,10 @@ func buildProxyRoute(route *model.ProxyRoute, input ProxyRouteInput) (*model.Pro
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
upstreamsJSON, err := json.Marshal(upstreams)
|
||||||
|
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
|
||||||
@@ -110,9 +119,6 @@ func buildProxyRoute(route *model.ProxyRoute, input ProxyRouteInput) (*model.Pro
|
|||||||
if strings.Contains(domain, "://") || strings.Contains(domain, "/") {
|
if strings.Contains(domain, "://") || strings.Contains(domain, "/") {
|
||||||
return nil, errors.New("域名格式不合法")
|
return nil, errors.New("域名格式不合法")
|
||||||
}
|
}
|
||||||
if err := validateOriginURL(originURL); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := validateOriginHost(originHost); err != nil {
|
if err := validateOriginHost(originHost); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -135,8 +141,9 @@ func buildProxyRoute(route *model.ProxyRoute, input ProxyRouteInput) (*model.Pro
|
|||||||
route = &model.ProxyRoute{}
|
route = &model.ProxyRoute{}
|
||||||
}
|
}
|
||||||
route.Domain = domain
|
route.Domain = domain
|
||||||
route.OriginURL = originURL
|
route.OriginURL = upstreams[0]
|
||||||
route.OriginHost = originHost
|
route.OriginHost = originHost
|
||||||
|
route.Upstreams = string(upstreamsJSON)
|
||||||
route.Enabled = input.Enabled
|
route.Enabled = input.Enabled
|
||||||
route.EnableHTTPS = input.EnableHTTPS
|
route.EnableHTTPS = input.EnableHTTPS
|
||||||
route.CertID = input.CertID
|
route.CertID = input.CertID
|
||||||
@@ -177,6 +184,59 @@ func normalizeCustomHeaders(headers []ProxyRouteCustomHeaderInput) ([]ProxyRoute
|
|||||||
return normalized, nil
|
return normalized, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func normalizeUpstreams(originURL string, upstreams []string) ([]string, error) {
|
||||||
|
candidates := make([]string, 0, len(upstreams)+1)
|
||||||
|
if strings.TrimSpace(originURL) != "" {
|
||||||
|
candidates = append(candidates, originURL)
|
||||||
|
}
|
||||||
|
candidates = append(candidates, upstreams...)
|
||||||
|
trimmed := make([]string, 0, len(candidates))
|
||||||
|
for _, candidate := range candidates {
|
||||||
|
item := strings.TrimSpace(candidate)
|
||||||
|
if item == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
trimmed = append(trimmed, item)
|
||||||
|
}
|
||||||
|
unique := make([]string, 0, len(trimmed))
|
||||||
|
seen := make(map[string]struct{}, len(trimmed))
|
||||||
|
for _, item := range trimmed {
|
||||||
|
if _, ok := seen[item]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[item] = struct{}{}
|
||||||
|
unique = append(unique, item)
|
||||||
|
}
|
||||||
|
normalized := make([]string, 0, len(unique))
|
||||||
|
var scheme string
|
||||||
|
multiUpstream := len(unique) > 1
|
||||||
|
for _, item := range unique {
|
||||||
|
if err := validateOriginURL(item); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
parsed, err := url.ParseRequestURI(item)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("源站地址格式不合法")
|
||||||
|
}
|
||||||
|
if multiUpstream && parsed.Path != "" && parsed.Path != "/" {
|
||||||
|
return nil, errors.New("多上游模式暂不支持带路径的源站地址")
|
||||||
|
}
|
||||||
|
if multiUpstream && parsed.RawQuery != "" {
|
||||||
|
return nil, errors.New("多上游模式暂不支持带查询参数的源站地址")
|
||||||
|
}
|
||||||
|
if scheme == "" {
|
||||||
|
scheme = parsed.Scheme
|
||||||
|
} else if scheme != parsed.Scheme {
|
||||||
|
return nil, errors.New("同一规则的多个上游必须使用相同协议")
|
||||||
|
}
|
||||||
|
normalized = append(normalized, item)
|
||||||
|
}
|
||||||
|
if len(normalized) == 0 {
|
||||||
|
return nil, errors.New("至少填写一个上游地址")
|
||||||
|
}
|
||||||
|
return normalized, nil
|
||||||
|
}
|
||||||
|
|
||||||
func decodeStoredCustomHeaders(raw string) ([]ProxyRouteCustomHeaderInput, error) {
|
func decodeStoredCustomHeaders(raw string) ([]ProxyRouteCustomHeaderInput, error) {
|
||||||
text := strings.TrimSpace(raw)
|
text := strings.TrimSpace(raw)
|
||||||
if text == "" {
|
if text == "" {
|
||||||
@@ -291,6 +351,18 @@ func decodeStoredCacheRules(raw string) ([]string, error) {
|
|||||||
return normalized, nil
|
return normalized, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func decodeStoredUpstreams(raw string, fallbackOriginURL string) ([]string, error) {
|
||||||
|
text := strings.TrimSpace(raw)
|
||||||
|
if text == "" {
|
||||||
|
return normalizeUpstreams(fallbackOriginURL, nil)
|
||||||
|
}
|
||||||
|
var upstreams []string
|
||||||
|
if err := json.Unmarshal([]byte(text), &upstreams); err != nil {
|
||||||
|
return nil, errors.New("上游配置格式不合法")
|
||||||
|
}
|
||||||
|
return normalizeUpstreams(fallbackOriginURL, upstreams)
|
||||||
|
}
|
||||||
|
|
||||||
func validateOriginURL(raw string) error {
|
func validateOriginURL(raw string) error {
|
||||||
if raw == "" {
|
if raw == "" {
|
||||||
return errors.New("源站地址不能为空")
|
return errors.New("源站地址不能为空")
|
||||||
|
|||||||
@@ -36,14 +36,14 @@ const defaultPerformanceFields = {
|
|||||||
OpenRestyWorkerRlimitNofile: '65535',
|
OpenRestyWorkerRlimitNofile: '65535',
|
||||||
OpenRestyEventsUse: 'epoll',
|
OpenRestyEventsUse: 'epoll',
|
||||||
OpenRestyEventsMultiAcceptEnabled: true,
|
OpenRestyEventsMultiAcceptEnabled: true,
|
||||||
OpenRestyKeepaliveTimeout: '65',
|
OpenRestyKeepaliveTimeout: '20',
|
||||||
OpenRestyKeepaliveRequests: '1000',
|
OpenRestyKeepaliveRequests: '1000',
|
||||||
OpenRestyClientHeaderTimeout: '15',
|
OpenRestyClientHeaderTimeout: '15',
|
||||||
OpenRestyClientBodyTimeout: '15',
|
OpenRestyClientBodyTimeout: '15',
|
||||||
OpenRestyClientMaxBodySize: '64m',
|
OpenRestyClientMaxBodySize: '64m',
|
||||||
OpenRestyLargeClientHeaderBuffers: '4 16k',
|
OpenRestyLargeClientHeaderBuffers: '4 16k',
|
||||||
OpenRestySendTimeout: '30',
|
OpenRestySendTimeout: '30',
|
||||||
OpenRestyProxyConnectTimeout: '5',
|
OpenRestyProxyConnectTimeout: '3',
|
||||||
OpenRestyProxySendTimeout: '60',
|
OpenRestyProxySendTimeout: '60',
|
||||||
OpenRestyProxyReadTimeout: '60',
|
OpenRestyProxyReadTimeout: '60',
|
||||||
OpenRestyWebsocketEnabled: true,
|
OpenRestyWebsocketEnabled: true,
|
||||||
@@ -61,7 +61,7 @@ const defaultPerformanceFields = {
|
|||||||
OpenRestyCacheLevels: '1:2',
|
OpenRestyCacheLevels: '1:2',
|
||||||
OpenRestyCacheInactive: '30m',
|
OpenRestyCacheInactive: '30m',
|
||||||
OpenRestyCacheMaxSize: '1g',
|
OpenRestyCacheMaxSize: '1g',
|
||||||
OpenRestyCacheKeyTemplate: '$scheme$proxy_host$request_uri',
|
OpenRestyCacheKeyTemplate: '$scheme$host$request_uri',
|
||||||
OpenRestyCacheLockEnabled: true,
|
OpenRestyCacheLockEnabled: true,
|
||||||
OpenRestyCacheLockTimeout: '5s',
|
OpenRestyCacheLockTimeout: '5s',
|
||||||
OpenRestyCacheUseStale:
|
OpenRestyCacheUseStale:
|
||||||
@@ -211,7 +211,7 @@ export function PerformancePage() {
|
|||||||
optionMap.OpenRestyEventsMultiAcceptEnabled,
|
optionMap.OpenRestyEventsMultiAcceptEnabled,
|
||||||
true,
|
true,
|
||||||
),
|
),
|
||||||
OpenRestyKeepaliveTimeout: optionMap.OpenRestyKeepaliveTimeout ?? '65',
|
OpenRestyKeepaliveTimeout: optionMap.OpenRestyKeepaliveTimeout ?? '20',
|
||||||
OpenRestyKeepaliveRequests:
|
OpenRestyKeepaliveRequests:
|
||||||
optionMap.OpenRestyKeepaliveRequests ?? '1000',
|
optionMap.OpenRestyKeepaliveRequests ?? '1000',
|
||||||
OpenRestyClientHeaderTimeout:
|
OpenRestyClientHeaderTimeout:
|
||||||
@@ -222,7 +222,7 @@ export function PerformancePage() {
|
|||||||
optionMap.OpenRestyLargeClientHeaderBuffers ?? '4 16k',
|
optionMap.OpenRestyLargeClientHeaderBuffers ?? '4 16k',
|
||||||
OpenRestySendTimeout: optionMap.OpenRestySendTimeout ?? '30',
|
OpenRestySendTimeout: optionMap.OpenRestySendTimeout ?? '30',
|
||||||
OpenRestyProxyConnectTimeout:
|
OpenRestyProxyConnectTimeout:
|
||||||
optionMap.OpenRestyProxyConnectTimeout ?? '5',
|
optionMap.OpenRestyProxyConnectTimeout ?? '3',
|
||||||
OpenRestyProxySendTimeout: optionMap.OpenRestyProxySendTimeout ?? '60',
|
OpenRestyProxySendTimeout: optionMap.OpenRestyProxySendTimeout ?? '60',
|
||||||
OpenRestyProxyReadTimeout: optionMap.OpenRestyProxyReadTimeout ?? '60',
|
OpenRestyProxyReadTimeout: optionMap.OpenRestyProxyReadTimeout ?? '60',
|
||||||
OpenRestyWebsocketEnabled: toBoolean(
|
OpenRestyWebsocketEnabled: toBoolean(
|
||||||
@@ -251,7 +251,7 @@ export function PerformancePage() {
|
|||||||
OpenRestyCacheInactive: optionMap.OpenRestyCacheInactive ?? '30m',
|
OpenRestyCacheInactive: optionMap.OpenRestyCacheInactive ?? '30m',
|
||||||
OpenRestyCacheMaxSize: optionMap.OpenRestyCacheMaxSize ?? '1g',
|
OpenRestyCacheMaxSize: optionMap.OpenRestyCacheMaxSize ?? '1g',
|
||||||
OpenRestyCacheKeyTemplate:
|
OpenRestyCacheKeyTemplate:
|
||||||
optionMap.OpenRestyCacheKeyTemplate ?? '$scheme$proxy_host$request_uri',
|
optionMap.OpenRestyCacheKeyTemplate ?? '$scheme$host$request_uri',
|
||||||
OpenRestyCacheLockEnabled: toBoolean(
|
OpenRestyCacheLockEnabled: toBoolean(
|
||||||
optionMap.OpenRestyCacheLockEnabled,
|
optionMap.OpenRestyCacheLockEnabled,
|
||||||
true,
|
true,
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ const proxyRouteSchema = z
|
|||||||
})()),
|
})()),
|
||||||
'请输入合法的回源主机名',
|
'请输入合法的回源主机名',
|
||||||
),
|
),
|
||||||
|
upstreams_text: z.string(),
|
||||||
enabled: z.boolean(),
|
enabled: z.boolean(),
|
||||||
enable_https: z.boolean(),
|
enable_https: z.boolean(),
|
||||||
cert_id: z.string(),
|
cert_id: z.string(),
|
||||||
@@ -108,6 +109,15 @@ const proxyRouteSchema = z
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const upstreams = parseUpstreamsText(value.origin_url, value.upstreams_text);
|
||||||
|
if (upstreams.length === 0) {
|
||||||
|
context.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
path: ['origin_url'],
|
||||||
|
message: '至少需要一个上游地址',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (value.cache_enabled) {
|
if (value.cache_enabled) {
|
||||||
const cacheRules = parseCacheRulesText(value.cache_rules_text);
|
const cacheRules = parseCacheRulesText(value.cache_rules_text);
|
||||||
if (value.cache_policy !== 'url' && cacheRules.length === 0) {
|
if (value.cache_policy !== 'url' && cacheRules.length === 0) {
|
||||||
@@ -164,6 +174,7 @@ const defaultValues: ProxyRouteFormValues = {
|
|||||||
domain: '',
|
domain: '',
|
||||||
origin_url: '',
|
origin_url: '',
|
||||||
origin_host: '',
|
origin_host: '',
|
||||||
|
upstreams_text: '',
|
||||||
enabled: true,
|
enabled: true,
|
||||||
enable_https: false,
|
enable_https: false,
|
||||||
cert_id: '',
|
cert_id: '',
|
||||||
@@ -234,6 +245,25 @@ function parseCacheRulesText(value: string) {
|
|||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseUpstreams(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 parseUpstreamsText(primary: string, value: string) {
|
||||||
|
return [primary.trim(), ...value.split(/\r?\n/)]
|
||||||
|
.map((item) => item.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
function buildCachePolicyLabel(policy: string) {
|
function buildCachePolicyLabel(policy: string) {
|
||||||
switch (policy) {
|
switch (policy) {
|
||||||
case 'suffix':
|
case 'suffix':
|
||||||
@@ -271,6 +301,7 @@ function toPayload(values: ProxyRouteFormValues): ProxyRouteMutationPayload {
|
|||||||
domain: values.domain.trim(),
|
domain: values.domain.trim(),
|
||||||
origin_url: values.origin_url.trim(),
|
origin_url: values.origin_url.trim(),
|
||||||
origin_host: values.origin_host.trim(),
|
origin_host: values.origin_host.trim(),
|
||||||
|
upstreams: parseUpstreamsText(values.origin_url, values.upstreams_text).slice(1),
|
||||||
enabled: values.enabled,
|
enabled: values.enabled,
|
||||||
enable_https: values.enable_https,
|
enable_https: values.enable_https,
|
||||||
cert_id:
|
cert_id:
|
||||||
@@ -291,11 +322,13 @@ 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);
|
const cacheRules = parseCacheRules(route.cache_rules);
|
||||||
|
const upstreams = parseUpstreams(route.upstreams);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
domain: route.domain,
|
domain: route.domain,
|
||||||
origin_url: route.origin_url,
|
origin_url: route.origin_url,
|
||||||
origin_host: route.origin_host || '',
|
origin_host: route.origin_host || '',
|
||||||
|
upstreams_text: upstreams.slice(1).join('\n'),
|
||||||
enabled: route.enabled,
|
enabled: route.enabled,
|
||||||
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) : '',
|
||||||
@@ -608,6 +641,7 @@ export function ProxyRoutesPage() {
|
|||||||
{routes.map((route) => {
|
{routes.map((route) => {
|
||||||
const headers = parseCustomHeaders(route.custom_headers);
|
const headers = parseCustomHeaders(route.custom_headers);
|
||||||
const cacheRules = parseCacheRules(route.cache_rules);
|
const cacheRules = parseCacheRules(route.cache_rules);
|
||||||
|
const upstreams = parseUpstreams(route.upstreams);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr key={route.id} className="align-top">
|
<tr key={route.id} className="align-top">
|
||||||
@@ -620,6 +654,9 @@ export function ProxyRoutesPage() {
|
|||||||
<p className="text-xs text-[var(--foreground-muted)]">
|
<p className="text-xs text-[var(--foreground-muted)]">
|
||||||
回源主机名: {route.origin_host || '$host'}
|
回源主机名: {route.origin_host || '$host'}
|
||||||
</p>
|
</p>
|
||||||
|
<p className="text-xs text-[var(--foreground-muted)]">
|
||||||
|
上游数量: {Math.max(upstreams.length, 1)}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-3 py-4">
|
<td className="px-3 py-4">
|
||||||
@@ -770,6 +807,17 @@ export function ProxyRoutesPage() {
|
|||||||
/>
|
/>
|
||||||
</ResourceField>
|
</ResourceField>
|
||||||
|
|
||||||
|
<ResourceField
|
||||||
|
label="附加上游"
|
||||||
|
hint="可选。每行一个上游地址,用于同一规则下的负载均衡;需与主上游保持相同协议,且不能带路径或查询参数。"
|
||||||
|
error={form.formState.errors.upstreams_text?.message}
|
||||||
|
>
|
||||||
|
<ResourceTextarea
|
||||||
|
placeholder={'https://origin-b.internal\nhttps://origin-c.internal'}
|
||||||
|
{...form.register('upstreams_text')}
|
||||||
|
/>
|
||||||
|
</ResourceField>
|
||||||
|
|
||||||
<div className="grid gap-4 lg:grid-cols-2">
|
<div className="grid gap-4 lg:grid-cols-2">
|
||||||
<ToggleField
|
<ToggleField
|
||||||
label="启用规则"
|
label="启用规则"
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export interface ProxyRouteItem {
|
|||||||
domain: string;
|
domain: string;
|
||||||
origin_url: string;
|
origin_url: string;
|
||||||
origin_host: string;
|
origin_host: string;
|
||||||
|
upstreams: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
enable_https: boolean;
|
enable_https: boolean;
|
||||||
cert_id: number | null;
|
cert_id: number | null;
|
||||||
@@ -25,6 +26,7 @@ export interface ProxyRouteMutationPayload {
|
|||||||
domain: string;
|
domain: string;
|
||||||
origin_url: string;
|
origin_url: string;
|
||||||
origin_host: string;
|
origin_host: string;
|
||||||
|
upstreams: string[];
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
enable_https: boolean;
|
enable_https: boolean;
|
||||||
cert_id: number | null;
|
cert_id: number | null;
|
||||||
|
|||||||
@@ -80,12 +80,12 @@ const defaultOperationFields = {
|
|||||||
OpenRestyWorkerRlimitNofile: '65535',
|
OpenRestyWorkerRlimitNofile: '65535',
|
||||||
OpenRestyEventsUse: 'epoll',
|
OpenRestyEventsUse: 'epoll',
|
||||||
OpenRestyEventsMultiAcceptEnabled: true,
|
OpenRestyEventsMultiAcceptEnabled: true,
|
||||||
OpenRestyKeepaliveTimeout: '65',
|
OpenRestyKeepaliveTimeout: '20',
|
||||||
OpenRestyKeepaliveRequests: '1000',
|
OpenRestyKeepaliveRequests: '1000',
|
||||||
OpenRestyClientHeaderTimeout: '15',
|
OpenRestyClientHeaderTimeout: '15',
|
||||||
OpenRestyClientBodyTimeout: '15',
|
OpenRestyClientBodyTimeout: '15',
|
||||||
OpenRestySendTimeout: '30',
|
OpenRestySendTimeout: '30',
|
||||||
OpenRestyProxyConnectTimeout: '5',
|
OpenRestyProxyConnectTimeout: '3',
|
||||||
OpenRestyProxySendTimeout: '60',
|
OpenRestyProxySendTimeout: '60',
|
||||||
OpenRestyProxyReadTimeout: '60',
|
OpenRestyProxyReadTimeout: '60',
|
||||||
OpenRestyProxyBufferingEnabled: true,
|
OpenRestyProxyBufferingEnabled: true,
|
||||||
@@ -100,7 +100,7 @@ const defaultOperationFields = {
|
|||||||
OpenRestyCacheLevels: '1:2',
|
OpenRestyCacheLevels: '1:2',
|
||||||
OpenRestyCacheInactive: '30m',
|
OpenRestyCacheInactive: '30m',
|
||||||
OpenRestyCacheMaxSize: '1g',
|
OpenRestyCacheMaxSize: '1g',
|
||||||
OpenRestyCacheKeyTemplate: '$scheme$proxy_host$request_uri',
|
OpenRestyCacheKeyTemplate: '$scheme$host$request_uri',
|
||||||
OpenRestyCacheLockEnabled: true,
|
OpenRestyCacheLockEnabled: true,
|
||||||
OpenRestyCacheLockTimeout: '5s',
|
OpenRestyCacheLockTimeout: '5s',
|
||||||
OpenRestyCacheUseStale:
|
OpenRestyCacheUseStale:
|
||||||
@@ -353,7 +353,7 @@ export function SettingsPage() {
|
|||||||
optionMap.OpenRestyEventsMultiAcceptEnabled,
|
optionMap.OpenRestyEventsMultiAcceptEnabled,
|
||||||
true,
|
true,
|
||||||
),
|
),
|
||||||
OpenRestyKeepaliveTimeout: optionMap.OpenRestyKeepaliveTimeout ?? '65',
|
OpenRestyKeepaliveTimeout: optionMap.OpenRestyKeepaliveTimeout ?? '20',
|
||||||
OpenRestyKeepaliveRequests:
|
OpenRestyKeepaliveRequests:
|
||||||
optionMap.OpenRestyKeepaliveRequests ?? '1000',
|
optionMap.OpenRestyKeepaliveRequests ?? '1000',
|
||||||
OpenRestyClientHeaderTimeout:
|
OpenRestyClientHeaderTimeout:
|
||||||
@@ -361,7 +361,7 @@ export function SettingsPage() {
|
|||||||
OpenRestyClientBodyTimeout: optionMap.OpenRestyClientBodyTimeout ?? '15',
|
OpenRestyClientBodyTimeout: optionMap.OpenRestyClientBodyTimeout ?? '15',
|
||||||
OpenRestySendTimeout: optionMap.OpenRestySendTimeout ?? '30',
|
OpenRestySendTimeout: optionMap.OpenRestySendTimeout ?? '30',
|
||||||
OpenRestyProxyConnectTimeout:
|
OpenRestyProxyConnectTimeout:
|
||||||
optionMap.OpenRestyProxyConnectTimeout ?? '5',
|
optionMap.OpenRestyProxyConnectTimeout ?? '3',
|
||||||
OpenRestyProxySendTimeout: optionMap.OpenRestyProxySendTimeout ?? '60',
|
OpenRestyProxySendTimeout: optionMap.OpenRestyProxySendTimeout ?? '60',
|
||||||
OpenRestyProxyReadTimeout: optionMap.OpenRestyProxyReadTimeout ?? '60',
|
OpenRestyProxyReadTimeout: optionMap.OpenRestyProxyReadTimeout ?? '60',
|
||||||
OpenRestyProxyBufferingEnabled: toBoolean(
|
OpenRestyProxyBufferingEnabled: toBoolean(
|
||||||
@@ -381,7 +381,7 @@ export function SettingsPage() {
|
|||||||
OpenRestyCacheInactive: optionMap.OpenRestyCacheInactive ?? '30m',
|
OpenRestyCacheInactive: optionMap.OpenRestyCacheInactive ?? '30m',
|
||||||
OpenRestyCacheMaxSize: optionMap.OpenRestyCacheMaxSize ?? '1g',
|
OpenRestyCacheMaxSize: optionMap.OpenRestyCacheMaxSize ?? '1g',
|
||||||
OpenRestyCacheKeyTemplate:
|
OpenRestyCacheKeyTemplate:
|
||||||
optionMap.OpenRestyCacheKeyTemplate ?? '$scheme$proxy_host$request_uri',
|
optionMap.OpenRestyCacheKeyTemplate ?? '$scheme$host$request_uri',
|
||||||
OpenRestyCacheLockEnabled: toBoolean(
|
OpenRestyCacheLockEnabled: toBoolean(
|
||||||
optionMap.OpenRestyCacheLockEnabled,
|
optionMap.OpenRestyCacheLockEnabled,
|
||||||
true,
|
true,
|
||||||
|
|||||||
Reference in New Issue
Block a user