fix: startup banner hardcodes 127.0.0.1 regardless of server.host (#307)

The Web UI startup banner always printed 127.0.0.1, even when
server.host was bound to 0.0.0.0 or a specific interface. This made
users believe the host setting was ignored (the actual listen binding
was correct). Pass the configured host into the banner and expand
wildcard binds to the machine's real addresses so LAN URLs are shown.

Fixes #301

Co-authored-by: sycpro <sycpro@sycprodeMacBook-Pro.local>
This commit is contained in:
苏尼克
2026-09-13 15:27:38 +08:00
committed by GitHub
co-authored by sycpro
parent 6edc70f3fc
commit c563567502
3 changed files with 146 additions and 4 deletions
+1
View File
@@ -86,6 +86,7 @@ func main() {
}
termout.PrintStartupWebUI(termout.StartupWebUIOptions{
Scheme: scheme,
Host: cfg.Server.Host,
Port: port,
SelfSigned: scheme == "https" && cfg.Server.TLSAutoSelfSign,
HTTPRedirect: scheme == "https" && config.ServerHTTPRedirectEnabled(&cfg.Server),
+51 -4
View File
@@ -2,13 +2,17 @@ package termout
import (
"fmt"
"io"
"net"
"os"
"strconv"
"strings"
)
// StartupWebUIOptions configures the startup Web UI banner.
type StartupWebUIOptions struct {
Scheme string
Host string
Port int
SelfSigned bool
HTTPRedirect bool
@@ -22,9 +26,46 @@ func PrintConfigCreated() {
s.BlankLine()
}
// startupHosts 返回横幅应展示的访问地址。通配地址(含空 host)展开为回环地址
// 加本机非回环 IPv4;显式 host 原样展示。横幅此前硬编码 127.0.0.1,导致
// 绑定 0.0.0.0 的用户误以为 server.host 配置未生效(issue #301)。
func startupHosts(host string) []string {
host = strings.TrimSpace(host)
if host != "" && host != "0.0.0.0" && host != "::" && host != "[::]" {
return []string{host}
}
hosts := []string{"127.0.0.1"}
addrs, err := net.InterfaceAddrs()
if err != nil {
return hosts
}
for _, addr := range addrs {
ipNet, ok := addr.(*net.IPNet)
if !ok || ipNet.IP.IsLoopback() || ipNet.IP.To4() == nil {
continue
}
ip := ipNet.IP.String()
dup := false
for _, h := range hosts {
if h == ip {
dup = true
break
}
}
if !dup {
hosts = append(hosts, ip)
}
}
return hosts
}
// PrintStartupWebUI prints a colored startup banner for the Web UI.
func PrintStartupWebUI(opts StartupWebUIOptions) {
s := New(os.Stdout)
printStartupWebUI(os.Stdout, opts)
}
func printStartupWebUI(out io.Writer, opts StartupWebUIOptions) {
s := New(out)
scheme := opts.Scheme
if scheme == "" {
scheme = "http"
@@ -33,17 +74,23 @@ func PrintStartupWebUI(opts StartupWebUIOptions) {
if port <= 0 {
port = 8080
}
url := fmt.Sprintf("%s://127.0.0.1:%d/", scheme, port)
hosts := startupHosts(opts.Host)
urlFor := func(h string) string {
return scheme + "://" + net.JoinHostPort(h, strconv.Itoa(port)) + "/"
}
s.BlankLine()
s.Println(s.Bold(s.Cyan("CYBERSTRIKE AI")) + s.Dim(" / secure workspace"))
s.Println(s.Dim(strings.Repeat("─", 60)))
s.Println(s.Green("● ONLINE") + " " + s.Bold(s.White(url)))
s.Println(s.Green("● ONLINE") + " " + s.Bold(s.White(urlFor(hosts[0]))))
for _, h := range hosts[1:] {
s.Println(s.Dim(" Network ") + s.Bold(s.White(urlFor(h))))
}
if opts.SelfSigned {
s.Println(s.Dim(" TLS ") + s.Yellow("self-signed") + s.Dim(" · accept the browser warning once"))
}
if opts.HTTPRedirect {
s.Println(s.Dim(" Redirect ") + fmt.Sprintf("http://127.0.0.1:%d/ → HTTPS", port))
s.Println(s.Dim(" Redirect ") + fmt.Sprintf("http://%s/ → HTTPS", net.JoinHostPort(hosts[0], strconv.Itoa(port))))
}
s.BlankLine()
}
+94
View File
@@ -1,10 +1,104 @@
package termout
import (
"bytes"
"net"
"strings"
"testing"
)
func TestStartupHostsExplicitHost(t *testing.T) {
for _, host := range []string{"192.168.1.5", "127.0.0.1", "10.0.0.8"} {
got := startupHosts(host)
if len(got) != 1 || got[0] != host {
t.Fatalf("startupHosts(%q) = %v, want [%q]", host, got, host)
}
}
}
func TestStartupHostsWildcardExpandsToLocalAddresses(t *testing.T) {
for _, host := range []string{"", "0.0.0.0", "::", "[::]"} {
got := startupHosts(host)
if len(got) == 0 || got[0] != "127.0.0.1" {
t.Fatalf("startupHosts(%q) = %v, want first entry 127.0.0.1", host, got)
}
seen := map[string]bool{}
for _, h := range got {
if seen[h] {
t.Fatalf("startupHosts(%q) contains duplicate %q", host, h)
}
seen[h] = true
ip := net.ParseIP(h)
if ip == nil {
t.Fatalf("startupHosts(%q) returned non-IP %q", host, h)
}
if ip.IsLoopback() && h != "127.0.0.1" {
t.Fatalf("startupHosts(%q) returned unexpected loopback %q", host, h)
}
}
}
}
func TestPrintStartupWebUIReflectsConfiguredHost(t *testing.T) {
var buf bytes.Buffer
printStartupWebUI(&buf, StartupWebUIOptions{
Scheme: "http",
Host: "192.168.1.5",
Port: 8080,
})
out := buf.String()
if !strings.Contains(out, "http://192.168.1.5:8080/") {
t.Fatalf("banner should show configured host, got:\n%s", out)
}
if strings.Contains(out, "127.0.0.1") {
t.Fatalf("banner should not fall back to 127.0.0.1 for explicit host, got:\n%s", out)
}
}
func TestPrintStartupWebUIWildcardShowsLoopbackFirst(t *testing.T) {
var buf bytes.Buffer
printStartupWebUI(&buf, StartupWebUIOptions{
Scheme: "https",
Host: "0.0.0.0",
Port: 8443,
})
out := buf.String()
if !strings.Contains(out, "https://127.0.0.1:8443/") {
t.Fatalf("wildcard banner should include loopback URL, got:\n%s", out)
}
for _, h := range startupHosts("0.0.0.0")[1:] {
if !strings.Contains(out, "https://"+h+":8443/") {
t.Fatalf("wildcard banner should include network URL for %s, got:\n%s", h, out)
}
}
}
func TestPrintStartupWebUIIPv6HostBracketed(t *testing.T) {
var buf bytes.Buffer
printStartupWebUI(&buf, StartupWebUIOptions{
Scheme: "http",
Host: "::1",
Port: 8080,
})
if !strings.Contains(buf.String(), "http://[::1]:8080/") {
t.Fatalf("IPv6 host should be bracketed in URL, got:\n%s", buf.String())
}
}
func TestPrintStartupWebUIRedirectUsesConfiguredHost(t *testing.T) {
var buf bytes.Buffer
printStartupWebUI(&buf, StartupWebUIOptions{
Scheme: "https",
Host: "10.1.2.3",
Port: 8080,
HTTPRedirect: true,
})
out := buf.String()
if !strings.Contains(out, "http://10.1.2.3:8080/") || !strings.Contains(out, "https://10.1.2.3:8080/") {
t.Fatalf("redirect line should use configured host, got:\n%s", out)
}
}
func TestDisplayWidthEmoji(t *testing.T) {
if got := displayWidth("🚀"); got != 2 {
t.Fatalf("displayWidth(emoji) = %d, want 2", got)