diff --git a/cmd/server/flags.go b/cmd/server/flags.go index de1317001..d4bd8962a 100644 --- a/cmd/server/flags.go +++ b/cmd/server/flags.go @@ -45,6 +45,11 @@ var flags = []cli.Flag{ Name: "server-host", Usage: "server fully qualified url (://)", }, + &cli.StringFlag{ + EnvVars: []string{"WOODPECKER_ROOT_URL"}, + Name: "root-url", + Usage: "server url root (used for statics loading when having a url path prefix)", + }, &cli.StringFlag{ EnvVars: []string{"WOODPECKER_SERVER_ADDR"}, Name: "server-addr", diff --git a/cmd/server/server.go b/cmd/server/server.go index cf3a99af1..99fe589ad 100644 --- a/cmd/server/server.go +++ b/cmd/server/server.go @@ -354,6 +354,7 @@ func setupEvilGlobals(c *cli.Context, v store.Store, f forge.Forge) { server.Config.Server.StatusContext = c.String("status-context") server.Config.Server.StatusContextFormat = c.String("status-context-format") server.Config.Server.SessionExpires = c.Duration("session-expires") + server.Config.Server.RootURL = strings.TrimSuffix(c.String("root-url"), "/") server.Config.Pipeline.Networks = c.StringSlice("network") server.Config.Pipeline.Volumes = c.StringSlice("volume") server.Config.Pipeline.Privileged = c.StringSlice("escalate") diff --git a/docs/docs/30-administration/00-setup.md b/docs/docs/30-administration/00-setup.md index 71a26b501..68837201b 100644 --- a/docs/docs/30-administration/00-setup.md +++ b/docs/docs/30-administration/00-setup.md @@ -174,3 +174,5 @@ A [Prometheus endpoint](./90-prometheus.md) is exposed. ## Behind a proxy See the [proxy guide](./70-proxy.md) if you want to see a setup behind Apache, Nginx, Caddy or ngrok. + +In the case you need to use Woodpecker with a URL path prefix (like: https://example.org/woodpecker/), you can use the option [`WOODPECKER_ROOT_URL`](./10-server-config.md#woodpecker_root_url). diff --git a/docs/docs/30-administration/10-server-config.md b/docs/docs/30-administration/10-server-config.md index e594b52e2..5a15872b9 100644 --- a/docs/docs/30-administration/10-server-config.md +++ b/docs/docs/30-administration/10-server-config.md @@ -404,6 +404,12 @@ Specify a configuration service endpoint, see [Configuration Extension](./100-ex Specify how many seconds before timeout when fetching the Woodpecker configuration from a Forge +### `WOODPECKER_ROOT_URL` +> Default: `` + +Server URL path prefix (used for statics loading when having a url path prefix), should start with `/` + +Example: `WOODPECKER_ROOT_URL=/woodpecker` --- diff --git a/server/config.go b/server/config.go index 863314786..35008c791 100644 --- a/server/config.go +++ b/server/config.go @@ -66,6 +66,7 @@ var Config = struct { StatusContext string StatusContextFormat string SessionExpires time.Duration + RootURL string // Open bool // Orgs map[string]struct{} // Admins map[string]struct{} diff --git a/server/web/config.go b/server/web/config.go index e2ce1dfc1..535daa74f 100644 --- a/server/web/config.go +++ b/server/web/config.go @@ -40,11 +40,12 @@ func Config(c *gin.Context) { } configData := map[string]interface{}{ - "user": user, - "csrf": csrf, - "docs": server.Config.Server.Docs, - "version": version.String(), - "forge": server.Config.Services.Forge.Name(), + "user": user, + "csrf": csrf, + "docs": server.Config.Server.Docs, + "version": version.String(), + "forge": server.Config.Services.Forge.Name(), + "root_url": server.Config.Server.RootURL, } // default func map with json parser. @@ -61,7 +62,10 @@ func Config(c *gin.Context) { if err := tmpl.Execute(c.Writer, configData); err != nil { log.Error().Err(err).Msgf("could not execute template") c.AbortWithStatus(http.StatusInternalServerError) + return } + + c.Status(http.StatusOK) } const configTemplate = ` @@ -70,4 +74,5 @@ window.WOODPECKER_CSRF = "{{ .csrf }}"; window.WOODPECKER_VERSION = "{{ .version }}"; window.WOODPECKER_DOCS = "{{ .docs }}"; window.WOODPECKER_FORGE = "{{ .forge }}"; +window.WOODPECKER_ROOT_URL = "{{ .root_url }}"; ` diff --git a/server/web/web.go b/server/web/web.go index 3d83e9575..1e9e3efa3 100644 --- a/server/web/web.go +++ b/server/web/web.go @@ -18,21 +18,27 @@ import ( "crypto/md5" "fmt" "net/http" + "regexp" "time" "github.com/gin-gonic/gin" "github.com/rs/zerolog/log" + "github.com/woodpecker-ci/woodpecker/server" "github.com/woodpecker-ci/woodpecker/web" ) // etag is an identifier for a resource version // it lets caches determine if resource is still the same and not send it again -var etag = fmt.Sprintf("%x", md5.Sum([]byte(time.Now().String()))) +var ( + etag = fmt.Sprintf("%x", md5.Sum([]byte(time.Now().String()))) + indexHTML []byte +) // New returns a gin engine to serve the web frontend. func New() (*gin.Engine, error) { e := gin.New() + indexHTML = parseIndex() e.Use(setupCache) @@ -64,15 +70,22 @@ func redirect(location string, status ...int) func(ctx *gin.Context) { func handleIndex(c *gin.Context) { rw := c.Writer + rw.Header().Set("Content-Type", "text/html; charset=UTF-8") + rw.WriteHeader(http.StatusOK) + if _, err := rw.Write(indexHTML); err != nil { + log.Error().Err(err).Msg("can not write index.html") + } +} + +func parseIndex() []byte { data, err := web.Lookup("index.html") if err != nil { log.Fatal().Err(err).Msg("can not find index.html") } - rw.Header().Set("Content-Type", "text/html; charset=UTF-8") - rw.WriteHeader(200) - if _, err := rw.Write(data); err != nil { - log.Error().Err(err).Msg("can not write index.html") + if server.Config.Server.RootURL == "" { + return data } + return regexp.MustCompile(`/\S+\.(js|css|png|svg)`).ReplaceAll(data, []byte(server.Config.Server.RootURL+"$0")) } func setupCache(c *gin.Context) { diff --git a/web/src/compositions/useApiClient.ts b/web/src/compositions/useApiClient.ts index 436989692..88c527bc4 100644 --- a/web/src/compositions/useApiClient.ts +++ b/web/src/compositions/useApiClient.ts @@ -7,7 +7,7 @@ let apiClient: WoodpeckerClient | undefined; export default (): WoodpeckerClient => { if (!apiClient) { const config = useConfig(); - const server = ''; + const server = config.rootURL ?? ''; const token = null; const csrf = config.csrf || null; diff --git a/web/src/compositions/useConfig.ts b/web/src/compositions/useConfig.ts index 3875f3aaf..aa5bb00a8 100644 --- a/web/src/compositions/useConfig.ts +++ b/web/src/compositions/useConfig.ts @@ -7,6 +7,7 @@ declare global { WOODPECKER_VERSION: string | undefined; WOODPECKER_CSRF: string | undefined; WOODPECKER_FORGE: string | undefined; + WOODPECKER_ROOT_URL: string | undefined; } } @@ -16,4 +17,5 @@ export default () => ({ version: window.WOODPECKER_VERSION, csrf: window.WOODPECKER_CSRF || null, forge: window.WOODPECKER_FORGE || null, + rootURL: window.WOODPECKER_ROOT_URL || null, });