mirror of
https://github.com/dunglas/frankenphp.git
synced 2025-12-24 13:38:11 +08:00
Compare commits
169 Commits
v1.0.0-bet
...
refactor/b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e118b9f681 | ||
|
|
ab7ce9cb18 | ||
|
|
5afde55ebf | ||
|
|
da63e700b0 | ||
|
|
5a8e5f9518 | ||
|
|
3d9f344a50 | ||
|
|
175b9a0296 | ||
|
|
65c8720250 | ||
|
|
59dd04234f | ||
|
|
36a6daa8ba | ||
|
|
85b36acce1 | ||
|
|
d9618ac917 | ||
|
|
be08c1e717 | ||
|
|
62d53253d1 | ||
|
|
f35e276ee6 | ||
|
|
a736a5a2ce | ||
|
|
c71e55551c | ||
|
|
7454826d4b | ||
|
|
49baf02035 | ||
|
|
a92d774742 | ||
|
|
effb5805f1 | ||
|
|
5d32f32639 | ||
|
|
bccbaafc84 | ||
|
|
6f108a4203 | ||
|
|
3bdd6fd072 | ||
|
|
183451a13f | ||
|
|
2ef7762068 | ||
|
|
b624a13430 | ||
|
|
00b1d0e4b6 | ||
|
|
d4c313f3db | ||
|
|
f19c153d06 | ||
|
|
3692818d0c | ||
|
|
7830aae549 | ||
|
|
2055142904 | ||
|
|
9b9957f5cf | ||
|
|
5bda50cbd7 | ||
|
|
11711bb6da | ||
|
|
497876443c | ||
|
|
94776119bd | ||
|
|
229cb9e3e6 | ||
|
|
67fdefc416 | ||
|
|
e962f4394f | ||
|
|
0054b92115 | ||
|
|
34e4ef1e84 | ||
|
|
9bf991ca88 | ||
|
|
64672a267a | ||
|
|
f16cb0b5f0 | ||
|
|
efa49848e4 | ||
|
|
2eabec8c92 | ||
|
|
f6873efee4 | ||
|
|
47ada94c41 | ||
|
|
a571d990ec | ||
|
|
b7c8d4cd49 | ||
|
|
7c5f18fe3f | ||
|
|
0015493543 | ||
|
|
3709c2a50b | ||
|
|
8dd22269dc | ||
|
|
f71f9875ca | ||
|
|
184b86c6e3 | ||
|
|
77f858d009 | ||
|
|
517e086786 | ||
|
|
ebd5b45dda | ||
|
|
ebcd937092 | ||
|
|
781e8607ae | ||
|
|
2ac2e9ec38 | ||
|
|
b2e92a673a | ||
|
|
b6e2277863 | ||
|
|
9410418c5c | ||
|
|
1da3dbc93a | ||
|
|
fd6e28df2a | ||
|
|
efa8b243c9 | ||
|
|
e3e2ab3612 | ||
|
|
9046b97fa5 | ||
|
|
cb02ce4783 | ||
|
|
9b09be22be | ||
|
|
1564c762ee | ||
|
|
4e00ed1a7b | ||
|
|
6ba945091b | ||
|
|
e19aa75e72 | ||
|
|
d7e40985d2 | ||
|
|
50feb79326 | ||
|
|
b845670f07 | ||
|
|
72c22f3c6e | ||
|
|
d93bfc0cb9 | ||
|
|
b9eae571e5 | ||
|
|
72d983ec5e | ||
|
|
d427f55298 | ||
|
|
5e946fc02a | ||
|
|
ad1c4500d3 | ||
|
|
5c9d7d3f6d | ||
|
|
58597bfeab | ||
|
|
5235cb9ae1 | ||
|
|
e3361c2b3f | ||
|
|
dee84ed906 | ||
|
|
537f899939 | ||
|
|
7536b89815 | ||
|
|
49d81c4ea2 | ||
|
|
ee38702846 | ||
|
|
dd1af53432 | ||
|
|
8664f8f4bc | ||
|
|
6509cddd2a | ||
|
|
bb931c99ff | ||
|
|
f1ccb0a9c5 | ||
|
|
99e0a7038a | ||
|
|
723f40be16 | ||
|
|
8eb1e9dea9 | ||
|
|
c9bf9940d1 | ||
|
|
b675d09c49 | ||
|
|
e9dfe4000e | ||
|
|
5a85a11c60 | ||
|
|
de5de146c3 | ||
|
|
b32e738d75 | ||
|
|
c884d26799 | ||
|
|
b4aa8038ff | ||
|
|
2e72b50d10 | ||
|
|
39b4f75365 | ||
|
|
d80cc88ae1 | ||
|
|
2c583bcb16 | ||
|
|
aa1d968dcf | ||
|
|
ca76e3a763 | ||
|
|
b56b47d187 | ||
|
|
4c70ae285a | ||
|
|
3d2c9b6f6f | ||
|
|
12791636ee | ||
|
|
981f954cf2 | ||
|
|
0266175df8 | ||
|
|
9d56f2530a | ||
|
|
4d2cd5f715 | ||
|
|
a4938102e1 | ||
|
|
bb1ed8b212 | ||
|
|
e7bd54cc00 | ||
|
|
ea2c7c2895 | ||
|
|
8475ad9bc8 | ||
|
|
35d4075c50 | ||
|
|
255dd4b6d6 | ||
|
|
ce1b02b1bd | ||
|
|
c1b8c5df5f | ||
|
|
43722a9dbe | ||
|
|
385b47ee3b | ||
|
|
90caf2701a | ||
|
|
e13e394700 | ||
|
|
6874e44ddd | ||
|
|
2a205b16ce | ||
|
|
dfa19978f8 | ||
|
|
120006a297 | ||
|
|
8c57f4cc85 | ||
|
|
7f9c50e6f5 | ||
|
|
ec35afdc7f | ||
|
|
51038bbdf5 | ||
|
|
c615fe0087 | ||
|
|
af3ed6e26d | ||
|
|
669a0175f3 | ||
|
|
52caf17a40 | ||
|
|
e193a374d3 | ||
|
|
8b46d79560 | ||
|
|
9bf24b7d88 | ||
|
|
c624971fa7 | ||
|
|
b4780b6495 | ||
|
|
b04326ee83 | ||
|
|
5874072d46 | ||
|
|
7d41aa5243 | ||
|
|
2d91a606fd | ||
|
|
69416cc0d6 | ||
|
|
29be0c0cdb | ||
|
|
eca8cc7350 | ||
|
|
f8e7b161b5 | ||
|
|
374c581ee5 | ||
|
|
53a7f64984 | ||
|
|
3f9cda365f |
@@ -1,79 +0,0 @@
|
||||
version: 2.1
|
||||
|
||||
orbs:
|
||||
gh: circleci/github-cli@2.2.0
|
||||
|
||||
jobs:
|
||||
release_mac_arm:
|
||||
macos:
|
||||
xcode: 14.3.1
|
||||
environment:
|
||||
HOMEBREW_NO_AUTO_UPDATE: 1
|
||||
steps:
|
||||
- checkout
|
||||
- run: brew install --formula go automake cmake composer
|
||||
- run:
|
||||
name: Clone static-php-cli
|
||||
command: git clone --depth 1 https://github.com/crazywhalecc/static-php-cli
|
||||
- restore_cache:
|
||||
keys:
|
||||
- spc-{{ checksum "static-php-cli/composer.json" }}
|
||||
- run:
|
||||
name: Install static-php-cli and fetch libraries sources
|
||||
working_directory: static-php-cli/
|
||||
command: |
|
||||
composer install --no-dev -a
|
||||
./bin/spc fetch --with-php=8.2 -A
|
||||
- save_cache:
|
||||
key: spc-{{ checksum "static-php-cli/composer.json" }}
|
||||
paths:
|
||||
- static-php-cli/downloads/
|
||||
- static-php-cli/vendor/
|
||||
- run:
|
||||
working_directory: static-php-cli/
|
||||
name: Build libphp.a
|
||||
command: ./bin/spc build --enable-zts --build-embed "bcmath,calendar,ctype,curl,dba,dom,exif,filter,fileinfo,gd,iconv,intl,mbstring,mbregex,mysqli,mysqlnd,opcache,openssl,pcntl,pdo,pdo_mysql,pdo_pgsql,pdo_sqlite,pgsql,phar,posix,readline,redis,session,simplexml,sockets,sqlite3,tokenizer,xml,xmlreader,xmlwriter,zip,zlib,apcu"
|
||||
- run:
|
||||
working_directory: static-php-cli/
|
||||
name: Set CGO flags
|
||||
command: |
|
||||
if [ -z "$CIRCLE_TAG" ]; then export FRANKENPHP_VERSION=$CIRCLE_SHA1; else export FRANKENPHP_VERSION=${CIRCLE_TAG:1}; fi
|
||||
echo "export CGO_CFLAGS='-DFRANKENPHP_VERSION=$FRANKENPHP_VERSION $(./buildroot/bin/php-config --includes | sed s#-I/#-I$PWD/buildroot/#g)'" >> "$BASH_ENV"
|
||||
echo "export CGO_LDFLAGS='-framework CoreFoundation -framework SystemConfiguration $(./buildroot/bin/php-config --ldflags) $(./buildroot/bin/php-config --libs)'" >> "$BASH_ENV"
|
||||
echo "export PHP_VERSION='$(./buildroot/bin/php-config --version)'" >> "$BASH_ENV"
|
||||
echo "export FRANKENPHP_VERSION='$FRANKENPHP_VERSION'" >> "$BASH_ENV"
|
||||
- restore_cache:
|
||||
keys:
|
||||
- go-mod-v4-{{ checksum "caddy/go.sum" }}
|
||||
- run:
|
||||
name: Build FrankenPHP
|
||||
working_directory: caddy/frankenphp/
|
||||
command: |
|
||||
go env
|
||||
go build -buildmode=pie -tags "cgo netgo osusergo static_build" -ldflags "-linkmode=external -extldflags -static-pie -w -s -X 'github.com/caddyserver/caddy/v2.CustomVersion=FrankenPHP $FRANKENPHP_VERSION PHP $PHP_VERSION Caddy'" -o frankenphp-mac-arm64
|
||||
./frankenphp-mac-arm64 version
|
||||
- gh/setup
|
||||
- run:
|
||||
name: Upload asset
|
||||
working_directory: caddy/frankenphp/
|
||||
command: |
|
||||
if [ -n "$CIRCLE_TAG" ]; then
|
||||
gh release upload $CIRCLE_TAG frankenphp-mac-arm64 --repo dunglas/frankenphp --clobber
|
||||
fi
|
||||
- store_artifacts:
|
||||
path: caddy/frankenphp/frankenphp-mac-arm64
|
||||
destination: frankenphp-mac-arm64
|
||||
- save_cache:
|
||||
key: go-mod-v4-{{ checksum "caddy/go.sum" }}
|
||||
paths:
|
||||
- "~/go/pkg/mod"
|
||||
|
||||
workflows:
|
||||
release:
|
||||
jobs:
|
||||
- release_mac_arm:
|
||||
filters:
|
||||
branches:
|
||||
ignore: /.*/
|
||||
tags:
|
||||
only: /^v.*/
|
||||
@@ -8,3 +8,7 @@
|
||||
!**/*.c
|
||||
!**/*.h
|
||||
!testdata/*.php
|
||||
!testdata/*.txt
|
||||
!build-static.sh
|
||||
!app.tar
|
||||
!app_checksum.txt
|
||||
|
||||
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
@@ -1 +0,0 @@
|
||||
github: dunglas
|
||||
78
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
Normal file
78
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
Normal file
@@ -0,0 +1,78 @@
|
||||
---
|
||||
name: Bug Report
|
||||
description: File a bug report
|
||||
labels: [bug]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report!
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
label: What happened?
|
||||
description: |
|
||||
Tell us what you do, what you get and what you expected.
|
||||
Provide us with some step-by-step instructions to reproduce the issue.
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: build
|
||||
attributes:
|
||||
label: Build Type
|
||||
description: What build of FrankenPHP do you use?
|
||||
options:
|
||||
- Docker (Debian Bookworm)
|
||||
- Docker (Alpine)
|
||||
- Official static build
|
||||
- Standalone binary
|
||||
- Custom (tell us more in the description)
|
||||
default: 0
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: worker
|
||||
attributes:
|
||||
label: Worker Mode
|
||||
description: Does the problem happen only when using the worker mode?
|
||||
options:
|
||||
- "Yes"
|
||||
- "No"
|
||||
default: 0
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: os
|
||||
attributes:
|
||||
label: Operating System
|
||||
description: What operating system are you executing FrankenPHP with?
|
||||
options:
|
||||
- GNU/Linux
|
||||
- macOS
|
||||
- Other (tell us more in the description)
|
||||
default: 0
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: arch
|
||||
attributes:
|
||||
label: CPU Architecture
|
||||
description: What CPU architecture are you using?
|
||||
options:
|
||||
- x86_64
|
||||
- Apple Silicon
|
||||
- x86
|
||||
- aarch64
|
||||
- Other (tell us more in the description)
|
||||
default: 0
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Relevant log output
|
||||
description: |
|
||||
Please copy and paste any relevant log output.
|
||||
This will be automatically formatted into code,
|
||||
so no need for backticks.
|
||||
render: shell
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature Request
|
||||
description: Suggest an idea for this project
|
||||
labels: [enhancement]
|
||||
body:
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Describe you feature request
|
||||
value: |
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is.
|
||||
Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions
|
||||
or features you've considered.
|
||||
31
.github/dependabot.yaml
vendored
Normal file
31
.github/dependabot.yaml
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
---
|
||||
version: 2
|
||||
updates:
|
||||
-
|
||||
package-ecosystem: gomod
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
commit-message:
|
||||
prefix: chore
|
||||
-
|
||||
package-ecosystem: gomod
|
||||
directory: /caddy
|
||||
schedule:
|
||||
interval: weekly
|
||||
commit-message:
|
||||
prefix: chore(caddy)
|
||||
# These packages must be in sync with versions
|
||||
# used by github.com/caddyserver/caddy/v2
|
||||
ignore:
|
||||
-
|
||||
dependency-name: github.com/google/cel-go
|
||||
-
|
||||
dependency-name: github.com/quic-go/*
|
||||
-
|
||||
package-ecosystem: github-actions
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
commit-message:
|
||||
prefix: ci
|
||||
258
.github/workflows/docker.yaml
vendored
Normal file
258
.github/workflows/docker.yaml
vendored
Normal file
@@ -0,0 +1,258 @@
|
||||
---
|
||||
name: Build Docker images
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- v*.*.*
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'FrankenPHP version'
|
||||
required: false
|
||||
type: string
|
||||
schedule:
|
||||
- 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
|
||||
outputs:
|
||||
# Push if it's a scheduled job, a tag, or if we're committing to the main branch
|
||||
push: ${{ (github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && inputs.version) || startsWith(github.ref, 'refs/tags/') || (github.ref == 'refs/heads/main' && github.event_name != 'pull_request')) && true || false }}
|
||||
variants: ${{ steps.matrix.outputs.variants }}
|
||||
platforms: ${{ steps.matrix.outputs.platforms }}
|
||||
metadata: ${{ steps.matrix.outputs.metadata }}
|
||||
php_version: ${{ steps.check.outputs.php_version }}
|
||||
php82_version: ${{ steps.check.outputs.php82_version }}
|
||||
php83_version: ${{ steps.check.outputs.php83_version }}
|
||||
skip: ${{ steps.check.outputs.skip }}
|
||||
ref: ${{ steps.check.outputs.ref || (github.event_name == 'workflow_dispatch' && inputs.version) || '' }}
|
||||
steps:
|
||||
-
|
||||
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="; "")')
|
||||
{
|
||||
echo php_version="${PHP_83_LATEST},${PHP_82_LATEST}"
|
||||
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="; "")')
|
||||
|
||||
if [[ "${FRANKENPHP_82_LATEST}" == "${PHP_82_LATEST}" ]] && [[ "${FRANKENPHP_83_LATEST}" == "${PHP_83_LATEST}" ]]; then
|
||||
echo skip=true >> "${GITHUB_OUTPUT}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
{
|
||||
echo ref="$(gh release view --repo dunglas/frankenphp --json tagName --jq '.tagName')"
|
||||
echo skip=false
|
||||
} >> "${GITHUB_OUTPUT}"
|
||||
-
|
||||
uses: actions/checkout@v4
|
||||
if: ${{ !fromJson(steps.check.outputs.skip) }}
|
||||
with:
|
||||
ref: ${{ steps.check.outputs.ref }}
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
version: latest
|
||||
-
|
||||
name: Create variants matrix
|
||||
if: ${{ !fromJson(steps.check.outputs.skip) }}
|
||||
id: matrix
|
||||
run: |
|
||||
METADATA="$(docker buildx bake --print | jq -c)"
|
||||
{
|
||||
echo metadata="${METADATA}"
|
||||
echo variants="$(jq -c '.group.default.targets|map(sub("runner-|builder-"; ""))|unique' <<< "${METADATA}")"
|
||||
echo platforms="$(jq -c 'first(.target[]) | .platforms' <<< "${METADATA}")"
|
||||
} >> "${GITHUB_OUTPUT}"
|
||||
env:
|
||||
SHA: ${{ github.sha }}
|
||||
VERSION: ${{ (github.ref_type == 'tag' && github.ref_name) || steps.check.outputs.ref || github.sha }}
|
||||
PHP_VERSION: ${{ steps.check.outputs.php_version }}
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- prepare
|
||||
if: ${{ !fromJson(needs.prepare.outputs.skip) }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
variant: ${{ fromJson(needs.prepare.outputs.variants) }}
|
||||
platform: ${{ fromJson(needs.prepare.outputs.platforms) }}
|
||||
include:
|
||||
-
|
||||
race: ""
|
||||
qemu: true
|
||||
-
|
||||
platform: linux/amd64
|
||||
qemu: false
|
||||
race: "-race" # The Go race detector is only supported on amd64
|
||||
-
|
||||
platform: linux/386
|
||||
qemu: false
|
||||
exclude:
|
||||
# arm/v6 is only available for Alpine: https://github.com/docker-library/golang/issues/502
|
||||
-
|
||||
variant: php-${{ needs.prepare.outputs.php82_version }}-bookworm
|
||||
platform: linux/arm/v6
|
||||
-
|
||||
variant: php-${{ needs.prepare.outputs.php83_version }}-bookworm
|
||||
platform: linux/arm/v6
|
||||
steps:
|
||||
-
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ needs.prepare.outputs.ref }}
|
||||
-
|
||||
name: Set up QEMU
|
||||
if: matrix.qemu
|
||||
uses: docker/setup-qemu-action@v3
|
||||
with:
|
||||
platforms: ${{ matrix.platform }}
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
platforms: ${{ matrix.platform }}
|
||||
version: latest
|
||||
-
|
||||
name: Login to DockerHub
|
||||
if: fromJson(needs.prepare.outputs.push)
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
-
|
||||
name: Build
|
||||
id: build
|
||||
uses: docker/bake-action@v4
|
||||
with:
|
||||
pull: true
|
||||
load: ${{ !fromJson(needs.prepare.outputs.push) }}
|
||||
targets: |
|
||||
builder-${{ matrix.variant }}
|
||||
runner-${{ matrix.variant }}
|
||||
# Remove tags to prevent "can't push tagged ref [...] by digest" error
|
||||
set: |
|
||||
*.tags=
|
||||
*.platform=${{ matrix.platform }}
|
||||
*.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' || '' }}
|
||||
env:
|
||||
SHA: ${{ github.sha }}
|
||||
VERSION: ${{ github.ref_type == 'tag' && github.ref_name || needs.prepare.outputs.ref || github.sha }}
|
||||
PHP_VERSION: ${{ needs.prepare.outputs.php_version }}
|
||||
-
|
||||
# Workaround for https://github.com/actions/runner/pull/2477#issuecomment-1501003600
|
||||
name: Export metadata
|
||||
if: fromJson(needs.prepare.outputs.push)
|
||||
run: |
|
||||
mkdir -p /tmp/metadata/builder /tmp/metadata/runner
|
||||
|
||||
builderDigest=$(jq -r '."builder-${{ matrix.variant }}"."containerimage.digest"' <<< "${METADATA}")
|
||||
touch "/tmp/metadata/builder/${builderDigest#sha256:}"
|
||||
|
||||
runnerDigest=$(jq -r '."runner-${{ matrix.variant }}"."containerimage.digest"' <<< "${METADATA}")
|
||||
touch "/tmp/metadata/runner/${runnerDigest#sha256:}"
|
||||
env:
|
||||
METADATA: ${{ steps.build.outputs.metadata }}
|
||||
-
|
||||
name: Upload builder metadata
|
||||
if: fromJson(needs.prepare.outputs.push)
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: metadata-builder-${{ matrix.variant }}
|
||||
path: /tmp/metadata/builder/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
-
|
||||
name: Upload runner metadata
|
||||
if: fromJson(needs.prepare.outputs.push)
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: metadata-runner-${{ matrix.variant }}
|
||||
path: /tmp/metadata/runner/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
-
|
||||
name: Run tests
|
||||
if: ${{ !matrix.qemu && !fromJson(needs.prepare.outputs.push) }}
|
||||
run: |
|
||||
docker run --platform=${{ matrix.platform }} --rm \
|
||||
"$(jq -r '."builder-${{ matrix.variant }}"."containerimage.config.digest"' <<< "${METADATA}")" \
|
||||
sh -c 'go test ${{ matrix.race }} -v ./... && cd caddy && go test ${{ matrix.race }} -v ./...'
|
||||
env:
|
||||
METADATA: ${{ steps.build.outputs.metadata }}
|
||||
# Adapted from https://docs.docker.com/build/ci/github-actions/multi-platform/
|
||||
push:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- prepare
|
||||
- build
|
||||
if: fromJson(needs.prepare.outputs.push)
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
variant: ${{ fromJson(needs.prepare.outputs.variants) }}
|
||||
target: ['builder', 'runner']
|
||||
steps:
|
||||
-
|
||||
name: Download metadata
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: metadata-${{ matrix.target }}-${{ matrix.variant }}
|
||||
path: /tmp/metadata
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
# Temporary fix for https://github.com/docker/buildx/issues/2229
|
||||
version: "https://github.com/jedevc/buildx.git#imagetools-resolver-copy-dupe"
|
||||
-
|
||||
name: Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
-
|
||||
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 ' *)
|
||||
env:
|
||||
METADATA: ${{ needs.prepare.outputs.metadata }}
|
||||
-
|
||||
name: Inspect image
|
||||
run: |
|
||||
# shellcheck disable=SC2046,SC2086
|
||||
docker buildx imagetools inspect $(jq -cr '.target."${{ matrix.target }}-${{ matrix.variant }}".tags | first' <<< ${METADATA})
|
||||
env:
|
||||
METADATA: ${{ needs.prepare.outputs.metadata }}
|
||||
188
.github/workflows/docker.yml
vendored
188
.github/workflows/docker.yml
vendored
@@ -1,188 +0,0 @@
|
||||
name: Build Docker images
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- v*.*.*
|
||||
workflow_dispatch:
|
||||
inputs: {}
|
||||
jobs:
|
||||
prepare:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
# Push only if it's a tag or if we're committing in the main branch
|
||||
push: ${{toJson(startsWith(github.ref, 'refs/tags/') || (github.ref == 'refs/heads/main' && github.event_name != 'pull_request'))}}
|
||||
variants: ${{ steps.matrix.outputs.variants }}
|
||||
platforms: ${{ steps.matrix.outputs.platforms }}
|
||||
metadata: ${{ steps.matrix.outputs.metadata }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
with:
|
||||
version: latest
|
||||
|
||||
- name: Create variants matrix
|
||||
id: matrix
|
||||
run: |
|
||||
METADATA=$(docker buildx bake --print | jq -c)
|
||||
echo "metadata=$METADATA" >> "$GITHUB_OUTPUT"
|
||||
echo "variants=$(jq -c '.group.default.targets|map(sub("runner-|builder-"; ""))|unique' <<< $METADATA)" >> "$GITHUB_OUTPUT"
|
||||
echo "platforms=$(jq -c 'first(.target[]) | .platforms' <<< $METADATA)" >> "$GITHUB_OUTPUT"
|
||||
env:
|
||||
LATEST: '1' # TODO: unset this variable when releasing the first stable version
|
||||
SHA: ${{github.sha}}
|
||||
VERSION: ${{github.ref_type == 'tag' && github.ref_name || github.sha}}
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- prepare
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
variant: ${{ fromJson(needs.prepare.outputs.variants) }}
|
||||
platform: ${{ fromJson(needs.prepare.outputs.platforms) }}
|
||||
include:
|
||||
- race: ""
|
||||
qemu: true
|
||||
- platform: linux/amd64
|
||||
qemu: false
|
||||
race: "-race" # The Go race detector is only supported on amd64
|
||||
- platform: linux/386
|
||||
qemu: false
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
if: matrix.qemu
|
||||
uses: docker/setup-qemu-action@v2
|
||||
with:
|
||||
platforms: ${{matrix.platform}}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
with:
|
||||
platforms: ${{matrix.platform}}
|
||||
version: latest
|
||||
|
||||
- name: Login to DockerHub
|
||||
if: fromJson(needs.prepare.outputs.push)
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{secrets.REGISTRY_USERNAME}}
|
||||
password: ${{secrets.REGISTRY_PASSWORD}}
|
||||
|
||||
- name: Build
|
||||
id: build
|
||||
uses: docker/bake-action@v3
|
||||
with:
|
||||
pull: true
|
||||
load: ${{!fromJson(needs.prepare.outputs.push)}}
|
||||
targets: |
|
||||
builder-${{matrix.variant}}
|
||||
runner-${{matrix.variant}}
|
||||
# Remove tags to prevent "can't push tagged ref [...] by digest" error
|
||||
set: |
|
||||
*.tags=
|
||||
*.platform=${{matrix.platform}}
|
||||
*.cache-from=type=gha,scope=${{github.ref}}-${{matrix.platform}}
|
||||
*.cache-from=type=gha,scope=refs/heads/main-${{matrix.platform}}
|
||||
*.cache-to=type=gha,scope=${{github.ref}}-${{matrix.platform}}
|
||||
${{fromJson(needs.prepare.outputs.push) && '*.output=type=image,name=dunglas/frankenphp,push-by-digest=true,name-canonical=true,push=true' || ''}}
|
||||
env:
|
||||
LATEST: '1' # TODO: unset this variable when releasing the first stable version
|
||||
SHA: ${{github.sha}}
|
||||
VERSION: ${{github.ref_type == 'tag' && github.ref_name || github.sha}}
|
||||
|
||||
# Workaround for https://github.com/actions/runner/pull/2477#issuecomment-1501003600
|
||||
- name: Export metadata
|
||||
if: fromJson(needs.prepare.outputs.push)
|
||||
run: |
|
||||
mkdir -p /tmp/metadata/builder /tmp/metadata/runner
|
||||
|
||||
builderDigest=$(jq -r '."builder-${{matrix.variant}}"."containerimage.digest"' <<< $METADATA)
|
||||
touch "/tmp/metadata/builder/${builderDigest#sha256:}"
|
||||
|
||||
runnerDigest=$(jq -r '."runner-${{matrix.variant}}"."containerimage.digest"' <<< $METADATA)
|
||||
touch "/tmp/metadata/runner/${runnerDigest#sha256:}"
|
||||
env:
|
||||
METADATA: ${{steps.build.outputs.metadata}}
|
||||
|
||||
- name: Upload runner metadata
|
||||
if: fromJson(needs.prepare.outputs.push)
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: metadata-builder-${{matrix.variant}}
|
||||
path: /tmp/metadata/builder/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
- name: Upload runner metadata
|
||||
if: fromJson(needs.prepare.outputs.push)
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: metadata-runner-${{matrix.variant}}
|
||||
path: /tmp/metadata/runner/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
- name: Run tests
|
||||
if: '!matrix.qemu'
|
||||
continue-on-error: ${{fromJson(needs.prepare.outputs.push)}}
|
||||
run: |
|
||||
docker run --platform=${{matrix.platform}} --rm \
|
||||
$(jq -r '."builder-${{matrix.variant}}"."containerimage.config.digest"' <<< $METADATA) \
|
||||
sh -c 'go test ${{matrix.race}} -v ./... && cd caddy && go test ${{matrix.race}} -v ./...'
|
||||
env:
|
||||
METADATA: ${{steps.build.outputs.metadata}}
|
||||
|
||||
# Adapted from https://docs.docker.com/build/ci/github-actions/multi-platform/
|
||||
push:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- prepare
|
||||
- build
|
||||
if: fromJson(needs.prepare.outputs.push)
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
variant: ${{ fromJson(needs.prepare.outputs.variants) }}
|
||||
target: ['builder', 'runner']
|
||||
steps:
|
||||
- name: Download metadata
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: metadata-${{matrix.target}}-${{matrix.variant}}
|
||||
path: /tmp/metadata
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
with:
|
||||
version: latest
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{secrets.REGISTRY_USERNAME}}
|
||||
password: ${{secrets.REGISTRY_PASSWORD}}
|
||||
|
||||
- name: Create manifest list and push
|
||||
working-directory: /tmp/metadata
|
||||
run: |
|
||||
docker buildx imagetools create $(jq -cr '.target."${{matrix.target}}-${{matrix.variant}}".tags | map("-t " + .) | join(" ")' <<< $METADATA) \
|
||||
$(printf 'dunglas/frankenphp@sha256:%s ' *)
|
||||
env:
|
||||
METADATA: ${{needs.prepare.outputs.metadata}}
|
||||
|
||||
- name: Inspect image
|
||||
run: |
|
||||
docker buildx imagetools inspect $(jq -cr '.target."${{matrix.target}}-${{matrix.variant}}".tags | first' <<< $METADATA)
|
||||
env:
|
||||
METADATA: ${{needs.prepare.outputs.metadata}}
|
||||
|
||||
43
.github/workflows/lint.yaml
vendored
Normal file
43
.github/workflows/lint.yaml
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
---
|
||||
name: Lint Code Base
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Lint Code Base
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: read
|
||||
statuses: write
|
||||
|
||||
steps:
|
||||
-
|
||||
name: Checkout Code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
-
|
||||
name: Lint Code Base
|
||||
uses: super-linter/super-linter@v5
|
||||
env:
|
||||
VALIDATE_ALL_CODEBASE: true
|
||||
DEFAULT_BRANCH: main
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
LINTER_RULES_PATH: /
|
||||
FILTER_REGEX_EXCLUDE: '.*C-Thread-Pool/.*'
|
||||
MARKDOWN_CONFIG_FILE: .markdown-lint.yaml
|
||||
VALIDATE_CPP: false
|
||||
VALIDATE_JSCPD: false
|
||||
VALIDATE_GO: false
|
||||
VALIDATE_PHP_PHPCS: false
|
||||
VALIDATE_PHP_PHPSTAN: false
|
||||
VALIDATE_PHP_PSALM: false
|
||||
VALIDATE_TERRAGRUNT: false
|
||||
258
.github/workflows/static.yaml
vendored
Normal file
258
.github/workflows/static.yaml
vendored
Normal file
@@ -0,0 +1,258 @@
|
||||
---
|
||||
name: Build binary releases
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- v*.*.*
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'FrankenPHP version'
|
||||
required: false
|
||||
type: string
|
||||
schedule:
|
||||
- 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
|
||||
outputs:
|
||||
push: ${{ toJson((steps.check.outputs.ref || (github.event_name == 'workflow_dispatch' && inputs.version) || startsWith(github.ref, 'refs/tags/') || (github.ref == 'refs/heads/main' && github.event_name != 'pull_request')) && true || false) }}
|
||||
platforms: ${{ steps.matrix.outputs.platforms }}
|
||||
metadata: ${{ steps.matrix.outputs.metadata }}
|
||||
ref: ${{ steps.check.outputs.ref }}
|
||||
steps:
|
||||
-
|
||||
name: Get version
|
||||
id: check
|
||||
if: github.event_name == 'schedule'
|
||||
run: |
|
||||
ref="${{ (github.ref_type == 'tag' && github.ref_name) || (github.event_name == 'workflow_dispatch' && inputs.version) || '' }}"
|
||||
if [[ -z "${ref}" ]]; then
|
||||
ref="$(gh release view --repo dunglas/frankenphp --json tagName --jq '.tagName')"
|
||||
fi
|
||||
|
||||
echo "ref=${ref}" >> "${GITHUB_OUTPUT}"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
-
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ steps.check.outputs.ref }}
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
version: latest
|
||||
-
|
||||
name: Create platforms matrix
|
||||
id: matrix
|
||||
run: |
|
||||
METADATA="$(docker buildx bake --print static-builder | jq -c)"
|
||||
{
|
||||
echo metadata="${METADATA}"
|
||||
echo platforms="$(jq -c 'first(.target[]) | .platforms' <<< "${METADATA}")"
|
||||
} >> "${GITHUB_OUTPUT}"
|
||||
env:
|
||||
SHA: ${{ github.sha }}
|
||||
VERSION: ${{ steps.check.outputs.ref || github.sha }}
|
||||
build-linux:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform: ${{ fromJson(needs.prepare.outputs.platforms) }}
|
||||
include:
|
||||
- race: ""
|
||||
qemu: true
|
||||
- platform: linux/amd64
|
||||
qemu: false
|
||||
name: Build ${{ matrix.platform }} static binary
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ prepare ]
|
||||
steps:
|
||||
-
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ needs.prepare.outputs.ref }}
|
||||
-
|
||||
name: Set up QEMU
|
||||
if: matrix.qemu
|
||||
uses: docker/setup-qemu-action@v3
|
||||
with:
|
||||
platforms: ${{ matrix.platform }}
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
platforms: ${{ matrix.platform }}
|
||||
version: latest
|
||||
-
|
||||
name: Login to DockerHub
|
||||
if: ${{ fromJson(needs.prepare.outputs.push) }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
-
|
||||
name: Build
|
||||
id: build
|
||||
uses: docker/bake-action@v4
|
||||
with:
|
||||
pull: true
|
||||
load: ${{ !fromJson(needs.prepare.outputs.push) }}
|
||||
targets: static-builder
|
||||
set: |
|
||||
*.tags=
|
||||
*.platform=${{ matrix.platform }}
|
||||
*.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' || '' }}
|
||||
env:
|
||||
SHA: ${{ github.sha }}
|
||||
VERSION: ${{ (github.ref_type == 'tag' && github.ref_name) || needs.prepare.outputs.ref || github.sha}}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
-
|
||||
# Workaround for https://github.com/actions/runner/pull/2477#issuecomment-1501003600
|
||||
name: Export metadata
|
||||
if: fromJson(needs.prepare.outputs.push)
|
||||
run: |
|
||||
mkdir -p /tmp/metadata
|
||||
|
||||
# shellcheck disable=SC2086
|
||||
digest=$(jq -r '."static-builder"."containerimage.digest"' <<< ${METADATA})
|
||||
touch "/tmp/metadata/${digest#sha256:}"
|
||||
env:
|
||||
METADATA: ${{ steps.build.outputs.metadata }}
|
||||
-
|
||||
name: Upload metadata
|
||||
if: fromJson(needs.prepare.outputs.push)
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: metadata-static-builder
|
||||
path: /tmp/metadata/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
-
|
||||
name: Copy binary
|
||||
if: ${{ !fromJson(needs.prepare.outputs.push) }}
|
||||
run: |
|
||||
digest=$(jq -r '."static-builder"."containerimage.config.digest"' <<< "${METADATA}")
|
||||
docker create --platform=${{ matrix.platform }} --name static-builder "${digest}"
|
||||
docker cp "static-builder:/go/src/app/dist/${BINARY}" "${BINARY}"
|
||||
env:
|
||||
METADATA: ${{ steps.build.outputs.metadata }}
|
||||
BINARY: frankenphp-linux-${{ matrix.platform == 'linux/amd64' && 'x86_64' || 'aarch64' }}
|
||||
-
|
||||
name: Upload artifact
|
||||
if: ${{ !fromJson(needs.prepare.outputs.push) }}
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: frankenphp-linux-${{ matrix.platform == 'linux/amd64' && 'x86_64' || 'aarch64' }}
|
||||
path: frankenphp-linux-${{ matrix.platform == 'linux/amd64' && 'x86_64' || 'aarch64' }}
|
||||
# Adapted from https://docs.docker.com/build/ci/github-actions/multi-platform/
|
||||
push:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- prepare
|
||||
- build-linux
|
||||
if: fromJson(needs.prepare.outputs.push)
|
||||
#if: fromJson(needs.prepare.outputs.push) && (needs.prepare.outputs.ref || github.ref_type == 'tag')
|
||||
steps:
|
||||
-
|
||||
name: Download metadata
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: metadata-static-builder
|
||||
path: /tmp/metadata
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
version: latest
|
||||
-
|
||||
name: Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
-
|
||||
name: Create manifest list and push
|
||||
working-directory: /tmp/metadata
|
||||
run: |
|
||||
# shellcheck disable=SC2046,SC2086
|
||||
docker buildx imagetools create $(jq -cr '.target."static-builder".tags | map("-t " + .) | join(" ")' <<< "${METADATA}") \
|
||||
$(printf 'dunglas/frankenphp@sha256:%s ' *)
|
||||
env:
|
||||
METADATA: ${{ needs.prepare.outputs.metadata }}
|
||||
-
|
||||
name: Inspect image
|
||||
run: |
|
||||
# shellcheck disable=SC2046,SC2086
|
||||
docker buildx imagetools inspect "$(jq -cr '.target."static-builder".tags | first' <<< "${METADATA}")"
|
||||
env:
|
||||
METADATA: ${{ needs.prepare.outputs.metadata }}
|
||||
-
|
||||
name: Copy binary
|
||||
run: |
|
||||
tag=$(jq -cr '.target."static-builder".tags | first' <<< "${METADATA}")
|
||||
docker cp "$(docker create --platform=linux/amd64 --name static-builder "${tag}"):/go/src/app/dist/frankenphp-linux-x86_64" frankenphp-linux-x86_64 ; docker rm static-builder
|
||||
docker cp "$(docker create --platform=linux/arm64 --name static-builder "${tag}"):/go/src/app/dist/frankenphp-linux-aarch64" frankenphp-linux-aarch64 ; docker rm static-builder
|
||||
env:
|
||||
METADATA: ${{ needs.prepare.outputs.metadata }}
|
||||
-
|
||||
name: Upload asset
|
||||
if: needs.prepare.outputs.ref || github.ref_type == 'tag'
|
||||
run: gh release upload "${{ (github.ref_type == 'tag' && github.ref_name) || needs.prepare.outputs.ref }}" frankenphp-linux-x86_64 frankenphp-linux-aarch64 --repo dunglas/frankenphp --clobber
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
build-mac:
|
||||
name: Build macOS x86_64 binaries
|
||||
runs-on: macos-latest
|
||||
needs: [ prepare ]
|
||||
env:
|
||||
HOMEBREW_NO_AUTO_UPDATE: 1
|
||||
steps:
|
||||
-
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ needs.prepare.outputs.ref }}
|
||||
-
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.21'
|
||||
cache-dependency-path: |
|
||||
go.sum
|
||||
caddy/go.sum
|
||||
-
|
||||
name: Set FRANKENPHP_VERSION
|
||||
run: |
|
||||
if [ "${GITHUB_REF_TYPE}" == "tag" ]; then
|
||||
export FRANKENPHP_VERSION=${GITHUB_REF_NAME:1}
|
||||
elif [ "${GITHUB_EVENT_NAME}" == "schedule" ]; then
|
||||
export FRANKENPHP_VERSION="${{ needs.prepare.outputs.ref }}"
|
||||
else
|
||||
export FRANKENPHP_VERSION=${GITHUB_SHA}
|
||||
fi
|
||||
|
||||
echo "FRANKENPHP_VERSION=${FRANKENPHP_VERSION}" >> "${GITHUB_ENV}"
|
||||
-
|
||||
name: Build FrankenPHP
|
||||
run: ./build-static.sh
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
RELEASE: ${{ (needs.prepare.outputs.ref || github.ref_type == 'tag') && '1' || '' }}
|
||||
-
|
||||
name: Upload artifact
|
||||
if: github.ref_type == 'branch'
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: frankenphp-mac-x86_64
|
||||
path: dist/frankenphp-mac-x86_64
|
||||
137
.github/workflows/static.yml
vendored
137
.github/workflows/static.yml
vendored
@@ -1,137 +0,0 @@
|
||||
name: Build binary releases
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- v*.*.*
|
||||
workflow_dispatch:
|
||||
inputs: {}
|
||||
jobs:
|
||||
release:
|
||||
name: Build Linux x86_64 binary
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Create release
|
||||
if: github.ref_type == 'tag'
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
generateReleaseNotes: true
|
||||
allowUpdates: true
|
||||
omitBodyDuringUpdate: true
|
||||
omitNameDuringUpdate: true
|
||||
build-linux:
|
||||
name: Build Linux x86_64 binary
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
with:
|
||||
version: latest
|
||||
|
||||
- name: Build
|
||||
id: build
|
||||
uses: docker/bake-action@v3
|
||||
with:
|
||||
pull: true
|
||||
load: true
|
||||
targets: static-builder
|
||||
set: |
|
||||
*.cache-from=type=gha,scope=${{github.ref}}-static-builder
|
||||
*.cache-from=type=gha,scope=refs/heads/main-static-builder
|
||||
*.cache-to=type=gha,scope=${{github.ref}}-static-builder
|
||||
env:
|
||||
VERSION: ${{github.ref_type == 'tag' && github.ref_name || github.sha}}
|
||||
SHA: ${{github.sha}}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Copy binary
|
||||
run: docker cp $(docker create --name static-builder dunglas/frankenphp:static-builder):/go/src/app/caddy/frankenphp/frankenphp frankenphp-linux-x86_64 ; docker rm static-builder
|
||||
|
||||
- name: Upload asset
|
||||
if: github.ref_type == 'tag'
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
generateReleaseNotes: true
|
||||
allowUpdates: true
|
||||
omitBodyDuringUpdate: true
|
||||
omitNameDuringUpdate: true
|
||||
artifacts: frankenphp-linux-x86_64
|
||||
|
||||
- name: Upload artifact
|
||||
if: github.ref_type == 'branch'
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: frankenphp-linux-x86_64
|
||||
|
||||
build-mac:
|
||||
name: Build macOS x86_64 binaries
|
||||
runs-on: macos-latest
|
||||
env:
|
||||
HOMEBREW_NO_AUTO_UPDATE: 1
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
repository: crazywhalecc/static-php-cli
|
||||
path: static-php-cli
|
||||
|
||||
- name: Install missing system dependencies
|
||||
run: brew install automake
|
||||
|
||||
- uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: '1.21'
|
||||
cache-dependency-path: |
|
||||
go.sum
|
||||
caddy/go.sum
|
||||
|
||||
- name: Install static-php-cli dependencies
|
||||
working-directory: static-php-cli/
|
||||
run: composer install --no-dev -a
|
||||
|
||||
- name: Fetch libraries sources
|
||||
working-directory: static-php-cli/
|
||||
run: ./bin/spc fetch --with-php=8.2 -A
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build libphp.a
|
||||
working-directory: static-php-cli/
|
||||
run: ./bin/spc build --enable-zts --build-embed "bcmath,calendar,ctype,curl,dba,dom,exif,filter,fileinfo,gd,iconv,intl,mbstring,mbregex,mysqli,mysqlnd,opcache,openssl,pcntl,pdo,pdo_mysql,pdo_pgsql,pdo_sqlite,pgsql,phar,posix,readline,redis,session,simplexml,sockets,sqlite3,tokenizer,xml,xmlreader,xmlwriter,zip,zlib,apcu"
|
||||
|
||||
- name: Set CGO flags
|
||||
working-directory: static-php-cli/
|
||||
run: |
|
||||
if [ "$GITHUB_REF_TYPE" == "tag" ]; then export FRANKENPHP_VERSION=${GITHUB_REF_NAME:1}; else export FRANKENPHP_VERSION=$GITHUB_SHA; fi
|
||||
echo "CGO_CFLAGS=-DFRANKENPHP_VERSION=$FRANKENPHP_VERSION $(./buildroot/bin/php-config --includes | sed s#-I/#-I$PWD/buildroot/#g)" >> "$GITHUB_ENV"
|
||||
echo "CGO_LDFLAGS=-framework CoreFoundation -framework SystemConfiguration $(./buildroot/bin/php-config --ldflags) $(./buildroot/bin/php-config --libs)" >> "$GITHUB_ENV"
|
||||
echo "PHP_VERSION=$(./buildroot/bin/php-config --version)" >> "$GITHUB_ENV"
|
||||
echo "FRANKENPHP_VERSION=$FRANKENPHP_VERSION" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Build FrankenPHP
|
||||
working-directory: caddy/frankenphp/
|
||||
run: |
|
||||
go build -buildmode=pie -tags "cgo netgo osusergo static_build" -ldflags "-linkmode=external -extldflags -static-pie -w -s -X 'github.com/caddyserver/caddy/v2.CustomVersion=FrankenPHP $FRANKENPHP_VERSION PHP $PHP_VERSION Caddy'" -o frankenphp-mac-x86_64
|
||||
./frankenphp-mac-x86_64 version
|
||||
|
||||
- name: Upload asset
|
||||
if: github.ref_type == 'tag'
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
generateReleaseNotes: true
|
||||
allowUpdates: true
|
||||
omitBodyDuringUpdate: true
|
||||
omitNameDuringUpdate: true
|
||||
artifacts: caddy/frankenphp/frankenphp-mac-x86_64
|
||||
|
||||
- name: Upload binary
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: caddy/frankenphp/frankenphp-mac-x86_64
|
||||
71
.github/workflows/tests.yaml
vendored
Normal file
71
.github/workflows/tests.yaml
vendored
Normal file
@@ -0,0 +1,71 @@
|
||||
---
|
||||
name: Tests
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
jobs:
|
||||
tests:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
php-versions: ['8.2', '8.3']
|
||||
env:
|
||||
GOEXPERIMENT: cgocheck2
|
||||
steps:
|
||||
-
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.21'
|
||||
cache-dependency-path: |
|
||||
go.sum
|
||||
caddy/go.sum
|
||||
-
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php-versions }}
|
||||
ini-file: development
|
||||
coverage: none
|
||||
tools: none
|
||||
env:
|
||||
phpts: ts
|
||||
-
|
||||
name: Set CGO flags
|
||||
run: |
|
||||
echo "CGO_CFLAGS=$(php-config --includes)" >> "$GITHUB_ENV"
|
||||
-
|
||||
name: Build
|
||||
run: go build
|
||||
-
|
||||
name: Build testcli binary
|
||||
working-directory: internal/testcli/
|
||||
run: go build
|
||||
-
|
||||
name: Run library tests
|
||||
run: go test -race -v ./...
|
||||
-
|
||||
name: Run Caddy module tests
|
||||
working-directory: caddy/
|
||||
run: go test -race -v ./...
|
||||
-
|
||||
name: Build the server
|
||||
working-directory: caddy/frankenphp/
|
||||
run: go build
|
||||
-
|
||||
name: Start the server
|
||||
working-directory: testdata/
|
||||
run: sudo ../caddy/frankenphp/frankenphp start
|
||||
-
|
||||
name: Run integrations tests
|
||||
run: ./reload_test.sh
|
||||
-
|
||||
name: Lint Go code
|
||||
uses: golangci/golangci-lint-action@v3
|
||||
with:
|
||||
version: latest
|
||||
41
.github/workflows/tests.yml
vendored
41
.github/workflows/tests.yml
vendored
@@ -1,41 +0,0 @@
|
||||
name: Tests
|
||||
on: [push, pull_request]
|
||||
jobs:
|
||||
tests:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
php-versions: ['8.2', '8.3']
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: '1.21'
|
||||
cache-dependency-path: |
|
||||
go.sum
|
||||
caddy/go.sum
|
||||
|
||||
- uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php-versions }}
|
||||
ini-file: development
|
||||
extensions: opcache
|
||||
coverage: none
|
||||
env:
|
||||
phpts: ts
|
||||
|
||||
- name: Set CGO flags
|
||||
run: echo "CGO_CFLAGS=$(php-config --includes)" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Build
|
||||
run: go build
|
||||
env:
|
||||
GOEXPERIMENT: cgocheck2
|
||||
|
||||
- name: Run library tests
|
||||
run: go test -race -v ./...
|
||||
|
||||
- name: Run Caddy module tests
|
||||
working-directory: caddy/
|
||||
run: go test -race -v ./...
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,5 +1,7 @@
|
||||
/caddy/frankenphp/frankenphp
|
||||
/internal/testserver/testserver
|
||||
/internal/testcli/testcli
|
||||
/dist
|
||||
.idea/
|
||||
.vscode/
|
||||
__debug_bin
|
||||
|
||||
6
.hadolint.yaml
Normal file
6
.hadolint.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
ignored:
|
||||
- DL3006
|
||||
- DL3008
|
||||
- DL3018
|
||||
- DL3022
|
||||
4
.markdown-lint.yaml
Normal file
4
.markdown-lint.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
no-hard-tabs: false
|
||||
MD013: false
|
||||
MD033: false
|
||||
@@ -1,4 +1,5 @@
|
||||
[](https://circleci.com/gh/Pithikos/C-Thread-Pool)
|
||||
[](https://github.com/Pithikos/C-Thread-Pool/actions?query=workflow%3Atests+branch%3Amaster)
|
||||
|
||||
|
||||
# C Thread Pool
|
||||
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
/*
|
||||
* WHAT THIS EXAMPLE DOES
|
||||
*
|
||||
* We create a pool of 4 threads and then add 40 tasks to the pool(20 task1
|
||||
* functions and 20 task2 functions). task1 and task2 simply print which thread is running them.
|
||||
*
|
||||
* As soon as we add the tasks to the pool, the threads will run them. It can happen that
|
||||
* you see a single thread running all the tasks (highly unlikely). It is up the OS to
|
||||
* decide which thread will run what. So it is not an error of the thread pool but rather
|
||||
* a decision of the OS.
|
||||
*
|
||||
* */
|
||||
|
||||
#include <stdio.h>
|
||||
#include <pthread.h>
|
||||
#include <stdint.h>
|
||||
#include "thpool.h"
|
||||
|
||||
void task(void *arg){
|
||||
printf("Thread #%u working on %d\n", (int)pthread_self(), (int) arg);
|
||||
}
|
||||
|
||||
|
||||
int main(){
|
||||
|
||||
puts("Making threadpool with 4 threads");
|
||||
threadpool thpool = thpool_init(4);
|
||||
|
||||
puts("Adding 40 tasks to threadpool");
|
||||
int i;
|
||||
for (i=0; i<40; i++){
|
||||
thpool_add_work(thpool, task, (void*)(uintptr_t)i);
|
||||
};
|
||||
|
||||
thpool_wait(thpool);
|
||||
puts("Killing threadpool");
|
||||
thpool_destroy(thpool);
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -14,6 +14,9 @@
|
||||
#ifndef _POSIX_C_SOURCE
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
#endif
|
||||
#ifndef _XOPEN_SOURCE
|
||||
#define _XOPEN_SOURCE 500
|
||||
#endif
|
||||
#endif
|
||||
#include <unistd.h>
|
||||
#include <signal.h>
|
||||
@@ -25,6 +28,9 @@
|
||||
#if defined(__linux__)
|
||||
#include <sys/prctl.h>
|
||||
#endif
|
||||
#if defined(__FreeBSD__) || defined(__OpenBSD__)
|
||||
#include <pthread_np.h>
|
||||
#endif
|
||||
|
||||
#include "thpool.h"
|
||||
|
||||
@@ -40,6 +46,13 @@
|
||||
#define err(str)
|
||||
#endif
|
||||
|
||||
#ifndef THPOOL_THREAD_NAME
|
||||
#define THPOOL_THREAD_NAME thpool
|
||||
#endif
|
||||
|
||||
#define STRINGIFY(x) #x
|
||||
#define TOSTRING(x) STRINGIFY(x)
|
||||
|
||||
static volatile int threads_keepalive;
|
||||
static volatile int threads_on_hold;
|
||||
|
||||
@@ -211,7 +224,7 @@ void thpool_wait(thpool_* thpool_p){
|
||||
|
||||
/* Destroy the threadpool */
|
||||
void thpool_destroy(thpool_* thpool_p){
|
||||
/* No need to destory if it's NULL */
|
||||
/* No need to destroy if it's NULL */
|
||||
if (thpool_p == NULL) return ;
|
||||
|
||||
volatile int threads_total = thpool_p->num_threads_alive;
|
||||
@@ -260,7 +273,7 @@ void thpool_pause(thpool_* thpool_p) {
|
||||
/* Resume all threads in threadpool */
|
||||
void thpool_resume(thpool_* thpool_p) {
|
||||
// resuming a single threadpool hasn't been
|
||||
// implemented yet, meanwhile this supresses
|
||||
// implemented yet, meanwhile this suppresses
|
||||
// the warnings
|
||||
(void)thpool_p;
|
||||
|
||||
@@ -314,7 +327,7 @@ static void thread_hold(int sig_id) {
|
||||
|
||||
/* What each thread is doing
|
||||
*
|
||||
* In principle this is an endless loop. The only time this loop gets interuppted is once
|
||||
* In principle this is an endless loop. The only time this loop gets interrupted is once
|
||||
* thpool_destroy() is invoked or the program exits.
|
||||
*
|
||||
* @param thread thread that will run this function
|
||||
@@ -322,15 +335,18 @@ static void thread_hold(int sig_id) {
|
||||
*/
|
||||
static void* thread_do(struct thread* thread_p){
|
||||
|
||||
/* Set thread name for profiling and debuging */
|
||||
/* Set thread name for profiling and debugging */
|
||||
char thread_name[16] = {0};
|
||||
snprintf(thread_name, 16, "thpool-%d", thread_p->id);
|
||||
|
||||
snprintf(thread_name, 16, TOSTRING(THPOOL_THREAD_NAME) "-%d", thread_p->id);
|
||||
|
||||
#if defined(__linux__)
|
||||
/* Use prctl instead to prevent using _GNU_SOURCE flag and implicit declaration */
|
||||
prctl(PR_SET_NAME, thread_name);
|
||||
#elif defined(__APPLE__) && defined(__MACH__)
|
||||
pthread_setname_np(thread_name);
|
||||
#elif defined(__FreeBSD__) || defined(__OpenBSD__)
|
||||
pthread_set_name_np(thread_p->pthread, thread_name);
|
||||
#else
|
||||
err("thread_do(): pthread_setname_np is not supported on this system");
|
||||
#endif
|
||||
@@ -520,6 +536,8 @@ static void bsem_init(bsem *bsem_p, int value) {
|
||||
|
||||
/* Reset semaphore to 0 */
|
||||
static void bsem_reset(bsem *bsem_p) {
|
||||
pthread_mutex_destroy(&(bsem_p->mutex));
|
||||
pthread_cond_destroy(&(bsem_p->cond));
|
||||
bsem_init(bsem_p, 0);
|
||||
}
|
||||
|
||||
|
||||
112
CONTRIBUTING.md
112
CONTRIBUTING.md
@@ -1,80 +1,107 @@
|
||||
# Contributing
|
||||
|
||||
## Compiling PHP
|
||||
|
||||
### With Docker (Linux)
|
||||
|
||||
Build the dev Docker image:
|
||||
|
||||
docker build -t frankenphp-dev -f dev.Dockerfile .
|
||||
docker run --cap-add=SYS_PTRACE --security-opt seccomp=unconfined -p 8080:8080 -p 443:443 -v $PWD:/go/src/app -it frankenphp-dev
|
||||
```console
|
||||
docker build -t frankenphp-dev -f dev.Dockerfile .
|
||||
docker run --cap-add=SYS_PTRACE --security-opt seccomp=unconfined -p 8080:8080 -p 443:443 -p 443:443/udp -v $PWD:/go/src/app -it frankenphp-dev
|
||||
```
|
||||
|
||||
The image contains the usual development tools (Go, GDB, Valgrind, Neovim...).
|
||||
The image contains the usual development tools (Go, GDB, Valgrind, Neovim...).
|
||||
|
||||
If docker version is lower than 23.0, build is failed by dockerignore [pattern issue](https://github.com/moby/moby/pull/42676). Add directories to `.dockerignore`.
|
||||
|
||||
```patch
|
||||
!testdata/*.php
|
||||
!testdata/*.txt
|
||||
+!caddy
|
||||
+!C-Thread-Pool
|
||||
+!internal
|
||||
```
|
||||
|
||||
### Without Docker (Linux and macOS)
|
||||
|
||||
[Follow the instructions to compile from sources](docs/compile.md) and pass the `--debug` configuration flag.
|
||||
[Follow the instructions to compile from sources](https://frankenphp.dev/docs/compile/) and pass the `--debug` configuration flag.
|
||||
|
||||
## Running the test suite
|
||||
|
||||
go test -race -v ./...
|
||||
```console
|
||||
go test -race -v ./...
|
||||
```
|
||||
|
||||
## Caddy module
|
||||
|
||||
Build Caddy with the FrankenPHP Caddy module:
|
||||
|
||||
cd caddy/frankenphp/
|
||||
go build
|
||||
cd ../../
|
||||
```console
|
||||
cd caddy/frankenphp/
|
||||
go build
|
||||
cd ../../
|
||||
```
|
||||
|
||||
Run the Caddy with the FrankenPHP Caddy module:
|
||||
|
||||
cd testdata/
|
||||
../caddy/frankenphp/frankenphp run
|
||||
```console
|
||||
cd testdata/
|
||||
../caddy/frankenphp/frankenphp run
|
||||
```
|
||||
|
||||
The server is listening on `127.0.0.1:8080`:
|
||||
|
||||
curl -vk https://localhost/phpinfo.php
|
||||
```console
|
||||
curl -vk https://localhost/phpinfo.php
|
||||
```
|
||||
|
||||
## Minimal test server
|
||||
|
||||
Build the minimal test server:
|
||||
|
||||
cd internal/testserver/
|
||||
go build
|
||||
cd ../../
|
||||
```console
|
||||
cd internal/testserver/
|
||||
go build
|
||||
cd ../../
|
||||
```
|
||||
|
||||
Run the test server:
|
||||
|
||||
cd testdata/
|
||||
../internal/testserver/testserver
|
||||
```console
|
||||
cd testdata/
|
||||
../internal/testserver/testserver
|
||||
```
|
||||
|
||||
The server is listening on `127.0.0.1:8080`:
|
||||
|
||||
curl -v http://127.0.0.1:8080/phpinfo.php
|
||||
```console
|
||||
curl -v http://127.0.0.1:8080/phpinfo.php
|
||||
```
|
||||
|
||||
# Building Docker Images Locally
|
||||
## Building Docker Images Locally
|
||||
|
||||
Print bake plan:
|
||||
|
||||
```
|
||||
```console
|
||||
docker buildx bake -f docker-bake.hcl --print
|
||||
```
|
||||
|
||||
Build FrankenPHP images for amd64 locally:
|
||||
|
||||
```
|
||||
```console
|
||||
docker buildx bake -f docker-bake.hcl --pull --load --set "*.platform=linux/amd64"
|
||||
```
|
||||
|
||||
Build FrankenPHP images for arm64 locally:
|
||||
|
||||
```
|
||||
```console
|
||||
docker buildx bake -f docker-bake.hcl --pull --load --set "*.platform=linux/arm64"
|
||||
```
|
||||
|
||||
Build FrankenPHP images from scratch for arm64 & amd64 and push to Docker Hub:
|
||||
|
||||
```
|
||||
```console
|
||||
docker buildx bake -f docker-bake.hcl --pull --no-cache --push
|
||||
```
|
||||
|
||||
@@ -82,6 +109,7 @@ docker buildx bake -f docker-bake.hcl --pull --no-cache --push
|
||||
|
||||
1. Open `.github/workflows/tests.yml`
|
||||
2. Enable PHP debug symbols
|
||||
|
||||
```patch
|
||||
- uses: shivammathur/setup-php@v2
|
||||
# ...
|
||||
@@ -89,33 +117,40 @@ docker buildx bake -f docker-bake.hcl --pull --no-cache --push
|
||||
phpts: ts
|
||||
+ debug: true
|
||||
```
|
||||
|
||||
3. Enable `tmate` to connect to the container
|
||||
|
||||
```patch
|
||||
- name: Set include flags
|
||||
-
|
||||
name: Set CGO flags
|
||||
run: echo "CGO_CFLAGS=$(php-config --includes)" >> "$GITHUB_ENV"
|
||||
+ - run: |
|
||||
sudo apt install gdb
|
||||
+ -
|
||||
+ run: |
|
||||
+ sudo apt install gdb
|
||||
+ mkdir -p /home/runner/.config/gdb/
|
||||
+ printf "set auto-load safe-path /\nhandle SIG34 nostop noprint pass" > /home/runner/.config/gdb/gdbinit
|
||||
+ - uses: mxschmitt/action-tmate@v3
|
||||
+ env:
|
||||
+ GOFLAGS: "-w -gcflags=all=-N -gcflags=all=-l"
|
||||
+ -
|
||||
+ uses: mxschmitt/action-tmate@v3
|
||||
```
|
||||
4. Open `frankenphp.go`
|
||||
5. Enable `cgosymbolizer`
|
||||
|
||||
4. Connect to the container
|
||||
5. Open `frankenphp.go`
|
||||
6. Enable `cgosymbolizer`
|
||||
|
||||
```patch
|
||||
- //_ "github.com/ianlancetaylor/cgosymbolizer"
|
||||
+ _ "github.com/ianlancetaylor/cgosymbolizer"
|
||||
```
|
||||
6. Download the module: `go get`
|
||||
7. In the container, you can use GDB and the like:
|
||||
```sh
|
||||
sudo apt install gdb
|
||||
mkdir -p /home/runner/.config/gdb/
|
||||
|
||||
7. Download the module: `go get`
|
||||
8. In the container, you can use GDB and the like:
|
||||
|
||||
```console
|
||||
go test -c -ldflags=-w
|
||||
gdb --args ./frankenphp.test -test.run ^MyTest$
|
||||
```
|
||||
8. When the bug is fixed, revert all these changes
|
||||
|
||||
9. When the bug is fixed, revert all these changes
|
||||
|
||||
## Misc Dev Resources
|
||||
|
||||
@@ -134,10 +169,9 @@ docker buildx bake -f docker-bake.hcl --pull --no-cache --push
|
||||
* [Bake file definition](https://docs.docker.com/build/customize/bake/file-definition/)
|
||||
* [docker buildx build](https://docs.docker.com/engine/reference/commandline/buildx_build/)
|
||||
|
||||
|
||||
## Useful Command
|
||||
|
||||
```
|
||||
```console
|
||||
apk add strace util-linux gdb
|
||||
strace -e 'trace=!futex,epoll_ctl,epoll_pwait,tgkill,rt_sigreturn' -p 1
|
||||
```
|
||||
```
|
||||
|
||||
61
Dockerfile
61
Dockerfile
@@ -4,12 +4,12 @@ FROM php-base AS common
|
||||
WORKDIR /app
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get -y --no-install-recommends install \
|
||||
mailcap \
|
||||
libcap2-bin \
|
||||
&& \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
apt-get -y --no-install-recommends install \
|
||||
mailcap \
|
||||
libcap2-bin \
|
||||
&& \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN set -eux; \
|
||||
mkdir -p \
|
||||
@@ -24,7 +24,7 @@ COPY --link caddy/frankenphp/Caddyfile /etc/caddy/Caddyfile
|
||||
COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/local/bin/
|
||||
|
||||
CMD ["--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"]
|
||||
HEALTHCHECK CMD curl -f https://localhost/healthz || exit 1
|
||||
HEALTHCHECK CMD curl -f http://localhost:2019/metrics || exit 1
|
||||
|
||||
# See https://caddyserver.com/docs/conventions#file-locations for details
|
||||
ENV XDG_CONFIG_HOME /config
|
||||
@@ -46,6 +46,7 @@ LABEL org.opencontainers.image.vendor="Kévin Dunglas"
|
||||
FROM common AS builder
|
||||
|
||||
ARG FRANKENPHP_VERSION='dev'
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
|
||||
COPY --from=golang-base /usr/local/go /usr/local/go
|
||||
|
||||
@@ -53,30 +54,30 @@ ENV PATH /usr/local/go/bin:$PATH
|
||||
|
||||
# This is required to link the FrankenPHP binary to the PHP binary
|
||||
RUN apt-get update && \
|
||||
apt-get -y --no-install-recommends install \
|
||||
libargon2-dev \
|
||||
libcurl4-openssl-dev \
|
||||
libonig-dev \
|
||||
libreadline-dev \
|
||||
libsodium-dev \
|
||||
libsqlite3-dev \
|
||||
libssl-dev \
|
||||
libxml2-dev \
|
||||
zlib1g-dev \
|
||||
&& \
|
||||
apt-get clean
|
||||
apt-get -y --no-install-recommends install \
|
||||
libargon2-dev \
|
||||
libbrotli-dev \
|
||||
libcurl4-openssl-dev \
|
||||
libonig-dev \
|
||||
libreadline-dev \
|
||||
libsodium-dev \
|
||||
libsqlite3-dev \
|
||||
libssl-dev \
|
||||
libxml2-dev \
|
||||
zlib1g-dev \
|
||||
&& \
|
||||
apt-get clean
|
||||
|
||||
WORKDIR /go/src/app
|
||||
|
||||
COPY --link go.mod go.sum ./
|
||||
RUN go mod graph | awk '{if ($1 !~ "@") print $2}' | xargs go get
|
||||
|
||||
RUN mkdir caddy && cd caddy
|
||||
COPY --link caddy/go.mod caddy/go.sum ./caddy/
|
||||
|
||||
RUN cd caddy && \
|
||||
go mod graph | awk '{if ($1 !~ "@") print $2}' | xargs go get
|
||||
WORKDIR /go/src/app/caddy
|
||||
COPY --link caddy/go.mod caddy/go.sum ./
|
||||
RUN go mod graph | awk '{if ($1 !~ "@") print $2}' | xargs go get
|
||||
|
||||
WORKDIR /go/src/app
|
||||
COPY --link *.* ./
|
||||
COPY --link caddy caddy
|
||||
COPY --link C-Thread-Pool C-Thread-Pool
|
||||
@@ -87,11 +88,13 @@ COPY --link testdata testdata
|
||||
# see https://github.com/docker-library/php/blob/master/8.2/bookworm/zts/Dockerfile#L57-L59 for PHP values
|
||||
ENV CGO_LDFLAGS="-lssl -lcrypto -lreadline -largon2 -lcurl -lonig -lz $PHP_LDFLAGS" CGO_CFLAGS="-DFRANKENPHP_VERSION=$FRANKENPHP_VERSION $PHP_CFLAGS" CGO_CPPFLAGS=$PHP_CPPFLAGS
|
||||
|
||||
RUN cd caddy/frankenphp && \
|
||||
GOBIN=/usr/local/bin go install -ldflags "-X 'github.com/caddyserver/caddy/v2.CustomVersion=FrankenPHP $FRANKENPHP_VERSION PHP $PHP_VERSION Caddy'" && \
|
||||
setcap cap_net_bind_service=+ep /usr/local/bin/frankenphp && \
|
||||
cp Caddyfile /etc/caddy/Caddyfile && \
|
||||
frankenphp version
|
||||
WORKDIR /go/src/app/caddy/frankenphp
|
||||
RUN GOBIN=/usr/local/bin go install -ldflags "-w -s -X 'github.com/caddyserver/caddy/v2.CustomVersion=FrankenPHP $FRANKENPHP_VERSION PHP $PHP_VERSION Caddy'" && \
|
||||
setcap cap_net_bind_service=+ep /usr/local/bin/frankenphp && \
|
||||
cp Caddyfile /etc/caddy/Caddyfile && \
|
||||
frankenphp version
|
||||
|
||||
WORKDIR /go/src/app
|
||||
|
||||
|
||||
FROM common AS runner
|
||||
|
||||
58
README.md
58
README.md
@@ -4,9 +4,9 @@
|
||||
|
||||
FrankenPHP is a modern application server for PHP built on top of the [Caddy](https://caddyserver.com/) web server.
|
||||
|
||||
FrankenPHP gives superpowers to your PHP apps thanks to its stunning features: [*Early Hints*](docs/early-hints.md), [worker mode](docs/worker.md), [real-time capabilities](docs/mercure.md), automatic HTTPS, HTTP/2, and HTTP/3 support...
|
||||
FrankenPHP gives superpowers to your PHP apps thanks to its stunning features: [*Early Hints*](https://frankenphp.dev/docs/early-hints/), [worker mode](https://frankenphp.dev/docs/worker/), [real-time capabilities](https://frankenphp.dev/docs/mercure/), automatic HTTPS, HTTP/2, and HTTP/3 support...
|
||||
|
||||
FrankenPHP works with any PHP app and makes your Symfony projects faster than ever thanks to provided integration with the worker mode (Laravel Octane support coming).
|
||||
FrankenPHP works with any PHP app and makes your Symfony and Laravel projects faster than ever thanks to the provided integration with the worker mode.
|
||||
|
||||
FrankenPHP can also be used as a standalone Go library to embed PHP in any app using `net/http`.
|
||||
|
||||
@@ -16,35 +16,61 @@ FrankenPHP can also be used as a standalone Go library to embed PHP in any app u
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Docker
|
||||
|
||||
```console
|
||||
docker run -v $PWD:/app/public \
|
||||
-p 80:80 -p 443:443 \
|
||||
-p 80:80 -p 443:443 -p 443:443/udp \
|
||||
dunglas/frankenphp
|
||||
```
|
||||
|
||||
Go to `https://localhost`, and enjoy!
|
||||
|
||||
If you prefer not to use Docker, we provide standalone FrankenPHP binaries for Linux and macOS
|
||||
containing [PHP 8.2](https://www.php.net/releases/8.2/en.php) and most popular PHP extensions: [Download FrankenPHP](https://github.com/dunglas/frankenphp/releases)
|
||||
> [!TIP]
|
||||
>
|
||||
> Do not attempt to use `https://127.0.0.1`. Use `localhost` and accept the self-signed certificate.
|
||||
> Use the [`SERVER_NAME` environment variable](docs/config.md#environment-variables) to change the domain to use.
|
||||
|
||||
> Note: do not attempt to use `https://127.0.0.1`. Use `localhost` and accept the self-signed certificate. Caddy has an automatic TLS handling that auto-trusts some local-based hostnames like `localhost`, but it does not apply to IP addresses. More details [on Caddy's "automatic https" docs](https://caddyserver.com/docs/automatic-https#hostname-requirements).
|
||||
### Standalone Binary
|
||||
|
||||
If you prefer not to use Docker, we provide standalone FrankenPHP binaries for Linux and macOS
|
||||
containing [PHP 8.3](https://www.php.net/releases/8.3/en.php) and most popular PHP extensions: [Download FrankenPHP](https://github.com/dunglas/frankenphp/releases)
|
||||
|
||||
To serve the content of the current directory, run:
|
||||
|
||||
```console
|
||||
./frankenphp php-server
|
||||
```
|
||||
|
||||
You can also run command-line scripts with:
|
||||
|
||||
```console
|
||||
./frankenphp php-cli /path/to/your/script.php
|
||||
```
|
||||
|
||||
## Docs
|
||||
|
||||
* [The worker mode](docs/worker.md)
|
||||
* [Early Hints support (103 HTTP status code)](docs/early-hints.md)
|
||||
* [Real-time](docs/mercure.md)
|
||||
* [Configuration](docs/config.md)
|
||||
* [Docker images](docs/docker.md)
|
||||
* [Compile from sources](docs/compile.md)
|
||||
* [Create static binaries](docs/static.md)
|
||||
* [The worker mode](https://frankenphp.dev/docs/worker/)
|
||||
* [Early Hints support (103 HTTP status code)](https://frankenphp.dev/docs/early-hints/)
|
||||
* [Real-time](https://frankenphp.dev/docs/mercure/)
|
||||
* [Configuration](https://frankenphp.dev/docs/config/)
|
||||
* [Docker images](https://frankenphp.dev/docs/docker/)
|
||||
* [Deploy in production](docs/production.md)
|
||||
* [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/)
|
||||
* [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)
|
||||
* [Contributing and debugging](CONTRIBUTING.md)
|
||||
* [Contributing and debugging](https://frankenphp.dev/docs/contributing/)
|
||||
|
||||
## Examples and Skeletons
|
||||
|
||||
* [Symfony apps](https://github.com/dunglas/frankenphp-demo)
|
||||
* [Symfony](https://github.com/dunglas/symfony-docker)
|
||||
* [API Platform](https://api-platform.com/docs/distribution/)
|
||||
* [Laravel](https://frankenphp.dev/docs/laravel/)
|
||||
* [Sulu](https://sulu.io/blog/running-sulu-with-frankenphp)
|
||||
* [WordPress](https://github.com/dunglas/frankenphp-wordpress)
|
||||
* [Drupal](https://github.com/dunglas/frankenphp-drupal)
|
||||
* [Sulu](https://sulu.io/blog/running-sulu-with-frankenphp)
|
||||
* [Joomla](https://github.com/alexandreelise/frankenphp-joomla)
|
||||
* [TYPO3](https://github.com/ochorocho/franken-typo3)
|
||||
|
||||
@@ -21,7 +21,7 @@ COPY --link caddy/frankenphp/Caddyfile /etc/caddy/Caddyfile
|
||||
COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/local/bin/
|
||||
|
||||
CMD ["--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"]
|
||||
HEALTHCHECK CMD curl -f https://localhost/healthz || exit 1
|
||||
HEALTHCHECK CMD curl -f http://localhost:2019/metrics || exit 1
|
||||
|
||||
# See https://caddyserver.com/docs/conventions#file-locations for details
|
||||
ENV XDG_CONFIG_HOME /config
|
||||
@@ -43,14 +43,17 @@ LABEL org.opencontainers.image.vendor="Kévin Dunglas"
|
||||
FROM common AS builder
|
||||
|
||||
ARG FRANKENPHP_VERSION='dev'
|
||||
SHELL ["/bin/ash", "-eo", "pipefail", "-c"]
|
||||
|
||||
COPY --link --from=golang-base /usr/local/go /usr/local/go
|
||||
|
||||
ENV PATH /usr/local/go/bin:$PATH
|
||||
|
||||
# hadolint ignore=SC2086
|
||||
RUN apk add --no-cache --virtual .build-deps \
|
||||
$PHPIZE_DEPS \
|
||||
argon2-dev \
|
||||
brotli-dev \
|
||||
coreutils \
|
||||
curl-dev \
|
||||
gnu-libiconv-dev \
|
||||
@@ -67,11 +70,11 @@ WORKDIR /go/src/app
|
||||
COPY --link go.mod go.sum ./
|
||||
RUN go mod graph | awk '{if ($1 !~ "@") print $2}' | xargs go get
|
||||
|
||||
RUN mkdir caddy && cd caddy
|
||||
COPY caddy/go.mod caddy/go.sum ./caddy/
|
||||
|
||||
RUN cd caddy && go mod graph | awk '{if ($1 !~ "@") print $2}' | xargs go get
|
||||
WORKDIR /go/src/app/caddy
|
||||
COPY caddy/go.mod caddy/go.sum ./
|
||||
RUN go mod graph | awk '{if ($1 !~ "@") print $2}' | xargs go get
|
||||
|
||||
WORKDIR /go/src/app
|
||||
COPY --link *.* ./
|
||||
COPY --link caddy caddy
|
||||
COPY --link C-Thread-Pool C-Thread-Pool
|
||||
@@ -82,11 +85,13 @@ COPY --link testdata testdata
|
||||
# see https://github.com/docker-library/php/blob/master/8.2/bookworm/zts/Dockerfile#L57-L59 for php values
|
||||
ENV CGO_LDFLAGS="-lssl -lcrypto -lreadline -largon2 -lcurl -lonig -lz $PHP_LDFLAGS" CGO_CFLAGS="-DFRANKENPHP_VERSION=$FRANKENPHP_VERSION $PHP_CFLAGS" CGO_CPPFLAGS=$PHP_CPPFLAGS
|
||||
|
||||
RUN cd caddy/frankenphp && \
|
||||
GOBIN=/usr/local/bin go install -ldflags "-X 'github.com/caddyserver/caddy/v2.CustomVersion=FrankenPHP $FRANKENPHP_VERSION PHP $PHP_VERSION Caddy'" && \
|
||||
WORKDIR /go/src/app/caddy/frankenphp
|
||||
RUN GOBIN=/usr/local/bin go install -ldflags "-w -s -extldflags '-Wl,-z,stack-size=0x80000' -X 'github.com/caddyserver/caddy/v2.CustomVersion=FrankenPHP $FRANKENPHP_VERSION PHP $PHP_VERSION Caddy'" && \
|
||||
setcap cap_net_bind_service=+ep /usr/local/bin/frankenphp && \
|
||||
frankenphp version
|
||||
|
||||
WORKDIR /go/src/app
|
||||
|
||||
|
||||
FROM common AS runner
|
||||
|
||||
|
||||
0
app_checksum.txt
Normal file
0
app_checksum.txt
Normal file
155
build-static.sh
Executable file
155
build-static.sh
Executable file
@@ -0,0 +1,155 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -o errexit
|
||||
|
||||
if ! type "git" > /dev/null; then
|
||||
echo "The \"git\" command must be installed."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
arch="$(uname -m)"
|
||||
os="$(uname -s | tr '[:upper:]' '[:lower:]')"
|
||||
if [ "${os}" = "darwin" ]; then
|
||||
os="mac"
|
||||
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
|
||||
fi
|
||||
|
||||
if [ -z "${PHP_EXTENSION_LIBS}" ]; then
|
||||
export PHP_EXTENSION_LIBS="bzip2,freetype,libavif,libjpeg,liblz4,libwebp,libzip"
|
||||
fi
|
||||
|
||||
if [ -z "${PHP_VERSION}" ]; then
|
||||
export PHP_VERSION="8.3"
|
||||
fi
|
||||
|
||||
if [ -z "${FRANKENPHP_VERSION}" ]; then
|
||||
FRANKENPHP_VERSION="$(git rev-parse --verify HEAD)"
|
||||
export FRANKENPHP_VERSION
|
||||
elif [ -d ".git/" ]; then
|
||||
CURRENT_REF="$(git rev-parse --abbrev-ref HEAD)"
|
||||
export CURRENT_REF
|
||||
|
||||
if echo "${FRANKENPHP_VERSION}" | grep -F -q "."; then
|
||||
# Tag
|
||||
|
||||
# Trim "v" prefix if any
|
||||
FRANKENPHP_VERSION=${FRANKENPHP_VERSION#v}
|
||||
export FRANKENPHP_VERSION
|
||||
|
||||
git checkout "v${FRANKENPHP_VERSION}"
|
||||
else
|
||||
git checkout "${FRANKENPHP_VERSION}"
|
||||
fi
|
||||
fi
|
||||
|
||||
bin="frankenphp-${os}-${arch}"
|
||||
|
||||
if [ -n "${CLEAN}" ]; then
|
||||
rm -Rf dist/
|
||||
go clean -cache
|
||||
fi
|
||||
|
||||
# Build libphp if ncessary
|
||||
if [ -f "dist/static-php-cli/buildroot/lib/libphp.a" ]; then
|
||||
cd dist/static-php-cli
|
||||
else
|
||||
mkdir -p dist/
|
||||
cd dist/
|
||||
|
||||
if [ -d "static-php-cli/" ]; then
|
||||
cd static-php-cli/
|
||||
git pull
|
||||
else
|
||||
git clone --depth 1 https://github.com/crazywhalecc/static-php-cli
|
||||
cd static-php-cli/
|
||||
fi
|
||||
|
||||
if type "brew" > /dev/null; then
|
||||
packages="composer"
|
||||
if ! type "go" > /dev/null; then
|
||||
packages="${packages} go"
|
||||
fi
|
||||
if [ -n "${RELEASE}" ]; then
|
||||
packages="${packages} gh"
|
||||
fi
|
||||
|
||||
# shellcheck disable=SC2086
|
||||
brew install --formula --quiet ${packages}
|
||||
fi
|
||||
|
||||
composer install --no-dev -a
|
||||
|
||||
if [ "${os}" = "linux" ]; then
|
||||
extraOpts="--disable-opcache-jit"
|
||||
fi
|
||||
|
||||
if [ -n "${DEBUG_SYMBOLS}" ]; then
|
||||
extraOpts="${extraOpts} --no-strip"
|
||||
fi
|
||||
|
||||
./bin/spc doctor
|
||||
./bin/spc fetch --with-php="${PHP_VERSION}" --for-extensions="${PHP_EXTENSIONS}"
|
||||
# the Brotli library must always be built as it is required by http://github.com/dunglas/caddy-cbrotli
|
||||
# shellcheck disable=SC2086
|
||||
./bin/spc build --enable-zts --build-embed ${extraOpts} "${PHP_EXTENSIONS}" --with-libs="brotli,${PHP_EXTENSION_LIBS}"
|
||||
fi
|
||||
|
||||
CGO_CFLAGS="-DFRANKENPHP_VERSION=${FRANKENPHP_VERSION} -I${PWD}/buildroot/include/ $(./buildroot/bin/php-config --includes | sed s#-I/#-I"${PWD}"/buildroot/#g)"
|
||||
if [ -n "${DEBUG_SYMBOLS}" ]; then
|
||||
CGO_CFLAGS="-g ${CGO_CFLAGS}"
|
||||
fi
|
||||
export CGO_CFLAGS
|
||||
|
||||
if [ "${os}" = "mac" ]; then
|
||||
export CGO_LDFLAGS="-framework CoreFoundation -framework SystemConfiguration"
|
||||
fi
|
||||
|
||||
CGO_LDFLAGS="${CGO_LDFLAGS} ${PWD}/buildroot/lib/libbrotlicommon.a ${PWD}/buildroot/lib/libbrotlienc.a ${PWD}/buildroot/lib/libbrotlidec.a $(./buildroot/bin/php-config --ldflags) $(./buildroot/bin/php-config --libs)"
|
||||
export CGO_LDFLAGS
|
||||
|
||||
LIBPHP_VERSION="$(./buildroot/bin/php-config --version)"
|
||||
export LIBPHP_VERSION
|
||||
|
||||
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
|
||||
fi
|
||||
|
||||
if [ "${os}" = "linux" ]; then
|
||||
extraExtldflags="-Wl,-z,stack-size=0x80000"
|
||||
fi
|
||||
|
||||
if [ -z "${DEBUG_SYMBOLS}" ]; then
|
||||
extraLdflags="-w -s"
|
||||
fi
|
||||
|
||||
cd caddy/frankenphp/
|
||||
go env
|
||||
go build -buildmode=pie -tags "cgo netgo osusergo static_build" -ldflags "-linkmode=external -extldflags '-static-pie ${extraExtldflags}' ${extraLdflags} -X 'github.com/caddyserver/caddy/v2.CustomVersion=FrankenPHP ${FRANKENPHP_VERSION} PHP ${LIBPHP_VERSION} Caddy'" -o "../../dist/${bin}"
|
||||
cd ../..
|
||||
|
||||
if [ -d "${EMBED}" ]; then
|
||||
truncate -s 0 app.tar
|
||||
truncate -s 0 app_checksum.txt
|
||||
fi
|
||||
|
||||
"dist/${bin}" version
|
||||
|
||||
if [ -n "${RELEASE}" ]; then
|
||||
gh release upload "v${FRANKENPHP_VERSION}" "dist/${bin}" --repo dunglas/frankenphp --clobber
|
||||
fi
|
||||
|
||||
if [ -n "${CURRENT_REF}" ]; then
|
||||
git checkout "${CURRENT_REF}"
|
||||
fi
|
||||
306
caddy/caddy.go
306
caddy/caddy.go
@@ -4,8 +4,10 @@
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
@@ -13,15 +15,20 @@ import (
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp/fileserver"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite"
|
||||
"github.com/dunglas/frankenphp"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
const defaultDocumentRoot = "public"
|
||||
|
||||
func init() {
|
||||
caddy.RegisterModule(FrankenPHPApp{})
|
||||
caddy.RegisterModule(FrankenPHPModule{})
|
||||
httpcaddyfile.RegisterGlobalOption("frankenphp", parseGlobalOption)
|
||||
httpcaddyfile.RegisterHandlerDirective("php", parseCaddyfile)
|
||||
httpcaddyfile.RegisterDirective("php_server", parsePhpServer)
|
||||
}
|
||||
|
||||
type mainPHPinterpreterKeyType int
|
||||
@@ -58,7 +65,7 @@ type FrankenPHPApp struct {
|
||||
func (a FrankenPHPApp) CaddyModule() caddy.ModuleInfo {
|
||||
return caddy.ModuleInfo{
|
||||
ID: "frankenphp",
|
||||
New: func() caddy.Module { return a },
|
||||
New: func() caddy.Module { return &a },
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,8 +96,6 @@ func (f *FrankenPHPApp) Start() error {
|
||||
}
|
||||
}
|
||||
|
||||
logger.Info("FrankenPHP started 🐘", zap.String("php_version", frankenphp.Version().Version))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -165,6 +170,10 @@ func (f *FrankenPHPApp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||
if wc.FileName == "" {
|
||||
return errors.New(`The "file" argument must be specified`)
|
||||
}
|
||||
|
||||
if frankenphp.EmbeddedAppPath != "" && filepath.IsLocal(wc.FileName) {
|
||||
wc.FileName = filepath.Join(frankenphp.EmbeddedAppPath, wc.FileName)
|
||||
}
|
||||
}
|
||||
|
||||
f.Workers = append(f.Workers, wc)
|
||||
@@ -189,7 +198,7 @@ func parseGlobalOption(d *caddyfile.Dispenser, _ interface{}) (interface{}, erro
|
||||
}
|
||||
|
||||
type FrankenPHPModule struct {
|
||||
// Root sets the root folder to the site. Default: `root` directive.
|
||||
// Root sets the root folder to the site. Default: `root` directive, or the path of the public directory of the embed app it exists.
|
||||
Root string `json:"root,omitempty"`
|
||||
// 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"`
|
||||
@@ -213,8 +222,18 @@ func (f *FrankenPHPModule) Provision(ctx caddy.Context) error {
|
||||
f.logger = ctx.Logger(f)
|
||||
|
||||
if f.Root == "" {
|
||||
f.Root = "{http.vars.root}"
|
||||
if frankenphp.EmbeddedAppPath == "" {
|
||||
f.Root = "{http.vars.root}"
|
||||
} else {
|
||||
f.Root = filepath.Join(frankenphp.EmbeddedAppPath, defaultDocumentRoot)
|
||||
f.ResolveRootSymlink = false
|
||||
}
|
||||
} else {
|
||||
if frankenphp.EmbeddedAppPath != "" && filepath.IsLocal(f.Root) {
|
||||
f.Root = filepath.Join(frankenphp.EmbeddedAppPath, f.Root)
|
||||
}
|
||||
}
|
||||
|
||||
if len(f.SplitPath) == 0 {
|
||||
f.SplitPath = []string{".php"}
|
||||
}
|
||||
@@ -224,19 +243,27 @@ func (f *FrankenPHPModule) Provision(ctx caddy.Context) error {
|
||||
|
||||
// ServeHTTP implements caddyhttp.MiddlewareHandler.
|
||||
// TODO: Expose TLS versions as env vars, as Apache's mod_ssl: https://github.com/caddyserver/caddy/blob/master/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go#L298
|
||||
func (f FrankenPHPModule) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
|
||||
func (f FrankenPHPModule) ServeHTTP(w http.ResponseWriter, r *http.Request, _ caddyhttp.Handler) error {
|
||||
origReq := r.Context().Value(caddyhttp.OriginalRequestCtxKey).(http.Request)
|
||||
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
|
||||
|
||||
documentRoot := repl.ReplaceKnown(f.Root, "")
|
||||
fr := frankenphp.NewRequestWithContext(r, documentRoot, f.logger)
|
||||
fc, _ := frankenphp.FromContext(fr.Context())
|
||||
fc.ResolveRootSymlink = f.ResolveRootSymlink
|
||||
fc.SplitPath = f.SplitPath
|
||||
|
||||
fc.Env["REQUEST_URI"] = origReq.URL.RequestURI()
|
||||
env := make(map[string]string, len(f.Env)+1)
|
||||
env["REQUEST_URI"] = origReq.URL.RequestURI()
|
||||
for k, v := range f.Env {
|
||||
fc.Env[k] = repl.ReplaceKnown(v, "")
|
||||
env[k] = repl.ReplaceKnown(v, "")
|
||||
}
|
||||
|
||||
fr, err := frankenphp.NewRequestWithContext(
|
||||
r,
|
||||
frankenphp.WithRequestDocumentRoot(documentRoot, f.ResolveRootSymlink),
|
||||
frankenphp.WithRequestSplitPath(f.SplitPath),
|
||||
frankenphp.WithRequestEnv(env),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return frankenphp.ServeHTTP(w, fr)
|
||||
@@ -283,12 +310,265 @@ func (f *FrankenPHPModule) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||
|
||||
// parseCaddyfile unmarshals tokens from h into a new Middleware.
|
||||
func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
|
||||
var m FrankenPHPModule
|
||||
m := FrankenPHPModule{}
|
||||
err := m.UnmarshalCaddyfile(h.Dispenser)
|
||||
|
||||
return m, err
|
||||
}
|
||||
|
||||
// parsePhpServer parses the php_server directive, which has a similar syntax
|
||||
// to the php_fastcgi directive. A line such as this:
|
||||
//
|
||||
// php_server
|
||||
//
|
||||
// is equivalent to a route consisting of:
|
||||
//
|
||||
// # Add trailing slash for directory requests
|
||||
// @canonicalPath {
|
||||
// file {path}/index.php
|
||||
// not path */
|
||||
// }
|
||||
// redir @canonicalPath {path}/ 308
|
||||
//
|
||||
// # If the requested file does not exist, try index files
|
||||
// @indexFiles file {
|
||||
// try_files {path} {path}/index.php index.php
|
||||
// split_path .php
|
||||
// }
|
||||
// rewrite @indexFiles {http.matchers.file.relative}
|
||||
//
|
||||
// # FrankenPHP!
|
||||
// @phpFiles path *.php
|
||||
// php @phpFiles
|
||||
// file_server
|
||||
//
|
||||
// parsePhpServer is freely inspired from the php_fastgci directive of the Caddy server (Apache License 2.0, Matthew Holt and The Caddy Authors)
|
||||
func parsePhpServer(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error) {
|
||||
if !h.Next() {
|
||||
return nil, h.ArgErr()
|
||||
}
|
||||
|
||||
// set up FrankenPHP
|
||||
phpsrv := FrankenPHPModule{}
|
||||
|
||||
// set up file server
|
||||
fsrv := fileserver.FileServer{}
|
||||
disableFsrv := false
|
||||
|
||||
// set up the set of file extensions allowed to execute PHP code
|
||||
extensions := []string{".php"}
|
||||
|
||||
// set the default index file for the try_files rewrites
|
||||
indexFile := "index.php"
|
||||
|
||||
// set up for explicitly overriding try_files
|
||||
tryFiles := []string{}
|
||||
|
||||
// if the user specified a matcher token, use that
|
||||
// matcher in a route that wraps both of our routes;
|
||||
// either way, strip the matcher token and pass
|
||||
// the remaining tokens to the unmarshaler so that
|
||||
// we can gain the rest of the directive syntax
|
||||
userMatcherSet, err := h.ExtractMatcherSet()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// make a new dispenser from the remaining tokens so that we
|
||||
// can reset the dispenser back to this point for the
|
||||
// php unmarshaler to read from it as well
|
||||
dispenser := h.NewFromNextSegment()
|
||||
|
||||
// read the subdirectives that we allow as overrides to
|
||||
// the php_server shortcut
|
||||
// NOTE: we delete the tokens as we go so that the php
|
||||
// unmarshal doesn't see these subdirectives which it cannot handle
|
||||
for dispenser.Next() {
|
||||
for dispenser.NextBlock(0) {
|
||||
// ignore any sub-subdirectives that might
|
||||
// have the same name somewhere within
|
||||
// the php passthrough tokens
|
||||
if dispenser.Nesting() != 1 {
|
||||
continue
|
||||
}
|
||||
|
||||
// parse the php_server subdirectives
|
||||
switch dispenser.Val() {
|
||||
case "root":
|
||||
if !dispenser.NextArg() {
|
||||
return nil, dispenser.ArgErr()
|
||||
}
|
||||
phpsrv.Root = dispenser.Val()
|
||||
fsrv.Root = phpsrv.Root
|
||||
dispenser.DeleteN(2)
|
||||
|
||||
case "split":
|
||||
extensions = dispenser.RemainingArgs()
|
||||
dispenser.DeleteN(len(extensions) + 1)
|
||||
if len(extensions) == 0 {
|
||||
return nil, dispenser.ArgErr()
|
||||
}
|
||||
|
||||
case "index":
|
||||
args := dispenser.RemainingArgs()
|
||||
dispenser.DeleteN(len(args) + 1)
|
||||
if len(args) != 1 {
|
||||
return nil, dispenser.ArgErr()
|
||||
}
|
||||
indexFile = args[0]
|
||||
|
||||
case "try_files":
|
||||
args := dispenser.RemainingArgs()
|
||||
dispenser.DeleteN(len(args) + 1)
|
||||
if len(args) < 1 {
|
||||
return nil, dispenser.ArgErr()
|
||||
}
|
||||
tryFiles = args
|
||||
|
||||
case "file_server":
|
||||
args := dispenser.RemainingArgs()
|
||||
dispenser.DeleteN(len(args) + 1)
|
||||
if len(args) < 1 || args[0] != "off" {
|
||||
return nil, dispenser.ArgErr()
|
||||
}
|
||||
disableFsrv = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// reset the dispenser after we're done so that the frankenphp
|
||||
// unmarshaler can read it from the start
|
||||
dispenser.Reset()
|
||||
|
||||
if frankenphp.EmbeddedAppPath != "" {
|
||||
if phpsrv.Root == "" {
|
||||
phpsrv.Root = filepath.Join(frankenphp.EmbeddedAppPath, defaultDocumentRoot)
|
||||
fsrv.Root = phpsrv.Root
|
||||
phpsrv.ResolveRootSymlink = false
|
||||
} else if filepath.IsLocal(fsrv.Root) {
|
||||
phpsrv.Root = filepath.Join(frankenphp.EmbeddedAppPath, phpsrv.Root)
|
||||
fsrv.Root = phpsrv.Root
|
||||
}
|
||||
}
|
||||
|
||||
// set up a route list that we'll append to
|
||||
routes := caddyhttp.RouteList{}
|
||||
|
||||
// set the list of allowed path segments on which to split
|
||||
phpsrv.SplitPath = extensions
|
||||
|
||||
// if the index is turned off, we skip the redirect and try_files
|
||||
if indexFile != "off" {
|
||||
// route to redirect to canonical path if index PHP file
|
||||
redirMatcherSet := caddy.ModuleMap{
|
||||
"file": h.JSON(fileserver.MatchFile{
|
||||
TryFiles: []string{"{http.request.uri.path}/" + indexFile},
|
||||
}),
|
||||
"not": h.JSON(caddyhttp.MatchNot{
|
||||
MatcherSetsRaw: []caddy.ModuleMap{
|
||||
{
|
||||
"path": h.JSON(caddyhttp.MatchPath{"*/"}),
|
||||
},
|
||||
},
|
||||
}),
|
||||
}
|
||||
redirHandler := caddyhttp.StaticResponse{
|
||||
StatusCode: caddyhttp.WeakString(strconv.Itoa(http.StatusPermanentRedirect)),
|
||||
Headers: http.Header{"Location": []string{"{http.request.orig_uri.path}/"}},
|
||||
}
|
||||
redirRoute := caddyhttp.Route{
|
||||
MatcherSetsRaw: []caddy.ModuleMap{redirMatcherSet},
|
||||
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(redirHandler, "handler", "static_response", nil)},
|
||||
}
|
||||
|
||||
// if tryFiles wasn't overridden, use a reasonable default
|
||||
if len(tryFiles) == 0 {
|
||||
tryFiles = []string{"{http.request.uri.path}", "{http.request.uri.path}/" + indexFile, indexFile}
|
||||
}
|
||||
|
||||
// route to rewrite to PHP index file
|
||||
rewriteMatcherSet := caddy.ModuleMap{
|
||||
"file": h.JSON(fileserver.MatchFile{
|
||||
TryFiles: tryFiles,
|
||||
SplitPath: extensions,
|
||||
}),
|
||||
}
|
||||
rewriteHandler := rewrite.Rewrite{
|
||||
URI: "{http.matchers.file.relative}",
|
||||
}
|
||||
rewriteRoute := caddyhttp.Route{
|
||||
MatcherSetsRaw: []caddy.ModuleMap{rewriteMatcherSet},
|
||||
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(rewriteHandler, "handler", "rewrite", nil)},
|
||||
}
|
||||
|
||||
routes = append(routes, redirRoute, rewriteRoute)
|
||||
}
|
||||
|
||||
// route to actually pass requests to PHP files;
|
||||
// match only requests that are for PHP files
|
||||
pathList := []string{}
|
||||
for _, ext := range extensions {
|
||||
pathList = append(pathList, "*"+ext)
|
||||
}
|
||||
phpMatcherSet := caddy.ModuleMap{
|
||||
"path": h.JSON(pathList),
|
||||
}
|
||||
|
||||
// the rest of the config is specified by the user
|
||||
// using the php directive syntax
|
||||
dispenser.Next() // consume the directive name
|
||||
err = phpsrv.UnmarshalCaddyfile(dispenser)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// create the PHP route which is
|
||||
// conditional on matching PHP files
|
||||
phpRoute := caddyhttp.Route{
|
||||
MatcherSetsRaw: []caddy.ModuleMap{phpMatcherSet},
|
||||
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(phpsrv, "handler", "php", nil)},
|
||||
}
|
||||
routes = append(routes, phpRoute)
|
||||
|
||||
// create the file server route
|
||||
if !disableFsrv {
|
||||
fileRoute := caddyhttp.Route{
|
||||
MatcherSetsRaw: []caddy.ModuleMap{},
|
||||
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(fsrv, "handler", "file_server", nil)},
|
||||
}
|
||||
routes = append(routes, fileRoute)
|
||||
}
|
||||
|
||||
subroute := caddyhttp.Subroute{
|
||||
Routes: routes,
|
||||
}
|
||||
|
||||
// the user's matcher is a prerequisite for ours, so
|
||||
// wrap ours in a subroute and return that
|
||||
if userMatcherSet != nil {
|
||||
return []httpcaddyfile.ConfigValue{
|
||||
{
|
||||
Class: "route",
|
||||
Value: caddyhttp.Route{
|
||||
MatcherSetsRaw: []caddy.ModuleMap{userMatcherSet},
|
||||
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(subroute, "handler", "subroute", nil)},
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// otherwise, return the literal subroute instead of
|
||||
// individual routes, to ensure they stay together and
|
||||
// are treated as a single unit, without necessarily
|
||||
// creating an actual subroute in the output
|
||||
return []httpcaddyfile.ConfigValue{
|
||||
{
|
||||
Class: "route",
|
||||
Value: subroute,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Interface guards
|
||||
var (
|
||||
_ caddy.App = (*FrankenPHPApp)(nil)
|
||||
|
||||
@@ -139,3 +139,53 @@ func TestEnv(t *testing.T) {
|
||||
|
||||
tester.AssertGetResponse("http://localhost:9080/env.php", http.StatusOK, "bazbar")
|
||||
}
|
||||
|
||||
func TestPHPServerDirective(t *testing.T) {
|
||||
tester := caddytest.NewTester(t)
|
||||
tester.InitServer(`
|
||||
{
|
||||
skip_install_trust
|
||||
admin localhost:2999
|
||||
http_port 9080
|
||||
https_port 9443
|
||||
|
||||
frankenphp
|
||||
order php_server before reverse_proxy
|
||||
}
|
||||
|
||||
localhost:9080 {
|
||||
root * ../testdata
|
||||
php_server
|
||||
}
|
||||
`, "caddyfile")
|
||||
|
||||
tester.AssertGetResponse("http://localhost:9080", http.StatusOK, "I am by birth a Genevese (i not set)")
|
||||
tester.AssertGetResponse("http://localhost:9080/hello.txt", http.StatusOK, "Hello")
|
||||
tester.AssertGetResponse("http://localhost:9080/not-found.txt", http.StatusOK, "I am by birth a Genevese (i not set)")
|
||||
}
|
||||
|
||||
func TestPHPServerDirectiveDisableFileServer(t *testing.T) {
|
||||
tester := caddytest.NewTester(t)
|
||||
tester.InitServer(`
|
||||
{
|
||||
skip_install_trust
|
||||
admin localhost:2999
|
||||
http_port 9080
|
||||
https_port 9443
|
||||
|
||||
frankenphp
|
||||
order php_server before respond
|
||||
}
|
||||
|
||||
localhost:9080 {
|
||||
root * ../testdata
|
||||
php_server {
|
||||
file_server off
|
||||
}
|
||||
respond "Not found" 404
|
||||
}
|
||||
`, "caddyfile")
|
||||
|
||||
tester.AssertGetResponse("http://localhost:9080", http.StatusOK, "I am by birth a Genevese (i not set)")
|
||||
tester.AssertGetResponse("http://localhost:9080/hello.txt", http.StatusNotFound, "Not found")
|
||||
}
|
||||
|
||||
@@ -5,28 +5,32 @@
|
||||
#worker /path/to/your/worker.php
|
||||
{$FRANKENPHP_CONFIG}
|
||||
}
|
||||
|
||||
# https://caddyserver.com/docs/caddyfile/directives#sorting-algorithm
|
||||
order mercure after encode
|
||||
order vulcain after reverse_proxy
|
||||
order php_server before file_server
|
||||
order php before file_server
|
||||
}
|
||||
|
||||
{$SERVER_NAME:localhost}
|
||||
{$CADDY_EXTRA_CONFIG}
|
||||
|
||||
log {
|
||||
# Redact the authorization query parameter that can be set by Mercure
|
||||
format filter {
|
||||
wrap console
|
||||
fields {
|
||||
uri query {
|
||||
replace authorization REDACTED
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
route {
|
||||
# Healthcheck URL
|
||||
skip_log /healthz
|
||||
respond /healthz 200
|
||||
{$SERVER_NAME:localhost} {
|
||||
#log {
|
||||
# # Redact the authorization query parameter that can be set by Mercure
|
||||
# format filter {
|
||||
# wrap console
|
||||
# fields {
|
||||
# uri query {
|
||||
# replace authorization REDACTED
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
#}
|
||||
|
||||
root * public/
|
||||
encode zstd br gzip
|
||||
|
||||
# Uncomment the following lines to enable Mercure and Vulcain modules
|
||||
#mercure {
|
||||
# # Transport to use (default to Bolt)
|
||||
@@ -44,25 +48,7 @@ route {
|
||||
#}
|
||||
#vulcain
|
||||
|
||||
# Add trailing slash for directory requests
|
||||
@canonicalPath {
|
||||
file {path}/index.php
|
||||
not path */
|
||||
}
|
||||
redir @canonicalPath {path}/ 308
|
||||
{$CADDY_SERVER_EXTRA_DIRECTIVES}
|
||||
|
||||
# If the requested file does not exist, try index files
|
||||
@indexFiles file {
|
||||
try_files {path} {path}/index.php index.php
|
||||
split_path .php
|
||||
}
|
||||
rewrite @indexFiles {http.matchers.file.relative}
|
||||
|
||||
# FrankenPHP!
|
||||
@phpFiles path *.php
|
||||
php @phpFiles
|
||||
encode zstd gzip
|
||||
file_server
|
||||
|
||||
respond 404
|
||||
php_server
|
||||
}
|
||||
|
||||
@@ -1,18 +1,25 @@
|
||||
// Copied from https://github.com/caddyserver/xcaddy/blob/b7fd102f41e12be4735dc77b0391823989812ce8/environment.go#L251
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
caddycmd "github.com/caddyserver/caddy/v2/cmd"
|
||||
|
||||
_ "go.uber.org/automaxprocs"
|
||||
"go.uber.org/automaxprocs/maxprocs"
|
||||
"go.uber.org/zap"
|
||||
|
||||
// plug in Caddy modules here.
|
||||
_ "github.com/caddyserver/caddy/v2/modules/standard"
|
||||
_ "github.com/dunglas/caddy-cbrotli"
|
||||
_ "github.com/dunglas/frankenphp/caddy"
|
||||
_ "github.com/dunglas/mercure/caddy"
|
||||
_ "github.com/dunglas/vulcain/caddy"
|
||||
)
|
||||
|
||||
func main() {
|
||||
undo, err := maxprocs.Set()
|
||||
defer undo()
|
||||
if err != nil {
|
||||
caddy.Log().Warn("failed to set GOMAXPROCS", zap.Error(err))
|
||||
}
|
||||
|
||||
caddycmd.Main()
|
||||
}
|
||||
|
||||
202
caddy/go.mod
202
caddy/go.mod
@@ -1,27 +1,32 @@
|
||||
module github.com/dunglas/frankenphp/caddy
|
||||
|
||||
go 1.20
|
||||
go 1.21
|
||||
|
||||
replace github.com/dunglas/frankenphp => ../
|
||||
|
||||
replace (
|
||||
// some packages must match versions defined in go.mod of caddyserver/caddy/v2
|
||||
github.com/caddyserver/certmagic => github.com/caddyserver/certmagic v0.19.2
|
||||
github.com/google/cel-go => github.com/google/cel-go v0.15.1
|
||||
github.com/quic-go/quic-go => github.com/quic-go/quic-go v0.37.6
|
||||
)
|
||||
retract v1.0.0-rc.1 // Human error
|
||||
|
||||
require (
|
||||
github.com/caddyserver/caddy/v2 v2.7.4
|
||||
github.com/dunglas/frankenphp v1.0.0-beta.1
|
||||
github.com/dunglas/mercure/caddy v0.15.2
|
||||
github.com/dunglas/vulcain/caddy v0.5.0
|
||||
github.com/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/mercure/caddy v0.15.9
|
||||
github.com/dunglas/vulcain/caddy v1.0.1
|
||||
github.com/spf13/cobra v1.8.0
|
||||
go.uber.org/automaxprocs v1.5.3
|
||||
go.uber.org/zap v1.26.0
|
||||
)
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.0.0 // indirect
|
||||
github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df // indirect
|
||||
github.com/micromdm/scep/v2 v2.1.0 // indirect
|
||||
github.com/smallstep/go-attestation v0.4.4-0.20240109183208-413678f90935 // indirect
|
||||
go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect
|
||||
github.com/BurntSushi/toml v1.3.2 // indirect
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
@@ -29,57 +34,55 @@ require (
|
||||
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
|
||||
github.com/MauriceGit/skiplist v0.0.0-20211105230623-77f5c8d3e145 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.1 // indirect
|
||||
github.com/RoaringBitmap/roaring v1.3.0 // indirect
|
||||
github.com/alecthomas/chroma/v2 v2.7.0 // indirect
|
||||
github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df // indirect
|
||||
github.com/RoaringBitmap/roaring v1.8.0 // indirect
|
||||
github.com/alecthomas/chroma/v2 v2.12.0 // indirect
|
||||
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bits-and-blooms/bitset v1.8.0 // indirect
|
||||
github.com/caddyserver/certmagic v0.19.2 // indirect
|
||||
github.com/bits-and-blooms/bitset v1.13.0 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
|
||||
github.com/cespare/xxhash v1.1.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/chzyer/readline v1.5.1 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.3 // 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.1.1 // indirect
|
||||
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
|
||||
github.com/dlclark/regexp2 v1.7.0 // indirect
|
||||
github.com/dunglas/httpsfv v1.0.1 // indirect
|
||||
github.com/dunglas/mercure v0.15.2 // indirect
|
||||
github.com/dunglas/vulcain v0.5.0 // indirect
|
||||
github.com/dlclark/regexp2 v1.10.0 // indirect
|
||||
github.com/dunglas/httpsfv v1.0.2 // indirect
|
||||
github.com/dunglas/mercure v0.15.9 // indirect
|
||||
github.com/dunglas/vulcain v1.0.1 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.3 // indirect
|
||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.4.0 // indirect
|
||||
github.com/getkin/kin-openapi v0.118.0 // indirect
|
||||
github.com/go-chi/chi v4.1.2+incompatible // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.5.0 // indirect
|
||||
github.com/getkin/kin-openapi v0.122.0 // indirect
|
||||
github.com/go-chi/chi/v5 v5.0.10 // indirect
|
||||
github.com/go-kit/kit v0.13.0 // indirect
|
||||
github.com/go-kit/log v0.2.1 // indirect
|
||||
github.com/go-logfmt/logfmt v0.6.0 // indirect
|
||||
github.com/go-logr/logr v1.2.4 // indirect
|
||||
github.com/go-logr/logr v1.3.0 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.20.0 // indirect
|
||||
github.com/go-openapi/swag v0.22.4 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.20.2 // indirect
|
||||
github.com/go-openapi/swag v0.22.5 // indirect
|
||||
github.com/go-sql-driver/mysql v1.7.1 // indirect
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
||||
github.com/gofrs/uuid v4.4.0+incompatible // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
|
||||
github.com/golang/glog v1.1.2 // indirect
|
||||
github.com/golang/mock v1.6.0 // indirect
|
||||
github.com/golang/glog v1.2.0 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/cel-go v0.18.0 // indirect
|
||||
github.com/google/certificate-transparency-go v1.1.4 // indirect
|
||||
github.com/google/go-tpm v0.3.3 // indirect
|
||||
github.com/google/brotli/go/cbrotli v0.0.0-20240116120200-adbc354d23af // indirect
|
||||
github.com/google/cel-go v0.15.1 // indirect
|
||||
github.com/google/certificate-transparency-go v1.1.7 // indirect
|
||||
github.com/google/go-tpm v0.9.0 // indirect
|
||||
github.com/google/go-tspi v0.3.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20230912144702-c363fe2c2ed8 // indirect
|
||||
github.com/google/uuid v1.3.1 // indirect
|
||||
github.com/gorilla/handlers v1.5.1 // indirect
|
||||
github.com/gorilla/mux v1.8.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 // indirect
|
||||
github.com/hashicorp/golang-lru v0.5.4 // indirect
|
||||
github.com/google/pprof v0.0.0-20240125082051-42cd04596328 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/handlers v1.5.2 // indirect
|
||||
github.com/gorilla/mux v1.8.1 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.1 // indirect
|
||||
github.com/hashicorp/golang-lru v1.0.2 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/huandu/xstrings v1.4.0 // indirect
|
||||
github.com/imdario/mergo v0.3.16 // indirect
|
||||
@@ -90,109 +93,104 @@ require (
|
||||
github.com/jackc/pgio v1.0.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgproto3/v2 v2.3.2 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||
github.com/jackc/pgtype v1.14.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect
|
||||
github.com/jackc/pgtype v1.14.1 // indirect
|
||||
github.com/jackc/pgx/v4 v4.18.1 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/kevburnsjr/skipfilter v0.0.1 // indirect
|
||||
github.com/klauspost/compress v1.16.7 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
|
||||
github.com/klauspost/compress v1.17.5 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
|
||||
github.com/libdns/libdns v0.2.1 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/manifoldco/promptui v0.9.0 // indirect
|
||||
github.com/mastercactapus/proxyprotocol v0.0.4 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||
github.com/mholt/acmez v1.2.0 // indirect
|
||||
github.com/micromdm/scep/v2 v2.1.0 // indirect
|
||||
github.com/miekg/dns v1.1.56 // indirect
|
||||
github.com/miekg/dns v1.1.58 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/go-ps v1.0.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
|
||||
github.com/mschoch/smat v0.2.0 // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.12.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.9 // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.15.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.1.1 // indirect
|
||||
github.com/perimeterx/marshmallow v1.1.5 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/prometheus/client_golang v1.16.0 // indirect
|
||||
github.com/prometheus/client_model v0.4.0 // indirect
|
||||
github.com/prometheus/common v0.44.0 // indirect
|
||||
github.com/prometheus/procfs v0.11.1 // indirect
|
||||
github.com/prometheus/client_golang v1.18.0 // indirect
|
||||
github.com/prometheus/client_model v0.5.0 // indirect
|
||||
github.com/prometheus/common v0.46.0 // indirect
|
||||
github.com/prometheus/procfs v0.12.0 // indirect
|
||||
github.com/quic-go/qpack v0.4.0 // indirect
|
||||
github.com/quic-go/qtls-go1-20 v0.3.4 // indirect
|
||||
github.com/quic-go/quic-go v0.38.1 // indirect
|
||||
github.com/quic-go/quic-go v0.41.0 // indirect
|
||||
github.com/rs/xid v1.5.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/shopspring/decimal v1.3.1 // indirect
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/slackhq/nebula v1.7.2 // indirect
|
||||
github.com/smallstep/certificates v0.24.3-rc1 // indirect
|
||||
github.com/smallstep/go-attestation v0.4.4-0.20230509120429-e17291421738 // indirect
|
||||
github.com/slackhq/nebula v1.8.2 // indirect
|
||||
github.com/smallstep/certificates v0.25.0 // indirect
|
||||
github.com/smallstep/nosql v0.6.0 // indirect
|
||||
github.com/smallstep/truststore v0.12.1 // indirect
|
||||
github.com/spf13/afero v1.9.5 // indirect
|
||||
github.com/spf13/cast v1.5.1 // indirect
|
||||
github.com/spf13/cobra v1.7.0 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.11.0 // indirect
|
||||
github.com/spf13/cast v1.6.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/spf13/viper v1.16.0 // indirect
|
||||
github.com/spf13/viper v1.18.2 // indirect
|
||||
github.com/stoewer/go-strcase v1.3.0 // indirect
|
||||
github.com/subosito/gotenv v1.4.2 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/tailscale/tscert v0.0.0-20230806124524-28a91b69a046 // indirect
|
||||
github.com/tidwall/gjson v1.15.0 // indirect
|
||||
github.com/tidwall/gjson v1.17.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
github.com/unrolled/secure v1.13.0 // indirect
|
||||
github.com/unrolled/secure v1.14.0 // indirect
|
||||
github.com/urfave/cli v1.22.14 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
|
||||
github.com/yuin/goldmark v1.5.5 // indirect
|
||||
github.com/yuin/goldmark v1.6.0 // indirect
|
||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc // indirect
|
||||
github.com/zeebo/blake3 v0.2.3 // indirect
|
||||
go.etcd.io/bbolt v1.3.7 // indirect
|
||||
go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.42.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/autoprop v0.42.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/aws v1.17.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/b3 v1.17.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/jaeger v1.17.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/ot v1.17.0 // indirect
|
||||
go.opentelemetry.io/otel v1.16.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.16.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.16.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.16.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.16.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.16.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.16.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v0.19.0 // indirect
|
||||
go.etcd.io/bbolt v1.3.8 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/autoprop v0.45.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/aws v1.20.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/b3 v1.20.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/jaeger v1.20.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/ot v1.20.0 // indirect
|
||||
go.opentelemetry.io/otel v1.21.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.21.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.21.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.21.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.0.0 // indirect
|
||||
go.step.sm/cli-utils v0.8.0 // indirect
|
||||
go.step.sm/crypto v0.35.0 // indirect
|
||||
go.step.sm/linkedca v0.20.0 // indirect
|
||||
go.step.sm/crypto v0.36.0 // indirect
|
||||
go.step.sm/linkedca v0.20.1 // indirect
|
||||
go.uber.org/mock v0.4.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/crypto v0.13.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
||||
golang.org/x/mod v0.12.0 // indirect
|
||||
golang.org/x/net v0.15.0 // indirect
|
||||
golang.org/x/sync v0.3.0 // indirect
|
||||
golang.org/x/sys v0.12.0 // indirect
|
||||
golang.org/x/term v0.12.0 // indirect
|
||||
golang.org/x/text v0.13.0 // indirect
|
||||
golang.org/x/tools v0.13.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20230803162519-f966b187b2e5 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230911183012-2d3300fd4832 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230911183012-2d3300fd4832 // indirect
|
||||
google.golang.org/grpc v1.58.0 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
golang.org/x/crypto v0.18.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a // indirect
|
||||
golang.org/x/mod v0.14.0 // indirect
|
||||
golang.org/x/net v0.20.0 // indirect
|
||||
golang.org/x/sync v0.6.0 // indirect
|
||||
golang.org/x/sys v0.16.0 // indirect
|
||||
golang.org/x/term v0.16.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
golang.org/x/tools v0.17.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240125205218-1f4bbc51befe // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240125205218-1f4bbc51befe // indirect
|
||||
google.golang.org/grpc v1.61.0 // indirect
|
||||
google.golang.org/protobuf v1.32.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
howett.net/plist v1.0.0 // indirect
|
||||
howett.net/plist v1.0.1 // indirect
|
||||
)
|
||||
|
||||
1588
caddy/go.sum
1588
caddy/go.sum
File diff suppressed because it is too large
Load Diff
44
caddy/php-cli.go
Normal file
44
caddy/php-cli.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
caddycmd "github.com/caddyserver/caddy/v2/cmd"
|
||||
"github.com/dunglas/frankenphp"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
caddycmd.RegisterCommand(caddycmd.Command{
|
||||
Name: "php-cli",
|
||||
Usage: "script.php [args ...]",
|
||||
Short: "Runs a PHP command",
|
||||
Long: `
|
||||
Executes a PHP script similarly to the CLI SAPI.`,
|
||||
CobraFunc: func(cmd *cobra.Command) {
|
||||
cmd.DisableFlagParsing = true
|
||||
cmd.RunE = caddycmd.WrapCommandFuncForCobra(cmdPHPCLI)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func cmdPHPCLI(fs caddycmd.Flags) (int, error) {
|
||||
args := os.Args[2:]
|
||||
if len(args) < 1 {
|
||||
return 1, errors.New("the path to the PHP script is required")
|
||||
}
|
||||
|
||||
if frankenphp.EmbeddedAppPath != "" {
|
||||
if _, err := os.Stat(args[0]); err != nil {
|
||||
args[0] = filepath.Join(frankenphp.EmbeddedAppPath, args[0])
|
||||
}
|
||||
}
|
||||
|
||||
status := frankenphp.ExecuteScriptCLI(args[0], args)
|
||||
os.Exit(status)
|
||||
|
||||
return status, nil
|
||||
}
|
||||
328
caddy/php-server.go
Normal file
328
caddy/php-server.go
Normal file
@@ -0,0 +1,328 @@
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
mercureModule "github.com/dunglas/mercure/caddy"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||
caddycmd "github.com/caddyserver/caddy/v2/cmd"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp/encode"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp/fileserver"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite"
|
||||
"github.com/caddyserver/certmagic"
|
||||
"github.com/dunglas/frankenphp"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
caddycmd.RegisterCommand(caddycmd.Command{
|
||||
Name: "php-server",
|
||||
Usage: "[--domain <example.com>] [--root <path>] [--listen <addr>] [--worker /path/to/worker.php<,nb-workers>] [--access-log] [--debug] [--no-compress] [--mercure]",
|
||||
Short: "Spins up a production-ready PHP server",
|
||||
Long: `
|
||||
A simple but production-ready PHP server. Useful for quick deployments,
|
||||
demos, and development.
|
||||
|
||||
The listener's socket address can be customized with the --listen flag.
|
||||
|
||||
If a domain name is specified with --domain, the default listener address
|
||||
will be changed to the HTTPS port and the server will use HTTPS. If using
|
||||
a public domain, ensure A/AAAA records are properly configured before
|
||||
using this option.
|
||||
|
||||
For more advanced use cases, see https://github.com/dunglas/frankenphp/blob/main/docs/config.md`,
|
||||
CobraFunc: func(cmd *cobra.Command) {
|
||||
cmd.Flags().StringP("domain", "d", "", "Domain name at which to serve the files")
|
||||
cmd.Flags().StringP("root", "r", "", "The path to the root of the site")
|
||||
cmd.Flags().StringP("listen", "l", "", "The address to which to bind the listener")
|
||||
cmd.Flags().StringArrayP("worker", "w", []string{}, "Worker script")
|
||||
cmd.Flags().BoolP("access-log", "a", false, "Enable the access log")
|
||||
cmd.Flags().BoolP("debug", "v", false, "Enable verbose debug logs")
|
||||
cmd.Flags().BoolP("mercure", "m", false, "Enable the built-in Mercure.rocks hub")
|
||||
cmd.Flags().BoolP("no-compress", "", false, "Disable Zstandard, Brotli and Gzip compression")
|
||||
cmd.RunE = caddycmd.WrapCommandFuncForCobra(cmdPHPServer)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// cmdPHPServer is freely inspired from the file-server command of the Caddy server (Apache License 2.0, Matthew Holt and The Caddy Authors)
|
||||
func cmdPHPServer(fs caddycmd.Flags) (int, error) {
|
||||
caddy.TrapSignals()
|
||||
|
||||
domain := fs.String("domain")
|
||||
root := fs.String("root")
|
||||
listen := fs.String("listen")
|
||||
accessLog := fs.Bool("access-log")
|
||||
debug := fs.Bool("debug")
|
||||
compress := !fs.Bool("no-compress")
|
||||
mercure := fs.Bool("mercure")
|
||||
|
||||
workers, err := fs.GetStringArray("worker")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
var workersOption []workerConfig
|
||||
if len(workers) != 0 {
|
||||
workersOption = make([]workerConfig, 0, len(workers))
|
||||
for _, worker := range workers {
|
||||
parts := strings.SplitN(worker, ",", 2)
|
||||
if frankenphp.EmbeddedAppPath != "" && filepath.IsLocal(parts[0]) {
|
||||
parts[0] = filepath.Join(frankenphp.EmbeddedAppPath, parts[0])
|
||||
}
|
||||
|
||||
var num int
|
||||
if len(parts) > 1 {
|
||||
num, _ = strconv.Atoi(parts[1])
|
||||
}
|
||||
|
||||
workersOption = append(workersOption, workerConfig{FileName: parts[0], Num: num})
|
||||
}
|
||||
}
|
||||
|
||||
if frankenphp.EmbeddedAppPath != "" {
|
||||
if _, err := os.Stat(filepath.Join(frankenphp.EmbeddedAppPath, "php.ini")); err == nil {
|
||||
iniScanDir := os.Getenv("PHP_INI_SCAN_DIR")
|
||||
|
||||
if err := os.Setenv("PHP_INI_SCAN_DIR", iniScanDir+":"+frankenphp.EmbeddedAppPath); err != nil {
|
||||
return caddy.ExitCodeFailedStartup, err
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := os.Stat(filepath.Join(frankenphp.EmbeddedAppPath, "Caddyfile")); err == nil {
|
||||
config, _, err := caddycmd.LoadConfig(filepath.Join(frankenphp.EmbeddedAppPath, "Caddyfile"), "")
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, err
|
||||
}
|
||||
|
||||
if err = caddy.Load(config, true); err != nil {
|
||||
return caddy.ExitCodeFailedStartup, err
|
||||
}
|
||||
|
||||
select {}
|
||||
}
|
||||
|
||||
if root == "" {
|
||||
root = filepath.Join(frankenphp.EmbeddedAppPath, defaultDocumentRoot)
|
||||
} else if filepath.IsLocal(root) {
|
||||
root = filepath.Join(frankenphp.EmbeddedAppPath, root)
|
||||
}
|
||||
}
|
||||
|
||||
const indexFile = "index.php"
|
||||
extensions := []string{"php"}
|
||||
tryFiles := []string{"{http.request.uri.path}", "{http.request.uri.path}/" + indexFile, indexFile}
|
||||
|
||||
phpHandler := FrankenPHPModule{
|
||||
Root: root,
|
||||
SplitPath: extensions,
|
||||
}
|
||||
|
||||
// route to redirect to canonical path if index PHP file
|
||||
redirMatcherSet := caddy.ModuleMap{
|
||||
"file": caddyconfig.JSON(fileserver.MatchFile{
|
||||
Root: root,
|
||||
TryFiles: []string{"{http.request.uri.path}/" + indexFile},
|
||||
}, nil),
|
||||
"not": caddyconfig.JSON(caddyhttp.MatchNot{
|
||||
MatcherSetsRaw: []caddy.ModuleMap{
|
||||
{
|
||||
"path": caddyconfig.JSON(caddyhttp.MatchPath{"*/"}, nil),
|
||||
},
|
||||
},
|
||||
}, nil),
|
||||
}
|
||||
redirHandler := caddyhttp.StaticResponse{
|
||||
StatusCode: caddyhttp.WeakString(strconv.Itoa(http.StatusPermanentRedirect)),
|
||||
Headers: http.Header{"Location": []string{"{http.request.orig_uri.path}/"}},
|
||||
}
|
||||
redirRoute := caddyhttp.Route{
|
||||
MatcherSetsRaw: []caddy.ModuleMap{redirMatcherSet},
|
||||
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(redirHandler, "handler", "static_response", nil)},
|
||||
}
|
||||
|
||||
// route to rewrite to PHP index file
|
||||
rewriteMatcherSet := caddy.ModuleMap{
|
||||
"file": caddyconfig.JSON(fileserver.MatchFile{
|
||||
Root: root,
|
||||
TryFiles: tryFiles,
|
||||
SplitPath: extensions,
|
||||
}, nil),
|
||||
}
|
||||
rewriteHandler := rewrite.Rewrite{
|
||||
URI: "{http.matchers.file.relative}",
|
||||
}
|
||||
rewriteRoute := caddyhttp.Route{
|
||||
MatcherSetsRaw: []caddy.ModuleMap{rewriteMatcherSet},
|
||||
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(rewriteHandler, "handler", "rewrite", nil)},
|
||||
}
|
||||
|
||||
// route to actually pass requests to PHP files;
|
||||
// match only requests that are for PHP files
|
||||
pathList := []string{}
|
||||
for _, ext := range extensions {
|
||||
pathList = append(pathList, "*"+ext)
|
||||
}
|
||||
phpMatcherSet := caddy.ModuleMap{
|
||||
"path": caddyconfig.JSON(pathList, nil),
|
||||
}
|
||||
|
||||
// create the PHP route which is
|
||||
// conditional on matching PHP files
|
||||
phpRoute := caddyhttp.Route{
|
||||
MatcherSetsRaw: []caddy.ModuleMap{phpMatcherSet},
|
||||
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(phpHandler, "handler", "php", nil)},
|
||||
}
|
||||
|
||||
fileRoute := caddyhttp.Route{
|
||||
MatcherSetsRaw: []caddy.ModuleMap{},
|
||||
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(fileserver.FileServer{Root: root}, "handler", "file_server", nil)},
|
||||
}
|
||||
|
||||
subroute := caddyhttp.Subroute{
|
||||
Routes: caddyhttp.RouteList{redirRoute, rewriteRoute, phpRoute, fileRoute},
|
||||
}
|
||||
|
||||
if compress {
|
||||
gzip, err := caddy.GetModule("http.encoders.gzip")
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, err
|
||||
}
|
||||
|
||||
br, err := caddy.GetModule("http.encoders.br")
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, err
|
||||
}
|
||||
|
||||
zstd, err := caddy.GetModule("http.encoders.zstd")
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, err
|
||||
}
|
||||
|
||||
encodeRoute := caddyhttp.Route{
|
||||
MatcherSetsRaw: []caddy.ModuleMap{},
|
||||
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(encode.Encode{
|
||||
EncodingsRaw: caddy.ModuleMap{
|
||||
"zstd": caddyconfig.JSON(zstd.New(), nil),
|
||||
"br": caddyconfig.JSON(br.New(), nil),
|
||||
"gzip": caddyconfig.JSON(gzip.New(), nil),
|
||||
},
|
||||
Prefer: []string{"zstd", "br", "gzip"},
|
||||
}, "handler", "encode", nil)},
|
||||
}
|
||||
|
||||
subroute.Routes = append(caddyhttp.RouteList{encodeRoute}, subroute.Routes...)
|
||||
}
|
||||
|
||||
if mercure {
|
||||
mercurePublisherJwtKey := os.Getenv("MERCURE_PUBLISHER_JWT_KEY")
|
||||
if mercurePublisherJwtKey == "" {
|
||||
panic(`The "MERCURE_PUBLISHER_JWT_KEY" environment variable must be set to use the Mercure.rocks hub`)
|
||||
}
|
||||
|
||||
mercureSubscriberJwtKey := os.Getenv("MERCURE_SUBSCRIBER_JWT_KEY")
|
||||
if mercureSubscriberJwtKey == "" {
|
||||
panic(`The "MERCURE_SUBSCRIBER_JWT_KEY" environment variable must be set to use the Mercure.rocks hub`)
|
||||
}
|
||||
|
||||
mercureRoute := caddyhttp.Route{
|
||||
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(
|
||||
mercureModule.Mercure{
|
||||
PublisherJWT: mercureModule.JWTConfig{
|
||||
Alg: os.Getenv("MERCURE_PUBLISHER_JWT_ALG"),
|
||||
Key: mercurePublisherJwtKey,
|
||||
},
|
||||
SubscriberJWT: mercureModule.JWTConfig{
|
||||
Alg: os.Getenv("MERCURE_SUBSCRIBER_JWT_ALG"),
|
||||
Key: mercureSubscriberJwtKey,
|
||||
},
|
||||
},
|
||||
"handler",
|
||||
"mercure",
|
||||
nil,
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
subroute.Routes = append(caddyhttp.RouteList{mercureRoute}, subroute.Routes...)
|
||||
}
|
||||
|
||||
route := caddyhttp.Route{
|
||||
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(subroute, "handler", "subroute", nil)},
|
||||
}
|
||||
|
||||
if domain != "" {
|
||||
route.MatcherSetsRaw = []caddy.ModuleMap{
|
||||
{
|
||||
"host": caddyconfig.JSON(caddyhttp.MatchHost{domain}, nil),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
server := &caddyhttp.Server{
|
||||
ReadHeaderTimeout: caddy.Duration(10 * time.Second),
|
||||
IdleTimeout: caddy.Duration(30 * time.Second),
|
||||
MaxHeaderBytes: 1024 * 10,
|
||||
Routes: caddyhttp.RouteList{route},
|
||||
}
|
||||
if listen == "" {
|
||||
if domain == "" {
|
||||
listen = ":80"
|
||||
} else {
|
||||
listen = ":" + strconv.Itoa(certmagic.HTTPSPort)
|
||||
}
|
||||
}
|
||||
server.Listen = []string{listen}
|
||||
if accessLog {
|
||||
server.Logs = &caddyhttp.ServerLogConfig{}
|
||||
}
|
||||
|
||||
httpApp := caddyhttp.App{
|
||||
Servers: map[string]*caddyhttp.Server{"php": server},
|
||||
}
|
||||
|
||||
var false bool
|
||||
cfg := &caddy.Config{
|
||||
Admin: &caddy.AdminConfig{
|
||||
Disabled: true,
|
||||
Config: &caddy.ConfigSettings{
|
||||
Persist: &false,
|
||||
},
|
||||
},
|
||||
AppsRaw: caddy.ModuleMap{
|
||||
"http": caddyconfig.JSON(httpApp, nil),
|
||||
"frankenphp": caddyconfig.JSON(FrankenPHPApp{Workers: workersOption}, nil),
|
||||
},
|
||||
}
|
||||
|
||||
if debug {
|
||||
cfg.Logging = &caddy.Logging{
|
||||
Logs: map[string]*caddy.CustomLog{
|
||||
"default": {
|
||||
BaseLog: caddy.BaseLog{Level: zap.DebugLevel.CapitalString()},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
err = caddy.Run(cfg)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, err
|
||||
}
|
||||
|
||||
log.Printf("Caddy serving PHP app on %s", listen)
|
||||
|
||||
select {}
|
||||
}
|
||||
291
cgi.go
291
cgi.go
@@ -1,5 +1,6 @@
|
||||
package frankenphp
|
||||
|
||||
import "C"
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net"
|
||||
@@ -8,217 +9,155 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// populateEnv returns a set of CGI environment variables for the request.
|
||||
type serverKey int
|
||||
|
||||
const (
|
||||
contentLength serverKey = iota
|
||||
documentRoot
|
||||
documentUri
|
||||
gatewayInterface
|
||||
httpHost
|
||||
https
|
||||
pathInfo
|
||||
phpSelf
|
||||
remoteAddr
|
||||
remoteHost
|
||||
remotePort
|
||||
requestScheme
|
||||
scriptFilename
|
||||
scriptName
|
||||
serverName
|
||||
serverPort
|
||||
serverProtocol
|
||||
serverSoftware
|
||||
sslProtocol
|
||||
)
|
||||
|
||||
func allocServerVariable(cArr *[27]*C.char, env map[string]string, serverKey serverKey, envKey string, val string) {
|
||||
if val, ok := env[envKey]; ok {
|
||||
cArr[serverKey] = C.CString(val)
|
||||
delete(env, envKey)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
cArr[serverKey] = C.CString(val)
|
||||
}
|
||||
|
||||
// computeKnownVariables 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 {
|
||||
func computeKnownVariables(request *http.Request) (cArr [27]*C.char) {
|
||||
fc, fcOK := FromContext(request.Context())
|
||||
if !fcOK {
|
||||
panic("not a FrankenPHP request")
|
||||
}
|
||||
|
||||
if fc.populated {
|
||||
return nil
|
||||
// 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
|
||||
}
|
||||
|
||||
_, 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:]
|
||||
// Remove [] from IPv6 addresses
|
||||
ip = strings.Replace(ip, "[", "", 1)
|
||||
ip = strings.Replace(ip, "]", "", 1)
|
||||
|
||||
ra, raOK := fc.env["REMOTE_ADDR"]
|
||||
if raOK {
|
||||
cArr[remoteAddr] = C.CString(ra)
|
||||
delete(fc.env, "REMOTE_ADDR")
|
||||
} else {
|
||||
cArr[remoteAddr] = C.CString(ip)
|
||||
}
|
||||
|
||||
if rh, ok := fc.env["REMOTE_HOST"]; ok {
|
||||
cArr[remoteHost] = C.CString(rh) // For speed, remote host lookups disabled
|
||||
delete(fc.env, "REMOTE_HOST")
|
||||
} else {
|
||||
if raOK {
|
||||
cArr[remoteHost] = C.CString(ip)
|
||||
} 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
|
||||
cArr[remoteHost] = cArr[remoteAddr]
|
||||
}
|
||||
}
|
||||
|
||||
if _, ok := fc.Env["DOCUMENT_ROOT"]; !ok {
|
||||
// make sure file root is absolute
|
||||
root, err := filepath.Abs(fc.DocumentRoot)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
allocServerVariable(&cArr, fc.env, remotePort, "REMOTE_PORT", port)
|
||||
allocServerVariable(&cArr, fc.env, documentRoot, "DOCUMENT_ROOT", fc.documentRoot)
|
||||
allocServerVariable(&cArr, fc.env, pathInfo, "PATH_INFO", fc.pathInfo)
|
||||
allocServerVariable(&cArr, fc.env, phpSelf, "PHP_SELF", request.URL.Path)
|
||||
allocServerVariable(&cArr, fc.env, documentUri, "DOCUMENT_URI", fc.docURI)
|
||||
allocServerVariable(&cArr, fc.env, scriptFilename, "SCRIPT_FILENAME", fc.scriptFilename)
|
||||
allocServerVariable(&cArr, fc.env, scriptName, "SCRIPT_NAME", fc.scriptName)
|
||||
|
||||
if fc.ResolveRootSymlink {
|
||||
if root, err = filepath.EvalSymlinks(root); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
var rs string
|
||||
if request.TLS == nil {
|
||||
rs = "http"
|
||||
} else {
|
||||
rs = "https"
|
||||
|
||||
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["PHP_SELF"]; !ok {
|
||||
fc.Env["PHP_SELF"] = fpath
|
||||
}
|
||||
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"
|
||||
if h, ok := fc.env["HTTPS"]; ok {
|
||||
cArr[https] = C.CString(h)
|
||||
delete(fc.env, "HTTPS")
|
||||
} else {
|
||||
fc.Env["REQUEST_SCHEME"] = "https"
|
||||
}
|
||||
}
|
||||
|
||||
if request.TLS != nil {
|
||||
if _, ok := fc.Env["HTTPS"]; !ok {
|
||||
fc.Env["HTTPS"] = "on"
|
||||
cArr[https] = C.CString("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
|
||||
if p, ok := fc.env["SSL_PROTOCOL"]; ok {
|
||||
cArr[sslProtocol] = C.CString(p)
|
||||
delete(fc.env, "SSL_PROTOCOL")
|
||||
} else {
|
||||
if v, ok := tlsProtocolStrings[request.TLS.Version]; ok {
|
||||
cArr[sslProtocol] = C.CString(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
allocServerVariable(&cArr, fc.env, requestScheme, "REQUEST_SCHEME", rs)
|
||||
|
||||
if fc.Env["SERVER_NAME"] == "" || fc.Env["SERVER_PORT"] == "" {
|
||||
reqHost, reqPort, _ := net.SplitHostPort(request.Host)
|
||||
if fc.Env["SERVER_NAME"] == "" {
|
||||
fc.Env["SERVER_NAME"] = reqHost
|
||||
}
|
||||
if fc.Env["SERVER_PORT"] == "" {
|
||||
fc.Env["SERVER_PORT"] = reqPort
|
||||
}
|
||||
reqHost, reqPort, _ := net.SplitHostPort(request.Host)
|
||||
|
||||
if fc.Env["SERVER_NAME"] == "" {
|
||||
// whatever, just assume there was no port
|
||||
fc.Env["SERVER_NAME"] = request.Host
|
||||
}
|
||||
if reqHost == "" {
|
||||
// whatever, just assume there was no port
|
||||
reqHost = request.Host
|
||||
}
|
||||
|
||||
if reqPort == "" {
|
||||
// compliance with the CGI specification requires that
|
||||
// the SERVER_PORT variable MUST be set to the TCP/IP port number on which this request is received from the client
|
||||
// even if the port is the default port for the scheme and could otherwise be omitted from a URI.
|
||||
// https://tools.ietf.org/html/rfc3875#section-4.1.15
|
||||
if fc.Env["SERVER_PORT"] == "" {
|
||||
if fc.Env["REQUEST_SCHEME"] == "https" {
|
||||
fc.Env["SERVER_PORT"] = "443"
|
||||
} else {
|
||||
fc.Env["SERVER_PORT"] = "80"
|
||||
}
|
||||
switch rs {
|
||||
case "https":
|
||||
reqPort = "443"
|
||||
case "http":
|
||||
reqPort = "80"
|
||||
}
|
||||
}
|
||||
|
||||
allocServerVariable(&cArr, fc.env, serverName, "SERVER_NAME", reqHost)
|
||||
if reqPort != "" {
|
||||
allocServerVariable(&cArr, fc.env, serverPort, "SERVER_PORT", reqPort)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
// These values can not be override
|
||||
cArr[contentLength] = C.CString(request.Header.Get("Content-Length"))
|
||||
|
||||
// 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, ", ")
|
||||
}
|
||||
}
|
||||
allocServerVariable(&cArr, fc.env, gatewayInterface, "GATEWAY_INTERFACE", "CGI/1.1")
|
||||
allocServerVariable(&cArr, fc.env, serverProtocol, "SERVER_PROTOCOL", request.Proto)
|
||||
allocServerVariable(&cArr, fc.env, serverSoftware, "SERVER_SOFTWARE", "FrankenPHP")
|
||||
allocServerVariable(&cArr, fc.env, httpHost, "HTTP_HOST", request.Host) // added here, since not always part of headers
|
||||
|
||||
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
|
||||
return
|
||||
}
|
||||
|
||||
// splitPos returns the index where path should
|
||||
@@ -227,12 +166,12 @@ func populateEnv(request *http.Request) error {
|
||||
// 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 {
|
||||
if len(fc.splitPath) == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
lowerPath := strings.ToLower(path)
|
||||
for _, split := range fc.SplitPath {
|
||||
for _, split := range fc.splitPath {
|
||||
if idx := strings.Index(lowerPath, strings.ToLower(split)); idx > -1 {
|
||||
return idx + len(split)
|
||||
}
|
||||
|
||||
@@ -3,62 +3,64 @@ FROM golang:1.21-alpine
|
||||
|
||||
ENV CFLAGS="-ggdb3"
|
||||
ENV PHPIZE_DEPS \
|
||||
autoconf \
|
||||
dpkg-dev \
|
||||
file \
|
||||
g++ \
|
||||
gcc \
|
||||
libc-dev \
|
||||
make \
|
||||
pkgconfig \
|
||||
re2c
|
||||
autoconf \
|
||||
dpkg-dev \
|
||||
file \
|
||||
g++ \
|
||||
gcc \
|
||||
libc-dev \
|
||||
make \
|
||||
pkgconfig \
|
||||
re2c
|
||||
|
||||
RUN apk add --no-cache \
|
||||
$PHPIZE_DEPS \
|
||||
argon2-dev \
|
||||
curl-dev \
|
||||
oniguruma-dev \
|
||||
readline-dev \
|
||||
libsodium-dev \
|
||||
sqlite-dev \
|
||||
openssl-dev \
|
||||
libxml2-dev \
|
||||
zlib-dev \
|
||||
bison \
|
||||
nss-tools \
|
||||
# Dev tools \
|
||||
git \
|
||||
clang \
|
||||
llvm \
|
||||
gdb \
|
||||
valgrind \
|
||||
neovim \
|
||||
zsh \
|
||||
libtool && \
|
||||
echo 'set auto-load safe-path /' > /root/.gdbinit
|
||||
$PHPIZE_DEPS \
|
||||
argon2-dev \
|
||||
brotli-dev \
|
||||
curl-dev \
|
||||
oniguruma-dev \
|
||||
readline-dev \
|
||||
libsodium-dev \
|
||||
sqlite-dev \
|
||||
openssl-dev \
|
||||
libxml2-dev \
|
||||
zlib-dev \
|
||||
bison \
|
||||
nss-tools \
|
||||
# Dev tools \
|
||||
git \
|
||||
clang \
|
||||
llvm \
|
||||
gdb \
|
||||
valgrind \
|
||||
neovim \
|
||||
zsh \
|
||||
libtool && \
|
||||
echo 'set auto-load safe-path /' > /root/.gdbinit
|
||||
|
||||
RUN git clone --branch=PHP-8.2 https://github.com/php/php-src.git && \
|
||||
cd php-src && \
|
||||
# --enable-embed is only necessary to generate libphp.so, we don't use this SAPI directly
|
||||
./buildconf --force && \
|
||||
./configure \
|
||||
--enable-embed \
|
||||
--enable-zts \
|
||||
--disable-zend-signals \
|
||||
--enable-zend-max-execution-timers \
|
||||
--enable-debug && \
|
||||
make -j$(nproc) && \
|
||||
make install && \
|
||||
ldconfig /etc/ld.so.conf.d && \
|
||||
cp php.ini-development /usr/local/lib/php.ini && \
|
||||
echo -e "zend_extension=opcache.so\nopcache.enable=1" >> /usr/local/lib/php.ini &&\
|
||||
php --version
|
||||
WORKDIR /usr/local/src/php
|
||||
RUN git clone --branch=PHP-8.3 https://github.com/php/php-src.git . && \
|
||||
# --enable-embed is only necessary to generate libphp.so, we don't use this SAPI directly
|
||||
./buildconf --force && \
|
||||
./configure \
|
||||
--enable-embed \
|
||||
--enable-zts \
|
||||
--disable-zend-signals \
|
||||
--enable-zend-max-execution-timers \
|
||||
--enable-debug && \
|
||||
make -j"$(nproc)" && \
|
||||
make install && \
|
||||
ldconfig /etc/ld.so.conf.d && \
|
||||
cp php.ini-development /usr/local/lib/php.ini && \
|
||||
echo "zend_extension=opcache.so" >> /usr/local/lib/php.ini && \
|
||||
echo "opcache.enable=1" >> /usr/local/lib/php.ini && \
|
||||
php --version
|
||||
|
||||
WORKDIR /go/src/app
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN cd caddy/frankenphp && \
|
||||
go build
|
||||
WORKDIR /go/src/app/caddy/frankenphp
|
||||
RUN go build
|
||||
|
||||
WORKDIR /go/src/app
|
||||
CMD [ "zsh" ]
|
||||
|
||||
111
dev.Dockerfile
111
dev.Dockerfile
@@ -3,66 +3,69 @@ FROM golang:1.21
|
||||
|
||||
ENV CFLAGS="-ggdb3"
|
||||
ENV PHPIZE_DEPS \
|
||||
autoconf \
|
||||
dpkg-dev \
|
||||
file \
|
||||
g++ \
|
||||
gcc \
|
||||
libc-dev \
|
||||
make \
|
||||
pkg-config \
|
||||
re2c
|
||||
autoconf \
|
||||
dpkg-dev \
|
||||
file \
|
||||
g++ \
|
||||
gcc \
|
||||
libc-dev \
|
||||
make \
|
||||
pkg-config \
|
||||
re2c
|
||||
|
||||
# hadolint ignore=DL3009
|
||||
RUN apt-get update && \
|
||||
apt-get -y --no-install-recommends install \
|
||||
$PHPIZE_DEPS \
|
||||
libargon2-dev \
|
||||
libcurl4-openssl-dev \
|
||||
libonig-dev \
|
||||
libreadline-dev \
|
||||
libsodium-dev \
|
||||
libsqlite3-dev \
|
||||
libssl-dev \
|
||||
libxml2-dev \
|
||||
zlib1g-dev \
|
||||
bison \
|
||||
libnss3-tools \
|
||||
# Dev tools \
|
||||
git \
|
||||
clang \
|
||||
llvm \
|
||||
gdb \
|
||||
valgrind \
|
||||
neovim \
|
||||
zsh \
|
||||
libtool-bin && \
|
||||
echo 'set auto-load safe-path /' > /root/.gdbinit && \
|
||||
echo '* soft core unlimited' >> /etc/security/limits.conf \
|
||||
&& \
|
||||
apt-get clean
|
||||
apt-get -y --no-install-recommends install \
|
||||
$PHPIZE_DEPS \
|
||||
libargon2-dev \
|
||||
libbrotli-dev \
|
||||
libcurl4-openssl-dev \
|
||||
libonig-dev \
|
||||
libreadline-dev \
|
||||
libsodium-dev \
|
||||
libsqlite3-dev \
|
||||
libssl-dev \
|
||||
libxml2-dev \
|
||||
zlib1g-dev \
|
||||
bison \
|
||||
libnss3-tools \
|
||||
# Dev tools \
|
||||
git \
|
||||
clang \
|
||||
llvm \
|
||||
gdb \
|
||||
valgrind \
|
||||
neovim \
|
||||
zsh \
|
||||
libtool-bin && \
|
||||
echo 'set auto-load safe-path /' > /root/.gdbinit && \
|
||||
echo '* soft core unlimited' >> /etc/security/limits.conf \
|
||||
&& \
|
||||
apt-get clean
|
||||
|
||||
RUN git clone --branch=PHP-8.2 https://github.com/php/php-src.git && \
|
||||
cd php-src && \
|
||||
# --enable-embed is only necessary to generate libphp.so, we don't use this SAPI directly
|
||||
./buildconf --force && \
|
||||
./configure \
|
||||
--enable-embed \
|
||||
--enable-zts \
|
||||
--disable-zend-signals \
|
||||
--enable-zend-max-execution-timers \
|
||||
--enable-debug && \
|
||||
make -j$(nproc) && \
|
||||
make install && \
|
||||
ldconfig && \
|
||||
cp php.ini-development /usr/local/lib/php.ini && \
|
||||
echo "zend_extension=opcache.so\nopcache.enable=1" >> /usr/local/lib/php.ini &&\
|
||||
php --version
|
||||
WORKDIR /usr/local/src/php
|
||||
RUN git clone --branch=PHP-8.3 https://github.com/php/php-src.git . && \
|
||||
# --enable-embed is only necessary to generate libphp.so, we don't use this SAPI directly
|
||||
./buildconf --force && \
|
||||
./configure \
|
||||
--enable-embed \
|
||||
--enable-zts \
|
||||
--disable-zend-signals \
|
||||
--enable-zend-max-execution-timers \
|
||||
--enable-debug && \
|
||||
make -j"$(nproc)" && \
|
||||
make install && \
|
||||
ldconfig && \
|
||||
cp php.ini-development /usr/local/lib/php.ini && \
|
||||
echo "zend_extension=opcache.so" >> /usr/local/lib/php.ini && \
|
||||
echo "opcache.enable=1" >> /usr/local/lib/php.ini && \
|
||||
php --version
|
||||
|
||||
WORKDIR /go/src/app
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN cd caddy/frankenphp && \
|
||||
go build
|
||||
WORKDIR /go/src/app/caddy/frankenphp
|
||||
RUN go build
|
||||
|
||||
WORKDIR /go/src/app
|
||||
CMD [ "zsh" ]
|
||||
|
||||
@@ -6,6 +6,10 @@ variable "VERSION" {
|
||||
default = "dev"
|
||||
}
|
||||
|
||||
variable "PHP_VERSION" {
|
||||
default = "8.2,8.3"
|
||||
}
|
||||
|
||||
variable "GO_VERSION" {
|
||||
default = "1.21"
|
||||
}
|
||||
@@ -20,13 +24,17 @@ variable "CACHE" {
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable DEFAULT_PHP_VERSION {
|
||||
default = "8.3"
|
||||
}
|
||||
|
||||
function "tag" {
|
||||
params = [version, os, php-version, tgt]
|
||||
result = [
|
||||
version != "" ? format("%s:%s%s-php%s-%s", IMAGE_NAME, version, tgt == "builder" ? "-builder" : "", php-version, os) : "",
|
||||
php-version == "8.2" && os == "bookworm" && version != "" ? format("%s:%s%s", IMAGE_NAME, version, tgt == "builder" ? "-builder" : "") : "",
|
||||
php-version == "8.2" && version != "" ? format("%s:%s%s-%s", IMAGE_NAME, version, tgt == "builder" ? "-builder" : "", os) : "",
|
||||
php-version == "8.2" && version == "latest" ? format("%s:%s%s", IMAGE_NAME, os, tgt == "builder" ? "-builder" : "") : "",
|
||||
php-version == DEFAULT_PHP_VERSION && os == "bookworm" && version != "" ? format("%s:%s%s", IMAGE_NAME, version, tgt == "builder" ? "-builder" : "") : "",
|
||||
php-version == DEFAULT_PHP_VERSION && version != "" ? format("%s:%s%s-%s", IMAGE_NAME, version, tgt == "builder" ? "-builder" : "", os) : "",
|
||||
php-version == DEFAULT_PHP_VERSION && version == "latest" ? format("%s:%s%s", IMAGE_NAME, os, tgt == "builder" ? "-builder" : "") : "",
|
||||
os == "bookworm" && version != "" ? format("%s:%s%s-php%s", IMAGE_NAME, version, tgt == "builder" ? "-builder" : "", php-version) : "",
|
||||
]
|
||||
}
|
||||
@@ -55,11 +63,21 @@ function "__semver" {
|
||||
result = v == {} ? [clean_tag(VERSION)] : v.prerelease == null ? ["latest", v.major, "${v.major}.${v.minor}", "${v.major}.${v.minor}.${v.patch}"] : ["${v.major}.${v.minor}.${v.patch}-${v.prerelease}"]
|
||||
}
|
||||
|
||||
function "php_version" {
|
||||
params = [v]
|
||||
result = _php_version(v, regexall("(?P<major>\\d+)\\.(?P<minor>\\d+)", v)[0])
|
||||
}
|
||||
|
||||
function "_php_version" {
|
||||
params = [v, m]
|
||||
result = "${m.major}.${m.minor}" == DEFAULT_PHP_VERSION ? [v, "${m.major}.${m.minor}", "${m.major}"] : [v, "${m.major}.${m.minor}"]
|
||||
}
|
||||
|
||||
target "default" {
|
||||
name = "${tgt}-php-${replace(php-version, ".", "-")}-${os}"
|
||||
matrix = {
|
||||
os = ["bookworm", "alpine"]
|
||||
php-version = ["8.2", "8.3.0RC2"]
|
||||
php-version = split(",", PHP_VERSION)
|
||||
tgt = ["builder", "runner"]
|
||||
}
|
||||
contexts = {
|
||||
@@ -69,17 +87,25 @@ target "default" {
|
||||
dockerfile = os == "alpine" ? "alpine.Dockerfile" : "Dockerfile"
|
||||
context = "./"
|
||||
target = tgt
|
||||
platforms = [
|
||||
# arm/v6 is only available for Alpine: https://github.com/docker-library/golang/issues/502
|
||||
platforms = os == "alpine" ? [
|
||||
"linux/amd64",
|
||||
"linux/386",
|
||||
"linux/arm/v6",
|
||||
"linux/arm/v7",
|
||||
"linux/arm64",
|
||||
] : [
|
||||
"linux/amd64",
|
||||
"linux/386",
|
||||
"linux/arm/v7",
|
||||
"linux/arm64"
|
||||
]
|
||||
tags = distinct(flatten([
|
||||
LATEST ? tag("latest", os, php-version, tgt) : [],
|
||||
tag(SHA == "" ? "" : "sha-${substr(SHA, 0, 7)}", os, php-version, tgt),
|
||||
[for v in semver(VERSION) : tag(v, os, php-version, tgt)]
|
||||
tags = distinct(flatten(
|
||||
[for pv in php_version(php-version) : flatten([
|
||||
LATEST ? tag("latest", os, pv, tgt) : [],
|
||||
tag(SHA == "" ? "" : "sha-${substr(SHA, 0, 7)}", os, pv, tgt),
|
||||
[for v in semver(VERSION) : tag(v, os, pv, tgt)]
|
||||
])
|
||||
]))
|
||||
labels = {
|
||||
"org.opencontainers.image.created" = "${timestamp()}"
|
||||
@@ -97,7 +123,20 @@ target "static-builder" {
|
||||
}
|
||||
dockerfile = "static-builder.Dockerfile"
|
||||
context = "./"
|
||||
tags = ["${IMAGE_NAME}:static-builder"]
|
||||
platforms = [
|
||||
"linux/amd64",
|
||||
"linux/arm64",
|
||||
]
|
||||
tags = distinct(flatten([
|
||||
LATEST ? "${IMAGE_NAME}:static-builder" : "",
|
||||
SHA == "" ? "" : "${IMAGE_NAME}:static-builder-sha-${substr(SHA, 0, 7)}",
|
||||
[for v in semver(VERSION) : v == "latest" ? "${IMAGE_NAME}:static-builder": "${IMAGE_NAME}:static-builder-${v}"]
|
||||
]))
|
||||
labels = {
|
||||
"org.opencontainers.image.created" = "${timestamp()}"
|
||||
"org.opencontainers.image.version" = VERSION
|
||||
"org.opencontainers.image.revision" = SHA
|
||||
}
|
||||
args = {
|
||||
FRANKENPHP_VERSION = VERSION
|
||||
}
|
||||
|
||||
@@ -68,12 +68,37 @@ make -j$(sysctl -n hw.logicalcpu)
|
||||
sudo make install
|
||||
```
|
||||
|
||||
#### Compile the Go App
|
||||
## Compile the Go App
|
||||
|
||||
You can now use the Go library and compile our Caddy build:
|
||||
|
||||
```
|
||||
```console
|
||||
curl -L https://github.com/dunglas/frankenphp/archive/refs/heads/main.tar.gz | tar x
|
||||
cd frankenphp-main/caddy/frankenphp
|
||||
CGO_CFLAGS=$(php-config --includes) go build
|
||||
CGO_CFLAGS=$(php-config --includes) CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)" go build
|
||||
```
|
||||
|
||||
### Using xcaddy
|
||||
|
||||
Alternatively, use [xcaddy](https://github.com/caddyserver/xcaddy) to compile FrankenPHP with [custom Caddy modules](https://caddyserver.com/docs/modules/):
|
||||
|
||||
```console
|
||||
CGO_ENABLED=1 \
|
||||
XCADDY_GO_BUILD_FLAGS="-ldflags '-w -s'" \
|
||||
xcaddy build \
|
||||
--output frankenphp \
|
||||
--with github.com/dunglas/frankenphp/caddy \
|
||||
--with github.com/dunglas/mercure/caddy \
|
||||
--with github.com/dunglas/vulcain/caddy
|
||||
# Add extra Caddy modules here
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
>
|
||||
> If you're using musl libc (the default on Alpine Linux) and Symfony,
|
||||
> you may need to increase the default stack size.
|
||||
> Otherwise, you may get errors like `PHP Fatal error: Maximum call stack size of 83360 bytes reached during compilation. Try splitting expression`
|
||||
>
|
||||
> To do so, change the `XCADDY_GO_BUILD_FLAGS` environment variable to something like
|
||||
> `XCADDY_GO_BUILD_FLAGS=$'-ldflags "-w -s -extldflags \'-Wl,-z,stack-size=0x80000\'"'`
|
||||
> (change the value of the stack size according to your app needs).
|
||||
|
||||
136
docs/config.md
136
docs/config.md
@@ -1,85 +1,157 @@
|
||||
# Configuration
|
||||
|
||||
FrankenPHP, Caddy as well the Mercure and Vulcain modules can be configured using [the formats supported by Caddy](https://caddyserver.com/docs/getting-started#your-first-config).
|
||||
FrankenPHP, Caddy as well as the Mercure and Vulcain modules can be configured using [the formats supported by Caddy](https://caddyserver.com/docs/getting-started#your-first-config).
|
||||
|
||||
In the Docker image, the `Caddyfile` is located at `/etc/Caddyfile`.
|
||||
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 located at `/usr/local/lib/php.ini`.
|
||||
In the Docker image, the `php.ini` file is not present, you can create it or `COPY` manually.
|
||||
|
||||
## Caddy Directives
|
||||
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.
|
||||
|
||||
To register the FrankenPHP executor, the `frankenphp` directive must be set in Caddy global options, then the `php` HTTP directive must be set under routes serving PHP scripts:
|
||||
```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;
|
||||
```
|
||||
|
||||
Then, you can use the `php` HTTP directive to execute PHP scripts:
|
||||
## Caddyfile Config
|
||||
|
||||
To register the FrankenPHP executor, the `frankenphp` [global option](https://caddyserver.com/docs/caddyfile/concepts#global-options) must be set, then the `php_server` or the `php` [HTTP directives](https://caddyserver.com/docs/caddyfile/concepts#directives) may be used within the site blocks to serve your PHP app.
|
||||
|
||||
Minimal example:
|
||||
|
||||
```caddyfile
|
||||
{
|
||||
frankenphp
|
||||
# Enable FrankenPHP
|
||||
frankenphp
|
||||
# Configure when the directive must be executed
|
||||
order php_server before file_server
|
||||
}
|
||||
|
||||
localhost {
|
||||
route {
|
||||
php {
|
||||
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.
|
||||
env <key> <value> # Sets an extra environment variable to the given value. Can be specified more than once for multiple environment variables.
|
||||
}
|
||||
}
|
||||
# Enable compression (optional)
|
||||
encode zstd br gzip
|
||||
# Execute PHP files in the current directory and serve assets
|
||||
php_server
|
||||
}
|
||||
```
|
||||
|
||||
Optionnaly, the number of threads to create and [worker scripts](worker.md) to start with the server can be specified under the global directive.
|
||||
Optionally, the number of threads to create and [worker scripts](worker.md) to start with the server can be specified under the global option.
|
||||
|
||||
```caddyfile
|
||||
{
|
||||
frankenphp {
|
||||
num_threads <num_threads> # Sets the number of PHP threads to start. Default: 2x the number of available CPUs.
|
||||
worker {
|
||||
file <path> # Sets the path to the worker script.
|
||||
num <num> # Sets the number of PHP threads to start, defaults to 2x the number of available CPUs.
|
||||
env <key> <value> # Sets an extra environment variable to the given value. Can be specified more than once for multiple environment variables.
|
||||
}
|
||||
}
|
||||
frankenphp {
|
||||
num_threads <num_threads> # Sets the number of PHP threads to start. Default: 2x the number of available CPUs.
|
||||
worker {
|
||||
file <path> # Sets the path to the worker script.
|
||||
num <num> # Sets the number of PHP threads to start, defaults to 2x the number of available CPUs.
|
||||
env <key> <value> # Sets an extra environment variable to the given value. Can be specified more than once for multiple environment variables.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ...
|
||||
```
|
||||
|
||||
Alternatively, the short form of the `worker` directive can also be used:
|
||||
Alternatively, you may use the one-line short form of the `worker` option:
|
||||
|
||||
```caddyfile
|
||||
{
|
||||
frankenphp {
|
||||
worker <file> <num>
|
||||
}
|
||||
frankenphp {
|
||||
worker <file> <num>
|
||||
}
|
||||
}
|
||||
|
||||
# ...
|
||||
```
|
||||
|
||||
You can also define multiple workers if you serve multiple apps on the same server:
|
||||
|
||||
```caddyfile
|
||||
{
|
||||
frankenphp {
|
||||
worker /path/to/app/public/index.php <num>
|
||||
worker /path/to/other/public/index.php <num>
|
||||
}
|
||||
}
|
||||
|
||||
app.example.com {
|
||||
root * /path/to/app/public
|
||||
php_server
|
||||
}
|
||||
|
||||
other.example.com {
|
||||
root * /path/to/other/public
|
||||
php_server
|
||||
}
|
||||
...
|
||||
```
|
||||
|
||||
Using the `php_server` directive is generally what you need,
|
||||
but if you need full control, you can use the lower level `php` directive:
|
||||
|
||||
Using the `php_server` directive is equivalent to this configuration:
|
||||
|
||||
```caddyfile
|
||||
route {
|
||||
# Add trailing slash for directory requests
|
||||
@canonicalPath {
|
||||
file {path}/index.php
|
||||
not path */
|
||||
}
|
||||
redir @canonicalPath {path}/ 308
|
||||
# If the requested file does not exist, try index files
|
||||
@indexFiles file {
|
||||
try_files {path} {path}/index.php index.php
|
||||
split_path .php
|
||||
}
|
||||
rewrite @indexFiles {http.matchers.file.relative}
|
||||
# FrankenPHP!
|
||||
@phpFiles path *.php
|
||||
php @phpFiles
|
||||
file_server
|
||||
}
|
||||
```
|
||||
|
||||
The `php_server` and the `php` directives have the following options:
|
||||
|
||||
```caddyfile
|
||||
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.
|
||||
env <key> <value> # Sets an extra environment variable to the given value. Can be specified more than once for multiple environment variables.
|
||||
}
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
The following environment variables can be used to inject Caddy directives in the `Caddyfile` without modifying it:
|
||||
|
||||
* `SERVER_NAME` change the server name
|
||||
* `SERVER_NAME`: change [the addresses on which to listen](https://caddyserver.com/docs/caddyfile/concepts#addresses), the provided hostnames will also be used for the generated TLS certificate
|
||||
* `CADDY_GLOBAL_OPTIONS`: inject [global options](https://caddyserver.com/docs/caddyfile/options)
|
||||
* `FRANKENPHP_CONFIG`: inject config under the `frankenphp` directive
|
||||
|
||||
Unlike with FPM and CLI SAPIs, environment variables are **not** exposed by default in superglobals `$_SERVER` and `$_ENV`.
|
||||
|
||||
To propagate environment variables to `$_SERVER` and `$_ENV`, set the `php.ini` `variables_order` directive to `EGPS`.
|
||||
To propagate environment variables to `$_SERVER` and `$_ENV`, set the `php.ini` `variables_order` directive to `EGPCS`.
|
||||
|
||||
## PHP config
|
||||
|
||||
To load [additional PHP configuration files](https://www.php.net/manual/en/configuration.file.php#configuration.file.scan),
|
||||
the `PHP_INI_SCAN_DIR` environment variable can be used.
|
||||
When set, PHP will load all the file with the `.ini` extension present in the given directories.
|
||||
|
||||
## Enable the Debug Mode
|
||||
|
||||
When using the Docker image, set the `CADDY_DEBUG` environment variable to `debug` to enable the debug mode:
|
||||
When using the Docker image, set the `CADDY_GLOBAL_OPTIONS` environment variable to `debug` to enable the debug mode:
|
||||
|
||||
```console
|
||||
docker run -v $PWD:/app/public \
|
||||
-e CADDY_GLOBAL_OPTIONS=debug \
|
||||
-p 80:80 -p 443:443 \
|
||||
-p 80:80 -p 443:443 -p 443:443/udp \
|
||||
dunglas/frankenphp
|
||||
```
|
||||
|
||||
BIN
docs/digitalocean-dns.png
Normal file
BIN
docs/digitalocean-dns.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 126 KiB |
BIN
docs/digitalocean-droplet.png
Normal file
BIN
docs/digitalocean-droplet.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 159 KiB |
119
docs/docker.md
119
docs/docker.md
@@ -1,6 +1,6 @@
|
||||
# Building Custom Docker Image
|
||||
|
||||
[FrankenPHP Docker images](https://hub.docker.com/r/dunglas/frankenphp) are based on [official PHP images](https://hub.docker.com/_/php/). Alpine Linux and Debian variants are provided for popular architectures. Variants for PHP 8.2 and PHP 8.3 are provided. [Browse tags](https://hub.docker.com/repository/docker/dunglas/frankenphp).
|
||||
[FrankenPHP Docker images](https://hub.docker.com/r/dunglas/frankenphp) are based on [official PHP images](https://hub.docker.com/_/php/). Alpine Linux and Debian variants are provided for popular architectures. Variants for PHP 8.2 and PHP 8.3 are provided. [Browse tags](https://hub.docker.com/r/dunglas/frankenphp/tags).
|
||||
|
||||
## How to Use The Images
|
||||
|
||||
@@ -15,30 +15,65 @@ COPY . /app/public
|
||||
Then, run the commands to build and run the Docker image:
|
||||
|
||||
```console
|
||||
$ docker build -t my-php-app .
|
||||
$ docker run -it --rm --name my-running-app my-php-app
|
||||
docker build -t my-php-app .
|
||||
docker run -it --rm --name my-running-app my-php-app
|
||||
```
|
||||
|
||||
## How to Install More PHP Extensions
|
||||
|
||||
The [`docker-php-extension-installer`](https://github.com/mlocati/docker-php-extension-installer) script is provided in the base image.
|
||||
Adding additional PHP extensions is straightforwardd:
|
||||
Adding additional PHP extensions is straightforward:
|
||||
|
||||
```dockerfile
|
||||
FROM dunglas/frankenphp
|
||||
|
||||
# add additional extensions here:
|
||||
RUN install-php-extensions \
|
||||
pdo_mysql \
|
||||
gd \
|
||||
intl \
|
||||
zip \
|
||||
opcache
|
||||
|
||||
# ...
|
||||
pdo_mysql \
|
||||
gd \
|
||||
intl \
|
||||
zip \
|
||||
opcache
|
||||
```
|
||||
|
||||
# Enabling the Worker Mode by Default
|
||||
## How to Install More Caddy Modules
|
||||
|
||||
FrankenPHP is built on top of Caddy, and all [Caddy modules](https://caddyserver.com/docs/modules/) can be used with FrankenPHP.
|
||||
|
||||
The easiest way to install custom Caddy modules is to use [xcaddy](https://github.com/caddyserver/xcaddy):
|
||||
|
||||
```dockerfile
|
||||
FROM dunglas/frankenphp:latest-builder AS builder
|
||||
|
||||
# Copy xcaddy in the builder image
|
||||
COPY --from=caddy:builder /usr/bin/xcaddy /usr/bin/xcaddy
|
||||
|
||||
# CGO must be enabled to build FrankenPHP
|
||||
ENV CGO_ENABLED=1 XCADDY_SETCAP=1 XCADDY_GO_BUILD_FLAGS="-ldflags '-w -s'"
|
||||
RUN xcaddy build \
|
||||
--output /usr/local/bin/frankenphp \
|
||||
--with github.com/dunglas/frankenphp=./ \
|
||||
--with github.com/dunglas/frankenphp/caddy=./caddy/ \
|
||||
# Mercure and Vulcain are included in the official build, but feel free to remove them
|
||||
--with github.com/dunglas/mercure/caddy \
|
||||
--with github.com/dunglas/vulcain/caddy
|
||||
# Add extra Caddy modules here
|
||||
|
||||
FROM dunglas/frankenphp AS runner
|
||||
|
||||
# Replace the official binary by the one contained your custom modules
|
||||
COPY --from=builder /usr/local/bin/frankenphp /usr/local/bin/frankenphp
|
||||
```
|
||||
|
||||
The `builder` image provided by FrankenPHP contains a compiled version of libphp.
|
||||
[Builders images](https://hub.docker.com/r/dunglas/frankenphp/tags?name=builder) are provided for all versions of FrankenPHP and PHP, both for Alpine and Debian.
|
||||
|
||||
> [!TIP]
|
||||
>
|
||||
> If you're using Alpine Linux and Symfony,
|
||||
> you may need to [increase the default stack size](compile.md#using-xcaddy).
|
||||
|
||||
## Enabling the Worker Mode by Default
|
||||
|
||||
Set the `FRANKENPHP_CONFIG` environment variable to start FrankenPHP with a worker script:
|
||||
|
||||
@@ -50,20 +85,22 @@ FROM dunglas/frankenphp
|
||||
ENV FRANKENPHP_CONFIG="worker ./public/index.php"
|
||||
```
|
||||
|
||||
# Using a Volume in Development
|
||||
## Using a Volume in Development
|
||||
|
||||
To develop easily with FrankenPHP, mount the directory from your host containing the source code of the app as a volume in the Docker container:
|
||||
|
||||
```console
|
||||
docker run -v $PWD:/app/public -p 80:80 -p 443:443 my-php-app
|
||||
docker run -v $PWD:/app/public -p 80:80 -p 443:443 -p 443:443/udp --tty my-php-app
|
||||
```
|
||||
|
||||
> ![TIP]
|
||||
>
|
||||
> The `--tty` option allows to have nice human-readable logs instead of JSON logs.
|
||||
|
||||
With Docker Compose:
|
||||
|
||||
```yaml
|
||||
# compose.yml
|
||||
|
||||
version: '3.1'
|
||||
# compose.yaml
|
||||
|
||||
services:
|
||||
php:
|
||||
@@ -73,8 +110,52 @@ services:
|
||||
# uncomment the following line if you want to run this in a production environment
|
||||
# restart: always
|
||||
ports:
|
||||
- 80:80
|
||||
- 443:443
|
||||
- "80:80" # HTTP
|
||||
- "443:443" # HTTPS
|
||||
- "443:443/udp" # HTTP/3
|
||||
volumes:
|
||||
- ./:/app/public
|
||||
- caddy_data:/data
|
||||
- caddy_config:/config
|
||||
# comment the following line in production, it allows to have nice human-readable logs in dev
|
||||
tty: true
|
||||
|
||||
# Volumes needed for Caddy certificates and configuration
|
||||
volumes:
|
||||
caddy_data:
|
||||
caddy_config:
|
||||
```
|
||||
|
||||
## Running as a Non-Root User
|
||||
|
||||
FrankenPHP can run as non root user in Docker.
|
||||
|
||||
Here is a sample `Dockerfile` doing this:
|
||||
|
||||
```dockerfile
|
||||
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
|
||||
```
|
||||
|
||||
## 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.
|
||||
|
||||
127
docs/embed.md
Normal file
127
docs/embed.md
Normal file
@@ -0,0 +1,127 @@
|
||||
# PHP Apps As Standalone Binaries
|
||||
|
||||
FrankenPHP has the ability to embed the source code and assets of PHP applications in a static, self-contained binary.
|
||||
|
||||
Thanks to this feature, PHP applications can be distributed as standalone binaries that include the application itself, the PHP interpreter and Caddy, a production-level web server.
|
||||
|
||||
Learn more about this feature [in the presentation made by Kévin at SymfonyCon](https://dunglas.dev/2023/12/php-and-symfony-apps-as-standalone-binaries/).
|
||||
|
||||
## Preparing Your App
|
||||
|
||||
Before creating the self-contained binary be sure that your app is ready for embedding.
|
||||
|
||||
For instance you likely want to:
|
||||
|
||||
* Install the production dependencies of the app
|
||||
* Dump the autoloader
|
||||
* Enable the production mode of your application (if any)
|
||||
* Strip uneeded files such as `.git` or tests to reduce the size of your final binary
|
||||
|
||||
For instance, for a Symfony app, you can use the following commands:
|
||||
|
||||
```console
|
||||
# Export the project to get rid of .git/, etc
|
||||
mkdir $TMPDIR/my-prepared-app
|
||||
git archive HEAD | tar -x -C $TMPDIR/my-prepared-app
|
||||
cd $TMPDIR/my-prepared-app
|
||||
|
||||
# Set proper environment variables
|
||||
echo APP_ENV=prod > .env.local
|
||||
echo APP_DEBUG=0 >> .env.local
|
||||
|
||||
# Remove the tests
|
||||
rm -Rf tests/
|
||||
|
||||
# Install the dependencies
|
||||
composer install --ignore-platform-reqs --no-dev -a
|
||||
|
||||
# Optimize .env
|
||||
composer dump-env prod
|
||||
```
|
||||
|
||||
## Creating a Linux Binary
|
||||
|
||||
The easiest way to create a Linux binary is to use the Docker-based builder we provide.
|
||||
|
||||
1. Create a file named `static-build.Dockerfile` in the repository of your prepared app:
|
||||
|
||||
```dockerfile
|
||||
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder
|
||||
|
||||
# Copy your app
|
||||
WORKDIR /go/src/app/dist/app
|
||||
COPY . .
|
||||
|
||||
# Build the static binary, be sure to select only the PHP extensions you want
|
||||
WORKDIR /go/src/app/
|
||||
RUN EMBED=dist/app/ \
|
||||
PHP_EXTENSIONS=ctype,iconv,pdo_sqlite \
|
||||
./build-static.sh
|
||||
```
|
||||
|
||||
2. Build:
|
||||
|
||||
```console
|
||||
docker build -t static-app -f static-build.Dockerfile .
|
||||
```
|
||||
|
||||
3. Extract the binary
|
||||
|
||||
```console
|
||||
docker cp $(docker create --name static-app-tmp static-app):/go/src/app/dist/frankenphp-linux-x86_64 my-app ; docker rm static-app-tmp
|
||||
```
|
||||
|
||||
The resulting binary is the file named `my-app` in the current directory.
|
||||
|
||||
## Creating a Binary for Other OSes
|
||||
|
||||
If you don't want to use Docker, or want to build a macOS binary, use the shell script we provide:
|
||||
|
||||
```console
|
||||
git clone https://github.com/dunglas/frankenphp
|
||||
cd frankenphp
|
||||
EMBED=/path/to/your/app \
|
||||
PHP_EXTENSIONS=ctype,iconv,pdo_sqlite \
|
||||
./build-static.sh
|
||||
```
|
||||
|
||||
The resulting binary is the file named `frankenphp-<os>-<arch>` in the `dist/` directory.
|
||||
|
||||
## Using The Binary
|
||||
|
||||
This is it! The `my-app` file (or `dist/frankenphp-<os>-<arch>` on other OSes) contains your self-contained app!
|
||||
|
||||
To start the web app run:
|
||||
|
||||
```console
|
||||
./my-app php-server
|
||||
```
|
||||
|
||||
If your app contains a [worker script](worker.md), start the worker with something like:
|
||||
|
||||
```console
|
||||
./my-app php-server --worker public/index.php
|
||||
```
|
||||
|
||||
To enable HTTPS (a Let's Encrypt certificate is automatically created), HTTP/2 and HTTP/3, specify the domain name to use:
|
||||
|
||||
```console
|
||||
./my-app php-server --domain localhost
|
||||
```
|
||||
|
||||
You can also run the PHP CLI scripts embedded in your binary:
|
||||
|
||||
```console
|
||||
./my-app php-cli bin/console
|
||||
```
|
||||
|
||||
## Customizing The Build
|
||||
|
||||
[Read the static build documentation](static.md) to see how to customize the binary (extensions, PHP version...).
|
||||
|
||||
## Distributing The Binary
|
||||
|
||||
The created binary isn't compressed.
|
||||
To reduce the size of the file before sending it, you can compress it.
|
||||
|
||||
We recommend `xz`.
|
||||
98
docs/known-issues.md
Normal file
98
docs/known-issues.md
Normal file
@@ -0,0 +1,98 @@
|
||||
# Known Issues
|
||||
|
||||
## Fibers
|
||||
|
||||
Calling PHP functions and language constructs that themselves call [cgo](https://go.dev/blog/cgo) in [Fibers](https://www.php.net/manual/en/language.fibers.php) is known to cause crashes.
|
||||
|
||||
This issue [is being worked on by the Go project](https://github.com/golang/go/issues/62130).
|
||||
|
||||
In the meantime, one solution is not to use constructs (like `echo`) and functions (like `header()`) that delegate to Go from inside Fibers.
|
||||
|
||||
This code will likely crash because it uses `echo` in the Fiber:
|
||||
|
||||
```php
|
||||
$fiber = new Fiber(function() {
|
||||
echo 'In the Fiber'.PHP_EOL;
|
||||
echo 'Still inside'.PHP_EOL;
|
||||
});
|
||||
$fiber->start();
|
||||
```
|
||||
|
||||
Instead, return the value from the Fiber and use it outside:
|
||||
|
||||
```php
|
||||
$fiber = new Fiber(function() {
|
||||
Fiber::suspend('In the Fiber'.PHP_EOL));
|
||||
Fiber::suspend('Still inside'.PHP_EOL));
|
||||
});
|
||||
echo $fiber->start();
|
||||
echo $fiber->resume();
|
||||
$fiber->resume();
|
||||
```
|
||||
|
||||
## Unsupported PHP Extensions
|
||||
|
||||
The following extensions are known not to be compatible with FrankenPHP:
|
||||
|
||||
| Name | Reason | Alternatives |
|
||||
| ----------------------------------------------------------- | --------------- | -------------------------------------------------------------------------------------------------------------------- |
|
||||
| [imap](https://www.php.net/manual/en/imap.installation.php) | Not thread-safe | [javanile/php-imap2](https://github.com/javanile/php-imap2), [webklex/php-imap](https://github.com/Webklex/php-imap) |
|
||||
|
||||
## get_browser
|
||||
|
||||
The [get_browser()](https://www.php.net/manual/en/function.get-browser.php) function seems to perform badly after a while. A workaround is to cache (e.g. with APCU) the results per User Agent, as they are static.
|
||||
|
||||
## Standalone Binary and Alpine-based Docker Images
|
||||
|
||||
The standalone binary and Alpine-based docker images (`dunglas/frankenphp:*-alpine`) use [musl libc](https://musl.libc.org/) instead of [glibc and friends](https://www.etalabs.net/compare_libcs.html), to keep a smaller binary size. This may lead to some compatibility issues. In particular, the glob flag `GLOB_BRACE` is [not available](https://www.php.net/manual/en/function.glob.php)
|
||||
|
||||
## Using `https://127.0.0.1` with Docker
|
||||
|
||||
By default, FrankenPHP generates a TLS certificate for `localhost`.
|
||||
It's the easiest and recommended option for local development.
|
||||
|
||||
If you really want to use `127.0.0.1` as a host instead, it's possible to configure it to generate a certificate for it by setting the server name to `127.0.0.1`.
|
||||
|
||||
Unfortunately, this is not enough when using Docker because of [its networking system](https://docs.docker.com/network/).
|
||||
You will get a TLS error similar to `curl: (35) LibreSSL/3.3.6: error:1404B438:SSL routines:ST_CONNECT:tlsv1 alert internal error`.
|
||||
|
||||
If you're using Linux, a solution is to use [the host networking driver](https://docs.docker.com/network/network-tutorial-host/):
|
||||
|
||||
```console
|
||||
docker run \
|
||||
-e SERVER_NAME="127.0.0.1" \
|
||||
-v $PWD:/app/public \
|
||||
--network host \
|
||||
dunglas/frankenphp
|
||||
```
|
||||
|
||||
The host networking driver isn't supported on Mac and Windows. On these platforms, you will have to guess the IP address of the container and include it in the server names.
|
||||
|
||||
Run the `docker network inspect bridge` and look at the `Containers` key to identify the last currently assigned IP address under the `IPv4Address` key, and increment it by one. If no container is running, the first assigned IP address is usually `172.17.0.2`.
|
||||
|
||||
Then, include this in the `SERVER_NAME` environment variable:
|
||||
|
||||
```console
|
||||
docker run \
|
||||
-e SERVER_NAME="127.0.0.1, 172.17.0.3" \
|
||||
-v $PWD:/app/public \
|
||||
-p 80:80 -p 443:443 -p 443:443/udp \
|
||||
dunglas/frankenphp
|
||||
```
|
||||
|
||||
> ![CAUTION]
|
||||
>
|
||||
> Be sure to replace `172.17.0.3` with the IP that will be assigned to your container.
|
||||
|
||||
You should now be able to access `https://127.0.0.1` from the host machine.
|
||||
|
||||
If that's not the case, start FrankenPHP in debug mode to try to figure out the problem:
|
||||
|
||||
```console
|
||||
docker run \
|
||||
-e CADDY_GLOBAL_OPTIONS="debug"
|
||||
-e SERVER_NAME="127.0.0.1" \
|
||||
-v $PWD:/app/public \
|
||||
-p 80:80 -p 443:443 -p 443:443/udp \
|
||||
dunglas/frankenphp
|
||||
```
|
||||
78
docs/laravel.md
Normal file
78
docs/laravel.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# Laravel
|
||||
|
||||
## Docker
|
||||
|
||||
Serving a [Laravel](https://laravel.com) web application with FrankenPHP is as easy as mounting the project in the `/app` directory of the official Docker image.
|
||||
|
||||
Run this command from the main directory of your Laravel app:
|
||||
|
||||
```console
|
||||
docker run -p 80:80 -p 443:443 -p 443:443/udp -v $PWD:/app dunglas/frankenphp
|
||||
```
|
||||
|
||||
And enjoy!
|
||||
|
||||
## Local Installation
|
||||
|
||||
Alternatively, you can run your Laravel projects with FrankenPHP from your local machine:
|
||||
|
||||
1. [Download the binary corresponding to your system](https://github.com/dunglas/frankenphp/releases)
|
||||
2. Add the following configuration to a file named `Caddyfile` in the root directory of your Laravel project:
|
||||
|
||||
```caddyfile
|
||||
{
|
||||
frankenphp
|
||||
order php_server before file_server
|
||||
}
|
||||
|
||||
# The domain name of your server
|
||||
localhost {
|
||||
# Set the webroot to the public/ dir
|
||||
root * public/
|
||||
# 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
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. Start FrankenPHP from the root directory of your Laravel project: `./frankenphp run`
|
||||
|
||||
## Laravel Octane
|
||||
|
||||
Octane may be installed via the Composer package manager:
|
||||
|
||||
```console
|
||||
composer require laravel/octane
|
||||
```
|
||||
|
||||
After installing Octane, you may execute the `octane:install` Artisan command, which will install Octane's configuration file into your application:
|
||||
|
||||
```console
|
||||
php artisan octane:install --server=frankenphp
|
||||
```
|
||||
|
||||
The Octane server can be started via the `octane:start` Artisan command.
|
||||
|
||||
```console
|
||||
php artisan octane:start
|
||||
```
|
||||
|
||||
The `octane:start` command can take the following options:
|
||||
|
||||
* `--host`: The IP address the server should bind to (default: `127.0.0.1`)
|
||||
* `--port`: The port the server should be available on (default: `8000`)
|
||||
* `--admin-port`: The port the admin server should be available on (default: `2019`)
|
||||
* `--workers`: The number of workers that should be available to handle requests (default: `auto`)
|
||||
* `--max-requests`: The number of requests to process before reloading the server (default: `500`)
|
||||
* `--caddyfile`: The path to the FrankenPHP `Caddyfile` file
|
||||
* `--https`: Enable HTTPS, HTTP/2, and HTTP/3, and automatically generate and renew certificates
|
||||
* --http-redirect : Enable HTTP to HTTPS redirection (only enabled if --https is passed)
|
||||
* `--watch`: Automatically reload the server when the application is modified
|
||||
* `--poll`: Use file system polling while watching in order to watch files over a network
|
||||
* `--log-level`: Log messages at or above the specified log level
|
||||
|
||||
Learn more about [Laravel Octane in its official documentation](https://laravel.com/docs/octane).
|
||||
@@ -1,12 +1,12 @@
|
||||
# Real-time
|
||||
|
||||
FrankenPHP comes with a built-in Mercure hub!
|
||||
Mercure allows to push event in real-time to all the connected devices: they will receive instantly a JavaScript event.
|
||||
Mercure allows to push events in real-time to all the connected devices: they will receive a JavaScript event instantly.
|
||||
|
||||
No JS library or SDK required!
|
||||
|
||||

|
||||
|
||||
To enable the Mercure hub, update the `Caddyfile` as described [on Mercure's website](https://mercure.rocks/docs/hub/config).
|
||||
To enable the Mercure hub, update the `Caddyfile` as described [on Mercure's site](https://mercure.rocks/docs/hub/config).
|
||||
|
||||
To push Mercure updates from your code, we recommend the [Symfony Mercure Component](https://symfony.com/components/Mercure) (you don't need the Symfony full stack framework to use it).
|
||||
|
||||
138
docs/production.md
Normal file
138
docs/production.md
Normal file
@@ -0,0 +1,138 @@
|
||||
# Deploying in Production
|
||||
|
||||
In this tutorial, we will learn how to deploy a PHP application on a single server using Docker Compose.
|
||||
|
||||
If you're using Symfony, prefer reading the "[Deploy in production](https://github.com/dunglas/symfony-docker/blob/main/docs/production.md)" documentation entry of the Symfony Docker project (which uses FrankenPHP).
|
||||
|
||||
If you're using API Platform (which also uses FrankenPHP), refer to [the deployment documentation of the framework](https://api-platform.com/docs/deployment/).
|
||||
|
||||
## Preparing Your App
|
||||
|
||||
First, create a `Dockerfile` in the root directory of your PHP project:
|
||||
|
||||
```dockerfile
|
||||
FROM dunglas/frankenphp
|
||||
|
||||
# Be sure to replace "your-domain-name.example.com" by your domain name
|
||||
ENV SERVER_NAME=your-domain-name.example.com
|
||||
# If you want to disable HTTPS, use this value instead:
|
||||
#ENV SERVER_NAME=:80
|
||||
|
||||
# Enable PHP production settings
|
||||
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
|
||||
|
||||
# Copy the PHP files of your project in the public directory
|
||||
COPY . /app/public
|
||||
# If you use Symfony or Laravel, you need to copy the whole project instead:
|
||||
#COPY . /app
|
||||
```
|
||||
|
||||
Refer to "[Building Custom Docker Image](docker.md)" for more details and options,
|
||||
and to learn how to customize the configuration, install PHP extensions and Caddy modules.
|
||||
|
||||
If your project uses Composer,
|
||||
be sure to include it in the Docker image and to install your depedencies.
|
||||
|
||||
Then, add a `compose.yaml` file:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
php:
|
||||
image: dunglas/frankenphp
|
||||
restart: always
|
||||
ports:
|
||||
- "80:80" # HTTP
|
||||
- "443:443" # HTTPS
|
||||
- "443:443/udp" # HTTP/3
|
||||
volumes:
|
||||
- caddy_data:/data
|
||||
- caddy_config:/config
|
||||
|
||||
# Volumes needed for Caddy certificates and configuration
|
||||
volumes:
|
||||
caddy_data:
|
||||
caddy_config:
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> The previous examples are intended for production usage.
|
||||
> In development, you may want to use a volume, a different PHP configuration and a different value for the `SERVER_NAME` environment variable.
|
||||
>
|
||||
> Take a look to the [Symfony Docker](https://github.com/dunglas/symfony-docker) project
|
||||
> (which uses FrankenPHP) for a more advanced example using multi-stage images,
|
||||
> Composer, extra PHP extensions, etc.
|
||||
|
||||
Finally, if you use Git, commit these files and push.
|
||||
|
||||
## Preparing a Server
|
||||
|
||||
To deploy your application in production, you need a server.
|
||||
In this tutorial, we will use a virtual machine provided by DigitalOcean, but any Linux server can work.
|
||||
If you already have a Linux server with Docker installed, you can skip straight to [the next section](#configuring-a-domain-name).
|
||||
|
||||
Otherwise, use [this affiliate link](https://m.do.co/c/5d8aabe3ab80) to get $200 of free credit, create an account, then click on "Create a Droplet".
|
||||
Then, click on the "Marketplace" tab under the "Choose an image" section and search for the app named "Docker".
|
||||
This will provision an Ubuntu server with the latest versions of Docker and Docker Compose already installed!
|
||||
|
||||
For test purposes, the cheapest plans will be enough.
|
||||
For real production usage, you'll probably want to pick a plan in the "general purpose" section to fit your needs.
|
||||
|
||||

|
||||
|
||||
You can keep the defaults for other settings, or tweak them according to your needs.
|
||||
Don't forget to add your SSH key or create a password then press the "Finalize and create" button.
|
||||
|
||||
Then, wait a few seconds while your Droplet is provisioning.
|
||||
When your Droplet is ready, use SSH to connect:
|
||||
|
||||
```console
|
||||
ssh root@<droplet-ip>
|
||||
```
|
||||
|
||||
## Configuring a Domain Name
|
||||
|
||||
In most cases, you'll want to associate a domain name with your site.
|
||||
If you don't own a domain name yet, you'll have to buy one through a registrar.
|
||||
|
||||
Then create a DNS record of type `A` for your domain name pointing to the IP address of your server:
|
||||
|
||||
```dns
|
||||
your-domain-name.example.com. IN A 207.154.233.113
|
||||
```
|
||||
|
||||
Example with the DigitalOcean Domains service ("Networking" > "Domains"):
|
||||
|
||||

|
||||
|
||||
> [!NOTE]
|
||||
> Let's Encrypt, the service used by default by FrankenPHP to automatically generate a TLS certificate doesn't support using bare IP addresses. Using a domain name is mandatory to use Let's Encrypt.
|
||||
|
||||
## Deploying
|
||||
|
||||
Copy your project on the server using `git clone`, `scp`, or any other tool that may fit your need.
|
||||
If you use GitHub, you may want to use [a deploy key](https://docs.github.com/en/free-pro-team@latest/developers/overview/managing-deploy-keys#deploy-keys).
|
||||
Deploy keys are also [supported by GitLab](https://docs.gitlab.com/ee/user/project/deploy_keys/).
|
||||
|
||||
Example with Git:
|
||||
|
||||
```console
|
||||
git clone git@github.com:<username>/<project-name>.git
|
||||
```
|
||||
|
||||
Go into the directory containing your project (`<project-name>`), and start the app in production mode:
|
||||
|
||||
```console
|
||||
docker compose up -d --wait
|
||||
```
|
||||
|
||||
Your server is up and running, and a HTTPS certificate has been automatically generated for you.
|
||||
Go to `https://your-domain-name.example.com` and enjoy!
|
||||
|
||||
> [!CAUTION]
|
||||
> Docker can have a cache layer, make sure you have the right build for each deployment or rebuild your project with --no-cache option to avoid cache issue.
|
||||
|
||||
## Deploying on Multiple Nodes
|
||||
|
||||
If you want to deploy your app on a cluster of machines, you can use [Docker Swarm](https://docs.docker.com/engine/swarm/stack-deploy/),
|
||||
which is compatible with the provided Compose files.
|
||||
To deploy on Kubernetes, take a look at [the Helm chart provided with API Platform](https://api-platform.com/docs/deployment/kubernetes/), which can be easily adapted for use with Symfony Docker.
|
||||
@@ -5,18 +5,20 @@ it's possible to create a static build of FrankenPHP thanks to the great [static
|
||||
|
||||
With this method, a single, portable, binary will contain the PHP interpreter, the Caddy web server and FrankenPHP!
|
||||
|
||||
FrankenPHP also supports [embedding the PHP app in the static binary](embed.md).
|
||||
|
||||
## Linux
|
||||
|
||||
We provide a Docker image to build a Linux static binary:
|
||||
|
||||
```console
|
||||
docker buildx bake --load static-builder
|
||||
docker cp $(docker create --name static-builder dunglas/frankenphp:static-builder):/go/src/app/caddy/frankenphp/frankenphp frankenphp ; docker rm static-builder
|
||||
docker cp $(docker create --name static-builder dunglas/frankenphp:static-builder):/go/src/app/dist/frankenphp-linux-$(uname -m) frankenphp ; docker rm static-builder
|
||||
```
|
||||
|
||||
The resulting static binary is named `frankenphp` and is available in the current directory.
|
||||
|
||||
If you want to build the static binary without Docker, take a look to the `static-builder.Dockerfile` file.
|
||||
If you want to build the static binary without Docker, take a look at the macOS instructions, which also works for Linux.
|
||||
|
||||
### Custom Extensions
|
||||
|
||||
@@ -31,7 +33,17 @@ docker buildx bake --load --set static-builder.args.PHP_EXTENSIONS=opcache,pdo_s
|
||||
# ...
|
||||
```
|
||||
|
||||
See [the list of supported extensions](https://static-php-cli.zhamao.me/en/guide/extensions.html).
|
||||
To add libraries enabling additional functionality to the extensions you've enabled, you can pass use the `PHP_EXTENSION_LIBS` Docker ARG:
|
||||
|
||||
```console
|
||||
docker buildx bake \
|
||||
--load \
|
||||
--set static-builder.args.PHP_EXTENSIONS=gd \
|
||||
--set static-builder.args.PHP_EXTENSION_LIBS=libjpeg,libwebp \
|
||||
static-builder
|
||||
```
|
||||
|
||||
See also: [customizing the build](#customizing-the-build)
|
||||
|
||||
### GitHub Token
|
||||
|
||||
@@ -44,21 +56,26 @@ GITHUB_TOKEN="xxx" docker --load buildx bake static-builder
|
||||
|
||||
## macOS
|
||||
|
||||
Run the following command to create a static binary for macOS:
|
||||
Run the following script to create a static binary for macOS (you must have [Homebrew](https://brew.sh/) installed):
|
||||
|
||||
```console
|
||||
git clone --depth=1 https://github.com/crazywhalecc/static-php-cli.git
|
||||
cd static-php-cli
|
||||
composer install --no-dev -a
|
||||
./bin/spc doctor --auto-fix
|
||||
./bin/spc fetch --with-php=8.2 -A
|
||||
./bin/spc build --enable-zts --build-embed --debug "bcmath,calendar,ctype,curl,dba,dom,exif,filter,fileinfo,gd,iconv,intl,mbstring,mbregex,mysqli,mysqlnd,opcache,openssl,pcntl,pdo,pdo_mysql,pdo_pgsql,pdo_sqlite,pgsql,phar,posix,readline,redis,session,simplexml,sockets,sqlite3,tokenizer,xml,xmlreader,xmlwriter,zip,zlib,apcu"
|
||||
export CGO_CFLAGS="$(./buildroot/bin/php-config --includes | sed s#-I/#-I$PWD/buildroot/#g)"
|
||||
export CGO_LDFLAGS="-framework CoreFoundation -framework SystemConfiguration $(./buildroot/bin/php-config --ldflags) $(./buildroot/bin/php-config --libs)"
|
||||
|
||||
git clone --depth=1 https://github.com/dunglas/frankenphp.git
|
||||
cd frankenphp/caddy/frankenphp
|
||||
go build -buildmode=pie -tags "cgo netgo osusergo static_build" -ldflags "-linkmode=external -extldflags -static-pie"
|
||||
git clone https://github.com/dunglas/frankenphp
|
||||
cd frankenphp
|
||||
./build-static.sh
|
||||
```
|
||||
|
||||
See [the list of supported extensions](https://static-php-cli.zhamao.me/en/guide/extensions.html).
|
||||
Note: this script also works on Linux (and probably on other Unixes), and is used internally by the Docker based static builder we provide.
|
||||
|
||||
## Customizing The Build
|
||||
|
||||
The following environment variables can be passed to `docker build` and to the `build-static.sh`
|
||||
script to customize the static build:
|
||||
|
||||
* `FRANKENPHP_VERSION`: the version of FrankenPHP to use
|
||||
* `PHP_VERSION`: the version of PHP to use
|
||||
* `PHP_EXTENSIONS`: the PHP extensions to build ([list of supported extensions](https://static-php.dev/en/guide/extensions.html))
|
||||
* `PHP_EXTENSION_LIBS`: extra libraries to build that add extra features to the extensions
|
||||
* `EMBED`: path of the PHP application to embed in the binary
|
||||
* `CLEAN`: when set, libphp and all its dependencies are built from scratch (no cache)
|
||||
* `DEBUG_SYMBOLS`: when set, debug-symbols will not be stripped and will be added within the binary
|
||||
* `RELEASE`: (maintainers only) when set, the resulting binary will be uploaded on GitHub
|
||||
|
||||
@@ -3,76 +3,112 @@
|
||||
Boot your application once and keep it in memory.
|
||||
FrankenPHP will handle incoming requests in a few milliseconds.
|
||||
|
||||
## Starting Worker Scripts
|
||||
|
||||
### Docker
|
||||
|
||||
Set the value of the `FRANKENPHP_CONFIG` environment variable to `worker /path/to/your/worker/script.php`:
|
||||
|
||||
```console
|
||||
docker run \
|
||||
-e FRANKENPHP_CONFIG="worker /app/path/to/your/worker/script.php" \
|
||||
-v $PWD:/app \
|
||||
-p 80:80 -p 443:443 -p 443:443/udp \
|
||||
dunglas/frankenphp
|
||||
```
|
||||
|
||||
### Standalone Binary
|
||||
|
||||
Use the `--worker` option of the `php-server` command to serve the content of the current directory using a worker:
|
||||
|
||||
```console
|
||||
./frankenphp php-server --worker /path/to/your/worker/script.php
|
||||
```
|
||||
|
||||
## Symfony Runtime
|
||||
|
||||
The worker mode of FrankenPHP is supported by the [Symfony Runtime Component](https://symfony.com/doc/current/components/runtime.html).
|
||||
To start any Symfony application in a worker, install the FrankenPHP package of [PHP Runtime](https://github.com/php-runtime/runtime):
|
||||
|
||||
```console
|
||||
composer require runtime/frankenphp-symfony
|
||||
```
|
||||
|
||||
Start your app server by defining the `APP_RUNTIME` environment variable to use the FrankenPHP Symfony Runtime:
|
||||
|
||||
```console
|
||||
docker run \
|
||||
-e FRANKENPHP_CONFIG="worker ./public/index.php" \
|
||||
-e APP_RUNTIME=Runtime\\FrankenPhpSymfony\\Runtime \
|
||||
-v $PWD:/app \
|
||||
-p 80:80 -p 443:443 -p 443:443/udp \
|
||||
dunglas/frankenphp
|
||||
```
|
||||
|
||||
## Laravel Octane
|
||||
|
||||
See [the dedicated documentation](laravel.md#laravel-octane).
|
||||
|
||||
## Custom Apps
|
||||
|
||||
The following example shows how to create your own worker script without relying on a third-party library:
|
||||
|
||||
```php
|
||||
<?php
|
||||
// public/index.php
|
||||
|
||||
// Prevent worker script termination when a client connection is interrupted
|
||||
ignore_user_abort(true);
|
||||
|
||||
// Boot your app
|
||||
require __DIR__.'/vendor/autoload.php';
|
||||
|
||||
$myApp = new \App\Kernel();
|
||||
$myApp->boot();
|
||||
|
||||
do {
|
||||
$running = frankenphp_handle_request(function () use ($myApp) {
|
||||
// Handler outside the loop for better performance (doing less work)
|
||||
$handler = static function () use ($myApp) {
|
||||
// Called when a request is received,
|
||||
// superglobals, php://input and the like are reset
|
||||
echo $myApp->handle($_GET, $_POST, $_COOKIE, $_FILES, $_SERVER);
|
||||
});
|
||||
};
|
||||
for($nbRequests = 0, $running = true; isset($_SERVER['MAX_REQUESTS']) && ($nbRequests < ((int)$_SERVER['MAX_REQUESTS'])) && $running; ++$nbRequests) {
|
||||
$running = \frankenphp_handle_request($handler);
|
||||
|
||||
// Do something after sending the HTTP response
|
||||
$myApp->terminate();
|
||||
|
||||
// Call the garbage collector to reduce the chances of it being triggered in the middle of a page generation
|
||||
gc_collect_cycles();
|
||||
} while ($running);
|
||||
|
||||
}
|
||||
// Cleanup
|
||||
$myApp->shutdown();
|
||||
```
|
||||
|
||||
Then, start your app and use the `FRANKENPHP_CONFIG` environment variable to configure your worker:
|
||||
Then, start your app and use the `FRANKENPHP_CONFIG` environment variable to configure your worker:
|
||||
|
||||
```sh
|
||||
```console
|
||||
docker run \
|
||||
-e FRANKENPHP_CONFIG="worker ./public/index.php" \
|
||||
-v $PWD:/app \
|
||||
-p 80:80 -p 443:443 \
|
||||
-p 80:80 -p 443:443 -p 443:443/udp \
|
||||
dunglas/frankenphp
|
||||
```
|
||||
|
||||
By default, one worker per CPU is started.
|
||||
You can also configure the number of workers to start:
|
||||
|
||||
```sh
|
||||
```console
|
||||
docker run \
|
||||
-e FRANKENPHP_CONFIG="worker ./public/index.php 42" \
|
||||
-v $PWD:/app \
|
||||
-p 80:80 -p 443:443 \
|
||||
dunglas/frankenphp
|
||||
```
|
||||
## Symfony Runtime
|
||||
|
||||
The worker mode of FrankenPHP is supported by the [Symfony Runtime Component](https://symfony.com/doc/current/components/runtime.html).
|
||||
To start any Symfony application in a worker, install the FrankenPHP package of [PHP Runtime](https://github.com/php-runtime/runtime):
|
||||
|
||||
```sh
|
||||
composer require runtime/frankenphp-symfony
|
||||
```
|
||||
|
||||
Start your app server by defining the `APP_RUNTIME` environment variable to use the FrankenPHP Symfony Runtime
|
||||
|
||||
```sh
|
||||
docker run \
|
||||
-e FRANKENPHP_CONFIG="worker ./public/index.php" \
|
||||
-e APP_RUNTIME=Runtime\\FrankenPhpSymfony\\Runtime \
|
||||
-v $PWD:/app \
|
||||
-p 80:80 -p 443:443 \
|
||||
-p 80:80 -p 443:443 -p 443:443/udp \
|
||||
dunglas/frankenphp
|
||||
```
|
||||
|
||||
## Laravel Octane
|
||||
### Restart the Worker After a Certain Number of Requests
|
||||
|
||||
Coming soon!
|
||||
As PHP was not originally designed for long-running processes, there are still many libraries and legacy codes that leak memory.
|
||||
A workaround to using this type of code in worker mode is to restart the worker script after processing a certain number of requests:
|
||||
|
||||
The previous worker snippet allows configuring a maximum number of request to handle by setting an environment variable named `MAX_REQUESTS`.
|
||||
|
||||
188
embed.go
Normal file
188
embed.go
Normal file
@@ -0,0 +1,188 @@
|
||||
package frankenphp
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
_ "embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// The path of the embedded PHP application (empty if none)
|
||||
var EmbeddedAppPath string
|
||||
|
||||
//go:embed app.tar
|
||||
var embeddedApp []byte
|
||||
|
||||
//go:embed app_checksum.txt
|
||||
var embeddedAppChecksum []byte
|
||||
|
||||
func init() {
|
||||
if len(embeddedApp) == 0 {
|
||||
// No embedded app
|
||||
return
|
||||
}
|
||||
|
||||
appPath := filepath.Join(os.TempDir(), "frankenphp_"+strings.TrimSuffix(string(embeddedAppChecksum[:]), "\n"))
|
||||
|
||||
if _, err := os.Stat(appPath); os.IsNotExist(err) {
|
||||
if err := untar(appPath); err != nil {
|
||||
os.RemoveAll(appPath)
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
EmbeddedAppPath = appPath
|
||||
}
|
||||
|
||||
// untar reads the tar file from r and writes it into dir.
|
||||
//
|
||||
// Adapted from https://github.com/golang/build/blob/master/cmd/buildlet/buildlet.go
|
||||
func untar(dir string) (err error) {
|
||||
t0 := time.Now()
|
||||
nFiles := 0
|
||||
madeDir := map[string]bool{}
|
||||
|
||||
tr := tar.NewReader(bytes.NewReader(embeddedApp))
|
||||
loggedChtimesError := false
|
||||
for {
|
||||
f, err := tr.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("tar error: %w", err)
|
||||
}
|
||||
if f.Typeflag == tar.TypeXGlobalHeader {
|
||||
// golang.org/issue/22748: git archive exports
|
||||
// a global header ('g') which after Go 1.9
|
||||
// (for a bit?) contained an empty filename.
|
||||
// Ignore it.
|
||||
continue
|
||||
}
|
||||
rel, err := nativeRelPath(f.Name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("tar file contained invalid name %q: %v", f.Name, err)
|
||||
}
|
||||
abs := filepath.Join(dir, rel)
|
||||
|
||||
fi := f.FileInfo()
|
||||
mode := fi.Mode()
|
||||
switch {
|
||||
case mode.IsRegular():
|
||||
// Make the directory. This is redundant because it should
|
||||
// already be made by a directory entry in the tar
|
||||
// beforehand. Thus, don't check for errors; the next
|
||||
// write will fail with the same error.
|
||||
dir := filepath.Dir(abs)
|
||||
if !madeDir[dir] {
|
||||
if err := os.MkdirAll(filepath.Dir(abs), mode.Perm()); err != nil {
|
||||
return err
|
||||
}
|
||||
madeDir[dir] = true
|
||||
}
|
||||
if runtime.GOOS == "darwin" && mode&0111 != 0 {
|
||||
// See comment in writeFile.
|
||||
err := os.Remove(abs)
|
||||
if err != nil && !errors.Is(err, fs.ErrNotExist) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
wf, err := os.OpenFile(abs, os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode.Perm())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
n, err := io.Copy(wf, tr)
|
||||
if closeErr := wf.Close(); closeErr != nil && err == nil {
|
||||
err = closeErr
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("error writing to %s: %v", abs, err)
|
||||
}
|
||||
if n != f.Size {
|
||||
return fmt.Errorf("only wrote %d bytes to %s; expected %d", n, abs, f.Size)
|
||||
}
|
||||
modTime := f.ModTime
|
||||
if modTime.After(t0) {
|
||||
// Clamp modtimes at system time. See
|
||||
// golang.org/issue/19062 when clock on
|
||||
// buildlet was behind the gitmirror server
|
||||
// doing the git-archive.
|
||||
modTime = t0
|
||||
}
|
||||
if !modTime.IsZero() {
|
||||
if err := os.Chtimes(abs, modTime, modTime); err != nil && !loggedChtimesError {
|
||||
// benign error. Gerrit doesn't even set the
|
||||
// modtime in these, and we don't end up relying
|
||||
// on it anywhere (the gomote push command relies
|
||||
// on digests only), so this is a little pointless
|
||||
// for now.
|
||||
log.Printf("error changing modtime: %v (further Chtimes errors suppressed)", err)
|
||||
loggedChtimesError = true // once is enough
|
||||
}
|
||||
}
|
||||
nFiles++
|
||||
case mode.IsDir():
|
||||
if err := os.MkdirAll(abs, mode.Perm()); err != nil {
|
||||
return err
|
||||
}
|
||||
madeDir[abs] = true
|
||||
case mode&os.ModeSymlink != 0:
|
||||
// TODO: ignore these for now. They were breaking x/build tests.
|
||||
// Implement these if/when we ever have a test that needs them.
|
||||
// But maybe we'd have to skip creating them on Windows for some builders
|
||||
// without permissions.
|
||||
default:
|
||||
return fmt.Errorf("tar file entry %s contained unsupported file type %v", f.Name, mode)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// nativeRelPath verifies that p is a non-empty relative path
|
||||
// using either slashes or the buildlet's native path separator,
|
||||
// and returns it canonicalized to the native path separator.
|
||||
func nativeRelPath(p string) (string, error) {
|
||||
if p == "" {
|
||||
return "", errors.New("path not provided")
|
||||
}
|
||||
|
||||
if filepath.Separator != '/' && strings.Contains(p, string(filepath.Separator)) {
|
||||
clean := filepath.Clean(p)
|
||||
if filepath.IsAbs(clean) {
|
||||
return "", fmt.Errorf("path %q is not relative", p)
|
||||
}
|
||||
if clean == ".." || strings.HasPrefix(clean, ".."+string(filepath.Separator)) {
|
||||
return "", fmt.Errorf("path %q refers to a parent directory", p)
|
||||
}
|
||||
if strings.HasPrefix(p, string(filepath.Separator)) || filepath.VolumeName(clean) != "" {
|
||||
// On Windows, this catches semi-relative paths like "C:" (meaning “the
|
||||
// current working directory on volume C:”) and "\windows" (meaning “the
|
||||
// windows subdirectory of the current drive letter”).
|
||||
return "", fmt.Errorf("path %q is relative to volume", p)
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
clean := path.Clean(p)
|
||||
if path.IsAbs(clean) {
|
||||
return "", fmt.Errorf("path %q is not relative", p)
|
||||
}
|
||||
if clean == ".." || strings.HasPrefix(clean, "../") {
|
||||
return "", fmt.Errorf("path %q refers to a parent directory", p)
|
||||
}
|
||||
canon := filepath.FromSlash(p)
|
||||
if filepath.VolumeName(canon) != "" {
|
||||
return "", fmt.Errorf("path %q begins with a native volume name", p)
|
||||
}
|
||||
return canon, nil
|
||||
}
|
||||
1217
frankenphp.c
1217
frankenphp.c
File diff suppressed because it is too large
Load Diff
331
frankenphp.go
331
frankenphp.go
@@ -6,24 +6,25 @@
|
||||
package frankenphp
|
||||
|
||||
//go:generate rm -Rf C-Thread-Pool/
|
||||
//go:generate git clone --branch=fix/SA_ONSTACK --depth=1 git@github.com:dunglas/C-Thread-Pool.git
|
||||
//go:generate rm -Rf C-Thread-Pool/.git C-Thread-Pool/.circleci C-Thread-Pool/docs C-Thread-Pool/tests
|
||||
//go:generate git clone --depth=1 git@github.com:Pithikos/C-Thread-Pool.git
|
||||
//go:generate rm -Rf C-Thread-Pool/.git C-Thread-Pool/.github C-Thread-Pool/docs C-Thread-Pool/tests C-Thread-Pool/example.c
|
||||
|
||||
// Use PHP includes corresponding to your PHP installation by running:
|
||||
//
|
||||
// export CGO_CFLAGS=$(php-config --includes)
|
||||
// export CGO_LDFLAGS=$(php-config --ldflags --libs)
|
||||
// export CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)"
|
||||
//
|
||||
// We also set these flags for hardening: https://github.com/docker-library/php/blob/master/8.2/bookworm/zts/Dockerfile#L57-L59
|
||||
|
||||
// #cgo darwin pkg-config: libxml-2.0 sqlite3
|
||||
// #cgo darwin pkg-config: libxml-2.0
|
||||
// #cgo CFLAGS: -Wall -Werror -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64
|
||||
// #cgo CFLAGS: -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib
|
||||
// #cgo CFLAGS: -DTHREAD_NAME=frankenphp
|
||||
// #cgo linux CFLAGS: -D_GNU_SOURCE
|
||||
// #cgo CPPFLAGS: -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64
|
||||
// #cgo darwin LDFLAGS: -L/opt/homebrew/opt/libiconv/lib -liconv
|
||||
// #cgo linux LDFLAGS: -Wl,-O1
|
||||
// #cgo LDFLAGS: -pie -L/usr/local/lib -L/usr/lib -lphp -lresolv -ldl -lm -lutil
|
||||
// #cgo linux LDFLAGS: -Wl,-O1 -lresolv
|
||||
// #cgo LDFLAGS: -pie -L/usr/local/lib -L/usr/lib -lphp -ldl -lm -lutil
|
||||
// #include <stdlib.h>
|
||||
// #include <stdint.h>
|
||||
// #include <php_variables.h>
|
||||
@@ -38,6 +39,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime"
|
||||
"runtime/cgo"
|
||||
"strconv"
|
||||
@@ -50,9 +52,11 @@ import (
|
||||
//_ "github.com/ianlancetaylor/cgosymbolizer"
|
||||
)
|
||||
|
||||
type key int
|
||||
type contextKeyStruct struct{}
|
||||
type handleKeyStruct struct{}
|
||||
|
||||
var contextKey key
|
||||
var contextKey = contextKeyStruct{}
|
||||
var handleKey = handleKeyStruct{}
|
||||
|
||||
var (
|
||||
InvalidRequestError = errors.New("not a FrankenPHP request")
|
||||
@@ -109,41 +113,22 @@ func (l syslogLevel) String() string {
|
||||
|
||||
// FrankenPHPContext provides contextual information about the Request to handle.
|
||||
type FrankenPHPContext struct {
|
||||
// The root directory of the PHP application.
|
||||
DocumentRoot string
|
||||
documentRoot string
|
||||
splitPath []string
|
||||
env map[string]string
|
||||
logger *zap.Logger
|
||||
|
||||
// The path in the URL will be split into two, with the first piece ending
|
||||
// with the value of SplitPath. The first piece will be assumed as the
|
||||
// actual resource (CGI script) name, and the second piece will be set to
|
||||
// PATH_INFO for the CGI script to use.
|
||||
//
|
||||
// Future enhancements should be careful to avoid CVE-2019-11043,
|
||||
// which can be mitigated with use of a try_files-like behavior
|
||||
// that 404s if the fastcgi path info is not found.
|
||||
SplitPath []string
|
||||
|
||||
// Path declared as root directory will be resolved to its absolute value
|
||||
// after the evaluation of any symbolic links.
|
||||
// Due to the nature of PHP opcache, root directory path is cached: when
|
||||
// using a symlinked directory as root this could generate errors when
|
||||
// symlink is changed without php-fpm being restarted; enabling this
|
||||
// directive will set $_SERVER['DOCUMENT_ROOT'] to the real directory path.
|
||||
ResolveRootSymlink bool
|
||||
|
||||
// CGI-like environment variables that will be available in $_SERVER.
|
||||
// This map is populated automatically, exisiting key are never replaced.
|
||||
Env map[string]string
|
||||
|
||||
// The logger associated with the current request
|
||||
Logger *zap.Logger
|
||||
|
||||
populated bool
|
||||
authPassword string
|
||||
docURI string
|
||||
pathInfo string
|
||||
scriptName string
|
||||
scriptFilename string
|
||||
|
||||
// Whether the request is already closed by us
|
||||
closed sync.Once
|
||||
|
||||
responseWriter http.ResponseWriter
|
||||
responseWriter http.ResponseWriter
|
||||
exitStatus C.int
|
||||
|
||||
done chan interface{}
|
||||
currentWorkerRequest cgo.Handle
|
||||
}
|
||||
@@ -158,19 +143,60 @@ func clientHasClosed(r *http.Request) bool {
|
||||
}
|
||||
|
||||
// NewRequestWithContext creates a new FrankenPHP request context.
|
||||
func NewRequestWithContext(r *http.Request, documentRoot string, l *zap.Logger) *http.Request {
|
||||
if l == nil {
|
||||
l = getLogger()
|
||||
func NewRequestWithContext(r *http.Request, opts ...RequestOption) (*http.Request, error) {
|
||||
fc := &FrankenPHPContext{
|
||||
done: make(chan interface{}),
|
||||
}
|
||||
for _, o := range opts {
|
||||
if err := o(fc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.WithValue(r.Context(), contextKey, &FrankenPHPContext{
|
||||
DocumentRoot: documentRoot,
|
||||
SplitPath: []string{".php"},
|
||||
Env: make(map[string]string),
|
||||
Logger: l,
|
||||
})
|
||||
if fc.documentRoot == "" {
|
||||
if EmbeddedAppPath != "" {
|
||||
fc.documentRoot = EmbeddedAppPath
|
||||
} else {
|
||||
var err error
|
||||
if fc.documentRoot, err = os.Getwd(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return r.WithContext(ctx)
|
||||
if fc.splitPath == nil {
|
||||
fc.splitPath = []string{".php"}
|
||||
}
|
||||
|
||||
if fc.env == nil {
|
||||
fc.env = make(map[string]string)
|
||||
}
|
||||
|
||||
if fc.logger == nil {
|
||||
fc.logger = getLogger()
|
||||
}
|
||||
|
||||
if splitPos := splitPos(fc, r.URL.Path); splitPos > -1 {
|
||||
fc.docURI = r.URL.Path[:splitPos]
|
||||
fc.pathInfo = r.URL.Path[splitPos:]
|
||||
|
||||
// Strip PATH_INFO from SCRIPT_NAME
|
||||
fc.scriptName = strings.TrimSuffix(r.URL.Path, fc.pathInfo)
|
||||
|
||||
// Ensure the SCRIPT_NAME has a leading slash for compliance with RFC3875
|
||||
// Info: https://tools.ietf.org/html/rfc3875#section-4.1.13
|
||||
if fc.scriptName != "" && !strings.HasPrefix(fc.scriptName, "/") {
|
||||
fc.scriptName = "/" + fc.scriptName
|
||||
}
|
||||
}
|
||||
|
||||
// SCRIPT_FILENAME is the absolute path of SCRIPT_NAME
|
||||
fc.scriptFilename = sanitizedPathJoin(fc.documentRoot, fc.scriptName)
|
||||
|
||||
c := context.WithValue(r.Context(), contextKey, fc)
|
||||
c = context.WithValue(c, handleKey, Handles())
|
||||
|
||||
return r.WithContext(c), nil
|
||||
}
|
||||
|
||||
// FromContext extracts the FrankenPHPContext from a context.
|
||||
@@ -273,7 +299,7 @@ func Init(options ...Option) error {
|
||||
|
||||
config := Config()
|
||||
|
||||
if config.Version.MajorVersion < 8 || config.Version.MinorVersion < 2 {
|
||||
if config.Version.MajorVersion < 8 || (config.Version.MajorVersion == 8 && config.Version.MinorVersion < 2) {
|
||||
return InvalidPHPVersionError
|
||||
}
|
||||
|
||||
@@ -298,7 +324,10 @@ func Init(options ...Option) error {
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Debug("FrankenPHP started")
|
||||
logger.Info("FrankenPHP started 🐘", zap.String("php_version", Version().Version))
|
||||
if EmbeddedAppPath != "" {
|
||||
logger.Info("embedded PHP app 📦", zap.String("path", EmbeddedAppPath))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -313,6 +342,11 @@ func Shutdown() {
|
||||
// Always reset the WaitGroup to ensure we're in a clean state
|
||||
workersReadyWG = sync.WaitGroup{}
|
||||
|
||||
// Remove the installed app
|
||||
if EmbeddedAppPath != "" {
|
||||
os.RemoveAll(EmbeddedAppPath)
|
||||
}
|
||||
|
||||
logger.Debug("FrankenPHP shut down")
|
||||
}
|
||||
|
||||
@@ -334,12 +368,12 @@ func updateServerContext(request *http.Request, create bool, mrh C.uintptr_t) er
|
||||
return InvalidRequestError
|
||||
}
|
||||
|
||||
authUser, authPassword, ok := request.BasicAuth()
|
||||
var cAuthUser, cAuthPassword *C.char
|
||||
if fc.authPassword != "" {
|
||||
cAuthPassword = C.CString(fc.authPassword)
|
||||
if ok && authPassword != "" {
|
||||
cAuthPassword = C.CString(authPassword)
|
||||
}
|
||||
|
||||
if authUser := fc.Env["REMOTE_USER"]; authUser != "" {
|
||||
if ok && authUser != "" {
|
||||
cAuthUser = C.CString(authUser)
|
||||
}
|
||||
|
||||
@@ -361,18 +395,24 @@ func updateServerContext(request *http.Request, create bool, mrh C.uintptr_t) er
|
||||
cContentType = C.CString(contentType)
|
||||
}
|
||||
|
||||
// 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
|
||||
var cPathTranslated *C.char
|
||||
if pathTranslated := fc.Env["PATH_TRANSLATED"]; pathTranslated != "" {
|
||||
cPathTranslated = C.CString(pathTranslated)
|
||||
if fc.pathInfo != "" {
|
||||
cPathTranslated = C.CString(sanitizedPathJoin(fc.documentRoot, fc.pathInfo)) // Info: http://www.oreilly.com/openbook/cgi/ch02_04.html
|
||||
}
|
||||
|
||||
cRequestUri := C.CString(request.URL.RequestURI())
|
||||
|
||||
var rh cgo.Handle
|
||||
if fc.responseWriter == nil {
|
||||
mrh = C.uintptr_t(cgo.NewHandle(request))
|
||||
h := cgo.NewHandle(request)
|
||||
request.Context().Value(handleKey).(*handleList).AddHandle(h)
|
||||
mrh = C.uintptr_t(h)
|
||||
} else {
|
||||
rh = cgo.NewHandle(request)
|
||||
request.Context().Value(handleKey).(*handleList).AddHandle(rh)
|
||||
}
|
||||
|
||||
ret := C.frankenphp_update_server_context(
|
||||
@@ -408,20 +448,14 @@ func ServeHTTP(responseWriter http.ResponseWriter, request *http.Request) error
|
||||
return InvalidRequestError
|
||||
}
|
||||
|
||||
if err := populateEnv(request); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fc.responseWriter = responseWriter
|
||||
fc.done = make(chan interface{})
|
||||
|
||||
rc := requestChan
|
||||
// Detect if a worker is available to handle this request
|
||||
if nil == fc.responseWriter {
|
||||
fc.Env["FRANKENPHP_WORKER"] = "1"
|
||||
} else if v, ok := workersRequestChans.Load(fc.Env["SCRIPT_FILENAME"]); ok {
|
||||
fc.Env["FRANKENPHP_WORKER"] = "1"
|
||||
rc = v.(chan *http.Request)
|
||||
if nil != fc.responseWriter {
|
||||
if v, ok := workersRequestChans.Load(fc.scriptFilename); ok {
|
||||
rc = v.(chan *http.Request)
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
@@ -440,7 +474,9 @@ func go_fetch_request() C.uintptr_t {
|
||||
return 0
|
||||
|
||||
case r := <-requestChan:
|
||||
return C.uintptr_t(cgo.NewHandle(r))
|
||||
h := cgo.NewHandle(r)
|
||||
r.Context().Value(handleKey).(*handleList).AddHandle(h)
|
||||
return C.uintptr_t(h)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -450,39 +486,35 @@ func maybeCloseContext(fc *FrankenPHPContext) {
|
||||
})
|
||||
}
|
||||
|
||||
// go_execute_script Note: only called in cgi-mode
|
||||
//
|
||||
//export go_execute_script
|
||||
func go_execute_script(rh unsafe.Pointer) {
|
||||
handle := cgo.Handle(rh)
|
||||
defer handle.Delete()
|
||||
|
||||
request := handle.Value().(*http.Request)
|
||||
fc, ok := FromContext(request.Context())
|
||||
if !ok {
|
||||
panic(InvalidRequestError)
|
||||
}
|
||||
defer maybeCloseContext(fc)
|
||||
defer func() {
|
||||
maybeCloseContext(fc)
|
||||
request.Context().Value(handleKey).(*handleList).FreeAll()
|
||||
}()
|
||||
|
||||
if err := updateServerContext(request, true, 0); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if C.frankenphp_request_startup() < 0 {
|
||||
panic(RequestStartupError)
|
||||
}
|
||||
|
||||
cFileName := C.CString(fc.Env["SCRIPT_FILENAME"])
|
||||
defer C.free(unsafe.Pointer(cFileName))
|
||||
|
||||
if C.frankenphp_execute_script(cFileName) < 0 {
|
||||
// scriptFilename is freed in frankenphp_execute_script()
|
||||
fc.exitStatus = C.frankenphp_execute_script(C.CString(fc.scriptFilename))
|
||||
if fc.exitStatus < 0 {
|
||||
panic(ScriptExecutionError)
|
||||
}
|
||||
|
||||
C.frankenphp_clean_server_context()
|
||||
C.frankenphp_request_shutdown()
|
||||
}
|
||||
|
||||
//export go_ub_write
|
||||
func go_ub_write(rh C.uintptr_t, cString *C.char, length C.int) (C.size_t, C.bool) {
|
||||
func go_ub_write(rh C.uintptr_t, cBuf *C.char, length C.int) (C.size_t, C.bool) {
|
||||
r := cgo.Handle(rh).Value().(*http.Request)
|
||||
fc, _ := FromContext(r.Context())
|
||||
|
||||
@@ -495,10 +527,13 @@ func go_ub_write(rh C.uintptr_t, cString *C.char, length C.int) (C.size_t, C.boo
|
||||
writer = fc.responseWriter
|
||||
}
|
||||
|
||||
i, _ := writer.Write([]byte(C.GoStringN(cString, length)))
|
||||
i, e := writer.Write(unsafe.Slice((*byte)(unsafe.Pointer(cBuf)), length))
|
||||
if e != nil {
|
||||
fc.logger.Error("write error", zap.Error(e))
|
||||
}
|
||||
|
||||
if fc.responseWriter == nil {
|
||||
fc.Logger.Info(writer.(*bytes.Buffer).String())
|
||||
fc.logger.Info(writer.(*bytes.Buffer).String())
|
||||
}
|
||||
|
||||
return C.size_t(i), C.bool(clientHasClosed(r))
|
||||
@@ -506,36 +541,88 @@ func go_ub_write(rh C.uintptr_t, cString *C.char, length C.int) (C.size_t, C.boo
|
||||
|
||||
//export go_register_variables
|
||||
func go_register_variables(rh C.uintptr_t, trackVarsArray *C.zval) {
|
||||
var env map[string]string
|
||||
r := cgo.Handle(rh).Value().(*http.Request)
|
||||
env = r.Context().Value(contextKey).(*FrankenPHPContext).Env
|
||||
fc := r.Context().Value(contextKey).(*FrankenPHPContext)
|
||||
|
||||
le := len(env) * 2
|
||||
cArr := (**C.char)(C.malloc(C.size_t(le) * C.size_t(unsafe.Sizeof((*C.char)(nil)))))
|
||||
defer C.free(unsafe.Pointer(cArr))
|
||||
|
||||
variables := unsafe.Slice(cArr, le)
|
||||
le := (len(fc.env) + len(r.Header)) * 2
|
||||
dynamicVariables := make([]*C.char, le)
|
||||
|
||||
var i int
|
||||
for k, v := range env {
|
||||
variables[i] = C.CString(k)
|
||||
// Add all HTTP headers to env variables
|
||||
for field, val := range r.Header {
|
||||
k := "HTTP_" + headerNameReplacer.Replace(strings.ToUpper(field))
|
||||
if _, ok := fc.env[k]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
dynamicVariables[i] = C.CString(k)
|
||||
i++
|
||||
|
||||
variables[i] = C.CString(v)
|
||||
dynamicVariables[i] = C.CString(strings.Join(val, ", "))
|
||||
i++
|
||||
}
|
||||
|
||||
C.frankenphp_register_bulk_variables(cArr, C.size_t(le), trackVarsArray)
|
||||
for k, v := range fc.env {
|
||||
dynamicVariables[i] = C.CString(k)
|
||||
i++
|
||||
|
||||
for _, v := range variables {
|
||||
C.free(unsafe.Pointer(v))
|
||||
dynamicVariables[i] = C.CString(v)
|
||||
i++
|
||||
}
|
||||
|
||||
var dynamicVariablesPtr **C.char = nil
|
||||
if le > 0 {
|
||||
dynamicVariablesPtr = &dynamicVariables[0]
|
||||
}
|
||||
|
||||
knownVariables := computeKnownVariables(r)
|
||||
C.frankenphp_register_bulk_variables(&knownVariables[0], dynamicVariablesPtr, C.size_t(le), trackVarsArray)
|
||||
|
||||
fc.env = nil
|
||||
}
|
||||
|
||||
//export go_apache_request_headers
|
||||
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)
|
||||
|
||||
pinner := runtime.Pinner{}
|
||||
pinnerHandle := C.uintptr_t(cgo.NewHandle(pinner))
|
||||
|
||||
rl := len(r.Header)
|
||||
scs := unsafe.Sizeof(C.go_string{})
|
||||
|
||||
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 {
|
||||
fd := unsafe.StringData(field)
|
||||
pinner.Pin(fd)
|
||||
|
||||
*header = C.go_string{C.size_t(len(field)), (*C.char)(unsafe.Pointer(fd))}
|
||||
header = (*C.go_string)(unsafe.Add(unsafe.Pointer(header), scs))
|
||||
|
||||
cv := strings.Join(val, ", ")
|
||||
vd := unsafe.StringData(cv)
|
||||
pinner.Pin(vd)
|
||||
|
||||
*header = C.go_string{C.size_t(len(cv)), (*C.char)(unsafe.Pointer(vd))}
|
||||
header = (*C.go_string)(unsafe.Add(unsafe.Pointer(header), scs))
|
||||
}
|
||||
|
||||
return headers, C.size_t(rl), 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) {
|
||||
parts := strings.SplitN(C.GoStringN(cString, length), ": ", 2)
|
||||
if len(parts) != 2 {
|
||||
fc.Logger.Debug("invalid header", zap.String("header", parts[0]))
|
||||
fc.logger.Debug("invalid header", zap.String("header", parts[0]))
|
||||
|
||||
return
|
||||
}
|
||||
@@ -588,7 +675,7 @@ func go_sapi_flush(rh C.uintptr_t) bool {
|
||||
}
|
||||
|
||||
if err := http.NewResponseController(fc.responseWriter).Flush(); err != nil {
|
||||
fc.Logger.Error("the current responseWriter is not a flusher", zap.Error(err))
|
||||
fc.logger.Error("the current responseWriter is not a flusher", zap.Error(err))
|
||||
}
|
||||
|
||||
return false
|
||||
@@ -598,7 +685,7 @@ func go_sapi_flush(rh C.uintptr_t) bool {
|
||||
func go_read_post(rh C.uintptr_t, cBuf *C.char, countBytes C.size_t) (readBytes C.size_t) {
|
||||
r := cgo.Handle(rh).Value().(*http.Request)
|
||||
|
||||
p := make([]byte, countBytes)
|
||||
p := unsafe.Slice((*byte)(unsafe.Pointer(cBuf)), countBytes)
|
||||
var err error
|
||||
for readBytes < countBytes && err == nil {
|
||||
var n int
|
||||
@@ -609,11 +696,7 @@ func go_read_post(rh C.uintptr_t, cBuf *C.char, countBytes C.size_t) (readBytes
|
||||
if err != nil && err != io.EOF {
|
||||
// invalid Read on closed Body may happen because of https://github.com/golang/go/issues/15527
|
||||
fc, _ := FromContext(r.Context())
|
||||
fc.Logger.Error("error while reading the request body", zap.Error(err))
|
||||
}
|
||||
|
||||
if readBytes != 0 {
|
||||
C.memcpy(unsafe.Pointer(cBuf), unsafe.Pointer(&p[0]), C.size_t(readBytes))
|
||||
fc.logger.Error("error while reading the request body", zap.Error(err))
|
||||
}
|
||||
|
||||
return
|
||||
@@ -627,16 +710,13 @@ func go_read_cookies(rh C.uintptr_t) *C.char {
|
||||
if len(cookies) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
cookieString := make([]string, len(cookies))
|
||||
for _, cookie := range r.Cookies() {
|
||||
cookieString = append(cookieString, cookie.String())
|
||||
cookieStrings := make([]string, len(cookies))
|
||||
for i, cookie := range cookies {
|
||||
cookieStrings[i] = cookie.String()
|
||||
}
|
||||
|
||||
cCookie := C.CString(strings.Join(cookieString, "; "))
|
||||
// freed in frankenphp_request_shutdown()
|
||||
|
||||
return cCookie
|
||||
return C.CString(strings.Join(cookieStrings, "; "))
|
||||
}
|
||||
|
||||
//export go_log
|
||||
@@ -665,3 +745,30 @@ func go_log(message *C.char, level C.int) {
|
||||
l.Info(m, zap.Stringer("syslog_level", syslogLevel(level)))
|
||||
}
|
||||
}
|
||||
|
||||
// ExecuteScriptCLI executes the PHP script passed as parameter.
|
||||
// It returns the exit status code of the script.
|
||||
func ExecuteScriptCLI(script string, args []string) int {
|
||||
cScript := C.CString(script)
|
||||
defer C.free(unsafe.Pointer(cScript))
|
||||
|
||||
argc, argv := convertArgs(args)
|
||||
defer freeArgs(argv)
|
||||
|
||||
return int(C.frankenphp_execute_script_cli(cScript, argc, (**C.char)(unsafe.Pointer(&argv[0]))))
|
||||
}
|
||||
|
||||
func convertArgs(args []string) (C.int, []*C.char) {
|
||||
argc := C.int(len(args))
|
||||
argv := make([]*C.char, argc)
|
||||
for i, arg := range args {
|
||||
argv[i] = C.CString(arg)
|
||||
}
|
||||
return argc, argv
|
||||
}
|
||||
|
||||
func freeArgs(argv []*C.char) {
|
||||
for _, arg := range argv {
|
||||
C.free(unsafe.Pointer(arg))
|
||||
}
|
||||
}
|
||||
|
||||
57
frankenphp.h
57
frankenphp.h
@@ -1,9 +1,9 @@
|
||||
#ifndef _FRANKENPPHP_H
|
||||
#define _FRANKENPPHP_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <Zend/zend_types.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifndef FRANKENPHP_VERSION
|
||||
#define FRANKENPHP_VERSION dev
|
||||
@@ -11,46 +11,43 @@
|
||||
#define STRINGIFY(x) #x
|
||||
#define TOSTRING(x) STRINGIFY(x)
|
||||
|
||||
typedef struct go_string {
|
||||
size_t len;
|
||||
const char *data;
|
||||
} go_string;
|
||||
|
||||
typedef struct frankenphp_version {
|
||||
unsigned char major_version;
|
||||
unsigned char minor_version;
|
||||
unsigned char release_version;
|
||||
const char *extra_version;
|
||||
const char *version;
|
||||
unsigned long version_id;
|
||||
unsigned char major_version;
|
||||
unsigned char minor_version;
|
||||
unsigned char release_version;
|
||||
const char *extra_version;
|
||||
const char *version;
|
||||
unsigned long version_id;
|
||||
} frankenphp_version;
|
||||
frankenphp_version frankenphp_get_version();
|
||||
|
||||
typedef struct frankenphp_config {
|
||||
frankenphp_version version;
|
||||
bool zts;
|
||||
bool zend_signals;
|
||||
bool zend_max_execution_timers;
|
||||
frankenphp_version version;
|
||||
bool zts;
|
||||
bool zend_signals;
|
||||
bool zend_max_execution_timers;
|
||||
} frankenphp_config;
|
||||
frankenphp_config frankenphp_get_config();
|
||||
|
||||
int frankenphp_init(int num_threads);
|
||||
|
||||
int frankenphp_update_server_context(
|
||||
bool create,
|
||||
uintptr_t current_request,
|
||||
uintptr_t main_request,
|
||||
bool create, uintptr_t current_request, uintptr_t main_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
|
||||
);
|
||||
int frankenphp_worker_reset_server_context();
|
||||
uintptr_t frankenphp_clean_server_context();
|
||||
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);
|
||||
int frankenphp_request_startup();
|
||||
int frankenphp_execute_script(const char *file_name);
|
||||
uintptr_t frankenphp_request_shutdown();
|
||||
void frankenphp_register_bulk_variables(char **variables, size_t size, zval *track_vars_array);
|
||||
int frankenphp_execute_script(char *file_name);
|
||||
void frankenphp_register_bulk_variables(char *known_variables[27],
|
||||
char **dynamic_variables, size_t size,
|
||||
zval *track_vars_array);
|
||||
|
||||
int frankenphp_execute_script_cli(char *script, int argc, char **argv);
|
||||
|
||||
#endif
|
||||
|
||||
@@ -12,3 +12,23 @@ function frankenphp_finish_request(): bool {}
|
||||
* @alias frankenphp_finish_request
|
||||
*/
|
||||
function fastcgi_finish_request(): bool {}
|
||||
|
||||
function frankenphp_request_headers(): array {}
|
||||
|
||||
/**
|
||||
* @alias frankenphp_request_headers
|
||||
*/
|
||||
function apache_request_headers(): array {}
|
||||
|
||||
/**
|
||||
* @alias frankenphp_response_headers
|
||||
*/
|
||||
function getallheaders(): array {}
|
||||
|
||||
function frankenphp_response_headers(): array|bool {}
|
||||
|
||||
/**
|
||||
* @alias frankenphp_response_headers
|
||||
*/
|
||||
function apache_response_headers(): array|bool {}
|
||||
|
||||
|
||||
@@ -1,29 +1,57 @@
|
||||
/* This is a generated file, edit the .stub.php file instead.
|
||||
* Stub hash: de4dc4063fafd8c933e3068c8349889a7ece5f03 */
|
||||
* Stub hash: 467f1406e17d3b8ca67bba5ea367194e60d8dd27 */
|
||||
|
||||
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_frankenphp_handle_request, 0, 1, _IS_BOOL, 0)
|
||||
ZEND_ARG_TYPE_INFO(0, callback, IS_CALLABLE, 0)
|
||||
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_frankenphp_handle_request, 0, 1,
|
||||
_IS_BOOL, 0)
|
||||
ZEND_ARG_TYPE_INFO(0, callback, IS_CALLABLE, 0)
|
||||
ZEND_END_ARG_INFO()
|
||||
|
||||
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_headers_send, 0, 0, IS_LONG, 0)
|
||||
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, status, IS_LONG, 0, "200")
|
||||
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, status, IS_LONG, 0, "200")
|
||||
ZEND_END_ARG_INFO()
|
||||
|
||||
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_frankenphp_finish_request, 0, 0, _IS_BOOL, 0)
|
||||
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_frankenphp_finish_request, 0, 0,
|
||||
_IS_BOOL, 0)
|
||||
ZEND_END_ARG_INFO()
|
||||
|
||||
#define arginfo_fastcgi_finish_request arginfo_frankenphp_finish_request
|
||||
|
||||
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_frankenphp_request_headers, 0,
|
||||
0, IS_ARRAY, 0)
|
||||
ZEND_END_ARG_INFO()
|
||||
|
||||
#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(frankenphp_request_headers);
|
||||
ZEND_FUNCTION(frankenphp_response_headers);
|
||||
|
||||
static const zend_function_entry ext_functions[] = {
|
||||
ZEND_FE(frankenphp_handle_request, arginfo_frankenphp_handle_request)
|
||||
ZEND_FE(headers_send, arginfo_headers_send)
|
||||
ZEND_FE(frankenphp_finish_request, arginfo_frankenphp_finish_request)
|
||||
ZEND_FALIAS(fastcgi_finish_request, frankenphp_finish_request, arginfo_fastcgi_finish_request)
|
||||
ZEND_FE_END
|
||||
};
|
||||
ZEND_FE(frankenphp_handle_request, arginfo_frankenphp_handle_request)
|
||||
ZEND_FE(headers_send, arginfo_headers_send) ZEND_FE(
|
||||
frankenphp_finish_request, arginfo_frankenphp_finish_request)
|
||||
ZEND_FALIAS(fastcgi_finish_request, frankenphp_finish_request,
|
||||
arginfo_fastcgi_finish_request)
|
||||
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};
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"net/textproto"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -61,10 +62,11 @@ func runTest(t *testing.T, test func(func(http.ResponseWriter, *http.Request), *
|
||||
defer frankenphp.Shutdown()
|
||||
|
||||
handler := func(w http.ResponseWriter, r *http.Request) {
|
||||
req := frankenphp.NewRequestWithContext(r, testDataDir, nil)
|
||||
if err := frankenphp.ServeHTTP(w, req); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
req, err := frankenphp.NewRequestWithContext(r, frankenphp.WithRequestDocumentRoot(testDataDir, false))
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = frankenphp.ServeHTTP(w, req)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
var ts *httptest.Server
|
||||
@@ -85,29 +87,6 @@ func runTest(t *testing.T, test func(func(http.ResponseWriter, *http.Request), *
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func BenchmarkHelloWorld(b *testing.B) {
|
||||
if err := frankenphp.Init(frankenphp.WithLogger(zap.NewNop())); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer frankenphp.Shutdown()
|
||||
cwd, _ := os.Getwd()
|
||||
testDataDir := cwd + "/testdata/"
|
||||
|
||||
handler := func(w http.ResponseWriter, r *http.Request) {
|
||||
req := frankenphp.NewRequestWithContext(r, testDataDir, nil)
|
||||
if err := frankenphp.ServeHTTP(w, req); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("GET", "http://example.com/index.php", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
handler(w, req)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHelloWorld_module(t *testing.T) { testHelloWorld(t, nil) }
|
||||
func TestHelloWorld_worker(t *testing.T) {
|
||||
testHelloWorld(t, &testOptions{workerScript: "index.php"})
|
||||
@@ -140,14 +119,17 @@ func testFinishRequest(t *testing.T, opts *testOptions) {
|
||||
}, opts)
|
||||
}
|
||||
|
||||
func TestServerVariable_module(t *testing.T) { testServerVariable(t, nil) }
|
||||
func TestServerVariable_module(t *testing.T) {
|
||||
testServerVariable(t, nil)
|
||||
}
|
||||
func TestServerVariable_worker(t *testing.T) {
|
||||
testServerVariable(t, &testOptions{workerScript: "server-variable.php"})
|
||||
}
|
||||
func testServerVariable(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/server-variable.php/baz/bat?foo=a&bar=b&i=%d#hash", i), nil)
|
||||
req := httptest.NewRequest("POST", fmt.Sprintf("http://example.com/server-variable.php/baz/bat?foo=a&bar=b&i=%d#hash", i), strings.NewReader("foo"))
|
||||
req.SetBasicAuth("kevin", "password")
|
||||
req.Header.Add("Content-Type", "text/plain")
|
||||
w := httptest.NewRecorder()
|
||||
handler(w, req)
|
||||
|
||||
@@ -163,7 +145,7 @@ func testServerVariable(t *testing.T, opts *testOptions) {
|
||||
assert.Contains(t, strBody, "[HTTP_AUTHORIZATION] => Basic a2V2aW46cGFzc3dvcmQ=")
|
||||
assert.Contains(t, strBody, "[DOCUMENT_ROOT]")
|
||||
assert.Contains(t, strBody, "[PHP_SELF] => /server-variable.php/baz/bat")
|
||||
assert.Contains(t, strBody, "[CONTENT_TYPE]")
|
||||
assert.Contains(t, strBody, "[CONTENT_TYPE] => text/plain")
|
||||
assert.Contains(t, strBody, fmt.Sprintf("[QUERY_STRING] => foo=a&bar=b&i=%d#hash", i))
|
||||
assert.Contains(t, strBody, fmt.Sprintf("[REQUEST_URI] => /server-variable.php/baz/bat?foo=a&bar=b&i=%d#hash", i))
|
||||
assert.Contains(t, strBody, "[CONTENT_LENGTH]")
|
||||
@@ -173,7 +155,7 @@ func testServerVariable(t *testing.T, opts *testOptions) {
|
||||
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, "[REQUEST_METHOD] => POST")
|
||||
assert.Contains(t, strBody, "[SERVER_NAME] => example.com")
|
||||
assert.Contains(t, strBody, "[SERVER_PROTOCOL] => HTTP/1.1")
|
||||
assert.Contains(t, strBody, "[SCRIPT_FILENAME]")
|
||||
@@ -195,12 +177,15 @@ func testPathInfo(t *testing.T, opts *testOptions) {
|
||||
testDataDir := cwd + "/testdata/"
|
||||
|
||||
requestURI := r.URL.RequestURI()
|
||||
rewriteRequest := frankenphp.NewRequestWithContext(r, testDataDir, nil)
|
||||
rewriteRequest.URL.Path = "/server-variable.php/pathinfo"
|
||||
fc, _ := frankenphp.FromContext(rewriteRequest.Context())
|
||||
fc.Env["REQUEST_URI"] = requestURI
|
||||
r.URL.Path = "/server-variable.php/pathinfo"
|
||||
|
||||
err := frankenphp.ServeHTTP(w, rewriteRequest)
|
||||
rewriteRequest, err := frankenphp.NewRequestWithContext(r,
|
||||
frankenphp.WithRequestDocumentRoot(testDataDir, false),
|
||||
frankenphp.WithRequestEnv(map[string]string{"REQUEST_URI": requestURI}),
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = frankenphp.ServeHTTP(w, rewriteRequest)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
@@ -241,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) {
|
||||
@@ -309,24 +315,18 @@ func testSession(t *testing.T, opts *testOptions) {
|
||||
|
||||
runTest(t, func(_ func(http.ResponseWriter, *http.Request), ts *httptest.Server, i int) {
|
||||
jar, err := cookiejar.New(&cookiejar.Options{})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
|
||||
client := &http.Client{Jar: jar}
|
||||
|
||||
resp1, err := client.Get(ts.URL + "/session.php")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
|
||||
body1, _ := io.ReadAll(resp1.Body)
|
||||
assert.Equal(t, "Count: 0\n", string(body1))
|
||||
|
||||
resp2, err := client.Get(ts.URL + "/session.php")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
|
||||
body2, _ := io.ReadAll(resp2.Body)
|
||||
assert.Equal(t, "Count: 1\n", string(body2))
|
||||
@@ -557,6 +557,64 @@ func TestVersion(t *testing.T) {
|
||||
assert.NotEmpty(t, v.Version, 0)
|
||||
}
|
||||
|
||||
func TestFiberNoCgo_module(t *testing.T) { testFiberNoCgo(t, &testOptions{}) }
|
||||
func TestFiberNonCgo_worker(t *testing.T) {
|
||||
testFiberNoCgo(t, &testOptions{workerScript: "fiber-no-cgo.php"})
|
||||
}
|
||||
func testFiberNoCgo(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/fiber-no-cgo.php?i=%d", i), nil)
|
||||
w := httptest.NewRecorder()
|
||||
handler(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
|
||||
assert.Equal(t, string(body), fmt.Sprintf("Fiber %d", i))
|
||||
}, opts)
|
||||
}
|
||||
|
||||
func TestRequestHeaders_module(t *testing.T) { testRequestHeaders(t, &testOptions{}) }
|
||||
func TestRequestHeaders_worker(t *testing.T) {
|
||||
testRequestHeaders(t, &testOptions{workerScript: "request-headers.php"})
|
||||
}
|
||||
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/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)
|
||||
|
||||
resp := w.Result()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
|
||||
assert.Contains(t, string(body), "[Content-Type] => text/plain")
|
||||
assert.Contains(t, string(body), fmt.Sprintf("[Frankenphp-I] => %d", i))
|
||||
}, opts)
|
||||
}
|
||||
|
||||
func TestExecuteScriptCLI(t *testing.T) {
|
||||
if _, err := os.Stat("internal/testcli/testcli"); err != nil {
|
||||
t.Skip("internal/testcli/testcli has not been compiled, run `cd internal/testcli/ && go build`")
|
||||
}
|
||||
|
||||
cmd := exec.Command("internal/testcli/testcli", "testdata/command.php", "foo", "bar")
|
||||
stdoutStderr, err := cmd.CombinedOutput()
|
||||
assert.Error(t, err)
|
||||
|
||||
if exitError, ok := err.(*exec.ExitError); ok {
|
||||
assert.Equal(t, 3, exitError.ExitCode())
|
||||
}
|
||||
|
||||
stdoutStderrStr := string(stdoutStderr)
|
||||
|
||||
assert.Contains(t, stdoutStderrStr, `"foo"`)
|
||||
assert.Contains(t, stdoutStderrStr, `"bar"`)
|
||||
assert.Contains(t, stdoutStderrStr, "From the CLI")
|
||||
}
|
||||
|
||||
func ExampleServeHTTP() {
|
||||
if err := frankenphp.Init(); err != nil {
|
||||
panic(err)
|
||||
@@ -564,10 +622,116 @@ func ExampleServeHTTP() {
|
||||
defer frankenphp.Shutdown()
|
||||
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
req := frankenphp.NewRequestWithContext(r, "/path/to/document/root", nil)
|
||||
req, err := frankenphp.NewRequestWithContext(r, frankenphp.WithRequestDocumentRoot("/path/to/document/root", false))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if err := frankenphp.ServeHTTP(w, req); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
})
|
||||
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||
}
|
||||
|
||||
func ExampleExecuteScriptCLI() {
|
||||
if len(os.Args) <= 1 {
|
||||
log.Println("Usage: my-program script.php")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
os.Exit(frankenphp.ExecuteScriptCLI(os.Args[1], os.Args))
|
||||
}
|
||||
|
||||
func BenchmarkHelloWorld(b *testing.B) {
|
||||
if err := frankenphp.Init(frankenphp.WithLogger(zap.NewNop())); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer frankenphp.Shutdown()
|
||||
cwd, _ := os.Getwd()
|
||||
testDataDir := cwd + "/testdata/"
|
||||
|
||||
handler := func(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := frankenphp.NewRequestWithContext(r, frankenphp.WithRequestDocumentRoot(testDataDir, false))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if err := frankenphp.ServeHTTP(w, req); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("GET", "http://example.com/index.php", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
handler(w, req)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkEcho(b *testing.B) {
|
||||
if err := frankenphp.Init(frankenphp.WithLogger(zap.NewNop())); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer frankenphp.Shutdown()
|
||||
cwd, _ := os.Getwd()
|
||||
testDataDir := cwd + "/testdata/"
|
||||
|
||||
handler := func(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := frankenphp.NewRequestWithContext(r, frankenphp.WithRequestDocumentRoot(testDataDir, false))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := frankenphp.ServeHTTP(w, req); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
const body = `{
|
||||
"squadName": "Super hero squad",
|
||||
"homeTown": "Metro City",
|
||||
"formed": 2016,
|
||||
"secretBase": "Super tower",
|
||||
"active": true,
|
||||
"members": [
|
||||
{
|
||||
"name": "Molecule Man",
|
||||
"age": 29,
|
||||
"secretIdentity": "Dan Jukes",
|
||||
"powers": ["Radiation resistance", "Turning tiny", "Radiation blast"]
|
||||
},
|
||||
{
|
||||
"name": "Madame Uppercut",
|
||||
"age": 39,
|
||||
"secretIdentity": "Jane Wilson",
|
||||
"powers": [
|
||||
"Million tonne punch",
|
||||
"Damage resistance",
|
||||
"Superhuman reflexes"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Eternal Flame",
|
||||
"age": 1000000,
|
||||
"secretIdentity": "Unknown",
|
||||
"powers": [
|
||||
"Immortality",
|
||||
"Heat Immunity",
|
||||
"Inferno",
|
||||
"Teleportation",
|
||||
"Interdimensional travel"
|
||||
]
|
||||
}
|
||||
]
|
||||
}`
|
||||
|
||||
r := strings.NewReader(body)
|
||||
req := httptest.NewRequest("POST", "http://example.com/echo.php", r)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
r.Reset(body)
|
||||
handler(w, req)
|
||||
}
|
||||
}
|
||||
|
||||
10
go.mod
10
go.mod
@@ -1,11 +1,13 @@
|
||||
module github.com/dunglas/frankenphp
|
||||
|
||||
go 1.20
|
||||
go 1.21
|
||||
|
||||
retract v1.0.0-rc.1 // Human error
|
||||
|
||||
require (
|
||||
github.com/stretchr/testify v1.8.1
|
||||
github.com/stretchr/testify v1.8.4
|
||||
go.uber.org/zap v1.26.0
|
||||
golang.org/x/net v0.0.0-20221004154528-8021a29435af
|
||||
golang.org/x/net v0.19.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -13,7 +15,7 @@ require (
|
||||
github.com/kr/pretty v0.2.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
21
go.sum
21
go.sum
@@ -1,4 +1,3 @@
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
||||
@@ -6,27 +5,23 @@ github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
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/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
|
||||
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
|
||||
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
|
||||
golang.org/x/net v0.0.0-20221004154528-8021a29435af h1:wv66FM3rLZGPdxpYL+ApnDe2HzHcTFta3z5nsc13wI4=
|
||||
golang.org/x/net v0.0.0-20221004154528-8021a29435af/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
||||
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
|
||||
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
17
internal/testcli/main.go
Normal file
17
internal/testcli/main.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/dunglas/frankenphp"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) <= 1 {
|
||||
log.Println("Usage: testcli script.php")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
os.Exit(frankenphp.ExecuteScriptCLI(os.Args[1], os.Args))
|
||||
}
|
||||
@@ -20,12 +20,11 @@ func main() {
|
||||
defer frankenphp.Shutdown()
|
||||
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
req, err := frankenphp.NewRequestWithContext(r)
|
||||
if err == nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
req := frankenphp.NewRequestWithContext(r, cwd, nil)
|
||||
if err := frankenphp.ServeHTTP(w, req); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
17
release.sh
17
release.sh
@@ -14,7 +14,12 @@ if ! type "git" > /dev/null; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ $# -ne 1 ]; then
|
||||
if ! type "gh" > /dev/null; then
|
||||
echo "The \"gh\" command must be installed."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ $# -ne 1 ]]; then
|
||||
echo "Usage: ./release.sh version" >&2
|
||||
exit 1
|
||||
fi
|
||||
@@ -37,3 +42,13 @@ git commit -S -a -m "chore: prepare release $1" || echo "skip"
|
||||
git tag -s -m "Version $1" "v$1"
|
||||
git tag -s -m "Version $1" "caddy/v$1"
|
||||
git push --follow-tags
|
||||
|
||||
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
|
||||
|
||||
4
reload_test.sh
Executable file
4
reload_test.sh
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
for ((i = 0 ; i < 100 ; i++)); do
|
||||
curl --no-progress-meter -o /dev/null http://localhost:2019/config/apps/frankenphp -: --no-progress-meter -o /dev/null -H 'Cache-Control: must-revalidate' -H 'Content-Type: application/json' --data-binary '{"workers":[{"file_name":"./index.php"}]}' -X PATCH http://localhost:2019/config/apps/frankenphp
|
||||
done
|
||||
72
request_options.go
Normal file
72
request_options.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package frankenphp
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// RequestOption instances allow to configure a FrankenPHP Request.
|
||||
type RequestOption func(h *FrankenPHPContext) error
|
||||
|
||||
// WithRequestDocumentRoot sets the root directory of the PHP application.
|
||||
// if resolveSymlink is true, oath declared as root directory will be resolved
|
||||
// to its absolute value after the evaluation of any symbolic links.
|
||||
// Due to the nature of PHP opcache, root directory path is cached: when
|
||||
// using a symlinked directory as root this could generate errors when
|
||||
// symlink is changed without PHP being restarted; enabling this
|
||||
// directive will set $_SERVER['DOCUMENT_ROOT'] to the real directory path.
|
||||
func WithRequestDocumentRoot(documentRoot string, resolveSymlink bool) RequestOption {
|
||||
return func(o *FrankenPHPContext) error {
|
||||
// make sure file root is absolute
|
||||
root, err := filepath.Abs(documentRoot)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resolveSymlink {
|
||||
if root, err = filepath.EvalSymlinks(root); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
o.documentRoot = root
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// The path in the URL will be split into two, with the first piece ending
|
||||
// with the value of SplitPath. The first piece will be assumed as the
|
||||
// actual resource (CGI script) name, and the second piece will be set to
|
||||
// PATH_INFO for the CGI script to use.
|
||||
//
|
||||
// Future enhancements should be careful to avoid CVE-2019-11043,
|
||||
// which can be mitigated with use of a try_files-like behavior
|
||||
// that 404s if the fastcgi path info is not found.
|
||||
func WithRequestSplitPath(splitPath []string) RequestOption {
|
||||
return func(o *FrankenPHPContext) error {
|
||||
o.splitPath = splitPath
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithEnv set CGI-like environment variables that will be available in $_SERVER.
|
||||
// Values set with WithEnv always have priority over automatically populated values.
|
||||
func WithRequestEnv(env map[string]string) RequestOption {
|
||||
return func(o *FrankenPHPContext) error {
|
||||
o.env = env
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithLogger sets the logger associated with the current request
|
||||
func WithRequestLogger(logger *zap.Logger) RequestOption {
|
||||
return func(o *FrankenPHPContext) error {
|
||||
o.logger = logger
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
64
smartpointer.go
Normal file
64
smartpointer.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package frankenphp
|
||||
|
||||
// #include <stdlib.h>
|
||||
import "C"
|
||||
import (
|
||||
"runtime/cgo"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
/*
|
||||
FrankenPHP is fairly complex because it shuffles handles/requests/contexts
|
||||
between C and Go. This simplifies the lifecycle management of per-request
|
||||
structures by allowing us to hold references until the end of the request
|
||||
and ensure they are always cleaned up.
|
||||
*/
|
||||
|
||||
// PointerList A list of pointers that can be freed at a later time
|
||||
type pointerList struct {
|
||||
Pointers []unsafe.Pointer
|
||||
}
|
||||
|
||||
// HandleList A list of pointers that can be freed at a later time
|
||||
type handleList struct {
|
||||
Handles []cgo.Handle
|
||||
}
|
||||
|
||||
// AddHandle Call when registering a handle for the very first time
|
||||
func (h *handleList) AddHandle(handle cgo.Handle) {
|
||||
h.Handles = append(h.Handles, handle)
|
||||
}
|
||||
|
||||
// AddPointer Call when creating a request-level C pointer for the very first time
|
||||
func (p *pointerList) AddPointer(ptr unsafe.Pointer) {
|
||||
p.Pointers = append(p.Pointers, ptr)
|
||||
}
|
||||
|
||||
// FreeAll frees all C pointers
|
||||
func (p *pointerList) FreeAll() {
|
||||
for _, ptr := range p.Pointers {
|
||||
C.free(ptr)
|
||||
}
|
||||
p.Pointers = nil // To avoid dangling pointers
|
||||
}
|
||||
|
||||
// FreeAll frees all handles
|
||||
func (h *handleList) FreeAll() {
|
||||
for _, p := range h.Handles {
|
||||
p.Delete()
|
||||
}
|
||||
}
|
||||
|
||||
// Pointers Get a new list of pointers
|
||||
func Pointers() *pointerList {
|
||||
return &pointerList{
|
||||
Pointers: make([]unsafe.Pointer, 0),
|
||||
}
|
||||
}
|
||||
|
||||
// Handles Get a new list of handles
|
||||
func Handles() *handleList {
|
||||
return &handleList{
|
||||
Handles: make([]cgo.Handle, 0, 8),
|
||||
}
|
||||
}
|
||||
@@ -1,48 +1,73 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
FROM golang-base
|
||||
|
||||
ARG FRANKENPHP_VERSION='dev'
|
||||
ARG PHP_VERSION='8.2'
|
||||
ARG PHP_EXTENSIONS='bcmath,calendar,ctype,curl,dba,dom,exif,filter,fileinfo,gd,iconv,intl,mbstring,mbregex,mysqli,mysqlnd,opcache,openssl,pcntl,pdo,pdo_mysql,pdo_pgsql,pdo_sqlite,pgsql,phar,posix,readline,redis,session,simplexml,sockets,sqlite3,tokenizer,xml,xmlreader,xmlwriter,zip,zlib,apcu'
|
||||
ARG FRANKENPHP_VERSION=''
|
||||
ENV FRANKENPHP_VERSION=${FRANKENPHP_VERSION}
|
||||
|
||||
ARG PHP_VERSION=''
|
||||
ENV PHP_VERSION=${PHP_VERSION}
|
||||
|
||||
ARG PHP_EXTENSIONS=''
|
||||
ENV PHP_EXTENSIONS=${PHP_EXTENSIONS}
|
||||
|
||||
ARG PHP_EXTENSION_LIBS=''
|
||||
ENV PHP_EXTENSION_LIBS=${PHP_EXTENSION_LIBS}
|
||||
|
||||
ARG CLEAN=''
|
||||
ARG EMBED=''
|
||||
SHELL ["/bin/ash", "-eo", "pipefail", "-c"]
|
||||
|
||||
LABEL org.opencontainers.image.title=FrankenPHP
|
||||
LABEL org.opencontainers.image.description="The modern PHP app server"
|
||||
LABEL org.opencontainers.image.url=https://frankenphp.dev
|
||||
LABEL org.opencontainers.image.source=https://github.com/dunglas/frankenphp
|
||||
LABEL org.opencontainers.image.licenses=MIT
|
||||
LABEL org.opencontainers.image.vendor="Kévin Dunglas"
|
||||
|
||||
RUN apk update; \
|
||||
apk add --no-cache \
|
||||
autoconf \
|
||||
automake \
|
||||
bash \
|
||||
binutils \
|
||||
bison \
|
||||
build-base \
|
||||
cmake \
|
||||
curl \
|
||||
file \
|
||||
flex \
|
||||
g++ \
|
||||
gcc \
|
||||
git \
|
||||
jq \
|
||||
libgcc \
|
||||
libstdc++ \
|
||||
linux-headers \
|
||||
m4 \
|
||||
make \
|
||||
php82 \
|
||||
php82-common \
|
||||
php82-curl \
|
||||
php82-dom \
|
||||
php82-mbstring \
|
||||
php82-openssl \
|
||||
php82-pcntl \
|
||||
php82-phar \
|
||||
php82-posix \
|
||||
php82-sodium \
|
||||
php82-tokenizer \
|
||||
php82-xml \
|
||||
php82-xmlwriter \
|
||||
pkgconfig \
|
||||
wget \
|
||||
xz ; \
|
||||
ln -sf /usr/bin/php82 /usr/bin/php
|
||||
apk add --no-cache \
|
||||
autoconf \
|
||||
automake \
|
||||
bash \
|
||||
binutils \
|
||||
bison \
|
||||
build-base \
|
||||
cmake \
|
||||
curl \
|
||||
file \
|
||||
flex \
|
||||
g++ \
|
||||
gcc \
|
||||
git \
|
||||
jq \
|
||||
libgcc \
|
||||
libstdc++ \
|
||||
libtool \
|
||||
linux-headers \
|
||||
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 \
|
||||
php83-curl \
|
||||
php83-dom \
|
||||
php83-mbstring \
|
||||
php83-openssl \
|
||||
php83-pcntl \
|
||||
php83-phar \
|
||||
php83-posix \
|
||||
php83-session \
|
||||
php83-sodium \
|
||||
php83-tokenizer \
|
||||
php83-xml \
|
||||
php83-xmlwriter; \
|
||||
ln -sf /usr/bin/php83 /usr/bin/php
|
||||
|
||||
# https://getcomposer.org/doc/03-cli.md#composer-allow-superuser
|
||||
ENV COMPOSER_ALLOW_SUPERUSER=1
|
||||
@@ -50,34 +75,17 @@ ENV PATH="${PATH}:/root/.composer/vendor/bin"
|
||||
|
||||
COPY --from=composer/composer:2-bin --link /composer /usr/bin/composer
|
||||
|
||||
WORKDIR /static-php-cli
|
||||
|
||||
RUN git clone --depth=1 https://github.com/crazywhalecc/static-php-cli . && \
|
||||
composer install --no-cache --no-dev --classmap-authoritative
|
||||
|
||||
RUN --mount=type=secret,id=github-token GITHUB_TOKEN=$(cat /run/secrets/github-token) ./bin/spc download --with-php=$PHP_VERSION --all
|
||||
|
||||
RUN ./bin/spc build --build-embed --enable-zts "$PHP_EXTENSIONS"
|
||||
|
||||
ENV PATH="/static-php-cli/buildroot/bin:/static-php-cli/buildroot/usr/bin:$PATH"
|
||||
|
||||
WORKDIR /go/src/app
|
||||
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod graph | awk '{if ($1 !~ "@") print $2}' | xargs go get
|
||||
|
||||
RUN mkdir caddy && cd caddy
|
||||
COPY caddy/go.mod caddy/go.sum ./caddy/
|
||||
|
||||
RUN cd caddy && go mod graph | awk '{if ($1 !~ "@") print $2}' | xargs go get
|
||||
WORKDIR /go/src/app/caddy
|
||||
COPY caddy/go.mod caddy/go.sum ./
|
||||
RUN go mod graph | awk '{if ($1 !~ "@") print $2}' | xargs go get
|
||||
|
||||
WORKDIR /go/src/app
|
||||
COPY *.* ./
|
||||
COPY caddy caddy
|
||||
COPY C-Thread-Pool C-Thread-Pool
|
||||
|
||||
RUN cd caddy/frankenphp && \
|
||||
CGO_CFLAGS="$(/static-php-cli/buildroot/bin/php-config --includes | sed s#-I/#-I/static-php-cli/buildroot/#g)" \
|
||||
CGO_LDFLAGS="-DFRANKENPHP_VERSION=$FRANKENPHP_VERSION $(/static-php-cli/buildroot/bin/php-config --ldflags) $(/static-php-cli/buildroot/bin/php-config --libs | sed -e 's/-lgcc_s//g')" \
|
||||
LIBPHP_VERSION="$(/static-php-cli/buildroot/bin/php-config --version)" \
|
||||
go build -buildmode=pie -tags "cgo netgo osusergo static_build" -ldflags "-linkmode=external -extldflags -static-pie -s -w -X 'github.com/caddyserver/caddy/v2.CustomVersion=FrankenPHP $FRANKENPHP_VERSION PHP $LIBPHP_VERSION Caddy'" && \
|
||||
./frankenphp version
|
||||
RUN --mount=type=secret,id=github-token GITHUB_TOKEN=$(cat /run/secrets/github-token) ./build-static.sh
|
||||
|
||||
3
testdata/Caddyfile
vendored
3
testdata/Caddyfile
vendored
@@ -23,10 +23,11 @@ http:// {
|
||||
}
|
||||
rewrite @indexFiles {http.matchers.file.relative}
|
||||
|
||||
encode zstd br gzip
|
||||
|
||||
# FrankenPHP!
|
||||
@phpFiles path *.php
|
||||
php @phpFiles
|
||||
encode zstd gzip
|
||||
file_server
|
||||
|
||||
respond 404
|
||||
|
||||
12
testdata/benchmark.Caddyfile
vendored
Normal file
12
testdata/benchmark.Caddyfile
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
frankenphp
|
||||
}
|
||||
|
||||
http:// {
|
||||
route {
|
||||
root * .
|
||||
|
||||
encode zstd br gzip
|
||||
php_server
|
||||
}
|
||||
}
|
||||
6
testdata/command.php
vendored
Normal file
6
testdata/command.php
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
|
||||
var_dump($argv, $_SERVER);
|
||||
echo "From the CLI\n";
|
||||
|
||||
exit(3);
|
||||
5
testdata/echo.php
vendored
Normal file
5
testdata/echo.php
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
header('Content-Type: text/plain');
|
||||
|
||||
echo file_get_contents('php://input');
|
||||
12
testdata/fiber-no-cgo.php
vendored
Normal file
12
testdata/fiber-no-cgo.php
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
require_once __DIR__.'/_executor.php';
|
||||
|
||||
return function() {
|
||||
$fiber = new Fiber(function() {
|
||||
Fiber::suspend('Fiber '.($_GET['i'] ?? ''));
|
||||
});
|
||||
echo $fiber->start();
|
||||
|
||||
$fiber->resume();
|
||||
};
|
||||
|
||||
1
testdata/hello.txt
vendored
Normal file
1
testdata/hello.txt
vendored
Normal file
@@ -0,0 +1 @@
|
||||
Hello
|
||||
7
testdata/large-response.php
vendored
Normal file
7
testdata/large-response.php
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__.'/_executor.php';
|
||||
|
||||
return function () {
|
||||
echo str_repeat("Hey\n", 1024);
|
||||
};
|
||||
66
testdata/load-test.js
vendored
Normal file
66
testdata/load-test.js
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
import http from 'k6/http'
|
||||
import { check } from 'k6'
|
||||
|
||||
export const options = {
|
||||
// A number specifying the number of VUs to run concurrently.
|
||||
vus: 100,
|
||||
// A string specifying the total duration of the test run.
|
||||
duration: '30s'
|
||||
|
||||
// The following section contains configuration options for execution of this
|
||||
// test script in Grafana Cloud.
|
||||
//
|
||||
// See https://grafana.com/docs/grafana-cloud/k6/get-started/run-cloud-tests-from-the-cli/
|
||||
// to learn about authoring and running k6 test scripts in Grafana k6 Cloud.
|
||||
//
|
||||
// ext: {
|
||||
// loadimpact: {
|
||||
// // The ID of the project to which the test is assigned in the k6 Cloud UI.
|
||||
// // By default tests are executed in default project.
|
||||
// projectID: "",
|
||||
// // The name of the test in the k6 Cloud UI.
|
||||
// // Test runs with the same name will be grouped.
|
||||
// name: "script.js"
|
||||
// }
|
||||
// },
|
||||
|
||||
// Uncomment this section to enable the use of Browser API in your tests.
|
||||
//
|
||||
// See https://grafana.com/docs/k6/latest/using-k6-browser/running-browser-tests/ to learn more
|
||||
// about using Browser API in your test scripts.
|
||||
//
|
||||
// scenarios: {
|
||||
// // The scenario name appears in the result summary, tags, and so on.
|
||||
// // You can give the scenario any name, as long as each name in the script is unique.
|
||||
// ui: {
|
||||
// // Executor is a mandatory parameter for browser-based tests.
|
||||
// // Shared iterations in this case tells k6 to reuse VUs to execute iterations.
|
||||
// //
|
||||
// // See https://grafana.com/docs/k6/latest/using-k6/scenarios/executors/ for other executor types.
|
||||
// executor: 'shared-iterations',
|
||||
// options: {
|
||||
// browser: {
|
||||
// // This is a mandatory parameter that instructs k6 to launch and
|
||||
// // connect to a chromium-based browser, and use it to run UI-based
|
||||
// // tests.
|
||||
// type: 'chromium',
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// }
|
||||
}
|
||||
|
||||
const payload = 'foo\n'.repeat(1000)
|
||||
|
||||
// The function that defines VU logic.
|
||||
//
|
||||
// See https://grafana.com/docs/k6/latest/examples/get-started-with-k6/ to learn more
|
||||
// about authoring k6 scripts.
|
||||
//
|
||||
export default function () {
|
||||
const res = http.post('http://localhost/echo.php', payload)
|
||||
check(res, {
|
||||
'is status 200': (r) => r.status === 200,
|
||||
'is echoed': (r) => r.body === payload
|
||||
})
|
||||
}
|
||||
7
testdata/request-headers.php
vendored
Normal file
7
testdata/request-headers.php
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__.'/_executor.php';
|
||||
|
||||
return function() {
|
||||
print_r(apache_request_headers());
|
||||
};
|
||||
13
testdata/response-headers.php
vendored
Normal file
13
testdata/response-headers.php
vendored
Normal 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());
|
||||
};
|
||||
68
worker.go
68
worker.go
@@ -4,7 +4,6 @@ package frankenphp
|
||||
// #include "frankenphp.h"
|
||||
import "C"
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
@@ -50,44 +49,37 @@ func startWorkers(fileName string, nbWorkers int, env map[string]string) error {
|
||||
errs []error
|
||||
)
|
||||
|
||||
if env == nil {
|
||||
env = make(map[string]string, 1)
|
||||
}
|
||||
|
||||
env["FRANKENPHP_WORKER"] = "1"
|
||||
|
||||
l := getLogger()
|
||||
for i := 0; i < nbWorkers; i++ {
|
||||
go func() {
|
||||
defer shutdownWG.Done()
|
||||
for {
|
||||
// Create main dummy request
|
||||
fc := &FrankenPHPContext{
|
||||
Env: make(map[string]string, len(env)+1),
|
||||
}
|
||||
fc.Env["SCRIPT_FILENAME"] = absFileName
|
||||
for k, v := range env {
|
||||
fc.Env[k] = v
|
||||
}
|
||||
|
||||
r, err := http.NewRequestWithContext(context.WithValue(
|
||||
context.Background(),
|
||||
contextKey,
|
||||
fc,
|
||||
), "GET", "", nil)
|
||||
|
||||
r, err := http.NewRequest(http.MethodGet, filepath.Base(absFileName), nil)
|
||||
if err != nil {
|
||||
// TODO: this should never happen, maybe can we remove this block?
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
errs = append(errs, fmt.Errorf("workers %q: unable to create main worker request: %w", absFileName, err))
|
||||
|
||||
return
|
||||
panic(err)
|
||||
}
|
||||
r, err = NewRequestWithContext(
|
||||
r,
|
||||
WithRequestDocumentRoot(filepath.Dir(absFileName), false),
|
||||
WithRequestEnv(env),
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
l.Debug("starting", zap.String("worker", absFileName))
|
||||
if err := ServeHTTP(nil, r); err != nil {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
errs = append(errs, fmt.Errorf("workers %q: unable to start: %w", absFileName, err))
|
||||
|
||||
return
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fc := r.Context().Value(contextKey).(*FrankenPHPContext)
|
||||
if fc.currentWorkerRequest != 0 {
|
||||
// Terminate the pending HTTP request handled by the worker
|
||||
maybeCloseContext(fc.currentWorkerRequest.Value().(*http.Request).Context().Value(contextKey).(*FrankenPHPContext))
|
||||
@@ -98,7 +90,11 @@ func startWorkers(fileName string, nbWorkers int, env map[string]string) error {
|
||||
// TODO: make the max restart configurable
|
||||
if _, ok := workersRequestChans.Load(absFileName); ok {
|
||||
workersReadyWG.Add(1)
|
||||
l.Error("unexpected termination, restarting", zap.String("worker", absFileName))
|
||||
if fc.exitStatus == 0 {
|
||||
l.Info("restarting", zap.String("worker", absFileName))
|
||||
} else {
|
||||
l.Error("unexpected termination, restarting", zap.String("worker", absFileName), zap.Int("exit_status", int(fc.exitStatus)))
|
||||
}
|
||||
} else {
|
||||
break
|
||||
}
|
||||
@@ -138,7 +134,7 @@ func go_frankenphp_worker_handle_request_start(mrh C.uintptr_t) C.uintptr_t {
|
||||
mainRequest := cgo.Handle(mrh).Value().(*http.Request)
|
||||
fc := mainRequest.Context().Value(contextKey).(*FrankenPHPContext)
|
||||
|
||||
v, ok := workersRequestChans.Load(fc.Env["SCRIPT_FILENAME"])
|
||||
v, ok := workersRequestChans.Load(fc.scriptFilename)
|
||||
if !ok {
|
||||
// Probably shutting down
|
||||
return 0
|
||||
@@ -148,23 +144,24 @@ func go_frankenphp_worker_handle_request_start(mrh C.uintptr_t) C.uintptr_t {
|
||||
|
||||
l := getLogger()
|
||||
|
||||
l.Debug("waiting for request", zap.String("worker", fc.Env["SCRIPT_FILENAME"]))
|
||||
l.Debug("waiting for request", zap.String("worker", fc.scriptFilename))
|
||||
|
||||
var r *http.Request
|
||||
select {
|
||||
case <-done:
|
||||
l.Debug("shutting down", zap.String("worker", fc.Env["SCRIPT_FILENAME"]))
|
||||
l.Debug("shutting down", zap.String("worker", fc.scriptFilename))
|
||||
|
||||
return 0
|
||||
case r = <-rc:
|
||||
}
|
||||
|
||||
fc.currentWorkerRequest = cgo.NewHandle(r)
|
||||
r.Context().Value(handleKey).(*handleList).AddHandle(fc.currentWorkerRequest)
|
||||
|
||||
l.Debug("request handling started", zap.String("worker", fc.Env["SCRIPT_FILENAME"]), zap.String("url", r.RequestURI))
|
||||
l.Debug("request handling started", zap.String("worker", fc.scriptFilename), zap.String("url", r.RequestURI))
|
||||
if err := updateServerContext(r, false, mrh); err != nil {
|
||||
// Unexpected error
|
||||
l.Debug("unexpected error", zap.String("worker", fc.Env["SCRIPT_FILENAME"]), zap.String("url", r.RequestURI), zap.Error(err))
|
||||
l.Debug("unexpected error", zap.String("worker", fc.scriptFilename), zap.String("url", r.RequestURI), zap.Error(err))
|
||||
|
||||
return 0
|
||||
}
|
||||
@@ -179,8 +176,7 @@ func go_frankenphp_finish_request(mrh, rh C.uintptr_t, deleteHandle bool) {
|
||||
fc := r.Context().Value(contextKey).(*FrankenPHPContext)
|
||||
|
||||
if deleteHandle {
|
||||
rHandle.Delete()
|
||||
|
||||
r.Context().Value(handleKey).(*handleList).FreeAll()
|
||||
cgo.Handle(mrh).Value().(*http.Request).Context().Value(contextKey).(*FrankenPHPContext).currentWorkerRequest = 0
|
||||
}
|
||||
|
||||
@@ -188,10 +184,10 @@ func go_frankenphp_finish_request(mrh, rh C.uintptr_t, deleteHandle bool) {
|
||||
|
||||
var fields []zap.Field
|
||||
if mrh == 0 {
|
||||
fields = append(fields, zap.String("worker", fc.Env["SCRIPT_FILENAME"]), zap.String("url", r.RequestURI))
|
||||
fields = append(fields, zap.String("worker", fc.scriptFilename), zap.String("url", r.RequestURI))
|
||||
} else {
|
||||
fields = append(fields, zap.String("url", r.RequestURI))
|
||||
}
|
||||
|
||||
fc.Logger.Debug("request handling finished", fields...)
|
||||
fc.logger.Debug("request handling finished", fields...)
|
||||
}
|
||||
|
||||
@@ -98,7 +98,11 @@ func ExampleServeHTTP_workers() {
|
||||
defer frankenphp.Shutdown()
|
||||
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
req := frankenphp.NewRequestWithContext(r, "/path/to/document/root", nil)
|
||||
req, err := frankenphp.NewRequestWithContext(r, frankenphp.WithRequestDocumentRoot("/path/to/document/root", false))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if err := frankenphp.ServeHTTP(w, req); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user