Compare commits

...

20 Commits

Author SHA1 Message Date
Kévin Dunglas
b320dcd38c chore: reset stats in benchmarks 2024-02-12 11:42:14 +01:00
Kévin Dunglas
36b752d0a6 feat: compress Linux binaries with UPX (#572) 2024-02-12 10:01:27 +01:00
Kévin Dunglas
feaa950d89 feat: compile with Go 1.22 (#568) 2024-02-12 10:00:46 +01:00
Kévin Dunglas
f90e4614b6 ci: fix the Mac build (#569) 2024-02-11 22:53:21 +01:00
Hemanth Bollamreddi
f152a5fdaf docs: fix rootless Docker example (#565)
* Fix docs for running docker in non root mode

* Fix comments

* minor fixes

---------

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2024-02-11 11:33:08 +01:00
Holger Dörner
b60fc5c374 Make md5 binary OS dependent 2024-02-09 23:48:43 +01:00
Kévin Dunglas
5da805d9ee ci: use Apple Silicon machines when useful (#550)
* ci: add Apple Silicon build

* ci: let the CI build Apple Silicon binaries
2024-02-04 18:25:04 +01:00
Kévin Dunglas
a996c2ee28 chore: prepare release 1.1.0 2024-02-03 12:32:48 +01:00
Kévin Dunglas
4cb77b552d ci: add more headers to the k6 load test (#544) 2024-02-03 12:25:35 +01:00
Kévin Dunglas
241ca55d7a feat: automatically import environment variables in $_SERVER 2024-02-03 12:25:18 +01:00
Kévin Dunglas
ae958516ea feat: enable resolve_root_symlink by default (#546)
* feat: enable resolve_root_symlink by default

* Update docs/config.md

Co-authored-by: Francis Lavoie <lavofr@gmail.com>

* fix init

---------

Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2024-02-02 15:58:33 +01:00
Kévin Dunglas
b61900eae1 ci: fix push or Docker dev images 2024-02-02 12:21:59 +01:00
Kévin Dunglas
e53b1ce891 ci: use master Buildx version 2024-02-01 23:51:08 +01:00
Kévin Dunglas
6dee113a01 perf: add $_SERVER creation benchmark 2024-02-01 11:40:36 +01:00
Kévin Dunglas
fe7e9e7c79 fix: crash when using apache_request_headers() (#536)
* fix: potential crash when using apache_request_headers()

* refactor: better apache_request_headers
2024-02-01 10:00:11 +01:00
Kévin Dunglas
ab7ce9cb18 ci: temporary fix for Buildx crash 2024-01-31 16:29:45 +01:00
Kévin Dunglas
5afde55ebf ci: debug Docker buildx crash 2024-01-31 15:59:54 +01:00
Kévin Dunglas
da63e700b0 ci: fix scheduled Docker images build (ctd) 2024-01-31 15:21:03 +01:00
Kévin Dunglas
5a8e5f9518 feat: add apache_response_headers() function (#530) 2024-01-31 12:34:30 +01:00
Kévin Dunglas
3d9f344a50 docs: Docker image updates and tags 2024-01-30 22:07:33 +01:00
25 changed files with 380 additions and 99 deletions

View File

@@ -19,6 +19,7 @@ on:
- cron: '0 4 * * *'
env:
IMAGE_NAME: ${{ (github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && inputs.version) || startsWith(github.ref, 'refs/tags/')) && 'dunglas/frankenphp' || 'dunglas/frankenphp-dev' }}
LATEST: ${{ (github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && inputs.version) || startsWith(github.ref, 'refs/tags/')) && '0' || '1' }}
jobs:
prepare:
runs-on: ubuntu-latest
@@ -37,6 +38,8 @@ jobs:
-
name: Check PHP versions
id: check
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
PHP_82_LATEST=$(skopeo inspect docker://docker.io/library/php:8.2 --override-os linux --override-arch amd64 | jq -r '.Env[] | select(test("^PHP_VERSION=")) | sub("^PHP_VERSION="; "")')
PHP_83_LATEST=$(skopeo inspect docker://docker.io/library/php:8.3 --override-os linux --override-arch amd64 | jq -r '.Env[] | select(test("^PHP_VERSION=")) | sub("^PHP_VERSION="; "")')
@@ -45,13 +48,13 @@ jobs:
echo php82_version="${PHP_82_LATEST//./-}"
echo php83_version="${PHP_83_LATEST//./-}"
} >> "${GITHUB_OUTPUT}"
# Check if the Docker images must be rebuilt
if [[ "${GITHUB_EVENT_NAME}" != "schedule" ]]; then
echo skip=false >> "${GITHUB_OUTPUT}"
exit 0
fi
FRANKENPHP_82_LATEST=$(skopeo inspect docker://docker.io/dunglas/frankenphp:latest-php8.2 --override-os linux --override-arch amd64 | jq -r '.Env[] | select(test("^PHP_VERSION=")) | sub("^PHP_VERSION="; "")')
FRANKENPHP_83_LATEST=$(skopeo inspect docker://docker.io/dunglas/frankenphp:latest-php8.3 --override-os linux --override-arch amd64 | jq -r '.Env[] | select(test("^PHP_VERSION=")) | sub("^PHP_VERSION="; "")')
@@ -59,7 +62,7 @@ jobs:
echo skip=true >> "${GITHUB_OUTPUT}"
exit 0
fi
{
echo ref="$(gh release view --repo dunglas/frankenphp --json tagName --jq '.tagName')"
echo skip=false
@@ -89,7 +92,6 @@ jobs:
SHA: ${{ github.sha }}
VERSION: ${{ (github.ref_type == 'tag' && github.ref_name) || steps.check.outputs.ref || github.sha }}
PHP_VERSION: ${{ steps.check.outputs.php_version }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
build:
runs-on: ubuntu-latest
needs:
@@ -160,7 +162,7 @@ jobs:
*.cache-from=type=gha,scope=${{ needs.prepare.outputs.ref || github.ref }}-${{ matrix.platform }}
*.cache-from=type=gha,scope=refs/heads/main-${{ matrix.platform }}
*.cache-to=type=gha,scope=${{ needs.prepare.outputs.ref || github.ref }}-${{ matrix.platform }},ignore-error=true
${{ fromJson(needs.prepare.outputs.push) && '*.output=type=image,name=dunglas/frankenphp,push-by-digest=true,name-canonical=true,push=true' || '' }}
${{ fromJson(needs.prepare.outputs.push) && format('*.output=type=image,name={0},push-by-digest=true,name-canonical=true,push=true', env.IMAGE_NAME) || '' }}
env:
SHA: ${{ github.sha }}
VERSION: ${{ github.ref_type == 'tag' && github.ref_name || needs.prepare.outputs.ref || github.sha }}
@@ -229,7 +231,8 @@ jobs:
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
version: latest
# Temporary fix for https://github.com/docker/buildx/issues/2229
version: "https://github.com/docker/buildx.git#master"
-
name: Login to DockerHub
uses: docker/login-action@v3
@@ -240,9 +243,10 @@ jobs:
name: Create manifest list and push
working-directory: /tmp/metadata
run: |
set -x
# shellcheck disable=SC2046,SC2086
docker buildx imagetools create $(jq -cr '.target."${{ matrix.target }}-${{ matrix.variant }}".tags | map("-t " + .) | join(" ")' <<< ${METADATA}) \
$(printf 'dunglas/frankenphp@sha256:%s ' *)
$(printf "${IMAGE_NAME}@sha256:%s " *)
env:
METADATA: ${{ needs.prepare.outputs.metadata }}
-

View File

@@ -19,6 +19,7 @@ on:
- cron: '0 0 * * *'
env:
IMAGE_NAME: ${{ (github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && inputs.version) || startsWith(github.ref, 'refs/tags/')) && 'dunglas/frankenphp' || 'dunglas/frankenphp-dev' }}
LATEST: ${{ (github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && inputs.version) || startsWith(github.ref, 'refs/tags/')) && '0' || '1' }}
jobs:
prepare:
runs-on: ubuntu-latest
@@ -113,7 +114,7 @@ jobs:
*.cache-from=type=gha,scope=${{ needs.prepare.outputs.ref || github.ref }}-static-builder
*.cache-from=type=gha,scope=refs/heads/main-static-builder
*.cache-to=type=gha,scope=${{ needs.prepare.outputs.ref || github.ref }}-static-builder,ignore-error=true
${{ fromJson(needs.prepare.outputs.push) && '*.output=type=image,name=dunglas/frankenphp,push-by-digest=true,name-canonical=true,push=true' || '' }}
${{ fromJson(needs.prepare.outputs.push) && format('*.output=type=image,name={0},push-by-digest=true,name-canonical=true,push=true', env.IMAGE_NAME) || '' }}
env:
SHA: ${{ github.sha }}
VERSION: ${{ (github.ref_type == 'tag' && github.ref_name) || needs.prepare.outputs.ref || github.sha}}
@@ -188,7 +189,7 @@ jobs:
run: |
# shellcheck disable=SC2046,SC2086
docker buildx imagetools create $(jq -cr '.target."static-builder".tags | map("-t " + .) | join(" ")' <<< "${METADATA}") \
$(printf 'dunglas/frankenphp@sha256:%s ' *)
$(printf "${IMAGE_NAME}@sha256:%s " *)
env:
METADATA: ${{ needs.prepare.outputs.metadata }}
-
@@ -213,8 +214,12 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
build-mac:
name: Build macOS x86_64 binaries
runs-on: macos-latest
strategy:
fail-fast: false
matrix:
platform: ['arm64', 'x86_64']
name: Build macOS ${{ matrix.platform }} binaries
runs-on: ${{ matrix.platform == 'arm64' && 'macos-14' || 'macos-13' }}
needs: [ prepare ]
env:
HOMEBREW_NO_AUTO_UPDATE: 1
@@ -226,7 +231,7 @@ jobs:
-
uses: actions/setup-go@v5
with:
go-version: '1.21'
go-version: '1.22'
cache-dependency-path: |
go.sum
caddy/go.sum
@@ -253,5 +258,5 @@ jobs:
if: github.ref_type == 'branch'
uses: actions/upload-artifact@v3
with:
name: frankenphp-mac-x86_64
path: dist/frankenphp-mac-x86_64
name: frankenphp-mac-${{ matrix.platform }}
path: dist/frankenphp-mac-${{ matrix.platform }}

View File

@@ -22,7 +22,7 @@ jobs:
-
uses: actions/setup-go@v5
with:
go-version: '1.21'
go-version: '1.22'
cache-dependency-path: |
go.sum
caddy/go.sum

View File

@@ -59,6 +59,7 @@ You can also run command-line scripts with:
* [Create **standalone**, self-executable PHP apps](https://frankenphp.dev/docs/embed/)
* [Create static binaries](https://frankenphp.dev/docs/static/)
* [Compile from sources](https://frankenphp.dev/docs/compile/)
* [Laravel integration](https://frankenphp.dev/docs/laravel/)
* [Known issues](https://frankenphp.dev/docs/known-issues/)
* [Demo app (Symfony) and benchmarks](https://github.com/dunglas/frankenphp-demo)
* [Go library documentation](https://pkg.go.dev/github.com/dunglas/frankenphp)

View File

@@ -9,17 +9,14 @@ fi
arch="$(uname -m)"
os="$(uname -s | tr '[:upper:]' '[:lower:]')"
md5binary="md5sum"
if [ "${os}" = "darwin" ]; then
os="mac"
md5binary="md5 -q"
fi
if [ -z "${PHP_EXTENSIONS}" ]; then
if [ "${os}" = "mac" ] && [ "${arch}" = "x86_64" ]; then
# Temporary fix for https://github.com/crazywhalecc/static-php-cli/issues/280 (remove ldap)
export PHP_EXTENSIONS="apcu,bcmath,bz2,calendar,ctype,curl,dba,dom,exif,fileinfo,filter,gd,iconv,igbinary,intl,mbregex,mbstring,mysqli,mysqlnd,opcache,openssl,pcntl,pdo,pdo_mysql,pdo_pgsql,pdo_sqlite,pgsql,phar,posix,readline,redis,session,simplexml,sockets,sodium,sqlite3,sysvsem,tokenizer,xml,xmlreader,xmlwriter,zip,zlib"
else
export PHP_EXTENSIONS="apcu,bcmath,bz2,calendar,ctype,curl,dba,dom,exif,fileinfo,filter,gd,iconv,igbinary,intl,ldap,mbregex,mbstring,mysqli,mysqlnd,opcache,openssl,pcntl,pdo,pdo_mysql,pdo_pgsql,pdo_sqlite,pgsql,phar,posix,readline,redis,session,simplexml,sockets,sodium,sqlite3,sysvsem,tokenizer,xml,xmlreader,xmlwriter,zip,zlib"
fi
export PHP_EXTENSIONS="apcu,bcmath,bz2,calendar,ctype,curl,dba,dom,exif,fileinfo,filter,gd,iconv,igbinary,intl,ldap,mbregex,mbstring,mysqli,mysqlnd,opcache,openssl,pcntl,pdo,pdo_mysql,pdo_pgsql,pdo_sqlite,pgsql,phar,posix,readline,redis,session,simplexml,sockets,sodium,sqlite3,sysvsem,tokenizer,xml,xmlreader,xmlwriter,zip,zlib"
fi
if [ -z "${PHP_EXTENSION_LIBS}" ]; then
@@ -57,7 +54,7 @@ if [ -n "${CLEAN}" ]; then
go clean -cache
fi
# Build libphp if ncessary
# Build libphp if necessary
if [ -f "dist/static-php-cli/buildroot/lib/libphp.a" ]; then
cd dist/static-php-cli
else
@@ -73,16 +70,20 @@ else
fi
if type "brew" > /dev/null; then
packages="composer"
if ! type "composer" > /dev/null; then
packages="composer"
fi
if ! type "go" > /dev/null; then
packages="${packages} go"
fi
if [ -n "${RELEASE}" ]; then
if [ -n "${RELEASE}" ] && ! type "gh" > /dev/null; then
packages="${packages} gh"
fi
# shellcheck disable=SC2086
brew install --formula --quiet ${packages}
if [ -n "${packages}" ]; then
# shellcheck disable=SC2086
brew install --formula --quiet ${packages}
fi
fi
composer install --no-dev -a
@@ -123,7 +124,7 @@ cd ../..
# Embed PHP app, if any
if [ -n "${EMBED}" ] && [ -d "${EMBED}" ]; then
tar -cf app.tar -C "${EMBED}" .
md5 -q app.tar > app_checksum.txt
${md5binary} app.tar > app_checksum.txt
fi
if [ "${os}" = "linux" ]; then
@@ -144,6 +145,10 @@ if [ -d "${EMBED}" ]; then
truncate -s 0 app_checksum.txt
fi
if type "upx" > /dev/null; then
upx --best "dist/${bin}"
fi
"dist/${bin}" version
if [ -n "${RELEASE}" ]; then

View File

@@ -203,7 +203,7 @@ type FrankenPHPModule struct {
// SplitPath sets the substrings for splitting the URI into two parts. The first matching substring will be used to split the "path info" from the path. The first piece is suffixed with the matching substring and will be assumed as the actual resource (CGI script) name. The second piece will be set to PATH_INFO for the CGI script to use. Default: `.php`.
SplitPath []string `json:"split_path,omitempty"`
// ResolveRootSymlink enables resolving the `root` directory to its actual value by evaluating a symbolic link, if one exists.
ResolveRootSymlink bool `json:"resolve_root_symlink,omitempty"`
ResolveRootSymlink *bool `json:"resolve_root_symlink,omitempty"`
// Env sets an extra environment variable to the given value. Can be specified more than once for multiple environment variables.
Env map[string]string `json:"env,omitempty"`
logger *zap.Logger
@@ -225,8 +225,9 @@ func (f *FrankenPHPModule) Provision(ctx caddy.Context) error {
if frankenphp.EmbeddedAppPath == "" {
f.Root = "{http.vars.root}"
} else {
rrs := false
f.Root = filepath.Join(frankenphp.EmbeddedAppPath, defaultDocumentRoot)
f.ResolveRootSymlink = false
f.ResolveRootSymlink = &rrs
}
} else {
if frankenphp.EmbeddedAppPath != "" && filepath.IsLocal(f.Root) {
@@ -238,6 +239,11 @@ func (f *FrankenPHPModule) Provision(ctx caddy.Context) error {
f.SplitPath = []string{".php"}
}
if f.ResolveRootSymlink == nil {
rrs := true
f.ResolveRootSymlink = &rrs
}
return nil
}
@@ -257,7 +263,7 @@ func (f FrankenPHPModule) ServeHTTP(w http.ResponseWriter, r *http.Request, _ ca
fr, err := frankenphp.NewRequestWithContext(
r,
frankenphp.WithRequestDocumentRoot(documentRoot, f.ResolveRootSymlink),
frankenphp.WithRequestDocumentRoot(documentRoot, *f.ResolveRootSymlink),
frankenphp.WithRequestSplitPath(f.SplitPath),
frankenphp.WithRequestEnv(env),
)
@@ -298,9 +304,18 @@ func (f *FrankenPHPModule) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
case "resolve_root_symlink":
if d.NextArg() {
if v, err := strconv.ParseBool(d.Val()); err == nil {
f.ResolveRootSymlink = &v
if d.NextArg() {
return d.ArgErr()
}
}
return d.ArgErr()
}
f.ResolveRootSymlink = true
rrs := true
f.ResolveRootSymlink = &rrs
}
}
}
@@ -444,7 +459,8 @@ func parsePhpServer(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error)
if phpsrv.Root == "" {
phpsrv.Root = filepath.Join(frankenphp.EmbeddedAppPath, defaultDocumentRoot)
fsrv.Root = phpsrv.Root
phpsrv.ResolveRootSymlink = false
rrs := false
phpsrv.ResolveRootSymlink = &rrs
} else if filepath.IsLocal(fsrv.Root) {
phpsrv.Root = filepath.Join(frankenphp.EmbeddedAppPath, phpsrv.Root)
fsrv.Root = phpsrv.Root

View File

@@ -2,6 +2,8 @@ module github.com/dunglas/frankenphp/caddy
go 1.21
toolchain go1.22.0
replace github.com/dunglas/frankenphp => ../
retract v1.0.0-rc.1 // Human error
@@ -10,7 +12,7 @@ require (
github.com/caddyserver/caddy/v2 v2.7.6
github.com/caddyserver/certmagic v0.20.0
github.com/dunglas/caddy-cbrotli v1.0.0
github.com/dunglas/frankenphp v1.0.3
github.com/dunglas/frankenphp v1.1.0
github.com/dunglas/mercure/caddy v0.15.9
github.com/dunglas/vulcain/caddy v1.0.1
github.com/spf13/cobra v1.8.0

View File

@@ -125,9 +125,11 @@ func cmdPHPServer(fs caddycmd.Flags) (int, error) {
extensions := []string{"php"}
tryFiles := []string{"{http.request.uri.path}", "{http.request.uri.path}/" + indexFile, indexFile}
rrs := true
phpHandler := FrankenPHPModule{
Root: root,
SplitPath: extensions,
Root: root,
SplitPath: extensions,
ResolveRootSymlink: &rrs,
}
// route to redirect to canonical path if index PHP file

View File

@@ -1,5 +1,5 @@
# syntax=docker/dockerfile:1
FROM golang:1.21-alpine
FROM golang:1.22-alpine
ENV CFLAGS="-ggdb3"
ENV PHPIZE_DEPS \

View File

@@ -1,5 +1,5 @@
# syntax=docker/dockerfile:1
FROM golang:1.21
FROM golang:1.22
ENV CFLAGS="-ggdb3"
ENV PHPIZE_DEPS \

View File

@@ -11,7 +11,7 @@ variable "PHP_VERSION" {
}
variable "GO_VERSION" {
default = "1.21"
default = "1.22"
}
variable "SHA" {}

View File

@@ -6,15 +6,16 @@ In the Docker image, the `Caddyfile` is located at `/etc/caddy/Caddyfile`.
You can also configure PHP using `php.ini` as usual.
In the Docker image, the `php.ini` file is not present, you can create it or `COPY` manually.
If you copy `php.ini` from `$PHP_INI_DIR/php.ini-production` or `$PHP_INI_DIR/php.ini-development`, you also must set the variable `variables_order = "EGPCS"`, because the default value for `variables_order` is `"EGPCS"`, but in `php.ini-production` and `php.ini-development` we have `"GPCS"`. And in this case, `worker` does not work properly.
In the Docker image, the `php.ini` file is not present, you can create it or `COPY` manually:
```dockerfile
FROM dunglas/frankenphp
RUN cp $PHP_INI_DIR/php.ini-production $PHP_INI_DIR/php.ini; \
sed -i 's/variables_order = "GPCS"/variables_order = "EGPCS"/' $PHP_INI_DIR/php.ini;
# Developement:
RUN cp $PHP_INI_DIR/php.ini-development $PHP_INI_DIR/php.ini
# Or production:
RUN cp $PHP_INI_DIR/php.ini-production $PHP_INI_DIR/php.ini
```
## Caddyfile Config
@@ -122,7 +123,7 @@ The `php_server` and the `php` directives have the following options:
php_server [<matcher>] {
root <directory> # Sets the root folder to the site. Default: `root` directive.
split_path <delim...> # Sets the substrings for splitting the URI into two parts. The first matching substring will be used to split the "path info" from the path. The first piece is suffixed with the matching substring and will be assumed as the actual resource (CGI script) name. The second piece will be set to PATH_INFO for the CGI script to use. Default: `.php`
resolve_root_symlink # Enables resolving the `root` directory to its actual value by evaluating a symbolic link, if one exists.
resolve_root_symlink false # Disables resolving the `root` directory to its actual value by evaluating a symbolic link, if one exists (enabled by default).
env <key> <value> # Sets an extra environment variable to the given value. Can be specified more than once for multiple environment variables.
}
```

View File

@@ -136,11 +136,29 @@ Here is a sample `Dockerfile` doing this:
FROM dunglas/frankenphp
ARG USER=www-data
USER ${USER}
RUN adduser -D ${USER} \
# Caddy requires an additional capability to bind to port 80 and 443
setcap CAP_NET_BIND_SERVICE=+eip /usr/local/bin/frankenphp
# Caddy requires write access to /data/caddy and /config/caddy
RUN chown -R ${USER}:${USER} /data/caddy && chown -R ${USER}:${USER} /config/caddy
RUN \
# Use "adduser -D ${USER}" for alpine based distros
useradd -D ${USER}; \
# Add additional capability to bind to port 80 and 443
setcap CAP_NET_BIND_SERVICE=+eip /usr/local/bin/frankenphp; \
# Give write access to /data/caddy and /config/caddy
chown -R ${USER}:${USER} /data/caddy && chown -R ${USER}:${USER} /config/caddy;
USER ${USER}
```
## Updates
The Docker images are built:
* when a new release is tagged
* daily at 4am UTC, if new versions of the official PHP images are available
## Development Versions
Development versions are available in the [`dunglas/frankenphp-dev`](https://hub.docker.com/repository/docker/dunglas/frankenphp-dev) Docker repository.
A new build is triggered every time a commit is pushed to the main branch of the GitHub repository.
The `latest*` tags point to the head of the `main` branch.
Tags of the form `sha-<git-commit-hash>` are also available.

View File

@@ -32,10 +32,7 @@ Alternatively, you can run your Laravel projects with FrankenPHP from your local
# Enable compression (optional)
encode zstd br gzip
# Execute PHP files in the current directory and serve assets
php_server {
# Required for the public/storage/ dir
resolve_root_symlink
}
php_server
}
```

View File

@@ -243,7 +243,7 @@ PHP_FUNCTION(frankenphp_finish_request) { /* {{{ */
} /* }}} */
/* {{{ Fetch all HTTP request headers */
PHP_FUNCTION(apache_request_headers) {
PHP_FUNCTION(frankenphp_request_headers) {
if (zend_parse_parameters_none() == FAILURE) {
RETURN_THROWS();
}
@@ -261,7 +261,59 @@ PHP_FUNCTION(apache_request_headers) {
add_assoc_stringl_ex(return_value, key.data, key.len, val.data, val.len);
}
free(headers.r0);
go_apache_request_cleanup(headers.r2);
}
/* }}} */
// add_response_header and apache_response_headers are copied from
// https://github.com/php/php-src/blob/master/sapi/cli/php_cli_server.c
// Copyright (c) The PHP Group
// Licensed under The PHP License
// Original authors: Moriyoshi Koizumi <moriyoshi@php.net> and Xinchen Hui
// <laruence@php.net>
static void add_response_header(sapi_header_struct *h,
zval *return_value) /* {{{ */
{
if (h->header_len > 0) {
char *s;
size_t len = 0;
ALLOCA_FLAG(use_heap)
char *p = strchr(h->header, ':');
if (NULL != p) {
len = p - h->header;
}
if (len > 0) {
while (len != 0 &&
(h->header[len - 1] == ' ' || h->header[len - 1] == '\t')) {
len--;
}
if (len) {
s = do_alloca(len + 1, use_heap);
memcpy(s, h->header, len);
s[len] = 0;
do {
p++;
} while (*p == ' ' || *p == '\t');
add_assoc_stringl_ex(return_value, s, len, p,
h->header_len - (p - h->header));
free_alloca(s, use_heap);
}
}
}
}
/* }}} */
PHP_FUNCTION(frankenphp_response_headers) /* {{{ */
{
if (zend_parse_parameters_none() == FAILURE) {
RETURN_THROWS();
}
array_init(return_value);
zend_llist_apply_with_argument(
&SG(sapi_headers).headers,
(llist_apply_with_arg_func_t)add_response_header, return_value);
}
/* }}} */
@@ -623,6 +675,11 @@ 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);
/* In CGI mode, we consider the environment to be a part of the server
* variables
*/
php_import_environment_variables(track_vars_array);
go_register_variables(ctx->current_request ? ctx->current_request
: ctx->main_request,
track_vars_array);
@@ -784,8 +841,12 @@ static char *cli_script;
static int cli_argc;
static char **cli_argv;
// Adapted from https://github.com/php/php-src/sapi/cli/php_cli.c (The PHP
// Group, The PHP License)
// CLI code is adapted from
// https://github.com/php/php-src/blob/master/sapi/cli/php_cli.c Copyright (c)
// The PHP Group Licensed under The PHP License Original uthors: Edin Kadribasic
// <edink@php.net>, Marcus Boerger <helly@php.net> and Johannes Schlueter
// <johannes@php.net> Parts based on CGI SAPI Module by Rasmus Lerdorf, Stig
// Bakken and Zeev Suraski
static void cli_register_file_handles(bool no_close) /* {{{ */
{
php_stream *s_in, *s_out, *s_err;

View File

@@ -582,24 +582,41 @@ func go_register_variables(rh C.uintptr_t, trackVarsArray *C.zval) {
}
//export go_apache_request_headers
func go_apache_request_headers(rh C.uintptr_t) (*C.go_string, C.size_t) {
func go_apache_request_headers(rh C.uintptr_t) (*C.go_string, C.size_t, C.uintptr_t) {
r := cgo.Handle(rh).Value().(*http.Request)
rl := len(r.Header)
scs := unsafe.Sizeof(C.go_string{})
pinner := &runtime.Pinner{}
pinnerHandle := C.uintptr_t(cgo.NewHandle(pinner))
headers := make([]C.go_string, 0, len(r.Header)*2)
headers := (*C.go_string)(unsafe.Pointer(C.malloc(C.size_t(rl*2) * (C.size_t)(scs))))
header := headers
for field, val := range r.Header {
*header = C.go_string{C.size_t(len(field)), (*C.char)(unsafe.Pointer(unsafe.StringData(field)))}
header = (*C.go_string)(unsafe.Add(unsafe.Pointer(header), scs))
fd := unsafe.StringData(field)
pinner.Pin(fd)
cv := strings.Join(val, ", ")
*header = C.go_string{C.size_t(len(cv)), (*C.char)(unsafe.Pointer(unsafe.StringData(cv)))}
header = (*C.go_string)(unsafe.Add(unsafe.Pointer(header), scs))
vd := unsafe.StringData(cv)
pinner.Pin(vd)
headers = append(
headers,
C.go_string{C.size_t(len(field)), (*C.char)(unsafe.Pointer(fd))},
C.go_string{C.size_t(len(cv)), (*C.char)(unsafe.Pointer(vd))},
)
}
return headers, C.size_t(rl)
sd := unsafe.SliceData(headers)
pinner.Pin(sd)
return sd, C.size_t(len(r.Header)), pinnerHandle
}
//export go_apache_request_cleanup
func go_apache_request_cleanup(rh C.uintptr_t) {
h := cgo.Handle(rh)
p := h.Value().(*runtime.Pinner)
p.Unpin()
h.Delete()
}
func addHeader(fc *FrankenPHPContext, cString *C.char, length C.int) {

View File

@@ -13,9 +13,22 @@ function frankenphp_finish_request(): bool {}
*/
function fastcgi_finish_request(): bool {}
function frankenphp_request_headers(): array {}
/**
* @alias frankenphp_request_headers
*/
function apache_request_headers(): array {}
/**
* @alias apache_request_headers
* @alias frankenphp_response_headers
*/
function getallheaders(): array {}
function frankenphp_response_headers(): array|bool {}
/**
* @alias frankenphp_response_headers
*/
function apache_response_headers(): array|bool {}

View File

@@ -1,5 +1,5 @@
/* This is a generated file, edit the .stub.php file instead.
* Stub hash: f925a1c280fb955eb32d0823cbd4f360b0cbabed */
* Stub hash: 467f1406e17d3b8ca67bba5ea367194e60d8dd27 */
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_frankenphp_handle_request, 0, 1,
_IS_BOOL, 0)
@@ -16,16 +16,25 @@ ZEND_END_ARG_INFO()
#define arginfo_fastcgi_finish_request arginfo_frankenphp_finish_request
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_apache_request_headers, 0, 0,
IS_ARRAY, 0)
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_frankenphp_request_headers, 0,
0, IS_ARRAY, 0)
ZEND_END_ARG_INFO()
#define arginfo_getallheaders arginfo_apache_request_headers
#define arginfo_apache_request_headers arginfo_frankenphp_request_headers
#define arginfo_getallheaders arginfo_frankenphp_request_headers
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_frankenphp_response_headers, 0,
0, MAY_BE_ARRAY | MAY_BE_BOOL)
ZEND_END_ARG_INFO()
#define arginfo_apache_response_headers arginfo_frankenphp_response_headers
ZEND_FUNCTION(frankenphp_handle_request);
ZEND_FUNCTION(headers_send);
ZEND_FUNCTION(frankenphp_finish_request);
ZEND_FUNCTION(apache_request_headers);
ZEND_FUNCTION(frankenphp_request_headers);
ZEND_FUNCTION(frankenphp_response_headers);
static const zend_function_entry ext_functions[] = {
ZEND_FE(frankenphp_handle_request, arginfo_frankenphp_handle_request)
@@ -33,6 +42,16 @@ static const zend_function_entry ext_functions[] = {
frankenphp_finish_request, arginfo_frankenphp_finish_request)
ZEND_FALIAS(fastcgi_finish_request, frankenphp_finish_request,
arginfo_fastcgi_finish_request)
ZEND_FE(apache_request_headers, arginfo_apache_request_headers)
ZEND_FALIAS(getallheaders, apache_request_headers,
arginfo_getallheaders) ZEND_FE_END};
ZEND_FE(frankenphp_request_headers,
arginfo_frankenphp_request_headers)
ZEND_FALIAS(apache_request_headers,
frankenphp_request_headers,
arginfo_apache_request_headers)
ZEND_FALIAS(getallheaders, frankenphp_response_headers,
arginfo_getallheaders)
ZEND_FE(frankenphp_response_headers,
arginfo_frankenphp_response_headers)
ZEND_FALIAS(apache_response_headers,
frankenphp_response_headers,
arginfo_apache_response_headers)
ZEND_FE_END};

View File

@@ -226,6 +226,27 @@ func testHeaders(t *testing.T, opts *testOptions) {
}, opts)
}
func TestResponseHeaders_module(t *testing.T) { testResponseHeaders(t, nil) }
func TestResponseHeaders_worker(t *testing.T) {
testResponseHeaders(t, &testOptions{workerScript: "response-headers.php"})
}
func testResponseHeaders(t *testing.T, opts *testOptions) {
runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/response-headers.php?i=%d", i), nil)
w := httptest.NewRecorder()
handler(w, req)
resp := w.Result()
body, _ := io.ReadAll(resp.Body)
assert.Contains(t, string(body), "'X-Powered-By' => 'PH")
assert.Contains(t, string(body), "'Foo' => 'bar',")
assert.Contains(t, string(body), "'Foo2' => 'bar2',")
assert.Contains(t, string(body), fmt.Sprintf("'I' => '%d',", i))
assert.NotContains(t, string(body), "Invalid")
}, opts)
}
func TestInput_module(t *testing.T) { testInput(t, nil) }
func TestInput_worker(t *testing.T) { testInput(t, &testOptions{workerScript: "input.php"}) }
func testInput(t *testing.T, opts *testOptions) {
@@ -553,15 +574,15 @@ func testFiberNoCgo(t *testing.T, opts *testOptions) {
}, opts)
}
func TestApacheRequestHeaders_module(t *testing.T) { testApacheRequestHeaders(t, &testOptions{}) }
func TestApacheRequestHeaders_worker(t *testing.T) {
testApacheRequestHeaders(t, &testOptions{workerScript: "apache-request-headers.php"})
func TestRequestHeaders_module(t *testing.T) { testRequestHeaders(t, &testOptions{}) }
func TestRequestHeaders_worker(t *testing.T) {
testRequestHeaders(t, &testOptions{workerScript: "request-headers.php"})
}
func testApacheRequestHeaders(t *testing.T, opts *testOptions) {
func testRequestHeaders(t *testing.T, opts *testOptions) {
runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/apache-request-headers.php?i=%d", i), nil)
req.Header.Add("Content-Type", "text/plain")
req.Header.Add("Frankenphp-I", strconv.Itoa(i))
req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/request-headers.php?i=%d", i), nil)
req.Header.Add(strings.Clone("Content-Type"), strings.Clone("text/plain"))
req.Header.Add(strings.Clone("Frankenphp-I"), strings.Clone(strconv.Itoa(i)))
w := httptest.NewRecorder()
handler(w, req)
@@ -644,6 +665,7 @@ func BenchmarkHelloWorld(b *testing.B) {
req := httptest.NewRequest("GET", "http://example.com/index.php", nil)
w := httptest.NewRecorder()
b.ResetTimer()
for i := 0; i < b.N; i++ {
handler(w, req)
}
@@ -709,8 +731,77 @@ func BenchmarkEcho(b *testing.B) {
req := httptest.NewRequest("POST", "http://example.com/echo.php", r)
w := httptest.NewRecorder()
b.ResetTimer()
for i := 0; i < b.N; i++ {
r.Reset(body)
handler(w, req)
}
}
func BenchmarkServerSuperGlobal(b *testing.B) {
if err := frankenphp.Init(frankenphp.WithLogger(zap.NewNop())); err != nil {
panic(err)
}
defer frankenphp.Shutdown()
cwd, _ := os.Getwd()
testDataDir := cwd + "/testdata/"
// Mimicks headers of a request sent by Firefox to GitHub
headers := http.Header{}
headers.Add(strings.Clone("Accept"), strings.Clone("text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8"))
headers.Add(strings.Clone("Accept-Encoding"), strings.Clone("gzip, deflate, br"))
headers.Add(strings.Clone("Accept-Language"), strings.Clone("fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3"))
headers.Add(strings.Clone("Cache-Control"), strings.Clone("no-cache"))
headers.Add(strings.Clone("Connection"), strings.Clone("keep-alive"))
headers.Add(strings.Clone("Cookie"), strings.Clone("user_session=myrandomuuid; __Host-user_session_same_site=myotherrandomuuid; dotcom_user=dunglas; logged_in=yes; _foo=barbarbarbarbarbar; _device_id=anotherrandomuuid; color_mode=foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobar; preferred_color_mode=light; tz=Europe%2FParis; has_recent_activity=1"))
headers.Add(strings.Clone("DNT"), strings.Clone("1"))
headers.Add(strings.Clone("Host"), strings.Clone("example.com"))
headers.Add(strings.Clone("Pragma"), strings.Clone("no-cache"))
headers.Add(strings.Clone("Sec-Fetch-Dest"), strings.Clone("document"))
headers.Add(strings.Clone("Sec-Fetch-Mode"), strings.Clone("navigate"))
headers.Add(strings.Clone("Sec-Fetch-Site"), strings.Clone("cross-site"))
headers.Add(strings.Clone("Sec-GPC"), strings.Clone("1"))
headers.Add(strings.Clone("Upgrade-Insecure-Requests"), strings.Clone("1"))
headers.Add(strings.Clone("User-Agent"), strings.Clone("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:122.0) Gecko/20100101 Firefox/122.0"))
// Env vars available in a typical Docker container
env := map[string]string{
"HOSTNAME": "a88e81aa22e4",
"PHP_INI_DIR": "/usr/local/etc/php",
"HOME": "/root",
"GODEBUG": "cgocheck=0",
"PHP_LDFLAGS": "-Wl,-O1 -pie",
"PHP_CFLAGS": "-fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64",
"PHP_VERSION": "8.3.2",
"GPG_KEYS": "1198C0117593497A5EC5C199286AF1F9897469DC C28D937575603EB4ABB725861C0779DC5C0A9DE4 AFD8691FDAEDF03BDF6E460563F15A9B715376CA",
"PHP_CPPFLAGS": "-fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64",
"PHP_ASC_URL": "https://www.php.net/distributions/php-8.3.2.tar.xz.asc",
"PHP_URL": "https://www.php.net/distributions/php-8.3.2.tar.xz",
"PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"XDG_CONFIG_HOME": "/config",
"XDG_DATA_HOME": "/data",
"PHPIZE_DEPS": "autoconf dpkg-dev file g++ gcc libc-dev make pkg-config re2c",
"PWD": "/app",
"PHP_SHA256": "4ffa3e44afc9c590e28dc0d2d31fc61f0139f8b335f11880a121b9f9b9f0634e",
}
handler := func(w http.ResponseWriter, r *http.Request) {
req, err := frankenphp.NewRequestWithContext(r, frankenphp.WithRequestDocumentRoot(testDataDir, false), frankenphp.WithRequestEnv(env))
if err != nil {
panic(err)
}
r.Header = headers
if err := frankenphp.ServeHTTP(w, req); err != nil {
panic(err)
}
}
req := httptest.NewRequest("GET", "http://example.com/server-variable.php", nil)
w := httptest.NewRecorder()
b.ResetTimer()
for i := 0; i < b.N; i++ {
handler(w, req)
}
}

2
go.mod
View File

@@ -2,6 +2,8 @@ module github.com/dunglas/frankenphp
go 1.21
toolchain go1.22.0
retract v1.0.0-rc.1 // Human error
require (

View File

@@ -47,8 +47,3 @@ tags=$(git tag --list --sort=-version:refname 'v*')
previous_tag=$(awk 'NR==2 {print;exit}' <<< "${tags}")
gh release create --draft --generate-notes --latest --notes-start-tag "${previous_tag}" --verify-tag "v$1"
if [[ "$(uname -s)" = "Darwin" ]]; then
rm -Rf dist/*
FRANKENPHP_VERSION=$1 RELEASE=1 ./build-static.sh
fi

View File

@@ -33,6 +33,7 @@ RUN apk update; \
bison \
build-base \
cmake \
composer \
curl \
file \
flex \
@@ -47,11 +48,6 @@ RUN apk update; \
m4 \
make \
pkgconfig \
wget \
xz ; \
apk add --no-cache \
--repository=https://dl-cdn.alpinelinux.org/alpine/edge/main \
--repository=https://dl-cdn.alpinelinux.org/alpine/edge/community \
php83 \
php83-common \
php83-ctype \
@@ -66,14 +62,14 @@ RUN apk update; \
php83-sodium \
php83-tokenizer \
php83-xml \
php83-xmlwriter; \
php83-xmlwriter \
upx \
wget \
xz ; \
ln -sf /usr/bin/php83 /usr/bin/php
# https://getcomposer.org/doc/03-cli.md#composer-allow-superuser
ENV COMPOSER_ALLOW_SUPERUSER=1
ENV PATH="${PATH}:/root/.composer/vendor/bin"
COPY --from=composer/composer:2-bin --link /composer /usr/bin/composer
WORKDIR /go/src/app
COPY go.mod go.sum ./

25
testdata/load-test.js vendored
View File

@@ -58,7 +58,30 @@ const payload = 'foo\n'.repeat(1000)
// about authoring k6 scripts.
//
export default function () {
const res = http.post('http://localhost/echo.php', payload)
const params = {
headers: {
Accept:
'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
// 'Accept-Encoding': 'br',
'Accept-Language': 'fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
Cookie:
'user_session=myrandomuuid; __Host-user_session_same_site=myotherrandomuuid; dotcom_user=dunglas; logged_in=yes; _foo=barbarbarbarbarbar; _device_id=anotherrandomuuid; color_mode=foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobar; preferred_color_mode=light; tz=Europe%2FParis; has_recent_activity=1',
DNT: '1',
Host: 'example.com',
Pragma: 'no-cache',
'Sec-Fetch-Dest': 'document',
'Sec-Fetch-Mode': 'navigate',
'Sec-Fetch-Site': 'cross-site',
'Sec-GPC': '1',
'Upgrade-Insecure-Requests': '1',
'User-Agent':
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:122.0) Gecko/20100101 Firefox/122.0'
}
}
const res = http.post('http://localhost/echo.php', payload, params)
check(res, {
'is status 200': (r) => r.status === 200,
'is echoed': (r) => r.body === payload

13
testdata/response-headers.php vendored Normal file
View File

@@ -0,0 +1,13 @@
<?php
require_once __DIR__.'/_executor.php';
return function () {
header('Foo: bar');
header('Foo2: bar2');
header('Invalid');
header('I: ' . ($_GET['i'] ?? 'i not set'));
http_response_code(201);
var_export(apache_response_headers());
};