caddy.go 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  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. "errors"
  7. "net/http"
  8. "strconv"
  9. "github.com/caddyserver/caddy/v2"
  10. "github.com/caddyserver/caddy/v2/caddyconfig"
  11. "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
  12. "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
  13. "github.com/caddyserver/caddy/v2/modules/caddyhttp"
  14. "github.com/dunglas/frankenphp"
  15. "go.uber.org/zap"
  16. )
  17. func init() {
  18. caddy.RegisterModule(FrankenPHPApp{})
  19. caddy.RegisterModule(FrankenPHPModule{})
  20. httpcaddyfile.RegisterGlobalOption("frankenphp", parseGlobalOption)
  21. httpcaddyfile.RegisterHandlerDirective("php", parseCaddyfile)
  22. }
  23. type mainPHPinterpreterKeyType int
  24. var mainPHPInterpreterKey mainPHPinterpreterKeyType
  25. var phpInterpreter = caddy.NewUsagePool()
  26. type phpInterpreterDestructor struct{}
  27. func (phpInterpreterDestructor) Destruct() error {
  28. frankenphp.Shutdown()
  29. return nil
  30. }
  31. type workerConfig struct {
  32. // FileName sets the path to the worker script.
  33. FileName string `json:"file_name,omitempty"`
  34. // Num sets the number of workers to start.
  35. Num int `json:"num,omitempty"`
  36. // Env sets an extra environment variable to the given value. Can be specified more than once for multiple environment variables.
  37. Env map[string]string `json:"env,omitempty"`
  38. }
  39. type FrankenPHPApp struct {
  40. // NumThreads sets the number of PHP threads to start. Default: 2x the number of available CPUs.
  41. NumThreads int `json:"num_threads,omitempty"`
  42. // Workers configures the worker scripts to start.
  43. Workers []workerConfig `json:"workers,omitempty"`
  44. }
  45. // CaddyModule returns the Caddy module information.
  46. func (a FrankenPHPApp) CaddyModule() caddy.ModuleInfo {
  47. return caddy.ModuleInfo{
  48. ID: "frankenphp",
  49. New: func() caddy.Module { return a },
  50. }
  51. }
  52. func (f *FrankenPHPApp) Start() error {
  53. repl := caddy.NewReplacer()
  54. logger := caddy.Log()
  55. opts := []frankenphp.Option{frankenphp.WithNumThreads(f.NumThreads), frankenphp.WithLogger(logger)}
  56. for _, w := range f.Workers {
  57. opts = append(opts, frankenphp.WithWorkers(repl.ReplaceKnown(w.FileName, ""), w.Num, w.Env))
  58. }
  59. _, loaded, err := phpInterpreter.LoadOrNew(mainPHPInterpreterKey, func() (caddy.Destructor, error) {
  60. if err := frankenphp.Init(opts...); err != nil {
  61. return nil, err
  62. }
  63. return phpInterpreterDestructor{}, nil
  64. })
  65. if err != nil {
  66. return err
  67. }
  68. if loaded {
  69. frankenphp.Shutdown()
  70. if err := frankenphp.Init(opts...); err != nil {
  71. return err
  72. }
  73. }
  74. logger.Info("FrankenPHP started 🐘", zap.String("php_version", frankenphp.Version().Version))
  75. return nil
  76. }
  77. func (*FrankenPHPApp) Stop() error {
  78. caddy.Log().Info("FrankenPHP stopped 🐘")
  79. return nil
  80. }
  81. // UnmarshalCaddyfile implements caddyfile.Unmarshaler.
  82. func (f *FrankenPHPApp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
  83. for d.Next() {
  84. for d.NextBlock(0) {
  85. switch d.Val() {
  86. case "num_threads":
  87. if !d.NextArg() {
  88. return d.ArgErr()
  89. }
  90. v, err := strconv.Atoi(d.Val())
  91. if err != nil {
  92. return err
  93. }
  94. f.NumThreads = v
  95. case "worker":
  96. wc := workerConfig{}
  97. if d.NextArg() {
  98. wc.FileName = d.Val()
  99. }
  100. if d.NextArg() {
  101. v, err := strconv.Atoi(d.Val())
  102. if err != nil {
  103. return err
  104. }
  105. wc.Num = v
  106. }
  107. for d.NextBlock(1) {
  108. v := d.Val()
  109. switch v {
  110. case "file":
  111. if !d.NextArg() {
  112. return d.ArgErr()
  113. }
  114. wc.FileName = d.Val()
  115. case "num":
  116. if !d.NextArg() {
  117. return d.ArgErr()
  118. }
  119. v, err := strconv.Atoi(d.Val())
  120. if err != nil {
  121. return err
  122. }
  123. wc.Num = v
  124. case "env":
  125. args := d.RemainingArgs()
  126. if len(args) != 2 {
  127. return d.ArgErr()
  128. }
  129. if wc.Env == nil {
  130. wc.Env = make(map[string]string)
  131. }
  132. wc.Env[args[0]] = args[1]
  133. }
  134. if wc.FileName == "" {
  135. return errors.New(`The "file" argument must be specified`)
  136. }
  137. }
  138. f.Workers = append(f.Workers, wc)
  139. }
  140. }
  141. }
  142. return nil
  143. }
  144. func parseGlobalOption(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
  145. app := &FrankenPHPApp{}
  146. if err := app.UnmarshalCaddyfile(d); err != nil {
  147. return nil, err
  148. }
  149. // tell Caddyfile adapter that this is the JSON for an app
  150. return httpcaddyfile.App{
  151. Name: "frankenphp",
  152. Value: caddyconfig.JSON(app, nil),
  153. }, nil
  154. }
  155. type FrankenPHPModule struct {
  156. // Root sets the root folder to the site. Default: `root` directive.
  157. Root string `json:"root,omitempty"`
  158. // 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`.
  159. SplitPath []string `json:"split_path,omitempty"`
  160. // ResolveRootSymlink enables resolving the `root` directory to its actual value by evaluating a symbolic link, if one exists.
  161. ResolveRootSymlink bool `json:"resolve_root_symlink,omitempty"`
  162. // Env sets an extra environment variable to the given value. Can be specified more than once for multiple environment variables.
  163. Env map[string]string `json:"env,omitempty"`
  164. logger *zap.Logger
  165. }
  166. // CaddyModule returns the Caddy module information.
  167. func (FrankenPHPModule) CaddyModule() caddy.ModuleInfo {
  168. return caddy.ModuleInfo{
  169. ID: "http.handlers.php",
  170. New: func() caddy.Module { return new(FrankenPHPModule) },
  171. }
  172. }
  173. // Provision sets up the module.
  174. func (f *FrankenPHPModule) Provision(ctx caddy.Context) error {
  175. f.logger = ctx.Logger(f)
  176. if f.Root == "" {
  177. f.Root = "{http.vars.root}"
  178. }
  179. if len(f.SplitPath) == 0 {
  180. f.SplitPath = []string{".php"}
  181. }
  182. return nil
  183. }
  184. // ServeHTTP implements caddyhttp.MiddlewareHandler.
  185. // 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
  186. func (f FrankenPHPModule) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
  187. origReq := r.Context().Value(caddyhttp.OriginalRequestCtxKey).(http.Request)
  188. repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
  189. documentRoot := repl.ReplaceKnown(f.Root, "")
  190. fr := frankenphp.NewRequestWithContext(r, documentRoot, f.logger)
  191. fc, _ := frankenphp.FromContext(fr.Context())
  192. fc.ResolveRootSymlink = f.ResolveRootSymlink
  193. fc.SplitPath = f.SplitPath
  194. fc.Env["REQUEST_URI"] = origReq.URL.RequestURI()
  195. for k, v := range f.Env {
  196. fc.Env[k] = repl.ReplaceKnown(v, "")
  197. }
  198. return frankenphp.ServeHTTP(w, fr)
  199. }
  200. // UnmarshalCaddyfile implements caddyfile.Unmarshaler.
  201. func (f *FrankenPHPModule) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
  202. for d.Next() {
  203. for d.NextBlock(0) {
  204. switch d.Val() {
  205. case "root":
  206. if !d.NextArg() {
  207. return d.ArgErr()
  208. }
  209. f.Root = d.Val()
  210. case "split":
  211. f.SplitPath = d.RemainingArgs()
  212. if len(f.SplitPath) == 0 {
  213. return d.ArgErr()
  214. }
  215. case "env":
  216. args := d.RemainingArgs()
  217. if len(args) != 2 {
  218. return d.ArgErr()
  219. }
  220. if f.Env == nil {
  221. f.Env = make(map[string]string)
  222. }
  223. f.Env[args[0]] = args[1]
  224. case "resolve_root_symlink":
  225. if d.NextArg() {
  226. return d.ArgErr()
  227. }
  228. f.ResolveRootSymlink = true
  229. }
  230. }
  231. }
  232. return nil
  233. }
  234. // parseCaddyfile unmarshals tokens from h into a new Middleware.
  235. func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
  236. var m FrankenPHPModule
  237. err := m.UnmarshalCaddyfile(h.Dispenser)
  238. return m, err
  239. }
  240. // Interface guards
  241. var (
  242. _ caddy.App = (*FrankenPHPApp)(nil)
  243. _ caddy.Provisioner = (*FrankenPHPModule)(nil)
  244. _ caddyhttp.MiddlewareHandler = (*FrankenPHPModule)(nil)
  245. _ caddyfile.Unmarshaler = (*FrankenPHPModule)(nil)
  246. )