diff --git a/caddy/caddy.go b/caddy/caddy.go index fa8efb28b..cf3c8d79f 100644 --- a/caddy/caddy.go +++ b/caddy/caddy.go @@ -228,7 +228,7 @@ func (f *FrankenPHPModule) Provision(ctx caddy.Context) error { // 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 { +func (f FrankenPHPModule) ServeHTTP(w http.ResponseWriter, r *http.Request, _ caddyhttp.Handler) error { origReq := r.Context().Value(caddyhttp.OriginalRequestCtxKey).(http.Request) repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) @@ -318,6 +318,8 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) // @phpFiles path *.php // php @phpFiles // file_server +// +// parsePhpServer is freely inspired from the php_fastgci directive of the Caddy server (Apache License 2.0, Matthew Holt and The Caddy Authors) func parsePhpServer(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error) { if !h.Next() { return nil, h.ArgErr() diff --git a/caddy/command.go b/caddy/command.go new file mode 100644 index 000000000..49e3e6170 --- /dev/null +++ b/caddy/command.go @@ -0,0 +1,228 @@ +package caddy + +import ( + "encoding/json" + "log" + "net/http" + "strconv" + "time" + + "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddyconfig" + caddycmd "github.com/caddyserver/caddy/v2/cmd" + "github.com/caddyserver/caddy/v2/modules/caddyhttp" + "github.com/caddyserver/caddy/v2/modules/caddyhttp/encode" + "github.com/caddyserver/caddy/v2/modules/caddyhttp/fileserver" + "github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite" + "github.com/caddyserver/certmagic" + "go.uber.org/zap" + + "github.com/spf13/cobra" +) + +func init() { + caddycmd.RegisterCommand(caddycmd.Command{ + Name: "php-server", + Usage: "[--domain ] [--root ] [--listen ] [--access-log]", + Short: "Spins up a production-ready PHP server", + Long: ` +A simple but production-ready PHP server. Useful for quick deployments, +demos, and development. + +The listener's socket address can be customized with the --listen flag. + +If a domain name is specified with --domain, the default listener address +will be changed to the HTTPS port and the server will use HTTPS. If using +a public domain, ensure A/AAAA records are properly configured before +using this option. + +For more advanced use cases, see https://github.com/dunglas/frankenphp/blob/main/docs/config.md`, + CobraFunc: func(cmd *cobra.Command) { + cmd.Flags().StringP("domain", "d", "", "Domain name at which to serve the files") + cmd.Flags().StringP("root", "r", "", "The path to the root of the site") + cmd.Flags().StringP("listen", "l", "", "The address to which to bind the listener") + cmd.Flags().BoolP("access-log", "a", false, "Enable the access log") + cmd.Flags().BoolP("debug", "v", false, "Enable verbose debug logs") + cmd.Flags().BoolP("no-compress", "", false, "Disable Zstandard and Gzip compression") + cmd.RunE = caddycmd.WrapCommandFuncForCobra(cmdPHPServer) + }, + }) +} + +// cmdPHPServer is freely inspired from the file-server command of the Caddy server (Apache License 2.0, Matthew Holt and The Caddy Authors) +func cmdPHPServer(fs caddycmd.Flags) (int, error) { + caddy.TrapSignals() + + domain := fs.String("domain") + root := fs.String("root") + listen := fs.String("listen") + accessLog := fs.Bool("access-log") + debug := fs.Bool("debug") + compress := !fs.Bool("no-compress") + + const indexFile = "index.php" + extensions := []string{"php"} + tryFiles := []string{"{http.request.uri.path}", "{http.request.uri.path}/" + indexFile, indexFile} + + phpHandler := FrankenPHPModule{ + Root: root, + SplitPath: extensions, + } + + // route to redirect to canonical path if index PHP file + redirMatcherSet := caddy.ModuleMap{ + "file": caddyconfig.JSON(fileserver.MatchFile{ + Root: root, + TryFiles: []string{"{http.request.uri.path}/" + indexFile}, + }, nil), + "not": caddyconfig.JSON(caddyhttp.MatchNot{ + MatcherSetsRaw: []caddy.ModuleMap{ + { + "path": caddyconfig.JSON(caddyhttp.MatchPath{"*/"}, nil), + }, + }, + }, nil), + } + redirHandler := caddyhttp.StaticResponse{ + StatusCode: caddyhttp.WeakString(strconv.Itoa(http.StatusPermanentRedirect)), + Headers: http.Header{"Location": []string{"{http.request.orig_uri.path}/"}}, + } + redirRoute := caddyhttp.Route{ + MatcherSetsRaw: []caddy.ModuleMap{redirMatcherSet}, + HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(redirHandler, "handler", "static_response", nil)}, + } + + // route to rewrite to PHP index file + rewriteMatcherSet := caddy.ModuleMap{ + "file": caddyconfig.JSON(fileserver.MatchFile{ + Root: root, + TryFiles: tryFiles, + SplitPath: extensions, + }, nil), + } + rewriteHandler := rewrite.Rewrite{ + URI: "{http.matchers.file.relative}", + } + rewriteRoute := caddyhttp.Route{ + MatcherSetsRaw: []caddy.ModuleMap{rewriteMatcherSet}, + HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(rewriteHandler, "handler", "rewrite", nil)}, + } + + // route to actually pass requests to PHP files; + // match only requests that are for PHP files + pathList := []string{} + for _, ext := range extensions { + pathList = append(pathList, "*"+ext) + } + phpMatcherSet := caddy.ModuleMap{ + "path": caddyconfig.JSON(pathList, nil), + } + + // create the PHP route which is + // conditional on matching PHP files + phpRoute := caddyhttp.Route{ + MatcherSetsRaw: []caddy.ModuleMap{phpMatcherSet}, + HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(phpHandler, "handler", "php", nil)}, + } + + fileRoute := caddyhttp.Route{ + MatcherSetsRaw: []caddy.ModuleMap{}, + HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(fileserver.FileServer{Root: root}, "handler", "file_server", nil)}, + } + + subroute := caddyhttp.Subroute{ + Routes: caddyhttp.RouteList{redirRoute, rewriteRoute, phpRoute, fileRoute}, + } + + if compress { + gzip, err := caddy.GetModule("http.encoders.gzip") + if err != nil { + return caddy.ExitCodeFailedStartup, err + } + + zstd, err := caddy.GetModule("http.encoders.zstd") + if err != nil { + return caddy.ExitCodeFailedStartup, err + } + + encodeRoute := caddyhttp.Route{ + MatcherSetsRaw: []caddy.ModuleMap{}, + HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(encode.Encode{ + EncodingsRaw: caddy.ModuleMap{ + "zstd": caddyconfig.JSON(zstd.New(), nil), + "gzip": caddyconfig.JSON(gzip.New(), nil), + }, + Prefer: []string{"zstd", "gzip"}, + }, "handler", "encode", nil)}, + } + + subroute.Routes = append(caddyhttp.RouteList{encodeRoute}, subroute.Routes...) + } + + route := caddyhttp.Route{ + HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(subroute, "handler", "subroute", nil)}, + } + + if domain != "" { + route.MatcherSetsRaw = []caddy.ModuleMap{ + { + "host": caddyconfig.JSON(caddyhttp.MatchHost{domain}, nil), + }, + } + } + + server := &caddyhttp.Server{ + ReadHeaderTimeout: caddy.Duration(10 * time.Second), + IdleTimeout: caddy.Duration(30 * time.Second), + MaxHeaderBytes: 1024 * 10, + Routes: caddyhttp.RouteList{route}, + } + if listen == "" { + if domain == "" { + listen = ":80" + } else { + listen = ":" + strconv.Itoa(certmagic.HTTPSPort) + } + } + server.Listen = []string{listen} + if accessLog { + server.Logs = &caddyhttp.ServerLogConfig{} + } + + httpApp := caddyhttp.App{ + Servers: map[string]*caddyhttp.Server{"php": server}, + } + + var false bool + cfg := &caddy.Config{ + Admin: &caddy.AdminConfig{ + Disabled: true, + Config: &caddy.ConfigSettings{ + Persist: &false, + }, + }, + AppsRaw: caddy.ModuleMap{ + "http": caddyconfig.JSON(httpApp, nil), + "frankenphp": caddyconfig.JSON(FrankenPHPApp{}, nil), + }, + } + + if debug { + cfg.Logging = &caddy.Logging{ + Logs: map[string]*caddy.CustomLog{ + "default": { + BaseLog: caddy.BaseLog{Level: zap.DebugLevel.CapitalString()}, + }, + }, + } + } + + err := caddy.Run(cfg) + if err != nil { + return caddy.ExitCodeFailedStartup, err + } + + log.Printf("Caddy serving PHP app on %s", listen) + + select {} +} diff --git a/caddy/go.mod b/caddy/go.mod index c2af6dfbd..b5486a777 100644 --- a/caddy/go.mod +++ b/caddy/go.mod @@ -13,9 +13,11 @@ replace ( require ( github.com/caddyserver/caddy/v2 v2.7.4 + github.com/caddyserver/certmagic v0.19.2 github.com/dunglas/frankenphp v1.0.0-beta.1 github.com/dunglas/mercure/caddy v0.15.2 github.com/dunglas/vulcain/caddy v0.5.0 + github.com/spf13/cobra v1.7.0 go.uber.org/automaxprocs v1.5.3 go.uber.org/zap v1.26.0 ) @@ -35,7 +37,6 @@ require ( github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bits-and-blooms/bitset v1.8.0 // indirect - github.com/caddyserver/certmagic v0.19.2 // indirect github.com/cenkalti/backoff/v4 v4.2.1 // indirect github.com/cespare/xxhash v1.1.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect @@ -138,7 +139,6 @@ require ( github.com/smallstep/truststore v0.12.1 // indirect github.com/spf13/afero v1.9.5 // indirect github.com/spf13/cast v1.5.1 // indirect - github.com/spf13/cobra v1.7.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/viper v1.16.0 // indirect diff --git a/testdata/large-response.php b/testdata/large-response.php new file mode 100644 index 000000000..52a9aab55 --- /dev/null +++ b/testdata/large-response.php @@ -0,0 +1,7 @@ +