From b90e7904a56e171f82cc79595f037e76bdaec41d Mon Sep 17 00:00:00 2001 From: qwerty287 <80460567+qwerty287@users.noreply.github.com> Date: Sat, 29 Apr 2023 17:51:50 +0200 Subject: [PATCH] Support path prefix (#1714) closes #1636 closes #1429 supersedes #1586 Uses a different approach: just take the index.html compiled by vite and replace the paths to js and other files using regex. This is not compatible with the dev proxy which is also the reason why we can't use go templates for this. --- cmd/server/flags.go | 5 ++++ cmd/server/server.go | 1 + docs/docs/30-administration/00-setup.md | 2 ++ .../30-administration/10-server-config.md | 6 +++++ server/config.go | 1 + server/web/config.go | 15 ++++++++---- server/web/web.go | 23 +++++++++++++++---- web/src/compositions/useApiClient.ts | 2 +- web/src/compositions/useConfig.ts | 2 ++ 9 files changed, 46 insertions(+), 11 deletions(-) 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, });