caddy.go 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. // Package frankenphp provides a PHP module for Caddy.
  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. "log"
  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. log.Print("Destructor called")
  29. frankenphp.Shutdown()
  30. return nil
  31. }
  32. type workerConfig struct {
  33. FileName string `json:"file_name,omitempty"`
  34. Num int `json:"num,omitempty"`
  35. }
  36. type FrankenPHPApp struct {
  37. NumThreads int `json:"num_threads,omitempty"`
  38. Workers []workerConfig `json:"workers,omitempty"`
  39. }
  40. // CaddyModule returns the Caddy module information.
  41. func (a *FrankenPHPApp) CaddyModule() caddy.ModuleInfo {
  42. return caddy.ModuleInfo{
  43. ID: "frankenphp",
  44. New: func() caddy.Module { return a },
  45. }
  46. }
  47. func (f *FrankenPHPApp) Start() error {
  48. var opts []frankenphp.Option
  49. if f.NumThreads != 0 {
  50. opts = append(opts, frankenphp.WithNumThreads(f.NumThreads))
  51. }
  52. for _, w := range f.Workers {
  53. num := 1
  54. if w.Num > 1 {
  55. num = w.Num
  56. }
  57. opts = append(opts, frankenphp.WithWorkers(w.FileName, num))
  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. log.Print("FrankenPHP started")
  75. return nil
  76. }
  77. func (*FrankenPHPApp) Stop() error {
  78. //frankenphp.Shutdown()
  79. log.Print("FrankenPHP stopped")
  80. return nil
  81. }
  82. // UnmarshalCaddyfile implements caddyfile.Unmarshaler.
  83. func (f *FrankenPHPApp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
  84. for d.Next() {
  85. for d.NextBlock(0) {
  86. switch d.Val() {
  87. case "num_threads":
  88. if !d.NextArg() {
  89. return d.ArgErr()
  90. }
  91. v, err := strconv.Atoi(d.Val())
  92. if err != nil {
  93. return err
  94. }
  95. f.NumThreads = v
  96. case "worker":
  97. if !d.NextArg() {
  98. return d.ArgErr()
  99. }
  100. wc := workerConfig{FileName: d.Val()}
  101. if d.NextArg() {
  102. v, err := strconv.Atoi(d.Val())
  103. if err != nil {
  104. return err
  105. }
  106. wc.Num = v
  107. }
  108. f.Workers = append(f.Workers, wc)
  109. }
  110. }
  111. }
  112. return nil
  113. }
  114. func parseGlobalOption(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
  115. app := &FrankenPHPApp{}
  116. if err := app.UnmarshalCaddyfile(d); err != nil {
  117. return nil, err
  118. }
  119. // tell Caddyfile adapter that this is the JSON for an app
  120. return httpcaddyfile.App{
  121. Name: "frankenphp",
  122. Value: caddyconfig.JSON(app, nil),
  123. }, nil
  124. }
  125. type FrankenPHPModule struct {
  126. Root string `json:"root,omitempty"`
  127. SplitPath []string `json:"split_path,omitempty"`
  128. ResolveRootSymlink bool `json:"resolve_root_symlink,omitempty"`
  129. Env map[string]string `json:"env,omitempty"`
  130. logger *zap.Logger
  131. }
  132. // CaddyModule returns the Caddy module information.
  133. func (FrankenPHPModule) CaddyModule() caddy.ModuleInfo {
  134. return caddy.ModuleInfo{
  135. ID: "http.handlers.php",
  136. New: func() caddy.Module { return new(FrankenPHPModule) },
  137. }
  138. }
  139. // Provision sets up the module.
  140. func (f *FrankenPHPModule) Provision(ctx caddy.Context) error {
  141. f.logger = ctx.Logger(f)
  142. if f.Root == "" {
  143. f.Root = "{http.vars.root}"
  144. }
  145. if len(f.SplitPath) == 0 {
  146. f.SplitPath = []string{".php"}
  147. }
  148. return nil
  149. }
  150. // ServeHTTP implements caddyhttp.MiddlewareHandler.
  151. // 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
  152. func (f FrankenPHPModule) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
  153. origReq := r.Context().Value(caddyhttp.OriginalRequestCtxKey).(http.Request)
  154. repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
  155. documentRoot := repl.ReplaceKnown(f.Root, "")
  156. fr := frankenphp.NewRequestWithContext(r, documentRoot)
  157. fc, _ := frankenphp.FromContext(fr.Context())
  158. fc.ResolveRootSymlink = f.ResolveRootSymlink
  159. fc.SplitPath = f.SplitPath
  160. fc.Env["REQUEST_URI"] = origReq.URL.RequestURI()
  161. for k, v := range f.Env {
  162. fc.Env[k] = repl.ReplaceKnown(v, "")
  163. }
  164. return frankenphp.ServeHTTP(w, fr)
  165. }
  166. // UnmarshalCaddyfile implements caddyfile.Unmarshaler.
  167. func (f *FrankenPHPModule) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
  168. for d.Next() {
  169. for d.NextBlock(0) {
  170. switch d.Val() {
  171. case "root":
  172. if !d.NextArg() {
  173. return d.ArgErr()
  174. }
  175. f.Root = d.Val()
  176. case "split":
  177. f.SplitPath = d.RemainingArgs()
  178. if len(f.SplitPath) == 0 {
  179. return d.ArgErr()
  180. }
  181. case "env":
  182. args := d.RemainingArgs()
  183. if len(args) != 2 {
  184. return d.ArgErr()
  185. }
  186. if f.Env == nil {
  187. f.Env = make(map[string]string)
  188. }
  189. f.Env[args[0]] = args[1]
  190. case "resolve_root_symlink":
  191. if d.NextArg() {
  192. return d.ArgErr()
  193. }
  194. f.ResolveRootSymlink = true
  195. }
  196. }
  197. }
  198. return nil
  199. }
  200. // parseCaddyfile unmarshals tokens from h into a new Middleware.
  201. func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
  202. var m FrankenPHPModule
  203. err := m.UnmarshalCaddyfile(h.Dispenser)
  204. return m, err
  205. }
  206. // Interface guards
  207. var (
  208. _ caddy.App = (*FrankenPHPApp)(nil)
  209. _ caddy.Provisioner = (*FrankenPHPModule)(nil)
  210. _ caddyhttp.MiddlewareHandler = (*FrankenPHPModule)(nil)
  211. _ caddyfile.Unmarshaler = (*FrankenPHPModule)(nil)
  212. )