mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-27 21:03:16 +00:00
feat(webui): 外部入口 base_url 配置 —— 穿透场景下链接不再靠猜
用户指出 webui 实际是经 https://homeagent.jianfgit.xyz/ 穿透出去的, 应当支持配置 base URL。实测确认了这个诉求的正当性。 ## 实测发现的约束(决定方案) 1. **子域形态在外部不可用**:`*.homeagent.jianfgit.xyz` 泛解析存在, 但外层只给 `*.jianfgit.xyz` 通配证书 —— 该证书**不匹配三级子域**, 实测 `huawei-smarthome.homeagent.jianfgit.xyz` 外部握手失败(HTTP 000)。 外层只放行 `homeagent.jianfgit.xyz` 这一个 Host。 2. **路径挂载形态外部可用**:实测 `https://homeagent.jianfgit.xyz/api/v1/device/online` → 200。 所以「一个外部 Host + 路径挂载」是这条链路的正解,且已经工作。 3. 外层 nginx/WAF 会带 `X-Forwarded-Proto: https` 与 `X-Forwarded-Host`, 因此即使不配置也能推出正确链接;配 base_url 则是显式兜底。 ## 新增设置项 base_url 三级优先解析「对外入口」(resolveEntry): 1. **配置项 base_url** —— 外部入口是部署事实,不该靠请求猜。 经多层网关时请求可能带内网 Host,按它推导会拼出用户点不开的链接。 2. **X-Forwarded-Proto / X-Forwarded-Host** —— 反代层给权威信息时可靠。 3. **请求自身** —— 直连时的正确来源。 base_url 的主机名同时用作**子域反代的基域名**:入口是 homeagent.example.com 时,插件服务自然是 <标签>.homeagent.example.com。 服务清单另增 entry_url 字段,直接给出「外部入口是什么」,便于前端与排错。 ## 生效点 - `/api/v1/proxy/services`:url / url_portal / base_domain / entry_url - `/api/v1/device/gateway`:url / url_portal / http_url / host - 两处原先各自推导 portalHost,现统一走 resolveEntry,避免再次漂移 ## 部署后实测(生产,经真实外部入口) entry_url: https://homeagent.jianfgit.xyz base_domain: homeagent.jianfgit.xyz remotedevice | https://devices.homeagent.jianfgit.xyz | https://homeagent.jianfgit.xyz/api/v1/device/ ← 外部可点 huawei_smarthome| https://huawei-smarthome.homeagent.jianfgit.xyz 发现端点(外部视角): url_portal = wss://homeagent.jianfgit.xyz/api/v1/device/ws ← 外部可连 ## 判据 +3 条:TestBaseURLOverridesRequestDerived(内网 Host 场景下必须用 base_url)、 TestEntryPrefersForwardedHeaders(XFF 优先于请求自身)、 TestBaseURLTolerant(尾斜杠/空格容错 —— 手填配置最常见的两种手误)。 另更新一条旧判据的期望值:外部入口是 portal.example.com 时,子域基名应取 **实际入口**而非本机配置的 localhost(后者对远程用户无意义)。
This commit is contained in:
@ -235,7 +235,11 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
||||
// 基域名:默认 localhost ⇒ <标签>.localhost:<端口> 开箱即用(RFC 6761 强制
|
||||
// 解析到 loopback,无需 DNS/证书/hosts)。远程访问时改成本机可达的域名,
|
||||
// 如 webui.example.com ⇒ <标签>.webui.example.com。
|
||||
s.Settings().RegisterDef(sdk.ConfigDef{Key: "base_domain", Default: "localhost", Type: "string", DisplayName: "反代基域名", Description: "插件服务反代的基域名。默认 localhost,此时 <插件标签>.localhost:<端口> 直接可用;远程访问填如 webui.example.com", Category: "webui"})
|
||||
// 外部入口 base URL:经 frp/nginx 穿透时,请求 Host 往往是内网地址或
|
||||
// 缺少协议信息,而生成给用户的链接必须是**外部可点的**。填这里即覆盖。
|
||||
// 例:https://homeagent.example.com —— 生成的链接一律用它做协议+主机。
|
||||
s.Settings().RegisterDef(sdk.ConfigDef{Key: "base_url", Default: "", Type: "string", DisplayName: "外部入口 Base URL", Description: "经反代/穿透暴露给外部的完整入口地址(含协议),如 https://homeagent.example.com。留空则按请求推导(直连时正确;经多层网关时可能拼错)。填了它,所有生成的外部链接都用它", Category: "webui"})
|
||||
s.Settings().RegisterDef(sdk.ConfigDef{Key: "base_domain", Default: "localhost", Type: "string", DisplayName: "反代基域名", Description: "插件服务按子域反代时用的基域名(如 webui.example.com 则 <插件标签>.webui.example.com)。默认 localhost 只对本机浏览器有效。若外层未放行子域,请改用手填反代条目或路径挂载", Category: "webui"})
|
||||
// 手填反代条目(自动发现之外的补充):每行 `<标签> <上游地址> [ws] [auth=none]`
|
||||
s.Settings().RegisterDef(sdk.ConfigDef{Key: "proxy_routes", Default: "", Type: "text", DisplayName: "手填反代条目", Description: "每行一条:<子域标签> <上游地址> [ws] [auth=none|homeagent]。插件的 proxies 声明会自动发现,这里只用于补充未声明/第三方服务。例:grafana 127.0.0.1:3000", Category: "webui"})
|
||||
s.Settings().RegisterDef(sdk.ConfigDef{Key: "device_gateway_enabled", Default: "false", Type: "bool", DisplayName: "设备网关反代", Description: "启用后 /api/v1/device/* 反代到 remotedevice 插件(默认关闭,避免硬耦合)", Category: "webui"})
|
||||
|
||||
@ -428,6 +428,78 @@ func portalHostWithPort(host, hostPort string) string {
|
||||
return h + p
|
||||
}
|
||||
|
||||
// baseURLSetting 读外部入口 base_url 设置(空 = 未配置,按请求推导)。
|
||||
func baseURLSetting(settings sdk.SettingsAPI) string {
|
||||
if settings == nil {
|
||||
return ""
|
||||
}
|
||||
v, err := settings.Get("base_url")
|
||||
if err != nil || v == nil {
|
||||
return ""
|
||||
}
|
||||
s, _ := v.(string)
|
||||
return strings.TrimRight(strings.TrimSpace(s), "/")
|
||||
}
|
||||
|
||||
// resolveEntry 解析「对外入口」的 scheme / host(含端口)/ 子域基域名。
|
||||
//
|
||||
// 三级优先,因为每种来源在不同部署下才可靠:
|
||||
//
|
||||
// 1. **配置项 base_url**(最可靠)。经 frp/nginx 穿透时,请求可能带内网
|
||||
// Host、或缺失协议,按请求推导会拼出用户点不开的链接。填了 base_url
|
||||
// 就一律以它为准 —— 这是「webui 应当支持配置 baseurl」的直接诉求:
|
||||
// 外部入口是**部署事实**,服务端不该靠猜。
|
||||
//
|
||||
// 2. **X-Forwarded-Proto / X-Forwarded-Host**(反代层正确设置时可靠)。
|
||||
// 实测本项目外层 nginx/WAF 会带 X-Forwarded-Proto=https 与
|
||||
// X-Forwarded-Host=<外部域名>,于是不需要任何配置也能推出正确链接。
|
||||
//
|
||||
// 3. **请求自身**(直连时的正确来源)。
|
||||
//
|
||||
// 返回的 host 保证端口恰好出现一次(见 portalHostWithPort)。
|
||||
func (h *Handler) resolveEntry(r *http.Request) (scheme, host, domain string) {
|
||||
_, port := h.proxySchemeAndPort(r)
|
||||
|
||||
if bu := baseURLSetting(h.settings); bu != "" {
|
||||
if u, err := url.Parse(bu); err == nil && u.Host != "" {
|
||||
scheme = u.Scheme
|
||||
if scheme == "" {
|
||||
scheme = "https"
|
||||
}
|
||||
host = u.Host // 已含端口(若有)
|
||||
// base_url 的**主机名**就是子域反代的基域名:外部入口是
|
||||
// homeagent.example.com 时,插件服务自然是 <标签>.homeagent.example.com。
|
||||
domain = u.Hostname()
|
||||
return scheme, host, domain
|
||||
}
|
||||
}
|
||||
|
||||
if p := r.Header.Get("X-Forwarded-Proto"); p != "" {
|
||||
scheme = strings.ToLower(strings.TrimSpace(strings.Split(p, ",")[0]))
|
||||
} else if r.TLS != nil {
|
||||
scheme = "https"
|
||||
} else {
|
||||
scheme = "http"
|
||||
}
|
||||
if fh := r.Header.Get("X-Forwarded-Host"); fh != "" {
|
||||
// 外层给了权威 Host:它的主机名即基域名。
|
||||
host = portalHostWithPort(strings.TrimSpace(strings.Split(fh, ",")[0]), port)
|
||||
if hn := hostnameOf(host); hn != "" {
|
||||
return scheme, host, hn
|
||||
}
|
||||
}
|
||||
host = portalHostWithPort(r.Host, port)
|
||||
return scheme, host, proxyBaseDomain(h.settings)
|
||||
}
|
||||
|
||||
// hostnameOf 去掉端口,返回纯主机名。
|
||||
func hostnameOf(hostport string) string {
|
||||
if h, _, err := net.SplitHostPort(hostport); err == nil {
|
||||
return h
|
||||
}
|
||||
return hostport
|
||||
}
|
||||
|
||||
// matchProxyPath 按**最长前缀**匹配路径挂载的服务。
|
||||
//
|
||||
// 边界要卡在路径分隔符上:/api/v1/device 不能匹配 /api/v1/devicefoo
|
||||
@ -574,7 +646,15 @@ type proxyServiceEntry struct {
|
||||
|
||||
// listProxyServices 汇总反代服务清单(含被拒条目,供配置页排错)。
|
||||
// schemePort 由调用方按当前请求推导(本机 http:8080 / 远程 https:443 等)。
|
||||
func (h *Handler) listProxyServices(scheme, hostPort, portalHost string) []proxyServiceEntry {
|
||||
// portOf 从 host:port 里取 ":port"(无端口返回空串)。
|
||||
func portOf(hostport string) string {
|
||||
if _, p, err := net.SplitHostPort(hostport); err == nil {
|
||||
return ":" + p
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (h *Handler) listProxyServices(scheme, hostPort, portalHost, domain string) []proxyServiceEntry {
|
||||
t := currentProxyTable()
|
||||
metas := map[string]sdk.PluginMeta{}
|
||||
if h.pluginMgr != nil {
|
||||
@ -600,7 +680,11 @@ func (h *Handler) listProxyServices(scheme, hostPort, portalHost string) []proxy
|
||||
e.PluginZh = r.Plugin
|
||||
}
|
||||
if r.Err == "" {
|
||||
e.URL = fmt.Sprintf("%s://%s.%s%s", scheme, r.Host, t.base, hostPort)
|
||||
baseForSub := t.base
|
||||
if domain != "" {
|
||||
baseForSub = domain
|
||||
}
|
||||
e.URL = fmt.Sprintf("%s://%s.%s%s", scheme, r.Host, baseForSub, hostPort)
|
||||
if r.Path != "" {
|
||||
// portalHostWithPort 保证端口恰好出现一次(见其注释:
|
||||
// 生产实例的 Host 自带 :8080,直接追加会拼出 8080:8080)
|
||||
@ -797,12 +881,8 @@ func (h *Handler) handleProxyServices(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
scheme, port := h.proxySchemeAndPort(r)
|
||||
portalHost := portalHostWithPort(r.Host, port)
|
||||
if hh := r.Header.Get("X-Forwarded-Host"); hh != "" {
|
||||
portalHost = portalHostWithPort(strings.TrimSpace(strings.Split(hh, ",")[0]), port)
|
||||
}
|
||||
svcs := h.listProxyServices(scheme, port, portalHost)
|
||||
scheme, portalHost, domain := h.resolveEntry(r)
|
||||
svcs := h.listProxyServices(scheme, portOf(portalHost), portalHost, domain)
|
||||
// 可达性探测:并发带超时,避免一个坏上游拖住整个清单。
|
||||
var wg sync.WaitGroup
|
||||
for i := range svcs {
|
||||
@ -819,15 +899,21 @@ func (h *Handler) handleProxyServices(w http.ResponseWriter, r *http.Request) {
|
||||
}(&svcs[i])
|
||||
}
|
||||
wg.Wait()
|
||||
base := "localhost"
|
||||
if h.settings != nil {
|
||||
base = proxyBaseDomain(h.settings)
|
||||
// base_domain 优先报告**推导出的基域名**(base_url / X-Forwarded-Host /
|
||||
// 配置项),它是前端拼子域链接的依据;只有推导不出时才回落配置项。
|
||||
base := domain
|
||||
if base == "" {
|
||||
base = "localhost"
|
||||
if h.settings != nil {
|
||||
base = proxyBaseDomain(h.settings)
|
||||
}
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"services": svcs,
|
||||
"base_domain": base,
|
||||
"scheme": scheme,
|
||||
"port": port,
|
||||
"port": portOf(portalHost),
|
||||
"entry_url": scheme + "://" + portalHost,
|
||||
})
|
||||
}
|
||||
|
||||
@ -888,7 +974,11 @@ func (h *Handler) handleDeviceGatewayDiscovery(w http.ResponseWriter, r *http.Re
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
scheme, port := h.proxySchemeAndPort(r)
|
||||
// 用 resolveEntry 而非 proxySchemeAndPort:前者认得 base_url 与
|
||||
// X-Forwarded-*,能给出**外部可点**的入口;后者只看请求自身,
|
||||
// 穿透场景下会拼出内网地址。
|
||||
scheme, portalHost, domain := h.resolveEntry(r)
|
||||
port := portOf(portalHost)
|
||||
wsScheme := "ws"
|
||||
if scheme == "https" {
|
||||
wsScheme = "wss"
|
||||
@ -923,15 +1013,14 @@ func (h *Handler) handleDeviceGatewayDiscovery(w http.ResponseWriter, r *http.Re
|
||||
// 无任何 DNS 依赖,永远可解析 ⇒ 设备客户端的正确选择。
|
||||
//
|
||||
// 两者都是「同一个端口」,单端口穿透的前提不受影响。
|
||||
out["host"] = route.Host + "." + t.base
|
||||
out["url"] = wsScheme + "://" + route.Host + "." + t.base + port + "/api/v1/device/ws"
|
||||
out["http_url"] = scheme + "://" + route.Host + "." + t.base + port
|
||||
// 门户同源形态:用**客户端实际访问用的 host**,保证它一定能解析。
|
||||
// portalHostWithPort 负责让端口恰好出现一次(Host 可能已带端口)。
|
||||
portalHost := portalHostWithPort(r.Host, port)
|
||||
if h := r.Header.Get("X-Forwarded-Host"); h != "" {
|
||||
portalHost = portalHostWithPort(strings.TrimSpace(strings.Split(h, ",")[0]), port)
|
||||
baseForSub := t.base
|
||||
if domain != "" {
|
||||
baseForSub = domain
|
||||
}
|
||||
out["host"] = route.Host + "." + baseForSub
|
||||
out["url"] = wsScheme + "://" + route.Host + "." + baseForSub + port + "/api/v1/device/ws"
|
||||
out["http_url"] = scheme + "://" + route.Host + "." + baseForSub + port
|
||||
// 门户同源形态:portalHost 已由上面的 resolveEntry 给出(外部可点的入口)。
|
||||
if route.Path != "" {
|
||||
// 声明了路径挂载 ⇒ 门户同源形态就是它(无 DNS 依赖,设备客户端首选)
|
||||
out["url_portal"] = wsScheme + "://" + portalHost + route.Path + "/ws"
|
||||
|
||||
@ -462,7 +462,7 @@ func TestListProxyServicesIncludesURLAndErrors(t *testing.T) {
|
||||
t.Cleanup(func() { SetProxyDeclProvider(prev); InvalidateProxyRoutes() })
|
||||
|
||||
h := NewHandler(proxyTestSettings(t))
|
||||
svcs := h.listProxyServices("http", ":8080", "localhost:8080")
|
||||
svcs := h.listProxyServices("http", ":8080", "localhost:8080", "localhost")
|
||||
if len(svcs) != 2 {
|
||||
t.Fatalf("入口数 = %d,期望 2(含坏条目)", len(svcs))
|
||||
}
|
||||
@ -925,8 +925,11 @@ func TestDeviceGatewayDiscovery(t *testing.T) {
|
||||
URL string `json:"url"`
|
||||
}
|
||||
json.Unmarshal(rec2.Body.Bytes(), &got2)
|
||||
if got2.URL != "wss://devices.localhost/api/v1/device/ws" {
|
||||
t.Errorf("https 场景 url = %q,期望 wss 且无端口", got2.URL)
|
||||
// 外部入口是 portal.example.com 时,插件服务自然挂在
|
||||
// devices.portal.example.com —— 子域基名取自**实际入口**,
|
||||
// 而不是本机配置的 localhost(后者对远程用户毫无意义)。
|
||||
if got2.URL != "wss://devices.portal.example.com/api/v1/device/ws" {
|
||||
t.Errorf("https 场景 url = %q,期望基于 X-Forwarded-Host 的 wss 地址且无端口", got2.URL)
|
||||
}
|
||||
|
||||
// ★ 安全:不得把设备令牌带回来(门户凭证不该换来设备执行权)
|
||||
@ -1312,3 +1315,130 @@ func TestDeviceGatewayDiscoveryNoDuplicatePort(t *testing.T) {
|
||||
t.Errorf("url_portal = %q", got.URLPortal)
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 外部入口 base_url ----
|
||||
//
|
||||
// 真实场景:webui 经 frp/nginx 穿透到 https://homeagent.example.com。
|
||||
// 此时请求可能带内网 Host、或缺失协议,按请求推导会拼出用户点不开的链接
|
||||
// (本项目实测:外层未放行子域,只有 homeagent.jianfgit.xyz 这一个 Host
|
||||
// 带通配证书,三级子域外部握手失败)。
|
||||
//
|
||||
// base_url 让「外部入口」成为**可配置的部署事实**,而不是靠猜。
|
||||
func TestBaseURLOverridesRequestDerived(t *testing.T) {
|
||||
cfgReg := internalConfig.NewConfigRegistry("")
|
||||
seedWebUIConfig(cfgReg)
|
||||
webuiCfg := cfgReg.PluginConfig("webui")
|
||||
webuiCfg.Set("base_url", "https://homeagent.example.com")
|
||||
settings := sdk.NewSettings("webui", cfgReg)
|
||||
|
||||
prev := declProvider
|
||||
SetProxyDeclProvider(func() []proxyDecl {
|
||||
return []proxyDecl{{
|
||||
Plugin: "remotedevice", Name: "gateway", Host: "devices",
|
||||
Path: "/api/v1/device", Target: "127.0.0.1:9890",
|
||||
WebSocket: true, Auth: sdk.ProxyAuthNone,
|
||||
}}
|
||||
})
|
||||
manualProxyRoutes = ""
|
||||
InvalidateProxyRoutes()
|
||||
t.Cleanup(func() { SetProxyDeclProvider(prev); InvalidateProxyRoutes() })
|
||||
|
||||
h := NewHandler(testSDK(sdk.SDKConfig{Settings: settings}))
|
||||
|
||||
// 请求来自内网(Host 是 127.0.0.1:8080)—— 这正是穿透场景的真实样子
|
||||
rec := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("GET", "/api/v1/device/gateway", nil)
|
||||
r.Host = "127.0.0.1:8080"
|
||||
h.handleDeviceGatewayDiscovery(rec, r)
|
||||
|
||||
var got struct {
|
||||
URL string `json:"url"`
|
||||
URLPortal string `json:"url_portal"`
|
||||
}
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// 门户同源形态必须是外部可点的
|
||||
if got.URLPortal != "wss://homeagent.example.com/api/v1/device/ws" {
|
||||
t.Errorf("url_portal = %q,期望用 base_url 推导的 wss 外部地址", got.URLPortal)
|
||||
}
|
||||
// 子域形态也必须用 base_url 的域(example.com → devices.example.com)
|
||||
if got.URL != "wss://devices.homeagent.example.com/api/v1/device/ws" {
|
||||
t.Errorf("url = %q,子域应基于 base_url 的主机名", got.URL)
|
||||
}
|
||||
|
||||
// 服务清单同样以 base_url 为准
|
||||
rec2 := httptest.NewRecorder()
|
||||
r2 := httptest.NewRequest("GET", "/api/v1/proxy/services", nil)
|
||||
r2.Host = "127.0.0.1:8080"
|
||||
r2.Header.Set("X-API-Key", "test-api-key")
|
||||
h.handleProxyServices(rec2, r2)
|
||||
var out struct {
|
||||
BaseDomain string `json:"base_domain"`
|
||||
EntryURL string `json:"entry_url"`
|
||||
Services []struct {
|
||||
URLPortal string `json:"url_portal"`
|
||||
} `json:"services"`
|
||||
}
|
||||
if err := json.Unmarshal(rec2.Body.Bytes(), &out); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if out.EntryURL != "https://homeagent.example.com" {
|
||||
t.Errorf("entry_url = %q", out.EntryURL)
|
||||
}
|
||||
if out.BaseDomain != "homeagent.example.com" {
|
||||
t.Errorf("base_domain = %q,应取自 base_url", out.BaseDomain)
|
||||
}
|
||||
if len(out.Services) != 1 || !strings.HasPrefix(out.Services[0].URLPortal, "https://homeagent.example.com/") {
|
||||
t.Errorf("服务入口未用 base_url: %+v", out.Services)
|
||||
}
|
||||
}
|
||||
|
||||
// 未配 base_url 时:X-Forwarded-* 优先于请求自身(反代层已给权威信息)。
|
||||
func TestEntryPrefersForwardedHeaders(t *testing.T) {
|
||||
h := NewHandler(proxyTestSettings(t))
|
||||
r := httptest.NewRequest("GET", "/", nil)
|
||||
r.Host = "10.0.0.5:8080" // 内网地址(穿透场景常见)
|
||||
r.Header.Set("X-Forwarded-Proto", "https")
|
||||
r.Header.Set("X-Forwarded-Host", "portal.example.com")
|
||||
scheme, host, domain := h.resolveEntry(r)
|
||||
if scheme != "https" {
|
||||
t.Errorf("scheme = %q,应取 X-Forwarded-Proto", scheme)
|
||||
}
|
||||
if host != "portal.example.com" {
|
||||
t.Errorf("host = %q,应取 X-Forwarded-Host", host)
|
||||
}
|
||||
if domain != "portal.example.com" {
|
||||
t.Errorf("domain = %q,应取 X-Forwarded-Host 的主机名", domain)
|
||||
}
|
||||
|
||||
// 都没有时回落到请求自身
|
||||
r2 := httptest.NewRequest("GET", "/", nil)
|
||||
r2.Host = "192.168.2.60:8080"
|
||||
s2, h2, d2 := h.resolveEntry(r2)
|
||||
if s2 != "http" || h2 != "192.168.2.60:8080" {
|
||||
t.Errorf("回落失败: scheme=%q host=%q", s2, h2)
|
||||
}
|
||||
if d2 != "localhost" {
|
||||
t.Errorf("未配 base_url 且无 XFF 时,基域名应回落配置默认值,实际 %q", d2)
|
||||
}
|
||||
}
|
||||
|
||||
// base_url 末尾斜杠/多余空格必须被容忍(手填配置最常见的两种手误)。
|
||||
func TestBaseURLTolerant(t *testing.T) {
|
||||
for _, raw := range []string{"https://h.example.com/", " https://h.example.com ", "https://h.example.com"} {
|
||||
cfgReg := internalConfig.NewConfigRegistry("")
|
||||
seedWebUIConfig(cfgReg)
|
||||
cfgReg.PluginConfig("webui").Set("base_url", raw)
|
||||
h := NewHandler(testSDK(sdk.SDKConfig{Settings: sdk.NewSettings("webui", cfgReg)}))
|
||||
r := httptest.NewRequest("GET", "/", nil)
|
||||
r.Host = "127.0.0.1:8080"
|
||||
_, host, domain := h.resolveEntry(r)
|
||||
if host != "h.example.com" {
|
||||
t.Errorf("base_url=%q → host=%q,期望 h.example.com(应容忍尾斜杠/空格)", raw, host)
|
||||
}
|
||||
if domain != "h.example.com" {
|
||||
t.Errorf("base_url=%q → domain=%q", raw, domain)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user