Skip to content

Commit

Permalink
WIP: enable x-accel
Browse files Browse the repository at this point in the history
  • Loading branch information
withinboredom committed Dec 17, 2023
1 parent 11da3be commit 2c0bc98
Show file tree
Hide file tree
Showing 6 changed files with 158 additions and 19 deletions.
133 changes: 129 additions & 4 deletions caddy/caddy.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ package caddy
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"path/filepath"
"strconv"
"strings"

"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
Expand Down Expand Up @@ -205,8 +207,11 @@ type FrankenPHPModule struct {
// 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
Env map[string]string `json:"env,omitempty"`
logger *zap.Logger
responseMatchers map[string]caddyhttp.ResponseMatcher
handleResponseSegments []*caddyfile.Dispenser
HandleResponse []caddyhttp.ResponseHandler `json:"handle_response,omitempty"`
}

// CaddyModule returns the Caddy module information.
Expand Down Expand Up @@ -238,12 +243,20 @@ func (f *FrankenPHPModule) Provision(ctx caddy.Context) error {
f.SplitPath = []string{".php"}
}

for i, rh := range f.HandleResponse {
f.logger.Info("Provisioning routes")
err := rh.Provision(ctx)
if err != nil {
return fmt.Errorf("provisioning response handler %d: %v", i, err)
}
}

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, _ caddyhttp.Handler) error {
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)

Expand All @@ -266,13 +279,80 @@ func (f FrankenPHPModule) ServeHTTP(w http.ResponseWriter, r *http.Request, _ ca
return err
}

return frankenphp.ServeHTTP(w, fr)
return frankenphp.ServeHTTP(w, fr, func(status int, header http.Header, r2 *http.Request) bool {
f.logger.Debug("Checking against matchers", zap.Int("number", len(f.HandleResponse)))
for i, rh := range f.HandleResponse {

f.logger.Debug("Checking response against matcher", zap.Any("matcher", rh))
if rh.Match != nil && !rh.Match.Match(status, header) {
continue
}

// if we are only changing the status code, do just that
if statusCodeStr := rh.StatusCode.String(); statusCodeStr != "" {
statusCode, err := strconv.Atoi(repl.ReplaceAll(statusCodeStr, ""))
if err != nil {
f.logger.Warn("Unable to replace status code string")
return false
}
if statusCode != 0 {
w.WriteHeader(statusCode)
}
break
}

// we are about to replace the response and close it. Frankenphp should handle this gracefully without
// killing the script or exploding.
f.logger.Info("Handling response", zap.Int("handler", i))

// use the replacer to so the original it can be routed. We use the "reverse_proxy" strings so that
// configuration is backwards compatible.
for field, value := range header {
repl.Set("http.reverse_proxy.header."+field, strings.Join(value, ","))
}
repl.Set("http.reverse_proxy.status_code", status)
repl.Set("http.reverse_proxy.status_text", strconv.Itoa(status))

routeErr := rh.Routes.Compile(next).ServeHTTP(w, r2)

if routeErr != nil {
f.logger.Warn("Failure handling route while handling response", zap.Any("error", routeErr))

// this is likely a 404 and we'll treat it as such
// todo: surely we can get the actual error code?
w.WriteHeader(404)

return true
}

f.logger.Debug("Matched response successfully!")

return true
}

f.logger.Debug("Did not match any response!")

return false
})
}

const matcherPrefix = "@"

// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
func (f *FrankenPHPModule) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {

f.responseMatchers = make(map[string]caddyhttp.ResponseMatcher)

for d.Next() {
for d.NextBlock(0) {
if strings.HasPrefix(d.Val(), matcherPrefix) {
err := caddyhttp.ParseNamedResponseMatcher(d.NewFromNextSegment(), f.responseMatchers)
if err != nil {
return err
}
continue
}

switch d.Val() {
case "root":
if !d.NextArg() {
Expand Down Expand Up @@ -301,6 +381,12 @@ func (f *FrankenPHPModule) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
return d.ArgErr()
}
f.ResolveRootSymlink = true

case "handle_response":
// delegate the parsing of handle_response to the caller,
// since we need the httpcaddyfile.Helper to parse subroutes.
// See f.FinalizeUnmarshalCaddyfile
f.handleResponseSegments = append(f.handleResponseSegments, d.NewFromNextSegment())
}
}
}
Expand Down Expand Up @@ -522,6 +608,45 @@ func parsePhpServer(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error)
return nil, err
}

for _, d := range phpsrv.handleResponseSegments {
d.Next()
args := d.RemainingArgs()
if len(args) != 1 {
return nil, d.Errf("must use a named response matcher, starting with '@'")
}

var matcher *caddyhttp.ResponseMatcher
if !strings.HasPrefix(args[0], matcherPrefix) {
return nil, d.Errf("must use a named response matcher, starting with '@'")
}

foundMatcher, ok := phpsrv.responseMatchers[args[0]]
if !ok {
return nil, d.Errf("no named response matcher defined with name '%s'", args[0][1:])
}
matcher = &foundMatcher

handler, err := httpcaddyfile.ParseSegmentAsSubroute(h.WithDispenser(d.NewFromNextSegment()))
if err != nil {
return nil, err
}

subroute, ok := handler.(*caddyhttp.Subroute)
if !ok {
return nil, d.Errf("Segment was not parsed as a subroute")
}

phpsrv.HandleResponse = append(phpsrv.HandleResponse, caddyhttp.ResponseHandler{
Match: matcher,
Routes: subroute.Routes,
})

// clean up bits we needed just to parse the file
// todo: is there a better way??
phpsrv.responseMatchers = nil
phpsrv.handleResponseSegments = nil
}

// create the PHP route which is
// conditional on matching PHP files
phpRoute := caddyhttp.Route{
Expand Down
14 changes: 7 additions & 7 deletions docker-bake.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ variable "VERSION" {
}

variable "PHP_VERSION" {
default = "8.2,8.3"
default = "8.3"
}

variable "GO_VERSION" {
Expand Down Expand Up @@ -76,9 +76,9 @@ function "_php_version" {
target "default" {
name = "${tgt}-php-${replace(php-version, ".", "-")}-${os}"
matrix = {
os = ["bookworm", "alpine"]
os = ["bookworm"]
php-version = split(",", PHP_VERSION)
tgt = ["builder", "runner"]
tgt = ["runner"]
}
contexts = {
php-base = "docker-image://php:${php-version}-zts-${os}"
Expand All @@ -89,10 +89,10 @@ target "default" {
target = tgt
platforms = [
"linux/amd64",
"linux/386",
"linux/arm/v6",
"linux/arm/v7",
"linux/arm64",
#"linux/386",
#"linux/arm/v6",
#"linux/arm/v7",
#"linux/arm64",
]
tags = distinct(flatten(
[for pv in php_version(php-version) : flatten([
Expand Down
16 changes: 15 additions & 1 deletion frankenphp.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ type FrankenPHPContext struct {

done chan interface{}
currentWorkerRequest cgo.Handle

responseFilter func(status int, headers http.Header, r *http.Request) bool
}

func clientHasClosed(r *http.Request) bool {
Expand Down Expand Up @@ -431,7 +433,7 @@ func updateServerContext(request *http.Request, create bool, mrh C.uintptr_t) er
}

// ServeHTTP executes a PHP script according to the given context.
func ServeHTTP(responseWriter http.ResponseWriter, request *http.Request) error {
func ServeHTTP(responseWriter http.ResponseWriter, request *http.Request, responseFilter func(status int, headers http.Header, r *http.Request) bool) error {
shutdownWG.Add(1)
defer shutdownWG.Done()

Expand All @@ -442,6 +444,8 @@ func ServeHTTP(responseWriter http.ResponseWriter, request *http.Request) error

fc.responseWriter = responseWriter

fc.responseFilter = responseFilter

rc := requestChan
// Detect if a worker is available to handle this request
if nil == fc.responseWriter {
Expand Down Expand Up @@ -592,6 +596,14 @@ func go_write_headers(rh C.uintptr_t, status C.int, headers *C.zend_llist) {
current = current.next
}

handled := fc.responseFilter(int(status), fc.responseWriter.Header(), r)
fc.logger.Debug("Sent response to filter", zap.Int("status", int(status)), zap.Any("headers", fc.responseWriter.Header()))
if handled {
fc.logger.Debug("Filter handled request, php is done...")
fc.responseWriter = nil
return
}

fc.responseWriter.WriteHeader(int(status))

if status >= 100 && status < 200 {
Expand Down Expand Up @@ -619,6 +631,8 @@ func go_sapi_flush(rh C.uintptr_t) bool {
}
}

fc.logger.Debug("Sending response to response filter before flushing")

if err := http.NewResponseController(fc.responseWriter).Flush(); err != nil {
fc.logger.Error("the current responseWriter is not a flusher", zap.Error(err))
}
Expand Down
10 changes: 5 additions & 5 deletions frankenphp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ func runTest(t *testing.T, test func(func(http.ResponseWriter, *http.Request), *
req, err := frankenphp.NewRequestWithContext(r, frankenphp.WithRequestDocumentRoot(testDataDir, false))
assert.NoError(t, err)

err = frankenphp.ServeHTTP(w, req)
err = frankenphp.ServeHTTP(w, req, nil)
assert.NoError(t, err)
}

Expand Down Expand Up @@ -185,7 +185,7 @@ func testPathInfo(t *testing.T, opts *testOptions) {
)
assert.NoError(t, err)

err = frankenphp.ServeHTTP(w, rewriteRequest)
err = frankenphp.ServeHTTP(w, rewriteRequest, nil)
assert.NoError(t, err)
}

Expand Down Expand Up @@ -602,7 +602,7 @@ func ExampleServeHTTP() {
panic(err)
}

if err := frankenphp.ServeHTTP(w, req); err != nil {
if err := frankenphp.ServeHTTP(w, req, nil); err != nil {
panic(err)
}
})
Expand Down Expand Up @@ -632,7 +632,7 @@ func BenchmarkHelloWorld(b *testing.B) {
panic(err)
}

if err := frankenphp.ServeHTTP(w, req); err != nil {
if err := frankenphp.ServeHTTP(w, req, nil); err != nil {
panic(err)
}
}
Expand All @@ -658,7 +658,7 @@ func BenchmarkEcho(b *testing.B) {
if err != nil {
panic(err)
}
if err := frankenphp.ServeHTTP(w, req); err != nil {
if err := frankenphp.ServeHTTP(w, req, nil); err != nil {
panic(err)
}
}
Expand Down
2 changes: 1 addition & 1 deletion worker.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ func startWorkers(fileName string, nbWorkers int, env map[string]string) error {
}

l.Debug("starting", zap.String("worker", absFileName))
if err := ServeHTTP(nil, r); err != nil {
if err := ServeHTTP(nil, r, nil); err != nil {
panic(err)
}

Expand Down
2 changes: 1 addition & 1 deletion worker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ func ExampleServeHTTP_workers() {
panic(err)
}

if err := frankenphp.ServeHTTP(w, req); err != nil {
if err := frankenphp.ServeHTTP(w, req, nil); err != nil {
panic(err)
}
})
Expand Down

0 comments on commit 2c0bc98

Please sign in to comment.