static-file-server/handle/handle.go
2022-05-24 23:33:29 +08:00

236 lines
6.6 KiB
Go

package handle
import (
"crypto/tls"
"crypto/md5"
"fmt"
"log"
"net/http"
"strings"
)
var (
// These assignments are for unit testing.
listenAndServe = http.ListenAndServe
listenAndServeTLS = defaultListenAndServeTLS
setHandler = http.HandleFunc
)
var (
// Server options to be set prior to calling the listening function.
// minTLSVersion is the minimum allowed TLS version to be used by the
// server.
minTLSVersion uint16 = tls.VersionTLS10
)
// defaultListenAndServeTLS is the default implementation of the listening
// function for serving with TLS enabled. This is, effectively, a copy from
// the standard library but with the ability to set the minimum TLS version.
func defaultListenAndServeTLS(
binding, certFile, keyFile string, handler http.Handler,
) error {
if handler == nil {
handler = http.DefaultServeMux
}
server := &http.Server{
Addr: binding,
Handler: handler,
TLSConfig: &tls.Config{
MinVersion: minTLSVersion,
},
}
return server.ListenAndServeTLS(certFile, keyFile)
}
// SetMinimumTLSVersion to be used by the server.
func SetMinimumTLSVersion(version uint16) {
if version < tls.VersionTLS10 {
version = tls.VersionTLS10
} else if version > tls.VersionTLS13 {
version = tls.VersionTLS13
}
minTLSVersion = version
}
// ListenerFunc accepts the {hostname:port} binding string required by HTTP
// listeners and the handler (router) function and returns any errors that
// occur.
type ListenerFunc func(string, http.HandlerFunc) error
// FileServerFunc is used to serve the file from the local file system to the
// requesting client.
type FileServerFunc func(http.ResponseWriter, *http.Request, string)
// WithReferrers returns a function that evaluates the HTTP 'Referer' header
// value and returns HTTP error 403 if the value is not found in the whitelist.
// If one of the whitelisted referrers are an empty string, then it is allowed
// for the 'Referer' HTTP header key to not be set.
func WithReferrers(serveFile FileServerFunc, referrers []string) FileServerFunc {
return func(w http.ResponseWriter, r *http.Request, name string) {
if !validReferrer(referrers, r.Referer()) {
http.Error(
w,
fmt.Sprintf("Invalid source '%s'", r.Referer()),
http.StatusForbidden,
)
return
}
serveFile(w, r, name)
}
}
// WithLogging returns a function that logs information about the request prior
// to serving the requested file.
func WithLogging(serveFile FileServerFunc) FileServerFunc {
return func(w http.ResponseWriter, r *http.Request, name string) {
referer := r.Referer()
if len(referer) == 0 {
log.Printf(
"REQ from '%s': %s %s %s%s -> %s\n",
r.RemoteAddr,
r.Method,
r.Proto,
r.Host,
r.URL.Path,
name,
)
} else {
log.Printf(
"REQ from '%s' (REFERER: '%s'): %s %s %s%s -> %s\n",
r.RemoteAddr,
referer,
r.Method,
r.Proto,
r.Host,
r.URL.Path,
name,
)
}
serveFile(w, r, name)
}
}
// Basic file handler servers files from the passed folder.
func Basic(serveFile FileServerFunc, folder string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
serveFile(w, r, folder+r.URL.Path)
}
}
// Prefix file handler is an alternative to Basic where a URL prefix is removed
// prior to serving a file (http://my.machine/prefix/file.txt will serve
// file.txt from the root of the folder being served (ignoring 'prefix')).
func Prefix(serveFile FileServerFunc, folder, urlPrefix string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if !strings.HasPrefix(r.URL.Path, urlPrefix) {
http.NotFound(w, r)
return
}
serveFile(w, r, folder+strings.TrimPrefix(r.URL.Path, urlPrefix))
}
}
// IgnoreIndex wraps an HTTP request. In the event of a folder root request,
// this function will automatically return 'NOT FOUND' as opposed to default
// behavior where the index file for that directory is retrieved.
func IgnoreIndex(serve http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if strings.HasSuffix(r.URL.Path, "/") {
http.NotFound(w, r)
return
}
serve(w, r)
}
}
// AddCorsWildcardHeaders wraps an HTTP request to notify client browsers that
// resources should be allowed to be retrieved by any other domain.
func AddCorsWildcardHeaders(serve http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Headers", "*")
serve(w, r)
}
}
// Access Control through url parameters. The access key is set by ACCESS_KEY.
// md5sum is computed by queried path + access key
// (e.g. "/my/file" + ACCESS_KEY)
func AddAccessKey(serve http.HandlerFunc, accessKey string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Get key or md5sum from this access.
keys, key_ok := r.URL.Query()["key"]
var code string
if !key_ok || len(keys[0]) < 1 {
// In case a code is provided
codes, code_ok := r.URL.Query()["code"]
if !code_ok || len(codes[0]) < 1 {
http.NotFound(w, r)
return
}
code = codes[0]
} else {
// In case a key is provided, convert to code.
data := []byte(r.URL.Path + keys[0])
hash := md5.Sum(data)
code = fmt.Sprintf("%x", hash)
}
code = strings.ToUpper(code)
// Compute the correct md5sum of this access.
local_data := []byte(r.URL.Path + accessKey)
hash := md5.Sum(local_data)
local_code := fmt.Sprintf("%x", hash)
local_code = strings.ToUpper(local_code)
// Compare the two.
if code != local_code {
http.NotFound(w, r)
return
}
serve(w, r)
}
}
// Listening function for serving the handler function.
func Listening() ListenerFunc {
return func(binding string, handler http.HandlerFunc) error {
setHandler("/", handler)
return listenAndServe(binding, nil)
}
}
// TLSListening function for serving the handler function with encryption.
func TLSListening(tlsCert, tlsKey string) ListenerFunc {
return func(binding string, handler http.HandlerFunc) error {
setHandler("/", handler)
return listenAndServeTLS(binding, tlsCert, tlsKey, nil)
}
}
// validReferrer returns true if the passed referrer can be resolved by the
// passed list of referrers.
func validReferrer(s []string, e string) bool {
// Whitelisted referer list is empty. All requests are allowed.
if len(s) == 0 {
return true
}
for _, a := range s {
// Handle blank HTTP Referer header, if configured
if a == "" {
if e == "" {
return true
}
// Continue loop (all strings start with "")
continue
}
// Compare header with allowed prefixes
if strings.HasPrefix(e, a) {
return true
}
}
return false
}