@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, 02:15 UTC

master
go 441 lines 12.9 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 fastcgi
16
17 import (
18 "encoding/json"
19 "net/http"
20 "slices"
21 "strconv"
22
23 "github.com/caddyserver/caddy/v2"
24 "github.com/caddyserver/caddy/v2/caddyconfig"
25 "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
26 "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
27 "github.com/caddyserver/caddy/v2/modules/caddyhttp"
28 "github.com/caddyserver/caddy/v2/modules/caddyhttp/fileserver"
29 "github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy"
30 "github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite"
31 )
32
33 func init() {
34 httpcaddyfile.RegisterDirective("php_fastcgi", parsePHPFastCGI)
35 }
36
37 // UnmarshalCaddyfile deserializes Caddyfile tokens into h.
38 //
39 // transport fastcgi {
40 // root <path>
41 // split <at>
42 // env <key> <value>
43 // resolve_root_symlink
44 // dial_timeout <duration>
45 // read_timeout <duration>
46 // write_timeout <duration>
47 // capture_stderr
48 // }
49 func (t *Transport) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
50 d.Next() // consume transport name
51 for d.NextBlock(0) {
52 switch d.Val() {
53 case "root":
54 if !d.NextArg() {
55 return d.ArgErr()
56 }
57 t.Root = d.Val()
58
59 case "split":
60 t.SplitPath = d.RemainingArgs()
61 if len(t.SplitPath) == 0 {
62 return d.ArgErr()
63 }
64
65 case "env":
66 args := d.RemainingArgs()
67 if len(args) != 2 {
68 return d.ArgErr()
69 }
70 if t.EnvVars == nil {
71 t.EnvVars = make(map[string]string)
72 }
73 t.EnvVars[args[0]] = args[1]
74
75 case "resolve_root_symlink":
76 if d.NextArg() {
77 return d.ArgErr()
78 }
79 t.ResolveRootSymlink = true
80
81 case "dial_timeout":
82 if !d.NextArg() {
83 return d.ArgErr()
84 }
85 dur, err := caddy.ParseDuration(d.Val())
86 if err != nil {
87 return d.Errf("bad timeout value %s: %v", d.Val(), err)
88 }
89 t.DialTimeout = caddy.Duration(dur)
90
91 case "read_timeout":
92 if !d.NextArg() {
93 return d.ArgErr()
94 }
95 dur, err := caddy.ParseDuration(d.Val())
96 if err != nil {
97 return d.Errf("bad timeout value %s: %v", d.Val(), err)
98 }
99 t.ReadTimeout = caddy.Duration(dur)
100
101 case "write_timeout":
102 if !d.NextArg() {
103 return d.ArgErr()
104 }
105 dur, err := caddy.ParseDuration(d.Val())
106 if err != nil {
107 return d.Errf("bad timeout value %s: %v", d.Val(), err)
108 }
109 t.WriteTimeout = caddy.Duration(dur)
110
111 case "capture_stderr":
112 if d.NextArg() {
113 return d.ArgErr()
114 }
115 t.CaptureStderr = true
116
117 default:
118 return d.Errf("unrecognized subdirective %s", d.Val())
119 }
120 }
121 return nil
122 }
123
124 // parsePHPFastCGI parses the php_fastcgi directive, which has the same syntax
125 // as the reverse_proxy directive (in fact, the reverse_proxy's directive
126 // Unmarshaler is invoked by this function) but the resulting proxy is specially
127 // configured for most™️ PHP apps over FastCGI. A line such as this:
128 //
129 // php_fastcgi localhost:7777
130 //
131 // is equivalent to a route consisting of:
132 //
133 // # Add trailing slash for directory requests
134 // # This redirection is automatically disabled if "{http.request.uri.path}/index.php"
135 // # doesn't appear in the try_files list
136 // @canonicalPath {
137 // file {path}/index.php
138 // not path */
139 // }
140 // redir @canonicalPath {path}/ 308
141 //
142 // # If the requested file does not exist, try index files
143 // @indexFiles file {
144 // try_files {path} {path}/index.php index.php
145 // split_path .php
146 // }
147 // rewrite @indexFiles {http.matchers.file.relative}
148 //
149 // # Proxy PHP files to the FastCGI responder
150 // @phpFiles path *.php
151 // reverse_proxy @phpFiles localhost:7777 {
152 // transport fastcgi {
153 // split .php
154 // }
155 // }
156 //
157 // Thus, this directive produces multiple handlers, each with a different
158 // matcher because multiple consecutive handlers are necessary to support
159 // the common PHP use case. If this "common" config is not compatible
160 // with a user's PHP requirements, they can use a manual approach based
161 // on the example above to configure it precisely as they need.
162 //
163 // If a matcher is specified by the user, for example:
164 //
165 // php_fastcgi /subpath localhost:7777
166 //
167 // then the resulting handlers are wrapped in a subroute that uses the
168 // user's matcher as a prerequisite to enter the subroute. In other
169 // words, the directive's matcher is necessary, but not sufficient.
170 func parsePHPFastCGI(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error) {
171 if !h.Next() {
172 return nil, h.ArgErr()
173 }
174
175 // set up the transport for FastCGI, and specifically PHP
176 fcgiTransport := Transport{}
177
178 // set up the set of file extensions allowed to execute PHP code
179 extensions := []string{".php"}
180
181 // set the default index file for the try_files rewrites
182 indexFile := "index.php"
183
184 // set up for explicitly overriding try_files
185 var tryFiles []string
186
187 // if the user specified a matcher token, use that
188 // matcher in a route that wraps both of our routes;
189 // either way, strip the matcher token and pass
190 // the remaining tokens to the unmarshaler so that
191 // we can gain the rest of the reverse_proxy syntax
192 userMatcherSet, err := h.ExtractMatcherSet()
193 if err != nil {
194 return nil, err
195 }
196
197 // make a new dispenser from the remaining tokens so that we
198 // can reset the dispenser back to this point for the
199 // reverse_proxy unmarshaler to read from it as well
200 dispenser := h.NewFromNextSegment()
201
202 // read the subdirectives that we allow as overrides to
203 // the php_fastcgi shortcut
204 // NOTE: we delete the tokens as we go so that the reverse_proxy
205 // unmarshal doesn't see these subdirectives which it cannot handle
206 for dispenser.Next() {
207 for dispenser.NextBlock(0) {
208 // ignore any sub-subdirectives that might
209 // have the same name somewhere within
210 // the reverse_proxy passthrough tokens
211 if dispenser.Nesting() != 1 {
212 continue
213 }
214
215 // parse the php_fastcgi subdirectives
216 switch dispenser.Val() {
217 case "root":
218 if !dispenser.NextArg() {
219 return nil, dispenser.ArgErr()
220 }
221 fcgiTransport.Root = dispenser.Val()
222 dispenser.DeleteN(2)
223
224 case "split":
225 extensions = dispenser.RemainingArgs()
226 dispenser.DeleteN(len(extensions) + 1)
227 if len(extensions) == 0 {
228 return nil, dispenser.ArgErr()
229 }
230
231 case "env":
232 args := dispenser.RemainingArgs()
233 dispenser.DeleteN(len(args) + 1)
234 if len(args) != 2 {
235 return nil, dispenser.ArgErr()
236 }
237 if fcgiTransport.EnvVars == nil {
238 fcgiTransport.EnvVars = make(map[string]string)
239 }
240 fcgiTransport.EnvVars[args[0]] = args[1]
241
242 case "index":
243 args := dispenser.RemainingArgs()
244 dispenser.DeleteN(len(args) + 1)
245 if len(args) != 1 {
246 return nil, dispenser.ArgErr()
247 }
248 indexFile = args[0]
249
250 case "try_files":
251 args := dispenser.RemainingArgs()
252 dispenser.DeleteN(len(args) + 1)
253 if len(args) < 1 {
254 return nil, dispenser.ArgErr()
255 }
256 tryFiles = args
257
258 case "resolve_root_symlink":
259 args := dispenser.RemainingArgs()
260 dispenser.DeleteN(len(args) + 1)
261 fcgiTransport.ResolveRootSymlink = true
262
263 case "dial_timeout":
264 if !dispenser.NextArg() {
265 return nil, dispenser.ArgErr()
266 }
267 dur, err := caddy.ParseDuration(dispenser.Val())
268 if err != nil {
269 return nil, dispenser.Errf("bad timeout value %s: %v", dispenser.Val(), err)
270 }
271 fcgiTransport.DialTimeout = caddy.Duration(dur)
272 dispenser.DeleteN(2)
273
274 case "read_timeout":
275 if !dispenser.NextArg() {
276 return nil, dispenser.ArgErr()
277 }
278 dur, err := caddy.ParseDuration(dispenser.Val())
279 if err != nil {
280 return nil, dispenser.Errf("bad timeout value %s: %v", dispenser.Val(), err)
281 }
282 fcgiTransport.ReadTimeout = caddy.Duration(dur)
283 dispenser.DeleteN(2)
284
285 case "write_timeout":
286 if !dispenser.NextArg() {
287 return nil, dispenser.ArgErr()
288 }
289 dur, err := caddy.ParseDuration(dispenser.Val())
290 if err != nil {
291 return nil, dispenser.Errf("bad timeout value %s: %v", dispenser.Val(), err)
292 }
293 fcgiTransport.WriteTimeout = caddy.Duration(dur)
294 dispenser.DeleteN(2)
295
296 case "capture_stderr":
297 args := dispenser.RemainingArgs()
298 dispenser.DeleteN(len(args) + 1)
299 fcgiTransport.CaptureStderr = true
300 }
301 }
302 }
303
304 // reset the dispenser after we're done so that the reverse_proxy
305 // unmarshaler can read it from the start
306 dispenser.Reset()
307
308 // set up a route list that we'll append to
309 routes := caddyhttp.RouteList{}
310
311 // set the list of allowed path segments on which to split
312 fcgiTransport.SplitPath = extensions
313
314 // if the index is turned off, we skip the redirect and try_files
315 if indexFile != "off" {
316 var dirRedir bool
317 dirIndex := "{http.request.uri.path}/" + indexFile
318
319 // if tryFiles wasn't overridden, use a reasonable default
320 if len(tryFiles) == 0 {
321 tryFiles = []string{"{http.request.uri.path}", dirIndex, indexFile}
322 dirRedir = true
323 } else {
324 dirRedir = slices.Contains(tryFiles, dirIndex)
325 }
326
327 if dirRedir {
328 // route to redirect to canonical path if index PHP file
329 redirMatcherSet := caddy.ModuleMap{
330 "file": h.JSON(fileserver.MatchFile{
331 TryFiles: []string{dirIndex},
332 }),
333 "not": h.JSON(caddyhttp.MatchNot{
334 MatcherSetsRaw: []caddy.ModuleMap{
335 {
336 "path": h.JSON(caddyhttp.MatchPath{"*/"}),
337 },
338 },
339 }),
340 }
341 // The matcher above judges the cleaned request path, so the
342 // Location must not echo the raw original path: a request for
343 // /%2Fevil.com%2F..%2Fsub matches <root>/sub/index.php but would
344 // redirect to //evil.com/../sub/, which browsers resolve to
345 // another host. A reference relative to the last path segment
346 // resolves against the URL the client actually requested, so it
347 // can only ever land on the same origin; the leading ./ keeps a
348 // segment containing ':' from being read as a scheme.
349 redirHandler := caddyhttp.StaticResponse{
350 StatusCode: caddyhttp.WeakString(strconv.Itoa(http.StatusPermanentRedirect)),
351 Headers: http.Header{"Location": []string{"./{http.request.orig_uri.path.file_escaped}/{http.request.orig_uri.prefixed_query}"}},
352 }
353 redirRoute := caddyhttp.Route{
354 MatcherSetsRaw: []caddy.ModuleMap{redirMatcherSet},
355 HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(redirHandler, "handler", "static_response", nil)},
356 }
357
358 routes = append(routes, redirRoute)
359 }
360
361 // route to rewrite to PHP index file
362 rewriteMatcherSet := caddy.ModuleMap{
363 "file": h.JSON(fileserver.MatchFile{
364 TryFiles: tryFiles,
365 SplitPath: extensions,
366 }),
367 }
368 rewriteHandler := rewrite.Rewrite{
369 URI: "{http.matchers.file.relative}",
370 }
371 rewriteRoute := caddyhttp.Route{
372 MatcherSetsRaw: []caddy.ModuleMap{rewriteMatcherSet},
373 HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(rewriteHandler, "handler", "rewrite", nil)},
374 }
375
376 routes = append(routes, rewriteRoute)
377 }
378
379 // route to actually reverse proxy requests to PHP files;
380 // match only requests that are for PHP files
381 pathList := []string{}
382 for _, ext := range extensions {
383 pathList = append(pathList, "*"+ext)
384 }
385 rpMatcherSet := caddy.ModuleMap{
386 "path": h.JSON(pathList),
387 }
388
389 // create the reverse proxy handler which uses our FastCGI transport
390 rpHandler := &reverseproxy.Handler{
391 TransportRaw: caddyconfig.JSONModuleObject(fcgiTransport, "protocol", "fastcgi", nil),
392 }
393
394 // the rest of the config is specified by the user
395 // using the reverse_proxy directive syntax
396 dispenser.Next() // consume the directive name
397 err = rpHandler.UnmarshalCaddyfile(dispenser)
398 if err != nil {
399 return nil, err
400 }
401 err = rpHandler.FinalizeUnmarshalCaddyfile(h)
402 if err != nil {
403 return nil, err
404 }
405
406 // create the final reverse proxy route which is
407 // conditional on matching PHP files
408 rpRoute := caddyhttp.Route{
409 MatcherSetsRaw: []caddy.ModuleMap{rpMatcherSet},
410 HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(rpHandler, "handler", "reverse_proxy", nil)},
411 }
412
413 subroute := caddyhttp.Subroute{
414 Routes: append(routes, rpRoute),
415 }
416
417 // the user's matcher is a prerequisite for ours, so
418 // wrap ours in a subroute and return that
419 if userMatcherSet != nil {
420 return []httpcaddyfile.ConfigValue{
421 {
422 Class: "route",
423 Value: caddyhttp.Route{
424 MatcherSetsRaw: []caddy.ModuleMap{userMatcherSet},
425 HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(subroute, "handler", "subroute", nil)},
426 },
427 },
428 }, nil
429 }
430
431 // otherwise, return the literal subroute instead of
432 // individual routes, to ensure they stay together and
433 // are treated as a single unit, without necessarily
434 // creating an actual subroute in the output
435 return []httpcaddyfile.ConfigValue{
436 {
437 Class: "route",
438 Value: subroute,
439 },
440 }, nil
441 }