forked from NYANDEV/forgejo
8c996c6505
When running gitea for the first time, the stylesheets for the installation page are broken since the middleware that statically serves stylesheets does not get executed by chi. This is because if no handlers are registered in chi, it will drop all middleware. This commit introduces a "dummy" handler to deal with that quirk. Closes #13725 Thanks: Lunny Xiao <xiaolunwen@gmail.com> for finding the quirk Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
295 lines
8.1 KiB
Go
295 lines
8.1 KiB
Go
// Copyright 2020 The Gitea Authors. All rights reserved.
|
|
// Use of this source code is governed by a MIT-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package routes
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path"
|
|
"strings"
|
|
"text/template"
|
|
"time"
|
|
|
|
"code.gitea.io/gitea/modules/httpcache"
|
|
"code.gitea.io/gitea/modules/log"
|
|
"code.gitea.io/gitea/modules/metrics"
|
|
"code.gitea.io/gitea/modules/public"
|
|
"code.gitea.io/gitea/modules/setting"
|
|
"code.gitea.io/gitea/modules/storage"
|
|
"code.gitea.io/gitea/routers"
|
|
|
|
"github.com/go-chi/chi"
|
|
"github.com/go-chi/chi/middleware"
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
)
|
|
|
|
type routerLoggerOptions struct {
|
|
req *http.Request
|
|
Identity *string
|
|
Start *time.Time
|
|
ResponseWriter http.ResponseWriter
|
|
}
|
|
|
|
// SignedUserName returns signed user's name via context
|
|
// FIXME currently no any data stored on gin.Context but macaron.Context, so this will
|
|
// return "" before we remove macaron totally
|
|
func SignedUserName(req *http.Request) string {
|
|
if v, ok := req.Context().Value("SignedUserName").(string); ok {
|
|
return v
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func setupAccessLogger(c chi.Router) {
|
|
logger := log.GetLogger("access")
|
|
|
|
logTemplate, _ := template.New("log").Parse(setting.AccessLogTemplate)
|
|
c.Use(func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
|
start := time.Now()
|
|
next.ServeHTTP(w, req)
|
|
identity := "-"
|
|
if val := SignedUserName(req); val != "" {
|
|
identity = val
|
|
}
|
|
rw := w
|
|
|
|
buf := bytes.NewBuffer([]byte{})
|
|
err := logTemplate.Execute(buf, routerLoggerOptions{
|
|
req: req,
|
|
Identity: &identity,
|
|
Start: &start,
|
|
ResponseWriter: rw,
|
|
})
|
|
if err != nil {
|
|
log.Error("Could not set up macaron access logger: %v", err.Error())
|
|
}
|
|
|
|
err = logger.SendLog(log.INFO, "", "", 0, buf.String(), "")
|
|
if err != nil {
|
|
log.Error("Could not set up macaron access logger: %v", err.Error())
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
// LoggerHandler is a handler that will log the routing to the default gitea log
|
|
func LoggerHandler(level log.Level) func(next http.Handler) http.Handler {
|
|
return func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
|
start := time.Now()
|
|
|
|
_ = log.GetLogger("router").Log(0, level, "Started %s %s for %s", log.ColoredMethod(req.Method), req.RequestURI, req.RemoteAddr)
|
|
|
|
next.ServeHTTP(w, req)
|
|
|
|
ww := middleware.NewWrapResponseWriter(w, req.ProtoMajor)
|
|
|
|
status := ww.Status()
|
|
_ = log.GetLogger("router").Log(0, level, "Completed %s %s %v %s in %v", log.ColoredMethod(req.Method), req.RequestURI, log.ColoredStatus(status), log.ColoredStatus(status, http.StatusText(status)), log.ColoredTime(time.Since(start)))
|
|
})
|
|
}
|
|
}
|
|
|
|
// Recovery returns a middleware that recovers from any panics and writes a 500 and a log if so.
|
|
// Although similar to macaron.Recovery() the main difference is that this error will be created
|
|
// with the gitea 500 page.
|
|
func Recovery() func(next http.Handler) http.Handler {
|
|
return func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
|
defer func() {
|
|
if err := recover(); err != nil {
|
|
combinedErr := fmt.Sprintf("PANIC: %v\n%s", err, string(log.Stack(2)))
|
|
http.Error(w, combinedErr, 500)
|
|
}
|
|
}()
|
|
|
|
next.ServeHTTP(w, req)
|
|
})
|
|
}
|
|
}
|
|
|
|
func storageHandler(storageSetting setting.Storage, prefix string, objStore storage.ObjectStorage) func(next http.Handler) http.Handler {
|
|
return func(next http.Handler) http.Handler {
|
|
if storageSetting.ServeDirect {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
|
if req.Method != "GET" && req.Method != "HEAD" {
|
|
next.ServeHTTP(w, req)
|
|
return
|
|
}
|
|
|
|
if !strings.HasPrefix(req.RequestURI, "/"+prefix) {
|
|
next.ServeHTTP(w, req)
|
|
return
|
|
}
|
|
|
|
rPath := strings.TrimPrefix(req.RequestURI, "/"+prefix)
|
|
u, err := objStore.URL(rPath, path.Base(rPath))
|
|
if err != nil {
|
|
if os.IsNotExist(err) || errors.Is(err, os.ErrNotExist) {
|
|
log.Warn("Unable to find %s %s", prefix, rPath)
|
|
http.Error(w, "file not found", 404)
|
|
return
|
|
}
|
|
log.Error("Error whilst getting URL for %s %s. Error: %v", prefix, rPath, err)
|
|
http.Error(w, fmt.Sprintf("Error whilst getting URL for %s %s", prefix, rPath), 500)
|
|
return
|
|
}
|
|
http.Redirect(
|
|
w,
|
|
req,
|
|
u.String(),
|
|
301,
|
|
)
|
|
})
|
|
}
|
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
|
if req.Method != "GET" && req.Method != "HEAD" {
|
|
next.ServeHTTP(w, req)
|
|
return
|
|
}
|
|
|
|
if !strings.HasPrefix(req.RequestURI, "/"+prefix) {
|
|
next.ServeHTTP(w, req)
|
|
return
|
|
}
|
|
|
|
rPath := strings.TrimPrefix(req.RequestURI, "/"+prefix)
|
|
rPath = strings.TrimPrefix(rPath, "/")
|
|
|
|
fi, err := objStore.Stat(rPath)
|
|
if err == nil && httpcache.HandleTimeCache(req, w, fi) {
|
|
return
|
|
}
|
|
|
|
//If we have matched and access to release or issue
|
|
fr, err := objStore.Open(rPath)
|
|
if err != nil {
|
|
if os.IsNotExist(err) || errors.Is(err, os.ErrNotExist) {
|
|
log.Warn("Unable to find %s %s", prefix, rPath)
|
|
http.Error(w, "file not found", 404)
|
|
return
|
|
}
|
|
log.Error("Error whilst opening %s %s. Error: %v", prefix, rPath, err)
|
|
http.Error(w, fmt.Sprintf("Error whilst opening %s %s", prefix, rPath), 500)
|
|
return
|
|
}
|
|
defer fr.Close()
|
|
|
|
_, err = io.Copy(w, fr)
|
|
if err != nil {
|
|
log.Error("Error whilst rendering %s %s. Error: %v", prefix, rPath, err)
|
|
http.Error(w, fmt.Sprintf("Error whilst rendering %s %s", prefix, rPath), 500)
|
|
return
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// NewChi creates a chi Router
|
|
func NewChi() chi.Router {
|
|
c := chi.NewRouter()
|
|
c.Use(middleware.RealIP)
|
|
if !setting.DisableRouterLog && setting.RouterLogLevel != log.NONE {
|
|
if log.GetLogger("router").GetLevel() <= setting.RouterLogLevel {
|
|
c.Use(LoggerHandler(setting.RouterLogLevel))
|
|
}
|
|
}
|
|
c.Use(Recovery())
|
|
if setting.EnableAccessLog {
|
|
setupAccessLogger(c)
|
|
}
|
|
|
|
c.Use(public.Custom(
|
|
&public.Options{
|
|
SkipLogging: setting.DisableRouterLog,
|
|
},
|
|
))
|
|
c.Use(public.Static(
|
|
&public.Options{
|
|
Directory: path.Join(setting.StaticRootPath, "public"),
|
|
SkipLogging: setting.DisableRouterLog,
|
|
},
|
|
))
|
|
|
|
c.Use(storageHandler(setting.Avatar.Storage, "avatars", storage.Avatars))
|
|
c.Use(storageHandler(setting.RepoAvatar.Storage, "repo-avatars", storage.RepoAvatars))
|
|
|
|
return c
|
|
}
|
|
|
|
// RegisterInstallRoute registers the install routes
|
|
func RegisterInstallRoute(c chi.Router) {
|
|
m := NewMacaron()
|
|
RegisterMacaronInstallRoute(m)
|
|
|
|
// We need at least one handler in chi so that it does not drop
|
|
// our middleware: https://github.com/go-gitea/gitea/issues/13725#issuecomment-735244395
|
|
c.Get("/", func(w http.ResponseWriter, req *http.Request) {
|
|
m.ServeHTTP(w, req)
|
|
})
|
|
|
|
c.NotFound(func(w http.ResponseWriter, req *http.Request) {
|
|
m.ServeHTTP(w, req)
|
|
})
|
|
|
|
c.MethodNotAllowed(func(w http.ResponseWriter, req *http.Request) {
|
|
m.ServeHTTP(w, req)
|
|
})
|
|
}
|
|
|
|
// NormalRoutes represents non install routes
|
|
func NormalRoutes() http.Handler {
|
|
r := chi.NewRouter()
|
|
|
|
// for health check
|
|
r.Head("/", func(w http.ResponseWriter, req *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
})
|
|
|
|
if setting.HasRobotsTxt {
|
|
r.Get("/robots.txt", func(w http.ResponseWriter, req *http.Request) {
|
|
filePath := path.Join(setting.CustomPath, "robots.txt")
|
|
fi, err := os.Stat(filePath)
|
|
if err == nil && httpcache.HandleTimeCache(req, w, fi) {
|
|
return
|
|
}
|
|
http.ServeFile(w, req, filePath)
|
|
})
|
|
}
|
|
|
|
r.Get("/apple-touch-icon.png", func(w http.ResponseWriter, req *http.Request) {
|
|
http.Redirect(w, req, path.Join(setting.StaticURLPrefix, "img/apple-touch-icon.png"), 301)
|
|
})
|
|
|
|
// prometheus metrics endpoint
|
|
if setting.Metrics.Enabled {
|
|
c := metrics.NewCollector()
|
|
prometheus.MustRegister(c)
|
|
|
|
r.Get("/metrics", routers.Metrics)
|
|
}
|
|
|
|
return r
|
|
}
|
|
|
|
// DelegateToMacaron delegates other routes to macaron
|
|
func DelegateToMacaron(r chi.Router) {
|
|
m := NewMacaron()
|
|
RegisterMacaronRoutes(m)
|
|
|
|
r.NotFound(func(w http.ResponseWriter, req *http.Request) {
|
|
m.ServeHTTP(w, req)
|
|
})
|
|
|
|
r.MethodNotAllowed(func(w http.ResponseWriter, req *http.Request) {
|
|
m.ServeHTTP(w, req)
|
|
})
|
|
}
|