caddy.go 18 KB

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