caddy.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599
  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 frankenphp.PreparedEnv `json:"env,omitempty"`
  174. logger *zap.Logger
  175. }
  176. // CaddyModule returns the Caddy module information.
  177. func (FrankenPHPModule) CaddyModule() caddy.ModuleInfo {
  178. return caddy.ModuleInfo{
  179. ID: "http.handlers.php",
  180. New: func() caddy.Module { return new(FrankenPHPModule) },
  181. }
  182. }
  183. // Provision sets up the module.
  184. func (f *FrankenPHPModule) Provision(ctx caddy.Context) error {
  185. f.logger = ctx.Logger(f)
  186. if f.Root == "" {
  187. if frankenphp.EmbeddedAppPath == "" {
  188. f.Root = "{http.vars.root}"
  189. } else {
  190. rrs := false
  191. f.Root = filepath.Join(frankenphp.EmbeddedAppPath, defaultDocumentRoot)
  192. f.ResolveRootSymlink = &rrs
  193. }
  194. } else {
  195. if frankenphp.EmbeddedAppPath != "" && filepath.IsLocal(f.Root) {
  196. f.Root = filepath.Join(frankenphp.EmbeddedAppPath, f.Root)
  197. }
  198. }
  199. if len(f.SplitPath) == 0 {
  200. f.SplitPath = []string{".php"}
  201. }
  202. if f.ResolveRootSymlink == nil {
  203. rrs := true
  204. f.ResolveRootSymlink = &rrs
  205. }
  206. return nil
  207. }
  208. // ServeHTTP implements caddyhttp.MiddlewareHandler.
  209. // 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
  210. func (f FrankenPHPModule) ServeHTTP(w http.ResponseWriter, r *http.Request, _ caddyhttp.Handler) error {
  211. origReq := r.Context().Value(caddyhttp.OriginalRequestCtxKey).(http.Request)
  212. repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
  213. documentRoot := repl.ReplaceKnown(f.Root, "")
  214. env := make(map[string]string, len(f.Env)+1)
  215. env["REQUEST_URI\x00"] = origReq.URL.RequestURI()
  216. for k, v := range f.Env {
  217. env[k] = repl.ReplaceKnown(v, "")
  218. }
  219. fr, err := frankenphp.NewRequestWithContext(
  220. r,
  221. frankenphp.WithRequestDocumentRoot(documentRoot, *f.ResolveRootSymlink),
  222. frankenphp.WithRequestSplitPath(f.SplitPath),
  223. frankenphp.WithRequestPreparedEnv(env),
  224. )
  225. if err != nil {
  226. return err
  227. }
  228. return frankenphp.ServeHTTP(w, fr)
  229. }
  230. // UnmarshalCaddyfile implements caddyfile.Unmarshaler.
  231. func (f *FrankenPHPModule) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
  232. for d.Next() {
  233. for d.NextBlock(0) {
  234. switch d.Val() {
  235. case "root":
  236. if !d.NextArg() {
  237. return d.ArgErr()
  238. }
  239. f.Root = d.Val()
  240. case "split":
  241. f.SplitPath = d.RemainingArgs()
  242. if len(f.SplitPath) == 0 {
  243. return d.ArgErr()
  244. }
  245. case "env":
  246. args := d.RemainingArgs()
  247. if len(args) != 2 {
  248. return d.ArgErr()
  249. }
  250. if f.Env == nil {
  251. f.Env = make(frankenphp.PreparedEnv)
  252. }
  253. f.Env[args[0]+"\x00"] = args[1]
  254. case "resolve_root_symlink":
  255. if d.NextArg() {
  256. if v, err := strconv.ParseBool(d.Val()); err == nil {
  257. f.ResolveRootSymlink = &v
  258. if d.NextArg() {
  259. return d.ArgErr()
  260. }
  261. }
  262. return d.ArgErr()
  263. }
  264. rrs := true
  265. f.ResolveRootSymlink = &rrs
  266. }
  267. }
  268. }
  269. return nil
  270. }
  271. // parseCaddyfile unmarshals tokens from h into a new Middleware.
  272. func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
  273. m := FrankenPHPModule{}
  274. err := m.UnmarshalCaddyfile(h.Dispenser)
  275. return m, err
  276. }
  277. // parsePhpServer parses the php_server directive, which has a similar syntax
  278. // to the php_fastcgi directive. A line such as this:
  279. //
  280. // php_server
  281. //
  282. // is equivalent to a route consisting of:
  283. //
  284. // # Add trailing slash for directory requests
  285. // @canonicalPath {
  286. // file {path}/index.php
  287. // not path */
  288. // }
  289. // redir @canonicalPath {path}/ 308
  290. //
  291. // # If the requested file does not exist, try index files
  292. // @indexFiles file {
  293. // try_files {path} {path}/index.php index.php
  294. // split_path .php
  295. // }
  296. // rewrite @indexFiles {http.matchers.file.relative}
  297. //
  298. // # FrankenPHP!
  299. // @phpFiles path *.php
  300. // php @phpFiles
  301. // file_server
  302. //
  303. // parsePhpServer is freely inspired from the php_fastgci directive of the Caddy server (Apache License 2.0, Matthew Holt and The Caddy Authors)
  304. func parsePhpServer(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error) {
  305. if !h.Next() {
  306. return nil, h.ArgErr()
  307. }
  308. // set up FrankenPHP
  309. phpsrv := FrankenPHPModule{}
  310. // set up file server
  311. fsrv := fileserver.FileServer{}
  312. disableFsrv := false
  313. // set up the set of file extensions allowed to execute PHP code
  314. extensions := []string{".php"}
  315. // set the default index file for the try_files rewrites
  316. indexFile := "index.php"
  317. // set up for explicitly overriding try_files
  318. tryFiles := []string{}
  319. // if the user specified a matcher token, use that
  320. // matcher in a route that wraps both of our routes;
  321. // either way, strip the matcher token and pass
  322. // the remaining tokens to the unmarshaler so that
  323. // we can gain the rest of the directive syntax
  324. userMatcherSet, err := h.ExtractMatcherSet()
  325. if err != nil {
  326. return nil, err
  327. }
  328. // make a new dispenser from the remaining tokens so that we
  329. // can reset the dispenser back to this point for the
  330. // php unmarshaler to read from it as well
  331. dispenser := h.NewFromNextSegment()
  332. // read the subdirectives that we allow as overrides to
  333. // the php_server shortcut
  334. // NOTE: we delete the tokens as we go so that the php
  335. // unmarshal doesn't see these subdirectives which it cannot handle
  336. for dispenser.Next() {
  337. for dispenser.NextBlock(0) {
  338. // ignore any sub-subdirectives that might
  339. // have the same name somewhere within
  340. // the php passthrough tokens
  341. if dispenser.Nesting() != 1 {
  342. continue
  343. }
  344. // parse the php_server subdirectives
  345. switch dispenser.Val() {
  346. case "root":
  347. if !dispenser.NextArg() {
  348. return nil, dispenser.ArgErr()
  349. }
  350. phpsrv.Root = dispenser.Val()
  351. fsrv.Root = phpsrv.Root
  352. dispenser.DeleteN(2)
  353. case "split":
  354. extensions = dispenser.RemainingArgs()
  355. dispenser.DeleteN(len(extensions) + 1)
  356. if len(extensions) == 0 {
  357. return nil, dispenser.ArgErr()
  358. }
  359. case "index":
  360. args := dispenser.RemainingArgs()
  361. dispenser.DeleteN(len(args) + 1)
  362. if len(args) != 1 {
  363. return nil, dispenser.ArgErr()
  364. }
  365. indexFile = args[0]
  366. case "try_files":
  367. args := dispenser.RemainingArgs()
  368. dispenser.DeleteN(len(args) + 1)
  369. if len(args) < 1 {
  370. return nil, dispenser.ArgErr()
  371. }
  372. tryFiles = args
  373. case "file_server":
  374. args := dispenser.RemainingArgs()
  375. dispenser.DeleteN(len(args) + 1)
  376. if len(args) < 1 || args[0] != "off" {
  377. return nil, dispenser.ArgErr()
  378. }
  379. disableFsrv = true
  380. }
  381. }
  382. }
  383. // reset the dispenser after we're done so that the frankenphp
  384. // unmarshaler can read it from the start
  385. dispenser.Reset()
  386. if frankenphp.EmbeddedAppPath != "" {
  387. if phpsrv.Root == "" {
  388. phpsrv.Root = filepath.Join(frankenphp.EmbeddedAppPath, defaultDocumentRoot)
  389. fsrv.Root = phpsrv.Root
  390. rrs := false
  391. phpsrv.ResolveRootSymlink = &rrs
  392. } else if filepath.IsLocal(fsrv.Root) {
  393. phpsrv.Root = filepath.Join(frankenphp.EmbeddedAppPath, phpsrv.Root)
  394. fsrv.Root = phpsrv.Root
  395. }
  396. }
  397. // set up a route list that we'll append to
  398. routes := caddyhttp.RouteList{}
  399. // set the list of allowed path segments on which to split
  400. phpsrv.SplitPath = extensions
  401. // if the index is turned off, we skip the redirect and try_files
  402. if indexFile != "off" {
  403. // route to redirect to canonical path if index PHP file
  404. redirMatcherSet := caddy.ModuleMap{
  405. "file": h.JSON(fileserver.MatchFile{
  406. TryFiles: []string{"{http.request.uri.path}/" + indexFile},
  407. }),
  408. "not": h.JSON(caddyhttp.MatchNot{
  409. MatcherSetsRaw: []caddy.ModuleMap{
  410. {
  411. "path": h.JSON(caddyhttp.MatchPath{"*/"}),
  412. },
  413. },
  414. }),
  415. }
  416. redirHandler := caddyhttp.StaticResponse{
  417. StatusCode: caddyhttp.WeakString(strconv.Itoa(http.StatusPermanentRedirect)),
  418. Headers: http.Header{"Location": []string{"{http.request.orig_uri.path}/"}},
  419. }
  420. redirRoute := caddyhttp.Route{
  421. MatcherSetsRaw: []caddy.ModuleMap{redirMatcherSet},
  422. HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(redirHandler, "handler", "static_response", nil)},
  423. }
  424. // if tryFiles wasn't overridden, use a reasonable default
  425. if len(tryFiles) == 0 {
  426. tryFiles = []string{"{http.request.uri.path}", "{http.request.uri.path}/" + indexFile, indexFile}
  427. }
  428. // route to rewrite to PHP index file
  429. rewriteMatcherSet := caddy.ModuleMap{
  430. "file": h.JSON(fileserver.MatchFile{
  431. TryFiles: tryFiles,
  432. SplitPath: extensions,
  433. }),
  434. }
  435. rewriteHandler := rewrite.Rewrite{
  436. URI: "{http.matchers.file.relative}",
  437. }
  438. rewriteRoute := caddyhttp.Route{
  439. MatcherSetsRaw: []caddy.ModuleMap{rewriteMatcherSet},
  440. HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(rewriteHandler, "handler", "rewrite", nil)},
  441. }
  442. routes = append(routes, redirRoute, rewriteRoute)
  443. }
  444. // route to actually pass requests to PHP files;
  445. // match only requests that are for PHP files
  446. pathList := []string{}
  447. for _, ext := range extensions {
  448. pathList = append(pathList, "*"+ext)
  449. }
  450. phpMatcherSet := caddy.ModuleMap{
  451. "path": h.JSON(pathList),
  452. }
  453. // the rest of the config is specified by the user
  454. // using the php directive syntax
  455. dispenser.Next() // consume the directive name
  456. err = phpsrv.UnmarshalCaddyfile(dispenser)
  457. if err != nil {
  458. return nil, err
  459. }
  460. // create the PHP route which is
  461. // conditional on matching PHP files
  462. phpRoute := caddyhttp.Route{
  463. MatcherSetsRaw: []caddy.ModuleMap{phpMatcherSet},
  464. HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(phpsrv, "handler", "php", nil)},
  465. }
  466. routes = append(routes, phpRoute)
  467. // create the file server route
  468. if !disableFsrv {
  469. fileRoute := caddyhttp.Route{
  470. MatcherSetsRaw: []caddy.ModuleMap{},
  471. HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(fsrv, "handler", "file_server", nil)},
  472. }
  473. routes = append(routes, fileRoute)
  474. }
  475. subroute := caddyhttp.Subroute{
  476. Routes: routes,
  477. }
  478. // the user's matcher is a prerequisite for ours, so
  479. // wrap ours in a subroute and return that
  480. if userMatcherSet != nil {
  481. return []httpcaddyfile.ConfigValue{
  482. {
  483. Class: "route",
  484. Value: caddyhttp.Route{
  485. MatcherSetsRaw: []caddy.ModuleMap{userMatcherSet},
  486. HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(subroute, "handler", "subroute", nil)},
  487. },
  488. },
  489. }, nil
  490. }
  491. // otherwise, return the literal subroute instead of
  492. // individual routes, to ensure they stay together and
  493. // are treated as a single unit, without necessarily
  494. // creating an actual subroute in the output
  495. return []httpcaddyfile.ConfigValue{
  496. {
  497. Class: "route",
  498. Value: subroute,
  499. },
  500. }, nil
  501. }
  502. // Interface guards
  503. var (
  504. _ caddy.App = (*FrankenPHPApp)(nil)
  505. _ caddy.Provisioner = (*FrankenPHPModule)(nil)
  506. _ caddyhttp.MiddlewareHandler = (*FrankenPHPModule)(nil)
  507. _ caddyfile.Unmarshaler = (*FrankenPHPModule)(nil)
  508. )