Skip to content

Support separate bind address and server name#49

Merged
Svtter merged 9 commits into
mainfrom
ralph/issue-48
May 14, 2026
Merged

Support separate bind address and server name#49
Svtter merged 9 commits into
mainfrom
ralph/issue-48

Conversation

@Svtter
Copy link
Copy Markdown
Contributor

@Svtter Svtter commented May 14, 2026

Problem

--host controls both the bind address and the externally-visible server name. When deploying on an internal machine that listens on 0.0.0.0, generated links and CORS origins expose 0.0.0.0 or 127.0.0.1 instead of the actual LAN IP (e.g. 192.168.2.14).

Approach

Add a --server-name CLI flag that decouples the public hostname/IP from the bind address.

  • internal/cli/cli.go: parse --server-name from flags, forward it to server.New.
  • internal/server/server.go:
    • New() now accepts a serverName parameter.
    • isValidServerName() validates the value — accepts IPv4 addresses and hostnames matching ^[a-zA-Z0-9]([a-zA-Z0-9.\-]*[a-zA-Z0-9])?$; rejects IPv6, URLs, and names with special characters.
    • Origin() returns http://<serverName>:<port> when set, otherwise falls back to the listener address.
    • Origins() returns a single-element slice with the server-name origin instead of expanding all interfaces.
  • Usage string updated to document --server-name.

Usage: sth start --host 0.0.0.0 --port 3939 --server-name 192.168.2.14

Tests

New tests in internal/server/server_test.go:

  • TestServerNameOverridesOrigin — verifies Origin() and Origins() return the custom address.
  • TestServerNameDomain — verifies hostname values like myhost.local work.
  • TestServerNameEmptyFallsBack — verifies empty serverName preserves existing behaviour.
  • TestServerNameValidation — rejects 21 invalid inputs (spaces, slashes, IPv6, URLs, leading/trailing dots/hyphens, etc.).
  • TestServerNameValidationAcceptsValid — accepts 10 valid inputs (IPv4, FQDN, short names, edge-case IPs).

All existing tests updated to pass the new serverName parameter to New().

Closes #48

Agent added 5 commits May 14, 2026 18:21
Add --server-name flag to 'sth start' that controls the externally
visible address while --host controls the actual bind address.

Usage: sth start --host 0.0.0.0 --port 3939 --server-name 192.168.2.14

Closes #48
- Validate --server-name rejects spaces, slashes, colons, and protocol prefixes
- Origins() now includes 127.0.0.1 as fallback when serverName is set
- Add tests for domain serverName, empty string fallback, and validation
Replace denylist-based serverName validation with positive allowlist
using net.ParseIP for IPs and hostname regex for domain names. Add
boundary test cases for special characters, leading/trailing dots, and
valid hostname patterns. Add HTTP-only scheme comment on Origin().
- Only accept IPv4 in isValidServerName to avoid generating invalid URLs
  (IPv6 addresses need bracket wrapping in URLs which is not supported)
- Move ip := address.IP after serverName check in Origin() for clarity
- Add IPv6 test cases to invalid serverName list
@github-actions
Copy link
Copy Markdown

此变更集功能完整、测试覆盖充分,代码风格与现有代码一致。以下为新引入的更具体的发现:

可合并

本次 PR 新增 --server-name 参数,将绑定的监听 IP 与对外暴露的主机名/IP 解耦,解决部署在 0.0.0.0 时生成链接出现 127.0.0.1 的问题。功能实现完整,API 设计合理,向后兼容,测试涵盖正向、逆向边界场景。

阻塞项:无

建议项

  1. 正则候选字串容错server.go:33):正则 ^[a-zA-Z0-9]([a-zA-Z0-9.\-]*[a-zA-Z0-9])?$ 会匹配含有连续点号的值(如 a..b),虽然用户已有 CLI 访问权限不算安全漏洞,但技术上这不是合法的 DNS 名称。可在 isValidServerName 中增加 strings.Contains(name, "..") 检查或者强化正则来拒绝这种情况。

New%20session%20-%202026-05-14T11%3A03%3A18.508Z
opencode session  |  github run

@github-actions
Copy link
Copy Markdown

发现遗漏

分析总结

将 Issue #48 作为 spec 对比 PR 代码,发现以下遗漏:

MEDIUM

  • baseURL() 不使用 serverName 构造对外返回的会话 URLhandleCreatePathSessionhandleCreateUploadedSession 中使用 baseURL(r)(基于 r.Host),而非 s.serverName。当客户端通过不同于 --server-name 的地址(如默认 127.0.0.1:3939)创建 session 时,API 返回的 URL(如 http://127.0.0.1:3939/s/<id>/)与预期不符,违背了 spec 中 --server-name 作为"对外显示/返回的地址"的定位。建议:在 Server 上添加 baseURL() 方法,当 serverName 非空时优先使用它,或在 handleCreatePathSession/handleCreateUploadedSession 中判断 s.serverName

LOW

  • --bind 别名未实现:Issue Support separate bind address and server name #48 spec 中提及 --host / --bind 作为监听地址参数,但 PR 仅实现了 --host,未添加 --bind 别名支持。建议:在 runStart 中增加对 flags["bind"] 的读取,作为 host 的备选来源。

New%20session%20-%202026-05-14T11%3A03%3A11.997Z
opencode session  |  github run

- Reject consecutive dots in server names (e.g. a..b) in isValidServerName
- Use serverName in session creation response URLs via serverBaseURL()
- Add --bind as alias for --host per issue spec
- Add test for serverName used in session URLs
- Add a..b and host..domain.com to invalid test cases
@github-actions
Copy link
Copy Markdown

发现遗漏

分析总结

将 Issue #48 作为 spec,对比 PR 实现代码,发现以下遗漏:

MEDIUM

  • serverBaseURL() 使用 s.port(配置端口)而非实际监听端口server.go:1342serverBaseURL() 使用 s.port(用户传入的配置值),而 Origin() (server.go:500) 和 Origins() (server.go:527) 正确使用 address.Port(从 listener 获取的实际端口)。当使用 --port 0 或端口自动分配时,API 返回的 session URL 会包含错误端口(如 http://192.168.2.14:0/s/<id>/)。Spec 要求 --server-name 控制"对外显示/返回的地址"——端口是地址的一部分,错误端口导致地址不完整。建议:在 serverBaseURL() 中使用 s.listener.Addr().(*net.TCPAddr).Port 替代 s.port

LOW

  • --bind 别名未完全实现且未在用法中文档化 — Issue Support separate bind address and server name #48 spec 提及 --host / --bind 作为监听地址参数,PR 在 cli.go:75-76 添加了 flags["bind"] 检查但仅作为 else if 回退(--host 优先),且 --bind 未出现在 usage string 中。虽然 parseArgs 接受任意 --key value,但用户无从知晓该选项。建议:将 --bind 作为 --host 的同等别名(而非回退),并在 usage 中补充文档。

New%20session%20-%202026-05-14T11%3A14%3A55.078Z
opencode session  |  github run

@github-actions
Copy link
Copy Markdown

可合并

总结

PR 功能完整、测试覆盖充分,新增 --server-name 参数将监听地址与对外暴露的主机名解耦,解决了 deploy 在 0.0.0.0 时生成链接显示 127.0.0.1 的问题。代码编译通过、go vet 无警告、新旧测试全部通过。已处理之前 review 中提出的 baseURL 不感知 serverName 的问题(新增 serverBaseURL() 方法)和 --bind 别名(已在 cli.go:76-78 中实现)。

阻塞项:无

建议项

  1. serverBaseURL() 使用了 s.port 而非实际监听端口server.go:1341)。Origin()Origins() 正确使用了 listener.Addr().(*net.TCPAddr).Port,而 serverBaseURL() 使用 s.port。当 port=0(如测试中随机端口分配)时,session 创建 API 返回的 URL 中包含 :0 而非实际端口。由于 CLI 层已拒绝 port <= 0,生产不受影响,但建议改为从 listener 获取实际端口以保持一致性。

  2. serverBaseURLserverBaseURL() 硬编码 http 方案server.go:1341)。与现有代码一致,但在 baseURL(r) 中已有 r.TLS != nil 判断。未来若支持 HTTPS,serverBaseURL 也应感知 TLS。

  3. isValidServerName 接受 0.0.0.0 作为合法值。技术上 IPv4 地址是合法的,但用户若传入 --server-name 0.0.0.0 仍会生成无意义链接。可考虑增加对 unspecified address 的检查。

New%20session%20-%202026-05-14T11%3A15%3A01.394Z
opencode session  |  github run

…-Proto, document --bind

- serverBaseURL now uses actual listener port instead of configured port
- serverBaseURL respects X-Forwarded-Proto header and TLS for scheme
- Add comment explaining why '..' check is needed despite regex
- Rename host variable to bindAddr in cli.go for clarity
- Document --bind flag in usage string
@github-actions
Copy link
Copy Markdown

可合并

本次 PR 新增 --server-name 参数,将监听地址与对外暴露的主机名解耦,解决了部署在 0.0.0.0 时生成链接出现 127.0.0.1 的问题。功能实现完整,验证逻辑严密(正则 + IP 解析 + 连续点号检测),测试覆盖充分(正向/边界/异常),向后兼容。

阻塞项:无

建议项:

  1. --bind--host 非等同别名cli.go:74-78):--bind 仅在 --host 未提供时生效,若同时传入两者则 --host 静默胜出。建议让二者完全等价,或将用法字符串中的 --bind 标记为备选以避免用户误解。
  2. serverBaseURLs.port 的默认回退server.go:1344):当 listener 尚未创建时回退到 s.port;若将来允许随机端口(如测试中 port=0)则可能生成 :0 的 URL。建议在 isValidServerName 中拒绝 unspecified 地址(0.0.0.0),因为将其作为服务名称无实际意义。

New%20session%20-%202026-05-14T11%3A18%3A42.518Z
opencode session  |  github run

…, improve regex

- serverBaseURL omits port 80 (http) and 443 (https) in generated URLs
- Extract determineScheme() for consistent scheme detection across baseURL and serverBaseURL
- Improve hostnamePattern regex to natively reject consecutive dots
- Warn when both --host and --bind are specified
- Add unit tests for serverBaseURL port handling, fallback, and X-Forwarded-Proto
@github-actions
Copy link
Copy Markdown

可合并

总览

PR 新增 --server-name 参数,将监听地址与对外暴露的主机名解耦,解决了部署在 0.0.0.0 时生成链接出现 127.0.0.1 的问题。实现完整,测试覆盖充分(8 个新测试 + 更新所有现有测试),向后兼容,已处理前期 review 反馈(serverBaseURL--bind 别名、listener 端口等),代码编译通过、测试全部通过。

关键设计评价

  • isValidServerName:先检查 net.ParseIP 再 fallback 到正则,正则能正确拒绝连续点号(如 a..b)、首尾点号/连字符等非法格式
  • serverBaseURL:正确使用 determineScheme(r) 感知 X-Forwarded-Proto,正确省略默认端口(http:80, https:443),优先从 listener 获取实际端口
  • Origin()/Origins():使用 address.Port(实际监听端口),与 serverBaseURL 行为一致
  • 并发安全:三个方法均正确使用 RLock/RUnlock

阻塞项:无

建议项

  1. --bind--host 非等同别名cli.go:76-83):当两者同时指定时 --host 胜出,但 usage 中 [--host 0.0.0.0] [--bind 0.0.0.0] 的并列写法可能让用户误以为它们是等价的可互换选项。建议将 --bind 标注为 --host 的别名/备选,或在 usage 中明确其优先级关系。不过这不是功能性缺陷。

  2. TestServerBaseURLDefaultPort/ForwardedProto/Fallback 未启动 server:这三个测试直接调用 serverBaseURL(r) 而不启动 srv.Start(),导致 listener 为 nil,依赖 s.port 回退。虽然测试逻辑正确(serverBaseURL 本身处理了 listener nil 的情况),但未覆盖实际 HTTP handler 调用路径。建议增加一个集成测试:启动 server 后通过 HTTP 请求创建 session 验证返回的 URL。当前已有 TestServerNameUsedInSessionURL 覆盖了该场景,所以非阻塞。

  3. isValidServerName 接受 0.0.0.0 等无效 IP:技术上 IPv4 地址合法,但 --server-name 0.0.0.0 生成的链接仍然无意义。可在 isValidServerName 中增加 ip.IsUnspecified() 检查来拒绝这类地址。

New%20session%20-%202026-05-14T11%3A23%3A15.154Z
opencode session  |  github run

@github-actions
Copy link
Copy Markdown

无遗漏

分析总结

将 Issue #48 作为 spec 对比 PR 实现,所有需求均已覆盖:

Spec 要求 vs 实现对照

要求 实现 状态
--host / --bind 作为监听地址 cli.go:74-83 同时解析两者,--host 优先,冲突时输出警告
--server-name CLI 参数 cli.go:97-100 解析并传递给 server.New
serverName 用于 Origin() server.go:500-502 返回 http://serverName:port
serverName 用于 Origins() server.go:526-529 返回单元素 slice
serverName 用于 API 返回的 session URL server.go:1340-1359 serverBaseURL() 方法,被 handleCreatePathSession(L842) 和 handleCreateUploadedSession(L962) 调用
isValidServerName() 验证 server.go:39-44 支持 IPv4 和主机名正则,拒绝 IPv6/URL/特殊字符
Usage 字符串更新 cli.go:57 包含 --server-name <addr>
向后兼容(空 serverName 回退) server.go:1358 回退到 baseURL(r)Origin()/Origins() 保留原行为
测试覆盖 6 个测试函数覆盖 Origin 覆盖、域名、空值回退、校验正反例、session URL 集成

此前 review 中发现的问题均已修复

  • baseURL() 不感知 serverName → 已新增 serverBaseURL() 方法
  • --bind 别名未实现 → 已添加
  • 连续点号 a..b 校验 → 正则已排除(TestServerNameValidation 包含 "a..b""host..domain.com"

PR 功能完整,spec 中所有需求均已实现,无遗漏。

New%20session%20-%202026-05-14T11%3A23%3A20.297Z
opencode session  |  github run

@github-actions
Copy link
Copy Markdown

无遗漏

将 Issue #48 作为 spec 对比 PR 实现,所有需求均已覆盖:

Spec 要求 实现位置 状态
--host / --bind 作为监听地址 cli.go:70-83 同时解析两者,--host 优先
--server-name CLI 参数 cli.go:94-97 解析并传递给 server.New
serverName 用于 Origin() server.go:500-502 返回 http://<serverName>:<port>
serverName 用于 Origins() server.go:526-529 返回单元素 slice
对外展示的 session URL 使用 serverName server.go:1340-1359 serverBaseURL(),被 handleCreatePathSessionhandleCreateUploadedSession 调用
isValidServerName() 校验 server.go:39-44 支持 IPv4 + 主机名正则,拒绝 IPv6/URL/特殊字符/连续点号
Usage 包含新参数 cli.go:57 包含 --server-name <addr>--bind
向后兼容(空 serverName 回退) server.go:1358 回退到 baseURL(r)Origin()/Origins() 保留原行为
测试覆盖 8 个新测试覆盖 Origin 覆盖、域名、空值回退、校验正反例、session URL 集成、端口省略、X-Forwarded-Proto

此前 review 中指出的 baseURL() 不感知 serverName--bind 别名缺失、连续点号校验等问题均已在当前版本中修复。PR 功能完整,无遗漏。

New%20session%20-%202026-05-14T11%3A28%3A40.500Z
opencode session  |  github run

@github-actions
Copy link
Copy Markdown

可合并

总结

PR 新增 --server-name 参数,将监听地址与对外暴露的主机名解耦,解决了 0.0.0.0 部署场景下生成链接显示 127.0.0.1 的问题。功能实现完整:serverBaseURL() 正确用于 session 创建 API 返回的 URL;isValidServerName 验证逻辑严密(正则准确拒绝连续点号、首尾连字符、IPv6 等非法格式);Origin()/Origins() 行为正确;测试全面覆盖正向、边界、异常场景。代码质量高,并发安全处理正确,向后兼容。

阻塞项:无

建议项

  1. --host vs --bind 优先级设计矛盾cli.go:73-83):警告信息标明 --host 已弃用、建议使用 --bind,但两者同时传入时 --host 却优先。若 --host 确实已弃用,应让 --bind 覆盖 --host(或报错拒绝二义性输入)。当前行为虽不影响功能,但会困惑用户。

  2. isValidServerName 接受 0.0.0.0 作为合法值server.go:39-44):--server-name 0.0.0.0 可通过校验,但生成的链接 http://0.0.0.0:3939 无实际意义。建议在 isValidServerName 中增加 ip.IsUnspecified() 检查拒绝这类地址。

  3. serverBaseURLs.port 默认回退server.go:1348):当 listener 为 nil 时回退到 s.port。当前代码路径不会触发此问题(handler 调用时 listener 必然已创建),但若未来在 Server 尚未 Start 时调用该方法可能生成 :0 的 URL。

New%20session%20-%202026-05-14T11%3A28%3A41.503Z
opencode session  |  github run

@Svtter Svtter merged commit 30a263c into main May 14, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support separate bind address and server name

1 participant