caddy.go 18 KB

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