Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(caddy): a command to start a PHP server #238

Merged
merged 9 commits into from
Oct 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.

dunglas marked this conversation as resolved.
Show resolved Hide resolved
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);
};