2019-07-01 06:07:58 +08: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-21 11:21:33 +08:00
|
|
|
package fileserver
|
2019-05-21 00:59:20 +08:00
|
|
|
|
|
|
|
import (
|
2019-05-21 05:46:34 +08:00
|
|
|
"bytes"
|
2022-10-09 02:56:35 +08:00
|
|
|
"context"
|
2021-11-16 02:53:54 +08:00
|
|
|
_ "embed"
|
2019-05-21 05:46:34 +08:00
|
|
|
"encoding/json"
|
2024-01-02 13:48:55 +08:00
|
|
|
"errors"
|
2021-05-01 10:17:23 +08:00
|
|
|
"fmt"
|
2022-09-08 11:14:11 +08:00
|
|
|
"io"
|
2022-07-31 03:07:44 +08:00
|
|
|
"io/fs"
|
2019-05-21 00:59:20 +08:00
|
|
|
"net/http"
|
2019-05-21 05:46:34 +08:00
|
|
|
"os"
|
|
|
|
"path"
|
|
|
|
"strings"
|
2021-06-17 04:28:34 +08:00
|
|
|
"sync"
|
2021-05-01 10:17:23 +08:00
|
|
|
"text/template"
|
2019-05-21 05:46:34 +08:00
|
|
|
|
2023-08-14 23:41:15 +08:00
|
|
|
"go.uber.org/zap"
|
|
|
|
|
2019-07-03 02:37:06 +08:00
|
|
|
"github.com/caddyserver/caddy/v2"
|
|
|
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
2021-05-01 10:17:23 +08:00
|
|
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp/templates"
|
2019-05-21 00:59:20 +08:00
|
|
|
)
|
|
|
|
|
2023-08-29 23:34:20 +08:00
|
|
|
// BrowseTemplate is the default template document to use for
|
|
|
|
// file listings. By default, its default value is an embedded
|
|
|
|
// document. You can override this value at program start, or
|
|
|
|
// if you are running Caddy via config, you can specify a
|
|
|
|
// custom template_file in the browse configuration.
|
|
|
|
//
|
2021-11-16 02:53:54 +08:00
|
|
|
//go:embed browse.html
|
2023-08-29 23:34:20 +08:00
|
|
|
var BrowseTemplate string
|
2021-11-16 02:53:54 +08:00
|
|
|
|
2019-05-21 00:59:20 +08:00
|
|
|
// Browse configures directory browsing.
|
|
|
|
type Browse struct {
|
2023-08-29 23:34:20 +08:00
|
|
|
// Filename of the template to use instead of the embedded browse template.
|
2019-05-23 02:32:36 +08:00
|
|
|
TemplateFile string `json:"template_file,omitempty"`
|
2024-02-06 12:31:26 +08:00
|
|
|
// Determines whether or not targets of symlinks should be revealed.
|
|
|
|
RevealSymlinks bool `json:"reveal_symlinks,omitempty"`
|
2019-05-21 05:46:34 +08:00
|
|
|
}
|
2019-05-21 00:59:20 +08:00
|
|
|
|
2024-01-14 04:12:43 +08:00
|
|
|
func (fsrv *FileServer) serveBrowse(fileSystem fs.FS, root, dirPath string, w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
|
2020-11-27 00:37:42 +08: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 23:55:49 +08: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-22 03:03:52 +08: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 23:55:49 +08: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.
|
2022-03-02 06:32:39 +08:00
|
|
|
// We also redirect if the path is empty, because this implies the path
|
|
|
|
// prefix was fully stripped away by a `handle_path` handler for example.
|
|
|
|
// See https://github.com/caddyserver/caddy/issues/4466.
|
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 23:55:49 +08:00
|
|
|
origReq := r.Context().Value(caddyhttp.OriginalRequestCtxKey).(http.Request)
|
2022-03-02 06:32:39 +08:00
|
|
|
if r.URL.Path == "" || path.Base(origReq.URL.Path) == path.Base(r.URL.Path) {
|
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 23:55:49 +08:00
|
|
|
if !strings.HasSuffix(origReq.URL.Path, "/") {
|
|
|
|
fsrv.logger.Debug("redirecting to trailing slash to preserve hrefs", zap.String("request_path", r.URL.Path))
|
2022-07-08 04:10:19 +08:00
|
|
|
return redirect(w, r, origReq.URL.Path+"/")
|
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 23:55:49 +08:00
|
|
|
}
|
2019-05-22 03:03:52 +08:00
|
|
|
}
|
|
|
|
|
2024-01-14 04:12:43 +08:00
|
|
|
dir, err := fsrv.openFile(fileSystem, dirPath, w)
|
2019-05-21 05:46:34 +08:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer dir.Close()
|
|
|
|
|
2019-12-30 04:12:52 +08:00
|
|
|
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
|
2019-05-21 05:46:34 +08:00
|
|
|
|
2023-05-16 00:48:05 +08:00
|
|
|
// TODO: not entirely sure if path.Clean() is necessary here but seems like a safe plan (i.e. /%2e%2e%2f) - someone could verify this
|
2024-01-14 04:12:43 +08:00
|
|
|
listing, err := fsrv.loadDirectoryContents(r.Context(), fileSystem, dir.(fs.ReadDirFile), root, path.Clean(r.URL.EscapedPath()), repl)
|
2019-05-21 05:46:34 +08:00
|
|
|
switch {
|
2024-01-02 13:48:55 +08:00
|
|
|
case errors.Is(err, fs.ErrPermission):
|
2019-05-21 05:46:34 +08:00
|
|
|
return caddyhttp.Error(http.StatusForbidden, err)
|
2024-01-02 13:48:55 +08:00
|
|
|
case errors.Is(err, fs.ErrNotExist):
|
2019-11-16 08:32:13 +08:00
|
|
|
return fsrv.notFound(w, r, next)
|
2019-05-21 05:46:34 +08:00
|
|
|
case err != nil:
|
|
|
|
return caddyhttp.Error(http.StatusInternalServerError, err)
|
|
|
|
}
|
|
|
|
|
2023-05-19 23:59:40 +08:00
|
|
|
fsrv.browseApplyQueryParams(w, r, listing)
|
2019-05-21 05:46:34 +08:00
|
|
|
|
2021-06-17 04:28:34 +08:00
|
|
|
buf := bufPool.Get().(*bytes.Buffer)
|
2022-08-16 12:31:45 +08:00
|
|
|
buf.Reset()
|
2021-06-17 04:28:34 +08:00
|
|
|
defer bufPool.Put(buf)
|
|
|
|
|
2019-05-21 05:46:34 +08:00
|
|
|
acceptHeader := strings.ToLower(strings.Join(r.Header["Accept"], ","))
|
2021-06-17 04:28:34 +08:00
|
|
|
|
|
|
|
// write response as either JSON or HTML
|
2019-05-21 05:46:34 +08:00
|
|
|
if strings.Contains(acceptHeader, "application/json") {
|
2021-06-17 04:28:34 +08:00
|
|
|
if err := json.NewEncoder(buf).Encode(listing.Items); err != nil {
|
2019-05-21 05:46:34 +08:00
|
|
|
return caddyhttp.Error(http.StatusInternalServerError, err)
|
|
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
|
|
} else {
|
2021-05-01 10:17:23 +08:00
|
|
|
var fs http.FileSystem
|
|
|
|
if fsrv.Root != "" {
|
|
|
|
fs = http.Dir(repl.ReplaceAll(fsrv.Root, "."))
|
|
|
|
}
|
|
|
|
|
2023-08-08 03:40:31 +08:00
|
|
|
tplCtx := &templateContext{
|
2021-05-01 10:17:23 +08:00
|
|
|
TemplateContext: templates.TemplateContext{
|
|
|
|
Root: fs,
|
|
|
|
Req: r,
|
|
|
|
RespHeader: templates.WrappedHeader{Header: w.Header()},
|
|
|
|
},
|
|
|
|
browseTemplateContext: listing,
|
|
|
|
}
|
|
|
|
|
2021-06-17 04:28:34 +08:00
|
|
|
tpl, err := fsrv.makeBrowseTemplate(tplCtx)
|
2021-05-01 10:17:23 +08:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("parsing browse template: %v", err)
|
|
|
|
}
|
2021-06-17 04:28:34 +08:00
|
|
|
if err := tpl.Execute(buf, tplCtx); err != nil {
|
2019-05-21 05:46:34 +08:00
|
|
|
return caddyhttp.Error(http.StatusInternalServerError, err)
|
|
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
|
|
}
|
2019-06-22 04:36:26 +08:00
|
|
|
|
2020-11-23 05:50:29 +08:00
|
|
|
_, _ = buf.WriteTo(w)
|
2019-05-21 00:59:20 +08:00
|
|
|
|
2019-05-21 05:46:34 +08:00
|
|
|
return nil
|
2019-05-21 00:59:20 +08:00
|
|
|
}
|
|
|
|
|
2024-01-14 04:12:43 +08:00
|
|
|
func (fsrv *FileServer) loadDirectoryContents(ctx context.Context, fileSystem fs.FS, dir fs.ReadDirFile, root, urlPath string, repl *caddy.Replacer) (*browseTemplateContext, error) {
|
2022-07-31 03:07:44 +08:00
|
|
|
files, err := dir.ReadDir(10000) // TODO: this limit should probably be configurable
|
2022-09-08 11:14:11 +08:00
|
|
|
if err != nil && err != io.EOF {
|
2023-05-19 23:59:40 +08:00
|
|
|
return nil, err
|
2019-05-21 05:46:34 +08:00
|
|
|
}
|
|
|
|
|
2020-12-30 23:03:33 +08:00
|
|
|
// user can presumably browse "up" to parent folder if path is longer than "/"
|
|
|
|
canGoUp := len(urlPath) > 1
|
2019-05-21 05:46:34 +08:00
|
|
|
|
2024-01-14 04:12:43 +08:00
|
|
|
return fsrv.directoryListing(ctx, fileSystem, files, canGoUp, root, urlPath, repl), nil
|
2019-05-21 05:46:34 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// browseApplyQueryParams applies query parameters to the listing.
|
|
|
|
// It mutates the listing and may set cookies.
|
2021-05-01 10:17:23 +08:00
|
|
|
func (fsrv *FileServer) browseApplyQueryParams(w http.ResponseWriter, r *http.Request, listing *browseTemplateContext) {
|
2023-03-11 02:19:31 +08:00
|
|
|
layoutParam := r.URL.Query().Get("layout")
|
2019-05-21 05:46:34 +08:00
|
|
|
sortParam := r.URL.Query().Get("sort")
|
|
|
|
orderParam := r.URL.Query().Get("order")
|
|
|
|
limitParam := r.URL.Query().Get("limit")
|
2020-07-09 13:56:15 +08:00
|
|
|
offsetParam := r.URL.Query().Get("offset")
|
2019-05-21 05:46:34 +08:00
|
|
|
|
2023-03-11 02:19:31 +08:00
|
|
|
switch layoutParam {
|
|
|
|
case "list", "grid", "":
|
|
|
|
listing.Layout = layoutParam
|
|
|
|
default:
|
|
|
|
listing.Layout = "list"
|
|
|
|
}
|
|
|
|
|
|
|
|
// figure out what to sort by
|
2019-05-21 05:46:34 +08:00
|
|
|
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 13:56:15 +08:00
|
|
|
listing.applySortAndLimit(sortParam, orderParam, limitParam, offsetParam)
|
2019-05-21 05:46:34 +08:00
|
|
|
}
|
|
|
|
|
2021-05-01 10:17:23 +08:00
|
|
|
// makeBrowseTemplate creates the template to be used for directory listings.
|
2021-06-17 04:28:34 +08:00
|
|
|
func (fsrv *FileServer) makeBrowseTemplate(tplCtx *templateContext) (*template.Template, error) {
|
2021-05-01 10:17:23 +08: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-17 04:28:34 +08:00
|
|
|
return nil, fmt.Errorf("parsing browse template file: %v", err)
|
2021-05-01 10:17:23 +08:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
tpl = tplCtx.NewTemplate("default_listing")
|
2023-08-29 23:34:20 +08:00
|
|
|
tpl, err = tpl.Parse(BrowseTemplate)
|
2021-05-01 10:17:23 +08:00
|
|
|
if err != nil {
|
2021-06-17 04:28:34 +08:00
|
|
|
return nil, fmt.Errorf("parsing default browse template: %v", err)
|
2021-05-01 10:17:23 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-06-17 04:28:34 +08:00
|
|
|
return tpl, nil
|
2019-05-21 05:46:34 +08:00
|
|
|
}
|
|
|
|
|
2019-05-22 03:03:52 +08:00
|
|
|
// isSymlinkTargetDir returns true if f's symbolic link target
|
|
|
|
// is a directory.
|
2024-01-14 04:12:43 +08:00
|
|
|
func (fsrv *FileServer) isSymlinkTargetDir(fileSystem fs.FS, f fs.FileInfo, root, urlPath string) bool {
|
2019-05-22 03:03:52 +08:00
|
|
|
if !isSymlink(f) {
|
|
|
|
return false
|
|
|
|
}
|
2021-06-17 23:59:08 +08:00
|
|
|
target := caddyhttp.SanitizedPathJoin(root, path.Join(urlPath, f.Name()))
|
2024-01-14 04:12:43 +08:00
|
|
|
targetInfo, err := fs.Stat(fileSystem, target)
|
2019-05-22 03:03:52 +08:00
|
|
|
if err != nil {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
return targetInfo.IsDir()
|
2019-05-21 05:46:34 +08:00
|
|
|
}
|
2021-05-01 10:17:23 +08:00
|
|
|
|
2022-07-31 03:07:44 +08:00
|
|
|
// isSymlink return true if f is a symbolic link.
|
|
|
|
func isSymlink(f fs.FileInfo) bool {
|
|
|
|
return f.Mode()&os.ModeSymlink != 0
|
|
|
|
}
|
|
|
|
|
2021-05-01 10:17:23 +08: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
|
2023-05-19 23:59:40 +08:00
|
|
|
*browseTemplateContext
|
2021-05-01 10:17:23 +08:00
|
|
|
}
|
2021-06-17 04:28:34 +08:00
|
|
|
|
|
|
|
// bufPool is used to increase the efficiency of file listings.
|
|
|
|
var bufPool = sync.Pool{
|
2022-08-03 04:39:09 +08:00
|
|
|
New: func() any {
|
2021-06-17 04:28:34 +08:00
|
|
|
return new(bytes.Buffer)
|
|
|
|
},
|
|
|
|
}
|