caddy.go 16 KB

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