Skip to content

Commit 1ba4ed5

Browse files
committed
feat: add ui.path config option for serving UI/API behind a proxy
1 parent 26631d7 commit 1ba4ed5

11 files changed

Lines changed: 116 additions & 46 deletions

File tree

admin/server.go

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ type Server struct {
1818
Access string
1919
Color string
2020
Title string
21+
Path string
2122
Version string
2223
Commands string
2324
}
@@ -29,47 +30,50 @@ func (s *Server) ListenAndServe(l config.Listen, tlscfg *tls.Config) error {
2930

3031
func (s *Server) handler() http.Handler {
3132
mux := http.NewServeMux()
33+
p := strings.TrimRight(s.Path, "/")
3234

3335
switch s.Access {
3436
case "ro":
35-
mux.HandleFunc("/api/paths", forbidden)
36-
mux.HandleFunc("/api/manual", forbidden)
37-
mux.HandleFunc("/api/manual/", forbidden)
38-
mux.HandleFunc("/manual", forbidden)
39-
mux.HandleFunc("/manual/", forbidden)
37+
mux.HandleFunc(p+"/api/paths", forbidden)
38+
mux.HandleFunc(p+"/api/manual", forbidden)
39+
mux.HandleFunc(p+"/api/manual/", forbidden)
40+
mux.HandleFunc(p+"/manual", forbidden)
41+
mux.HandleFunc(p+"/manual/", forbidden)
4042
case "rw":
4143
// for historical reasons the configured config path starts with a '/'
4244
// but Consul treats all KV paths without a leading slash.
4345
pathsPrefix := strings.TrimPrefix(s.Cfg.Registry.Consul.KVPath, "/")
44-
mux.Handle("/api/paths", &api.ManualPathsHandler{Prefix: pathsPrefix})
45-
mux.Handle("/api/manual", &api.ManualHandler{BasePath: "/api/manual"})
46-
mux.Handle("/api/manual/", &api.ManualHandler{BasePath: "/api/manual"})
47-
mux.Handle("/manual", &ui.ManualHandler{
48-
BasePath: "/manual",
46+
mux.Handle(p+"/api/paths", &api.ManualPathsHandler{Prefix: pathsPrefix})
47+
mux.Handle(p+"/api/manual", &api.ManualHandler{BasePath: p + "/api/manual"})
48+
mux.Handle(p+"/api/manual/", &api.ManualHandler{BasePath: p + "/api/manual"})
49+
mux.Handle(p+"/manual", &ui.ManualHandler{
50+
BasePath: p + "/manual",
4951
Color: s.Color,
5052
Title: s.Title,
5153
Version: s.Version,
5254
Commands: s.Commands,
55+
Path: p,
5356
})
54-
mux.Handle("/manual/", &ui.ManualHandler{
55-
BasePath: "/manual",
57+
mux.Handle(p+"/manual/", &ui.ManualHandler{
58+
BasePath: p + "/manual",
5659
Color: s.Color,
5760
Title: s.Title,
5861
Version: s.Version,
5962
Commands: s.Commands,
63+
Path: p,
6064
})
6165
}
6266

63-
mux.Handle("/api/config", &api.ConfigHandler{Config: s.Cfg})
64-
mux.Handle("/api/routes", &api.RoutesHandler{})
65-
mux.Handle("/api/version", &api.VersionHandler{Version: s.Version})
66-
mux.Handle("/routes", &ui.RoutesHandler{Color: s.Color, Title: s.Title, Version: s.Version, RoutingTable: s.Cfg.UI.RoutingTable})
67-
mux.HandleFunc("/health", handleHealth)
67+
mux.Handle(p+"/api/config", &api.ConfigHandler{Config: s.Cfg})
68+
mux.Handle(p+"/api/routes", &api.RoutesHandler{})
69+
mux.Handle(p+"/api/version", &api.VersionHandler{Version: s.Version})
70+
mux.Handle(p+"/routes", &ui.RoutesHandler{Color: s.Color, Title: s.Title, Version: s.Version, Path: p, RoutingTable: s.Cfg.UI.RoutingTable})
71+
mux.HandleFunc(p+"/health", handleHealth)
6872

69-
mux.Handle("/assets/", http.FileServer(http.FS(ui.Static)))
70-
mux.HandleFunc("/favicon.ico", http.NotFound)
73+
mux.Handle(p+"/assets/", http.StripPrefix(p, http.FileServer(http.FS(ui.Static))))
74+
mux.HandleFunc(p+"/favicon.ico", http.NotFound)
7175

72-
mux.Handle("/", http.RedirectHandler("/routes", http.StatusSeeOther))
76+
mux.Handle(p+"/", http.RedirectHandler(p+"/routes", http.StatusSeeOther))
7377
return mux
7478
}
7579

admin/server_test.go

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@ func TestAdminServerAccess(t *testing.T) {
1414
code int
1515
}
1616

17-
testAccess := func(access string, tests []test) {
17+
testAccess := func(access, basePath string, tests []test) {
1818
srv := &Server{
1919
Access: access,
20+
Path: basePath,
2021
Cfg: &config.Config{
2122
Registry: config.Registry{
2223
Consul: config.Consul{
@@ -74,6 +75,37 @@ func TestAdminServerAccess(t *testing.T) {
7475
{"/", 303},
7576
}
7677

77-
testAccess("ro", roTests)
78-
testAccess("rw", rwTests)
78+
testAccess("ro", "", roTests)
79+
testAccess("rw", "", rwTests)
80+
81+
roTestsWithPath := []test{
82+
{"/fabio/api/manual", 403},
83+
{"/fabio/api/paths", 403},
84+
{"/fabio/api/config", 200},
85+
{"/fabio/api/routes", 200},
86+
{"/fabio/api/version", 200},
87+
{"/fabio/manual", 403},
88+
{"/fabio/routes", 200},
89+
{"/fabio/health", 200},
90+
{"/fabio/assets/logo.svg", 200},
91+
{"/fabio/assets/logo.bw.svg", 200},
92+
{"/fabio/", 303},
93+
}
94+
95+
rwTestsWithPath := []test{
96+
{"/fabio/api/manual", 200},
97+
{"/fabio/api/paths", 200},
98+
{"/fabio/api/config", 200},
99+
{"/fabio/api/routes", 200},
100+
{"/fabio/api/version", 200},
101+
{"/fabio/manual", 200},
102+
{"/fabio/routes", 200},
103+
{"/fabio/health", 200},
104+
{"/fabio/assets/logo.svg", 200},
105+
{"/fabio/assets/logo.bw.svg", 200},
106+
{"/fabio/", 303},
107+
}
108+
109+
testAccess("ro", "/fabio", roTestsWithPath)
110+
testAccess("rw", "/fabio", rwTestsWithPath)
79111
}

admin/ui/manual.go

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,19 @@ type ManualHandler struct {
1111
Title string
1212
Version string
1313
Commands string
14+
Path string
1415
}
1516

1617
func (h *ManualHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
17-
path := r.RequestURI[len(h.BasePath):]
18+
manualPath := r.RequestURI[len(h.BasePath):]
1819
data := struct {
1920
*ManualHandler
20-
Path string
21-
APIPath string
21+
ManualPath string
22+
APIPath string
2223
}{
2324
ManualHandler: h,
24-
Path: path,
25-
APIPath: "/api/manual" + path,
25+
ManualPath: manualPath,
26+
APIPath: h.Path + "/api/manual" + manualPath,
2627
}
2728
tmplManual.ExecuteTemplate(w, "manual", data)
2829
}
@@ -39,10 +40,10 @@ var tmplManual = template.Must(template.New("manual").Funcs(funcs).Parse( // lan
3940
<head>
4041
<meta charset="utf-8">
4142
<title>fabio{{if .Title}} - {{.Title}}{{end}}</title>
42-
<script type="text/javascript" src="/assets/code.jquery.com/jquery-3.6.0.min.js"></script>
43-
<link href="/assets/fonts/material-icons.css" rel="stylesheet">
44-
<link rel="stylesheet" href="/assets/cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css">
45-
<script src="/assets/cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script>
43+
<script type="text/javascript" src="{{.Path}}/assets/code.jquery.com/jquery-3.6.0.min.js"></script>
44+
<link href="{{.Path}}/assets/fonts/material-icons.css" rel="stylesheet">
45+
<link rel="stylesheet" href="{{.Path}}/assets/cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css">
46+
<script src="{{.Path}}/assets/cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script>
4647
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
4748
4849
<style type="text/css">
@@ -58,9 +59,9 @@ var tmplManual = template.Must(template.New("manual").Funcs(funcs).Parse( // lan
5859
5960
<div class="container">
6061
<div class="nav-wrapper">
61-
<a href="/" class="brand-logo"><img alt="Fabio Logo" style="margin: 15px 0" class="logo" src="/assets/logo.bw.svg"> {{if .Title}} - {{.Title}}{{end}}</a>
62+
<a href="{{.Path}}/" class="brand-logo"><img alt="Fabio Logo" style="margin: 15px 0" class="logo" src="{{.Path}}/assets/logo.bw.svg"> {{if .Title}} - {{.Title}}{{end}}</a>
6263
<ul id="nav-mobile" class="right hide-on-med-and-down">
63-
<li><a href="/routes">Routes</a></li>
64+
<li><a href="{{.Path}}/routes">Routes</a></li>
6465
<li><a class="dropdown-trigger dropdown-button" href="#" data-target="overrides">Overrides<i class="material-icons right">arrow_drop_down</i></a></li>
6566
<li><a href="https://github.com/fabiolb/fabio/blob/master/CHANGELOG.md">{{.Version}}</a></li>
6667
<li><a href="https://github.com/fabiolb/fabio">Github</a></li>
@@ -74,7 +75,7 @@ var tmplManual = template.Must(template.New("manual").Funcs(funcs).Parse( // lan
7475
<div class="container">
7576
7677
<div class="section">
77-
<h5>Manual Routes{{if .Path}} for "{{.Path}}"{{end}}</h5>
78+
<h5>Manual Routes{{if .ManualPath}} for "{{.ManualPath}}"{{end}}</h5>
7879
7980
<div class="row">
8081
<form class="col s12">
@@ -110,7 +111,7 @@ $(function(){
110111
M.textareaAutoResize($('#textarea1'));
111112
});
112113
113-
$.get('/api/paths', function(data) {
114+
$.get('{{.Path}}/api/paths', function(data) {
114115
const d = $("#overrides");
115116
$.each(data, function(idx, val) {
116117
let path = val;
@@ -119,7 +120,7 @@ $(function(){
119120
}
120121
d.append(
121122
$('<li />').append(
122-
$('<a />').attr('href', '/manual'+path).text(val)
123+
$('<a />').attr('href', '{{.Path}}/manual'+path).text(val)
123124
)
124125
);
125126
});

admin/ui/route.go

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ type RoutesHandler struct {
1212
Color string
1313
Title string
1414
Version string
15+
Path string
1516
RoutingTable config.RoutingTable
1617
}
1718

@@ -25,10 +26,10 @@ var tmplRoutes = template.Must(template.New("routes").Parse( // language=HTML
2526
<head>
2627
<meta charset="utf-8">
2728
<title>fabio{{if .Title}} - {{.Title}}{{end}}</title>
28-
<script type="text/javascript" src="/assets/code.jquery.com/jquery-3.6.0.min.js"></script>
29-
<link href="/assets/fonts/material-icons.css" rel="stylesheet">
30-
<link rel="stylesheet" href="/assets/cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css">
31-
<script src="/assets/cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script>
29+
<script type="text/javascript" src="{{.Path}}/assets/code.jquery.com/jquery-3.6.0.min.js"></script>
30+
<link href="{{.Path}}/assets/fonts/material-icons.css" rel="stylesheet">
31+
<link rel="stylesheet" href="{{.Path}}/assets/cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css">
32+
<script src="{{.Path}}/assets/cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script>
3233
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
3334
3435
<style type="text/css">
@@ -64,7 +65,7 @@ var tmplRoutes = template.Must(template.New("routes").Parse( // language=HTML
6465
6566
<div class="container">
6667
<div class="nav-wrapper">
67-
<a href="/" class="brand-logo"><img alt="Fabio Logo" style="margin: 15px 0" class="logo" src="/assets/logo.bw.svg"> {{if .Title}} - {{.Title}}{{end}}</a>
68+
<a href="{{.Path}}/" class="brand-logo"><img alt="Fabio Logo" style="margin: 15px 0" class="logo" src="{{.Path}}/assets/logo.bw.svg"> {{if .Title}} - {{.Title}}{{end}}</a>
6869
<ul id="nav-mobile" class="right hide-on-med-and-down">
6970
<li><a class="dropdown-trigger dropdown-button" href="#" data-target="overrides">Overrides<i class="material-icons right">arrow_drop_down</i></a></li>
7071
<li><a href="https://github.com/fabiolb/fabio/blob/master/CHANGELOG.md">{{.Version}}</a></li>
@@ -85,7 +86,7 @@ var tmplRoutes = template.Must(template.New("routes").Parse( // language=HTML
8586
</div>
8687
8788
<div class="section footer">
88-
<img alt="Fabio Logo" class="logo" src="/assets/logo.svg">
89+
<img alt="Fabio Logo" class="logo" src="{{.Path}}/assets/logo.svg">
8990
</div>
9091
9192
</div>
@@ -166,15 +167,15 @@ $(function(){
166167
doFilter(v);
167168
});
168169
169-
$.get("/api/routes", function(data) {
170+
$.get("{{.Path}}/api/routes", function(data) {
170171
renderRoutes(data);
171172
if (!params.filter) return;
172173
const v = decodeURIComponent(params.filter);
173174
$filter.val(v);
174175
doFilter(v);
175176
});
176177
177-
$.get('/api/paths', function(data) {
178+
$.get('{{.Path}}/api/paths', function(data) {
178179
const d = $("#overrides");
179180
$.each(data, function(idx, val) {
180181
let path = val;
@@ -183,7 +184,7 @@ $(function(){
183184
}
184185
d.append(
185186
$('<li />').append(
186-
$('<a />').attr('href', '/manual'+path).text(val)
187+
$('<a />').attr('href', '{{.Path}}/manual'+path).text(val)
187188
)
188189
);
189190
});

config/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ type UI struct {
6767
Color string
6868
Title string
6969
Access string
70+
Path string
7071
Listen Listen
7172
}
7273

config/load.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,7 @@ func load(cmdline, environ, envprefix []string, props *properties.Properties) (c
224224
f.StringVar(&uiListenerValue, "ui.addr", defaultValues.UIListenerValue, "Address the UI/API is listening on")
225225
f.StringVar(&cfg.UI.Color, "ui.color", defaultConfig.UI.Color, "background color of the UI")
226226
f.StringVar(&cfg.UI.Title, "ui.title", defaultConfig.UI.Title, "optional title for the UI")
227+
f.StringVar(&cfg.UI.Path, "ui.path", defaultConfig.UI.Path, "base path for the UI/API")
227228

228229
f.BoolVar(&cfg.UI.RoutingTable.Source.LinkEnabled, "ui.routingtable.source.linkenabled", defaultConfig.UI.RoutingTable.Source.LinkEnabled, "optional true/false flag if the source in the routing table of the admin UI should have a link")
229230
f.BoolVar(&cfg.UI.RoutingTable.Source.NewTab, "ui.routingtable.source.newtab", defaultConfig.UI.RoutingTable.Source.NewTab, "optional true/false flag if the source link should be opened in a new tab, not affected if linkenabled is false")

config/load_test.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -950,6 +950,13 @@ func TestLoad(t *testing.T) {
950950
return cfg
951951
},
952952
},
953+
{
954+
args: []string{"-ui.path", "/fabio"},
955+
cfg: func(cfg *Config) *Config {
956+
cfg.UI.Path = "/fabio"
957+
return cfg
958+
},
959+
},
953960
{
954961
desc: "ignore aws.apigw.cert.cn",
955962
args: []string{"-aws.apigw.cert.cn", "value"},

docs/content/feature/web-ui.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,6 @@ fabio supports a Web UI to examine the current routing table and manage the
77
manual overrides. By default it listens on `http://0.0.0.0:9998/` which can be
88
changed with the `ui.addr` option. The `ui.title` and `ui.color` options allow
99
customization of the title and the color of the header bar.
10+
11+
The `ui.path` option configures a base path for the UI and API, allowing fabio
12+
to be served behind a reverse proxy at a sub-path (e.g. `ui.path = /fabio`).

docs/content/ref/ui.path.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
title: "ui.path"
3+
---
4+
5+
`ui.path` configures the base path for the UI/API. This allows serving
6+
the admin UI behind a reverse proxy at a sub-path, e.g. `/fabio`.
7+
8+
The default is
9+
10+
ui.path =

fabio.properties

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1332,6 +1332,15 @@
13321332
# ui.title =
13331333

13341334

1335+
# ui.path configures the base path for the UI/API.
1336+
# This allows serving the admin UI behind a reverse proxy
1337+
# at a sub-path, e.g. /fabio.
1338+
#
1339+
# The default is
1340+
#
1341+
# ui.path =
1342+
1343+
13351344
# ui.routingtable.source.linkenabled optionally configure if the
13361345
# routing table's column "source" should be a clickable link.
13371346
#

0 commit comments

Comments
 (0)