mirror of
https://github.com/dunglas/frankenphp.git
synced 2025-12-24 13:38:11 +08:00
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
This commit is contained in:
@@ -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"]
|
||||
|
||||
37
README.md
37
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
|
||||
|
||||
```
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
142
caddy/go.mod
142
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
|
||||
)
|
||||
|
||||
574
caddy/go.sum
574
caddy/go.sum
File diff suppressed because it is too large
Load Diff
273
cgi.go
Normal file
273
cgi.go
Normal file
@@ -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)
|
||||
438
frankenphp.c
438
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);
|
||||
sapi_startup(&frankenphp_sapi_module);
|
||||
|
||||
if (frankenphp_module.startup(&frankenphp_module) == FAILURE) {
|
||||
return FAILURE;
|
||||
}
|
||||
|
||||
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;
|
||||
fprintf(stderr, "problem in php_request_startup\n");
|
||||
|
||||
SG(server_context) = ctx;
|
||||
php_request_shutdown((void *) 0);
|
||||
frankenphp_server_context *ctx = SG(server_context);
|
||||
SG(server_context) = NULL;
|
||||
free(ctx);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
385
frankenphp.go
385
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
|
||||
}
|
||||
func ExecuteScript(responseWriter http.ResponseWriter, request *http.Request) error {
|
||||
if atomic.LoadInt32(&started) < 1 {
|
||||
panic("FrankenPHP isn't started, call frankenphp.Startup()")
|
||||
}
|
||||
|
||||
if _, ok := fc.Env["DOCUMENT_ROOT"]; !ok {
|
||||
// make sure file root is absolute
|
||||
root, err := filepath.Abs(fc.DocumentRoot)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
runtime.LockOSThread()
|
||||
// todo: check if it's ok or not to call runtime.UnlockOSThread() to reuse this thread
|
||||
|
||||
if fc.ResolveRootSymlink {
|
||||
if root, err = filepath.EvalSymlinks(root); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
fc.Env["DOCUMENT_ROOT"] = root
|
||||
if C.frankenphp_create_server_context(0, nil) < 0 {
|
||||
return fmt.Errorf("error during request context creation")
|
||||
}
|
||||
|
||||
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"])
|
||||
if err := updateServerContext(request); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 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 C.frankenphp_request_startup() < 0 {
|
||||
return fmt.Errorf("error during PHP request startup")
|
||||
}
|
||||
|
||||
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
|
||||
fc := request.Context().Value(contextKey).(*FrankenPHPContext)
|
||||
fc.responseWriter = responseWriter
|
||||
|
||||
cFileName := C.CString(fc.Env["SCRIPT_FILENAME"])
|
||||
defer C.free(unsafe.Pointer(cFileName))
|
||||
|
||||
if C.frankenphp_execute_script(cFileName) < 0 {
|
||||
return fmt.Errorf("error during PHP script execution")
|
||||
}
|
||||
|
||||
if _, ok := fc.Env["REQUEST_SCHEME"]; !ok {
|
||||
if request.TLS == nil {
|
||||
fc.Env["REQUEST_SCHEME"] = "http"
|
||||
} else {
|
||||
fc.Env["REQUEST_SCHEME"] = "https"
|
||||
}
|
||||
}
|
||||
rh := C.frankenphp_clean_server_context()
|
||||
C.frankenphp_request_shutdown()
|
||||
|
||||
if request.TLS != nil {
|
||||
if _, ok := fc.Env["HTTPS"]; !ok {
|
||||
fc.Env["HTTPS"] = "on"
|
||||
}
|
||||
cgo.Handle(rh).Delete()
|
||||
|
||||
// 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, authPassword, ok = request.BasicAuth()
|
||||
if ok {
|
||||
fc.Env["REMOTE_USER"] = authUser
|
||||
}
|
||||
}
|
||||
|
||||
return authPassword, nil
|
||||
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)
|
||||
|
||||
//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))
|
||||
}
|
||||
|
||||
|
||||
12
frankenphp.h
12
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
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
@@ -14,177 +15,292 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestStartup(t *testing.T) {
|
||||
defer frankenphp.Shutdown()
|
||||
assert.Nil(t, frankenphp.Startup())
|
||||
}
|
||||
|
||||
func setRequestContext(t *testing.T, r *http.Request) *http.Request {
|
||||
t.Helper()
|
||||
|
||||
cwd, _ := os.Getwd()
|
||||
|
||||
return frankenphp.NewRequestWithContext(r, cwd+"/testdata/")
|
||||
}
|
||||
|
||||
func TestHelloWorld(t *testing.T) {
|
||||
defer frankenphp.Shutdown()
|
||||
func createTestHandler(t *testing.T, workerScript string) (shutdown func(), handler func(http.ResponseWriter, *http.Request), iterations int) {
|
||||
assert.Nil(t, frankenphp.Startup())
|
||||
|
||||
handler := func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Nil(t, frankenphp.ExecuteScript(w, setRequestContext(t, r)))
|
||||
if workerScript == "" {
|
||||
iterations = 1
|
||||
} else {
|
||||
iterations = 2
|
||||
cwd, _ := os.Getwd()
|
||||
|
||||
frankenphp.StartWorkers(cwd+"/testdata/"+workerScript, 1)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("GET", "http://example.com/index.php", nil)
|
||||
w := httptest.NewRecorder()
|
||||
handler(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
|
||||
assert.Equal(t, "I am by birth a Genevese", string(body))
|
||||
}
|
||||
|
||||
func TestServerVariable(t *testing.T) {
|
||||
defer frankenphp.Shutdown()
|
||||
|
||||
handler := func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Nil(t, frankenphp.ExecuteScript(w, setRequestContext(t, r)))
|
||||
shutdown = func() {
|
||||
if workerScript != "" {
|
||||
frankenphp.StopWorkers()
|
||||
}
|
||||
frankenphp.Shutdown()
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("GET", "http://example.com/server-variable.php?foo=a&bar=b#hash", nil)
|
||||
req.SetBasicAuth("kevin", "password")
|
||||
w := httptest.NewRecorder()
|
||||
handler(w, req)
|
||||
handler = func(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
|
||||
resp := w.Result()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
req := setRequestContext(t, r)
|
||||
if workerScript == "" {
|
||||
err = frankenphp.ExecuteScript(w, req)
|
||||
} else {
|
||||
err = frankenphp.WorkerHandleRequest(w, req)
|
||||
}
|
||||
|
||||
strBody := string(body)
|
||||
|
||||
assert.Contains(t, strBody, "[REMOTE_HOST]")
|
||||
assert.Contains(t, strBody, "[REMOTE_USER] => kevin")
|
||||
assert.Contains(t, strBody, "[PHP_AUTH_USER] => kevin")
|
||||
assert.Contains(t, strBody, "[PHP_AUTH_PW] => password")
|
||||
assert.Contains(t, strBody, "[HTTP_AUTHORIZATION] => Basic a2V2aW46cGFzc3dvcmQ=")
|
||||
assert.Contains(t, strBody, "[DOCUMENT_ROOT]")
|
||||
assert.Contains(t, strBody, "[CONTENT_TYPE]")
|
||||
assert.Contains(t, strBody, "[QUERY_STRING] => foo=a&bar=b#hash")
|
||||
assert.Contains(t, strBody, "[REQUEST_URI] => /server-variable.php?foo=a&bar=b#hash")
|
||||
assert.Contains(t, strBody, "[SCRIPT_NAME]")
|
||||
assert.Contains(t, strBody, "[CONTENT_LENGTH]")
|
||||
assert.Contains(t, strBody, "[REMOTE_ADDR]")
|
||||
assert.Contains(t, strBody, "[REMOTE_PORT]")
|
||||
assert.Contains(t, strBody, "[REQUEST_SCHEME] => http")
|
||||
assert.Contains(t, strBody, "[DOCUMENT_URI]")
|
||||
assert.Contains(t, strBody, "[AUTH_TYPE]")
|
||||
assert.Contains(t, strBody, "[REMOTE_IDENT]")
|
||||
assert.Contains(t, strBody, "[REQUEST_METHOD] => GET")
|
||||
assert.Contains(t, strBody, "[SERVER_NAME] => example.com")
|
||||
assert.Contains(t, strBody, "[SERVER_PROTOCOL] => HTTP/1.1")
|
||||
assert.Contains(t, strBody, "[SCRIPT_FILENAME]")
|
||||
assert.Contains(t, strBody, "[SERVER_SOFTWARE] => FrankenPHP")
|
||||
assert.Contains(t, strBody, "[REQUEST_TIME_FLOAT]")
|
||||
assert.Contains(t, strBody, "[REQUEST_TIME]")
|
||||
assert.Contains(t, strBody, "[REQUEST_TIME]")
|
||||
}
|
||||
|
||||
func TestPathInfo(t *testing.T) {
|
||||
defer frankenphp.Shutdown()
|
||||
|
||||
handler := func(w http.ResponseWriter, r *http.Request) {
|
||||
rewriteRequest := setRequestContext(t, r.Clone(context.TODO()))
|
||||
rewriteRequest.URL.Path = "/server-variable.php/pathinfo"
|
||||
rewriteRequest.Context().Value(frankenphp.FrankenPHPContextKey).(*frankenphp.FrankenPHPContext).Env["REQUEST_URI"] = r.URL.RequestURI()
|
||||
|
||||
assert.Nil(t, frankenphp.ExecuteScript(w, rewriteRequest))
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("GET", "http://example.com/pathinfo", nil)
|
||||
w := httptest.NewRecorder()
|
||||
handler(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
|
||||
strBody := string(body)
|
||||
|
||||
assert.Contains(t, strBody, "[PATH_INFO] => /pathinfo")
|
||||
assert.Contains(t, strBody, "[REQUEST_URI] => /pathinfo")
|
||||
assert.Contains(t, strBody, "[PATH_TRANSLATED] =>")
|
||||
assert.Contains(t, strBody, "[SCRIPT_NAME] => /server-variable.php")
|
||||
return
|
||||
}
|
||||
|
||||
func TestHeaders(t *testing.T) {
|
||||
func TestStartup(t *testing.T) {
|
||||
defer frankenphp.Shutdown()
|
||||
assert.Nil(t, frankenphp.Startup())
|
||||
frankenphp.Shutdown()
|
||||
|
||||
handler := func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Nil(t, frankenphp.ExecuteScript(w, setRequestContext(t, r)))
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("GET", "http://example.com/headers.php", nil)
|
||||
w := httptest.NewRecorder()
|
||||
handler(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
|
||||
assert.Equal(t, "Hello", string(body))
|
||||
assert.Equal(t, 201, resp.StatusCode)
|
||||
assert.Equal(t, "bar", resp.Header.Get("Foo"))
|
||||
assert.Equal(t, "bar2", resp.Header.Get("Foo2"))
|
||||
assert.Nil(t, frankenphp.Startup())
|
||||
}
|
||||
|
||||
func TestInput(t *testing.T) {
|
||||
defer frankenphp.Shutdown()
|
||||
func TestHelloWorld_module(t *testing.T) { testHelloWorld(t, "") }
|
||||
func TestHelloWorld_worker(t *testing.T) { testHelloWorld(t, "index.php") }
|
||||
func testHelloWorld(t *testing.T, scriptName string) {
|
||||
shutdown, handler, iterations := createTestHandler(t, scriptName)
|
||||
defer shutdown()
|
||||
|
||||
handler := func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Nil(t, frankenphp.ExecuteScript(w, setRequestContext(t, r)))
|
||||
for i := 0; i < iterations; i++ {
|
||||
req := httptest.NewRequest("GET", "http://example.com/index.php", nil)
|
||||
w := httptest.NewRecorder()
|
||||
handler(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
|
||||
assert.Equal(t, "I am by birth a Genevese", string(body))
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("POST", "http://example.com/input.php", strings.NewReader("post data"))
|
||||
w := httptest.NewRecorder()
|
||||
handler(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
|
||||
assert.Equal(t, "post data", string(body))
|
||||
assert.Equal(t, "bar", resp.Header.Get("Foo"))
|
||||
}
|
||||
|
||||
func TestPostSuperGlobals(t *testing.T) {
|
||||
defer frankenphp.Shutdown()
|
||||
func TestServerVariable_module(t *testing.T) { testServerVariable(t, "") }
|
||||
func TestServerVariable_worker(t *testing.T) {
|
||||
testServerVariable(t, "server-variable.php")
|
||||
}
|
||||
func testServerVariable(t *testing.T, scriptName string) {
|
||||
shutdown, handler, iterations := createTestHandler(t, scriptName)
|
||||
defer shutdown()
|
||||
|
||||
handler := func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Nil(t, frankenphp.ExecuteScript(w, setRequestContext(t, r)))
|
||||
for i := 0; i < iterations; i++ {
|
||||
req := httptest.NewRequest("GET", "http://example.com/server-variable.php?foo=a&bar=b#hash", nil)
|
||||
req.SetBasicAuth("kevin", "password")
|
||||
w := httptest.NewRecorder()
|
||||
handler(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
|
||||
strBody := string(body)
|
||||
|
||||
assert.Contains(t, strBody, "[REMOTE_HOST]")
|
||||
assert.Contains(t, strBody, "[REMOTE_USER] => kevin")
|
||||
assert.Contains(t, strBody, "[PHP_AUTH_USER] => kevin")
|
||||
assert.Contains(t, strBody, "[PHP_AUTH_PW] => password")
|
||||
assert.Contains(t, strBody, "[HTTP_AUTHORIZATION] => Basic a2V2aW46cGFzc3dvcmQ=")
|
||||
assert.Contains(t, strBody, "[DOCUMENT_ROOT]")
|
||||
assert.Contains(t, strBody, "[CONTENT_TYPE]")
|
||||
assert.Contains(t, strBody, "[QUERY_STRING] => foo=a&bar=b#hash")
|
||||
assert.Contains(t, strBody, "[REQUEST_URI] => /server-variable.php?foo=a&bar=b#hash")
|
||||
assert.Contains(t, strBody, "[SCRIPT_NAME]")
|
||||
assert.Contains(t, strBody, "[CONTENT_LENGTH]")
|
||||
assert.Contains(t, strBody, "[REMOTE_ADDR]")
|
||||
assert.Contains(t, strBody, "[REMOTE_PORT]")
|
||||
assert.Contains(t, strBody, "[REQUEST_SCHEME] => http")
|
||||
assert.Contains(t, strBody, "[DOCUMENT_URI]")
|
||||
assert.Contains(t, strBody, "[AUTH_TYPE]")
|
||||
assert.Contains(t, strBody, "[REMOTE_IDENT]")
|
||||
assert.Contains(t, strBody, "[REQUEST_METHOD] => GET")
|
||||
assert.Contains(t, strBody, "[SERVER_NAME] => example.com")
|
||||
assert.Contains(t, strBody, "[SERVER_PROTOCOL] => HTTP/1.1")
|
||||
assert.Contains(t, strBody, "[SCRIPT_FILENAME]")
|
||||
assert.Contains(t, strBody, "[SERVER_SOFTWARE] => FrankenPHP")
|
||||
assert.Contains(t, strBody, "[REQUEST_TIME_FLOAT]")
|
||||
assert.Contains(t, strBody, "[REQUEST_TIME]")
|
||||
assert.Contains(t, strBody, "[REQUEST_TIME]")
|
||||
}
|
||||
|
||||
formData := url.Values{"baz": {"bat"}}
|
||||
req := httptest.NewRequest("POST", "http://example.com/super-globals.php?foo=bar", strings.NewReader(formData.Encode()))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
w := httptest.NewRecorder()
|
||||
handler(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
|
||||
assert.Contains(t, string(body), "'foo' => 'bar'")
|
||||
assert.Contains(t, string(body), "'baz' => 'bat'")
|
||||
}
|
||||
|
||||
func TestCookies(t *testing.T) {
|
||||
defer frankenphp.Shutdown()
|
||||
|
||||
handler := func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Nil(t, frankenphp.ExecuteScript(w, setRequestContext(t, r)))
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("GET", "http://example.com/cookies.php", nil)
|
||||
req.AddCookie(&http.Cookie{Name: "foo", Value: "bar"})
|
||||
w := httptest.NewRecorder()
|
||||
handler(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
|
||||
assert.Contains(t, string(body), "'foo' => 'bar'")
|
||||
func TestPathInfo_module(t *testing.T) { testPathInfo(t, "") }
|
||||
func TestPathInfo_worker(t *testing.T) {
|
||||
testPathInfo(t, "server-variable.php")
|
||||
}
|
||||
func testPathInfo(t *testing.T, scriptName string) {
|
||||
shutdown, _, iterations := createTestHandler(t, scriptName)
|
||||
defer shutdown()
|
||||
|
||||
for i := 0; i < iterations; i++ {
|
||||
handler := func(w http.ResponseWriter, r *http.Request) {
|
||||
rewriteRequest := setRequestContext(t, r.Clone(context.TODO()))
|
||||
rewriteRequest.URL.Path = "/server-variable.php/pathinfo"
|
||||
fc, _ := frankenphp.FromContext(rewriteRequest.Context())
|
||||
fc.Env["REQUEST_URI"] = r.URL.RequestURI()
|
||||
|
||||
if scriptName == "" {
|
||||
assert.Nil(t, frankenphp.ExecuteScript(w, rewriteRequest))
|
||||
} else {
|
||||
assert.Nil(t, frankenphp.WorkerHandleRequest(w, rewriteRequest))
|
||||
}
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("GET", "http://example.com/pathinfo", nil)
|
||||
w := httptest.NewRecorder()
|
||||
handler(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
|
||||
strBody := string(body)
|
||||
|
||||
assert.Contains(t, strBody, "[PATH_INFO] => /pathinfo")
|
||||
assert.Contains(t, strBody, "[REQUEST_URI] => /pathinfo")
|
||||
assert.Contains(t, strBody, "[PATH_TRANSLATED] =>")
|
||||
assert.Contains(t, strBody, "[SCRIPT_NAME] => /server-variable.php")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeaders_module(t *testing.T) { testHeaders(t, "") }
|
||||
func TestHeaders_worker(t *testing.T) { testHeaders(t, "headers.php") }
|
||||
func testHeaders(t *testing.T, scriptName string) {
|
||||
shutdown, handler, iterations := createTestHandler(t, scriptName)
|
||||
defer shutdown()
|
||||
|
||||
for i := 0; i < iterations; i++ {
|
||||
req := httptest.NewRequest("GET", "http://example.com/headers.php", nil)
|
||||
w := httptest.NewRecorder()
|
||||
handler(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
|
||||
assert.Equal(t, "Hello", string(body))
|
||||
assert.Equal(t, 201, resp.StatusCode)
|
||||
assert.Equal(t, "bar", resp.Header.Get("Foo"))
|
||||
assert.Equal(t, "bar2", resp.Header.Get("Foo2"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestInput_module(t *testing.T) { testInput(t, "") }
|
||||
func TestInput_worker(t *testing.T) { testInput(t, "input.php") }
|
||||
func testInput(t *testing.T, scriptName string) {
|
||||
shutdown, handler, iterations := createTestHandler(t, scriptName)
|
||||
defer shutdown()
|
||||
|
||||
for i := 0; i < iterations; i++ {
|
||||
req := httptest.NewRequest("POST", "http://example.com/input.php", strings.NewReader("post data"))
|
||||
w := httptest.NewRecorder()
|
||||
handler(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
|
||||
assert.Equal(t, "post data", string(body))
|
||||
assert.Equal(t, "bar", resp.Header.Get("Foo"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestPostSuperGlobals_module(t *testing.T) { testPostSuperGlobals(t, "") }
|
||||
func TestPostSuperGlobals_worker(t *testing.T) { testPostSuperGlobals(t, "super-globals.php") }
|
||||
func testPostSuperGlobals(t *testing.T, scriptName string) {
|
||||
shutdown, handler, iterations := createTestHandler(t, scriptName)
|
||||
defer shutdown()
|
||||
|
||||
for i := 0; i < iterations; i++ {
|
||||
formData := url.Values{"baz": {"bat"}}
|
||||
req := httptest.NewRequest("POST", "http://example.com/super-globals.php?foo=bar", strings.NewReader(formData.Encode()))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
w := httptest.NewRecorder()
|
||||
handler(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
|
||||
assert.Contains(t, string(body), "'foo' => 'bar'")
|
||||
assert.Contains(t, string(body), "'baz' => 'bat'")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCookies_module(t *testing.T) { testCookies(t, "") }
|
||||
func TestCookies_worker(t *testing.T) { testCookies(t, "cookies.php") }
|
||||
func testCookies(t *testing.T, scriptName string) {
|
||||
shutdown, handler, iterations := createTestHandler(t, scriptName)
|
||||
defer shutdown()
|
||||
|
||||
for i := 0; i < iterations; i++ {
|
||||
req := httptest.NewRequest("GET", "http://example.com/cookies.php", nil)
|
||||
req.AddCookie(&http.Cookie{Name: "foo", Value: "bar"})
|
||||
w := httptest.NewRecorder()
|
||||
handler(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
|
||||
assert.Contains(t, string(body), "'foo' => 'bar'")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSession_module(t *testing.T) { testSession(t, "") }
|
||||
func TestSession_worker(t *testing.T) {
|
||||
testSession(t, "session.php")
|
||||
}
|
||||
func testSession(t *testing.T, scriptName string) {
|
||||
shutdown, handler, iterations := createTestHandler(t, scriptName)
|
||||
defer shutdown()
|
||||
|
||||
ts := httptest.NewServer(http.HandlerFunc(handler))
|
||||
defer ts.Close()
|
||||
|
||||
for i := 0; i < iterations; i++ {
|
||||
jar, err := cookiejar.New(&cookiejar.Options{})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
client := &http.Client{Jar: jar}
|
||||
|
||||
resp1, err := client.Get(ts.URL + "/session.php")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
body1, _ := io.ReadAll(resp1.Body)
|
||||
t.Log(string(body1))
|
||||
assert.Equal(t, "Count: 0\n", string(body1))
|
||||
|
||||
resp2, err := client.Get(ts.URL + "/session.php")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
body2, _ := io.ReadAll(resp2.Body)
|
||||
assert.Equal(t, "Count: 1\n", string(body2))
|
||||
}
|
||||
}
|
||||
|
||||
func TestPhpInfo_module(t *testing.T) { testPhpInfo(t, "") }
|
||||
func TestPhpInfo_worker(t *testing.T) { testPhpInfo(t, "phpinfo.php") }
|
||||
func testPhpInfo(t *testing.T, scriptName string) {
|
||||
shutdown, handler, iterations := createTestHandler(t, scriptName)
|
||||
defer shutdown()
|
||||
|
||||
for i := 0; i < iterations; i++ {
|
||||
req := httptest.NewRequest("GET", "http://example.com/phpinfo.php", nil)
|
||||
w := httptest.NewRecorder()
|
||||
handler(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
|
||||
assert.Contains(t, string(body), "frankenphp")
|
||||
}
|
||||
}
|
||||
|
||||
6
go.mod
6
go.mod
@@ -1,8 +1,8 @@
|
||||
module github.com/dunglas/frankenphp
|
||||
|
||||
go 1.17
|
||||
go 1.18
|
||||
|
||||
require github.com/stretchr/testify v1.7.0
|
||||
require github.com/stretchr/testify v1.7.1
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
@@ -10,5 +10,5 @@ require (
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20220512140231-539c8e751b99 // indirect
|
||||
)
|
||||
|
||||
8
go.sum
8
go.sum
@@ -11,11 +11,11 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20220512140231-539c8e751b99 h1:dbuHpmKjkDzSOMKAWl10QNlgaZUd3V1q99xc81tt2Kc=
|
||||
gopkg.in/yaml.v3 v3.0.0-20220512140231-539c8e751b99/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
9
testdata/_executor.php
vendored
Normal file
9
testdata/_executor.php
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
$fn = require $_SERVER['SCRIPT_FILENAME'];
|
||||
if (!isset($_SERVER['FRANKENPHP_WORKER'])) {
|
||||
$fn();
|
||||
return;
|
||||
}
|
||||
|
||||
while (frankenphp_handle_request($fn)) {}
|
||||
6
testdata/cookies.php
vendored
6
testdata/cookies.php
vendored
@@ -1,3 +1,7 @@
|
||||
<?php
|
||||
|
||||
echo var_export($_COOKIE);
|
||||
require_once __DIR__.'/_executor.php';
|
||||
|
||||
return function () {
|
||||
echo var_export($_COOKIE);
|
||||
};
|
||||
|
||||
12
testdata/headers.php
vendored
12
testdata/headers.php
vendored
@@ -1,7 +1,11 @@
|
||||
<?php
|
||||
|
||||
header('Foo: bar');
|
||||
header('Foo2: bar2');
|
||||
http_response_code(201);
|
||||
require_once __DIR__.'/_executor.php';
|
||||
|
||||
echo 'Hello';
|
||||
return function () {
|
||||
header('Foo: bar');
|
||||
header('Foo2: bar2');
|
||||
http_response_code(201);
|
||||
|
||||
echo 'Hello';
|
||||
};
|
||||
|
||||
5
testdata/index.php
vendored
5
testdata/index.php
vendored
@@ -1,4 +1,7 @@
|
||||
<?php
|
||||
|
||||
echo "I am by birth a Genevese";
|
||||
require_once __DIR__.'/_executor.php';
|
||||
|
||||
return function () {
|
||||
echo "I am by birth a Genevese";
|
||||
};
|
||||
|
||||
8
testdata/input.php
vendored
8
testdata/input.php
vendored
@@ -1,5 +1,9 @@
|
||||
<?php
|
||||
|
||||
header('Foo: bar');
|
||||
require_once __DIR__.'/_executor.php';
|
||||
|
||||
echo file_get_contents('php://input');
|
||||
return function () {
|
||||
header('Foo: bar');
|
||||
|
||||
echo file_get_contents('php://input');
|
||||
};
|
||||
|
||||
7
testdata/phpinfo.php
vendored
Normal file
7
testdata/phpinfo.php
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__.'/_executor.php';
|
||||
|
||||
return function () {
|
||||
phpinfo();
|
||||
};
|
||||
6
testdata/server-variable.php
vendored
6
testdata/server-variable.php
vendored
@@ -1,3 +1,7 @@
|
||||
<?php
|
||||
|
||||
echo print_r($_SERVER);
|
||||
require_once __DIR__.'/_executor.php';
|
||||
|
||||
return function () {
|
||||
echo print_r($_SERVER);
|
||||
};
|
||||
|
||||
15
testdata/session.php
vendored
Normal file
15
testdata/session.php
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__.'/_executor.php';
|
||||
|
||||
return function () {
|
||||
session_start();
|
||||
|
||||
if (isset($_SESSION['count'])) {
|
||||
$_SESSION['count']++;
|
||||
} else {
|
||||
$_SESSION['count'] = 0;
|
||||
}
|
||||
|
||||
echo 'Count: '.$_SESSION['count'].PHP_EOL;
|
||||
};
|
||||
10
testdata/super-globals.php
vendored
10
testdata/super-globals.php
vendored
@@ -1,5 +1,9 @@
|
||||
<?php
|
||||
|
||||
var_export($_GET);
|
||||
var_export($_POST);
|
||||
var_export($_SERVER['CONTENT_TYPE']);
|
||||
require_once __DIR__.'/_executor.php';
|
||||
|
||||
return function () {
|
||||
var_export($_GET);
|
||||
var_export($_POST);
|
||||
var_export($_SERVER);
|
||||
};
|
||||
|
||||
14
testdata/worker.php
vendored
Normal file
14
testdata/worker.php
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
$i = 0;
|
||||
do {
|
||||
$ok = frankenphp_handle_request(function () use ($i): void {
|
||||
echo sprintf("Requests handled: %d (request time: %s)\n", $i, $_SERVER['REQUEST_TIME_FLOAT']);
|
||||
|
||||
var_export($_GET);
|
||||
var_export($_POST);
|
||||
var_export($_SERVER);
|
||||
});
|
||||
|
||||
$i++;
|
||||
} while ($ok);
|
||||
142
worker.go
Normal file
142
worker.go
Normal file
@@ -0,0 +1,142 @@
|
||||
package frankenphp
|
||||
|
||||
// #include <stdlib.h>
|
||||
// #include "frankenphp.h"
|
||||
import "C"
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"runtime/cgo"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
var requestsChans sync.Map // map[fileName]cgo.NewHandle(chan *http.Request)
|
||||
var workersWaitGroup sync.WaitGroup
|
||||
|
||||
func WorkerHandleRequest(responseWriter http.ResponseWriter, request *http.Request) error {
|
||||
if atomic.LoadInt32(&started) < 1 {
|
||||
panic("FrankenPHP isn't started, call frankenphp.Startup()")
|
||||
}
|
||||
|
||||
if err := populateEnv(request); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fc, _ := FromContext(request.Context())
|
||||
v, ok := requestsChans.Load(fc.Env["SCRIPT_FILENAME"])
|
||||
if !ok {
|
||||
panic(fmt.Errorf("No worker started for script %s", fc.Env["SCRIPT_FILENAME"]))
|
||||
}
|
||||
rch := v.(cgo.Handle)
|
||||
rc := rch.Value().(chan *http.Request)
|
||||
|
||||
fc.responseWriter = responseWriter
|
||||
fc.done = make(chan interface{})
|
||||
fc.Env["FRANKENPHP_WORKER"] = "1"
|
||||
|
||||
rc <- request
|
||||
<-fc.done
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func StartWorkers(fileName string, nbWorkers int) {
|
||||
if _, ok := requestsChans.Load(fileName); ok {
|
||||
panic(fmt.Errorf("workers %q: already started", fileName))
|
||||
}
|
||||
|
||||
rc := make(chan *http.Request)
|
||||
rch := cgo.NewHandle(rc)
|
||||
|
||||
requestsChans.Store(fileName, rch)
|
||||
|
||||
for i := 0; i < nbWorkers; i++ {
|
||||
newWorker(fileName, rch)
|
||||
}
|
||||
}
|
||||
|
||||
func StopWorkers() {
|
||||
requestsChans.Range(func(k, v any) bool {
|
||||
close(v.(cgo.Handle).Value().(chan *http.Request))
|
||||
requestsChans.Delete(k)
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
workersWaitGroup.Wait()
|
||||
}
|
||||
|
||||
func newWorker(fileName string, requestsChanHandle cgo.Handle) {
|
||||
go func() {
|
||||
workersWaitGroup.Add(1)
|
||||
runtime.LockOSThread()
|
||||
|
||||
cFileName := C.CString(fileName)
|
||||
defer C.free(unsafe.Pointer(cFileName))
|
||||
|
||||
if C.frankenphp_create_server_context(C.uintptr_t(requestsChanHandle), cFileName) < 0 {
|
||||
panic(fmt.Errorf("error during request context creation"))
|
||||
}
|
||||
|
||||
if C.frankenphp_request_startup() < 0 {
|
||||
panic("error during PHP request startup")
|
||||
}
|
||||
|
||||
log.Printf("new worker started: %q", fileName)
|
||||
|
||||
if C.frankenphp_execute_script(cFileName) < 0 {
|
||||
panic("error during PHP script execution")
|
||||
}
|
||||
|
||||
C.frankenphp_request_shutdown()
|
||||
workersWaitGroup.Done()
|
||||
}()
|
||||
}
|
||||
|
||||
//export go_frankenphp_worker_handle_request_start
|
||||
func go_frankenphp_worker_handle_request_start(rch C.uintptr_t) C.uintptr_t {
|
||||
rc := cgo.Handle(rch).Value().(chan *http.Request)
|
||||
|
||||
log.Print("worker: waiting for request")
|
||||
r, ok := <-rc
|
||||
if !ok {
|
||||
// channel closed, server is shutting down
|
||||
log.Print("worker: shutting down")
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
log.Printf("worker: handling request %#v", r)
|
||||
|
||||
fc := r.Context().Value(contextKey).(*FrankenPHPContext)
|
||||
if fc == nil || fc.responseWriter == nil {
|
||||
panic("worker: not a valid worker request")
|
||||
}
|
||||
|
||||
if err := updateServerContext(r); err != nil {
|
||||
// Unexpected error
|
||||
log.Print(err)
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
C.frankenphp_worker_reset_server_context()
|
||||
|
||||
return C.uintptr_t(cgo.NewHandle(r))
|
||||
}
|
||||
|
||||
//export go_frankenphp_worker_handle_request_end
|
||||
func go_frankenphp_worker_handle_request_end(rh C.uintptr_t) {
|
||||
rHandle := cgo.Handle(rh)
|
||||
r := rHandle.Value().(*http.Request)
|
||||
fc := r.Context().Value(contextKey).(*FrankenPHPContext)
|
||||
|
||||
C.frankenphp_clean_server_context()
|
||||
cgo.Handle(rh).Delete()
|
||||
|
||||
close(fc.done)
|
||||
}
|
||||
42
worker_test.go
Normal file
42
worker_test.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package frankenphp_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestWorker(t *testing.T) {
|
||||
shutdown, handler, iterations := createTestHandler(t, "worker.php")
|
||||
defer shutdown()
|
||||
|
||||
for i := 0; i < iterations; i++ {
|
||||
formData := url.Values{"baz": {"bat"}}
|
||||
req := httptest.NewRequest("POST", "http://example.com/worker.php?foo=bar", strings.NewReader(formData.Encode()))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
w := httptest.NewRecorder()
|
||||
handler(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
|
||||
assert.Contains(t, string(body), fmt.Sprintf("Requests handled: %d", i*2))
|
||||
|
||||
formData2 := url.Values{"baz2": {"bat2"}}
|
||||
req2 := httptest.NewRequest("POST", "http://example.com/worker.php?foo2=bar2", strings.NewReader(formData2.Encode()))
|
||||
req2.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
w2 := httptest.NewRecorder()
|
||||
handler(w2, req2)
|
||||
|
||||
resp2 := w2.Result()
|
||||
body2, _ := io.ReadAll(resp2.Body)
|
||||
|
||||
assert.Contains(t, string(body2), fmt.Sprintf("Requests handled: %d", i*2+1))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user