This commit is contained in:
Thomas Miceli 2024-12-29 11:40:23 +01:00
parent d2f6fe1ab8
commit d75840eba8
11 changed files with 303 additions and 310 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -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(`<script src="%s"></script>`, 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)
}

View file

@ -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(`<script src="%s"></script>`, 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)
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -1,3 +1,4 @@
{{ define "error" }}
{{ template "header" .}}
<div class="mt-4">
@ -12,3 +13,4 @@
{{ end }}
</div>
{{ template "footer" .}}
{{end}}