123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298 |
- // Package caddy provides a PHP module for the Caddy web server.
- // FrankenPHP embeds the PHP interpreter directly in Caddy, giving it the ability to run your PHP scripts directly.
- // No PHP FPM required!
- package caddy
- import (
- "errors"
- "net/http"
- "strconv"
- "github.com/caddyserver/caddy/v2"
- "github.com/caddyserver/caddy/v2/caddyconfig"
- "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
- "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
- "github.com/caddyserver/caddy/v2/modules/caddyhttp"
- "github.com/dunglas/frankenphp"
- "go.uber.org/zap"
- )
- func init() {
- caddy.RegisterModule(FrankenPHPApp{})
- caddy.RegisterModule(FrankenPHPModule{})
- httpcaddyfile.RegisterGlobalOption("frankenphp", parseGlobalOption)
- httpcaddyfile.RegisterHandlerDirective("php", parseCaddyfile)
- }
- type mainPHPinterpreterKeyType int
- var mainPHPInterpreterKey mainPHPinterpreterKeyType
- var phpInterpreter = caddy.NewUsagePool()
- type phpInterpreterDestructor struct{}
- func (phpInterpreterDestructor) Destruct() error {
- frankenphp.Shutdown()
- return nil
- }
- type workerConfig struct {
- // FileName sets the path to the worker script.
- FileName string `json:"file_name,omitempty"`
- // Num sets the number of workers to start.
- Num int `json:"num,omitempty"`
- // Env sets an extra environment variable to the given value. Can be specified more than once for multiple environment variables.
- Env map[string]string `json:"env,omitempty"`
- }
- type FrankenPHPApp struct {
- // NumThreads sets the number of PHP threads to start. Default: 2x the number of available CPUs.
- NumThreads int `json:"num_threads,omitempty"`
- // Workers configures the worker scripts to start.
- Workers []workerConfig `json:"workers,omitempty"`
- }
- // CaddyModule returns the Caddy module information.
- func (a FrankenPHPApp) CaddyModule() caddy.ModuleInfo {
- return caddy.ModuleInfo{
- ID: "frankenphp",
- New: func() caddy.Module { return a },
- }
- }
- func (f *FrankenPHPApp) Start() error {
- repl := caddy.NewReplacer()
- logger := caddy.Log()
- opts := []frankenphp.Option{frankenphp.WithNumThreads(f.NumThreads), frankenphp.WithLogger(logger)}
- for _, w := range f.Workers {
- opts = append(opts, frankenphp.WithWorkers(repl.ReplaceKnown(w.FileName, ""), w.Num, w.Env))
- }
- _, loaded, err := phpInterpreter.LoadOrNew(mainPHPInterpreterKey, func() (caddy.Destructor, error) {
- if err := frankenphp.Init(opts...); err != nil {
- return nil, err
- }
- return phpInterpreterDestructor{}, nil
- })
- if err != nil {
- return err
- }
- if loaded {
- frankenphp.Shutdown()
- if err := frankenphp.Init(opts...); err != nil {
- return err
- }
- }
- logger.Info("FrankenPHP started 🐘", zap.String("php_version", frankenphp.Version().Version))
- return nil
- }
- func (*FrankenPHPApp) Stop() error {
- caddy.Log().Info("FrankenPHP stopped 🐘")
- return nil
- }
- // UnmarshalCaddyfile implements caddyfile.Unmarshaler.
- func (f *FrankenPHPApp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
- for d.Next() {
- for d.NextBlock(0) {
- switch d.Val() {
- case "num_threads":
- if !d.NextArg() {
- return d.ArgErr()
- }
- v, err := strconv.Atoi(d.Val())
- if err != nil {
- return err
- }
- f.NumThreads = v
- case "worker":
- wc := workerConfig{}
- if d.NextArg() {
- wc.FileName = d.Val()
- }
- if d.NextArg() {
- v, err := strconv.Atoi(d.Val())
- if err != nil {
- return err
- }
- wc.Num = v
- }
- for d.NextBlock(1) {
- v := d.Val()
- switch v {
- case "file":
- if !d.NextArg() {
- return d.ArgErr()
- }
- wc.FileName = d.Val()
- case "num":
- if !d.NextArg() {
- return d.ArgErr()
- }
- v, err := strconv.Atoi(d.Val())
- if err != nil {
- return err
- }
- wc.Num = v
- case "env":
- args := d.RemainingArgs()
- if len(args) != 2 {
- return d.ArgErr()
- }
- if wc.Env == nil {
- wc.Env = make(map[string]string)
- }
- wc.Env[args[0]] = args[1]
- }
- if wc.FileName == "" {
- return errors.New(`The "file" argument must be specified`)
- }
- }
- f.Workers = append(f.Workers, wc)
- }
- }
- }
- return nil
- }
- func parseGlobalOption(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
- app := &FrankenPHPApp{}
- if err := app.UnmarshalCaddyfile(d); err != nil {
- return nil, err
- }
- // tell Caddyfile adapter that this is the JSON for an app
- return httpcaddyfile.App{
- Name: "frankenphp",
- Value: caddyconfig.JSON(app, nil),
- }, nil
- }
- type FrankenPHPModule struct {
- // Root sets the root folder to the site. Default: `root` directive.
- Root string `json:"root,omitempty"`
- // 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`.
- SplitPath []string `json:"split_path,omitempty"`
- // ResolveRootSymlink enables resolving the `root` directory to its actual value by evaluating a symbolic link, if one exists.
- ResolveRootSymlink bool `json:"resolve_root_symlink,omitempty"`
- // Env sets an extra environment variable to the given value. Can be specified more than once for multiple environment variables.
- Env map[string]string `json:"env,omitempty"`
- logger *zap.Logger
- }
- // CaddyModule returns the Caddy module information.
- func (FrankenPHPModule) CaddyModule() caddy.ModuleInfo {
- return caddy.ModuleInfo{
- ID: "http.handlers.php",
- New: func() caddy.Module { return new(FrankenPHPModule) },
- }
- }
- // Provision sets up the module.
- func (f *FrankenPHPModule) Provision(ctx caddy.Context) error {
- f.logger = ctx.Logger(f)
- if f.Root == "" {
- f.Root = "{http.vars.root}"
- }
- if len(f.SplitPath) == 0 {
- f.SplitPath = []string{".php"}
- }
- return nil
- }
- // ServeHTTP implements caddyhttp.MiddlewareHandler.
- // 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
- func (f FrankenPHPModule) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
- origReq := r.Context().Value(caddyhttp.OriginalRequestCtxKey).(http.Request)
- repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
- documentRoot := repl.ReplaceKnown(f.Root, "")
- fr := frankenphp.NewRequestWithContext(r, documentRoot, f.logger)
- fc, _ := frankenphp.FromContext(fr.Context())
- fc.ResolveRootSymlink = f.ResolveRootSymlink
- fc.SplitPath = f.SplitPath
- fc.Env["REQUEST_URI"] = origReq.URL.RequestURI()
- for k, v := range f.Env {
- fc.Env[k] = repl.ReplaceKnown(v, "")
- }
- return frankenphp.ServeHTTP(w, fr)
- }
- // UnmarshalCaddyfile implements caddyfile.Unmarshaler.
- func (f *FrankenPHPModule) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
- for d.Next() {
- for d.NextBlock(0) {
- switch d.Val() {
- case "root":
- if !d.NextArg() {
- return d.ArgErr()
- }
- f.Root = d.Val()
- case "split":
- f.SplitPath = d.RemainingArgs()
- if len(f.SplitPath) == 0 {
- return d.ArgErr()
- }
- case "env":
- args := d.RemainingArgs()
- if len(args) != 2 {
- return d.ArgErr()
- }
- if f.Env == nil {
- f.Env = make(map[string]string)
- }
- f.Env[args[0]] = args[1]
- case "resolve_root_symlink":
- if d.NextArg() {
- return d.ArgErr()
- }
- f.ResolveRootSymlink = true
- }
- }
- }
- return nil
- }
- // parseCaddyfile unmarshals tokens from h into a new Middleware.
- func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
- var m FrankenPHPModule
- err := m.UnmarshalCaddyfile(h.Dispenser)
- return m, err
- }
- // Interface guards
- var (
- _ caddy.App = (*FrankenPHPApp)(nil)
- _ caddy.Provisioner = (*FrankenPHPModule)(nil)
- _ caddyhttp.MiddlewareHandler = (*FrankenPHPModule)(nil)
- _ caddyfile.Unmarshaler = (*FrankenPHPModule)(nil)
- )
|