@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 356 lines 10.6 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 "path/filepath"
19 "strconv"
20 "strings"
21
22 "github.com/caddyserver/caddy/v2"
23 "github.com/caddyserver/caddy/v2/caddyconfig"
24 "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
25 "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
26 "github.com/caddyserver/caddy/v2/modules/caddyhttp"
27 "github.com/caddyserver/caddy/v2/modules/caddyhttp/encode"
28 "github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite"
29 )
30
31 func init() {
32 httpcaddyfile.RegisterHandlerDirective("file_server", parseCaddyfile)
33 httpcaddyfile.RegisterDirective("try_files", parseTryFiles)
34 }
35
36 // parseCaddyfile parses the file_server directive.
37 // See UnmarshalCaddyfile for the syntax.
38 func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
39 fsrv := new(FileServer)
40 err := fsrv.UnmarshalCaddyfile(h.Dispenser)
41 if err != nil {
42 return fsrv, err
43 }
44 err = fsrv.FinalizeUnmarshalCaddyfile(h)
45 if err != nil {
46 return nil, err
47 }
48 return fsrv, err
49 }
50
51 // UnmarshalCaddyfile parses the file_server directive. It enables
52 // the static file server and configures it with this syntax:
53 //
54 // file_server [<matcher>] [browse] {
55 // fs <filesystem>
56 // root <path>
57 // hide <files...>
58 // index <files...>
59 // browse [<template_file>] {
60 // reveal_symlinks
61 // sort <options...>
62 // file_limit <limit>
63 // plain_time_format <layout>
64 // }
65 // precompressed <formats...>
66 // status <status>
67 // disable_canonical_uris
68 // }
69 //
70 // The FinalizeUnmarshalCaddyfile method should be called after this
71 // to finalize setup of hidden Caddyfiles.
72 func (fsrv *FileServer) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
73 d.Next() // consume directive name
74
75 args := d.RemainingArgs()
76 switch len(args) {
77 case 0:
78 case 1:
79 if args[0] != "browse" {
80 return d.ArgErr()
81 }
82 fsrv.Browse = new(Browse)
83 default:
84 return d.ArgErr()
85 }
86
87 for nesting := d.Nesting(); d.NextBlock(nesting); {
88 switch d.Val() {
89 case "fs":
90 if !d.NextArg() {
91 return d.ArgErr()
92 }
93 if fsrv.FileSystem != "" {
94 return d.Err("file system already specified")
95 }
96 fsrv.FileSystem = d.Val()
97
98 case "hide":
99 hide := d.RemainingArgs()
100 if len(hide) == 0 {
101 return d.ArgErr()
102 }
103 // Append so that repeated "hide" subdirectives accumulate
104 // instead of overwriting, which also lets imported snippets
105 // compose with site-specific hides.
106 fsrv.Hide = append(fsrv.Hide, hide...)
107
108 case "index":
109 fsrv.IndexNames = d.RemainingArgs()
110 if len(fsrv.IndexNames) == 0 {
111 return d.ArgErr()
112 }
113
114 case "root":
115 if !d.Args(&fsrv.Root) {
116 return d.ArgErr()
117 }
118
119 case "browse":
120 if fsrv.Browse != nil {
121 return d.Err("browsing is already configured")
122 }
123 fsrv.Browse = new(Browse)
124 d.Args(&fsrv.Browse.TemplateFile)
125 for nesting := d.Nesting(); d.NextBlock(nesting); {
126 switch d.Val() {
127 case "reveal_symlinks":
128 if fsrv.Browse.RevealSymlinks {
129 return d.Err("Symlinks path reveal is already enabled")
130 }
131 fsrv.Browse.RevealSymlinks = true
132 case "sort":
133 for d.NextArg() {
134 dVal := d.Val()
135 switch dVal {
136 case sortByName, sortByNameDirFirst, sortBySize, sortByTime, sortOrderAsc, sortOrderDesc:
137 fsrv.Browse.SortOptions = append(fsrv.Browse.SortOptions, dVal)
138 default:
139 return d.Errf("unknown sort option '%s'", dVal)
140 }
141 }
142 case "file_limit":
143 fileLimit := d.RemainingArgs()
144 if len(fileLimit) != 1 {
145 return d.Err("file_limit should have an integer value")
146 }
147 val, err := strconv.Atoi(fileLimit[0])
148 if err != nil {
149 return d.Err("file_limit should have an integer value")
150 }
151 if fsrv.Browse.FileLimit != 0 {
152 return d.Err("file_limit is already enabled")
153 }
154 fsrv.Browse.FileLimit = val
155 case "plain_time_format":
156 plainTimeFormat := d.RemainingArgs()
157 if len(plainTimeFormat) == 0 {
158 return d.Err("plain_time_format is missing a time layout argument")
159 }
160 if len(plainTimeFormat) > 1 {
161 return d.Err("plain_time_format accepts exactly one time layout argument; enclose layouts containing spaces in quotes")
162 }
163 if plainTimeFormat[0] == "" {
164 return d.Err("plain_time_format requires a non-empty time layout argument")
165 }
166 if fsrv.Browse.PlainTimeFormat != "" {
167 return d.Err("plain_time_format is already set")
168 }
169 fsrv.Browse.PlainTimeFormat = plainTimeFormat[0]
170 default:
171 return d.Errf("unknown subdirective '%s'", d.Val())
172 }
173 }
174
175 case "precompressed":
176 fsrv.PrecompressedOrder = d.RemainingArgs()
177 if len(fsrv.PrecompressedOrder) == 0 {
178 fsrv.PrecompressedOrder = []string{"br", "zstd", "gzip"}
179 }
180
181 for _, format := range fsrv.PrecompressedOrder {
182 modID := "http.precompressed." + format
183 mod, err := caddy.GetModule(modID)
184 if err != nil {
185 return d.Errf("getting module named '%s': %v", modID, err)
186 }
187 inst := mod.New()
188 precompress, ok := inst.(encode.Precompressed)
189 if !ok {
190 return d.Errf("module %s is not a precompressor; is %T", modID, inst)
191 }
192 if fsrv.PrecompressedRaw == nil {
193 fsrv.PrecompressedRaw = make(caddy.ModuleMap)
194 }
195 fsrv.PrecompressedRaw[format] = caddyconfig.JSON(precompress, nil)
196 }
197
198 case "status":
199 if !d.NextArg() {
200 return d.ArgErr()
201 }
202 fsrv.StatusCode = caddyhttp.WeakString(d.Val())
203
204 case "disable_canonical_uris":
205 if d.NextArg() {
206 return d.ArgErr()
207 }
208 falseBool := false
209 fsrv.CanonicalURIs = &falseBool
210
211 case "pass_thru":
212 if d.NextArg() {
213 return d.ArgErr()
214 }
215 fsrv.PassThru = true
216
217 case "etag_file_extensions":
218 etagFileExtensions := d.RemainingArgs()
219 if len(etagFileExtensions) == 0 {
220 return d.ArgErr()
221 }
222 fsrv.EtagFileExtensions = etagFileExtensions
223
224 default:
225 return d.Errf("unknown subdirective '%s'", d.Val())
226 }
227 }
228
229 return nil
230 }
231
232 // FinalizeUnmarshalCaddyfile finalizes the Caddyfile parsing which
233 // requires having an httpcaddyfile.Helper to function, to setup hidden Caddyfiles.
234 func (fsrv *FileServer) FinalizeUnmarshalCaddyfile(h httpcaddyfile.Helper) error {
235 // Hide the Caddyfile (and any imported Caddyfiles).
236 // This needs to be done in here instead of UnmarshalCaddyfile
237 // because UnmarshalCaddyfile only has access to the dispenser
238 // and not the helper, and only the helper has access to the
239 // Caddyfiles function.
240 if configFiles := h.Caddyfiles(); len(configFiles) > 0 {
241 for _, file := range configFiles {
242 file = filepath.Clean(file)
243 if !fileHidden(file, fsrv.Hide) {
244 // if there's no path separator, the file server module will hide all
245 // files by that name, rather than a specific one; but we want to hide
246 // only this specific file, so ensure there's always a path separator
247 if !strings.Contains(file, separator) {
248 file = "." + separator + file
249 }
250 fsrv.Hide = append(fsrv.Hide, file)
251 }
252 }
253 }
254 return nil
255 }
256
257 // parseTryFiles parses the try_files directive. It combines a file matcher
258 // with a rewrite directive, so this is not a standard handler directive.
259 // A try_files directive has this syntax (notice no matcher tokens accepted):
260 //
261 // try_files <files...> {
262 // policy first_exist|smallest_size|largest_size|most_recently_modified
263 // }
264 //
265 // and is basically shorthand for:
266 //
267 // @try_files file {
268 // try_files <files...>
269 // policy first_exist|smallest_size|largest_size|most_recently_modified
270 // }
271 // rewrite @try_files {http.matchers.file.relative}
272 //
273 // This directive rewrites request paths only, preserving any other part
274 // of the URI, unless the part is explicitly given in the file list. For
275 // example, if any of the files in the list have a query string:
276 //
277 // try_files {path} index.php?{query}&p={path}
278 //
279 // then the query string will not be treated as part of the file name; and
280 // if that file matches, the given query string will replace any query string
281 // that already exists on the request URI.
282 func parseTryFiles(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error) {
283 if !h.Next() {
284 return nil, h.ArgErr()
285 }
286
287 tryFiles := h.RemainingArgs()
288 if len(tryFiles) == 0 {
289 return nil, h.ArgErr()
290 }
291
292 // parse out the optional try policy
293 var tryPolicy string
294 for h.NextBlock(0) {
295 switch h.Val() {
296 case "policy":
297 if tryPolicy != "" {
298 return nil, h.Err("try policy already configured")
299 }
300 if !h.NextArg() {
301 return nil, h.ArgErr()
302 }
303 tryPolicy = h.Val()
304
305 switch tryPolicy {
306 case tryPolicyFirstExist, tryPolicyFirstExistFallback, tryPolicyLargestSize, tryPolicySmallestSize, tryPolicyMostRecentlyMod:
307 default:
308 return nil, h.Errf("unrecognized try policy: %s", tryPolicy)
309 }
310 }
311 }
312
313 // makeRoute returns a route that tries the files listed in try
314 // and then rewrites to the matched file; userQueryString is
315 // appended to the rewrite rule.
316 makeRoute := func(try []string, userQueryString string) []httpcaddyfile.ConfigValue {
317 handler := rewrite.Rewrite{
318 URI: "{http.matchers.file.relative}" + userQueryString,
319 }
320 matcherSet := caddy.ModuleMap{
321 "file": h.JSON(MatchFile{TryFiles: try, TryPolicy: tryPolicy}),
322 }
323 return h.NewRoute(matcherSet, handler)
324 }
325
326 var result []httpcaddyfile.ConfigValue
327
328 // if there are query strings in the list, we have to split into
329 // a separate route for each item with a query string, because
330 // the rewrite is different for that item
331 try := make([]string, 0, len(tryFiles))
332 for _, item := range tryFiles {
333 if idx := strings.Index(item, "?"); idx >= 0 {
334 if len(try) > 0 {
335 result = append(result, makeRoute(try, "")...)
336 try = []string{}
337 }
338 result = append(result, makeRoute([]string{item[:idx]}, item[idx:])...)
339 continue
340 }
341 // accumulate consecutive non-query-string parameters
342 try = append(try, item)
343 }
344 if len(try) > 0 {
345 result = append(result, makeRoute(try, "")...)
346 }
347
348 // ensure that multiple routes (possible if rewrite targets
349 // have query strings, for example) are grouped together
350 // so only the first matching rewrite is performed (#2891)
351 h.GroupRoutes(result)
352
353 return result, nil
354 }
355
356 var _ caddyfile.Unmarshaler = (*FileServer)(nil)