2019-06-30 17:07:58 -05:00
|
|
|
// 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.
|
|
|
|
|
2019-05-20 22:21:33 -05:00
|
|
|
package fileserver
|
2019-05-20 11:59:20 -05:00
|
|
|
|
|
|
|
import (
|
2019-05-20 16:46:34 -05:00
|
|
|
"bytes"
|
|
|
|
"encoding/json"
|
2021-04-30 21:17:23 -05:00
|
|
|
"fmt"
|
2019-05-20 11:59:20 -05:00
|
|
|
"net/http"
|
2019-05-20 16:46:34 -05:00
|
|
|
"os"
|
|
|
|
"path"
|
|
|
|
"strings"
|
2021-06-16 15:28:34 -05:00
|
|
|
"sync"
|
2021-04-30 21:17:23 -05:00
|
|
|
"text/template"
|
2019-05-20 16:46:34 -05:00
|
|
|
|
2019-07-02 13:37:06 -05:00
|
|
|
"github.com/caddyserver/caddy/v2"
|
|
|
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
2021-04-30 21:17:23 -05:00
|
|
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp/templates"
|
2020-11-26 11:37:42 -05:00
|
|
|
"go.uber.org/zap"
|
2019-05-20 11:59:20 -05:00
|
|
|
)
|
|
|
|
|
|
|
|
// Browse configures directory browsing.
|
|
|
|
type Browse struct {
|
2019-12-23 14:45:35 -05:00
|
|
|
// Use this template file instead of the default browse template.
|
2019-05-22 13:32:36 -05:00
|
|
|
TemplateFile string `json:"template_file,omitempty"`
|
2019-05-20 16:46:34 -05:00
|
|
|
}
|
2019-05-20 11:59:20 -05:00
|
|
|
|
2020-11-24 14:24:44 -05:00
|
|
|
func (fsrv *FileServer) serveBrowse(root, dirPath string, w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
|
2020-11-26 11:37:42 -05:00
|
|
|
fsrv.logger.Debug("browse enabled; listing directory contents",
|
|
|
|
zap.String("path", dirPath),
|
|
|
|
zap.String("root", root))
|
|
|
|
|
fileserver: Only redirect if filename not rewritten (fix #4205)
This is the more correct implementation of 23dadc0d86dd75dad7559c25f20c9641bc7bc30f (#4179)... I think. This commit effectively undoes the revert in 8848df9c5d372a559d01512b7a4ef00e38867b55, but with corrections to the logic.
We *do* need to use the original request path (the path the browser knows) for redirects, since they are external, and rewrites are only internal.
However, if the path was rewritten to a non-canonical path, we should not redirect to canonicalize that, since rewrites are intentional by the site owner. Canonicalizing the path involves modifying only the suffix (base element, or filename) of the path. Thus, if a rewrite involves only the prefix (like how handle_path strips a path prefix), then we can (hopefully!) safely redirect using the original URI since the filename was not rewritten.
So basically, if rewrites modify the filename, we should not canonicalize those requests. If rewrites only modify another part of the path (commonly a prefix), we should be OK to redirect.
2021-06-17 10:55:49 -05:00
|
|
|
// Navigation on the client-side gets messed up if the
|
|
|
|
// URL doesn't end in a trailing slash because hrefs to
|
|
|
|
// "b/c" at path "/a" end up going to "/b/c" instead
|
2019-05-21 14:03:52 -05:00
|
|
|
// of "/a/b/c" - so we have to redirect in this case
|
fileserver: Only redirect if filename not rewritten (fix #4205)
This is the more correct implementation of 23dadc0d86dd75dad7559c25f20c9641bc7bc30f (#4179)... I think. This commit effectively undoes the revert in 8848df9c5d372a559d01512b7a4ef00e38867b55, but with corrections to the logic.
We *do* need to use the original request path (the path the browser knows) for redirects, since they are external, and rewrites are only internal.
However, if the path was rewritten to a non-canonical path, we should not redirect to canonicalize that, since rewrites are intentional by the site owner. Canonicalizing the path involves modifying only the suffix (base element, or filename) of the path. Thus, if a rewrite involves only the prefix (like how handle_path strips a path prefix), then we can (hopefully!) safely redirect using the original URI since the filename was not rewritten.
So basically, if rewrites modify the filename, we should not canonicalize those requests. If rewrites only modify another part of the path (commonly a prefix), we should be OK to redirect.
2021-06-17 10:55:49 -05:00
|
|
|
// so that the path is "/a/" and the client constructs
|
|
|
|
// relative hrefs "b/c" to be "/a/b/c".
|
|
|
|
//
|
|
|
|
// Only redirect if the last element of the path (the filename) was not
|
|
|
|
// rewritten; if the admin wanted to rewrite to the canonical path, they
|
|
|
|
// would have, and we have to be very careful not to introduce unwanted
|
|
|
|
// redirects and especially redirect loops! (Redirecting using the
|
|
|
|
// original URI is necessary because that's the URI the browser knows,
|
|
|
|
// we don't want to redirect from internally-rewritten URIs.)
|
|
|
|
// See https://github.com/caddyserver/caddy/issues/4205.
|
|
|
|
origReq := r.Context().Value(caddyhttp.OriginalRequestCtxKey).(http.Request)
|
|
|
|
if path.Base(origReq.URL.Path) == path.Base(r.URL.Path) {
|
|
|
|
if !strings.HasSuffix(origReq.URL.Path, "/") {
|
|
|
|
fsrv.logger.Debug("redirecting to trailing slash to preserve hrefs", zap.String("request_path", r.URL.Path))
|
|
|
|
origReq.URL.Path += "/"
|
|
|
|
http.Redirect(w, r, origReq.URL.String(), http.StatusMovedPermanently)
|
|
|
|
return nil
|
|
|
|
}
|
2019-05-21 14:03:52 -05:00
|
|
|
}
|
|
|
|
|
2019-05-20 22:21:33 -05:00
|
|
|
dir, err := fsrv.openFile(dirPath, w)
|
2019-05-20 16:46:34 -05:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer dir.Close()
|
|
|
|
|
2019-12-29 15:12:52 -05:00
|
|
|
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
|
2019-05-20 16:46:34 -05:00
|
|
|
|
2019-05-21 14:03:52 -05:00
|
|
|
// calling path.Clean here prevents weird breadcrumbs when URL paths are sketchy like /%2e%2e%2f
|
2020-11-24 14:24:44 -05:00
|
|
|
listing, err := fsrv.loadDirectoryContents(dir, root, path.Clean(r.URL.Path), repl)
|
2019-05-20 16:46:34 -05:00
|
|
|
switch {
|
|
|
|
case os.IsPermission(err):
|
|
|
|
return caddyhttp.Error(http.StatusForbidden, err)
|
|
|
|
case os.IsNotExist(err):
|
2019-11-15 19:32:13 -05:00
|
|
|
return fsrv.notFound(w, r, next)
|
2019-05-20 16:46:34 -05:00
|
|
|
case err != nil:
|
|
|
|
return caddyhttp.Error(http.StatusInternalServerError, err)
|
|
|
|
}
|
|
|
|
|
2019-05-20 22:21:33 -05:00
|
|
|
fsrv.browseApplyQueryParams(w, r, &listing)
|
2019-05-20 16:46:34 -05:00
|
|
|
|
2021-06-16 15:28:34 -05:00
|
|
|
buf := bufPool.Get().(*bytes.Buffer)
|
|
|
|
defer bufPool.Put(buf)
|
|
|
|
|
2019-05-20 16:46:34 -05:00
|
|
|
acceptHeader := strings.ToLower(strings.Join(r.Header["Accept"], ","))
|
2021-06-16 15:28:34 -05:00
|
|
|
|
|
|
|
// write response as either JSON or HTML
|
2019-05-20 16:46:34 -05:00
|
|
|
if strings.Contains(acceptHeader, "application/json") {
|
2021-06-16 15:28:34 -05:00
|
|
|
if err := json.NewEncoder(buf).Encode(listing.Items); err != nil {
|
2019-05-20 16:46:34 -05:00
|
|
|
return caddyhttp.Error(http.StatusInternalServerError, err)
|
|
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
|
|
} else {
|
2021-04-30 21:17:23 -05:00
|
|
|
var fs http.FileSystem
|
|
|
|
if fsrv.Root != "" {
|
|
|
|
fs = http.Dir(repl.ReplaceAll(fsrv.Root, "."))
|
|
|
|
}
|
|
|
|
|
|
|
|
var tplCtx = &templateContext{
|
|
|
|
TemplateContext: templates.TemplateContext{
|
|
|
|
Root: fs,
|
|
|
|
Req: r,
|
|
|
|
RespHeader: templates.WrappedHeader{Header: w.Header()},
|
|
|
|
},
|
|
|
|
browseTemplateContext: listing,
|
|
|
|
}
|
|
|
|
|
2021-06-16 15:28:34 -05:00
|
|
|
tpl, err := fsrv.makeBrowseTemplate(tplCtx)
|
2021-04-30 21:17:23 -05:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("parsing browse template: %v", err)
|
|
|
|
}
|
2021-06-16 15:28:34 -05:00
|
|
|
if err := tpl.Execute(buf, tplCtx); err != nil {
|
2019-05-20 16:46:34 -05:00
|
|
|
return caddyhttp.Error(http.StatusInternalServerError, err)
|
|
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
|
|
}
|
2019-06-21 15:36:26 -05:00
|
|
|
|
2020-11-22 16:50:29 -05:00
|
|
|
_, _ = buf.WriteTo(w)
|
2019-05-20 11:59:20 -05:00
|
|
|
|
2019-05-20 16:46:34 -05:00
|
|
|
return nil
|
2019-05-20 11:59:20 -05:00
|
|
|
}
|
|
|
|
|
2021-04-30 21:17:23 -05:00
|
|
|
func (fsrv *FileServer) loadDirectoryContents(dir *os.File, root, urlPath string, repl *caddy.Replacer) (browseTemplateContext, error) {
|
2019-05-20 16:46:34 -05:00
|
|
|
files, err := dir.Readdir(-1)
|
|
|
|
if err != nil {
|
2021-04-30 21:17:23 -05:00
|
|
|
return browseTemplateContext{}, err
|
2019-05-20 16:46:34 -05:00
|
|
|
}
|
|
|
|
|
2020-12-30 10:03:33 -05:00
|
|
|
// user can presumably browse "up" to parent folder if path is longer than "/"
|
|
|
|
canGoUp := len(urlPath) > 1
|
2019-05-20 16:46:34 -05:00
|
|
|
|
2020-11-24 14:24:44 -05:00
|
|
|
return fsrv.directoryListing(files, canGoUp, root, urlPath, repl), nil
|
2019-05-20 16:46:34 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// browseApplyQueryParams applies query parameters to the listing.
|
|
|
|
// It mutates the listing and may set cookies.
|
2021-04-30 21:17:23 -05:00
|
|
|
func (fsrv *FileServer) browseApplyQueryParams(w http.ResponseWriter, r *http.Request, listing *browseTemplateContext) {
|
2019-05-20 16:46:34 -05:00
|
|
|
sortParam := r.URL.Query().Get("sort")
|
|
|
|
orderParam := r.URL.Query().Get("order")
|
|
|
|
limitParam := r.URL.Query().Get("limit")
|
2020-07-09 00:56:15 -05:00
|
|
|
offsetParam := r.URL.Query().Get("offset")
|
2019-05-20 16:46:34 -05:00
|
|
|
|
|
|
|
// first figure out what to sort by
|
|
|
|
switch sortParam {
|
|
|
|
case "":
|
|
|
|
sortParam = sortByNameDirFirst
|
|
|
|
if sortCookie, sortErr := r.Cookie("sort"); sortErr == nil {
|
|
|
|
sortParam = sortCookie.Value
|
|
|
|
}
|
|
|
|
case sortByName, sortByNameDirFirst, sortBySize, sortByTime:
|
|
|
|
http.SetCookie(w, &http.Cookie{Name: "sort", Value: sortParam, Secure: r.TLS != nil})
|
|
|
|
}
|
|
|
|
|
|
|
|
// then figure out the order
|
|
|
|
switch orderParam {
|
|
|
|
case "":
|
|
|
|
orderParam = "asc"
|
|
|
|
if orderCookie, orderErr := r.Cookie("order"); orderErr == nil {
|
|
|
|
orderParam = orderCookie.Value
|
|
|
|
}
|
|
|
|
case "asc", "desc":
|
|
|
|
http.SetCookie(w, &http.Cookie{Name: "order", Value: orderParam, Secure: r.TLS != nil})
|
|
|
|
}
|
|
|
|
|
|
|
|
// finally, apply the sorting and limiting
|
2020-07-09 00:56:15 -05:00
|
|
|
listing.applySortAndLimit(sortParam, orderParam, limitParam, offsetParam)
|
2019-05-20 16:46:34 -05:00
|
|
|
}
|
|
|
|
|
2021-04-30 21:17:23 -05:00
|
|
|
// makeBrowseTemplate creates the template to be used for directory listings.
|
2021-06-16 15:28:34 -05:00
|
|
|
func (fsrv *FileServer) makeBrowseTemplate(tplCtx *templateContext) (*template.Template, error) {
|
2021-04-30 21:17:23 -05:00
|
|
|
var tpl *template.Template
|
|
|
|
var err error
|
|
|
|
|
|
|
|
if fsrv.Browse.TemplateFile != "" {
|
|
|
|
tpl = tplCtx.NewTemplate(path.Base(fsrv.Browse.TemplateFile))
|
|
|
|
tpl, err = tpl.ParseFiles(fsrv.Browse.TemplateFile)
|
|
|
|
if err != nil {
|
2021-06-16 15:28:34 -05:00
|
|
|
return nil, fmt.Errorf("parsing browse template file: %v", err)
|
2021-04-30 21:17:23 -05:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
tpl = tplCtx.NewTemplate("default_listing")
|
|
|
|
tpl, err = tpl.Parse(defaultBrowseTemplate)
|
|
|
|
if err != nil {
|
2021-06-16 15:28:34 -05:00
|
|
|
return nil, fmt.Errorf("parsing default browse template: %v", err)
|
2021-04-30 21:17:23 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-06-16 15:28:34 -05:00
|
|
|
return tpl, nil
|
2019-05-20 16:46:34 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// isSymlink return true if f is a symbolic link
|
|
|
|
func isSymlink(f os.FileInfo) bool {
|
|
|
|
return f.Mode()&os.ModeSymlink != 0
|
|
|
|
}
|
|
|
|
|
2019-05-21 14:03:52 -05:00
|
|
|
// isSymlinkTargetDir returns true if f's symbolic link target
|
|
|
|
// is a directory.
|
|
|
|
func isSymlinkTargetDir(f os.FileInfo, root, urlPath string) bool {
|
|
|
|
if !isSymlink(f) {
|
|
|
|
return false
|
|
|
|
}
|
2021-06-17 10:59:08 -05:00
|
|
|
target := caddyhttp.SanitizedPathJoin(root, path.Join(urlPath, f.Name()))
|
2019-05-21 14:03:52 -05:00
|
|
|
targetInfo, err := os.Stat(target)
|
|
|
|
if err != nil {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
return targetInfo.IsDir()
|
2019-05-20 16:46:34 -05:00
|
|
|
}
|
2021-04-30 21:17:23 -05:00
|
|
|
|
|
|
|
// templateContext powers the context used when evaluating the browse template.
|
|
|
|
// It combines browse-specific features with the standard templates handler
|
|
|
|
// features.
|
|
|
|
type templateContext struct {
|
|
|
|
templates.TemplateContext
|
|
|
|
browseTemplateContext
|
|
|
|
}
|
2021-06-16 15:28:34 -05:00
|
|
|
|
|
|
|
// bufPool is used to increase the efficiency of file listings.
|
|
|
|
var bufPool = sync.Pool{
|
|
|
|
New: func() interface{} {
|
|
|
|
return new(bytes.Buffer)
|
|
|
|
},
|
|
|
|
}
|