caddy.go 16 KB

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