mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-13 18:36:37 +08:00
[功能] 添加 origin_host 字段以覆盖回源请求的 Host 头
This commit is contained in:
@@ -120,6 +120,7 @@ Origin
|
|||||||
稳定约束:
|
稳定约束:
|
||||||
|
|
||||||
* 一个域名只对应一个 `origin_url`
|
* 一个域名只对应一个 `origin_url`
|
||||||
|
* `proxy_routes.origin_host` 为可选字段,用于回源时覆盖 `Host` 请求头;未设置时默认透传访问域名
|
||||||
* `proxy_routes.domain` 必须唯一
|
* `proxy_routes.domain` 必须唯一
|
||||||
* `origin_url` 必须为合法 `http://` 或 `https://`
|
* `origin_url` 必须为合法 `http://` 或 `https://`
|
||||||
* `config_versions` 必须保存完整快照、渲染结果与 `checksum`
|
* `config_versions` 必须保存完整快照、渲染结果与 `checksum`
|
||||||
|
|||||||
@@ -116,6 +116,7 @@
|
|||||||
|
|
||||||
* 不新增平台化对象,除非设计文档明确要求
|
* 不新增平台化对象,除非设计文档明确要求
|
||||||
* `proxy_routes` 维持一条域名对应一个 `origin_url`
|
* `proxy_routes` 维持一条域名对应一个 `origin_url`
|
||||||
|
* `proxy_routes.origin_host` 为可选字段,仅用于覆盖回源 `Host` 请求头,不引入新的平台化对象
|
||||||
* `config_versions` 必须保存完整快照与渲染结果
|
* `config_versions` 必须保存完整快照与渲染结果
|
||||||
* 全局同时只能有一个激活版本
|
* 全局同时只能有一个激活版本
|
||||||
* 回滚通过重新激活旧版本实现
|
* 回滚通过重新激活旧版本实现
|
||||||
|
|||||||
@@ -1537,6 +1537,9 @@ const docTemplate = `{
|
|||||||
"origin_url": {
|
"origin_url": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"origin_host": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"redirect_http": {
|
"redirect_http": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1534,6 +1534,9 @@
|
|||||||
"origin_url": {
|
"origin_url": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"origin_host": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"redirect_http": {
|
"redirect_http": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -80,6 +80,8 @@ definitions:
|
|||||||
type: boolean
|
type: boolean
|
||||||
origin_url:
|
origin_url:
|
||||||
type: string
|
type: string
|
||||||
|
origin_host:
|
||||||
|
type: string
|
||||||
redirect_http:
|
redirect_http:
|
||||||
type: boolean
|
type: boolean
|
||||||
remark:
|
remark:
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ type ProxyRoute struct {
|
|||||||
ID uint `json:"id" gorm:"primaryKey"`
|
ID uint `json:"id" gorm:"primaryKey"`
|
||||||
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"`
|
||||||
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"`
|
||||||
@@ -40,6 +41,7 @@ func (route *ProxyRoute) Update() error {
|
|||||||
return DB.Model(&ProxyRoute{}).Where("id = ?", route.ID).Updates(map[string]any{
|
return DB.Model(&ProxyRoute{}).Where("id = ?", route.ID).Updates(map[string]any{
|
||||||
"domain": route.Domain,
|
"domain": route.Domain,
|
||||||
"origin_url": route.OriginURL,
|
"origin_url": route.OriginURL,
|
||||||
|
"origin_host": route.OriginHost,
|
||||||
"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",
|
||||||
|
"origin_host": "origin-a.internal",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"remark": "primary route",
|
"remark": "primary route",
|
||||||
}
|
}
|
||||||
@@ -55,6 +56,9 @@ func TestPhase1PublishLifecycle(t *testing.T) {
|
|||||||
if createdRoute.Domain != "app.example.com" {
|
if createdRoute.Domain != "app.example.com" {
|
||||||
t.Fatalf("unexpected created route domain: %s", createdRoute.Domain)
|
t.Fatalf("unexpected created route domain: %s", createdRoute.Domain)
|
||||||
}
|
}
|
||||||
|
if createdRoute.OriginHost != "origin-a.internal" {
|
||||||
|
t.Fatalf("unexpected created route origin host: %s", createdRoute.OriginHost)
|
||||||
|
}
|
||||||
|
|
||||||
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
|
||||||
@@ -101,6 +105,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",
|
||||||
|
"origin_host": "origin-b.internal",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"remark": "updated route",
|
"remark": "updated route",
|
||||||
}
|
}
|
||||||
@@ -110,6 +115,9 @@ func TestPhase1PublishLifecycle(t *testing.T) {
|
|||||||
if createdRoute.OriginURL != "https://origin-b.internal" {
|
if createdRoute.OriginURL != "https://origin-b.internal" {
|
||||||
t.Fatalf("unexpected updated route origin: %s", createdRoute.OriginURL)
|
t.Fatalf("unexpected updated route origin: %s", createdRoute.OriginURL)
|
||||||
}
|
}
|
||||||
|
if createdRoute.OriginHost != "origin-b.internal" {
|
||||||
|
t.Fatalf("unexpected updated route origin host: %s", createdRoute.OriginHost)
|
||||||
|
}
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
@@ -384,6 +384,7 @@ func TestPhase2CustomHeadersPreviewAndDiffLifecycle(t *testing.T) {
|
|||||||
createResp := performJSONRequest(t, engine, token, http.MethodPost, "/api/proxy-routes/", map[string]any{
|
createResp := performJSONRequest(t, engine, token, http.MethodPost, "/api/proxy-routes/", map[string]any{
|
||||||
"domain": "preview.example.com",
|
"domain": "preview.example.com",
|
||||||
"origin_url": "https://origin-a.internal",
|
"origin_url": "https://origin-a.internal",
|
||||||
|
"origin_host": "preview-origin.internal",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"custom_headers": []map[string]any{
|
"custom_headers": []map[string]any{
|
||||||
{"key": "X-Trace-Id", "value": "$request_id"},
|
{"key": "X-Trace-Id", "value": "$request_id"},
|
||||||
@@ -394,12 +395,16 @@ func TestPhase2CustomHeadersPreviewAndDiffLifecycle(t *testing.T) {
|
|||||||
if !strings.Contains(createdRoute.CustomHeaders, "X-Trace-Id") {
|
if !strings.Contains(createdRoute.CustomHeaders, "X-Trace-Id") {
|
||||||
t.Fatalf("expected custom headers to be stored as json, got %s", createdRoute.CustomHeaders)
|
t.Fatalf("expected custom headers to be stored as json, got %s", createdRoute.CustomHeaders)
|
||||||
}
|
}
|
||||||
|
if createdRoute.OriginHost != "preview-origin.internal" {
|
||||||
|
t.Fatalf("expected origin_host to be stored, got %s", createdRoute.OriginHost)
|
||||||
|
}
|
||||||
|
|
||||||
performJSONRequest(t, engine, token, http.MethodPost, "/api/config-versions/publish", nil)
|
performJSONRequest(t, engine, token, http.MethodPost, "/api/config-versions/publish", nil)
|
||||||
|
|
||||||
performJSONRequest(t, engine, token, http.MethodPut, "/api/proxy-routes/"+toString(createdRoute.ID), map[string]any{
|
performJSONRequest(t, engine, token, http.MethodPut, "/api/proxy-routes/"+toString(createdRoute.ID), map[string]any{
|
||||||
"domain": "preview.example.com",
|
"domain": "preview.example.com",
|
||||||
"origin_url": "https://origin-b.internal",
|
"origin_url": "https://origin-b.internal",
|
||||||
|
"origin_host": "preview-upstream.internal",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"custom_headers": []map[string]any{
|
"custom_headers": []map[string]any{
|
||||||
{"key": "X-Trace-Id", "value": "$request_id"},
|
{"key": "X-Trace-Id", "value": "$request_id"},
|
||||||
@@ -419,6 +424,9 @@ func TestPhase2CustomHeadersPreviewAndDiffLifecycle(t *testing.T) {
|
|||||||
if !strings.Contains(renderedConfig, `proxy_set_header X-Release "candidate";`) {
|
if !strings.Contains(renderedConfig, `proxy_set_header X-Release "candidate";`) {
|
||||||
t.Fatalf("expected preview endpoint to return custom header, got %s", renderedConfig)
|
t.Fatalf("expected preview endpoint to return custom header, got %s", renderedConfig)
|
||||||
}
|
}
|
||||||
|
if !strings.Contains(renderedConfig, `proxy_set_header Host "preview-upstream.internal";`) {
|
||||||
|
t.Fatalf("expected preview endpoint to return overridden host header, got %s", renderedConfig)
|
||||||
|
}
|
||||||
|
|
||||||
diffResp := performJSONRequest(t, engine, token, http.MethodGet, "/api/config-versions/diff", nil)
|
diffResp := performJSONRequest(t, engine, token, http.MethodGet, "/api/config-versions/diff", nil)
|
||||||
var diff map[string]any
|
var diff map[string]any
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ type ConfigOptionDiffItem struct {
|
|||||||
type snapshotRoute struct {
|
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"`
|
||||||
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"`
|
||||||
@@ -379,6 +380,7 @@ func buildSnapshotRoutes(routes []*model.ProxyRoute) ([]snapshotRoute, error) {
|
|||||||
items = append(items, snapshotRoute{
|
items = append(items, snapshotRoute{
|
||||||
Domain: route.Domain,
|
Domain: route.Domain,
|
||||||
OriginURL: route.OriginURL,
|
OriginURL: route.OriginURL,
|
||||||
|
OriginHost: route.OriginHost,
|
||||||
Enabled: route.Enabled,
|
Enabled: route.Enabled,
|
||||||
EnableHTTPS: route.EnableHTTPS,
|
EnableHTTPS: route.EnableHTTPS,
|
||||||
CertID: route.CertID,
|
CertID: route.CertID,
|
||||||
@@ -424,7 +426,7 @@ func normalizeSnapshotRoutes(routes []snapshotRoute) []snapshotRoute {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func snapshotRouteConfigEqual(left snapshotRoute, right snapshotRoute) bool {
|
func snapshotRouteConfigEqual(left snapshotRoute, right snapshotRoute) bool {
|
||||||
if left.Domain != right.Domain || left.OriginURL != right.OriginURL || 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 || !uintPointerEqual(left.CertID, right.CertID) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if len(left.CustomHeaders) != len(right.CustomHeaders) {
|
if len(left.CustomHeaders) != len(right.CustomHeaders) {
|
||||||
@@ -593,7 +595,7 @@ func renderRouteConfig(routes []*model.ProxyRoute) (string, []SupportFile, error
|
|||||||
return "", nil, fmt.Errorf("路由 %s 自定义请求头无效", route.Domain)
|
return "", nil, fmt.Errorf("路由 %s 自定义请求头无效", route.Domain)
|
||||||
}
|
}
|
||||||
if !route.EnableHTTPS {
|
if !route.EnableHTTPS {
|
||||||
builder.WriteString(renderHTTPProxyServer(route.Domain, route.OriginURL, customHeaders))
|
builder.WriteString(renderHTTPProxyServer(route.Domain, route.OriginURL, route.OriginHost, customHeaders))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if route.CertID == nil || *route.CertID == 0 {
|
if route.CertID == nil || *route.CertID == 0 {
|
||||||
@@ -610,9 +612,9 @@ func renderRouteConfig(routes []*model.ProxyRoute) (string, []SupportFile, error
|
|||||||
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, customHeaders))
|
builder.WriteString(renderHTTPProxyServer(route.Domain, route.OriginURL, route.OriginHost, customHeaders))
|
||||||
}
|
}
|
||||||
builder.WriteString(renderHTTPSServer(route.Domain, route.OriginURL, certificate.ID, customHeaders))
|
builder.WriteString(renderHTTPSServer(route.Domain, route.OriginURL, route.OriginHost, certificate.ID, customHeaders))
|
||||||
}
|
}
|
||||||
return builder.String(), dedupeSupportFiles(supportFiles), nil
|
return builder.String(), dedupeSupportFiles(supportFiles), nil
|
||||||
}
|
}
|
||||||
@@ -746,27 +748,31 @@ 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, customHeaders []ProxyRouteCustomHeaderInput) string {
|
func renderHTTPProxyServer(domain string, originURL string, originHost string, customHeaders []ProxyRouteCustomHeaderInput) string {
|
||||||
return fmt.Sprintf("server {\n listen 80;\n server_name %s;\n%s\n location / {\n%s proxy_pass %s;\n }\n}\n\n", domain, renderExactHostGuard(domain), renderProxyHeaderBlock(customHeaders), originURL)
|
return fmt.Sprintf("server {\n listen 80;\n server_name %s;\n%s\n location / {\n%s proxy_pass %s;\n }\n}\n\n", domain, renderExactHostGuard(domain), renderProxyHeaderBlock(originHost, customHeaders), originURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderHTTPRedirectServer(domain string) string {
|
func renderHTTPRedirectServer(domain string) string {
|
||||||
return fmt.Sprintf("server {\n listen 80;\n server_name %s;\n%s\n return 301 https://$host$request_uri;\n}\n\n", domain, renderExactHostGuard(domain))
|
return fmt.Sprintf("server {\n listen 80;\n server_name %s;\n%s\n return 301 https://$host$request_uri;\n}\n\n", domain, renderExactHostGuard(domain))
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderHTTPSServer(domain string, originURL string, certificateID uint, customHeaders []ProxyRouteCustomHeaderInput) string {
|
func renderHTTPSServer(domain string, originURL string, originHost string, certificateID uint, customHeaders []ProxyRouteCustomHeaderInput) 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;\n server_name %s;\n ssl_certificate %s;\n ssl_certificate_key %s;\n%s\n location / {\n%s proxy_pass %s;\n }\n}\n\n", domain, certPath, keyPath, renderExactHostGuard(domain), renderProxyHeaderBlock(customHeaders), originURL)
|
return fmt.Sprintf("server {\n listen 443 ssl;\n server_name %s;\n ssl_certificate %s;\n ssl_certificate_key %s;\n%s\n location / {\n%s proxy_pass %s;\n }\n}\n\n", domain, certPath, keyPath, renderExactHostGuard(domain), renderProxyHeaderBlock(originHost, customHeaders), originURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderExactHostGuard(domain string) string {
|
func renderExactHostGuard(domain string) string {
|
||||||
return fmt.Sprintf(" if ($host != %q) {\n return 404;\n }\n", domain)
|
return fmt.Sprintf(" if ($host != %q) {\n return 404;\n }\n", domain)
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderProxyHeaderBlock(customHeaders []ProxyRouteCustomHeaderInput) string {
|
func renderProxyHeaderBlock(originHost string, customHeaders []ProxyRouteCustomHeaderInput) string {
|
||||||
var builder strings.Builder
|
var builder strings.Builder
|
||||||
|
if strings.TrimSpace(originHost) != "" {
|
||||||
|
builder.WriteString(fmt.Sprintf(" proxy_set_header Host %s;\n", quoteNginxHeaderValue(originHost)))
|
||||||
|
} else {
|
||||||
builder.WriteString(" proxy_set_header Host $host;\n")
|
builder.WriteString(" proxy_set_header Host $host;\n")
|
||||||
|
}
|
||||||
builder.WriteString(" proxy_set_header X-Real-IP $remote_addr;\n")
|
builder.WriteString(" proxy_set_header X-Real-IP $remote_addr;\n")
|
||||||
builder.WriteString(" proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n")
|
builder.WriteString(" proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n")
|
||||||
builder.WriteString(" proxy_set_header X-Forwarded-Proto $scheme;\n")
|
builder.WriteString(" proxy_set_header X-Forwarded-Proto $scheme;\n")
|
||||||
|
|||||||
@@ -140,6 +140,31 @@ func TestPublishConfigVersionRendersCustomHeaders(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPublishConfigVersionOverridesOriginHostHeader(t *testing.T) {
|
||||||
|
setupServiceTestDB(t)
|
||||||
|
|
||||||
|
_, err := CreateProxyRoute(ProxyRouteInput{
|
||||||
|
Domain: "git.arctel.de",
|
||||||
|
OriginURL: "https://git.arctel.net",
|
||||||
|
OriginHost: "git.arctel.net",
|
||||||
|
Enabled: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreateProxyRoute failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := PublishConfigVersion("root")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("PublishConfigVersion failed: %v", err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(result.Version.RenderedConfig, `proxy_set_header Host "git.arctel.net";`) {
|
||||||
|
t.Fatal("expected rendered config to override host header for origin routing")
|
||||||
|
}
|
||||||
|
if !strings.Contains(result.Version.SnapshotJSON, `"origin_host":"git.arctel.net"`) {
|
||||||
|
t.Fatal("expected snapshot to include origin_host override")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestPreviewConfigVersionCanDisableWebsocketHeaders(t *testing.T) {
|
func TestPreviewConfigVersionCanDisableWebsocketHeaders(t *testing.T) {
|
||||||
setupServiceTestDB(t)
|
setupServiceTestDB(t)
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ type ProxyRouteCustomHeaderInput struct {
|
|||||||
type ProxyRouteInput struct {
|
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"`
|
||||||
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"`
|
||||||
@@ -74,6 +75,7 @@ func DeleteProxyRoute(id uint) error {
|
|||||||
func buildProxyRoute(route *model.ProxyRoute, input ProxyRouteInput) (*model.ProxyRoute, error) {
|
func buildProxyRoute(route *model.ProxyRoute, input ProxyRouteInput) (*model.ProxyRoute, error) {
|
||||||
domain := strings.ToLower(strings.TrimSpace(input.Domain))
|
domain := strings.ToLower(strings.TrimSpace(input.Domain))
|
||||||
originURL := strings.TrimSpace(input.OriginURL)
|
originURL := strings.TrimSpace(input.OriginURL)
|
||||||
|
originHost := strings.TrimSpace(input.OriginHost)
|
||||||
remark := strings.TrimSpace(input.Remark)
|
remark := strings.TrimSpace(input.Remark)
|
||||||
customHeaders, err := normalizeCustomHeaders(input.CustomHeaders)
|
customHeaders, err := normalizeCustomHeaders(input.CustomHeaders)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -92,6 +94,9 @@ func buildProxyRoute(route *model.ProxyRoute, input ProxyRouteInput) (*model.Pro
|
|||||||
if err := validateOriginURL(originURL); err != nil {
|
if err := validateOriginURL(originURL); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if err := validateOriginHost(originHost); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
if !input.EnableHTTPS {
|
if !input.EnableHTTPS {
|
||||||
input.RedirectHTTP = false
|
input.RedirectHTTP = false
|
||||||
input.CertID = nil
|
input.CertID = nil
|
||||||
@@ -112,6 +117,7 @@ func buildProxyRoute(route *model.ProxyRoute, input ProxyRouteInput) (*model.Pro
|
|||||||
}
|
}
|
||||||
route.Domain = domain
|
route.Domain = domain
|
||||||
route.OriginURL = originURL
|
route.OriginURL = originURL
|
||||||
|
route.OriginHost = originHost
|
||||||
route.Enabled = input.Enabled
|
route.Enabled = input.Enabled
|
||||||
route.EnableHTTPS = input.EnableHTTPS
|
route.EnableHTTPS = input.EnableHTTPS
|
||||||
route.CertID = input.CertID
|
route.CertID = input.CertID
|
||||||
@@ -178,6 +184,23 @@ func validateOriginURL(raw string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validateOriginHost(raw string) error {
|
||||||
|
if raw == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if strings.ContainsAny(raw, "/\\ \t\r\n") || strings.Contains(raw, "://") {
|
||||||
|
return errors.New("回源主机名格式不合法")
|
||||||
|
}
|
||||||
|
parsed, err := url.Parse("//" + raw)
|
||||||
|
if err != nil || parsed.Host == "" || parsed.Host != raw {
|
||||||
|
return errors.New("回源主机名格式不合法")
|
||||||
|
}
|
||||||
|
if parsed.Hostname() == "" {
|
||||||
|
return errors.New("回源主机名格式不合法")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func isUniqueConstraintError(err error) bool {
|
func isUniqueConstraintError(err error) bool {
|
||||||
return err != nil && strings.Contains(strings.ToLower(err.Error()), "unique")
|
return err != nil && strings.Contains(strings.ToLower(err.Error()), "unique")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,6 +69,24 @@ const proxyRouteSchema = z
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}, '请输入合法的源站地址'),
|
}, '请输入合法的源站地址'),
|
||||||
|
origin_host: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.refine(
|
||||||
|
(value) =>
|
||||||
|
!value ||
|
||||||
|
(!/[\/\\\s]/.test(value) &&
|
||||||
|
!value.includes('://') &&
|
||||||
|
(() => {
|
||||||
|
try {
|
||||||
|
const parsed = new URL(`http://${value}`);
|
||||||
|
return parsed.host === value && Boolean(parsed.hostname);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
})()),
|
||||||
|
'请输入合法的回源主机名',
|
||||||
|
),
|
||||||
enabled: z.boolean(),
|
enabled: z.boolean(),
|
||||||
enable_https: z.boolean(),
|
enable_https: z.boolean(),
|
||||||
cert_id: z.string(),
|
cert_id: z.string(),
|
||||||
@@ -129,6 +147,7 @@ type FeedbackState = {
|
|||||||
const defaultValues: ProxyRouteFormValues = {
|
const defaultValues: ProxyRouteFormValues = {
|
||||||
domain: '',
|
domain: '',
|
||||||
origin_url: '',
|
origin_url: '',
|
||||||
|
origin_host: '',
|
||||||
enabled: true,
|
enabled: true,
|
||||||
enable_https: false,
|
enable_https: false,
|
||||||
cert_id: '',
|
cert_id: '',
|
||||||
@@ -186,6 +205,7 @@ function toPayload(values: ProxyRouteFormValues): ProxyRouteMutationPayload {
|
|||||||
return {
|
return {
|
||||||
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(),
|
||||||
enabled: values.enabled,
|
enabled: values.enabled,
|
||||||
enable_https: values.enable_https,
|
enable_https: values.enable_https,
|
||||||
cert_id:
|
cert_id:
|
||||||
@@ -204,6 +224,7 @@ function toFormValues(route: ProxyRouteItem): ProxyRouteFormValues {
|
|||||||
return {
|
return {
|
||||||
domain: route.domain,
|
domain: route.domain,
|
||||||
origin_url: route.origin_url,
|
origin_url: route.origin_url,
|
||||||
|
origin_host: route.origin_host || '',
|
||||||
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) : '',
|
||||||
@@ -510,7 +531,12 @@ export function ProxyRoutesPage() {
|
|||||||
{route.domain}
|
{route.domain}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-3 py-4 text-[var(--foreground-secondary)]">
|
<td className="px-3 py-4 text-[var(--foreground-secondary)]">
|
||||||
{route.origin_url}
|
<div className="space-y-1">
|
||||||
|
<p>{route.origin_url}</p>
|
||||||
|
<p className="text-xs text-[var(--foreground-muted)]">
|
||||||
|
回源主机名: {route.origin_host || '$host'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-3 py-4">
|
<td className="px-3 py-4">
|
||||||
{route.enable_https ? (
|
{route.enable_https ? (
|
||||||
@@ -633,6 +659,16 @@ export function ProxyRoutesPage() {
|
|||||||
</ResourceField>
|
</ResourceField>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ResourceField
|
||||||
|
label="回源主机名"
|
||||||
|
hint="可选。填写后将覆盖回源请求的 Host,留空则默认使用访问域名 $host"
|
||||||
|
error={form.formState.errors.origin_host?.message}
|
||||||
|
>
|
||||||
|
<ResourceInput
|
||||||
|
{...form.register('origin_host')}
|
||||||
|
/>
|
||||||
|
</ResourceField>
|
||||||
|
|
||||||
<div className="grid gap-4 lg:grid-cols-2">
|
<div className="grid gap-4 lg:grid-cols-2">
|
||||||
<ToggleField
|
<ToggleField
|
||||||
label="启用规则"
|
label="启用规则"
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export interface ProxyRouteItem {
|
|||||||
id: number;
|
id: number;
|
||||||
domain: string;
|
domain: string;
|
||||||
origin_url: string;
|
origin_url: string;
|
||||||
|
origin_host: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
enable_https: boolean;
|
enable_https: boolean;
|
||||||
cert_id: number | null;
|
cert_id: number | null;
|
||||||
@@ -20,6 +21,7 @@ export interface ProxyRouteItem {
|
|||||||
export interface ProxyRouteMutationPayload {
|
export interface ProxyRouteMutationPayload {
|
||||||
domain: string;
|
domain: string;
|
||||||
origin_url: string;
|
origin_url: string;
|
||||||
|
origin_host: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
enable_https: boolean;
|
enable_https: boolean;
|
||||||
cert_id: number | null;
|
cert_id: number | null;
|
||||||
|
|||||||
@@ -397,7 +397,12 @@ export function WebsiteDetailPage({websiteId}: { websiteId: string }) {
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-3 py-4 text-[var(--foreground-secondary)]">
|
<td className="px-3 py-4 text-[var(--foreground-secondary)]">
|
||||||
<p className="max-w-72 break-all">{route.origin_url}</p>
|
<div className="max-w-72 space-y-1 break-all">
|
||||||
|
<p>{route.origin_url}</p>
|
||||||
|
<p className="text-xs text-[var(--foreground-muted)]">
|
||||||
|
回源主机名: {route.origin_host || '$host'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-3 py-4">
|
<td className="px-3 py-4">
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
|
|||||||
Reference in New Issue
Block a user