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:
qwerty287
2023-08-07 16:05:18 +02:00
committed by GitHub
parent 10b1cfcd3b
commit 67b7de5cc2
30 changed files with 162 additions and 101 deletions

View File

@@ -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) {

View File

@@ -67,7 +67,7 @@ var Config = struct {
StatusContext string
StatusContextFormat string
SessionExpires time.Duration
RootURL string
RootPath string
CustomCSSFile string
CustomJsFile string
Migrations struct {

View File

@@ -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),
}
}

View File

@@ -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{

View File

@@ -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{

View File

@@ -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{

View File

@@ -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")

View File

@@ -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 {

View File

@@ -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 }};
`

View File

@@ -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) {