Web UI over plain HTTP redirects to HTTPS when admin_http forbids HTTP
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
15537b90f0
commit
28a0c2952f
3 files changed
+63
-1
No files matched your search
@@ -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`.
|
||||
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user