mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2026-04-15 01:41:56 +00:00
Fix UI and backend paths with subpath (#1799)
I'm not sure if this is an ideal fix for this, but it seems to work for me. If you have another idea just let me know. Closes #1798 Closes #1773
This commit is contained in:
@@ -34,14 +34,10 @@ import (
|
||||
)
|
||||
|
||||
func HandleLogin(c *gin.Context) {
|
||||
var (
|
||||
w = c.Writer
|
||||
r = c.Request
|
||||
)
|
||||
if err := r.FormValue("error"); err != "" {
|
||||
http.Redirect(w, r, "/login/error?code="+err, 303)
|
||||
if err := c.Request.FormValue("error"); err != "" {
|
||||
c.Redirect(http.StatusSeeOther, server.Config.Server.RootPath+"/login/error?code="+err)
|
||||
} else {
|
||||
http.Redirect(w, r, "/authorize", 303)
|
||||
c.Redirect(http.StatusSeeOther, server.Config.Server.RootPath+"/authorize")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +52,7 @@ func HandleAuth(c *gin.Context) {
|
||||
tmpuser, err := _forge.Login(c, c.Writer, c.Request)
|
||||
if err != nil {
|
||||
log.Error().Msgf("cannot authenticate user. %s", err)
|
||||
c.Redirect(http.StatusSeeOther, "/login?error=oauth_error")
|
||||
c.Redirect(http.StatusSeeOther, server.Config.Server.RootPath+"/login?error=oauth_error")
|
||||
return
|
||||
}
|
||||
// this will happen when the user is redirected by the forge as
|
||||
@@ -77,7 +73,7 @@ func HandleAuth(c *gin.Context) {
|
||||
// if self-registration is disabled we should return a not authorized error
|
||||
if !config.Open && !config.IsAdmin(tmpuser) {
|
||||
log.Error().Msgf("cannot register %s. registration closed", tmpuser.Login)
|
||||
c.Redirect(http.StatusSeeOther, "/login?error=access_denied")
|
||||
c.Redirect(http.StatusSeeOther, server.Config.Server.RootPath+"/login?error=access_denied")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -87,7 +83,7 @@ func HandleAuth(c *gin.Context) {
|
||||
teams, terr := _forge.Teams(c, tmpuser)
|
||||
if terr != nil || !config.IsMember(teams) {
|
||||
log.Error().Err(terr).Msgf("cannot verify team membership for %s.", u.Login)
|
||||
c.Redirect(303, "/login?error=access_denied")
|
||||
c.Redirect(303, server.Config.Server.RootPath+"/login?error=access_denied")
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -108,7 +104,7 @@ func HandleAuth(c *gin.Context) {
|
||||
// insert the user into the database
|
||||
if err := _store.CreateUser(u); err != nil {
|
||||
log.Error().Msgf("cannot insert %s. %s", u.Login, err)
|
||||
c.Redirect(http.StatusSeeOther, "/login?error=internal_error")
|
||||
c.Redirect(http.StatusSeeOther, server.Config.Server.RootPath+"/login?error=internal_error")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -137,14 +133,14 @@ func HandleAuth(c *gin.Context) {
|
||||
teams, terr := _forge.Teams(c, u)
|
||||
if terr != nil || !config.IsMember(teams) {
|
||||
log.Error().Err(terr).Msgf("cannot verify team membership for %s.", u.Login)
|
||||
c.Redirect(http.StatusSeeOther, "/login?error=access_denied")
|
||||
c.Redirect(http.StatusSeeOther, server.Config.Server.RootPath+"/login?error=access_denied")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := _store.UpdateUser(u); err != nil {
|
||||
log.Error().Msgf("cannot update %s. %s", u.Login, err)
|
||||
c.Redirect(http.StatusSeeOther, "/login?error=internal_error")
|
||||
c.Redirect(http.StatusSeeOther, server.Config.Server.RootPath+"/login?error=internal_error")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -152,7 +148,7 @@ func HandleAuth(c *gin.Context) {
|
||||
tokenString, err := token.New(token.SessToken, u.Login).SignExpires(u.Hash, exp)
|
||||
if err != nil {
|
||||
log.Error().Msgf("cannot create token for %s. %s", u.Login, err)
|
||||
c.Redirect(http.StatusSeeOther, "/login?error=internal_error")
|
||||
c.Redirect(http.StatusSeeOther, server.Config.Server.RootPath+"/login?error=internal_error")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -187,13 +183,13 @@ func HandleAuth(c *gin.Context) {
|
||||
|
||||
httputil.SetCookie(c.Writer, c.Request, "user_sess", tokenString)
|
||||
|
||||
c.Redirect(http.StatusSeeOther, "/")
|
||||
c.Redirect(http.StatusSeeOther, server.Config.Server.RootPath+"/")
|
||||
}
|
||||
|
||||
func GetLogout(c *gin.Context) {
|
||||
httputil.DelCookie(c.Writer, c.Request, "user_sess")
|
||||
httputil.DelCookie(c.Writer, c.Request, "user_last")
|
||||
c.Redirect(http.StatusSeeOther, "/")
|
||||
c.Redirect(http.StatusSeeOther, server.Config.Server.RootPath+"/")
|
||||
}
|
||||
|
||||
func GetLoginToken(c *gin.Context) {
|
||||
|
||||
@@ -67,7 +67,7 @@ var Config = struct {
|
||||
StatusContext string
|
||||
StatusContextFormat string
|
||||
SessionExpires time.Duration
|
||||
RootURL string
|
||||
RootPath string
|
||||
CustomCSSFile string
|
||||
CustomJsFile string
|
||||
Migrations struct {
|
||||
|
||||
@@ -421,7 +421,7 @@ func (c *config) newOAuth2Config() *oauth2.Config {
|
||||
AuthURL: fmt.Sprintf("%s/site/oauth2/authorize", c.url),
|
||||
TokenURL: fmt.Sprintf("%s/site/oauth2/access_token", c.url),
|
||||
},
|
||||
RedirectURL: fmt.Sprintf("%s/authorize", server.Config.Server.OAuthHost),
|
||||
RedirectURL: fmt.Sprintf("%s%s/authorize", server.Config.Server.OAuthHost, server.Config.Server.RootPath),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -103,7 +103,7 @@ func (c *Gitea) oauth2Config(ctx context.Context) (*oauth2.Config, context.Conte
|
||||
AuthURL: fmt.Sprintf(authorizeTokenURL, c.url),
|
||||
TokenURL: fmt.Sprintf(accessTokenURL, c.url),
|
||||
},
|
||||
RedirectURL: fmt.Sprintf("%s/authorize", server.Config.Server.OAuthHost),
|
||||
RedirectURL: fmt.Sprintf("%s%s/authorize", server.Config.Server.OAuthHost, server.Config.Server.RootPath),
|
||||
},
|
||||
|
||||
context.WithValue(ctx, oauth2.HTTPClient, &http.Client{Transport: &http.Transport{
|
||||
|
||||
@@ -395,9 +395,9 @@ func (c *client) newConfig(req *http.Request) *oauth2.Config {
|
||||
|
||||
intendedURL := req.URL.Query()["url"]
|
||||
if len(intendedURL) > 0 {
|
||||
redirect = fmt.Sprintf("%s/authorize?url=%s", server.Config.Server.OAuthHost, intendedURL[0])
|
||||
redirect = fmt.Sprintf("%s%s/authorize?url=%s", server.Config.Server.OAuthHost, server.Config.Server.RootPath, intendedURL[0])
|
||||
} else {
|
||||
redirect = fmt.Sprintf("%s/authorize", server.Config.Server.OAuthHost)
|
||||
redirect = fmt.Sprintf("%s%s/authorize", server.Config.Server.OAuthHost, server.Config.Server.RootPath)
|
||||
}
|
||||
|
||||
return &oauth2.Config{
|
||||
|
||||
@@ -93,7 +93,7 @@ func (g *GitLab) oauth2Config(ctx context.Context) (*oauth2.Config, context.Cont
|
||||
TokenURL: fmt.Sprintf("%s/oauth/token", g.url),
|
||||
},
|
||||
Scopes: []string{defaultScope},
|
||||
RedirectURL: fmt.Sprintf("%s/authorize", server.Config.Server.OAuthHost),
|
||||
RedirectURL: fmt.Sprintf("%s%s/authorize", server.Config.Server.OAuthHost, server.Config.Server.RootPath),
|
||||
},
|
||||
|
||||
context.WithValue(ctx, oauth2.HTTPClient, &http.Client{Transport: &http.Transport{
|
||||
|
||||
@@ -23,7 +23,7 @@ import (
|
||||
"github.com/woodpecker-ci/woodpecker/server/router/middleware/session"
|
||||
)
|
||||
|
||||
func apiRoutes(e *gin.Engine) {
|
||||
func apiRoutes(e *gin.RouterGroup) {
|
||||
apiBase := e.Group("/api")
|
||||
{
|
||||
user := apiBase.Group("/user")
|
||||
|
||||
@@ -22,9 +22,9 @@ import (
|
||||
"github.com/rs/zerolog/log"
|
||||
swaggerfiles "github.com/swaggo/files"
|
||||
ginSwagger "github.com/swaggo/gin-swagger"
|
||||
|
||||
"github.com/woodpecker-ci/woodpecker/cmd/server/docs"
|
||||
"github.com/woodpecker-ci/woodpecker/server"
|
||||
|
||||
"github.com/woodpecker-ci/woodpecker/server/api"
|
||||
"github.com/woodpecker-ci/woodpecker/server/api/metrics"
|
||||
"github.com/woodpecker-ci/woodpecker/server/router/middleware/header"
|
||||
@@ -53,22 +53,25 @@ func Load(noRouteHandler http.HandlerFunc, middleware ...gin.HandlerFunc) http.H
|
||||
|
||||
e.NoRoute(gin.WrapF(noRouteHandler))
|
||||
|
||||
e.GET("/web-config.js", web.Config)
|
||||
|
||||
e.GET("/logout", api.GetLogout)
|
||||
e.GET("/login", api.HandleLogin)
|
||||
auth := e.Group("/authorize")
|
||||
base := e.Group(server.Config.Server.RootPath)
|
||||
{
|
||||
auth.GET("", api.HandleAuth)
|
||||
auth.POST("", api.HandleAuth)
|
||||
auth.POST("/token", api.GetLoginToken)
|
||||
base.GET("/web-config.js", web.Config)
|
||||
|
||||
base.GET("/logout", api.GetLogout)
|
||||
base.GET("/login", api.HandleLogin)
|
||||
auth := base.Group("/authorize")
|
||||
{
|
||||
auth.GET("", api.HandleAuth)
|
||||
auth.POST("", api.HandleAuth)
|
||||
auth.POST("/token", api.GetLoginToken)
|
||||
}
|
||||
|
||||
base.GET("/metrics", metrics.PromHandler())
|
||||
base.GET("/version", api.Version)
|
||||
base.GET("/healthz", api.Health)
|
||||
}
|
||||
|
||||
e.GET("/metrics", metrics.PromHandler())
|
||||
e.GET("/version", api.Version)
|
||||
e.GET("/healthz", api.Health)
|
||||
|
||||
apiRoutes(e)
|
||||
apiRoutes(base)
|
||||
if server.Config.Server.EnableSwagger {
|
||||
setupSwaggerConfigAndRoutes(e)
|
||||
}
|
||||
@@ -78,8 +81,8 @@ func Load(noRouteHandler http.HandlerFunc, middleware ...gin.HandlerFunc) http.H
|
||||
|
||||
func setupSwaggerConfigAndRoutes(e *gin.Engine) {
|
||||
docs.SwaggerInfo.Host = getHost(server.Config.Server.Host)
|
||||
docs.SwaggerInfo.BasePath = server.Config.Server.RootURL + "/api"
|
||||
e.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerfiles.Handler))
|
||||
docs.SwaggerInfo.BasePath = server.Config.Server.RootPath + "/api"
|
||||
e.GET(server.Config.Server.RootPath+"/swagger/*any", ginSwagger.WrapHandler(swaggerfiles.Handler))
|
||||
}
|
||||
|
||||
func getHost(s string) string {
|
||||
|
||||
@@ -45,7 +45,7 @@ func Config(c *gin.Context) {
|
||||
"docs": server.Config.Server.Docs,
|
||||
"version": version.String(),
|
||||
"forge": server.Config.Services.Forge.Name(),
|
||||
"root_url": server.Config.Server.RootURL,
|
||||
"root_path": server.Config.Server.RootPath,
|
||||
"enable_swagger": server.Config.Server.EnableSwagger,
|
||||
}
|
||||
|
||||
@@ -75,6 +75,6 @@ window.WOODPECKER_CSRF = "{{ .csrf }}";
|
||||
window.WOODPECKER_VERSION = "{{ .version }}";
|
||||
window.WOODPECKER_DOCS = "{{ .docs }}";
|
||||
window.WOODPECKER_FORGE = "{{ .forge }}";
|
||||
window.WOODPECKER_ROOT_URL = "{{ .root_url }}";
|
||||
window.WOODPECKER_ROOT_PATH = "{{ .root_path }}";
|
||||
window.WOODPECKER_ENABLE_SWAGGER = {{ .enable_swagger }};
|
||||
`
|
||||
|
||||
@@ -17,10 +17,11 @@ package web
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -54,24 +55,23 @@ func New() (*gin.Engine, error) {
|
||||
|
||||
e.Use(setupCache)
|
||||
|
||||
rootURL, _ := url.Parse(server.Config.Server.RootURL)
|
||||
rootPath := rootURL.Path
|
||||
rootPath := server.Config.Server.RootPath
|
||||
|
||||
httpFS, err := web.HTTPFS()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h := http.FileServer(&prefixFS{httpFS, rootPath})
|
||||
e.GET(rootPath+"/favicon.svg", redirect(server.Config.Server.RootURL+"/favicons/favicon-light-default.svg", http.StatusPermanentRedirect))
|
||||
e.GET(rootPath+"/favicons/*filepath", gin.WrapH(h))
|
||||
e.GET(rootPath+"/assets/*filepath", gin.WrapH(handleCustomFilesAndAssets(h)))
|
||||
f := &prefixFS{httpFS, rootPath}
|
||||
e.GET(rootPath+"/favicon.svg", redirect(server.Config.Server.RootPath+"/favicons/favicon-light-default.svg", http.StatusPermanentRedirect))
|
||||
e.GET(rootPath+"/favicons/*filepath", serveFile(f))
|
||||
e.GET(rootPath+"/assets/*filepath", handleCustomFilesAndAssets(f))
|
||||
|
||||
e.NoRoute(handleIndex)
|
||||
|
||||
return e, nil
|
||||
}
|
||||
|
||||
func handleCustomFilesAndAssets(assetHandler http.Handler) http.HandlerFunc {
|
||||
func handleCustomFilesAndAssets(fs *prefixFS) func(ctx *gin.Context) {
|
||||
serveFileOrEmptyContent := func(w http.ResponseWriter, r *http.Request, localFileName string) {
|
||||
if len(localFileName) > 0 {
|
||||
http.ServeFile(w, r, localFileName)
|
||||
@@ -80,13 +80,50 @@ func handleCustomFilesAndAssets(assetHandler http.Handler) http.HandlerFunc {
|
||||
http.ServeContent(w, r, localFileName, time.Now(), bytes.NewReader([]byte{}))
|
||||
}
|
||||
}
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if strings.HasSuffix(r.RequestURI, "/assets/custom.js") {
|
||||
serveFileOrEmptyContent(w, r, server.Config.Server.CustomJsFile)
|
||||
} else if strings.HasSuffix(r.RequestURI, "/assets/custom.css") {
|
||||
serveFileOrEmptyContent(w, r, server.Config.Server.CustomCSSFile)
|
||||
return func(ctx *gin.Context) {
|
||||
if strings.HasSuffix(ctx.Request.RequestURI, "/assets/custom.js") {
|
||||
serveFileOrEmptyContent(ctx.Writer, ctx.Request, server.Config.Server.CustomJsFile)
|
||||
} else if strings.HasSuffix(ctx.Request.RequestURI, "/assets/custom.css") {
|
||||
serveFileOrEmptyContent(ctx.Writer, ctx.Request, server.Config.Server.CustomCSSFile)
|
||||
} else {
|
||||
assetHandler.ServeHTTP(w, r)
|
||||
serveFile(fs)(ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func serveFile(f *prefixFS) func(ctx *gin.Context) {
|
||||
return func(ctx *gin.Context) {
|
||||
file, err := f.Open(ctx.Request.URL.Path)
|
||||
if err != nil {
|
||||
code := http.StatusInternalServerError
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
code = http.StatusNotFound
|
||||
} else if errors.Is(err, fs.ErrPermission) {
|
||||
code = http.StatusForbidden
|
||||
}
|
||||
ctx.Status(code)
|
||||
return
|
||||
}
|
||||
data, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
ctx.Status(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
var mime string
|
||||
switch {
|
||||
case strings.HasSuffix(ctx.Request.URL.Path, ".js"):
|
||||
mime = "text/javascript"
|
||||
case strings.HasSuffix(ctx.Request.URL.Path, ".css"):
|
||||
mime = "text/css"
|
||||
case strings.HasSuffix(ctx.Request.URL.Path, ".png"):
|
||||
mime = "image/png"
|
||||
case strings.HasSuffix(ctx.Request.URL.Path, ".svg"):
|
||||
mime = "image/svg"
|
||||
}
|
||||
ctx.Status(http.StatusOK)
|
||||
ctx.Writer.Header().Set("Content-Type", mime)
|
||||
if _, err := ctx.Writer.Write(replaceBytes(data)); err != nil {
|
||||
log.Error().Err(err).Msgf("can not write %s", ctx.Request.URL.Path)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -112,15 +149,24 @@ func handleIndex(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
func loadFile(path string) ([]byte, error) {
|
||||
data, err := web.Lookup(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return replaceBytes(data), nil
|
||||
}
|
||||
|
||||
func replaceBytes(data []byte) []byte {
|
||||
return bytes.ReplaceAll(data, []byte("/BASE_PATH"), []byte(server.Config.Server.RootPath))
|
||||
}
|
||||
|
||||
func parseIndex() []byte {
|
||||
data, err := web.Lookup("index.html")
|
||||
data, err := loadFile("index.html")
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("can not find 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"))
|
||||
return data
|
||||
}
|
||||
|
||||
func setupCache(c *gin.Context) {
|
||||
|
||||
Reference in New Issue
Block a user