Web UI over plain HTTP redirects to HTTPS when admin_http forbids HTTP
CI / agent (push) Successful in 3s
CI / site (push) Successful in 3s
CI / go (push) Successful in 35s

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Ilker ManapandClaude Opus 5.5 committed 2026-10-28 14:16:36 +01:00
1 parent 15537b90f0
commit 28a0c2952f
3 files changed
+63 -1

No files matched your search

+2
View File
@@ -68,6 +68,8 @@ publish a tag that has no section here.
`rollcall controllers` and the web UI.
- Addresses on secondary subnets with a gateway get their own routing table and a source rule in
`network-config`, so replies from a storage or tenant VLAN leave through that VLAN's gateway.
- With `admin_http` other than `allow`, browsers that open the web UI over HTTP are redirected to
HTTPS (nodes keep HTTP for images and the agent).
- `rollcall version`; CD-built binary carries `git describe`.
- Release notes from `CHANGELOG.md`.
+30
View File
@@ -165,3 +165,33 @@ func TestIntermediate(t *testing.T) {
t.Error("path-length-0 root signed an intermediate")
}
}
func TestUIRedirectsToHTTPS(t *testing.T) {
ok := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) })
try := func(admin, listen, remote, host string) (int, string) {
a := &App{cfg: &Config{AdminHTTP: admin, TLSListen: listen}}
req := httptest.NewRequest("GET", "/index.html?x=1", nil)
req.RemoteAddr, req.Host = remote, host
rec := httptest.NewRecorder()
a.uiOverHTTPS(ok).ServeHTTP(rec, req)
return rec.Code, rec.Header().Get("Location")
}
if c, l := try("loopback", ":443", "10.0.0.5:5555", "10.0.0.234"); c != 308 || l != "https://10.0.0.234/index.html?x=1" {
t.Errorf("remote over http: %d %s", c, l)
}
if c, l := try("deny", ":8443", "10.0.0.5:5555", "pxe.example:80"); c != 308 || l != "https://pxe.example:8443/index.html?x=1" {
t.Errorf("other port: %d %s", c, l)
}
if c, l := try("loopback", ":443", "[fd00::5]:5555", "[fd00::1]"); c != 308 || l != "https://[fd00::1]/index.html?x=1" {
t.Errorf("ipv6: %d %s", c, l)
}
if c, _ := try("loopback", ":443", "127.0.0.1:5555", "localhost"); c != 200 {
t.Errorf("loopback client redirected: %d", c)
}
if c, _ := try("allow", ":443", "10.0.0.5:5555", "10.0.0.234"); c != 200 {
t.Errorf("allow redirected: %d", c)
}
if c, _ := try("loopback", "", "10.0.0.5:5555", "10.0.0.234"); c != 200 {
t.Errorf("no TLS listener but redirected: %d", c)
}
}
+31 -1
View File
@@ -84,6 +84,36 @@ func (a *App) adminOverHTTP(r *http.Request) error {
return fmt.Errorf("the admin API is HTTPS-only here: https://%s (trust the CA from /ca.pem)", net.JoinHostPort(strings.Trim(h, "[]"), port))
}
// httpsURL is the region's HTTPS address for the host the client used.
func (a *App) httpsURL(r *http.Request) string {
_, port, _ := net.SplitHostPort(a.cfg.TLSListen)
h := r.Host
if hh, _, err := net.SplitHostPort(h); err == nil {
h = hh
}
h = strings.Trim(h, "[]")
if port == "" || port == "443" {
if strings.Contains(h, ":") {
return "https://[" + h + "]"
}
return "https://" + h
}
return "https://" + net.JoinHostPort(h, port)
}
// uiOverHTTPS sends browsers that reach the web UI over plain HTTP to HTTPS when
// the admin API is not allowed over HTTP anyway (the page could not work).
// Only the UI is redirected: nodes keep using HTTP for images and the agent.
func (a *App) uiOverHTTPS(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.TLS == nil && a.cfg.AdminHTTP != "" && a.cfg.AdminHTTP != "allow" && a.cfg.TLSListen != "" && a.adminOverHTTP(r) != nil {
http.Redirect(w, r, a.httpsURL(r)+r.URL.RequestURI(), http.StatusPermanentRedirect)
return
}
next.ServeHTTP(w, r)
})
}
func (a *App) adminAuth(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if err := a.adminOverHTTP(r); err != nil {
@@ -236,7 +266,7 @@ func (a *App) routes() http.Handler {
mux.Handle("GET /os/", http.StripPrefix("/os/", http.FileServer(http.Dir(a.cfg.OSDir))))
// embedded web UI
sub, _ := fs.Sub(webFS, "web")
mux.Handle("GET /", http.FileServer(http.FS(sub)))
mux.Handle("GET /", a.uiOverHTTPS(http.FileServer(http.FS(sub))))
return logging(mux)
}