Skip to content

Commit 3e0061d

Browse files
authored
Merge pull request #281 from kaisoz/kaisoz/console-base-path
fix(console): serve the routes under --base-path, not just at the root
2 parents 0fb93d8 + be7bcfa commit 3e0061d

3 files changed

Lines changed: 87 additions & 8 deletions

File tree

cmd/sam-console/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ func main() {
3636
ControlPlaneURL: *controlPlaneURL,
3737
AdminToken: adminToken,
3838
StaticDir: *staticDir,
39-
BasePath: *basePath,
39+
BasePath: console.NormalizeBasePath(*basePath),
4040
})
4141
if err != nil {
4242
log.Fatalf("Failed to initialize console server: %v", err)

internal/console/server.go

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"net/http/httputil"
1313
"net/url"
1414
"os"
15+
"path"
1516
"path/filepath"
1617
"time"
1718

@@ -28,6 +29,19 @@ type Config struct {
2829
BasePath string
2930
}
3031

32+
// NormalizeBasePath makes a base-path flag value safe to concatenate: no trailing
33+
// slash (else cookie paths become /console//) and a guaranteed leading slash.
34+
func NormalizeBasePath(p string) string {
35+
if p == "" {
36+
return ""
37+
}
38+
p = path.Clean("/" + p)
39+
if p == "/" {
40+
return ""
41+
}
42+
return p
43+
}
44+
3145
type Server struct {
3246
cfg Config
3347
mux *http.ServeMux
@@ -101,12 +115,14 @@ func NewServer(cfg Config) (*Server, error) {
101115
},
102116
}
103117

118+
routes := http.NewServeMux()
119+
104120
// Proxy all API requests to the control plane
105-
s.mux.Handle("/api/", http.StripPrefix("/api", proxy))
121+
routes.Handle("/api/", http.StripPrefix("/api", proxy))
106122

107123
// Serve static files
108124
fs := http.FileServer(http.Dir(s.cfg.StaticDir))
109-
s.mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
125+
routes.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
110126
// Basic check if file exists
111127
path := filepath.Join(s.cfg.StaticDir, r.URL.Path)
112128
if _, err := os.Stat(path); os.IsNotExist(err) && r.URL.Path != "/" {
@@ -119,12 +135,19 @@ func NewServer(cfg Config) (*Server, error) {
119135

120136
// OIDC login endpoints
121137
if s.provider != nil {
122-
s.mux.HandleFunc("/auth/login", s.HandleLogin)
123-
s.mux.HandleFunc("/auth/callback", s.HandleCallback)
124-
s.mux.HandleFunc("/auth/session", s.HandleSession)
138+
routes.HandleFunc("/auth/login", s.HandleLogin)
139+
routes.HandleFunc("/auth/callback", s.HandleCallback)
140+
routes.HandleFunc("/auth/session", s.HandleSession)
141+
}
142+
routes.HandleFunc("/auth/logout", s.HandleLogout)
143+
routes.HandleFunc("/info", s.HandleInfo)
144+
145+
// Serve under BasePath too, so the links this server emits resolve without the proxy
146+
// stripping the prefix. Still served at the root for proxies that do strip it.
147+
if s.cfg.BasePath != "" {
148+
s.mux.Handle(s.cfg.BasePath+"/", http.StripPrefix(s.cfg.BasePath, routes))
125149
}
126-
s.mux.HandleFunc("/auth/logout", s.HandleLogout)
127-
s.mux.HandleFunc("/info", s.HandleInfo)
150+
s.mux.Handle("/", routes)
128151

129152
return s, nil
130153
}

internal/console/server_test.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,3 +243,59 @@ func TestDiscoverProviderWithRetry(t *testing.T) {
243243
}
244244
})
245245
}
246+
247+
// TestNewServer_BasePathServesBothPrefixes: with a BasePath the console must answer both the
248+
// prefixed URLs it hands out (so a proxy can forward /console/* untouched) and the root.
249+
func TestNewServer_BasePathServesBothPrefixes(t *testing.T) {
250+
controlPlane := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
251+
w.WriteHeader(http.StatusOK) // empty /info: no OIDC, which the console tolerates
252+
}))
253+
defer controlPlane.Close()
254+
255+
srv, err := NewServer(Config{
256+
ControlPlaneURL: controlPlane.URL,
257+
AdminToken: "test-admin-token",
258+
StaticDir: t.TempDir(),
259+
BasePath: "/console",
260+
})
261+
if err != nil {
262+
t.Fatalf("failed to create server: %v", err)
263+
}
264+
console := httptest.NewServer(srv.Handler())
265+
defer console.Close()
266+
267+
client := &http.Client{CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }}
268+
for path, want := range map[string]int{
269+
"/info": http.StatusOK,
270+
"/console/info": http.StatusOK,
271+
"/console": http.StatusMovedPermanently, // ServeMux redirects to the subtree root
272+
} {
273+
resp, err := client.Get(console.URL + path)
274+
if err != nil {
275+
t.Fatalf("GET %s: %v", path, err)
276+
}
277+
_ = resp.Body.Close()
278+
if resp.StatusCode != want {
279+
t.Errorf("GET %s: got %d, want %d", path, resp.StatusCode, want)
280+
}
281+
}
282+
}
283+
284+
// BasePath is concatenated into cookie paths, redirect URLs and mux patterns, so malformed
285+
// flag values (trailing slash, missing leading slash) must be normalized where the flag is read.
286+
func TestNormalizeBasePath(t *testing.T) {
287+
for input, want := range map[string]string{
288+
"": "",
289+
"/": "",
290+
"/console": "/console",
291+
"/console/": "/console",
292+
"console": "/console",
293+
"console//": "/console",
294+
"//console": "/console",
295+
"/console//sub/": "/console/sub",
296+
} {
297+
if got := NormalizeBasePath(input); got != want {
298+
t.Errorf("NormalizeBasePath(%q) = %q, want %q", input, got, want)
299+
}
300+
}
301+
}

0 commit comments

Comments
 (0)