opengist/internal/web/run.go

339 lines
8.6 KiB
Go
Raw Normal View History

2023-03-14 15:22:52 +00:00
package web
import (
"context"
2023-03-20 12:30:25 +00:00
"crypto/md5"
2023-03-23 15:00:48 +00:00
"encoding/json"
2023-03-14 15:22:52 +00:00
"fmt"
"github.com/gorilla/sessions"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/rs/zerolog/log"
"html/template"
"io"
2023-04-06 09:47:30 +00:00
"io/fs"
2023-03-14 15:22:52 +00:00
"net/http"
"opengist/internal/config"
2023-03-19 02:18:56 +00:00
"opengist/internal/git"
2023-03-14 15:22:52 +00:00
"opengist/internal/models"
"os"
2023-03-14 15:22:52 +00:00
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
)
2023-04-06 09:47:30 +00:00
var dev = os.Getenv("DEV") == "1"
2023-03-14 15:22:52 +00:00
var store *sessions.CookieStore
var re = regexp.MustCompile("[^a-z0-9]+")
2023-03-23 15:00:48 +00:00
var fm = template.FuncMap{
"split": strings.Split,
"indexByte": strings.IndexByte,
"toInt": func(i string) int64 {
val, _ := strconv.ParseInt(i, 10, 64)
return val
},
"inc": func(i int64) int64 {
return i + 1
},
"splitGit": func(i string) []string {
return strings.FieldsFunc(i, func(r rune) bool {
return r == ',' || r == ' '
})
},
"lines": func(i string) []string {
return strings.Split(i, "\n")
},
"isMarkdown": func(i string) bool {
return ".md" == strings.ToLower(filepath.Ext(i))
},
"isCsv": func(i string) bool {
return ".csv" == strings.ToLower(filepath.Ext(i))
},
"csvFile": func(file *git.File) *git.CsvFile {
if ".csv" != strings.ToLower(filepath.Ext(file.Filename)) {
return nil
}
csvFile, err := git.ParseCsv(file)
if err != nil {
return nil
}
return csvFile
},
"httpStatusText": http.StatusText,
"loadedTime": func(startTime time.Time) string {
return fmt.Sprint(time.Since(startTime).Nanoseconds()/1e6) + "ms"
},
"slug": func(s string) string {
return strings.Trim(re.ReplaceAllString(strings.ToLower(s), "-"), "-")
},
"avatarUrl": func(userHash string) string {
return "https://www.gravatar.com/avatar/" + userHash + "?d=identicon&s=200"
},
"emailToMD5": func(email string) string {
return fmt.Sprintf("%x", md5.Sum([]byte(strings.ToLower(strings.TrimSpace(email)))))
},
"asset": func(jsfile string) string {
2023-04-06 09:47:30 +00:00
if dev {
return "http://localhost:16157/" + jsfile
}
2023-03-23 15:00:48 +00:00
return "/" + manifestEntries[jsfile].File
},
}
2023-03-14 15:22:52 +00:00
2023-04-06 09:47:30 +00:00
var EmbedFS fs.FS
2023-03-24 13:41:08 +00:00
2023-03-14 15:22:52 +00:00
type Template struct {
templates *template.Template
}
func (t *Template) Render(w io.Writer, name string, data interface{}, _ echo.Context) error {
return t.templates.ExecuteTemplate(w, name, data)
}
func Start() {
store = sessions.NewCookieStore([]byte("opengist"))
2023-03-24 13:41:08 +00:00
assetsFS := echo.MustSubFS(EmbedFS, "public/assets")
2023-03-14 15:22:52 +00:00
e := echo.New()
e.HideBanner = true
e.HidePort = true
e.Use(dataInit)
e.Pre(middleware.MethodOverrideWithConfig(middleware.MethodOverrideConfig{
Getter: middleware.MethodFromForm("_method"),
}))
e.Pre(middleware.RemoveTrailingSlash())
e.Pre(middleware.CORS())
e.Pre(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
2023-03-14 15:22:52 +00:00
LogURI: true, LogStatus: true, LogMethod: true,
LogValuesFunc: func(ctx echo.Context, v middleware.RequestLoggerValues) error {
log.Info().Str("URI", v.URI).Int("status", v.Status).Str("method", v.Method).
Str("ip", ctx.RealIP()).
2023-03-14 15:22:52 +00:00
Msg("HTTP")
return nil
},
}))
2023-03-18 23:49:06 +00:00
e.Use(middleware.Recover())
2023-03-14 15:22:52 +00:00
e.Use(middleware.Secure())
e.Renderer = &Template{
2023-03-24 13:41:08 +00:00
templates: template.Must(template.New("t").Funcs(fm).ParseFS(EmbedFS, "templates/*/*.html")),
2023-03-14 15:22:52 +00:00
}
e.HTTPErrorHandler = func(er error, ctx echo.Context) {
if err, ok := er.(*echo.HTTPError); ok {
if err.Code >= 500 {
log.Error().Int("code", err.Code).Err(err.Internal).Msg("HTTP: " + err.Message.(string))
}
setData(ctx, "error", err)
if errHtml := htmlWithCode(ctx, err.Code, "error.html"); errHtml != nil {
log.Fatal().Err(errHtml).Send()
}
} else {
log.Fatal().Err(er).Send()
}
}
2023-03-15 09:37:17 +00:00
e.Use(sessionInit)
2023-03-14 15:22:52 +00:00
e.Validator = NewValidator()
2023-04-06 09:47:30 +00:00
if !dev {
parseManifestEntries()
e.GET("/assets/*", cacheControl(echo.WrapHandler(http.StripPrefix("/assets", http.FileServer(http.FS(assetsFS))))))
}
2023-03-14 15:22:52 +00:00
// Web based routes
g1 := e.Group("")
{
g1.Use(middleware.CSRFWithConfig(middleware.CSRFConfig{
TokenLookup: "form:_csrf",
CookiePath: "/",
CookieHTTPOnly: true,
CookieSameSite: http.SameSiteStrictMode,
}))
g1.Use(csrfInit)
g1.GET("/", create, logged)
g1.POST("/", processCreate, logged)
g1.GET("/register", register)
g1.POST("/register", processRegister)
g1.GET("/login", login)
g1.POST("/login", processLogin)
g1.GET("/logout", logout)
g1.GET("/settings", userSettings, logged)
g1.POST("/settings/email", emailProcess, logged)
g1.DELETE("/settings/account", accountDeleteProcess, logged)
g1.POST("/settings/ssh-keys", sshKeysProcess, logged)
g1.DELETE("/settings/ssh-keys/:id", sshKeysDelete, logged)
2023-03-14 15:22:52 +00:00
g2 := g1.Group("/admin")
{
g2.Use(adminPermission)
g2.GET("", adminIndex)
g2.GET("/users", adminUsers)
g2.POST("/users/:user/delete", adminUserDelete)
g2.GET("/gists", adminGists)
g2.POST("/gists/:gist/delete", adminGistDelete)
g2.POST("/sync-fs", adminSyncReposFromFS)
g2.POST("/sync-db", adminSyncReposFromDB)
2023-03-14 15:22:52 +00:00
}
g1.GET("/all", allGists)
g1.GET("/:user", allGists)
g3 := g1.Group("/:user/:gistname")
{
g3.Use(gistInit)
2023-03-17 13:56:39 +00:00
g3.GET("", gistIndex)
g3.GET("/rev/:revision", gistIndex)
2023-03-14 15:22:52 +00:00
g3.GET("/revisions", revisions)
g3.GET("/archive/:revision", downloadZip)
g3.POST("/visibility", toggleVisibility, logged, writePermission)
g3.POST("/delete", deleteGist, logged, writePermission)
g3.GET("/raw/:revision/:file", rawFile)
g3.GET("/edit", edit, logged, writePermission)
g3.POST("/edit", processCreate, logged, writePermission)
g3.POST("/like", like, logged)
g3.GET("/likes", likes)
2023-03-14 22:26:39 +00:00
g3.POST("/fork", fork, logged)
g3.GET("/forks", forks)
2023-03-14 15:22:52 +00:00
}
}
debugStr := ""
// Git HTTP routes
2023-04-06 23:52:56 +00:00
if config.C.HttpGit {
2023-03-14 15:22:52 +00:00
e.Any("/:user/:gistname/*", gitHttp, gistInit)
2023-03-15 09:37:17 +00:00
debugStr = " (with Git over HTTP)"
2023-03-14 15:22:52 +00:00
}
e.Any("/*", noRouteFound)
2023-04-06 23:52:56 +00:00
addr := config.C.HttpHost + ":" + config.C.HttpPort
2023-03-14 15:22:52 +00:00
2023-04-06 23:52:56 +00:00
if config.C.HttpTLSEnabled {
2023-03-15 09:37:17 +00:00
log.Info().Msg("Starting HTTPS server on https://" + addr + debugStr)
2023-04-06 23:52:56 +00:00
if err := e.StartTLS(addr, config.C.HttpCertFile, config.C.HttpKeyFile); err != nil {
2023-03-15 09:37:17 +00:00
log.Fatal().Err(err).Msg("Failed to start HTTPS server")
}
} else {
log.Info().Msg("Starting HTTP server on http://" + addr + debugStr)
if err := e.Start(addr); err != nil {
log.Fatal().Err(err).Msg("Failed to start HTTP server")
}
2023-03-14 15:22:52 +00:00
}
}
func dataInit(next echo.HandlerFunc) echo.HandlerFunc {
return func(ctx echo.Context) error {
ctxValue := context.WithValue(ctx.Request().Context(), "data", echo.Map{})
ctx.SetRequest(ctx.Request().WithContext(ctxValue))
setData(ctx, "loadStartTime", time.Now())
2023-03-15 09:37:17 +00:00
setData(ctx, "signupDisabled", config.C.DisableSignup)
2023-03-14 15:22:52 +00:00
return next(ctx)
}
}
2023-03-15 09:37:17 +00:00
func sessionInit(next echo.HandlerFunc) echo.HandlerFunc {
2023-03-14 15:22:52 +00:00
return func(ctx echo.Context) error {
sess := getSession(ctx)
if sess.Values["user"] != nil {
2023-03-17 13:56:39 +00:00
var err error
var user *models.User
if user, err = models.GetUserById(sess.Values["user"].(uint)); err != nil {
2023-03-14 15:22:52 +00:00
sess.Values["user"] = nil
saveSession(sess, ctx)
setData(ctx, "userLogged", nil)
return redirect(ctx, "/all")
}
if user != nil {
setData(ctx, "userLogged", user)
}
return next(ctx)
}
setData(ctx, "userLogged", nil)
return next(ctx)
}
}
func csrfInit(next echo.HandlerFunc) echo.HandlerFunc {
return func(ctx echo.Context) error {
setCsrfHtmlForm(ctx)
return next(ctx)
}
}
func writePermission(next echo.HandlerFunc) echo.HandlerFunc {
return func(ctx echo.Context) error {
gist := getData(ctx, "gist")
user := getUserLogged(ctx)
2023-03-17 13:56:39 +00:00
if !gist.(*models.Gist).CanWrite(user) {
2023-03-14 15:22:52 +00:00
return redirect(ctx, "/"+gist.(*models.Gist).User.Username+"/"+gist.(*models.Gist).Uuid)
}
return next(ctx)
}
}
func adminPermission(next echo.HandlerFunc) echo.HandlerFunc {
return func(ctx echo.Context) error {
user := getUserLogged(ctx)
if user == nil || !user.IsAdmin {
return notFound("User not found")
}
return next(ctx)
}
}
func logged(next echo.HandlerFunc) echo.HandlerFunc {
return func(ctx echo.Context) error {
user := getUserLogged(ctx)
if user != nil {
return next(ctx)
}
2023-04-12 11:30:51 +00:00
return redirect(ctx, "/login")
2023-03-14 15:22:52 +00:00
}
}
2023-03-24 13:41:08 +00:00
func cacheControl(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
c.Response().Header().Set(echo.HeaderCacheControl, "public, max-age=31536000")
return next(c)
}
}
2023-03-14 15:22:52 +00:00
func noRouteFound(echo.Context) error {
return notFound("Page not found")
}
2023-03-23 15:00:48 +00:00
// ---
type Asset struct {
File string `json:"file"`
}
var manifestEntries map[string]Asset
func parseManifestEntries() {
2023-03-24 13:41:08 +00:00
file, err := EmbedFS.Open("public/manifest.json")
2023-03-23 15:00:48 +00:00
if err != nil {
log.Fatal().Err(err).Msg("Failed to open manifest.json")
}
byteValue, err := io.ReadAll(file)
if err != nil {
log.Fatal().Err(err).Msg("Failed to read manifest.json")
}
if err = json.Unmarshal(byteValue, &manifestEntries); err != nil {
log.Fatal().Err(err).Msg("Failed to unmarshal manifest.json")
}
}