mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-12-26 09:24:07 -05:00
e24ee48776
Fixes a wrong 302 redirect to the login page, see https://github.com/go-gitea/gitea/issues/11989. Also made it so the reserved username list is extended with those known entries so we avoid code duplication. Should be backported to 1.12.
178 lines
4.3 KiB
Go
178 lines
4.3 KiB
Go
// Copyright 2016 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 public
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"log"
|
|
"net/http"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"code.gitea.io/gitea/modules/setting"
|
|
|
|
"gitea.com/macaron/macaron"
|
|
)
|
|
|
|
// Options represents the available options to configure the macaron handler.
|
|
type Options struct {
|
|
Directory string
|
|
IndexFile string
|
|
SkipLogging bool
|
|
// if set to true, will enable caching. Expires header will also be set to
|
|
// expire after the defined time.
|
|
ExpiresAfter time.Duration
|
|
FileSystem http.FileSystem
|
|
Prefix string
|
|
}
|
|
|
|
// KnownPublicEntries list all direct children in the `public` directory
|
|
var KnownPublicEntries = []string{
|
|
"css",
|
|
"fomantic",
|
|
"img",
|
|
"js",
|
|
"serviceworker.js",
|
|
"vendor",
|
|
}
|
|
|
|
// Custom implements the macaron static handler for serving custom assets.
|
|
func Custom(opts *Options) macaron.Handler {
|
|
return opts.staticHandler(path.Join(setting.CustomPath, "public"))
|
|
}
|
|
|
|
// staticFileSystem implements http.FileSystem interface.
|
|
type staticFileSystem struct {
|
|
dir *http.Dir
|
|
}
|
|
|
|
func newStaticFileSystem(directory string) staticFileSystem {
|
|
if !filepath.IsAbs(directory) {
|
|
directory = filepath.Join(macaron.Root, directory)
|
|
}
|
|
dir := http.Dir(directory)
|
|
return staticFileSystem{&dir}
|
|
}
|
|
|
|
func (fs staticFileSystem) Open(name string) (http.File, error) {
|
|
return fs.dir.Open(name)
|
|
}
|
|
|
|
// StaticHandler sets up a new middleware for serving static files in the
|
|
func StaticHandler(dir string, opts *Options) macaron.Handler {
|
|
return opts.staticHandler(dir)
|
|
}
|
|
|
|
func (opts *Options) staticHandler(dir string) macaron.Handler {
|
|
// Defaults
|
|
if len(opts.IndexFile) == 0 {
|
|
opts.IndexFile = "index.html"
|
|
}
|
|
// Normalize the prefix if provided
|
|
if opts.Prefix != "" {
|
|
// Ensure we have a leading '/'
|
|
if opts.Prefix[0] != '/' {
|
|
opts.Prefix = "/" + opts.Prefix
|
|
}
|
|
// Remove any trailing '/'
|
|
opts.Prefix = strings.TrimRight(opts.Prefix, "/")
|
|
}
|
|
if opts.FileSystem == nil {
|
|
opts.FileSystem = newStaticFileSystem(dir)
|
|
}
|
|
|
|
return func(ctx *macaron.Context, log *log.Logger) {
|
|
opts.handle(ctx, log, opts)
|
|
}
|
|
}
|
|
|
|
func (opts *Options) handle(ctx *macaron.Context, log *log.Logger, opt *Options) bool {
|
|
if ctx.Req.Method != "GET" && ctx.Req.Method != "HEAD" {
|
|
return false
|
|
}
|
|
|
|
file := ctx.Req.URL.Path
|
|
// if we have a prefix, filter requests by stripping the prefix
|
|
if opt.Prefix != "" {
|
|
if !strings.HasPrefix(file, opt.Prefix) {
|
|
return false
|
|
}
|
|
file = file[len(opt.Prefix):]
|
|
if file != "" && file[0] != '/' {
|
|
return false
|
|
}
|
|
}
|
|
|
|
f, err := opt.FileSystem.Open(file)
|
|
if err != nil {
|
|
// 404 requests to any known entries in `public`
|
|
if path.Base(opts.Directory) == "public" {
|
|
parts := strings.Split(file, "/")
|
|
if len(parts) < 2 {
|
|
return false
|
|
}
|
|
for _, entry := range KnownPublicEntries {
|
|
if entry == parts[1] {
|
|
ctx.Resp.WriteHeader(404)
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
defer f.Close()
|
|
|
|
fi, err := f.Stat()
|
|
if err != nil {
|
|
log.Printf("[Static] %q exists, but fails to open: %v", file, err)
|
|
return true
|
|
}
|
|
|
|
// Try to serve index file
|
|
if fi.IsDir() {
|
|
// Redirect if missing trailing slash.
|
|
if !strings.HasSuffix(ctx.Req.URL.Path, "/") {
|
|
http.Redirect(ctx.Resp, ctx.Req.Request, path.Clean(ctx.Req.URL.Path+"/"), http.StatusFound)
|
|
return true
|
|
}
|
|
|
|
f, err = opt.FileSystem.Open(file)
|
|
if err != nil {
|
|
return false // Discard error.
|
|
}
|
|
defer f.Close()
|
|
|
|
fi, err = f.Stat()
|
|
if err != nil || fi.IsDir() {
|
|
return true
|
|
}
|
|
}
|
|
|
|
if !opt.SkipLogging {
|
|
log.Println("[Static] Serving " + file)
|
|
}
|
|
|
|
// Add an Expires header to the static content
|
|
if opt.ExpiresAfter > 0 {
|
|
ctx.Resp.Header().Set("Expires", time.Now().Add(opt.ExpiresAfter).UTC().Format(http.TimeFormat))
|
|
tag := GenerateETag(string(fi.Size()), fi.Name(), fi.ModTime().UTC().Format(http.TimeFormat))
|
|
ctx.Resp.Header().Set("ETag", tag)
|
|
if ctx.Req.Header.Get("If-None-Match") == tag {
|
|
ctx.Resp.WriteHeader(304)
|
|
return false
|
|
}
|
|
}
|
|
|
|
http.ServeContent(ctx.Resp, ctx.Req.Request, file, fi.ModTime(), f)
|
|
return true
|
|
}
|
|
|
|
// GenerateETag generates an ETag based on size, filename and file modification time
|
|
func GenerateETag(fileSize, fileName, modTime string) string {
|
|
etag := fileSize + fileName + modTime
|
|
return base64.StdEncoding.EncodeToString([]byte(etag))
|
|
}
|