2016-11-29 19:26:36 +03:00
// 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
2017-01-29 01:14:56 +03:00
import (
2018-02-04 01:37:05 +03:00
"encoding/base64"
2020-08-13 17:11:24 +03:00
"fmt"
2018-02-04 01:37:05 +03:00
"log"
"net/http"
2017-01-29 01:14:56 +03:00
"path"
2018-02-04 01:37:05 +03:00
"path/filepath"
"strings"
"time"
2017-01-29 01:14:56 +03:00
"code.gitea.io/gitea/modules/setting"
)
2016-11-29 19:26:36 +03:00
// Options represents the available options to configure the macaron handler.
type Options struct {
Directory string
2018-02-04 01:37:05 +03:00
IndexFile string
2016-11-29 19:26:36 +03:00
SkipLogging bool
2018-02-04 01:37:05 +03:00
// 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
2016-11-29 19:26:36 +03:00
}
2017-01-29 01:14:56 +03:00
2020-06-20 16:20:25 +03:00
// KnownPublicEntries list all direct children in the `public` directory
var KnownPublicEntries = [ ] string {
2020-04-19 00:01:06 +03:00
"css" ,
"img" ,
"js" ,
2020-06-20 16:20:25 +03:00
"serviceworker.js" ,
2020-04-19 00:01:06 +03:00
"vendor" ,
}
2017-01-29 01:14:56 +03:00
// Custom implements the macaron static handler for serving custom assets.
2020-11-13 15:51:07 +03:00
func Custom ( opts * Options ) func ( next http . Handler ) http . Handler {
2018-02-04 01:37:05 +03:00
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 ) {
2020-11-13 15:51:07 +03:00
directory = filepath . Join ( setting . AppWorkPath , directory )
2018-02-04 01:37:05 +03:00
}
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
2020-11-13 15:51:07 +03:00
func StaticHandler ( dir string , opts * Options ) func ( next http . Handler ) http . Handler {
2018-02-04 01:37:05 +03:00
return opts . staticHandler ( dir )
}
2020-11-13 15:51:07 +03:00
func ( opts * Options ) staticHandler ( dir string ) func ( next http . Handler ) http . Handler {
return func ( next http . Handler ) http . 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 )
2018-02-04 01:37:05 +03:00
}
2020-11-13 15:51:07 +03:00
return http . HandlerFunc ( func ( w http . ResponseWriter , req * http . Request ) {
if ! opts . handle ( w , req , opts ) {
next . ServeHTTP ( w , req )
}
} )
2018-02-04 01:37:05 +03:00
}
}
2020-11-13 15:51:07 +03:00
func ( opts * Options ) handle ( w http . ResponseWriter , req * http . Request , opt * Options ) bool {
if req . Method != "GET" && req . Method != "HEAD" {
2018-02-04 01:37:05 +03:00
return false
}
2020-11-13 15:51:07 +03:00
file := req . URL . Path
2018-02-04 01:37:05 +03:00
// 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 {
2020-04-19 00:01:06 +03:00
// 404 requests to any known entries in `public`
if path . Base ( opts . Directory ) == "public" {
parts := strings . Split ( file , "/" )
if len ( parts ) < 2 {
return false
}
2020-06-20 16:20:25 +03:00
for _ , entry := range KnownPublicEntries {
2020-04-19 00:01:06 +03:00
if entry == parts [ 1 ] {
2020-11-13 15:51:07 +03:00
w . WriteHeader ( 404 )
2020-04-19 00:01:06 +03:00
return true
}
}
}
2018-02-04 01:37:05 +03:00
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.
2020-11-13 15:51:07 +03:00
if ! strings . HasSuffix ( req . URL . Path , "/" ) {
http . Redirect ( w , req , path . Clean ( req . URL . Path + "/" ) , http . StatusFound )
2018-02-04 01:37:05 +03:00
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 ( ) {
2020-11-13 15:51:07 +03:00
return false
2018-02-04 01:37:05 +03:00
}
}
if ! opt . SkipLogging {
log . Println ( "[Static] Serving " + file )
}
// Add an Expires header to the static content
if opt . ExpiresAfter > 0 {
2020-11-13 15:51:07 +03:00
w . Header ( ) . Set ( "Expires" , time . Now ( ) . Add ( opt . ExpiresAfter ) . UTC ( ) . Format ( http . TimeFormat ) )
2020-09-04 16:15:54 +03:00
tag := GenerateETag ( fmt . Sprint ( fi . Size ( ) ) , fi . Name ( ) , fi . ModTime ( ) . UTC ( ) . Format ( http . TimeFormat ) )
2020-11-13 15:51:07 +03:00
w . Header ( ) . Set ( "ETag" , tag )
if req . Header . Get ( "If-None-Match" ) == tag {
w . WriteHeader ( 304 )
return true
2018-02-04 01:37:05 +03:00
}
}
2020-11-13 15:51:07 +03:00
http . ServeContent ( w , req , file , fi . ModTime ( ) , f )
2018-02-04 01:37:05 +03:00
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 ) )
2017-01-29 01:14:56 +03:00
}