@marcel91200 / caddy Mirror · read-only
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 11, 01:00 UTC

master
go 405 lines 11.8 KB
Raw
1 // Copyright 2015 Matthew Holt and The Caddy Authors
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14
15 package fileserver
16
17 import (
18 "context"
19 "io/fs"
20 "net/url"
21 "os"
22 "path"
23 "slices"
24 "sort"
25 "strconv"
26 "strings"
27 "time"
28
29 "github.com/dustin/go-humanize"
30 "go.uber.org/zap"
31 "go.uber.org/zap/zapcore"
32
33 "github.com/caddyserver/caddy/v2"
34 "github.com/caddyserver/caddy/v2/modules/caddyhttp"
35 )
36
37 func (fsrv *FileServer) directoryListing(ctx context.Context, fileSystem fs.FS, parentModTime time.Time, entries []fs.DirEntry, canGoUp bool, dirPath, urlPath string, repl *caddy.Replacer) *browseTemplateContext {
38 filesToHide := fsrv.transformHidePaths(repl)
39
40 // urlPath is escaped, but filesystem lookups need the decoded path
41 reqPath, _ := url.PathUnescape(urlPath)
42
43 tplCtx := &browseTemplateContext{
44 Name: path.Base(reqPath),
45 Path: urlPath,
46 CanGoUp: canGoUp,
47 lastModified: parentModTime,
48 // preallocate, since we know the upper bound on the number of
49 // items; avoids repeated slice growth/copy for large directories
50 Items: make([]fileInfo, 0, len(entries)),
51 }
52
53 for _, entry := range entries {
54 if err := ctx.Err(); err != nil {
55 break
56 }
57
58 name := entry.Name()
59
60 if fileHidden(caddyhttp.SanitizedPathJoin(dirPath, name), filesToHide) {
61 continue
62 }
63
64 info, err := entry.Info()
65 if err != nil {
66 if c := fsrv.logger.Check(zapcore.ErrorLevel, "could not get info about directory entry"); c != nil {
67 c.Write(zap.String("name", entry.Name()), zap.String("dir_path", dirPath))
68 }
69 continue
70 }
71
72 // keep track of the most recently modified item in the listing
73 modTime := info.ModTime()
74 if tplCtx.lastModified.IsZero() || modTime.After(tplCtx.lastModified) {
75 tplCtx.lastModified = modTime
76 }
77
78 fileIsSymlink := isSymlink(info)
79 size := info.Size()
80
81 // for a symlink, a single stat of its target tells us both whether
82 // the target is a directory and what its size is
83 var targetInfo fs.FileInfo
84 var targetPath string
85 if fileIsSymlink {
86 targetPath = caddyhttp.SanitizedPathJoin(dirPath, info.Name())
87 // An error most likely means the symlink target doesn't exist,
88 // which isn't entirely unusual and shouldn't fail the listing.
89 // In this case, targetInfo stays nil and we fall back to
90 // treating it as a non-directory, using the symlink's own size.
91 targetInfo, _ = fs.Stat(fileSystem, targetPath)
92 }
93
94 isDir := entry.IsDir() || (targetInfo != nil && targetInfo.IsDir())
95
96 // add the slash after the escape of path to avoid escaping the slash as well
97 if isDir {
98 name += "/"
99 tplCtx.NumDirs++
100 } else {
101 tplCtx.NumFiles++
102 }
103
104 if !isDir {
105 // increase the total by the symlink's size, not the target's size,
106 // by incrementing before we follow the symlink
107 tplCtx.TotalFileSize += size
108 }
109
110 symlinkPath := ""
111 if fileIsSymlink {
112 if targetInfo != nil {
113 size = targetInfo.Size()
114 }
115
116 if fsrv.Browse.RevealSymlinks {
117 symLinkTarget, err := os.Readlink(targetPath)
118 if err == nil {
119 symlinkPath = symLinkTarget
120 }
121 }
122 }
123
124 if !isDir {
125 // increase the total including the symlink target's size
126 tplCtx.TotalFileSizeFollowingSymlinks += size
127 }
128
129 u := url.URL{Path: "./" + name} // prepend with "./" to fix paths with ':' in the name
130
131 tplCtx.Items = append(tplCtx.Items, fileInfo{
132 IsDir: isDir,
133 IsSymlink: fileIsSymlink,
134 Name: name,
135 Size: size,
136 URL: u.String(),
137 ModTime: modTime.UTC(),
138 Mode: info.Mode(),
139 Tpl: tplCtx, // a reference up to the template context is useful
140 SymlinkPath: symlinkPath,
141 })
142 }
143
144 // this time is used for the Last-Modified header and comparing If-Modified-Since from client
145 // both are expected to be in UTC, so we convert to UTC here
146 // see: https://github.com/caddyserver/caddy/issues/6828
147 tplCtx.lastModified = tplCtx.lastModified.UTC()
148 return tplCtx
149 }
150
151 // browseTemplateContext provides the template context for directory listings.
152 type browseTemplateContext struct {
153 // The name of the directory (the last element of the path).
154 Name string `json:"name"`
155
156 // The full path of the request.
157 Path string `json:"path"`
158
159 // Whether the parent directory is browsable.
160 CanGoUp bool `json:"can_go_up"`
161
162 // The items (files and folders) in the path.
163 Items []fileInfo `json:"items,omitempty"`
164
165 // If ≠0 then Items starting from that many elements.
166 Offset int `json:"offset,omitempty"`
167
168 // If ≠0 then Items have been limited to that many elements.
169 Limit int `json:"limit,omitempty"`
170
171 // The number of directories in the listing.
172 NumDirs int `json:"num_dirs"`
173
174 // The number of files (items that aren't directories) in the listing.
175 NumFiles int `json:"num_files"`
176
177 // The total size of all files in the listing. Only includes the
178 // size of the files themselves, not the size of symlink targets
179 // (i.e. the calculation of this value does not follow symlinks).
180 TotalFileSize int64 `json:"total_file_size"`
181
182 // The total size of all files in the listing, including the
183 // size of the files targeted by symlinks.
184 TotalFileSizeFollowingSymlinks int64 `json:"total_file_size_following_symlinks"`
185
186 // Sort column used
187 Sort string `json:"sort,omitempty"`
188
189 // Sorting order
190 Order string `json:"order,omitempty"`
191
192 // Display format (list or grid)
193 Layout string `json:"layout,omitempty"`
194
195 // The most recent file modification date in the listing.
196 // Used for HTTP header purposes.
197 lastModified time.Time
198 }
199
200 // Breadcrumbs returns l.Path where every element maps
201 // the link to the text to display.
202 func (l browseTemplateContext) Breadcrumbs() []crumb {
203 if len(l.Path) == 0 {
204 return []crumb{}
205 }
206
207 // skip trailing slash
208 lpath := l.Path
209 if lpath[len(lpath)-1] == '/' {
210 lpath = lpath[:len(lpath)-1]
211 }
212 parts := strings.Split(lpath, "/")
213 result := make([]crumb, len(parts))
214 for i, p := range parts {
215 if i == 0 && p == "" {
216 p = "/"
217 }
218 // the directory name could include an encoded slash in its path,
219 // so the item name should be unescaped in the loop rather than unescaping the
220 // entire path outside the loop.
221 p, _ = url.PathUnescape(p)
222 lnk := strings.Repeat("../", len(parts)-i-1)
223 result[i] = crumb{Link: lnk, Text: p}
224 }
225
226 return result
227 }
228
229 func (l *browseTemplateContext) applySortAndLimit(sortParam, orderParam, limitParam string, offsetParam string) {
230 l.Sort = sortParam
231 l.Order = orderParam
232
233 // Only compute lowercase names when the selected sort comparator needs it;
234 // avoid O(n) work when sorting by time or when no sorting is requested.
235 if l.Sort == sortByName || l.Sort == sortByNameDirFirst || l.Sort == sortBySize {
236 for i := range l.Items {
237 l.Items[i].nameLower = strings.ToLower(l.Items[i].Name)
238 }
239 }
240
241 if l.Order == "desc" {
242 switch l.Sort {
243 case sortByName:
244 sort.Sort(sort.Reverse(byName(*l)))
245 case sortByNameDirFirst:
246 sort.Sort(sort.Reverse(byNameDirFirst(*l)))
247 case sortBySize:
248 sort.Sort(sort.Reverse(bySize(*l)))
249 case sortByTime:
250 sort.Sort(sort.Reverse(byTime(*l)))
251 }
252 } else {
253 switch l.Sort {
254 case sortByName:
255 sort.Sort(byName(*l))
256 case sortByNameDirFirst:
257 sort.Sort(byNameDirFirst(*l))
258 case sortBySize:
259 sort.Sort(bySize(*l))
260 case sortByTime:
261 sort.Sort(byTime(*l))
262 }
263 }
264
265 if offsetParam != "" {
266 offset, _ := strconv.Atoi(offsetParam)
267 if offset > 0 {
268 l.Items = l.Items[min(offset, len(l.Items)):]
269 l.Offset = offset
270 }
271 }
272
273 if limitParam != "" {
274 limit, _ := strconv.Atoi(limitParam)
275
276 if limit > 0 && limit <= len(l.Items) {
277 l.Items = l.Items[:limit]
278 l.Limit = limit
279 }
280 }
281 }
282
283 // crumb represents part of a breadcrumb menu,
284 // pairing a link with the text to display.
285 type crumb struct {
286 Link, Text string
287 }
288
289 // fileInfo contains serializable information
290 // about a file or directory.
291 type fileInfo struct {
292 Name string `json:"name"`
293 Size int64 `json:"size"`
294 URL string `json:"url"`
295 ModTime time.Time `json:"mod_time"`
296 Mode os.FileMode `json:"mode"`
297 IsDir bool `json:"is_dir"`
298 IsSymlink bool `json:"is_symlink"`
299 SymlinkPath string `json:"symlink_path,omitempty"`
300
301 // a pointer to the template context is useful inside nested templates
302 Tpl *browseTemplateContext `json:"-"`
303
304 // cached lowercase Name, precomputed once by applySortAndLimit so that
305 // byName/byNameDirFirst/bySize don't each recompute it on every one of
306 // the O(n log n) comparisons sort.Sort makes; not serialized.
307 nameLower string
308 }
309
310 // HasExt returns true if the filename has any of the given suffixes, case-insensitive.
311 func (fi fileInfo) HasExt(exts ...string) bool {
312 return slices.ContainsFunc(exts, func(ext string) bool {
313 return strings.HasSuffix(strings.ToLower(fi.Name), strings.ToLower(ext))
314 })
315 }
316
317 // HumanSize returns the size of the file as a
318 // human-readable string in IEC format (i.e.
319 // power of 2 or base 1024).
320 func (fi fileInfo) HumanSize() string {
321 return humanize.IBytes(uint64(fi.Size))
322 }
323
324 // HumanTotalFileSize returns the total size of all files
325 // in the listing as a human-readable string in IEC format
326 // (i.e. power of 2 or base 1024).
327 func (btc browseTemplateContext) HumanTotalFileSize() string {
328 return humanize.IBytes(uint64(btc.TotalFileSize))
329 }
330
331 // HumanTotalFileSizeFollowingSymlinks is the same as HumanTotalFileSize
332 // except the returned value reflects the size of symlink targets.
333 func (btc browseTemplateContext) HumanTotalFileSizeFollowingSymlinks() string {
334 return humanize.IBytes(uint64(btc.TotalFileSizeFollowingSymlinks))
335 }
336
337 // HumanModTime returns the modified time of the file
338 // as a human-readable string given by format.
339 func (fi fileInfo) HumanModTime(format string) string {
340 return fi.ModTime.Format(format)
341 }
342
343 type (
344 byName browseTemplateContext
345 byNameDirFirst browseTemplateContext
346 bySize browseTemplateContext
347 byTime browseTemplateContext
348 )
349
350 func (l byName) Len() int { return len(l.Items) }
351 func (l byName) Swap(i, j int) { l.Items[i], l.Items[j] = l.Items[j], l.Items[i] }
352
353 func (l byName) Less(i, j int) bool {
354 return l.Items[i].nameLower < l.Items[j].nameLower
355 }
356
357 func (l byNameDirFirst) Len() int { return len(l.Items) }
358 func (l byNameDirFirst) Swap(i, j int) { l.Items[i], l.Items[j] = l.Items[j], l.Items[i] }
359
360 func (l byNameDirFirst) Less(i, j int) bool {
361 // sort by name if both are dir or file
362 if l.Items[i].IsDir == l.Items[j].IsDir {
363 return l.Items[i].nameLower < l.Items[j].nameLower
364 }
365 // sort dir ahead of file
366 return l.Items[i].IsDir
367 }
368
369 func (l bySize) Len() int { return len(l.Items) }
370 func (l bySize) Swap(i, j int) { l.Items[i], l.Items[j] = l.Items[j], l.Items[i] }
371
372 func (l bySize) Less(i, j int) bool {
373 const directoryOffset = -1 << 31 // = -2147483648 (min int32)
374
375 iSize, jSize := l.Items[i].Size, l.Items[j].Size
376
377 // directory sizes depend on the file system; to
378 // provide a consistent experience, put them up front
379 // and sort them by name
380 if l.Items[i].IsDir {
381 iSize = directoryOffset
382 }
383 if l.Items[j].IsDir {
384 jSize = directoryOffset
385 }
386 if l.Items[i].IsDir && l.Items[j].IsDir {
387 return l.Items[i].nameLower < l.Items[j].nameLower
388 }
389
390 return iSize < jSize
391 }
392
393 func (l byTime) Len() int { return len(l.Items) }
394 func (l byTime) Swap(i, j int) { l.Items[i], l.Items[j] = l.Items[j], l.Items[i] }
395 func (l byTime) Less(i, j int) bool { return l.Items[i].ModTime.Before(l.Items[j].ModTime) }
396
397 const (
398 sortByName = "name"
399 sortByNameDirFirst = "namedirfirst"
400 sortBySize = "size"
401 sortByTime = "time"
402
403 sortOrderAsc = "asc"
404 sortOrderDesc = "desc"
405 )