caddy.go 16 KB

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