mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-30 06:16:37 +08:00
[功能] 添加 origin_host 字段以覆盖回源请求的 Host 头
This commit is contained in:
@@ -120,6 +120,7 @@ Origin
|
||||
稳定约束:
|
||||
|
||||
* 一个域名只对应一个 `origin_url`
|
||||
* `proxy_routes.origin_host` 为可选字段,用于回源时覆盖 `Host` 请求头;未设置时默认透传访问域名
|
||||
* `proxy_routes.domain` 必须唯一
|
||||
* `origin_url` 必须为合法 `http://` 或 `https://`
|
||||
* `config_versions` 必须保存完整快照、渲染结果与 `checksum`
|
||||
|
||||
@@ -116,6 +116,7 @@
|
||||
|
||||
* 不新增平台化对象,除非设计文档明确要求
|
||||
* `proxy_routes` 维持一条域名对应一个 `origin_url`
|
||||
* `proxy_routes.origin_host` 为可选字段,仅用于覆盖回源 `Host` 请求头,不引入新的平台化对象
|
||||
* `config_versions` 必须保存完整快照与渲染结果
|
||||
* 全局同时只能有一个激活版本
|
||||
* 回滚通过重新激活旧版本实现
|
||||
|
||||
@@ -1537,6 +1537,9 @@ const docTemplate = `{
|
||||
"origin_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"origin_host": {
|
||||
"type": "string"
|
||||
},
|
||||
"redirect_http": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
||||
@@ -1534,6 +1534,9 @@
|
||||
"origin_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"origin_host": {
|
||||
"type": "string"
|
||||
},
|
||||
"redirect_http": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -1574,4 +1577,4 @@
|
||||
"in": "header"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,6 +80,8 @@ definitions:
|
||||
type: boolean
|
||||
origin_url:
|
||||
type: string
|
||||
origin_host:
|
||||
type: string
|
||||
redirect_http:
|
||||
type: boolean
|
||||
remark:
|
||||
|
||||
@@ -6,6 +6,7 @@ type ProxyRoute struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
Domain string `json:"domain" gorm:"uniqueIndex;size:255;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"`
|
||||
EnableHTTPS bool `json:"enable_https" gorm:"column:enable_https;not null;default:false"`
|
||||
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{
|
||||
"domain": route.Domain,
|
||||
"origin_url": route.OriginURL,
|
||||
"origin_host": route.OriginHost,
|
||||
"enabled": route.Enabled,
|
||||
"enable_https": route.EnableHTTPS,
|
||||
"cert_id": route.CertID,
|
||||
|
||||
@@ -44,10 +44,11 @@ func TestPhase1PublishLifecycle(t *testing.T) {
|
||||
token := prepareRootToken(t)
|
||||
|
||||
createBody := map[string]any{
|
||||
"domain": "app.example.com",
|
||||
"origin_url": "https://origin-a.internal",
|
||||
"enabled": true,
|
||||
"remark": "primary route",
|
||||
"domain": "app.example.com",
|
||||
"origin_url": "https://origin-a.internal",
|
||||
"origin_host": "origin-a.internal",
|
||||
"enabled": true,
|
||||
"remark": "primary route",
|
||||
}
|
||||
resp := performJSONRequest(t, engine, token, http.MethodPost, "/api/proxy-routes/", createBody)
|
||||
var createdRoute model.ProxyRoute
|
||||
@@ -55,6 +56,9 @@ func TestPhase1PublishLifecycle(t *testing.T) {
|
||||
if createdRoute.Domain != "app.example.com" {
|
||||
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)
|
||||
var routes []model.ProxyRoute
|
||||
@@ -99,10 +103,11 @@ func TestPhase1PublishLifecycle(t *testing.T) {
|
||||
initialRendered := version1.RenderedConfig
|
||||
|
||||
updateBody := map[string]any{
|
||||
"domain": "app.example.com",
|
||||
"origin_url": "https://origin-b.internal",
|
||||
"enabled": true,
|
||||
"remark": "updated route",
|
||||
"domain": "app.example.com",
|
||||
"origin_url": "https://origin-b.internal",
|
||||
"origin_host": "origin-b.internal",
|
||||
"enabled": true,
|
||||
"remark": "updated route",
|
||||
}
|
||||
routePath := "/api/proxy-routes/" + toString(createdRoute.ID)
|
||||
resp = performJSONRequest(t, engine, token, http.MethodPut, routePath, updateBody)
|
||||
@@ -110,6 +115,9 @@ func TestPhase1PublishLifecycle(t *testing.T) {
|
||||
if createdRoute.OriginURL != "https://origin-b.internal" {
|
||||
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)
|
||||
var version2 model.ConfigVersion
|
||||
|
||||
@@ -382,9 +382,10 @@ func TestPhase2CustomHeadersPreviewAndDiffLifecycle(t *testing.T) {
|
||||
token := prepareRootToken(t)
|
||||
|
||||
createResp := performJSONRequest(t, engine, token, http.MethodPost, "/api/proxy-routes/", map[string]any{
|
||||
"domain": "preview.example.com",
|
||||
"origin_url": "https://origin-a.internal",
|
||||
"enabled": true,
|
||||
"domain": "preview.example.com",
|
||||
"origin_url": "https://origin-a.internal",
|
||||
"origin_host": "preview-origin.internal",
|
||||
"enabled": true,
|
||||
"custom_headers": []map[string]any{
|
||||
{"key": "X-Trace-Id", "value": "$request_id"},
|
||||
},
|
||||
@@ -394,13 +395,17 @@ func TestPhase2CustomHeadersPreviewAndDiffLifecycle(t *testing.T) {
|
||||
if !strings.Contains(createdRoute.CustomHeaders, "X-Trace-Id") {
|
||||
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.MethodPut, "/api/proxy-routes/"+toString(createdRoute.ID), map[string]any{
|
||||
"domain": "preview.example.com",
|
||||
"origin_url": "https://origin-b.internal",
|
||||
"enabled": true,
|
||||
"domain": "preview.example.com",
|
||||
"origin_url": "https://origin-b.internal",
|
||||
"origin_host": "preview-upstream.internal",
|
||||
"enabled": true,
|
||||
"custom_headers": []map[string]any{
|
||||
{"key": "X-Trace-Id", "value": "$request_id"},
|
||||
{"key": "X-Release", "value": "candidate"},
|
||||
@@ -419,6 +424,9 @@ func TestPhase2CustomHeadersPreviewAndDiffLifecycle(t *testing.T) {
|
||||
if !strings.Contains(renderedConfig, `proxy_set_header X-Release "candidate";`) {
|
||||
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)
|
||||
var diff map[string]any
|
||||
|
||||
@@ -54,6 +54,7 @@ type ConfigOptionDiffItem struct {
|
||||
type snapshotRoute struct {
|
||||
Domain string `json:"domain"`
|
||||
OriginURL string `json:"origin_url"`
|
||||
OriginHost string `json:"origin_host,omitempty"`
|
||||
Enabled bool `json:"enabled"`
|
||||
EnableHTTPS bool `json:"enable_https"`
|
||||
CertID *uint `json:"cert_id,omitempty"`
|
||||
@@ -379,6 +380,7 @@ func buildSnapshotRoutes(routes []*model.ProxyRoute) ([]snapshotRoute, error) {
|
||||
items = append(items, snapshotRoute{
|
||||
Domain: route.Domain,
|
||||
OriginURL: route.OriginURL,
|
||||
OriginHost: route.OriginHost,
|
||||
Enabled: route.Enabled,
|
||||
EnableHTTPS: route.EnableHTTPS,
|
||||
CertID: route.CertID,
|
||||
@@ -424,7 +426,7 @@ func normalizeSnapshotRoutes(routes []snapshotRoute) []snapshotRoute {
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
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)
|
||||
}
|
||||
if !route.EnableHTTPS {
|
||||
builder.WriteString(renderHTTPProxyServer(route.Domain, route.OriginURL, customHeaders))
|
||||
builder.WriteString(renderHTTPProxyServer(route.Domain, route.OriginURL, route.OriginHost, customHeaders))
|
||||
continue
|
||||
}
|
||||
if route.CertID == nil || *route.CertID == 0 {
|
||||
@@ -610,9 +612,9 @@ func renderRouteConfig(routes []*model.ProxyRoute) (string, []SupportFile, error
|
||||
if route.RedirectHTTP {
|
||||
builder.WriteString(renderHTTPRedirectServer(route.Domain))
|
||||
} 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
|
||||
}
|
||||
@@ -746,27 +748,31 @@ func nextVersionNumber(now time.Time) (string, error) {
|
||||
return fmt.Sprintf("%s-%03d", prefix, count+1), nil
|
||||
}
|
||||
|
||||
func renderHTTPProxyServer(domain string, originURL 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)
|
||||
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(originHost, customHeaders), originURL)
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
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))
|
||||
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 {
|
||||
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
|
||||
builder.WriteString(" proxy_set_header Host $host;\n")
|
||||
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 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-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) {
|
||||
setupServiceTestDB(t)
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ type ProxyRouteCustomHeaderInput struct {
|
||||
type ProxyRouteInput struct {
|
||||
Domain string `json:"domain"`
|
||||
OriginURL string `json:"origin_url"`
|
||||
OriginHost string `json:"origin_host"`
|
||||
Enabled bool `json:"enabled"`
|
||||
EnableHTTPS bool `json:"enable_https"`
|
||||
CertID *uint `json:"cert_id"`
|
||||
@@ -74,6 +75,7 @@ func DeleteProxyRoute(id uint) error {
|
||||
func buildProxyRoute(route *model.ProxyRoute, input ProxyRouteInput) (*model.ProxyRoute, error) {
|
||||
domain := strings.ToLower(strings.TrimSpace(input.Domain))
|
||||
originURL := strings.TrimSpace(input.OriginURL)
|
||||
originHost := strings.TrimSpace(input.OriginHost)
|
||||
remark := strings.TrimSpace(input.Remark)
|
||||
customHeaders, err := normalizeCustomHeaders(input.CustomHeaders)
|
||||
if err != nil {
|
||||
@@ -92,6 +94,9 @@ func buildProxyRoute(route *model.ProxyRoute, input ProxyRouteInput) (*model.Pro
|
||||
if err := validateOriginURL(originURL); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := validateOriginHost(originHost); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !input.EnableHTTPS {
|
||||
input.RedirectHTTP = false
|
||||
input.CertID = nil
|
||||
@@ -112,6 +117,7 @@ func buildProxyRoute(route *model.ProxyRoute, input ProxyRouteInput) (*model.Pro
|
||||
}
|
||||
route.Domain = domain
|
||||
route.OriginURL = originURL
|
||||
route.OriginHost = originHost
|
||||
route.Enabled = input.Enabled
|
||||
route.EnableHTTPS = input.EnableHTTPS
|
||||
route.CertID = input.CertID
|
||||
@@ -178,6 +184,23 @@ func validateOriginURL(raw string) error {
|
||||
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 {
|
||||
return err != nil && strings.Contains(strings.ToLower(err.Error()), "unique")
|
||||
}
|
||||
|
||||
@@ -69,6 +69,24 @@ const proxyRouteSchema = z
|
||||
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(),
|
||||
enable_https: z.boolean(),
|
||||
cert_id: z.string(),
|
||||
@@ -129,6 +147,7 @@ type FeedbackState = {
|
||||
const defaultValues: ProxyRouteFormValues = {
|
||||
domain: '',
|
||||
origin_url: '',
|
||||
origin_host: '',
|
||||
enabled: true,
|
||||
enable_https: false,
|
||||
cert_id: '',
|
||||
@@ -186,6 +205,7 @@ function toPayload(values: ProxyRouteFormValues): ProxyRouteMutationPayload {
|
||||
return {
|
||||
domain: values.domain.trim(),
|
||||
origin_url: values.origin_url.trim(),
|
||||
origin_host: values.origin_host.trim(),
|
||||
enabled: values.enabled,
|
||||
enable_https: values.enable_https,
|
||||
cert_id:
|
||||
@@ -204,6 +224,7 @@ function toFormValues(route: ProxyRouteItem): ProxyRouteFormValues {
|
||||
return {
|
||||
domain: route.domain,
|
||||
origin_url: route.origin_url,
|
||||
origin_host: route.origin_host || '',
|
||||
enabled: route.enabled,
|
||||
enable_https: route.enable_https,
|
||||
cert_id: route.cert_id ? String(route.cert_id) : '',
|
||||
@@ -510,7 +531,12 @@ export function ProxyRoutesPage() {
|
||||
{route.domain}
|
||||
</td>
|
||||
<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 className="px-3 py-4">
|
||||
{route.enable_https ? (
|
||||
@@ -633,6 +659,16 @@ export function ProxyRoutesPage() {
|
||||
</ResourceField>
|
||||
</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">
|
||||
<ToggleField
|
||||
label="启用规则"
|
||||
|
||||
@@ -7,6 +7,7 @@ export interface ProxyRouteItem {
|
||||
id: number;
|
||||
domain: string;
|
||||
origin_url: string;
|
||||
origin_host: string;
|
||||
enabled: boolean;
|
||||
enable_https: boolean;
|
||||
cert_id: number | null;
|
||||
@@ -20,6 +21,7 @@ export interface ProxyRouteItem {
|
||||
export interface ProxyRouteMutationPayload {
|
||||
domain: string;
|
||||
origin_url: string;
|
||||
origin_host: string;
|
||||
enabled: boolean;
|
||||
enable_https: boolean;
|
||||
cert_id: number | null;
|
||||
|
||||
@@ -397,7 +397,12 @@ export function WebsiteDetailPage({websiteId}: { websiteId: string }) {
|
||||
</div>
|
||||
</td>
|
||||
<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 className="px-3 py-4">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
|
||||
Reference in New Issue
Block a user