mirror of
				https://github.com/caddyserver/caddy.git
				synced 2025-10-25 02:33:21 +00:00 
			
		
		
		
	 14f9662f9c
			
		
	
	
		14f9662f9c
		
			
		
	
	
	
	
		
			
			- Rename http.var.* -> http.vars.* to be more consistent
- Prefixing a path matcher with * now invokes simple suffix matching
- Handlers and matchers that need a root path default to {http.vars.root}
- Clean replacer output on the file matcher's file selection suffix
		
	
			
		
			
				
	
	
		
			326 lines
		
	
	
	
		
			9.7 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			326 lines
		
	
	
	
		
			9.7 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright 2015 Matthew Holt and The Caddy Authors
 | |
| //
 | |
| // Licensed under the Apache License, Version 2.0 (the "License");
 | |
| // you may not use this file except in compliance with the License.
 | |
| // You may obtain a copy of the License at
 | |
| //
 | |
| //     http://www.apache.org/licenses/LICENSE-2.0
 | |
| //
 | |
| // Unless required by applicable law or agreed to in writing, software
 | |
| // distributed under the License is distributed on an "AS IS" BASIS,
 | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | |
| // See the License for the specific language governing permissions and
 | |
| // limitations under the License.
 | |
| 
 | |
| package fileserver
 | |
| 
 | |
| import (
 | |
| 	"bytes"
 | |
| 	"fmt"
 | |
| 	"html/template"
 | |
| 	weakrand "math/rand"
 | |
| 	"mime"
 | |
| 	"net/http"
 | |
| 	"os"
 | |
| 	"path"
 | |
| 	"path/filepath"
 | |
| 	"strconv"
 | |
| 	"strings"
 | |
| 	"sync"
 | |
| 	"time"
 | |
| 
 | |
| 	"github.com/caddyserver/caddy/v2"
 | |
| 	"github.com/caddyserver/caddy/v2/modules/caddyhttp"
 | |
| )
 | |
| 
 | |
| func init() {
 | |
| 	weakrand.Seed(time.Now().UnixNano())
 | |
| 
 | |
| 	caddy.RegisterModule(FileServer{})
 | |
| }
 | |
| 
 | |
| // FileServer implements a static file server responder for Caddy.
 | |
| type FileServer struct {
 | |
| 	Root       string   `json:"root,omitempty"` // default is current directory
 | |
| 	Hide       []string `json:"hide,omitempty"`
 | |
| 	IndexNames []string `json:"index_names,omitempty"`
 | |
| 	Browse     *Browse  `json:"browse,omitempty"`
 | |
| }
 | |
| 
 | |
| // CaddyModule returns the Caddy module information.
 | |
| func (FileServer) CaddyModule() caddy.ModuleInfo {
 | |
| 	return caddy.ModuleInfo{
 | |
| 		Name: "http.handlers.file_server",
 | |
| 		New:  func() caddy.Module { return new(FileServer) },
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // Provision sets up the static files responder.
 | |
| func (fsrv *FileServer) Provision(ctx caddy.Context) error {
 | |
| 	if fsrv.Root == "" {
 | |
| 		fsrv.Root = "{http.vars.root}"
 | |
| 	}
 | |
| 
 | |
| 	if fsrv.IndexNames == nil {
 | |
| 		fsrv.IndexNames = defaultIndexNames
 | |
| 	}
 | |
| 
 | |
| 	if fsrv.Browse != nil {
 | |
| 		var tpl *template.Template
 | |
| 		var err error
 | |
| 		if fsrv.Browse.TemplateFile != "" {
 | |
| 			tpl, err = template.ParseFiles(fsrv.Browse.TemplateFile)
 | |
| 			if err != nil {
 | |
| 				return fmt.Errorf("parsing browse template file: %v", err)
 | |
| 			}
 | |
| 		} else {
 | |
| 			tpl, err = template.New("default_listing").Parse(defaultBrowseTemplate)
 | |
| 			if err != nil {
 | |
| 				return fmt.Errorf("parsing default browse template: %v", err)
 | |
| 			}
 | |
| 		}
 | |
| 		fsrv.Browse.template = tpl
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, _ caddyhttp.Handler) error {
 | |
| 	repl := r.Context().Value(caddy.ReplacerCtxKey).(caddy.Replacer)
 | |
| 
 | |
| 	filesToHide := fsrv.transformHidePaths(repl)
 | |
| 
 | |
| 	root := repl.ReplaceAll(fsrv.Root, ".")
 | |
| 	suffix := repl.ReplaceAll(r.URL.Path, "")
 | |
| 	filename := sanitizedPathJoin(root, suffix)
 | |
| 
 | |
| 	// get information about the file
 | |
| 	info, err := os.Stat(filename)
 | |
| 	if err != nil {
 | |
| 		err = mapDirOpenError(err, filename)
 | |
| 		if os.IsNotExist(err) {
 | |
| 			return caddyhttp.Error(http.StatusNotFound, err)
 | |
| 		} else if os.IsPermission(err) {
 | |
| 			return caddyhttp.Error(http.StatusForbidden, err)
 | |
| 		}
 | |
| 		// TODO: treat this as resource exhaustion like with os.Open? Or unnecessary here?
 | |
| 		return caddyhttp.Error(http.StatusInternalServerError, err)
 | |
| 	}
 | |
| 
 | |
| 	// if the request mapped to a directory, see if
 | |
| 	// there is an index file we can serve
 | |
| 	if info.IsDir() && len(fsrv.IndexNames) > 0 {
 | |
| 		for _, indexPage := range fsrv.IndexNames {
 | |
| 			indexPath := sanitizedPathJoin(filename, indexPage)
 | |
| 			if fileHidden(indexPath, filesToHide) {
 | |
| 				// pretend this file doesn't exist
 | |
| 				continue
 | |
| 			}
 | |
| 
 | |
| 			indexInfo, err := os.Stat(indexPath)
 | |
| 			if err != nil {
 | |
| 				continue
 | |
| 			}
 | |
| 
 | |
| 			// we found an index file that might work,
 | |
| 			// so rewrite the request path
 | |
| 			r.URL.Path = path.Join(r.URL.Path, indexPage)
 | |
| 
 | |
| 			info = indexInfo
 | |
| 			filename = indexPath
 | |
| 			break
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// if still referencing a directory, delegate
 | |
| 	// to browse or return an error
 | |
| 	if info.IsDir() {
 | |
| 		if fsrv.Browse != nil && !fileHidden(filename, filesToHide) {
 | |
| 			return fsrv.serveBrowse(filename, w, r)
 | |
| 		}
 | |
| 		return caddyhttp.Error(http.StatusNotFound, nil)
 | |
| 	}
 | |
| 
 | |
| 	// TODO: content negotiation (brotli sidecar files, etc...)
 | |
| 
 | |
| 	// one last check to ensure the file isn't hidden (we might
 | |
| 	// have changed the filename from when we last checked)
 | |
| 	if fileHidden(filename, filesToHide) {
 | |
| 		return caddyhttp.Error(http.StatusNotFound, nil)
 | |
| 	}
 | |
| 
 | |
| 	// open the file
 | |
| 	file, err := fsrv.openFile(filename, w)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	defer file.Close()
 | |
| 
 | |
| 	// set the ETag - note that a conditional If-None-Match request is handled
 | |
| 	// by http.ServeContent below, which checks against this ETag value
 | |
| 	w.Header().Set("ETag", calculateEtag(info))
 | |
| 
 | |
| 	if w.Header().Get("Content-Type") == "" {
 | |
| 		mtyp := mime.TypeByExtension(filepath.Ext(filename))
 | |
| 		if mtyp == "" {
 | |
| 			// do not allow Go to sniff the content-type; see
 | |
| 			// https://www.youtube.com/watch?v=8t8JYpt0egE
 | |
| 			// TODO: Consider writing a default mime type of application/octet-stream - this is secure but violates spec
 | |
| 			w.Header()["Content-Type"] = nil
 | |
| 		} else {
 | |
| 			w.Header().Set("Content-Type", mtyp)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// let the standard library do what it does best; note, however,
 | |
| 	// that errors generated by ServeContent are written immediately
 | |
| 	// to the response, so we cannot handle them (but errors there
 | |
| 	// are rare)
 | |
| 	http.ServeContent(w, r, info.Name(), info.ModTime(), file)
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // openFile opens the file at the given filename. If there was an error,
 | |
| // the response is configured to inform the client how to best handle it
 | |
| // and a well-described handler error is returned (do not wrap the
 | |
| // returned error value).
 | |
| func (fsrv *FileServer) openFile(filename string, w http.ResponseWriter) (*os.File, error) {
 | |
| 	file, err := os.Open(filename)
 | |
| 	if err != nil {
 | |
| 		err = mapDirOpenError(err, filename)
 | |
| 		if os.IsNotExist(err) {
 | |
| 			return nil, caddyhttp.Error(http.StatusNotFound, err)
 | |
| 		} else if os.IsPermission(err) {
 | |
| 			return nil, caddyhttp.Error(http.StatusForbidden, err)
 | |
| 		}
 | |
| 		// maybe the server is under load and ran out of file descriptors?
 | |
| 		// have client wait arbitrary seconds to help prevent a stampede
 | |
| 		backoff := weakrand.Intn(maxBackoff-minBackoff) + minBackoff
 | |
| 		w.Header().Set("Retry-After", strconv.Itoa(backoff))
 | |
| 		return nil, caddyhttp.Error(http.StatusServiceUnavailable, err)
 | |
| 	}
 | |
| 	return file, nil
 | |
| }
 | |
| 
 | |
| // mapDirOpenError maps the provided non-nil error from opening name
 | |
| // to a possibly better non-nil error. In particular, it turns OS-specific errors
 | |
| // about opening files in non-directories into os.ErrNotExist. See golang/go#18984.
 | |
| // Adapted from the Go standard library; originally written by Nathaniel Caza.
 | |
| // https://go-review.googlesource.com/c/go/+/36635/
 | |
| // https://go-review.googlesource.com/c/go/+/36804/
 | |
| func mapDirOpenError(originalErr error, name string) error {
 | |
| 	if os.IsNotExist(originalErr) || os.IsPermission(originalErr) {
 | |
| 		return originalErr
 | |
| 	}
 | |
| 
 | |
| 	parts := strings.Split(name, string(filepath.Separator))
 | |
| 	for i := range parts {
 | |
| 		if parts[i] == "" {
 | |
| 			continue
 | |
| 		}
 | |
| 		fi, err := os.Stat(strings.Join(parts[:i+1], string(filepath.Separator)))
 | |
| 		if err != nil {
 | |
| 			return originalErr
 | |
| 		}
 | |
| 		if !fi.IsDir() {
 | |
| 			return os.ErrNotExist
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return originalErr
 | |
| }
 | |
| 
 | |
| // transformHidePaths performs replacements for all the elements of
 | |
| // fsrv.Hide and returns a new list of the transformed values.
 | |
| func (fsrv *FileServer) transformHidePaths(repl caddy.Replacer) []string {
 | |
| 	hide := make([]string, len(fsrv.Hide))
 | |
| 	for i := range fsrv.Hide {
 | |
| 		hide[i] = repl.ReplaceAll(fsrv.Hide[i], "")
 | |
| 	}
 | |
| 	return hide
 | |
| }
 | |
| 
 | |
| // sanitizedPathJoin performs filepath.Join(root, reqPath) that
 | |
| // is safe against directory traversal attacks. It uses logic
 | |
| // similar to that in the Go standard library, specifically
 | |
| // in the implementation of http.Dir. The root is assumed to
 | |
| // be a trusted path, but reqPath is not.
 | |
| func sanitizedPathJoin(root, reqPath string) string {
 | |
| 	// TODO: Caddy 1 uses this:
 | |
| 	// prevent absolute path access on Windows, e.g. http://localhost:5000/C:\Windows\notepad.exe
 | |
| 	// if runtime.GOOS == "windows" && len(reqPath) > 0 && filepath.IsAbs(reqPath[1:]) {
 | |
| 	// TODO.
 | |
| 	// }
 | |
| 
 | |
| 	// TODO: whereas std lib's http.Dir.Open() uses this:
 | |
| 	// if filepath.Separator != '/' && strings.ContainsRune(name, filepath.Separator) {
 | |
| 	// 	return nil, errors.New("http: invalid character in file path")
 | |
| 	// }
 | |
| 
 | |
| 	// TODO: see https://play.golang.org/p/oh77BiVQFti for another thing to consider
 | |
| 
 | |
| 	if root == "" {
 | |
| 		root = "."
 | |
| 	}
 | |
| 	return filepath.Join(root, filepath.FromSlash(path.Clean("/"+reqPath)))
 | |
| }
 | |
| 
 | |
| // fileHidden returns true if filename is hidden
 | |
| // according to the hide list.
 | |
| func fileHidden(filename string, hide []string) bool {
 | |
| 	nameOnly := filepath.Base(filename)
 | |
| 	sep := string(filepath.Separator)
 | |
| 
 | |
| 	for _, h := range hide {
 | |
| 		// assuming h is a glob/shell-like pattern,
 | |
| 		// use it to compare the whole file path;
 | |
| 		// but if there is no separator in h, then
 | |
| 		// just compare against the file's name
 | |
| 		compare := filename
 | |
| 		if !strings.Contains(h, sep) {
 | |
| 			compare = nameOnly
 | |
| 		}
 | |
| 
 | |
| 		hidden, err := filepath.Match(h, compare)
 | |
| 		if err != nil {
 | |
| 			// malformed pattern; fallback by checking prefix
 | |
| 			if strings.HasPrefix(filename, h) {
 | |
| 				return true
 | |
| 			}
 | |
| 		}
 | |
| 		if hidden {
 | |
| 			// file name or path matches hide pattern
 | |
| 			return true
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return false
 | |
| }
 | |
| 
 | |
| // calculateEtag produces a strong etag by default, although, for
 | |
| // efficiency reasons, it does not actually consume the contents
 | |
| // of the file to make a hash of all the bytes. ¯\_(ツ)_/¯
 | |
| // Prefix the etag with "W/" to convert it into a weak etag.
 | |
| // See: https://tools.ietf.org/html/rfc7232#section-2.3
 | |
| func calculateEtag(d os.FileInfo) string {
 | |
| 	t := strconv.FormatInt(d.ModTime().Unix(), 36)
 | |
| 	s := strconv.FormatInt(d.Size(), 36)
 | |
| 	return `"` + t + s + `"`
 | |
| }
 | |
| 
 | |
| var defaultIndexNames = []string{"index.html", "index.txt"}
 | |
| 
 | |
| var bufPool = sync.Pool{
 | |
| 	New: func() interface{} {
 | |
| 		return new(bytes.Buffer)
 | |
| 	},
 | |
| }
 | |
| 
 | |
| const minBackoff, maxBackoff = 2, 5
 | |
| 
 | |
| // Interface guards
 | |
| var (
 | |
| 	_ caddy.Provisioner           = (*FileServer)(nil)
 | |
| 	_ caddyhttp.MiddlewareHandler = (*FileServer)(nil)
 | |
| )
 |