Merge pull request #1943 from versity/ben/webui-prefix

feat: add option to change webui path prefix
This commit is contained in:
Ben McClelland
2026-03-09 17:05:54 -07:00
committed by GitHub
4 changed files with 76 additions and 9 deletions
+53 -3
View File
@@ -100,6 +100,7 @@ var (
webuiNoTLS bool
webuiGateways []string
webuiAdminGateways []string
webuiPathPrefix string
disableACLs bool
)
@@ -156,6 +157,7 @@ documentation can be found in the GitHub wiki.`,
admPorts = ctx.StringSlice("admin-port")
webuiGateways = ctx.StringSlice("webui-gateways")
webuiAdminGateways = ctx.StringSlice("webui-admin-gateways")
webuiPathPrefix = ctx.String("webui-path-prefix")
// Resolve relative UNIX socket paths to absolute before any backend
// (e.g. posix) can change the working directory via os.Chdir.
@@ -232,6 +234,12 @@ func initFlags() []cli.Flag {
Usage: "override auto-detected admin gateway URLs for WebUI (e.g. 'http://localhost:7080', 'https://admin.example.com'; can be specified multiple times)",
EnvVars: []string{"VGW_WEBUI_ADMIN_GATEWAYS"},
},
&cli.StringFlag{
Name: "webui-path-prefix",
Usage: "mount the WebUI under a path prefix (e.g. '/ui'); must be single segment path that starts with '/'",
EnvVars: []string{"VGW_WEBUI_PATH_PREFIX"},
Destination: &webuiPathPrefix,
},
&cli.StringFlag{
Name: "access",
Usage: "root user access key",
@@ -748,6 +756,11 @@ func runGateway(ctx context.Context, be backend.Backend) error {
return fmt.Errorf("root user access and secret key must be provided")
}
err := validateWebUIPathPrefix(webuiPathPrefix)
if err != nil {
log.Fatal(err)
}
if maxConnections < 1 {
log.Fatal("max-connections must be positive")
}
@@ -1108,6 +1121,9 @@ func runGateway(ctx context.Context, be backend.Backend) error {
if quiet {
webOpts = append(webOpts, webui.WithQuiet())
}
if webuiPathPrefix != "" {
webOpts = append(webOpts, webui.WithPathPrefix(webuiPathPrefix))
}
webSrv = webui.NewServer(&webui.ServerConfig{
Gateways: gateways,
@@ -1117,7 +1133,7 @@ func runGateway(ctx context.Context, be backend.Backend) error {
}
if !quiet {
printBanner(ports, admPorts, certFile != "" || keyFile != "", admCertFile != "" || admKeyFile != "", webuiPorts, webuiSSLEnabled)
printBanner(ports, admPorts, certFile != "" || keyFile != "", admCertFile != "" || admKeyFile != "", webuiPorts, webuiSSLEnabled, webuiPathPrefix)
}
servers := 1
@@ -1243,7 +1259,7 @@ Loop:
return saveErr
}
func printBanner(ports []string, admPorts []string, ssl, admSsl bool, webuiAddrs []string, webuiSsl bool) {
func printBanner(ports []string, admPorts []string, ssl, admSsl bool, webuiAddrs []string, webuiSsl bool, webuiPathPrefix string) {
if len(ports) == 0 {
fmt.Fprintf(os.Stderr, "No ports specified\n")
return
@@ -1459,7 +1475,7 @@ func printBanner(ports []string, admPorts []string, ssl, admSsl bool, webuiAddrs
if webuiSsl {
url = fmt.Sprintf("https://%s", hostPort)
}
lines = append(lines, leftText(" "+url))
lines = append(lines, leftText(" "+url+webuiPathPrefix))
}
}
}
@@ -1628,6 +1644,40 @@ func validateGatewayURLs(urls []string, urlType string) ([]string, error) {
return validURLs, nil
}
// validateWebUIPathPrefix validates --webui-path-prefix.
// Accepted format is a single path segment like "/ui".
func validateWebUIPathPrefix(prefix string) error {
if prefix == "" {
return nil
}
if strings.TrimSpace(prefix) != prefix {
return fmt.Errorf("invalid --webui-path-prefix %q: must not contain leading or trailing whitespace", prefix)
}
if !strings.HasPrefix(prefix, "/") {
return fmt.Errorf("invalid --webui-path-prefix %q: must start with '/' (example: '/ui')", prefix)
}
if strings.HasSuffix(prefix, "/") {
return fmt.Errorf("invalid --webui-path-prefix %q: must not end with '/'", prefix)
}
if strings.Count(prefix, "/") > 1 {
return fmt.Errorf("invalid --webui-path-prefix %q: only a single path segment is allowed (example: '/ui')", prefix)
}
if strings.ContainsAny(prefix, "?#") {
return fmt.Errorf("invalid --webui-path-prefix %q: query strings and fragments are not allowed", prefix)
}
if strings.Contains(prefix, "\\") {
return fmt.Errorf("invalid --webui-path-prefix %q: backslashes are not allowed", prefix)
}
return nil
}
// sortGatewayURLs sorts a list of URLs so that localhost and 127.0.0.1 URLs appear last
func sortGatewayURLs(urls []string) {
if len(urls) <= 1 {
+6
View File
@@ -237,6 +237,12 @@ ROOT_SECRET_ACCESS_KEY=
# paths and Linux abstract "@" sockets).
#VGW_WEBUI_PORT=
# The VGW_WEBUI_PATH_PREFIX option sets a URL path prefix for serving the Web
# GUI and its API endpoints (for example, '/ui'). This is useful when the
# gateway is running behind a reverse proxy that mounts the Web GUI under a
# subpath instead of the URL root. Leave unset to serve from '/'.
#VGW_WEBUI_PATH_PREFIX=
# The VGW_WEBUI_CERT and VGW_WEBUI_KEY options specify the TLS certificate and
# private key for the Web GUI server. If these are not specified and TLS is
# configured for the gateway (VGW_CERT and VGW_KEY), the Web GUI will use the
+1
View File
@@ -18,6 +18,7 @@ under the License.
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<base href="{{.BasePath}}">
<title>VersityGW Admin - Login</title>
<script src="assets/js/crypto-js.min.js"></script>
<script src="assets/css/tailwind.js"></script>
+16 -6
View File
@@ -41,6 +41,7 @@ type Server struct {
app *fiber.App
CertStorage *utils.CertStorage
config *ServerConfig
pathPrefix string
quiet bool
}
@@ -57,6 +58,11 @@ func WithTLS(cs *utils.CertStorage) Option {
return func(s *Server) { s.CertStorage = cs }
}
// WithPathPrefix mounts the entire web UI under the given path prefix
func WithPathPrefix(prefix string) Option {
return func(s *Server) { s.pathPrefix = prefix }
}
// NewServer creates a new GUI server instance
func NewServer(cfg *ServerConfig, opts ...Option) *Server {
app := fiber.New(fiber.Config{
@@ -98,12 +104,14 @@ func (s *Server) setupMiddleware() {
// setupRoutes configures all routes
func (s *Server) setupRoutes() {
// Serve index.html
s.app.Get("/", s.handleIndexHTML)
s.app.Get("/index.html", s.handleIndexHTML)
prefix := s.pathPrefix
// Serve index.html with server-side config injection
s.app.Get(prefix+"/", s.handleIndexHTML)
s.app.Get(prefix+"/index.html", s.handleIndexHTML)
// Serve embedded static files from web/
s.app.Use("/", filesystem.New(filesystem.Config{
s.app.Use(prefix+"/", filesystem.New(filesystem.Config{
Root: http.FS(webFS),
PathPrefix: "web",
Browse: false,
@@ -131,8 +139,10 @@ func (s *Server) handleIndexHTML(c *fiber.Ctx) error {
return fiber.ErrInternalServerError
}
html := strings.Replace(
string(data),
basePath := s.pathPrefix + "/"
html := strings.Replace(string(data), "{{.BasePath}}", basePath, 1)
html = strings.Replace(
html,
"</head>",
"<script>window.__VGWCONFIG__ = "+string(configJSON)+";</script></head>",
1,