Skip to content

Commit

Permalink
feat(caddy): add command to start a PHP server (#238)
Browse files Browse the repository at this point in the history
* feat(caddy): a command to start a PHP server

* docs and l shortcut

* fix some bugs and support for compression

* cleanup

* enable compression by default

* add -a shortcut

* refactor

* const

* docs
  • Loading branch information
dunglas authored Oct 5, 2023
1 parent b4780b6 commit c624971
Show file tree
Hide file tree
Showing 4 changed files with 240 additions and 3 deletions.
4 changes: 3 additions & 1 deletion caddy/caddy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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()
Expand Down
228 changes: 228 additions & 0 deletions caddy/command.go
Original file line number Diff line number Diff line change
@@ -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 <example.com>] [--root <path>] [--listen <addr>] [--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 {}
}
4 changes: 2 additions & 2 deletions caddy/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions testdata/large-response.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

require_once __DIR__.'/_executor.php';

return function () {
echo str_repeat("Hey\n", 1024);
};

0 comments on commit c624971

Please sign in to comment.