caddy.go 18 KB

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