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
| 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 | } |