caddy.go 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746
  1. // Package caddy provides a PHP module for the Caddy web server.
  2. // FrankenPHP embeds the PHP interpreter directly in Caddy, giving it the ability to run your PHP scripts directly.
  3. // No PHP FPM required!
  4. package caddy
  5. import (
  6. "encoding/json"
  7. "errors"
  8. "fmt"
  9. "github.com/dunglas/frankenphp/internal/fastabs"
  10. "github.com/prometheus/client_golang/prometheus"
  11. "net/http"
  12. "path/filepath"
  13. "strconv"
  14. "strings"
  15. "github.com/caddyserver/caddy/v2"
  16. "github.com/caddyserver/caddy/v2/caddyconfig"
  17. "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
  18. "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
  19. "github.com/caddyserver/caddy/v2/modules/caddyhttp"
  20. "github.com/caddyserver/caddy/v2/modules/caddyhttp/fileserver"
  21. "github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite"
  22. "github.com/dunglas/frankenphp"
  23. "go.uber.org/zap"
  24. )
  25. const defaultDocumentRoot = "public"
  26. var iniError = errors.New("'php_ini' must be in the format: php_ini \"<key>\" \"<value>\"")
  27. func init() {
  28. caddy.RegisterModule(FrankenPHPApp{})
  29. caddy.RegisterModule(FrankenPHPModule{})
  30. caddy.RegisterModule(FrankenPHPAdmin{})
  31. httpcaddyfile.RegisterGlobalOption("frankenphp", parseGlobalOption)
  32. httpcaddyfile.RegisterHandlerDirective("php", parseCaddyfile)
  33. httpcaddyfile.RegisterDirectiveOrder("php", "before", "file_server")
  34. httpcaddyfile.RegisterDirective("php_server", parsePhpServer)
  35. httpcaddyfile.RegisterDirectiveOrder("php_server", "before", "file_server")
  36. }
  37. var metrics = frankenphp.NewPrometheusMetrics(prometheus.DefaultRegisterer)
  38. type workerConfig struct {
  39. // FileName sets the path to the worker script.
  40. FileName string `json:"file_name,omitempty"`
  41. // Num sets the number of workers to start.
  42. Num int `json:"num,omitempty"`
  43. // Env sets an extra environment variable to the given value. Can be specified more than once for multiple environment variables.
  44. Env map[string]string `json:"env,omitempty"`
  45. // Directories to watch for file changes
  46. Watch []string `json:"watch,omitempty"`
  47. }
  48. type FrankenPHPApp struct {
  49. // NumThreads sets the number of PHP threads to start. Default: 2x the number of available CPUs.
  50. NumThreads int `json:"num_threads,omitempty"`
  51. // MaxThreads limits how many threads can be started at runtime. Default 2x NumThreads
  52. MaxThreads int `json:"max_threads,omitempty"`
  53. // Workers configures the worker scripts to start.
  54. Workers []workerConfig `json:"workers,omitempty"`
  55. // Overwrites the default php ini configuration
  56. PhpIni map[string]string `json:"php_ini,omitempty"`
  57. }
  58. // CaddyModule returns the Caddy module information.
  59. func (f FrankenPHPApp) CaddyModule() caddy.ModuleInfo {
  60. return caddy.ModuleInfo{
  61. ID: "frankenphp",
  62. New: func() caddy.Module { return &f },
  63. }
  64. }
  65. func (f *FrankenPHPApp) Start() error {
  66. repl := caddy.NewReplacer()
  67. logger := caddy.Log()
  68. opts := []frankenphp.Option{
  69. frankenphp.WithNumThreads(f.NumThreads),
  70. frankenphp.WithMaxThreads(f.MaxThreads),
  71. frankenphp.WithLogger(logger),
  72. frankenphp.WithMetrics(metrics),
  73. frankenphp.WithPhpIni(f.PhpIni),
  74. }
  75. for _, w := range f.Workers {
  76. opts = append(opts, frankenphp.WithWorkers(repl.ReplaceKnown(w.FileName, ""), w.Num, w.Env, w.Watch))
  77. }
  78. frankenphp.Shutdown()
  79. if err := frankenphp.Init(opts...); err != nil {
  80. return err
  81. }
  82. return nil
  83. }
  84. func (f *FrankenPHPApp) Stop() error {
  85. caddy.Log().Info("FrankenPHP stopped 🐘")
  86. // attempt a graceful shutdown if caddy is exiting
  87. // note: Exiting() is currently marked as 'experimental'
  88. // https://github.com/caddyserver/caddy/blob/e76405d55058b0a3e5ba222b44b5ef00516116aa/caddy.go#L810
  89. if caddy.Exiting() {
  90. frankenphp.Shutdown()
  91. }
  92. // reset configuration so it doesn't bleed into later tests
  93. f.Workers = nil
  94. f.NumThreads = 0
  95. return nil
  96. }
  97. // UnmarshalCaddyfile implements caddyfile.Unmarshaler.
  98. func (f *FrankenPHPApp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
  99. for d.Next() {
  100. for d.NextBlock(0) {
  101. switch d.Val() {
  102. case "num_threads":
  103. if !d.NextArg() {
  104. return d.ArgErr()
  105. }
  106. v, err := strconv.Atoi(d.Val())
  107. if err != nil {
  108. return err
  109. }
  110. f.NumThreads = v
  111. case "max_threads":
  112. if !d.NextArg() {
  113. return d.ArgErr()
  114. }
  115. if d.Val() == "auto" {
  116. f.MaxThreads = -1
  117. continue
  118. }
  119. v, err := strconv.ParseUint(d.Val(), 10, 32)
  120. if err != nil {
  121. return err
  122. }
  123. f.MaxThreads = int(v)
  124. case "php_ini":
  125. parseIniLine := func(d *caddyfile.Dispenser) error {
  126. key := d.Val()
  127. if !d.NextArg() {
  128. return iniError
  129. }
  130. if f.PhpIni == nil {
  131. f.PhpIni = make(map[string]string)
  132. }
  133. f.PhpIni[key] = d.Val()
  134. if d.NextArg() {
  135. return iniError
  136. }
  137. return nil
  138. }
  139. isBlock := false
  140. for d.NextBlock(1) {
  141. isBlock = true
  142. err := parseIniLine(d)
  143. if err != nil {
  144. return err
  145. }
  146. }
  147. if !isBlock {
  148. if !d.NextArg() {
  149. return iniError
  150. }
  151. err := parseIniLine(d)
  152. if err != nil {
  153. return err
  154. }
  155. }
  156. case "worker":
  157. wc := workerConfig{}
  158. if d.NextArg() {
  159. wc.FileName = d.Val()
  160. }
  161. if d.NextArg() {
  162. v, err := strconv.Atoi(d.Val())
  163. if err != nil {
  164. return err
  165. }
  166. wc.Num = v
  167. }
  168. for d.NextBlock(1) {
  169. v := d.Val()
  170. switch v {
  171. case "file":
  172. if !d.NextArg() {
  173. return d.ArgErr()
  174. }
  175. wc.FileName = d.Val()
  176. case "num":
  177. if !d.NextArg() {
  178. return d.ArgErr()
  179. }
  180. v, err := strconv.Atoi(d.Val())
  181. if err != nil {
  182. return err
  183. }
  184. wc.Num = v
  185. case "env":
  186. args := d.RemainingArgs()
  187. if len(args) != 2 {
  188. return d.ArgErr()
  189. }
  190. if wc.Env == nil {
  191. wc.Env = make(map[string]string)
  192. }
  193. wc.Env[args[0]] = args[1]
  194. case "watch":
  195. if !d.NextArg() {
  196. // the default if the watch directory is left empty:
  197. wc.Watch = append(wc.Watch, "./**/*.{php,yaml,yml,twig,env}")
  198. } else {
  199. wc.Watch = append(wc.Watch, d.Val())
  200. }
  201. }
  202. if wc.FileName == "" {
  203. return errors.New(`the "file" argument must be specified`)
  204. }
  205. if frankenphp.EmbeddedAppPath != "" && filepath.IsLocal(wc.FileName) {
  206. wc.FileName = filepath.Join(frankenphp.EmbeddedAppPath, wc.FileName)
  207. }
  208. }
  209. f.Workers = append(f.Workers, wc)
  210. }
  211. }
  212. }
  213. if f.MaxThreads > 0 && f.NumThreads > 0 && f.MaxThreads < f.NumThreads {
  214. return errors.New("'max_threads' must be greater than or equal to 'num_threads'")
  215. }
  216. return nil
  217. }
  218. func parseGlobalOption(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
  219. app := &FrankenPHPApp{}
  220. if err := app.UnmarshalCaddyfile(d); err != nil {
  221. return nil, err
  222. }
  223. // tell Caddyfile adapter that this is the JSON for an app
  224. return httpcaddyfile.App{
  225. Name: "frankenphp",
  226. Value: caddyconfig.JSON(app, nil),
  227. }, nil
  228. }
  229. type FrankenPHPModule struct {
  230. // Root sets the root folder to the site. Default: `root` directive, or the path of the public directory of the embed app it exists.
  231. Root string `json:"root,omitempty"`
  232. // SplitPath sets the substrings for splitting the URI into two parts. The first matching substring will be used to split the "path info" from the path. The first piece is suffixed with the matching substring and will be assumed as the actual resource (CGI script) name. The second piece will be set to PATH_INFO for the CGI script to use. Default: `.php`.
  233. SplitPath []string `json:"split_path,omitempty"`
  234. // ResolveRootSymlink enables resolving the `root` directory to its actual value by evaluating a symbolic link, if one exists.
  235. ResolveRootSymlink *bool `json:"resolve_root_symlink,omitempty"`
  236. // Env sets an extra environment variable to the given value. Can be specified more than once for multiple environment variables.
  237. Env map[string]string `json:"env,omitempty"`
  238. resolvedDocumentRoot string
  239. preparedEnv frankenphp.PreparedEnv
  240. preparedEnvNeedsReplacement bool
  241. logger *zap.Logger
  242. }
  243. // CaddyModule returns the Caddy module information.
  244. func (FrankenPHPModule) CaddyModule() caddy.ModuleInfo {
  245. return caddy.ModuleInfo{
  246. ID: "http.handlers.php",
  247. New: func() caddy.Module { return new(FrankenPHPModule) },
  248. }
  249. }
  250. // Provision sets up the module.
  251. func (f *FrankenPHPModule) Provision(ctx caddy.Context) error {
  252. f.logger = ctx.Logger(f)
  253. if f.Root == "" {
  254. if frankenphp.EmbeddedAppPath == "" {
  255. f.Root = "{http.vars.root}"
  256. } else {
  257. rrs := false
  258. f.Root = filepath.Join(frankenphp.EmbeddedAppPath, defaultDocumentRoot)
  259. f.ResolveRootSymlink = &rrs
  260. }
  261. } else {
  262. if frankenphp.EmbeddedAppPath != "" && filepath.IsLocal(f.Root) {
  263. f.Root = filepath.Join(frankenphp.EmbeddedAppPath, f.Root)
  264. }
  265. }
  266. if len(f.SplitPath) == 0 {
  267. f.SplitPath = []string{".php"}
  268. }
  269. if f.ResolveRootSymlink == nil {
  270. rrs := true
  271. f.ResolveRootSymlink = &rrs
  272. }
  273. if !needReplacement(f.Root) {
  274. root, err := fastabs.FastAbs(f.Root)
  275. if err != nil {
  276. return fmt.Errorf("unable to make the root path absolute: %w", err)
  277. }
  278. f.resolvedDocumentRoot = root
  279. if *f.ResolveRootSymlink {
  280. root, err := filepath.EvalSymlinks(root)
  281. if err != nil {
  282. return fmt.Errorf("unable to resolve root symlink: %w", err)
  283. }
  284. f.resolvedDocumentRoot = root
  285. }
  286. }
  287. if f.preparedEnv == nil {
  288. f.preparedEnv = frankenphp.PrepareEnv(f.Env)
  289. for _, e := range f.preparedEnv {
  290. if needReplacement(e) {
  291. f.preparedEnvNeedsReplacement = true
  292. break
  293. }
  294. }
  295. }
  296. return nil
  297. }
  298. // needReplacement checks if a string contains placeholders.
  299. func needReplacement(s string) bool {
  300. return strings.Contains(s, "{") || strings.Contains(s, "}")
  301. }
  302. // ServeHTTP implements caddyhttp.MiddlewareHandler.
  303. // TODO: Expose TLS versions as env vars, as Apache's mod_ssl: https://github.com/caddyserver/caddy/blob/master/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go#L298
  304. func (f FrankenPHPModule) ServeHTTP(w http.ResponseWriter, r *http.Request, _ caddyhttp.Handler) error {
  305. origReq := r.Context().Value(caddyhttp.OriginalRequestCtxKey).(http.Request)
  306. repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
  307. var documentRootOption frankenphp.RequestOption
  308. if f.resolvedDocumentRoot == "" {
  309. documentRootOption = frankenphp.WithRequestDocumentRoot(repl.ReplaceKnown(f.Root, ""), *f.ResolveRootSymlink)
  310. } else {
  311. documentRootOption = frankenphp.WithRequestResolvedDocumentRoot(f.resolvedDocumentRoot)
  312. }
  313. env := f.preparedEnv
  314. if f.preparedEnvNeedsReplacement {
  315. env = make(frankenphp.PreparedEnv, len(f.Env))
  316. for k, v := range f.preparedEnv {
  317. env[k] = repl.ReplaceKnown(v, "")
  318. }
  319. }
  320. fr, err := frankenphp.NewRequestWithContext(
  321. r,
  322. documentRootOption,
  323. frankenphp.WithRequestSplitPath(f.SplitPath),
  324. frankenphp.WithRequestPreparedEnv(env),
  325. frankenphp.WithOriginalRequest(&origReq),
  326. )
  327. if err != nil {
  328. return caddyhttp.Error(http.StatusInternalServerError, err)
  329. }
  330. if err = frankenphp.ServeHTTP(w, fr); err != nil {
  331. return caddyhttp.Error(http.StatusInternalServerError, err)
  332. }
  333. return nil
  334. }
  335. // UnmarshalCaddyfile implements caddyfile.Unmarshaler.
  336. func (f *FrankenPHPModule) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
  337. for d.Next() {
  338. for d.NextBlock(0) {
  339. switch d.Val() {
  340. case "root":
  341. if !d.NextArg() {
  342. return d.ArgErr()
  343. }
  344. f.Root = d.Val()
  345. case "split":
  346. f.SplitPath = d.RemainingArgs()
  347. if len(f.SplitPath) == 0 {
  348. return d.ArgErr()
  349. }
  350. case "env":
  351. args := d.RemainingArgs()
  352. if len(args) != 2 {
  353. return d.ArgErr()
  354. }
  355. if f.Env == nil {
  356. f.Env = make(map[string]string)
  357. f.preparedEnv = make(frankenphp.PreparedEnv)
  358. }
  359. f.Env[args[0]] = args[1]
  360. f.preparedEnv[args[0]+"\x00"] = args[1]
  361. case "resolve_root_symlink":
  362. if !d.NextArg() {
  363. continue
  364. }
  365. v, err := strconv.ParseBool(d.Val())
  366. if err != nil {
  367. return err
  368. }
  369. if d.NextArg() {
  370. return d.ArgErr()
  371. }
  372. f.ResolveRootSymlink = &v
  373. }
  374. }
  375. }
  376. return nil
  377. }
  378. // parseCaddyfile unmarshals tokens from h into a new Middleware.
  379. func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
  380. m := FrankenPHPModule{}
  381. err := m.UnmarshalCaddyfile(h.Dispenser)
  382. return m, err
  383. }
  384. // parsePhpServer parses the php_server directive, which has a similar syntax
  385. // to the php_fastcgi directive. A line such as this:
  386. //
  387. // php_server
  388. //
  389. // is equivalent to a route consisting of:
  390. //
  391. // # Add trailing slash for directory requests
  392. // @canonicalPath {
  393. // file {path}/index.php
  394. // not path */
  395. // }
  396. // redir @canonicalPath {path}/ 308
  397. //
  398. // # If the requested file does not exist, try index files
  399. // @indexFiles file {
  400. // try_files {path} {path}/index.php index.php
  401. // split_path .php
  402. // }
  403. // rewrite @indexFiles {http.matchers.file.relative}
  404. //
  405. // # FrankenPHP!
  406. // @phpFiles path *.php
  407. // php @phpFiles
  408. // file_server
  409. //
  410. // parsePhpServer is freely inspired from the php_fastgci directive of the Caddy server (Apache License 2.0, Matthew Holt and The Caddy Authors)
  411. func parsePhpServer(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error) {
  412. if !h.Next() {
  413. return nil, h.ArgErr()
  414. }
  415. // set up FrankenPHP
  416. phpsrv := FrankenPHPModule{}
  417. // set up file server
  418. fsrv := fileserver.FileServer{}
  419. disableFsrv := false
  420. // set up the set of file extensions allowed to execute PHP code
  421. extensions := []string{".php"}
  422. // set the default index file for the try_files rewrites
  423. indexFile := "index.php"
  424. // set up for explicitly overriding try_files
  425. var tryFiles []string
  426. // if the user specified a matcher token, use that
  427. // matcher in a route that wraps both of our routes;
  428. // either way, strip the matcher token and pass
  429. // the remaining tokens to the unmarshaler so that
  430. // we can gain the rest of the directive syntax
  431. userMatcherSet, err := h.ExtractMatcherSet()
  432. if err != nil {
  433. return nil, err
  434. }
  435. // make a new dispenser from the remaining tokens so that we
  436. // can reset the dispenser back to this point for the
  437. // php unmarshaler to read from it as well
  438. dispenser := h.NewFromNextSegment()
  439. // read the subdirectives that we allow as overrides to
  440. // the php_server shortcut
  441. // NOTE: we delete the tokens as we go so that the php
  442. // unmarshal doesn't see these subdirectives which it cannot handle
  443. for dispenser.Next() {
  444. for dispenser.NextBlock(0) {
  445. // ignore any sub-subdirectives that might
  446. // have the same name somewhere within
  447. // the php passthrough tokens
  448. if dispenser.Nesting() != 1 {
  449. continue
  450. }
  451. // parse the php_server subdirectives
  452. switch dispenser.Val() {
  453. case "root":
  454. if !dispenser.NextArg() {
  455. return nil, dispenser.ArgErr()
  456. }
  457. phpsrv.Root = dispenser.Val()
  458. fsrv.Root = phpsrv.Root
  459. dispenser.DeleteN(2)
  460. case "split":
  461. extensions = dispenser.RemainingArgs()
  462. dispenser.DeleteN(len(extensions) + 1)
  463. if len(extensions) == 0 {
  464. return nil, dispenser.ArgErr()
  465. }
  466. case "index":
  467. args := dispenser.RemainingArgs()
  468. dispenser.DeleteN(len(args) + 1)
  469. if len(args) != 1 {
  470. return nil, dispenser.ArgErr()
  471. }
  472. indexFile = args[0]
  473. case "try_files":
  474. args := dispenser.RemainingArgs()
  475. dispenser.DeleteN(len(args) + 1)
  476. if len(args) < 1 {
  477. return nil, dispenser.ArgErr()
  478. }
  479. tryFiles = args
  480. case "file_server":
  481. args := dispenser.RemainingArgs()
  482. dispenser.DeleteN(len(args) + 1)
  483. if len(args) < 1 || args[0] != "off" {
  484. return nil, dispenser.ArgErr()
  485. }
  486. disableFsrv = true
  487. }
  488. }
  489. }
  490. // reset the dispenser after we're done so that the frankenphp
  491. // unmarshaler can read it from the start
  492. dispenser.Reset()
  493. if frankenphp.EmbeddedAppPath != "" {
  494. if phpsrv.Root == "" {
  495. phpsrv.Root = filepath.Join(frankenphp.EmbeddedAppPath, defaultDocumentRoot)
  496. fsrv.Root = phpsrv.Root
  497. rrs := false
  498. phpsrv.ResolveRootSymlink = &rrs
  499. } else if filepath.IsLocal(fsrv.Root) {
  500. phpsrv.Root = filepath.Join(frankenphp.EmbeddedAppPath, phpsrv.Root)
  501. fsrv.Root = phpsrv.Root
  502. }
  503. }
  504. // set up a route list that we'll append to
  505. routes := caddyhttp.RouteList{}
  506. // set the list of allowed path segments on which to split
  507. phpsrv.SplitPath = extensions
  508. // if the index is turned off, we skip the redirect and try_files
  509. if indexFile != "off" {
  510. dirRedir := false
  511. dirIndex := "{http.request.uri.path}/" + indexFile
  512. tryPolicy := "first_exist_fallback"
  513. // if tryFiles wasn't overridden, use a reasonable default
  514. if len(tryFiles) == 0 {
  515. if disableFsrv {
  516. tryFiles = []string{dirIndex, indexFile}
  517. } else {
  518. tryFiles = []string{"{http.request.uri.path}", dirIndex, indexFile}
  519. }
  520. dirRedir = true
  521. } else {
  522. if !strings.HasSuffix(tryFiles[len(tryFiles)-1], ".php") {
  523. // use first_exist strategy if the last file is not a PHP file
  524. tryPolicy = ""
  525. }
  526. for _, tf := range tryFiles {
  527. if tf == dirIndex {
  528. dirRedir = true
  529. break
  530. }
  531. }
  532. }
  533. // route to redirect to canonical path if index PHP file
  534. if dirRedir {
  535. redirMatcherSet := caddy.ModuleMap{
  536. "file": h.JSON(fileserver.MatchFile{
  537. TryFiles: []string{dirIndex},
  538. }),
  539. "not": h.JSON(caddyhttp.MatchNot{
  540. MatcherSetsRaw: []caddy.ModuleMap{
  541. {
  542. "path": h.JSON(caddyhttp.MatchPath{"*/"}),
  543. },
  544. },
  545. }),
  546. }
  547. redirHandler := caddyhttp.StaticResponse{
  548. StatusCode: caddyhttp.WeakString(strconv.Itoa(http.StatusPermanentRedirect)),
  549. Headers: http.Header{"Location": []string{"{http.request.orig_uri.path}/"}},
  550. }
  551. redirRoute := caddyhttp.Route{
  552. MatcherSetsRaw: []caddy.ModuleMap{redirMatcherSet},
  553. HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(redirHandler, "handler", "static_response", nil)},
  554. }
  555. routes = append(routes, redirRoute)
  556. }
  557. // route to rewrite to PHP index file
  558. rewriteMatcherSet := caddy.ModuleMap{
  559. "file": h.JSON(fileserver.MatchFile{
  560. TryFiles: tryFiles,
  561. TryPolicy: tryPolicy,
  562. SplitPath: extensions,
  563. }),
  564. }
  565. rewriteHandler := rewrite.Rewrite{
  566. URI: "{http.matchers.file.relative}",
  567. }
  568. rewriteRoute := caddyhttp.Route{
  569. MatcherSetsRaw: []caddy.ModuleMap{rewriteMatcherSet},
  570. HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(rewriteHandler, "handler", "rewrite", nil)},
  571. }
  572. routes = append(routes, rewriteRoute)
  573. }
  574. // route to actually pass requests to PHP files;
  575. // match only requests that are for PHP files
  576. var pathList []string
  577. for _, ext := range extensions {
  578. pathList = append(pathList, "*"+ext)
  579. }
  580. phpMatcherSet := caddy.ModuleMap{
  581. "path": h.JSON(pathList),
  582. }
  583. // the rest of the config is specified by the user
  584. // using the php directive syntax
  585. dispenser.Next() // consume the directive name
  586. err = phpsrv.UnmarshalCaddyfile(dispenser)
  587. if err != nil {
  588. return nil, err
  589. }
  590. // create the PHP route which is
  591. // conditional on matching PHP files
  592. phpRoute := caddyhttp.Route{
  593. MatcherSetsRaw: []caddy.ModuleMap{phpMatcherSet},
  594. HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(phpsrv, "handler", "php", nil)},
  595. }
  596. routes = append(routes, phpRoute)
  597. // create the file server route
  598. if !disableFsrv {
  599. fileRoute := caddyhttp.Route{
  600. MatcherSetsRaw: []caddy.ModuleMap{},
  601. HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(fsrv, "handler", "file_server", nil)},
  602. }
  603. routes = append(routes, fileRoute)
  604. }
  605. subroute := caddyhttp.Subroute{
  606. Routes: routes,
  607. }
  608. // the user's matcher is a prerequisite for ours, so
  609. // wrap ours in a subroute and return that
  610. if userMatcherSet != nil {
  611. return []httpcaddyfile.ConfigValue{
  612. {
  613. Class: "route",
  614. Value: caddyhttp.Route{
  615. MatcherSetsRaw: []caddy.ModuleMap{userMatcherSet},
  616. HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(subroute, "handler", "subroute", nil)},
  617. },
  618. },
  619. }, nil
  620. }
  621. // otherwise, return the literal subroute instead of
  622. // individual routes, to ensure they stay together and
  623. // are treated as a single unit, without necessarily
  624. // creating an actual subroute in the output
  625. return []httpcaddyfile.ConfigValue{
  626. {
  627. Class: "route",
  628. Value: subroute,
  629. },
  630. }, nil
  631. }
  632. // Interface guards
  633. var (
  634. _ caddy.App = (*FrankenPHPApp)(nil)
  635. _ caddy.Provisioner = (*FrankenPHPModule)(nil)
  636. _ caddyhttp.MiddlewareHandler = (*FrankenPHPModule)(nil)
  637. _ caddyfile.Unmarshaler = (*FrankenPHPModule)(nil)
  638. )