Browse Source

feat: add a woker mode (#1)

* refactor: better memory management

* wip

* tmp

* introduce a go-like api

* upgraded to PHP 8.2

* Fix thread safety issues

* fix tests

* wip

* refactor worker

* worker prototype

* fix populate env

* session

* improve tests

* fix Caddy tests

* refactor
Kévin Dunglas 2 years ago
parent
commit
7d81fa51fe
10 changed files with 1178 additions and 567 deletions
  1. 3 3
      Dockerfile
  2. 13 24
      README.md
  3. 2 1
      caddy/caddy.go
  4. 1 1
      caddy/caddy_test.go
  5. 83 59
      caddy/go.mod
  6. 339 112
      caddy/go.sum
  7. 273 0
      cgi.go
  8. 371 67
      frankenphp.c
  9. 86 295
      frankenphp.go
  10. 7 5
      frankenphp.h

+ 3 - 3
Dockerfile

@@ -1,6 +1,6 @@
 FROM golang
 
-ARG PHP_VERSION=8.0.11
+ARG PHP_VERSION=8.1.5
 
 # Sury doesn't provide ZTS builds for now
 #RUN apt-get update && \
@@ -8,7 +8,7 @@ ARG PHP_VERSION=8.0.11
 #    wget -O /etc/apt/trusted.gpg.d/php.gpg https://packages.sury.org/php/apt.gpg && \
 #    sh -c 'echo "deb https://packages.sury.org/php/ $(lsb_release -sc) main" > /etc/apt/sources.list.d/php.list' && \
 #    apt-get update && \
-#    apt-get -y --no-install-recommends install php8.0-dev && \
+#    apt-get -y --no-install-recommends install php8.1-dev && \
 #    apt-get -y remove apt-transport-https lsb-release && \
 #    apt-get clean all
 #ENV CGO_CFLAGS="-I /usr/include/php/20200930 -I /usr/include/php/20200930/Zend -I /usr/include/php/20200930/TSRM -I /usr/include/php/20200930/main -I /usr/include/php/20200930/sapi/embed"
@@ -32,7 +32,7 @@ WORKDIR /go/src/app
 COPY . .
 
 RUN go get -d -v ./...
-#RUN go build -v
+RUN go build -v
 #RUN cd cmd/frankenphp && go install -v ./...
 
 #CMD ["frankenphp"]

+ 13 - 24
README.md

@@ -15,7 +15,7 @@ docker build -t frankenphp .
 
 #### Install PHP
 
-Most distributions doesn't provide packages containing ZTS builds of PHP.
+Most distributions don't provide packages containing ZTS builds of PHP.
 Because the Go HTTP server uses goroutines, a ZTS build is needed.
 
 Start by [downloading the latest version of PHP](https://www.php.net/downloads.php),
@@ -24,42 +24,31 @@ then follow the instructions according to your operating system.
 ##### Linux
 
 ```
-./configure --enable-embed --enable-zts
-make
+./configure \
+    --enable-embed=static \
+    --enable-zts
+make -j6
 make install
 ```
 
 ##### Mac
 
+The instructions to build on Mac and Linux are similar.
+However, on Mac, you have to use the [Homebrew](https://brew.sh/) package manager to install `libiconv` and `bison`.
+You also need to slightly tweak the configuration.
+
 ```
-brew install libiconv
+brew install libiconv bison
+echo 'export PATH="/opt/homebrew/opt/bison/bin:$PATH"' >> ~/.zshrc
 ./configure \
-    --enable-zts \
     --enable-embed=static \
+    --enable-zts \
     --with-iconv=/opt/homebrew/opt/libiconv/ \
     --without-pcre-jit
-make
+make -j6
 make install
 ```
 
-Then, you also need to build a Mac-compatible PHP shared library.
-As the standard PHP distribution doesn't provide one, you need to do a few extra steps:
-
-Start by adding those lines at the end of the `Makefile`:
-
-```
-libs/libphp.dylib: $(PHP_GLOBAL_OBJS) $(PHP_SAPI_OBJS)
-	$(LIBTOOL) --mode=link $(CC) -dynamiclib $(LIBPHP_CFLAGS) $(CFLAGS_CLEAN) $(EXTRA_CFLAGS) -rpath $(phptempdir) $(EXTRA_LDFLAGS) $(LDFLAGS) $(PHP_RPATHS) $(PHP_GLOBAL_OBJS) $(PHP_SAPI_OBJS) $(EXTRA_LIBS) $(ZEND_EXTRA_LIBS) -o $@
-	-@$(LIBTOOL) --silent --mode=install cp $@ $(phptempdir)/$@ >/dev/null 2>&1
-```
-
-Then run:
-
-```
-make libs/libphp.dylib
-sudo cp libs/libphp.dylib /usr/local/lib/
-```
-
 #### Compile the Go App
 
 ```

+ 2 - 1
caddy/caddy.go

@@ -50,6 +50,7 @@ func (f *FrankenPHPModule) Provision(ctx caddy.Context) error {
 	f.logger = ctx.Logger(f)
 
 	_, _, err := php.LoadOrNew("php", func() (caddy.Destructor, error) {
+		frankenphp.Startup()
 		return &phpDestructor{}, nil
 	})
 	if err != nil {
@@ -80,7 +81,7 @@ func (f FrankenPHPModule) ServeHTTP(w http.ResponseWriter, r *http.Request, next
 
 	documentRoot := repl.ReplaceKnown(f.Root, "")
 	fr := frankenphp.NewRequestWithContext(r, documentRoot)
-	fc := fr.Context().Value(frankenphp.FrankenPHPContextKey).(*frankenphp.FrankenPHPContext)
+	fc, _ := frankenphp.FromContext(fr.Context())
 	fc.ResolveRootSymlink = f.ResolveRootSymlink
 	fc.SplitPath = f.SplitPath
 

+ 1 - 1
caddy/caddy_test.go

@@ -31,7 +31,7 @@ func TestPHP(t *testing.T) {
 			}
 			rewrite @indexFiles {http.matchers.file.relative}
 
-			# Proxy PHP files to the FastCGI responder
+			# Handle PHP files with FrankenPHP
 			@phpFiles path *.php
 			php @phpFiles
 	

+ 83 - 59
caddy/go.mod

@@ -1,112 +1,136 @@
 module github.com/dunglas/frankenphp/caddy
 
-go 1.17
+go 1.18
 
 replace github.com/dunglas/frankenphp => ../
 
 require (
-	github.com/caddyserver/caddy/v2 v2.4.5
+	github.com/caddyserver/caddy/v2 v2.5.1
 	github.com/dunglas/frankenphp v0.0.0-00010101000000-000000000000
-	go.uber.org/zap v1.19.1
+	go.uber.org/zap v1.21.0
 )
 
 require (
+	filippo.io/edwards25519 v1.0.0-rc.1 // indirect
 	github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect
+	github.com/BurntSushi/toml v1.0.0 // indirect
 	github.com/Masterminds/goutils v1.1.1 // indirect
 	github.com/Masterminds/semver/v3 v3.1.1 // indirect
 	github.com/Masterminds/sprig/v3 v3.2.2 // indirect
-	github.com/alecthomas/chroma v0.9.2 // indirect
+	github.com/alecthomas/chroma v0.10.0 // indirect
 	github.com/antlr/antlr4 v0.0.0-20200503195918-621b933c7a7f // indirect
 	github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b // indirect
 	github.com/beorn7/perks v1.0.1 // indirect
-	github.com/caddyserver/certmagic v0.14.5 // indirect
+	github.com/caddyserver/certmagic v0.16.1 // indirect
+	github.com/cenkalti/backoff/v4 v4.1.2 // indirect
 	github.com/cespare/xxhash v1.1.0 // indirect
-	github.com/cespare/xxhash/v2 v2.1.1 // indirect
+	github.com/cespare/xxhash/v2 v2.1.2 // indirect
 	github.com/cheekybits/genny v1.0.0 // indirect
 	github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect
 	github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
-	github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 // indirect
 	github.com/dgraph-io/badger v1.6.2 // indirect
 	github.com/dgraph-io/badger/v2 v2.2007.4 // indirect
 	github.com/dgraph-io/ristretto v0.0.4-0.20200906165740-41ebdbffecfd // indirect
 	github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
 	github.com/dlclark/regexp2 v1.4.0 // indirect
 	github.com/dustin/go-humanize v1.0.1-0.20200219035652-afde56e7acac // indirect
-	github.com/fsnotify/fsnotify v1.4.9 // indirect
+	github.com/felixge/httpsnoop v1.0.2 // indirect
+	github.com/fsnotify/fsnotify v1.5.4 // indirect
 	github.com/go-chi/chi v4.1.2+incompatible // indirect
 	github.com/go-kit/kit v0.10.0 // indirect
-	github.com/go-logfmt/logfmt v0.5.0 // indirect
+	github.com/go-logfmt/logfmt v0.5.1 // indirect
+	github.com/go-logr/logr v1.2.2 // indirect
+	github.com/go-logr/stdr v1.2.2 // indirect
 	github.com/go-sql-driver/mysql v1.6.0 // indirect
 	github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
 	github.com/golang/protobuf v1.5.2 // indirect
-	github.com/golang/snappy v0.0.3 // indirect
+	github.com/golang/snappy v0.0.4 // indirect
 	github.com/google/cel-go v0.7.3 // indirect
 	github.com/google/uuid v1.3.0 // indirect
-	github.com/huandu/xstrings v1.3.1 // indirect
-	github.com/imdario/mergo v0.3.11 // indirect
-	github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a // indirect
-	github.com/klauspost/compress v1.13.4 // indirect
-	github.com/klauspost/cpuid/v2 v2.0.9 // indirect
+	github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect
+	github.com/huandu/xstrings v1.3.2 // indirect
+	github.com/imdario/mergo v0.3.12 // indirect
+	github.com/jackc/chunkreader/v2 v2.0.1 // indirect
+	github.com/jackc/pgconn v1.10.1 // indirect
+	github.com/jackc/pgio v1.0.0 // indirect
+	github.com/jackc/pgpassfile v1.0.0 // indirect
+	github.com/jackc/pgproto3/v2 v2.2.0 // indirect
+	github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
+	github.com/jackc/pgtype v1.9.0 // indirect
+	github.com/jackc/pgx/v4 v4.14.0 // indirect
+	github.com/klauspost/compress v1.15.0 // indirect
+	github.com/klauspost/cpuid/v2 v2.0.12 // indirect
 	github.com/libdns/libdns v0.2.1 // indirect
-	github.com/lucas-clemente/quic-go v0.23.0 // indirect
-	github.com/lunixbochs/vtclean v1.0.0 // indirect
-	github.com/manifoldco/promptui v0.8.0 // indirect
+	github.com/lucas-clemente/quic-go v0.26.0 // indirect
+	github.com/manifoldco/promptui v0.9.0 // indirect
 	github.com/marten-seemann/qpack v0.2.1 // indirect
-	github.com/marten-seemann/qtls-go1-16 v0.1.4 // indirect
-	github.com/marten-seemann/qtls-go1-17 v0.1.0 // indirect
+	github.com/marten-seemann/qtls-go1-16 v0.1.5 // indirect
+	github.com/marten-seemann/qtls-go1-17 v0.1.1 // indirect
+	github.com/marten-seemann/qtls-go1-18 v0.1.1 // indirect
 	github.com/mattn/go-colorable v0.1.8 // indirect
 	github.com/mattn/go-isatty v0.0.13 // indirect
 	github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
-	github.com/mholt/acmez v1.0.0 // indirect
-	github.com/micromdm/scep/v2 v2.0.0 // indirect
-	github.com/miekg/dns v1.1.42 // indirect
-	github.com/mitchellh/copystructure v1.0.0 // indirect
-	github.com/mitchellh/reflectwalk v1.0.1 // indirect
-	github.com/naoina/go-stringutil v0.1.0 // indirect
-	github.com/naoina/toml v0.1.1 // indirect
+	github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
+	github.com/mholt/acmez v1.0.2 // indirect
+	github.com/micromdm/scep/v2 v2.1.0 // indirect
+	github.com/miekg/dns v1.1.49 // indirect
+	github.com/mitchellh/copystructure v1.2.0 // indirect
+	github.com/mitchellh/go-ps v1.0.0 // indirect
+	github.com/mitchellh/reflectwalk v1.0.2 // indirect
 	github.com/nxadm/tail v1.4.8 // indirect
-	github.com/onsi/ginkgo v1.16.4 // indirect
+	github.com/onsi/ginkgo v1.16.5 // indirect
 	github.com/pkg/errors v0.9.1 // indirect
-	github.com/prometheus/client_golang v1.11.0 // indirect
+	github.com/prometheus/client_golang v1.12.2 // indirect
 	github.com/prometheus/client_model v0.2.0 // indirect
-	github.com/prometheus/common v0.26.0 // indirect
-	github.com/prometheus/procfs v0.6.0 // indirect
+	github.com/prometheus/common v0.34.0 // indirect
+	github.com/prometheus/procfs v0.7.3 // indirect
 	github.com/rs/xid v1.2.1 // indirect
 	github.com/russross/blackfriday/v2 v2.0.1 // indirect
-	github.com/samfoo/ansi v0.0.0-20160124022901-b6bd2ded7189 // indirect
 	github.com/shopspring/decimal v1.2.0 // indirect
 	github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
-	github.com/sirupsen/logrus v1.7.0 // indirect
-	github.com/smallstep/certificates v0.16.4 // indirect
-	github.com/smallstep/cli v0.16.1 // indirect
-	github.com/smallstep/nosql v0.3.8 // indirect
-	github.com/smallstep/truststore v0.9.6 // indirect
-	github.com/spf13/cast v1.3.1 // indirect
+	github.com/sirupsen/logrus v1.8.1 // indirect
+	github.com/slackhq/nebula v1.5.2 // indirect
+	github.com/smallstep/certificates v0.19.0 // indirect
+	github.com/smallstep/cli v0.18.0 // indirect
+	github.com/smallstep/nosql v0.4.0 // indirect
+	github.com/smallstep/truststore v0.11.0 // indirect
+	github.com/spf13/cast v1.4.1 // indirect
 	github.com/stoewer/go-strcase v1.2.0 // indirect
+	github.com/tailscale/tscert v0.0.0-20220125204807-4509a5fbaf74 // indirect
 	github.com/urfave/cli v1.22.5 // indirect
-	github.com/yuin/goldmark v1.4.0 // indirect
-	github.com/yuin/goldmark-highlighting v0.0.0-20210516132338-9216f9c5aa01 // indirect
+	github.com/yuin/goldmark v1.4.8 // indirect
+	github.com/yuin/goldmark-highlighting v0.0.0-20220208100518-594be1970594 // indirect
 	go.etcd.io/bbolt v1.3.6 // indirect
-	go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1 // indirect
-	go.step.sm/cli-utils v0.4.1 // indirect
-	go.step.sm/crypto v0.9.0 // indirect
-	go.step.sm/linkedca v0.0.0-20210611183751-27424aae8d25 // indirect
-	go.uber.org/atomic v1.7.0 // indirect
-	go.uber.org/multierr v1.6.0 // indirect
-	golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e // indirect
-	golang.org/x/mod v0.4.2 // indirect
-	golang.org/x/net v0.0.0-20210614182718-04defd469f4e // indirect
-	golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect
-	golang.org/x/term v0.0.0-20210503060354-a79de5458b56 // indirect
-	golang.org/x/text v0.3.6 // indirect
-	golang.org/x/tools v0.1.5 // indirect
-	golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
-	google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08 // indirect
-	google.golang.org/grpc v1.38.0 // indirect
-	google.golang.org/protobuf v1.27.1 // indirect
+	go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 // indirect
+	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.29.0 // indirect
+	go.opentelemetry.io/otel v1.4.0 // indirect
+	go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.4.0 // indirect
+	go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.4.0 // indirect
+	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.4.0 // indirect
+	go.opentelemetry.io/otel/internal/metric v0.27.0 // indirect
+	go.opentelemetry.io/otel/metric v0.27.0 // indirect
+	go.opentelemetry.io/otel/sdk v1.4.0 // indirect
+	go.opentelemetry.io/otel/trace v1.4.0 // indirect
+	go.opentelemetry.io/proto/otlp v0.12.0 // indirect
+	go.step.sm/cli-utils v0.7.0 // indirect
+	go.step.sm/crypto v0.16.1 // indirect
+	go.step.sm/linkedca v0.15.0 // indirect
+	go.uber.org/atomic v1.9.0 // indirect
+	go.uber.org/multierr v1.8.0 // indirect
+	golang.org/x/crypto v0.0.0-20220518034528-6f7dac969898 // indirect
+	golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect
+	golang.org/x/net v0.0.0-20220517181318-183a9ca12b87 // indirect
+	golang.org/x/sys v0.0.0-20220517195934-5e4e11fc645e // indirect
+	golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 // indirect
+	golang.org/x/text v0.3.8-0.20211004125949-5bd84dd9b33b // indirect
+	golang.org/x/tools v0.1.10 // indirect
+	golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df // indirect
+	google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf // indirect
+	google.golang.org/grpc v1.44.0 // indirect
+	google.golang.org/protobuf v1.28.0 // indirect
 	gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
-	gopkg.in/square/go-jose.v2 v2.5.1 // indirect
+	gopkg.in/square/go-jose.v2 v2.6.0 // indirect
 	gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
-	howett.net/plist v0.0.0-20181124034731-591f970eefbb // indirect
+	howett.net/plist v1.0.0 // indirect
 )

File diff suppressed because it is too large
+ 339 - 112
caddy/go.sum


+ 273 - 0
cgi.go

@@ -0,0 +1,273 @@
+package frankenphp
+
+import (
+	"crypto/tls"
+	"net"
+	"net/http"
+	"path/filepath"
+	"strings"
+)
+
+// populateEnv returns a set of CGI environment variables for the request.
+//
+// TODO: handle this case https://github.com/caddyserver/caddy/issues/3718
+// Inspired by https://github.com/caddyserver/caddy/blob/master/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go
+func populateEnv(request *http.Request) error {
+	fc, ok := FromContext(request.Context())
+	if !ok {
+		panic("not a FrankenPHP request")
+	}
+
+	if fc.populated {
+		return nil
+	}
+
+	_, addrOk := fc.Env["REMOTE_ADDR"]
+	_, portOk := fc.Env["REMOTE_PORT"]
+	if !addrOk || !portOk {
+		// Separate remote IP and port; more lenient than net.SplitHostPort
+		var ip, port string
+		if idx := strings.LastIndex(request.RemoteAddr, ":"); idx > -1 {
+			ip = request.RemoteAddr[:idx]
+			port = request.RemoteAddr[idx+1:]
+		} else {
+			ip = request.RemoteAddr
+		}
+
+		// Remove [] from IPv6 addresses
+		ip = strings.Replace(ip, "[", "", 1)
+		ip = strings.Replace(ip, "]", "", 1)
+
+		if _, ok := fc.Env["REMOTE_ADDR"]; !ok {
+			fc.Env["REMOTE_ADDR"] = ip
+		}
+		if _, ok := fc.Env["REMOTE_HOST"]; !ok {
+			fc.Env["REMOTE_HOST"] = ip // For speed, remote host lookups disabled
+		}
+		if _, ok := fc.Env["REMOTE_PORT"]; !ok {
+			fc.Env["REMOTE_PORT"] = port
+		}
+	}
+
+	if _, ok := fc.Env["DOCUMENT_ROOT"]; !ok {
+		// make sure file root is absolute
+		root, err := filepath.Abs(fc.DocumentRoot)
+		if err != nil {
+			return err
+		}
+
+		if fc.ResolveRootSymlink {
+			if root, err = filepath.EvalSymlinks(root); err != nil {
+				return err
+			}
+		}
+
+		fc.Env["DOCUMENT_ROOT"] = root
+	}
+
+	fpath := request.URL.Path
+	scriptName := fpath
+
+	docURI := fpath
+	// split "actual path" from "path info" if configured
+	if splitPos := splitPos(fc, fpath); splitPos > -1 {
+		docURI = fpath[:splitPos]
+		fc.Env["PATH_INFO"] = fpath[splitPos:]
+
+		// Strip PATH_INFO from SCRIPT_NAME
+		scriptName = strings.TrimSuffix(scriptName, fc.Env["PATH_INFO"])
+	}
+
+	// SCRIPT_FILENAME is the absolute path of SCRIPT_NAME
+	scriptFilename := sanitizedPathJoin(fc.Env["DOCUMENT_ROOT"], scriptName)
+
+	// Ensure the SCRIPT_NAME has a leading slash for compliance with RFC3875
+	// Info: https://tools.ietf.org/html/rfc3875#section-4.1.13
+	if scriptName != "" && !strings.HasPrefix(scriptName, "/") {
+		scriptName = "/" + scriptName
+	}
+
+	if _, ok := fc.Env["DOCUMENT_URI"]; !ok {
+		fc.Env["DOCUMENT_URI"] = docURI
+	}
+	if _, ok := fc.Env["SCRIPT_FILENAME"]; !ok {
+		fc.Env["SCRIPT_FILENAME"] = scriptFilename
+	}
+	if _, ok := fc.Env["SCRIPT_NAME"]; !ok {
+		fc.Env["SCRIPT_NAME"] = scriptName
+	}
+
+	if _, ok := fc.Env["REQUEST_SCHEME"]; !ok {
+		if request.TLS == nil {
+			fc.Env["REQUEST_SCHEME"] = "http"
+		} else {
+			fc.Env["REQUEST_SCHEME"] = "https"
+		}
+	}
+
+	if request.TLS != nil {
+		if _, ok := fc.Env["HTTPS"]; !ok {
+			fc.Env["HTTPS"] = "on"
+		}
+
+		// and pass the protocol details in a manner compatible with apache's mod_ssl
+		// (which is why these have a SSL_ prefix and not TLS_).
+		_, sslProtocolOk := fc.Env["SSL_PROTOCOL"]
+		v, versionOk := tlsProtocolStrings[request.TLS.Version]
+		if !sslProtocolOk && versionOk {
+			fc.Env["SSL_PROTOCOL"] = v
+		}
+	}
+
+	_, serverNameOk := fc.Env["SERVER_NAME"]
+	_, serverPortOk := fc.Env["SERVER_PORT"]
+	if !serverNameOk || !serverPortOk {
+		reqHost, reqPort, err := net.SplitHostPort(request.Host)
+		if err == nil {
+			if !serverNameOk {
+				fc.Env["SERVER_NAME"] = reqHost
+			}
+
+			// compliance with the CGI specification requires that
+			// SERVER_PORT should only exist if it's a valid numeric value.
+			// Info: https://www.ietf.org/rfc/rfc3875 Page 18
+			if !serverPortOk && reqPort != "" {
+				fc.Env["SERVER_PORT"] = reqPort
+			}
+		} else if !serverNameOk {
+			// whatever, just assume there was no port
+			fc.Env["SERVER_NAME"] = request.Host
+		}
+	}
+
+	// Variables defined in CGI 1.1 spec
+	// Some variables are unused but cleared explicitly to prevent
+	// the parent environment from interfering.
+	// We never override an entry previously set
+	if _, ok := fc.Env["REMOTE_IDENT"]; !ok {
+		fc.Env["REMOTE_IDENT"] = "" // Not used
+	}
+	if _, ok := fc.Env["AUTH_TYPE"]; !ok {
+		fc.Env["AUTH_TYPE"] = "" // Not used
+	}
+	if _, ok := fc.Env["CONTENT_LENGTH"]; !ok {
+		fc.Env["CONTENT_LENGTH"] = request.Header.Get("Content-Length")
+	}
+	if _, ok := fc.Env["CONTENT_TYPE"]; !ok {
+		fc.Env["CONTENT_TYPE"] = request.Header.Get("Content-Type")
+	}
+	if _, ok := fc.Env["GATEWAY_INTERFACE"]; !ok {
+		fc.Env["GATEWAY_INTERFACE"] = "CGI/1.1"
+	}
+	if _, ok := fc.Env["QUERY_STRING"]; !ok {
+		fc.Env["QUERY_STRING"] = request.URL.RawQuery
+	}
+	if _, ok := fc.Env["QUERY_STRING"]; !ok {
+		fc.Env["QUERY_STRING"] = request.URL.RawQuery
+	}
+	if _, ok := fc.Env["REQUEST_METHOD"]; !ok {
+		fc.Env["REQUEST_METHOD"] = request.Method
+	}
+	if _, ok := fc.Env["SERVER_PROTOCOL"]; !ok {
+		fc.Env["SERVER_PROTOCOL"] = request.Proto
+	}
+	if _, ok := fc.Env["SERVER_SOFTWARE"]; !ok {
+		fc.Env["SERVER_SOFTWARE"] = "FrankenPHP"
+	}
+	if _, ok := fc.Env["HTTP_HOST"]; !ok {
+		fc.Env["HTTP_HOST"] = request.Host // added here, since not always part of headers
+	}
+	if _, ok := fc.Env["REQUEST_URI"]; !ok {
+		fc.Env["REQUEST_URI"] = request.URL.RequestURI()
+	}
+
+	// compliance with the CGI specification requires that
+	// PATH_TRANSLATED should only exist if PATH_INFO is defined.
+	// Info: https://www.ietf.org/rfc/rfc3875 Page 14
+	if fc.Env["PATH_INFO"] != "" {
+		fc.Env["PATH_TRANSLATED"] = sanitizedPathJoin(fc.Env["DOCUMENT_ROOT"], fc.Env["PATH_INFO"]) // Info: http://www.oreilly.com/openbook/cgi/ch02_04.html
+	}
+
+	// Add all HTTP headers to env variables
+	for field, val := range request.Header {
+		k := "HTTP_" + headerNameReplacer.Replace(strings.ToUpper(field))
+		if _, ok := fc.Env[k]; !ok {
+			fc.Env[k] = strings.Join(val, ", ")
+		}
+	}
+
+	if _, ok := fc.Env["REMOTE_USER"]; !ok {
+		var (
+			authUser string
+			ok       bool
+		)
+		authUser, fc.authPassword, ok = request.BasicAuth()
+		if ok {
+			fc.Env["REMOTE_USER"] = authUser
+		}
+	}
+
+	fc.populated = true
+
+	return nil
+}
+
+// splitPos returns the index where path should
+// be split based on SplitPath.
+//
+// Adapted from https://github.com/caddyserver/caddy/blob/master/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go
+// Copyright 2015 Matthew Holt and The Caddy Authors
+func splitPos(fc *FrankenPHPContext, path string) int {
+	if len(fc.SplitPath) == 0 {
+		return 0
+	}
+
+	lowerPath := strings.ToLower(path)
+	for _, split := range fc.SplitPath {
+		if idx := strings.Index(lowerPath, strings.ToLower(split)); idx > -1 {
+			return idx + len(split)
+		}
+	}
+	return -1
+}
+
+// Map of supported protocols to Apache ssl_mod format
+// Note that these are slightly different from SupportedProtocols in caddytls/config.go
+var tlsProtocolStrings = map[uint16]string{
+	tls.VersionTLS10: "TLSv1",
+	tls.VersionTLS11: "TLSv1.1",
+	tls.VersionTLS12: "TLSv1.2",
+	tls.VersionTLS13: "TLSv1.3",
+}
+
+var headerNameReplacer = strings.NewReplacer(" ", "_", "-", "_")
+
+// SanitizedPathJoin performs filepath.Join(root, reqPath) that
+// is safe against directory traversal attacks. It uses logic
+// similar to that in the Go standard library, specifically
+// in the implementation of http.Dir. The root is assumed to
+// be a trusted path, but reqPath is not; and the output will
+// never be outside of root. The resulting path can be used
+// with the local file system.
+//
+// Adapted from https://github.com/caddyserver/caddy/blob/master/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go
+// Copyright 2015 Matthew Holt and The Caddy Authors
+func sanitizedPathJoin(root, reqPath string) string {
+	if root == "" {
+		root = "."
+	}
+
+	path := filepath.Join(root, filepath.Clean("/"+reqPath))
+
+	// filepath.Join also cleans the path, and cleaning strips
+	// the trailing slash, so we need to re-add it afterwards.
+	// if the length is 1, then it's a path to the root,
+	// and that should return ".", so we don't append the separator.
+	if strings.HasSuffix(reqPath, "/") && len(reqPath) > 1 {
+		path += separator
+	}
+
+	return path
+}
+
+const separator = string(filepath.Separator)

+ 371 - 67
frankenphp.c

@@ -5,19 +5,356 @@
 #include "_cgo_export.h"
 #include "php.h"
 #include "SAPI.h"
+#include "ext/standard/head.h"
+#include "ext/session/php_session.h"
 #include "php_main.h"
 #include "php_variables.h"
+#include "php_output.h"
 #include "Zend/zend_alloc.h"
 
+
+// Helper functions copied from the PHP source code
+
+#include "php.h"
+#include "SAPI.h"
+
+// main/php_variables.c
+
+static zend_always_inline void php_register_variable_quick(const char *name, size_t name_len, zval *val, HashTable *ht)
+{
+	zend_string *key = zend_string_init_interned(name, name_len, 0);
+
+	zend_hash_update_ind(ht, key, val);
+	zend_string_release_ex(key, 0);
+}
+
+static inline void php_register_server_variables(void)
+{
+	zval tmp;
+	zval *arr = &PG(http_globals)[TRACK_VARS_SERVER];
+	HashTable *ht;
+
+	zval_ptr_dtor_nogc(arr);
+	array_init(arr);
+
+	/* Server variables */
+	if (sapi_module.register_server_variables) {
+		sapi_module.register_server_variables(arr);
+	}
+	ht = Z_ARRVAL_P(arr);
+
+	/* PHP Authentication support */
+	if (SG(request_info).auth_user) {
+		ZVAL_STRING(&tmp, SG(request_info).auth_user);
+		php_register_variable_quick("PHP_AUTH_USER", sizeof("PHP_AUTH_USER")-1, &tmp, ht);
+	}
+	if (SG(request_info).auth_password) {
+		ZVAL_STRING(&tmp, SG(request_info).auth_password);
+		php_register_variable_quick("PHP_AUTH_PW", sizeof("PHP_AUTH_PW")-1, &tmp, ht);
+	}
+	if (SG(request_info).auth_digest) {
+		ZVAL_STRING(&tmp, SG(request_info).auth_digest);
+		php_register_variable_quick("PHP_AUTH_DIGEST", sizeof("PHP_AUTH_DIGEST")-1, &tmp, ht);
+	}
+
+	/* store request init time */
+	ZVAL_DOUBLE(&tmp, sapi_get_request_time());
+	php_register_variable_quick("REQUEST_TIME_FLOAT", sizeof("REQUEST_TIME_FLOAT")-1, &tmp, ht);
+	ZVAL_LONG(&tmp, zend_dval_to_lval(Z_DVAL(tmp)));
+	php_register_variable_quick("REQUEST_TIME", sizeof("REQUEST_TIME")-1, &tmp, ht);
+}
+
+
+// ext/session/php_session.c
+
+/* Initialized in MINIT, readonly otherwise. */
+static int my_module_number = 0;
+
+/* Dispatched by RINIT and by php_session_destroy */
+static inline void php_rinit_session_globals(void) /* {{{ */
+{
+	/* Do NOT init PS(mod_user_names) here! */
+	/* TODO: These could be moved to MINIT and removed. These should be initialized by php_rshutdown_session_globals() always when execution is finished. */
+	PS(id) = NULL;
+	PS(session_status) = php_session_none;
+	PS(in_save_handler) = 0;
+	PS(set_handler) = 0;
+	PS(mod_data) = NULL;
+	PS(mod_user_is_open) = 0;
+	PS(define_sid) = 1;
+	PS(session_vars) = NULL;
+	PS(module_number) = my_module_number;
+	ZVAL_UNDEF(&PS(http_session_vars));
+}
+/* }}} */
+
+/* Dispatched by RSHUTDOWN and by php_session_destroy */
+static inline void php_rshutdown_session_globals(void) /* {{{ */
+{
+	/* Do NOT destroy PS(mod_user_names) here! */
+	if (!Z_ISUNDEF(PS(http_session_vars))) {
+		zval_ptr_dtor(&PS(http_session_vars));
+		ZVAL_UNDEF(&PS(http_session_vars));
+	}
+	if (PS(mod_data) || PS(mod_user_implemented)) {
+		zend_try {
+			PS(mod)->s_close(&PS(mod_data));
+		} zend_end_try();
+	}
+	if (PS(id)) {
+		zend_string_release_ex(PS(id), 0);
+		PS(id) = NULL;
+	}
+
+	if (PS(session_vars)) {
+		zend_string_release_ex(PS(session_vars), 0);
+		PS(session_vars) = NULL;
+	}
+
+	/* User save handlers may end up directly here by misuse, bugs in user script, etc. */
+	/* Set session status to prevent error while restoring save handler INI value. */
+	PS(session_status) = php_session_none;
+}
+/* }}} */
+
+// End of copied functions
+
 typedef struct frankenphp_server_context {
-	uintptr_t response_writer;
 	uintptr_t request;
+	uintptr_t requests_chan;
+	char *worker_filename;
 	char *cookie_data;
 } frankenphp_server_context;
 
+ZEND_BEGIN_ARG_INFO_EX(arginfo_frankenphp_handle_request, 0, 0, 1)
+    ZEND_ARG_CALLABLE_INFO(false, handler, false)
+ZEND_END_ARG_INFO()
+
+PHP_FUNCTION(frankenphp_handle_request) {
+	zend_fcall_info fci;
+	zend_fcall_info_cache fcc;
+
+	if (zend_parse_parameters(ZEND_NUM_ARGS(), "f", &fci, &fcc) == FAILURE) {
+		RETURN_THROWS();
+	}
+
+	frankenphp_server_context *ctx = SG(server_context);
+
+	uintptr_t request = go_frankenphp_worker_handle_request_start(ctx->requests_chan);
+	if (!request) {
+		RETURN_FALSE;
+	}
+
+	// Call the PHP func
+	zval retval = {0};
+	fci.size = sizeof fci;
+	fci.retval = &retval;
+
+	zend_call_function(&fci, &fcc);
+
+	php_session_flush(1);
+
+	go_frankenphp_worker_handle_request_end(request);
+
+	// Adapted from php_request_shutdown
+
+	zend_try {
+		php_output_end_all();
+	} zend_end_try();
+
+
+	zend_try {
+		php_output_deactivate();
+	} zend_end_try();
+
+	php_rshutdown_session_globals();
+	php_rinit_session_globals();
+	
+	RETURN_TRUE;
+}
+
+// Adapted from php_request_startup()
+int frankenphp_worker_reset_server_context() {
+	int retval = SUCCESS;
+
+	zend_try {	
+		//PG(in_error_log) = 0;
+		//PG(during_request_startup) = 1;
+
+		php_output_activate();
+
+		/* initialize global variables */
+		//PG(modules_activated) = 0;
+		PG(header_is_being_sent) = 0;
+		PG(connection_status) = PHP_CONNECTION_NORMAL;
+		//PG(in_user_include) = 0;
+
+		// Keep the current execution context
+		//zend_activate();
+		sapi_activate();
+
+#ifdef ZEND_SIGNALS
+		//zend_signal_activate();
+#endif
+
+		if (PG(max_input_time) == -1) {
+			zend_set_timeout(EG(timeout_seconds), 1);
+		} else {
+			zend_set_timeout(PG(max_input_time), 1);
+		}
+
+		/* Disable realpath cache if an open_basedir is set */
+		//if (PG(open_basedir) && *PG(open_basedir)) {
+		//	CWDG(realpath_cache_size_limit) = 0;
+		//}
+
+		if (PG(expose_php)) {
+			sapi_add_header(SAPI_PHP_VERSION_HEADER, sizeof(SAPI_PHP_VERSION_HEADER)-1, 1);
+		}
+
+		if (PG(output_handler) && PG(output_handler)[0]) {
+			zval oh;
+
+			ZVAL_STRING(&oh, PG(output_handler));
+			php_output_start_user(&oh, 0, PHP_OUTPUT_HANDLER_STDFLAGS);
+			zval_ptr_dtor(&oh);
+		} else if (PG(output_buffering)) {
+			php_output_start_user(NULL, PG(output_buffering) > 1 ? PG(output_buffering) : 0, PHP_OUTPUT_HANDLER_STDFLAGS);
+		} else if (PG(implicit_flush)) {
+			php_output_set_implicit_flush(1);
+		}
+
+		/* We turn this off in php_execute_script() */
+		/* PG(during_request_startup) = 0; */
+
+		php_register_server_variables();
+
+		zend_hash_update(&EG(symbol_table), ZSTR_KNOWN(ZEND_STR_AUTOGLOBAL_SERVER), &PG(http_globals)[TRACK_VARS_SERVER]);
+		Z_ADDREF(PG(http_globals)[TRACK_VARS_SERVER]);
+
+		php_hash_environment();
+	} zend_catch {
+		retval = FAILURE;
+	} zend_end_try();
+
+	return retval;
+}
+
+static const zend_function_entry frankenphp_ext_functions[] = {
+    PHP_FE(frankenphp_handle_request, arginfo_frankenphp_handle_request)
+    PHP_FE_END
+};
+
+static zend_module_entry frankenphp_module = {
+    STANDARD_MODULE_HEADER,
+    "frankenphp",
+    frankenphp_ext_functions,    /* function table */
+    NULL,  					     /* initialization */
+    NULL,                        /* shutdown */
+    NULL,                        /* request initialization */
+    NULL,                        /* request shutdown */
+    NULL,                        /* information */
+    "dev",
+    STANDARD_MODULE_PROPERTIES
+};
+
+uintptr_t frankenphp_clean_server_context() {
+	frankenphp_server_context *ctx = SG(server_context);
+	if (ctx == NULL) return 0;
+
+	free(SG(request_info.auth_password));
+	SG(request_info.auth_password) = NULL;
+
+	free(SG(request_info.auth_user));
+	SG(request_info.auth_user) = NULL;
+
+	free((char *) SG(request_info.request_method));
+	SG(request_info.request_method) = NULL;
+
+	free(SG(request_info.query_string));
+	SG(request_info.query_string) = NULL;
+
+	free((char *) SG(request_info.content_type));
+	SG(request_info.content_type) = NULL;
+
+	free(SG(request_info.path_translated));
+	SG(request_info.path_translated) = NULL;
+
+	free(SG(request_info.request_uri));
+	SG(request_info.request_uri) = NULL;
+
+	return ctx->request;
+}
+
+uintptr_t frankenphp_request_shutdown()
+{
+	php_request_shutdown((void *) 0);
+
+	frankenphp_server_context *ctx = SG(server_context);
+
+	free(ctx->cookie_data);
+	((frankenphp_server_context*) SG(server_context))->cookie_data = NULL;
+
+	uintptr_t rh = frankenphp_clean_server_context();
+
+	free(ctx);
+	SG(server_context) = NULL;
+
+	return rh;
+}
+
+// set worker to 0 if not in worker mode
+int frankenphp_create_server_context(uintptr_t requests_chan, char* worker_filename)
+{
+	frankenphp_server_context *ctx;
+
+	(void) ts_resource(0);
+
+	// todo: use a pool
+	ctx = malloc(sizeof(frankenphp_server_context));
+	if (ctx == NULL) return FAILURE;
+
+	ctx->request = 0;
+	ctx->requests_chan = requests_chan;
+	ctx->worker_filename = worker_filename;
+	ctx->cookie_data = NULL;
+
+	SG(server_context) = ctx;
+
+	return SUCCESS;
+}
+
+void frankenphp_update_server_context(
+	uintptr_t request,
+
+	const char *request_method,
+	char *query_string,
+	zend_long content_length,
+	char *path_translated,
+	char *request_uri,
+	const char *content_type,
+	char *auth_user,
+	char *auth_password,
+	int proto_num
+) {
+	frankenphp_server_context *ctx = SG(server_context);
+
+	ctx->request = request;
+
+	SG(request_info).auth_password = auth_password;
+	SG(request_info).auth_user = auth_user;
+	SG(request_info).request_method = request_method;
+	SG(request_info).query_string = query_string;
+	SG(request_info).content_type = content_type;
+	SG(request_info).content_length = content_length;
+	SG(request_info).path_translated = path_translated;
+	SG(request_info).request_uri = request_uri;
+	SG(request_info).proto_num = proto_num;
+}
+
 static int frankenphp_startup(sapi_module_struct *sapi_module)
 {
-	return php_module_startup(sapi_module, NULL, 0);
+	return php_module_startup(sapi_module, &frankenphp_module, 1);
 }
 
 static int frankenphp_deactivate(void)
@@ -30,7 +367,9 @@ static size_t frankenphp_ub_write(const char *str, size_t str_length)
 {
 	frankenphp_server_context* ctx = SG(server_context);
 
-	return go_ub_write(ctx->response_writer, (char *) str, str_length);
+	if (ctx->request == 0) return 0; // TODO: write on stdout?
+
+	return go_ub_write(ctx->request, (char *) str, str_length);
 }
 
 static int frankenphp_send_headers(sapi_headers_struct *sapi_headers)
@@ -44,9 +383,11 @@ static int frankenphp_send_headers(sapi_headers_struct *sapi_headers)
 	int status;
 	frankenphp_server_context* ctx = SG(server_context);
 
+	if (ctx->request == 0) return SAPI_HEADER_SEND_FAILED;
+
 	h = zend_llist_get_first_ex(&sapi_headers->headers, &pos);
 	while (h) {
-		go_add_header(ctx->response_writer, h->header, h->header_len);
+		go_add_header(ctx->request, h->header, h->header_len);
 		h = zend_llist_get_next_ex(&sapi_headers->headers, &pos);
 	}
 
@@ -57,7 +398,7 @@ static int frankenphp_send_headers(sapi_headers_struct *sapi_headers)
 		status = atoi((SG(sapi_headers).http_status_line) + 9);
 	}
 
-	go_write_header(ctx->response_writer, status);
+	go_write_header(ctx->request, status);
 
 	return SAPI_HEADER_SENT_SUCCESSFULLY;
 }
@@ -66,12 +407,17 @@ static size_t frankenphp_read_post(char *buffer, size_t count_bytes)
 {
 	frankenphp_server_context* ctx = SG(server_context);
 
+	if (ctx->request == 0) return 0;
+
 	return go_read_post(ctx->request, buffer, count_bytes);
 }
 
 static char* frankenphp_read_cookies(void)
 {
 	frankenphp_server_context* ctx = SG(server_context);
+
+	if (ctx->request == 0) return "";
+
 	ctx->cookie_data = go_read_cookies(ctx->request);
 
 	return ctx->cookie_data;
@@ -82,6 +428,12 @@ static void frankenphp_register_variables(zval *track_vars_array)
 	// https://www.php.net/manual/en/reserved.variables.server.php
 	frankenphp_server_context* ctx = SG(server_context);
 
+	if (ctx->request == 0 && ctx->worker_filename != NULL) {
+		// todo: also register PHP_SELF etc
+		php_register_variable_safe("SCRIPT_FILENAME", ctx->worker_filename, strlen(ctx->worker_filename), track_vars_array);
+	}
+
+	// todo: import or not environment variables set in the parent process?
 	//php_import_environment_variables(track_vars_array);
 
 	go_register_variables(ctx->request, track_vars_array);
@@ -92,7 +444,7 @@ static void frankenphp_log_message(const char *message, int syslog_type_int)
 	// TODO: call Go logger
 }
 
-sapi_module_struct frankenphp_module = {
+sapi_module_struct frankenphp_sapi_module = {
 	"frankenphp",                       /* name */
 	"FrankenPHP", 						/* pretty name */
 
@@ -131,13 +483,9 @@ int frankenphp_init() {
 
     php_tsrm_startup();
     zend_signal_startup();
-    sapi_startup(&frankenphp_module);
-
-	if (frankenphp_module.startup(&frankenphp_module) == FAILURE) {
-		return FAILURE;
-	}
+    sapi_startup(&frankenphp_sapi_module);
 
-    return SUCCESS;
+	return frankenphp_sapi_module.startup(&frankenphp_sapi_module);
 }
 
 void frankenphp_shutdown()
@@ -147,55 +495,20 @@ void frankenphp_shutdown()
     tsrm_shutdown();
 }
 
-int frankenphp_request_startup(
-	uintptr_t response_writer,
-	uintptr_t request,
-
-	const char *request_method,
-	char *query_string,
-	zend_long content_length,
-	char *path_translated,
-	char *request_uri,
-	const char *content_type,
-	char *auth_user,
-	char *auth_password,
-	int proto_num
-) {
-	frankenphp_server_context *ctx;
-
-	(void) ts_resource(0);
-
-	ctx = emalloc(sizeof(frankenphp_server_context));
-	if (ctx == NULL) {
-		return FAILURE;
+int frankenphp_request_startup()
+{
+	if (php_request_startup() == SUCCESS) {
+		return SUCCESS;
 	}
 
-	ctx->response_writer = response_writer;
-	ctx->request = request;
-
-	SG(server_context) = ctx;
+	fprintf(stderr, "problem in php_request_startup\n");
 
-	SG(request_info).request_method = request_method;
-	SG(request_info).query_string = query_string;
-	SG(request_info).content_length = content_length;
-	SG(request_info).path_translated = path_translated;
-	SG(request_info).request_uri = request_uri;
-	SG(request_info).content_type = content_type;
-	if (auth_user != NULL)
-		SG(request_info).auth_user = estrdup(auth_user);
-	if (auth_password != NULL)
-		SG(request_info).auth_password = estrdup(auth_password);
-	SG(request_info).proto_num = proto_num;
-
-	if (php_request_startup() == FAILURE) {
-		php_request_shutdown(NULL);
-		SG(server_context) = NULL;
-		free(ctx);
-
-		return FAILURE;
-	}
+	php_request_shutdown((void *) 0);
+	frankenphp_server_context *ctx = SG(server_context);
+	SG(server_context) = NULL;
+	free(ctx);
 
-	return SUCCESS;
+	return FAILURE;
 }
 
 int frankenphp_execute_script(const char* file_name)
@@ -208,17 +521,8 @@ int frankenphp_execute_script(const char* file_name)
 	zend_first_try {
 		status = php_execute_script(&file_handle);
 	} zend_catch {
-    	/* int exit_status = EG(exit_status); */ \
+    	/* int exit_status = EG(exit_status); */
 	} zend_end_try();
 
 	return status;
 }
-
-void frankenphp_request_shutdown()
-{
-	frankenphp_server_context *ctx = SG(server_context);
-	php_request_shutdown(NULL);
-	if (ctx->cookie_data != NULL) free(ctx->cookie_data);
-	efree(ctx);
-	SG(server_context) = NULL;
-}

+ 86 - 295
frankenphp.go

@@ -2,7 +2,7 @@ package frankenphp
 
 // #cgo CFLAGS: -Wall -Wno-unused-variable
 // #cgo CFLAGS: -I/usr/local/include/php -I/usr/local/include/php/Zend -I/usr/local/include/php/TSRM -I/usr/local/include/php/main
-// #cgo LDFLAGS: -L/usr/local/lib -lphp
+// #cgo LDFLAGS: -L/usr/local/lib -L/opt/homebrew/opt/libiconv/lib -L/usr/lib -lphp -lxml2 -liconv -lresolv -lsqlite3
 // #include <stdlib.h>
 // #include <stdint.h>
 // #include "php_variables.h"
@@ -10,13 +10,11 @@ package frankenphp
 import "C"
 import (
 	"context"
-	"crypto/tls"
 	"fmt"
 	"io"
 	"log"
-	"net"
 	"net/http"
-	"path/filepath"
+	"runtime"
 	"runtime/cgo"
 	"strconv"
 	"strings"
@@ -26,9 +24,13 @@ import (
 
 var started int32
 
-type ContextKey string
+type key int
 
-const FrankenPHPContextKey ContextKey = "frankenphp"
+var contextKey key
+
+func init() {
+	log.SetFlags(log.LstdFlags | log.Lshortfile)
+}
 
 // FrankenPHP executes PHP scripts.
 type FrankenPHPContext struct {
@@ -56,10 +58,16 @@ type FrankenPHPContext struct {
 	// CGI-like environment variables that will be available in $_SERVER.
 	// This map is populated automatically, exisiting key are never replaced.
 	Env map[string]string
+
+	populated    bool
+	authPassword string
+
+	responseWriter http.ResponseWriter
+	done           chan interface{}
 }
 
 func NewRequestWithContext(r *http.Request, documentRoot string) *http.Request {
-	ctx := context.WithValue(r.Context(), FrankenPHPContextKey, &FrankenPHPContext{
+	ctx := context.WithValue(r.Context(), contextKey, &FrankenPHPContext{
 		DocumentRoot: documentRoot,
 		SplitPath:    []string{".php"},
 		Env:          make(map[string]string),
@@ -68,67 +76,62 @@ func NewRequestWithContext(r *http.Request, documentRoot string) *http.Request {
 	return r.WithContext(ctx)
 }
 
+func FromContext(ctx context.Context) (fctx *FrankenPHPContext, ok bool) {
+	fctx, ok = ctx.Value(contextKey).(*FrankenPHPContext)
+	return
+}
+
 // Startup starts the PHP engine.
+// Startup and Shutdown must be called in the same goroutine (ideally in the main function).
 func Startup() error {
 	if atomic.LoadInt32(&started) > 0 {
 		return nil
 	}
+	atomic.StoreInt32(&started, 1)
+
+	runtime.LockOSThread()
 
 	if C.frankenphp_init() < 0 {
 		return fmt.Errorf(`ZTS is not enabled, recompile PHP using the "--enable-zts" configuration option`)
 	}
-	atomic.StoreInt32(&started, 1)
 
 	return nil
 }
 
 // Shutdown stops the PHP engine.
+// Shutdown and Startup must be called in the same goroutine (ideally in the main function).
 func Shutdown() {
 	if atomic.LoadInt32(&started) < 1 {
 		return
 	}
+	atomic.StoreInt32(&started, 0)
 
 	C.frankenphp_shutdown()
-	atomic.StoreInt32(&started, 0)
 }
 
-func ExecuteScript(responseWriter http.ResponseWriter, request *http.Request) error {
-	if atomic.LoadInt32(&started) < 1 {
-		if err := Startup(); err != nil {
-			return err
-		}
-	}
-
-	authPassword, err := populateEnv(request)
-	if err != nil {
+func updateServerContext(request *http.Request) error {
+	if err := populateEnv(request); err != nil {
 		return err
 	}
 
-	fc := request.Context().Value(FrankenPHPContextKey).(*FrankenPHPContext)
+	fc, ok := FromContext(request.Context())
+	if !ok {
+		panic("not a FrankenPHP request")
+	}
 
 	var cAuthUser, cAuthPassword *C.char
-	if authPassword != "" {
-		cAuthPassword = C.CString(authPassword)
-		defer C.free(unsafe.Pointer(cAuthPassword))
+	if fc.authPassword != "" {
+		cAuthPassword = C.CString(fc.authPassword)
 	}
 
 	if authUser := fc.Env["REMOTE_USER"]; authUser != "" {
 		cAuthUser = C.CString(authUser)
-		defer C.free(unsafe.Pointer(cAuthUser))
 	}
 
-	wh := cgo.NewHandle(responseWriter)
-	defer wh.Delete()
-
 	rh := cgo.NewHandle(request)
-	defer rh.Delete()
 
 	cMethod := C.CString(request.Method)
-	defer C.free(unsafe.Pointer(cMethod))
-
 	cQueryString := C.CString(request.URL.RawQuery)
-	defer C.free(unsafe.Pointer(cQueryString))
-
 	contentLengthStr := request.Header.Get("Content-Length")
 	contentLength := 0
 	if contentLengthStr != "" {
@@ -139,20 +142,16 @@ func ExecuteScript(responseWriter http.ResponseWriter, request *http.Request) er
 	var cContentType *C.char
 	if contentType != "" {
 		cContentType = C.CString(contentType)
-		defer C.free(unsafe.Pointer(cContentType))
 	}
 
 	var cPathTranslated *C.char
 	if pathTranslated := fc.Env["PATH_TRANSLATED"]; pathTranslated != "" {
 		cPathTranslated = C.CString(pathTranslated)
-		defer C.free(unsafe.Pointer(cPathTranslated))
 	}
 
 	cRequestUri := C.CString(request.URL.RequestURI())
-	defer C.free(unsafe.Pointer(cRequestUri))
 
-	if C.frankenphp_request_startup(
-		C.uintptr_t(wh),
+	C.frankenphp_update_server_context(
 		C.uintptr_t(rh),
 
 		cMethod,
@@ -164,289 +163,77 @@ func ExecuteScript(responseWriter http.ResponseWriter, request *http.Request) er
 		cAuthUser,
 		cAuthPassword,
 		C.int(request.ProtoMajor*1000+request.ProtoMinor),
-	) < 0 {
-		return fmt.Errorf("error during PHP request startup")
-	}
-
-	cFileName := C.CString(fc.Env["SCRIPT_FILENAME"])
-	defer C.free(unsafe.Pointer(cFileName))
-
-	C.frankenphp_execute_script(cFileName)
-	C.frankenphp_request_shutdown()
+	)
 
 	return nil
 }
 
-// buildEnv returns a set of CGI environment variables for the request.
-//
-// TODO: handle this case https://github.com/caddyserver/caddy/issues/3718
-// Inspired by https://github.com/caddyserver/caddy/blob/master/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go
-func populateEnv(request *http.Request) (authPassword string, err error) {
-	fc := request.Context().Value(FrankenPHPContextKey).(*FrankenPHPContext)
-
-	_, addrOk := fc.Env["REMOTE_ADDR"]
-	_, portOk := fc.Env["REMOTE_PORT"]
-	if !addrOk || !portOk {
-		// Separate remote IP and port; more lenient than net.SplitHostPort
-		var ip, port string
-		if idx := strings.LastIndex(request.RemoteAddr, ":"); idx > -1 {
-			ip = request.RemoteAddr[:idx]
-			port = request.RemoteAddr[idx+1:]
-		} else {
-			ip = request.RemoteAddr
-		}
-
-		// Remove [] from IPv6 addresses
-		ip = strings.Replace(ip, "[", "", 1)
-		ip = strings.Replace(ip, "]", "", 1)
-
-		if _, ok := fc.Env["REMOTE_ADDR"]; !ok {
-			fc.Env["REMOTE_ADDR"] = ip
-		}
-		if _, ok := fc.Env["REMOTE_HOST"]; !ok {
-			fc.Env["REMOTE_HOST"] = ip // For speed, remote host lookups disabled
-		}
-		if _, ok := fc.Env["REMOTE_PORT"]; !ok {
-			fc.Env["REMOTE_PORT"] = port
-		}
-	}
-
-	if _, ok := fc.Env["DOCUMENT_ROOT"]; !ok {
-		// make sure file root is absolute
-		root, err := filepath.Abs(fc.DocumentRoot)
-		if err != nil {
-			return "", err
-		}
-
-		if fc.ResolveRootSymlink {
-			if root, err = filepath.EvalSymlinks(root); err != nil {
-				return "", err
-			}
-		}
-
-		fc.Env["DOCUMENT_ROOT"] = root
-	}
-
-	fpath := request.URL.Path
-	scriptName := fpath
-
-	docURI := fpath
-	// split "actual path" from "path info" if configured
-	if splitPos := splitPos(fc, fpath); splitPos > -1 {
-		docURI = fpath[:splitPos]
-		fc.Env["PATH_INFO"] = fpath[splitPos:]
-
-		// Strip PATH_INFO from SCRIPT_NAME
-		scriptName = strings.TrimSuffix(scriptName, fc.Env["PATH_INFO"])
-	}
-
-	// SCRIPT_FILENAME is the absolute path of SCRIPT_NAME
-	scriptFilename := sanitizedPathJoin(fc.Env["DOCUMENT_ROOT"], scriptName)
-
-	// Ensure the SCRIPT_NAME has a leading slash for compliance with RFC3875
-	// Info: https://tools.ietf.org/html/rfc3875#section-4.1.13
-	if scriptName != "" && !strings.HasPrefix(scriptName, "/") {
-		scriptName = "/" + scriptName
-	}
-
-	if _, ok := fc.Env["DOCUMENT_URI"]; !ok {
-		fc.Env["DOCUMENT_URI"] = docURI
-	}
-	if _, ok := fc.Env["SCRIPT_FILENAME"]; !ok {
-		fc.Env["SCRIPT_FILENAME"] = scriptFilename
-	}
-	if _, ok := fc.Env["SCRIPT_NAME"]; !ok {
-		fc.Env["SCRIPT_NAME"] = scriptName
-	}
-
-	if _, ok := fc.Env["REQUEST_SCHEME"]; !ok {
-		if request.TLS == nil {
-			fc.Env["REQUEST_SCHEME"] = "http"
-		} else {
-			fc.Env["REQUEST_SCHEME"] = "https"
-		}
-	}
-
-	if request.TLS != nil {
-		if _, ok := fc.Env["HTTPS"]; !ok {
-			fc.Env["HTTPS"] = "on"
-		}
-
-		// and pass the protocol details in a manner compatible with apache's mod_ssl
-		// (which is why these have a SSL_ prefix and not TLS_).
-		_, sslProtocolOk := fc.Env["SSL_PROTOCOL"]
-		v, versionOk := tlsProtocolStrings[request.TLS.Version]
-		if !sslProtocolOk && versionOk {
-			fc.Env["SSL_PROTOCOL"] = v
-		}
-	}
-
-	_, serverNameOk := fc.Env["SERVER_NAME"]
-	_, serverPortOk := fc.Env["SERVER_PORT"]
-	if !serverNameOk || !serverPortOk {
-		reqHost, reqPort, err := net.SplitHostPort(request.Host)
-		if err == nil {
-			if !serverNameOk {
-				fc.Env["SERVER_NAME"] = reqHost
-			}
-
-			// compliance with the CGI specification requires that
-			// SERVER_PORT should only exist if it's a valid numeric value.
-			// Info: https://www.ietf.org/rfc/rfc3875 Page 18
-			if !serverPortOk && reqPort != "" {
-				fc.Env["SERVER_PORT"] = reqPort
-			}
-		} else if !serverNameOk {
-			// whatever, just assume there was no port
-			fc.Env["SERVER_NAME"] = request.Host
-		}
-	}
-
-	// Variables defined in CGI 1.1 spec
-	// Some variables are unused but cleared explicitly to prevent
-	// the parent environment from interfering.
-	// We never override an entry previously set
-	if _, ok := fc.Env["REMOTE_IDENT"]; !ok {
-		fc.Env["REMOTE_IDENT"] = "" // Not used
-	}
-	if _, ok := fc.Env["AUTH_TYPE"]; !ok {
-		fc.Env["AUTH_TYPE"] = "" // Not used
-	}
-	if _, ok := fc.Env["CONTENT_LENGTH"]; !ok {
-		fc.Env["CONTENT_LENGTH"] = request.Header.Get("Content-Length")
-	}
-	if _, ok := fc.Env["CONTENT_TYPE"]; !ok {
-		fc.Env["CONTENT_TYPE"] = request.Header.Get("Content-Type")
-	}
-	if _, ok := fc.Env["GATEWAY_INTERFACE"]; !ok {
-		fc.Env["GATEWAY_INTERFACE"] = "CGI/1.1"
-	}
-	if _, ok := fc.Env["QUERY_STRING"]; !ok {
-		fc.Env["QUERY_STRING"] = request.URL.RawQuery
-	}
-	if _, ok := fc.Env["QUERY_STRING"]; !ok {
-		fc.Env["QUERY_STRING"] = request.URL.RawQuery
-	}
-	if _, ok := fc.Env["REQUEST_METHOD"]; !ok {
-		fc.Env["REQUEST_METHOD"] = request.Method
-	}
-	if _, ok := fc.Env["SERVER_PROTOCOL"]; !ok {
-		fc.Env["SERVER_PROTOCOL"] = request.Proto
-	}
-	if _, ok := fc.Env["SERVER_SOFTWARE"]; !ok {
-		fc.Env["SERVER_SOFTWARE"] = "FrankenPHP"
-	}
-	if _, ok := fc.Env["HTTP_HOST"]; !ok {
-		fc.Env["HTTP_HOST"] = request.Host // added here, since not always part of headers
-	}
-	if _, ok := fc.Env["REQUEST_URI"]; !ok {
-		fc.Env["REQUEST_URI"] = request.URL.RequestURI()
+func ExecuteScript(responseWriter http.ResponseWriter, request *http.Request) error {
+	if atomic.LoadInt32(&started) < 1 {
+		panic("FrankenPHP isn't started, call frankenphp.Startup()")
 	}
 
-	// compliance with the CGI specification requires that
-	// PATH_TRANSLATED should only exist if PATH_INFO is defined.
-	// Info: https://www.ietf.org/rfc/rfc3875 Page 14
-	if fc.Env["PATH_INFO"] != "" {
-		fc.Env["PATH_TRANSLATED"] = sanitizedPathJoin(fc.Env["DOCUMENT_ROOT"], fc.Env["PATH_INFO"]) // Info: http://www.oreilly.com/openbook/cgi/ch02_04.html
-	}
+	runtime.LockOSThread()
+	// todo: check if it's ok or not to call runtime.UnlockOSThread() to reuse this thread
 
-	// Add all HTTP headers to env variables
-	for field, val := range request.Header {
-		k := "HTTP_" + headerNameReplacer.Replace(strings.ToUpper(field))
-		if _, ok := fc.Env[k]; !ok {
-			fc.Env[k] = strings.Join(val, ", ")
-		}
+	if C.frankenphp_create_server_context(0, nil) < 0 {
+		return fmt.Errorf("error during request context creation")
 	}
 
-	if _, ok := fc.Env["REMOTE_USER"]; !ok {
-		var (
-			authUser string
-			ok       bool
-		)
-		authUser, authPassword, ok = request.BasicAuth()
-		if ok {
-			fc.Env["REMOTE_USER"] = authUser
-		}
+	if err := updateServerContext(request); err != nil {
+		return err
 	}
 
-	return authPassword, nil
-}
-
-// splitPos returns the index where path should
-// be split based on SplitPath.
-//
-// Adapted from https://github.com/caddyserver/caddy/blob/master/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go
-// Copyright 2015 Matthew Holt and The Caddy Authors
-func splitPos(fc *FrankenPHPContext, path string) int {
-	if len(fc.SplitPath) == 0 {
-		return 0
+	if C.frankenphp_request_startup() < 0 {
+		return fmt.Errorf("error during PHP request startup")
 	}
 
-	lowerPath := strings.ToLower(path)
-	for _, split := range fc.SplitPath {
-		if idx := strings.Index(lowerPath, strings.ToLower(split)); idx > -1 {
-			return idx + len(split)
-		}
-	}
-	return -1
-}
+	fc := request.Context().Value(contextKey).(*FrankenPHPContext)
+	fc.responseWriter = responseWriter
 
-// Map of supported protocols to Apache ssl_mod format
-// Note that these are slightly different from SupportedProtocols in caddytls/config.go
-var tlsProtocolStrings = map[uint16]string{
-	tls.VersionTLS10: "TLSv1",
-	tls.VersionTLS11: "TLSv1.1",
-	tls.VersionTLS12: "TLSv1.2",
-	tls.VersionTLS13: "TLSv1.3",
-}
+	cFileName := C.CString(fc.Env["SCRIPT_FILENAME"])
+	defer C.free(unsafe.Pointer(cFileName))
 
-var headerNameReplacer = strings.NewReplacer(" ", "_", "-", "_")
-
-// SanitizedPathJoin performs filepath.Join(root, reqPath) that
-// is safe against directory traversal attacks. It uses logic
-// similar to that in the Go standard library, specifically
-// in the implementation of http.Dir. The root is assumed to
-// be a trusted path, but reqPath is not; and the output will
-// never be outside of root. The resulting path can be used
-// with the local file system.
-//
-// Adapted from https://github.com/caddyserver/caddy/blob/master/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go
-// Copyright 2015 Matthew Holt and The Caddy Authors
-func sanitizedPathJoin(root, reqPath string) string {
-	if root == "" {
-		root = "."
+	if C.frankenphp_execute_script(cFileName) < 0 {
+		return fmt.Errorf("error during PHP script execution")
 	}
 
-	path := filepath.Join(root, filepath.Clean("/"+reqPath))
+	rh := C.frankenphp_clean_server_context()
+	C.frankenphp_request_shutdown()
 
-	// filepath.Join also cleans the path, and cleaning strips
-	// the trailing slash, so we need to re-add it afterwards.
-	// if the length is 1, then it's a path to the root,
-	// and that should return ".", so we don't append the separator.
-	if strings.HasSuffix(reqPath, "/") && len(reqPath) > 1 {
-		path += separator
-	}
+	cgo.Handle(rh).Delete()
 
-	return path
+	return nil
 }
 
-const separator = string(filepath.Separator)
-
 //export go_ub_write
-func go_ub_write(wh C.uintptr_t, cString *C.char, length C.int) C.size_t {
-	w := cgo.Handle(wh).Value().(http.ResponseWriter)
-	i, _ := w.Write([]byte(C.GoStringN(cString, length)))
+func go_ub_write(rh C.uintptr_t, cString *C.char, length C.int) C.size_t {
+	r := cgo.Handle(rh).Value().(*http.Request)
+	fc := r.Context().Value(contextKey).(*FrankenPHPContext)
+
+	i, _ := fc.responseWriter.Write([]byte(C.GoStringN(cString, length)))
 
 	return C.size_t(i)
 }
 
 //export go_register_variables
 func go_register_variables(rh C.uintptr_t, trackVarsArray *C.zval) {
-	r := cgo.Handle(rh).Value().(*http.Request)
-	for k, v := range r.Context().Value(FrankenPHPContextKey).(*FrankenPHPContext).Env {
+	var env map[string]string
+	if rh == 0 {
+		// Worker mode, waiting for a request, initialize some useful variables
+		env = map[string]string{"FRANKENPHP_WORKER": "1"}
+	} else {
+		r := cgo.Handle(rh).Value().(*http.Request)
+		env = r.Context().Value(contextKey).(*FrankenPHPContext).Env
+	}
+
+	env[fmt.Sprintf("REQUEST_%d", rh)] = "on"
+
+	for k, v := range env {
 		ck := C.CString(k)
 		cv := C.CString(v)
-		C.php_register_variable_safe(ck, cv, C.size_t(len(v)), trackVarsArray)
+
+		C.php_register_variable(ck, cv, trackVarsArray)
 
 		C.free(unsafe.Pointer(ck))
 		C.free(unsafe.Pointer(cv))
@@ -454,8 +241,9 @@ func go_register_variables(rh C.uintptr_t, trackVarsArray *C.zval) {
 }
 
 //export go_add_header
-func go_add_header(wh C.uintptr_t, cString *C.char, length C.int) {
-	w := cgo.Handle(wh).Value().(http.ResponseWriter)
+func go_add_header(rh C.uintptr_t, cString *C.char, length C.int) {
+	r := cgo.Handle(rh).Value().(*http.Request)
+	fc := r.Context().Value(contextKey).(*FrankenPHPContext)
 
 	parts := strings.SplitN(C.GoStringN(cString, length), ": ", 2)
 	if len(parts) != 2 {
@@ -464,13 +252,15 @@ func go_add_header(wh C.uintptr_t, cString *C.char, length C.int) {
 		return
 	}
 
-	w.Header().Add(parts[0], parts[1])
+	fc.responseWriter.Header().Add(parts[0], parts[1])
 }
 
 //export go_write_header
-func go_write_header(wh C.uintptr_t, status C.int) {
-	w := cgo.Handle(wh).Value().(http.ResponseWriter)
-	w.WriteHeader(int(status))
+func go_write_header(rh C.uintptr_t, status C.int) {
+	r := cgo.Handle(rh).Value().(*http.Request)
+	fc := r.Context().Value(contextKey).(*FrankenPHPContext)
+
+	fc.responseWriter.WriteHeader(int(status))
 }
 
 //export go_read_post
@@ -484,6 +274,7 @@ func go_read_post(rh C.uintptr_t, cBuf *C.char, countBytes C.size_t) C.size_t {
 	}
 
 	if readBytes != 0 {
+		// todo: memory leak?
 		C.memcpy(unsafe.Pointer(cBuf), unsafe.Pointer(&p[0]), C.size_t(readBytes))
 	}
 

+ 7 - 5
frankenphp.h

@@ -6,8 +6,8 @@
 int frankenphp_init();
 void frankenphp_shutdown();
 
-int frankenphp_request_startup(
-	uintptr_t response_writer,
+int frankenphp_create_server_context(uintptr_t requests_chan, char *worker_filename);
+void frankenphp_update_server_context(
 	uintptr_t request,
 
 	const char *request_method,
@@ -20,8 +20,10 @@ int frankenphp_request_startup(
 	char *auth_password,
 	int proto_num
 );
-void frankenphp_request_shutdown();
-
-int frankenphp_execute_script(const char* file_name);
+int frankenphp_worker_reset_server_context();
+uintptr_t frankenphp_clean_server_context();
+int frankenphp_request_startup();
+int frankenphp_execute_script(const char *file_name);
+uintptr_t frankenphp_request_shutdown();
 
 #endif

Some files were not shown because too many files changed in this diff