From d75840eba84700a241fb20cfd76226a43a5926ed Mon Sep 17 00:00:00 2001 From: Thomas Miceli Date: Sun, 29 Dec 2024 11:40:23 +0100 Subject: [PATCH] wip --- internal/cli/main.go | 4 +- internal/web/context/context.go | 27 ++++--- internal/web/context/store.go | 6 +- internal/web/handler/auth.go | 32 ++++---- internal/web/handler/gist.go | 112 +-------------------------- internal/web/server/middlewares.go | 118 ++++++++++++++++++++++++++++- internal/web/server/renderer.go | 98 +++++++++++------------- internal/web/server/router.go | 108 +++++++++++++------------- internal/web/server/server.go | 100 +++++++++++------------- internal/web/test/server.go | 6 +- templates/pages/error.html | 2 + 11 files changed, 303 insertions(+), 310 deletions(-) diff --git a/internal/cli/main.go b/internal/cli/main.go index 4c980fd..186b0e3 100644 --- a/internal/cli/main.go +++ b/internal/cli/main.go @@ -10,7 +10,7 @@ import ( "github.com/thomiceli/opengist/internal/index" "github.com/thomiceli/opengist/internal/memdb" "github.com/thomiceli/opengist/internal/ssh" - "github.com/thomiceli/opengist/internal/web" + "github.com/thomiceli/opengist/internal/web/server" "github.com/urfave/cli/v2" "os" "os/signal" @@ -37,7 +37,7 @@ var CmdStart = cli.Command{ Initialize(ctx) - go web.NewServer(os.Getenv("OG_DEV") == "1", path.Join(config.GetHomeDir(), "sessions"), false).Start() + go server.NewServer(os.Getenv("OG_DEV") == "1", path.Join(config.GetHomeDir(), "sessions"), false).Start() go ssh.Start() <-stopCtx.Done() diff --git a/internal/web/context/context.go b/internal/web/context/context.go index 9c3559c..208fc07 100644 --- a/internal/web/context/context.go +++ b/internal/web/context/context.go @@ -1,6 +1,7 @@ package context import ( + "context" "github.com/gorilla/sessions" "github.com/labstack/echo/v4" "github.com/rs/zerolog/log" @@ -9,29 +10,38 @@ import ( "github.com/thomiceli/opengist/internal/i18n" "html/template" "net/http" + "sync" ) type OGContext struct { echo.Context data echo.Map + lock sync.RWMutex store *Store User *db.User } -func NewContext(c echo.Context) *OGContext { +func NewContext(c echo.Context, sessionPath string) *OGContext { return &OGContext{ Context: c, data: make(echo.Map), + store: NewStore(sessionPath), } } func (ctx *OGContext) SetData(key string, value any) { + ctx.lock.Lock() + defer ctx.lock.Unlock() + ctx.data[key] = value } func (ctx *OGContext) GetData(key string) any { + ctx.lock.RLock() + defer ctx.lock.RUnlock() + return ctx.data[key] } @@ -45,14 +55,7 @@ func (ctx *OGContext) ErrorRes(code int, message string, err error) error { skipLogger.Error().Err(err).Msg(message) } - return &echo.HTTPError{Code: code, Message: message, Internal: err} -} - -func (ctx *OGContext) JsonErrorRes(code int, message string, err error) error { - if code >= 500 { - var skipLogger = log.With().CallerWithSkipFrameCount(3).Logger() - skipLogger.Error().Err(err).Msg(message) - } + ctx.SetRequest(ctx.Request().WithContext(context.WithValue(ctx.Request().Context(), "data", ctx.data))) return &echo.HTTPError{Code: code, Message: message, Internal: err} } @@ -138,3 +141,9 @@ func (ctx *OGContext) Tr(key string, args ...any) string { l := ctx.GetData("locale").(*i18n.Locale) return l.String(key, args...) } + +var ManifestEntries map[string]Asset + +type Asset struct { + File string `json:"file"` +} diff --git a/internal/web/context/store.go b/internal/web/context/store.go index 01a0ef0..162960e 100644 --- a/internal/web/context/store.go +++ b/internal/web/context/store.go @@ -16,13 +16,13 @@ type Store struct { } func NewStore(sessionsPath string) *Store { - return &Store{sessionsPath: sessionsPath} -} + s := &Store{sessionsPath: sessionsPath} -func (s *Store) setupSessionStore() { s.flashStore = sessions.NewCookieStore([]byte("opengist")) encryptKey, _ := utils.GenerateSecretKey(filepath.Join(s.sessionsPath, "session-encrypt.key")) s.UserStore = sessions.NewFilesystemStore(s.sessionsPath, config.SecretKey, encryptKey) s.UserStore.MaxLength(10 * 1024) gothic.Store = s.UserStore + + return s } diff --git a/internal/web/handler/auth.go b/internal/web/handler/auth.go index 499d8a2..945f140 100644 --- a/internal/web/handler/auth.go +++ b/internal/web/handler/auth.go @@ -443,7 +443,7 @@ func FinishWebAuthnBinding(ctx *context.OGContext) error { sess := ctx.GetSession() jsonWaSession, ok := sess.Values["webauthn_registration_session"].([]byte) if !ok { - return ctx.JsonErrorRes(401, "Cannot get WebAuthn registration session", nil) + return ctx.ErrorRes(401, "Cannot get WebAuthn registration session", nil) } user := ctx.User @@ -451,7 +451,7 @@ func FinishWebAuthnBinding(ctx *context.OGContext) error { // extract passkey name from request body, err := io.ReadAll(ctx.Request().Body) if err != nil { - return ctx.JsonErrorRes(400, "Failed to read request body", err) + return ctx.ErrorRes(400, "Failed to read request body", err) } ctx.Request().Body.Close() ctx.Request().Body = io.NopCloser(bytes.NewBuffer(body)) @@ -460,7 +460,7 @@ func FinishWebAuthnBinding(ctx *context.OGContext) error { _ = gojson.Unmarshal(body, &dto) if err = ctx.Validate(dto); err != nil { - return ctx.JsonErrorRes(400, "Invalid request", err) + return ctx.ErrorRes(400, "Invalid request", err) } passkeyName := dto.PasskeyName if passkeyName == "" { @@ -469,11 +469,11 @@ func FinishWebAuthnBinding(ctx *context.OGContext) error { waCredential, err := webauthn.FinishBinding(user, jsonWaSession, ctx.Request()) if err != nil { - return ctx.JsonErrorRes(403, "Failed binding attempt for passkey", err) + return ctx.ErrorRes(403, "Failed binding attempt for passkey", err) } if _, err = db.CreateFromCrendential(user.ID, passkeyName, waCredential); err != nil { - return ctx.JsonErrorRes(500, "Cannot create WebAuthn credential on database", err) + return ctx.ErrorRes(500, "Cannot create WebAuthn credential on database", err) } delete(sess.Values, "webauthn_registration_session") @@ -486,7 +486,7 @@ func FinishWebAuthnBinding(ctx *context.OGContext) error { func BeginWebAuthnLogin(ctx *context.OGContext) error { credsCreation, jsonWaSession, err := webauthn.BeginDiscoverableLogin() if err != nil { - return ctx.JsonErrorRes(401, "Cannot begin WebAuthn login", err) + return ctx.ErrorRes(401, "Cannot begin WebAuthn login", err) } sess := ctx.GetSession() @@ -501,12 +501,12 @@ func FinishWebAuthnLogin(ctx *context.OGContext) error { sess := ctx.GetSession() sessionData, ok := sess.Values["webauthn_login_session"].([]byte) if !ok { - return ctx.JsonErrorRes(401, "Cannot get WebAuthn login session", nil) + return ctx.ErrorRes(401, "Cannot get WebAuthn login session", nil) } userID, err := webauthn.FinishDiscoverableLogin(sessionData, ctx.Request()) if err != nil { - return ctx.JsonErrorRes(403, "Failed authentication attempt for passkey", err) + return ctx.ErrorRes(403, "Failed authentication attempt for passkey", err) } sess.Values["user"] = userID @@ -523,12 +523,12 @@ func BeginWebAuthnAssertion(ctx *context.OGContext) error { ogUser, err := db.GetUserById(sess.Values["mfaID"].(uint)) if err != nil { - return ctx.JsonErrorRes(500, "Cannot get user", err) + return ctx.ErrorRes(500, "Cannot get user", err) } credsCreation, jsonWaSession, err := webauthn.BeginLogin(ogUser) if err != nil { - return ctx.JsonErrorRes(401, "Cannot begin WebAuthn login", err) + return ctx.ErrorRes(401, "Cannot begin WebAuthn login", err) } sess.Values["webauthn_assertion_session"] = jsonWaSession @@ -542,18 +542,18 @@ func FinishWebAuthnAssertion(ctx *context.OGContext) error { sess := ctx.GetSession() sessionData, ok := sess.Values["webauthn_assertion_session"].([]byte) if !ok { - return ctx.JsonErrorRes(401, "Cannot get WebAuthn assertion session", nil) + return ctx.ErrorRes(401, "Cannot get WebAuthn assertion session", nil) } userId := sess.Values["mfaID"].(uint) ogUser, err := db.GetUserById(userId) if err != nil { - return ctx.JsonErrorRes(500, "Cannot get user", err) + return ctx.ErrorRes(500, "Cannot get user", err) } if err = webauthn.FinishLogin(ogUser, sessionData, ctx.Request()); err != nil { - return ctx.JsonErrorRes(403, "Failed authentication attempt for passkey", err) + return ctx.ErrorRes(403, "Failed authentication attempt for passkey", err) } sess.Values["user"] = userId @@ -803,13 +803,13 @@ func getAvatarUrlFromProvider(provider string, identifier string) string { } type ContextAuthInfo struct { - context *context.OGContext + Context *context.OGContext } func (auth ContextAuthInfo) RequireLogin() (bool, error) { - return auth.context.GetData("RequireLogin") == true, nil + return auth.Context.GetData("RequireLogin") == true, nil } func (auth ContextAuthInfo) AllowGistsWithoutLogin() (bool, error) { - return auth.context.GetData("AllowGistsWithoutLogin") == true, nil + return auth.Context.GetData("AllowGistsWithoutLogin") == true, nil } diff --git a/internal/web/handler/gist.go b/internal/web/handler/gist.go index 8ec6334..55bd28a 100644 --- a/internal/web/handler/gist.go +++ b/internal/web/handler/gist.go @@ -8,10 +8,8 @@ import ( "errors" "fmt" "github.com/thomiceli/opengist/internal/web/context" - "github.com/thomiceli/opengist/internal/web/server" "html/template" "net/url" - "path/filepath" "regexp" "strconv" "strings" @@ -25,116 +23,10 @@ import ( "github.com/thomiceli/opengist/internal/utils" "github.com/google/uuid" - "github.com/labstack/echo/v4" - "github.com/thomiceli/opengist/internal/config" "github.com/thomiceli/opengist/internal/db" "gorm.io/gorm" ) -func GistInit(next context.Handler) context.Handler { - return func(ctx *context.OGContext) error { - currUser := ctx.User - - userName := ctx.Param("user") - gistName := ctx.Param("gistname") - - switch filepath.Ext(gistName) { - case ".js": - ctx.SetData("gistpage", "js") - gistName = strings.TrimSuffix(gistName, ".js") - case ".json": - ctx.SetData("gistpage", "json") - gistName = strings.TrimSuffix(gistName, ".json") - case ".git": - ctx.SetData("gistpage", "git") - gistName = strings.TrimSuffix(gistName, ".git") - } - - gist, err := db.GetGist(userName, gistName) - if err != nil { - return ctx.NotFound("Gist not found") - } - - if gist.Private == db.PrivateVisibility { - if currUser == nil || currUser.ID != gist.UserID { - return ctx.NotFound("Gist not found") - } - } - - ctx.SetData("gist", gist) - - if config.C.SshGit { - var sshDomain string - - if config.C.SshExternalDomain != "" { - sshDomain = config.C.SshExternalDomain - } else { - sshDomain = strings.Split(ctx.Request().Host, ":")[0] - } - - if config.C.SshPort == "22" { - ctx.SetData("sshCloneUrl", sshDomain+":"+userName+"/"+gistName+".git") - } else { - ctx.SetData("sshCloneUrl", "ssh://"+sshDomain+":"+config.C.SshPort+"/"+userName+"/"+gistName+".git") - } - } - - baseHttpUrl := ctx.GetData("baseHttpUrl").(string) - - if config.C.HttpGit { - ctx.SetData("httpCloneUrl", baseHttpUrl+"/"+userName+"/"+gistName+".git") - } - - ctx.SetData("httpCopyUrl", baseHttpUrl+"/"+userName+"/"+gistName) - ctx.SetData("currentUrl", template.URL(ctx.Request().URL.Path)) - ctx.SetData("embedScript", fmt.Sprintf(``, baseHttpUrl+"/"+userName+"/"+gistName+".js")) - - nbCommits, err := gist.NbCommits() - if err != nil { - return ctx.ErrorRes(500, "Error fetching number of commits", err) - } - ctx.SetData("nbCommits", nbCommits) - - if currUser != nil { - hasLiked, err := currUser.HasLiked(gist) - if err != nil { - return ctx.ErrorRes(500, "Cannot get user like status", err) - } - ctx.SetData("hasLiked", hasLiked) - } - - if gist.Private > 0 { - ctx.SetData("NoIndex", true) - } - - return next(ctx) - } -} - -// GistSoftInit try to load a gist (same as gistInit) but does not return a 404 if the gist is not found -// useful for git clients using HTTP to obfuscate the existence of a private gist -func GistSoftInit(next echo.HandlerFunc) context.Handler { - return func(ctx *context.OGContext) error { - userName := ctx.Param("user") - gistName := ctx.Param("gistname") - - gistName = strings.TrimSuffix(gistName, ".git") - - gist, _ := db.GetGist(userName, gistName) - ctx.SetData("gist", gist) - - return next(ctx) - } -} - -// GistNewPushSoftInit has the same behavior as gistSoftInit but create a new gist empty instead -func GistNewPushSoftInit(next context.Handler) context.Handler { - return func(ctx *context.OGContext) error { - ctx.SetData("gist", new(db.Gist)) - return next(ctx) - } -} - func AllGists(ctx *context.OGContext) error { var err error var urlPage string @@ -382,7 +274,7 @@ func GistJson(ctx *context.OGContext) error { return ctx.ErrorRes(500, "Error joining js url", err) } - cssUrl, err := url.JoinPath(ctx.GetData("baseHttpUrl").(string), server.ManifestEntries["embed.css"].File) + cssUrl, err := url.JoinPath(ctx.GetData("baseHttpUrl").(string), context.ManifestEntries["embed.css"].File) if err != nil { return ctx.ErrorRes(500, "Error joining css url", err) } @@ -426,7 +318,7 @@ func GistJs(ctx *context.OGContext) error { } _ = w.Flush() - cssUrl, err := url.JoinPath(ctx.GetData("baseHttpUrl").(string), server.ManifestEntries["embed.css"].File) + cssUrl, err := url.JoinPath(ctx.GetData("baseHttpUrl").(string), context.ManifestEntries["embed.css"].File) if err != nil { return ctx.ErrorRes(500, "Error joining css url", err) } diff --git a/internal/web/server/middlewares.go b/internal/web/server/middlewares.go index 802e962..3999069 100644 --- a/internal/web/server/middlewares.go +++ b/internal/web/server/middlewares.go @@ -1,6 +1,7 @@ package server import ( + "fmt" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" "github.com/rs/zerolog/log" @@ -12,6 +13,7 @@ import ( "golang.org/x/text/language" "html/template" "net/http" + "path/filepath" "strings" "time" ) @@ -19,7 +21,7 @@ import ( func (s *Server) useCustomContext() { s.echo.Use(func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { - cc := context.NewContext(c) + cc := context.NewContext(c, s.sessionsPath) return next(cc) } }) @@ -47,6 +49,16 @@ func (s *Server) RegisterMiddlewares() { s.echo.Use(middleware.Secure()) s.echo.Use(Middleware(sessionInit).ToEcho()) + + if !s.ignoreCsrf { + s.echo.Use(middleware.CSRFWithConfig(middleware.CSRFConfig{ + TokenLookup: "form:_csrf,header:X-CSRF-Token", + CookiePath: "/", + CookieHTTPOnly: true, + CookieSameSite: http.SameSiteStrictMode, + })) + s.echo.Use(Middleware(csrfInit).ToEcho()) + } } func dataInit(next Handler) Handler { @@ -180,3 +192,107 @@ func loadSettings(ctx *context.OGContext) error { } return nil } + +func GistInit(next Handler) Handler { + return func(ctx *context.OGContext) error { + currUser := ctx.User + + userName := ctx.Param("user") + gistName := ctx.Param("gistname") + + switch filepath.Ext(gistName) { + case ".js": + ctx.SetData("gistpage", "js") + gistName = strings.TrimSuffix(gistName, ".js") + case ".json": + ctx.SetData("gistpage", "json") + gistName = strings.TrimSuffix(gistName, ".json") + case ".git": + ctx.SetData("gistpage", "git") + gistName = strings.TrimSuffix(gistName, ".git") + } + + gist, err := db.GetGist(userName, gistName) + if err != nil { + return ctx.NotFound("Gist not found") + } + + if gist.Private == db.PrivateVisibility { + if currUser == nil || currUser.ID != gist.UserID { + return ctx.NotFound("Gist not found") + } + } + + ctx.SetData("gist", gist) + + if config.C.SshGit { + var sshDomain string + + if config.C.SshExternalDomain != "" { + sshDomain = config.C.SshExternalDomain + } else { + sshDomain = strings.Split(ctx.Request().Host, ":")[0] + } + + if config.C.SshPort == "22" { + ctx.SetData("sshCloneUrl", sshDomain+":"+userName+"/"+gistName+".git") + } else { + ctx.SetData("sshCloneUrl", "ssh://"+sshDomain+":"+config.C.SshPort+"/"+userName+"/"+gistName+".git") + } + } + + baseHttpUrl := ctx.GetData("baseHttpUrl").(string) + + if config.C.HttpGit { + ctx.SetData("httpCloneUrl", baseHttpUrl+"/"+userName+"/"+gistName+".git") + } + + ctx.SetData("httpCopyUrl", baseHttpUrl+"/"+userName+"/"+gistName) + ctx.SetData("currentUrl", template.URL(ctx.Request().URL.Path)) + ctx.SetData("embedScript", fmt.Sprintf(``, baseHttpUrl+"/"+userName+"/"+gistName+".js")) + + nbCommits, err := gist.NbCommits() + if err != nil { + return ctx.ErrorRes(500, "Error fetching number of commits", err) + } + ctx.SetData("nbCommits", nbCommits) + + if currUser != nil { + hasLiked, err := currUser.HasLiked(gist) + if err != nil { + return ctx.ErrorRes(500, "Cannot get user like status", err) + } + ctx.SetData("hasLiked", hasLiked) + } + + if gist.Private > 0 { + ctx.SetData("NoIndex", true) + } + + return next(ctx) + } +} + +// GistSoftInit try to load a gist (same as gistInit) but does not return a 404 if the gist is not found +// useful for git clients using HTTP to obfuscate the existence of a private gist +func GistSoftInit(next Handler) Handler { + return func(ctx *context.OGContext) error { + userName := ctx.Param("user") + gistName := ctx.Param("gistname") + + gistName = strings.TrimSuffix(gistName, ".git") + + gist, _ := db.GetGist(userName, gistName) + ctx.SetData("gist", gist) + + return next(ctx) + } +} + +// GistNewPushSoftInit has the same behavior as gistSoftInit but create a new gist empty instead +func GistNewPushSoftInit(next Handler) Handler { + return func(ctx *context.OGContext) error { + ctx.SetData("gist", new(db.Gist)) + return next(ctx) + } +} diff --git a/internal/web/server/renderer.go b/internal/web/server/renderer.go index ad4cb6c..0cfda49 100644 --- a/internal/web/server/renderer.go +++ b/internal/web/server/renderer.go @@ -9,6 +9,7 @@ import ( "github.com/thomiceli/opengist/internal/db" "github.com/thomiceli/opengist/internal/git" "github.com/thomiceli/opengist/internal/index" + "github.com/thomiceli/opengist/internal/web/context" "github.com/thomiceli/opengist/internal/web/handler" "github.com/thomiceli/opengist/public" "github.com/thomiceli/opengist/templates" @@ -82,14 +83,33 @@ func (s *Server) setFuncMap() { return "https://www.gravatar.com/avatar/" + user.MD5Hash + "?d=identicon&s=200" } - return defaultAvatar() + if s.dev { + return "http://localhost:16157/default.png" + } + return config.C.ExternalUrl + "/" + context.ManifestEntries["default.png"].File + }, + "asset": func(file string) string { + if s.dev { + return "http://localhost:16157/" + file + } + return config.C.ExternalUrl + "/" + context.ManifestEntries[file].File + }, + "custom": func(file string) string { + assetpath, err := url.JoinPath("/", "assets", file) + if err != nil { + log.Error().Err(err).Msgf("Failed to join path for custom file %s", file) + } + return config.C.ExternalUrl + assetpath }, - "asset": asset, - "custom": customAsset, "dev": func() bool { return s.dev }, - "defaultAvatar": defaultAvatar, + "defaultAvatar": func() string { + if s.dev { + return "http://localhost:16157/default.png" + } + return config.C.ExternalUrl + "/" + context.ManifestEntries["default.png"].File + }, "visibilityStr": func(visibility db.Visibility, lowercase bool) string { s := "Public" switch visibility { @@ -128,8 +148,24 @@ func (s *Server) setFuncMap() { } return dict, nil }, - "addMetadataToSearchQuery": addMetadataToSearchQuery, - "indexEnabled": index.Enabled, + "addMetadataToSearchQuery": func(input, key, value string) string { + content, metadata := handler.ParseSearchQueryStr(input) + + metadata[key] = value + + var resultBuilder strings.Builder + resultBuilder.WriteString(content) + + for k, v := range metadata { + resultBuilder.WriteString(" ") + resultBuilder.WriteString(k) + resultBuilder.WriteString(":") + resultBuilder.WriteString(v) + } + + return strings.TrimSpace(resultBuilder.String()) + }, + "indexEnabled": index.Enabled, "isUrl": func(s string) bool { _, err := url.ParseRequestURI(s) return err == nil @@ -153,12 +189,6 @@ func (s *Server) setFuncMap() { } } -type Asset struct { - File string `json:"file"` -} - -var ManifestEntries map[string]Asset - func parseManifestEntries() { file, err := public.Files.Open("manifest.json") if err != nil { @@ -168,49 +198,7 @@ func parseManifestEntries() { if err != nil { log.Fatal().Err(err).Msg("Failed to read manifest.json") } - if err = gojson.Unmarshal(byteValue, &ManifestEntries); err != nil { + if err = gojson.Unmarshal(byteValue, &context.ManifestEntries); err != nil { log.Fatal().Err(err).Msg("Failed to unmarshal manifest.json") } } - -var dev = true - -func defaultAvatar() string { - if dev { - return "http://localhost:16157/default.png" - } - return config.C.ExternalUrl + "/" + ManifestEntries["default.png"].File -} - -func asset(file string) string { - if dev { - return "http://localhost:16157/" + file - } - return config.C.ExternalUrl + "/" + ManifestEntries[file].File -} - -func customAsset(file string) string { - assetpath, err := url.JoinPath("/", "assets", file) - if err != nil { - log.Error().Err(err).Msgf("Failed to join path for custom file %s", file) - } - return config.C.ExternalUrl + assetpath -} - -func addMetadataToSearchQuery(input, key, value string) string { - content, metadata := handler.ParseSearchQueryStr(input) - - metadata[key] = value - - var resultBuilder strings.Builder - resultBuilder.WriteString(content) - - for k, v := range metadata { - resultBuilder.WriteString(" ") - resultBuilder.WriteString(k) - resultBuilder.WriteString(":") - resultBuilder.WriteString(v) - } - - return strings.TrimSpace(resultBuilder.String()) -} diff --git a/internal/web/server/router.go b/internal/web/server/router.go index f81d7fb..165da14 100644 --- a/internal/web/server/router.go +++ b/internal/web/server/router.go @@ -2,7 +2,6 @@ package server import ( "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" "github.com/thomiceli/opengist/internal/config" "github.com/thomiceli/opengist/internal/index" "github.com/thomiceli/opengist/internal/web/context" @@ -21,15 +20,6 @@ func (s *Server) setupRoutes() { // Web based routes { - if !s.ignoreCsrf { - r.Use(middleware.CSRFWithConfig(middleware.CSRFConfig{ - TokenLookup: "form:_csrf,header:X-CSRF-Token", - CookiePath: "/", - CookieHTTPOnly: true, - CookieSameSite: http.SameSiteStrictMode, - })) - r.Use(csrfInit) - } r.GET("/", handler.Create, logged) r.POST("/", handler.ProcessCreate, logged) @@ -68,67 +58,67 @@ func (s *Server) setupRoutes() { r.DELETE("/settings/totp", handler.DisableTotp, logged) r.POST("/settings/totp/regenerate", handler.RegenerateTotpRecoveryCodes, logged) - g2 := g1.Group("/admin-panel") + g2 := r.SubGroup("/admin-panel") { g2.Use(adminPermission) - g2.GET("", adminIndex) - g2.GET("/users", Handler(adminUsers).ToEcho()) - g2.POST("/users/:user/delete", adminUserDelete) - g2.GET("/gists", adminGists) - g2.POST("/gists/:gist/delete", adminGistDelete) - g2.GET("/invitations", adminInvitations) - g2.POST("/invitations", adminInvitationsCreate) - g2.POST("/invitations/:id/delete", adminInvitationsDelete) - g2.POST("/sync-fs", adminSyncReposFromFS) - g2.POST("/sync-db", adminSyncReposFromDB) - g2.POST("/gc-repos", adminGcRepos) - g2.POST("/sync-previews", adminSyncGistPreviews) - g2.POST("/reset-hooks", adminResetHooks) - g2.POST("/index-gists", adminIndexGists) - g2.GET("/configuration", adminConfig) - g2.PUT("/set-config", adminSetConfig) + g2.GET("", handler.AdminIndex) + g2.GET("/users", handler.AdminUsers) + g2.POST("/users/:user/delete", handler.AdminUserDelete) + g2.GET("/gists", handler.AdminGists) + g2.POST("/gists/:gist/delete", handler.AdminGistDelete) + g2.GET("/invitations", handler.AdminInvitations) + g2.POST("/invitations", handler.AdminInvitationsCreate) + g2.POST("/invitations/:id/delete", handler.AdminInvitationsDelete) + g2.POST("/sync-fs", handler.AdminSyncReposFromFS) + g2.POST("/sync-db", handler.AdminSyncReposFromDB) + g2.POST("/gc-repos", handler.AdminGcRepos) + g2.POST("/sync-previews", handler.AdminSyncGistPreviews) + g2.POST("/reset-hooks", handler.AdminResetHooks) + g2.POST("/index-gists", handler.AdminIndexGists) + g2.GET("/configuration", handler.AdminConfig) + g2.PUT("/set-config", handler.AdminSetConfig) } if config.C.HttpGit { - e.Any("/init/*", gitHttp, gistNewPushSoftInit) + r.Any("/init/*", handler.GitHttp, GistNewPushSoftInit) } - g1.GET("/all", allGists, checkRequireLogin) + r.GET("/all", handler.AllGists, checkRequireLogin) if index.Enabled() { - g1.GET("/search", search, checkRequireLogin) + r.GET("/search", handler.Search, checkRequireLogin) } else { - g1.GET("/search", allGists, checkRequireLogin) + r.GET("/search", handler.AllGists, checkRequireLogin) } - g1.GET("/:user", allGists, checkRequireLogin) - g1.GET("/:user/liked", allGists, checkRequireLogin) - g1.GET("/:user/forked", allGists, checkRequireLogin) + r.GET("/:user", handler.AllGists, checkRequireLogin) + r.GET("/:user/liked", handler.AllGists, checkRequireLogin) + r.GET("/:user/forked", handler.AllGists, checkRequireLogin) - g3 := g1.Group("/:user/:gistname") + g3 := r.SubGroup("/:user/:gistname") { - g3.Use(makeCheckRequireLogin(true), gistInit) - g3.GET("", gistIndex) - g3.GET("/rev/:revision", gistIndex) - g3.GET("/revisions", revisions) - g3.GET("/archive/:revision", downloadZip) - g3.POST("/visibility", editVisibility, logged, writePermission) - g3.POST("/delete", deleteGist, logged, writePermission) - g3.GET("/raw/:revision/:file", rawFile) - g3.GET("/download/:revision/:file", downloadFile) - g3.GET("/edit", edit, logged, writePermission) - g3.POST("/edit", processCreate, logged, writePermission) - g3.POST("/like", like, logged) - g3.GET("/likes", likes, checkRequireLogin) - g3.POST("/fork", fork, logged) - g3.GET("/forks", forks, checkRequireLogin) - g3.PUT("/checkbox", checkbox, logged, writePermission) + g3.Use(makeCheckRequireLogin(true), GistInit) + g3.GET("", handler.GistIndex) + g3.GET("/rev/:revision", handler.GistIndex) + g3.GET("/revisions", handler.Revisions) + g3.GET("/archive/:revision", handler.DownloadZip) + g3.POST("/visibility", handler.EditVisibility, logged, writePermission) + g3.POST("/delete", handler.DeleteGist, logged, writePermission) + g3.GET("/raw/:revision/:file", handler.RawFile) + g3.GET("/download/:revision/:file", handler.DownloadFile) + g3.GET("/edit", handler.Edit, logged, writePermission) + g3.POST("/edit", handler.ProcessCreate, logged, writePermission) + g3.POST("/like", handler.Like, logged) + g3.GET("/likes", handler.Likes, checkRequireLogin) + g3.POST("/fork", handler.Fork, logged) + g3.GET("/forks", handler.Forks, checkRequireLogin) + g3.PUT("/checkbox", handler.Checkbox, logged, writePermission) } } customFs := os.DirFS(filepath.Join(config.GetHomeDir(), "custom")) - e.GET("/assets/*", func(ctx echo.Context) error { - if _, err := public.Files.Open(path.Join("assets", ctx.Param("*"))); !dev && err == nil { + r.GET("/assets/*", func(ctx *context.OGContext) error { + if _, err := public.Files.Open(path.Join("assets", ctx.Param("*"))); !s.dev && err == nil { ctx.Response().Header().Set("Cache-Control", "public, max-age=31536000") ctx.Response().Header().Set("Expires", time.Now().AddDate(1, 0, 0).Format(http.TimeFormat)) @@ -137,8 +127,8 @@ func (s *Server) setupRoutes() { // if the custom file is an .html template, render it if strings.HasSuffix(ctx.Param("*"), ".html") { - if err := html(ctx, ctx.Param("*")); err != nil { - return notFound("Page not found") + if err := ctx.HTML_(ctx.Param("*")); err != nil { + return ctx.NotFound("Page not found") } return nil } @@ -148,10 +138,10 @@ func (s *Server) setupRoutes() { // Git HTTP routes if config.C.HttpGit { - e.Any("/:user/:gistname/*", gitHttp, gistSoftInit) + r.Any("/:user/:gistname/*", handler.GitHttp, GistSoftInit) } - e.Any("/*", noRouteFound) + r.Any("/*", noRouteFound) } // Router wraps echo.Group to provide custom Handler support @@ -200,6 +190,10 @@ func (r *Router) PATCH(path string, h Handler, m ...Middleware) { r.Group.PATCH(path, Chain(h, m...).toEchoHandler()) } +func (r *Router) Any(path string, h Handler, m ...Middleware) { + r.Group.Any(path, Chain(h, m...).toEchoHandler()) +} + // Use registers middleware for the entire router group func (r *Router) Use(middleware ...Middleware) { for _, m := range middleware { diff --git a/internal/web/server/server.go b/internal/web/server/server.go index 7cc6342..987377b 100644 --- a/internal/web/server/server.go +++ b/internal/web/server/server.go @@ -2,29 +2,21 @@ package server import ( "errors" + "github.com/thomiceli/opengist/internal/utils" "github.com/thomiceli/opengist/internal/web/context" "github.com/thomiceli/opengist/internal/web/handler" "html/template" "io" "net/http" - "os" - "path" - "path/filepath" "strings" - "time" - - "github.com/thomiceli/opengist/internal/index" - "github.com/thomiceli/opengist/internal/utils" "github.com/gorilla/sessions" "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" "github.com/rs/zerolog/log" "github.com/thomiceli/opengist/internal/auth" "github.com/thomiceli/opengist/internal/config" "github.com/thomiceli/opengist/internal/db" "github.com/thomiceli/opengist/internal/i18n" - "github.com/thomiceli/opengist/public" ) type Template struct { @@ -60,7 +52,7 @@ func NewServer(isDev bool, sessionsPath string, ignoreCsrf bool) *Server { s.RegisterMiddlewares() s.setFuncMap() - s.setHTTPErrorHandler() + s.echo.HTTPErrorHandler = s.errorHandler e.Validator = utils.NewValidator() @@ -92,95 +84,95 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.echo.ServeHTTP(w, r) } -func writePermission(next echo.HandlerFunc) echo.HandlerFunc { - return func(ctx echo.Context) error { - gist := getData(ctx, "gist") - user := getUserLogged(ctx) +func writePermission(next Handler) Handler { + return func(ctx *context.OGContext) error { + gist := ctx.GetData("gist") + user := ctx.User if !gist.(*db.Gist).CanWrite(user) { - return redirect(ctx, "/"+gist.(*db.Gist).User.Username+"/"+gist.(*db.Gist).Identifier()) + return ctx.RedirectTo("/" + gist.(*db.Gist).User.Username + "/" + gist.(*db.Gist).Identifier()) } return next(ctx) } } -func adminPermission(next echo.HandlerFunc) echo.HandlerFunc { - return func(ctx echo.Context) error { - user := getUserLogged(ctx) +func adminPermission(next Handler) Handler { + return func(ctx *context.OGContext) error { + user := ctx.User if user == nil || !user.IsAdmin { - return notFound("User not found") + return ctx.NotFound("User not found") } return next(ctx) } } -func logged(next echo.HandlerFunc) echo.HandlerFunc { - return func(ctx echo.Context) error { - user := getUserLogged(ctx) +func logged(next Handler) Handler { + return func(ctx *context.OGContext) error { + user := ctx.User if user != nil { return next(ctx) } - return redirect(ctx, "/all") + return ctx.RedirectTo("/all") } } -func inMFASession(next echo.HandlerFunc) echo.HandlerFunc { - return func(ctx echo.Context) error { - sess := getSession(ctx) +func inMFASession(next Handler) Handler { + return func(ctx *context.OGContext) error { + sess := ctx.GetSession() _, ok := sess.Values["mfaID"].(uint) if !ok { - return errorRes(400, tr(ctx, "error.not-in-mfa-session"), nil) + return ctx.ErrorRes(400, ctx.Tr("error.not-in-mfa-session"), nil) } return next(ctx) } } -func makeCheckRequireLogin(isSingleGistAccess bool) echo.MiddlewareFunc { - return func(next echo.HandlerFunc) echo.HandlerFunc { - return func(ctx echo.Context) error { - if user := getUserLogged(ctx); user != nil { +func makeCheckRequireLogin(isSingleGistAccess bool) Middleware { + return func(next Handler) Handler { + return func(ctx *context.OGContext) error { + if user := ctx.User; user != nil { return next(ctx) } - allow, err := auth.ShouldAllowUnauthenticatedGistAccess(ContextAuthInfo{ctx}, isSingleGistAccess) + allow, err := auth.ShouldAllowUnauthenticatedGistAccess(handler.ContextAuthInfo{Context: ctx}, isSingleGistAccess) if err != nil { log.Fatal().Err(err).Msg("Failed to check if unauthenticated access is allowed") } if !allow { - addFlash(ctx, tr(ctx, "flash.auth.must-be-logged-in"), "error") - return redirect(ctx, "/login") + ctx.AddFlash(ctx.Tr("flash.auth.must-be-logged-in"), "error") + return ctx.RedirectTo("/login") } return next(ctx) } } } -func checkRequireLogin(next echo.HandlerFunc) echo.HandlerFunc { +func checkRequireLogin(next Handler) Handler { return makeCheckRequireLogin(false)(next) } -func noRouteFound(echo.Context) error { - return notFound("Page not found") +func noRouteFound(ctx *context.OGContext) error { + return ctx.NotFound("Page not found") } -func (s *Server) setHTTPErrorHandler() { - s.echo.HTTPErrorHandler = func(er error, c echo.Context) { - ctx := c.(*context.OGContext) - var httpErr *echo.HTTPError - if errors.As(er, &httpErr) { - acceptJson := strings.Contains(ctx.Request().Header.Get("Accept"), "application/json") - ctx.SetData("error", er) - if acceptJson { - if fatalErr := jsonWithCode(ctx, httpErr.Code, httpErr); fatalErr != nil { - log.Fatal().Err(fatalErr).Send() - } - } else { - if fatalErr := htmlWithCode(ctx, httpErr.Code, "error.html"); fatalErr != nil { - log.Fatal().Err(fatalErr).Send() - } +func (s *Server) errorHandler(err error, ctx echo.Context) { + var httpErr *echo.HTTPError + if errors.As(err, &httpErr) { + acceptJson := strings.Contains(ctx.Request().Header.Get("Accept"), "application/json") + data := ctx.Request().Context().Value("data").(echo.Map) + data["error"] = err + if acceptJson { + if err := ctx.JSON(httpErr.Code, httpErr); err != nil { + log.Fatal().Err(err).Send() } - } else { - log.Fatal().Err(er).Send() + return } + + if err := ctx.Render(httpErr.Code, "error", data); err != nil { + log.Fatal().Err(err).Send() + } + return } + + log.Fatal().Err(err).Send() } diff --git a/internal/web/test/server.go b/internal/web/test/server.go index ec9d6d0..f508eed 100644 --- a/internal/web/test/server.go +++ b/internal/web/test/server.go @@ -3,6 +3,7 @@ package test import ( "errors" "fmt" + "github.com/thomiceli/opengist/internal/web/server" "io" "net/http" "net/http/httptest" @@ -21,19 +22,18 @@ import ( "github.com/thomiceli/opengist/internal/db" "github.com/thomiceli/opengist/internal/git" "github.com/thomiceli/opengist/internal/memdb" - "github.com/thomiceli/opengist/internal/web" ) var databaseType string type testServer struct { - server *web.Server + server *server.Server sessionCookie string } func newTestServer() (*testServer, error) { s := &testServer{ - server: web.NewServer(true, path.Join(config.GetHomeDir(), "tmp", "sessions"), true), + server: server.NewServer(true, path.Join(config.GetHomeDir(), "tmp", "sessions"), true), } go s.start() diff --git a/templates/pages/error.html b/templates/pages/error.html index 5f868d3..fec6fd3 100644 --- a/templates/pages/error.html +++ b/templates/pages/error.html @@ -1,3 +1,4 @@ +{{ define "error" }} {{ template "header" .}}
@@ -12,3 +13,4 @@ {{ end }}
{{ template "footer" .}} +{{end}}