Merge branch 'main' into refactor/remove-new-req-with-context

This commit is contained in:
Alliballibaba
2026-03-07 12:38:26 +01:00
118 changed files with 6767 additions and 1569 deletions

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
* text=auto eol=lf

View File

@@ -7,7 +7,7 @@ USE_LATEST_PHP="${USE_LATEST_PHP:-0}"
if [[ -z "${GO_VERSION}" ]]; then
GO_VERSION="$(awk -F'"' '/variable "GO_VERSION"/ {f=1} f && /default/ {print $2; exit}' docker-bake.hcl)"
GO_VERSION="${GO_VERSION:-1.25}"
GO_VERSION="${GO_VERSION:-1.26}"
fi
if [[ -z "${PHP_VERSION}" ]]; then

View File

@@ -1,5 +1,5 @@
---
name: Build Docker images
name: Build Docker Images
concurrency:
cancel-in-progress: true
group: ${{ github.workflow }}-${{ github.ref }}
@@ -191,8 +191,10 @@ jobs:
- name: Run tests
if: ${{ !fromJson(needs.prepare.outputs.push) }}
run: |
# TODO: remove "containerimage.config.digest" fallback once all runners use buildx v0.18+
# which replaced it with "containerimage.digest" and "containerimage.descriptor"
docker run --platform="${PLATFORM}" --rm \
"$(jq -r ".\"builder-${VARIANT}\".\"containerimage.config.digest\"" <<< "${METADATA}")" \
"$(jq -r ".\"builder-${VARIANT}\" | .\"containerimage.config.digest\" // .\"containerimage.digest\"" <<< "${METADATA}")" \
sh -c "./go.sh test ${RACE} -v $(./go.sh list ./... | grep -v github.com/dunglas/frankenphp/internal/testext | grep -v github.com/dunglas/frankenphp/internal/extgen | tr '\n' ' ') && cd caddy && ../go.sh test ${RACE} -v ./..."
env:
METADATA: ${{ steps.build.outputs.metadata }}

22
.github/workflows/docs.yaml vendored Normal file
View File

@@ -0,0 +1,22 @@
---
name: Deploy Docs
on:
push:
branches:
- main
paths:
- "docs/**"
- "README.md"
- "CONTRIBUTING.md"
permissions: {}
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
deploy:
runs-on: ubuntu-slim
steps:
- name: Trigger website deployment
env:
GH_TOKEN: ${{ secrets.WEBSITE_DEPLOY_TOKEN }}
run: gh api repos/dunglas/frankenphp-website/actions/workflows/hugo.yaml/dispatches -f ref=main

View File

@@ -35,7 +35,7 @@ jobs:
USE_ZEND_ALLOC: 0
LIBRARY_PATH: ${{ github.workspace }}/php/target/lib:${{ github.workspace }}/watcher/target/lib
LD_LIBRARY_PATH: ${{ github.workspace }}/php/target/lib
# PHP doesn't free some memory on purpose, we have to disable leaks detection: https://go.dev/doc/go1.25#go-command
# PHP doesn't free some memory on purpose, we have to disable leaks detection: https://go.dev/doc/go1.26#go-command
ASAN_OPTIONS: detect_leaks=0
steps:
- name: Remove local PHP
@@ -45,14 +45,14 @@ jobs:
persist-credentials: false
- uses: actions/setup-go@v6
with:
go-version: "1.25"
go-version: "1.26"
cache-dependency-path: |
go.sum
caddy/go.sum
- name: Determine PHP version
id: determine-php-version
run: |
curl -fsSL 'https://www.php.net/releases/index.php?json&max=1&version=8.5' -o version.json
curl -fsSL 'https://www.php.net/releases/index.php?json&max=1&version=8.5' -o version.json 2>/dev/null || curl -fsSL 'https://phpmirror.static-php.dev/releases/index.php?json&max=1&version=8.5' -o version.json
echo version="$(jq -r 'keys[0]' version.json)" >> "$GITHUB_OUTPUT"
echo archive="$(jq -r '.[] .source[] | select(.filename |endswith(".xz")) | "https://www.php.net/distributions/" + .filename' version.json)" >> "$GITHUB_OUTPUT"
- name: Cache PHP
@@ -65,7 +65,8 @@ jobs:
name: Compile PHP
run: |
mkdir php/
curl -fsSL "${URL}" | tar -Jx -C php --strip-components=1
MIRROR_URL=${URL/https:\/\/www.php.net/https:\/\/phpmirror.static-php.dev}
(curl -fsSL "${URL}" || curl -fsSL "${MIRROR_URL}") | tar -Jx -C php --strip-components=1
cd php/
./configure \
CFLAGS="$CFLAGS" \

View File

@@ -179,7 +179,9 @@ jobs:
- name: Copy binary
run: |
# shellcheck disable=SC2034
digest=$(jq -r '."static-builder-musl"."${{ (fromJson(needs.prepare.outputs.push) && !matrix.debug && !matrix.mimalloc) && 'containerimage.digest' || 'containerimage.config.digest' }}"' <<< "${METADATA}")
# TODO: remove "containerimage.config.digest" fallback once all runners use buildx v0.18+
# which replaced it with "containerimage.digest" and "containerimage.descriptor"
digest=$(jq -r '."static-builder-musl" | ${{ (fromJson(needs.prepare.outputs.push) && !matrix.debug && !matrix.mimalloc) && '."containerimage.digest"' || '(."containerimage.config.digest" // ."containerimage.digest")' }}' <<< "${METADATA}")
docker create --platform="${PLATFORM}" --name static-builder-musl "${{ (fromJson(needs.prepare.outputs.push) && !matrix.debug && !matrix.mimalloc) && '${IMAGE_NAME}@${digest}' || '${digest}' }}"
docker cp "static-builder-musl:/go/src/app/dist/${BINARY}" "${BINARY}${{ matrix.debug && '-debug' || '' }}${{ matrix.mimalloc && '-mimalloc' || '' }}"
env:
@@ -330,7 +332,9 @@ jobs:
- name: Copy all frankenphp* files
run: |
# shellcheck disable=SC2034
digest=$(jq -r '."static-builder-gnu"."${{ fromJson(needs.prepare.outputs.push) && 'containerimage.digest' || 'containerimage.config.digest' }}"' <<< "${METADATA}")
# TODO: remove "containerimage.config.digest" fallback once all runners use buildx v0.18+
# which replaced it with "containerimage.digest" and "containerimage.descriptor"
digest=$(jq -r '."static-builder-gnu" | ${{ fromJson(needs.prepare.outputs.push) && '."containerimage.digest"' || '(."containerimage.config.digest" // ."containerimage.digest")' }}' <<< "${METADATA}")
container_id=$(docker create --platform="${PLATFORM}" "${{ fromJson(needs.prepare.outputs.push) && '${IMAGE_NAME}@${digest}' || '${digest}' }}")
mkdir -p gh-output
cd gh-output
@@ -449,12 +453,12 @@ jobs:
ref: ${{ needs.prepare.outputs.ref }}
persist-credentials: false
- uses: actions/setup-go@v6
with:
go-version: "1.25"
with: # zizmor: ignore[cache-poisoning]
go-version: "1.26"
cache-dependency-path: |
go.sum
caddy/go.sum
cache: false
cache: ${{ github.event_name != 'release' }}
- name: Set FRANKENPHP_VERSION
run: |
if [ "${GITHUB_REF_TYPE}" == "tag" ]; then

View File

@@ -35,13 +35,14 @@ jobs:
env:
GOMAXPROCS: 10
LIBRARY_PATH: ${{ github.workspace }}/watcher/target/lib
GOFLAGS: "-tags=nobadger,nomysql,nopgx"
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: actions/setup-go@v6
with:
go-version: "1.25"
go-version: "1.26"
cache-dependency-path: |
go.sum
caddy/go.sum
@@ -69,7 +70,7 @@ jobs:
run: ./frankenphp.test -test.v
- name: Run Caddy module tests
working-directory: caddy/
run: go test -tags nobadger,nomysql,nopgx -race -v ./...
run: go test -race -v ./...
- name: Run Fuzzing Tests
working-directory: caddy/
run: go test -fuzz FuzzRequest -fuzztime 20s
@@ -100,13 +101,15 @@ jobs:
fail-fast: false
matrix:
php-versions: ["8.3", "8.4", "8.5"]
env:
XCADDY_GO_BUILD_FLAGS: "-tags=nobadger,nomysql,nopgx"
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: actions/setup-go@v6
with:
go-version: "1.25"
go-version: "1.26"
cache-dependency-path: |
go.sum
caddy/go.sum
@@ -126,7 +129,7 @@ jobs:
- name: Download PHP sources
run: |
PHP_VERSION=$(php -r "echo PHP_VERSION;")
wget -q "https://www.php.net/distributions/php-${PHP_VERSION}.tar.gz"
wget -q "https://www.php.net/distributions/php-${PHP_VERSION}.tar.gz" || wget -q "https://phpmirror.static-php.dev/distributions/php-${PHP_VERSION}.tar.gz"
tar xzf "php-${PHP_VERSION}.tar.gz"
echo "GEN_STUB_SCRIPT=${PWD}/php-${PHP_VERSION}/build/gen_stub.php" >> "${GITHUB_ENV}"
- name: Set CGO flags
@@ -141,13 +144,14 @@ jobs:
runs-on: macos-latest
env:
HOMEBREW_NO_AUTO_UPDATE: 1
GOFLAGS: "-tags=nowatcher,nobadger,nomysql,nopgx"
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: actions/setup-go@v6
with:
go-version: "1.25"
go-version: "1.26"
cache-dependency-path: |
go.sum
caddy/go.sum
@@ -172,4 +176,4 @@ jobs:
run: go test -tags nowatcher -race -v ./...
- name: Run Caddy module tests
working-directory: caddy/
run: go test -tags nowatcher,nobadger,nomysql,nopgx -race -v ./...
run: go test -race -v ./...

214
.github/workflows/windows.yaml vendored Normal file
View File

@@ -0,0 +1,214 @@
---
name: Build Windows release
concurrency:
cancel-in-progress: true
group: ${{ github.workflow }}-${{ github.ref }}
on:
pull_request:
branches:
- main
paths-ignore:
- "docs/**"
push:
branches:
- main
tags:
- v*.*.*
paths-ignore:
- "docs/**"
workflow_dispatch:
inputs:
#checkov:skip=CKV_GHA_7
version:
description: "FrankenPHP version"
required: false
type: string
schedule:
- cron: "0 8 * * *"
permissions:
contents: read
env:
GOTOOLCHAIN: local
GOFLAGS: "-ldflags=-extldflags=-fuse-ld=lld -tags=nobadger,nomysql,nopgx"
PHP_DOWNLOAD_BASE: "https://downloads.php.net/~windows/releases/"
CC: clang
CXX: clang++
jobs:
build:
runs-on: windows-latest
steps:
- name: Configure Git
run: |
git config --global core.autocrlf false
git config --global core.eol lf
- name: Checkout Code
uses: actions/checkout@v6
with:
path: frankenphp
persist-credentials: false
- name: Set FRANKENPHP_VERSION
run: |
if ($env:GITHUB_REF_TYPE -eq "tag") {
$frankenphpVersion = $env:GITHUB_REF_NAME.Substring(1)
} elseif ($env:GITHUB_EVENT_NAME -eq "schedule") {
$frankenphpVersion = $env:GITHUB_REF
} else {
$frankenphpVersion = $env:GITHUB_SHA
}
"FRANKENPHP_VERSION=$frankenphpVersion" >> $env:GITHUB_ENV
- name: Setup Go
uses: actions/setup-go@v6
with: # zizmor: ignore[cache-poisoning]
go-version: "1.26"
cache-dependency-path: |
frankenphp/go.sum
frankenphp/caddy/go.sum
cache: ${{ github.event_name != 'release' }}
check-latest: true
- name: Install Vcpkg Libraries
working-directory: frankenphp
run: "vcpkg install"
- name: Download Watcher
run: |
$latestTag = gh release list --repo e-dant/watcher --limit 1 --exclude-drafts --exclude-pre-releases --json tagName --jq '.[0].tagName'
Write-Host "Latest Watcher version: $latestTag"
gh release download $latestTag --repo e-dant/watcher --pattern "*x86_64-pc-windows-msvc.tar" -O watcher.tar
tar -xf "watcher.tar" -C "$env:GITHUB_WORKSPACE"
Rename-Item -Path "$env:GITHUB_WORKSPACE\x86_64-pc-windows-msvc" -NewName "watcher"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Download PHP
run: |
$webContent = Invoke-WebRequest -Uri $env:PHP_DOWNLOAD_BASE
$links = $webContent.Links.Href | Where-Object { $_ -match "php-\d+\.\d+\.\d+-Win32-vs17-x64\.zip$" }
if (-not $links) { throw "Could not find PHP zip files at $env:PHP_DOWNLOAD_BASE" }
$latestFile = $links | Sort-Object { if ($_ -match '(\d+\.\d+\.\d+)') { [version]$matches[1] } } | Select-Object -Last 1
$version = if ($latestFile -match '(\d+\.\d+\.\d+)') { $matches[1] }
Write-Host "Detected latest PHP version: $version"
"PHP_VERSION=$version" >> $env:GITHUB_ENV
$phpZip = "php-$version-Win32-vs17-x64.zip"
$develZip = "php-devel-pack-$version-Win32-vs17-x64.zip"
$dirName = "frankenphp-$env:FRANKENPHP_VERSION-php-$version-Win32-vs17-x64"
"DIR_NAME=$dirName" >> $env:GITHUB_ENV
Invoke-WebRequest -Uri "$env:PHP_DOWNLOAD_BASE/$phpZip" -OutFile "$env:TEMP\php.zip"
Expand-Archive -Path "$env:TEMP\php.zip" -DestinationPath "$env:GITHUB_WORKSPACE\$dirName"
Invoke-WebRequest -Uri "$env:PHP_DOWNLOAD_BASE/$develZip" -OutFile "$env:TEMP\php-devel.zip"
Expand-Archive -Path "$env:TEMP\php-devel.zip" -DestinationPath "$env:GITHUB_WORKSPACE\php-devel"
- name: Prepare env
run: |
$vcpkgRoot = "$env:GITHUB_WORKSPACE\frankenphp\vcpkg_installed\x64-windows"
$watcherRoot = "$env:GITHUB_WORKSPACE\watcher"
$phpBin = "$env:GITHUB_WORKSPACE\$env:DIR_NAME"
$phpDevel = "$env:GITHUB_WORKSPACE\php-devel\php-$env:PHP_VERSION-devel-vs17-x64"
"C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Tools\Llvm\bin" >> $env:GITHUB_PATH
"$vcpkgRoot\bin" >> $env:GITHUB_PATH
"$watcherRoot" >> $env:GITHUB_PATH
"$phpBin" >> $env:GITHUB_PATH
"CGO_CFLAGS=-DFRANKENPHP_VERSION=$env:FRANKENPHP_VERSION -I$vcpkgRoot\include -I$watcherRoot -I$phpDevel\include -I$phpDevel\include\main -I$phpDevel\include\TSRM -I$phpDevel\include\Zend -I$phpDevel\include\ext" >> $env:GITHUB_ENV
"CGO_LDFLAGS=-L$vcpkgRoot\lib -lbrotlienc -L$watcherRoot -llibwatcher-c -L$phpBin -L$phpDevel\lib -lphp8ts -lphp8embed" >> $env:GITHUB_ENV
- name: Embed Windows icon and metadata
working-directory: frankenphp\caddy\frankenphp
run: |
go install github.com/josephspurrier/goversioninfo/cmd/goversioninfo@latest
$major = 0; $minor = 0; $patch = 0; $build = 0
if ($env:FRANKENPHP_VERSION -match '^(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)$') {
$major = [int]$Matches['major']
$minor = [int]$Matches['minor']
$patch = [int]$Matches['patch']
}
$json = @{
FixedFileInfo = @{
FileVersion = @{ Major = $major; Minor = $minor; Patch = $patch; Build = $build }
ProductVersion = @{ Major = $major; Minor = $minor; Patch = $patch; Build = $build }
}
StringFileInfo = @{
CompanyName = "FrankenPHP"
FileDescription = "The modern PHP app server"
FileVersion = $env:FRANKENPHP_VERSION
InternalName = "frankenphp"
OriginalFilename = "frankenphp.exe"
LegalCopyright = "(c) 2022 Kévin Dunglas, MIT License"
ProductName = "FrankenPHP"
ProductVersion = $env:FRANKENPHP_VERSION
Comments = "https://frankenphp.dev/"
}
VarFileInfo = @{
Translation = @{ LangID = 9; CharsetID = 1200 }
}
} | ConvertTo-Json -Depth 10
$json | Set-Content "versioninfo.json"
goversioninfo -64 -icon ..\..\frankenphp.ico versioninfo.json -o resource.syso
- name: Build FrankenPHP
run: |
$customVersion = "FrankenPHP $env:FRANKENPHP_VERSION PHP $env:PHP_VERSION Caddy"
go build -ldflags="-extldflags=-fuse-ld=lld -X 'github.com/caddyserver/caddy/v2.CustomVersion=$customVersion' -X 'github.com/caddyserver/caddy/v2/modules/caddyhttp.ServerHeader=FrankenPHP Caddy'"
working-directory: frankenphp\caddy\frankenphp
- name: Create Directory
run: |
Copy-Item frankenphp\caddy\frankenphp\frankenphp.exe $env:DIR_NAME
Copy-Item watcher\libwatcher-c.dll $env:DIR_NAME
Copy-Item frankenphp\vcpkg_installed\x64-windows\bin\brotlienc.dll $env:DIR_NAME
Copy-Item frankenphp\vcpkg_installed\x64-windows\bin\brotlidec.dll $env:DIR_NAME
Copy-Item frankenphp\vcpkg_installed\x64-windows\bin\brotlicommon.dll $env:DIR_NAME
Copy-Item frankenphp\vcpkg_installed\x64-windows\bin\pthreadVC3.dll $env:DIR_NAME
- name: Upload Artifact
if: github.event_name != 'release'
uses: actions/upload-artifact@v6
with:
name: ${{ env.DIR_NAME }}
path: ${{ env.DIR_NAME }}
if-no-files-found: error
- name: Zip Release Artifact
if: github.event_name == 'release'
run: Compress-Archive -Path "$env:DIR_NAME\*" -DestinationPath "$env:DIR_NAME.zip"
- name: Upload Release Asset
if: github.event_name == 'release'
run: gh release upload $env:GITHUB_EVENT_RELEASE_TAG_NAME "$env:DIR_NAME.zip" --clobber
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_EVENT_RELEASE_TAG_NAME: ${{ github.event.release.tag_name }}
- name: Run Tests
run: |
"opcache.enable=0`r`nopcache.enable_cli=0" | Out-File php.ini
$env:PHPRC = Get-Location
go test -race ./...
cd caddy
go test -race ./...
working-directory: ${{ github.workspace }}\frankenphp

View File

@@ -17,7 +17,7 @@ The image contains the usual development tools (Go, GDB, Valgrind, Neovim...) an
- additional configuration files: `/etc/frankenphp/php.d/*.ini`
- php extensions: `/usr/lib/frankenphp/modules/`
If your Docker version is lower than 23.0, the build will fail due to dockerignore [pattern issue](https://github.com/moby/moby/pull/42676). Add directories to `.dockerignore`.
If your Docker version is lower than 23.0, the build will fail due to dockerignore [pattern issue](https://github.com/moby/moby/pull/42676). Add directories to `.dockerignore`:
```patch
!testdata/*.php
@@ -30,14 +30,14 @@ If your Docker version is lower than 23.0, the build will fail due to dockerigno
[Follow the instructions to compile from sources](https://frankenphp.dev/docs/compile/) and pass the `--debug` configuration flag.
## Running the test suite
## Running the Test Suite
```console
export CGO_CFLAGS=$(php-config --includes) CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)"
export CGO_CFLAGS=-O0 -g $(php-config --includes) CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)"
go test -race -v ./...
```
## Caddy module
## Caddy Module
Build Caddy with the FrankenPHP Caddy module:
@@ -57,13 +57,13 @@ cd testdata/
The server is listening on `127.0.0.1:80`:
> [!NOTE]
> if you are using Docker, you will have to either bind container port 80 or execute from inside the container
> If you are using Docker, you will have to either bind container port 80 or execute from inside the container
```console
curl -vk http://127.0.0.1/phpinfo.php
```
## Minimal test server
## Minimal Test Server
Build the minimal test server:
@@ -86,9 +86,76 @@ The server is listening on `127.0.0.1:8080`:
curl -v http://127.0.0.1:8080/phpinfo.php
```
## Windows Development
1. Configure Git to always use `lf` line endings
```powershell
git config --global core.autocrlf false
git config --global core.eol lf
```
2. Install Visual Studio, Git, and Go:
```powershell
winget install -e --id Microsoft.VisualStudio.2022.Community --override "--passive --wait --add Microsoft.VisualStudio.Workload.NativeDesktop --add Microsoft.VisualStudio.Component.VC.Llvm.Clang --includeRecommended"
winget install -e --id GoLang.Go
winget install -e --id Git.Git
```
3. Install vcpkg:
```powershell
cd C:\
git clone https://github.com/microsoft/vcpkg
.\vcpkg\bootstrap-vcpkg.bat
```
4. [Download the latest version of the watcher library for Windows](https://github.com/e-dant/watcher/releases) and extract it to a directory named `C:\watcher`
5. [Download the latest **Thread Safe** version of PHP and of the PHP SDK for Windows](https://windows.php.net/download/), extract them in directories named `C:\php` and `C:\php-devel`
6. Clone the FrankenPHP Git repository:
```powershell
git clone https://github.com/php/frankenphp C:\frankenphp
cd C:\frankenphp
```
7. Install the dependencies:
```powershell
C:\vcpkg\vcpkg.exe install
```
8. Configure the needed environment variables (PowerShell):
```powershell
$env:PATH += ';C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\Llvm\bin'
$env:CC = 'clang'
$env:CXX = 'clang++'
$env:CGO_CFLAGS = "-O0 -g -IC:\frankenphp\vcpkg_installed\x64-windows\include -IC:\watcher -IC:\php-devel\include -IC:\php-devel\include\main -IC:\php-devel\include\TSRM -IC:\php-devel\include\Zend -IC:\php-devel\include\ext"
$env:CGO_LDFLAGS = '-LC:\frankenphp\vcpkg_installed\x64-windows\lib -lbrotlienc -LC:\watcher -llibwatcher-c -LC:\php -LC:\php-devel\lib -lphp8ts -lphp8embed'
```
9. Run the tests:
```powershell
go test -race -ldflags '-extldflags="-fuse-ld=lld"' ./...
cd caddy
go test -race -ldflags '-extldflags="-fuse-ld=lld"' -tags nobadger,nomysql,nopgx ./...
cd ..
```
10. Build the binary:
```powershell
cd caddy/frankenphp
go build -ldflags '-extldflags="-fuse-ld=lld"' -tags nobadger,nomysql,nopgx
cd ../..
```
## Building Docker Images Locally
Print bake plan:
Print Bake plan:
```console
docker buildx bake -f docker-bake.hcl --print
@@ -125,7 +192,7 @@ docker buildx bake -f docker-bake.hcl --pull --no-cache --push
docker cp $(docker create --name static-builder-musl dunglas/frankenphp:static-builder-musl):/go/src/app/dist/frankenphp-linux-$(uname -m) frankenphp
```
2. Replace your current version of `frankenphp` by the debug FrankenPHP executable
2. Replace your current version of `frankenphp` with the debug FrankenPHP executable
3. Start FrankenPHP as usual (alternatively, you can directly start FrankenPHP with GDB: `gdb --args frankenphp run`)
4. Attach to the process with GDB:
@@ -155,7 +222,7 @@ docker buildx bake -f docker-bake.hcl --pull --no-cache --push
```patch
- name: Set CGO flags
run: echo "CGO_CFLAGS=$(php-config --includes)" >> "$GITHUB_ENV"
run: echo "CGO_CFLAGS=-O0 -g $(php-config --includes)" >> "$GITHUB_ENV"
+ - run: |
+ sudo apt install gdb
+ mkdir -p /home/runner/.config/gdb/
@@ -207,7 +274,7 @@ strace -e 'trace=!futex,epoll_ctl,epoll_pwait,tgkill,rt_sigreturn' -p 1
## Translating the Documentation
To translate the documentation and the site in a new language,
To translate the documentation and the site into a new language,
follow these steps:
1. Create a new directory named with the language's 2-character ISO code in this repository's `docs/` directory
@@ -215,6 +282,6 @@ follow these steps:
3. Copy the `README.md` and `CONTRIBUTING.md` files from the root directory to the new directory
4. Translate the content of the files, but don't change the filenames, also don't translate strings starting with `> [!` (it's special markup for GitHub)
5. Create a Pull Request with the translations
6. In the [site repository](https://github.com/dunglas/frankenphp-website/tree/main), copy and translate the translation files in the `content/`, `data/` and `i18n/` directories
6. In the [site repository](https://github.com/dunglas/frankenphp-website/tree/main), copy and translate the translation files in the `content/`, `data/`, and `i18n/` directories
7. Translate the values in the created YAML file
8. Open a Pull Request on the site repository

View File

@@ -93,7 +93,8 @@ RUN --mount=type=secret,id=github-token \
sed 's/"//g' | \
xargs curl -L | \
tar xz --strip-components 1 && \
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release && \
# -Wno-error=use-after-free: GCC 12 on Bookworm i386 emits a spurious warning in libstdc++ basic_string.h
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_FLAGS="-Wno-error=use-after-free" && \
cmake --build build && \
cmake --install build && \
ldconfig
@@ -117,7 +118,7 @@ ENV CGO_LDFLAGS="-L/usr/local/lib -lssl -lcrypto -lreadline -largon2 -lcurl -lon
WORKDIR /go/src/app/caddy/frankenphp
RUN GOBIN=/usr/local/bin \
../../go.sh install -ldflags "-w -s -X 'github.com/caddyserver/caddy/v2.CustomVersion=FrankenPHP $FRANKENPHP_VERSION PHP $PHP_VERSION Caddy'" -buildvcs=true && \
../../go.sh install -ldflags "-w -s -X 'github.com/caddyserver/caddy/v2.CustomVersion=FrankenPHP $FRANKENPHP_VERSION PHP $PHP_VERSION Caddy' -X 'github.com/caddyserver/caddy/v2/modules/caddyhttp.ServerHeader=FrankenPHP Caddy'" -buildvcs=true && \
setcap cap_net_bind_service=+ep /usr/local/bin/frankenphp && \
cp Caddyfile /etc/frankenphp/Caddyfile && \
frankenphp version && \

View File

@@ -123,7 +123,7 @@ ENV CGO_LDFLAGS="-lssl -lcrypto -lreadline -largon2 -lcurl -lonig -lz $PHP_LDFLA
WORKDIR /go/src/app/caddy/frankenphp
RUN GOBIN=/usr/local/bin \
../../go.sh install -ldflags "-w -s -extldflags '-Wl,-z,stack-size=0x80000' -X 'github.com/caddyserver/caddy/v2.CustomVersion=FrankenPHP $FRANKENPHP_VERSION PHP $PHP_VERSION Caddy'" -buildvcs=true && \
../../go.sh install -ldflags "-w -s -extldflags '-Wl,-z,stack-size=0x80000' -X 'github.com/caddyserver/caddy/v2.CustomVersion=FrankenPHP $FRANKENPHP_VERSION PHP $PHP_VERSION Caddy' -X 'github.com/caddyserver/caddy/v2/modules/caddyhttp.ServerHeader=FrankenPHP Caddy'" -buildvcs=true && \
setcap cap_net_bind_service=+ep /usr/local/bin/frankenphp && \
([ -z "${NO_COMPRESS}" ] && upx --best /usr/local/bin/frankenphp || true) && \
frankenphp version && \

View File

@@ -62,7 +62,7 @@ fi
if [ -z "${PHP_VERSION}" ]; then
get_latest_php_version() {
input="$1"
json=$(curl -s "https://www.php.net/releases/index.php?json&version=$input")
json=$(curl -fsSL "https://www.php.net/releases/index.php?json&version=$input" 2>/dev/null || curl -fsSL "https://phpmirror.static-php.dev/releases/index.php?json&version=$input")
latest=$(echo "$json" | jq -r '.version')
if [[ "$latest" == "$input"* ]]; then
@@ -72,11 +72,11 @@ if [ -z "${PHP_VERSION}" ]; then
fi
}
PHP_VERSION="$(get_latest_php_version "8.4")"
PHP_VERSION="$(get_latest_php_version "8.5")"
export PHP_VERSION
fi
# default extension set
defaultExtensions="amqp,apcu,ast,bcmath,brotli,bz2,calendar,ctype,curl,dba,dom,exif,fileinfo,filter,ftp,gd,gmp,gettext,iconv,igbinary,imagick,intl,ldap,lz4,mbregex,mbstring,memcache,memcached,mysqli,mysqlnd,opcache,openssl,password-argon2,parallel,pcntl,pdo,pdo_mysql,pdo_pgsql,pdo_sqlite,pdo_sqlsrv,pgsql,phar,posix,protobuf,readline,redis,session,shmop,simplexml,soap,sockets,sodium,sqlite3,ssh2,sysvmsg,sysvsem,sysvshm,tidy,tokenizer,xlswriter,xml,xmlreader,xmlwriter,xsl,xz,zip,zlib,yaml,zstd"
defaultExtensions="amqp,apcu,ast,bcmath,brotli,bz2,calendar,ctype,curl,dba,dom,exif,fileinfo,filter,ftp,gd,gmp,gettext,iconv,igbinary,imagick,intl,ldap,lz4,mbregex,mbstring,memcached,mysqli,mysqlnd,opcache,openssl,password-argon2,parallel,pcntl,pdo,pdo_mysql,pdo_pgsql,pdo_sqlite,pgsql,phar,posix,protobuf,readline,redis,session,shmop,simplexml,soap,sockets,sodium,sqlite3,ssh2,sysvmsg,sysvsem,sysvshm,tidy,tokenizer,xlswriter,xml,xmlreader,xmlwriter,xsl,xz,zip,zlib,yaml,zstd"
defaultExtensionLibs="libavif,nghttp2,nghttp3,ngtcp2,watcher"
if [ -z "${FRANKENPHP_VERSION}" ]; then
@@ -147,6 +147,13 @@ else
spcCommand="./bin/spc"
fi
# turn potentially relative EMBED path into absolute path
if [ -n "${EMBED}" ]; then
if [[ "${EMBED}" != /* ]]; then
EMBED="${CURRENT_DIR}/${EMBED}"
fi
fi
# Extensions to build
if [ -z "${PHP_EXTENSIONS}" ]; then
# enable EMBED mode, first check if project has dumped extensions
@@ -178,9 +185,6 @@ fi
# Embed PHP app, if any
if [ -n "${EMBED}" ] && [ -d "${EMBED}" ]; then
if [[ "${EMBED}" != /* ]]; then
EMBED="${CURRENT_DIR}/${EMBED}"
fi
# shellcheck disable=SC2089
SPC_OPT_BUILD_ARGS="${SPC_OPT_BUILD_ARGS} --with-frankenphp-app='${EMBED}'"
fi

View File

@@ -3,12 +3,14 @@ package caddy
import (
"encoding/json"
"fmt"
"net/http"
"github.com/caddyserver/caddy/v2"
"github.com/dunglas/frankenphp"
"net/http"
)
type FrankenPHPAdmin struct{}
type FrankenPHPAdmin struct {
}
// if the id starts with "admin.api" the module will register AdminRoutes via module.Routes()
func (FrankenPHPAdmin) CaddyModule() caddy.ModuleInfo {

View File

@@ -6,6 +6,7 @@ import (
"fmt"
"io"
"net/http"
"strings"
"sync"
"testing"
@@ -249,7 +250,7 @@ func TestAddModuleWorkerViaAdminApi(t *testing.T) {
initialDebugState := getDebugState(t, tester)
initialWorkerCount := 0
for _, thread := range initialDebugState.ThreadDebugStates {
if thread.Name != "" && thread.Name != "ready" {
if strings.HasPrefix(thread.Name, "Worker PHP Thread") {
initialWorkerCount++
}
}
@@ -286,7 +287,7 @@ func TestAddModuleWorkerViaAdminApi(t *testing.T) {
workerFound := false
filename, _ := fastabs.FastAbs("../testdata/worker-with-counter.php")
for _, thread := range updatedDebugState.ThreadDebugStates {
if thread.Name != "" && thread.Name != "ready" {
if strings.HasPrefix(thread.Name, "Worker PHP Thread") {
updatedWorkerCount++
if thread.Name == "Worker PHP Thread - "+filename {
workerFound = true

View File

@@ -55,6 +55,8 @@ type FrankenPHPApp struct {
PhpIni map[string]string `json:"php_ini,omitempty"`
// The maximum amount of time a request may be stalled waiting for a thread
MaxWaitTime time.Duration `json:"max_wait_time,omitempty"`
// The maximum amount of time an autoscaled thread may be idle before being deactivated
MaxIdleTime time.Duration `json:"max_idle_time,omitempty"`
opts []frankenphp.Option
metrics frankenphp.Metrics
@@ -150,6 +152,7 @@ func (f *FrankenPHPApp) Start() error {
frankenphp.WithMetrics(f.metrics),
frankenphp.WithPhpIni(f.PhpIni),
frankenphp.WithMaxWaitTime(f.MaxWaitTime),
frankenphp.WithMaxIdleTime(f.MaxIdleTime),
)
for _, w := range f.Workers {
@@ -190,6 +193,7 @@ func (f *FrankenPHPApp) Stop() error {
f.Workers = nil
f.NumThreads = 0
f.MaxWaitTime = 0
f.MaxIdleTime = 0
optionsMU.Lock()
options = nil
@@ -242,6 +246,17 @@ func (f *FrankenPHPApp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
}
f.MaxWaitTime = v
case "max_idle_time":
if !d.NextArg() {
return d.ArgErr()
}
v, err := time.ParseDuration(d.Val())
if err != nil {
return d.Err("max_idle_time must be a valid duration (example: 30s)")
}
f.MaxIdleTime = v
case "php_ini":
parseIniLine := func(d *caddyfile.Dispenser) error {
key := d.Val()
@@ -298,7 +313,7 @@ func (f *FrankenPHPApp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
f.Workers = append(f.Workers, wc)
default:
return wrongSubDirectiveError("frankenphp", "num_threads, max_threads, php_ini, worker, max_wait_time", d.Val())
return wrongSubDirectiveError("frankenphp", "num_threads, max_threads, php_ini, worker, max_wait_time, max_idle_time", d.Val())
}
}
}

View File

@@ -11,6 +11,7 @@ import (
"sync"
"sync/atomic"
"testing"
"time"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddytest"
@@ -19,12 +20,62 @@ import (
"github.com/stretchr/testify/require"
)
// initServer initializes a Caddy test server and waits for it to be ready.
// After InitServer, it polls the server to handle a race condition on macOS where
// SO_REUSEPORT can briefly route connections to the old listener being shut down,
// resulting in "connection reset by peer".
func initServer(t *testing.T, tester *caddytest.Tester, config string, format string) {
t.Helper()
tester.InitServer(config, format)
client := &http.Client{Timeout: 1 * time.Second}
require.Eventually(t, func() bool {
resp, err := client.Get("http://localhost:" + testPort)
if err != nil {
return false
}
require.NoError(t, resp.Body.Close())
return true
}, 5*time.Second, 100*time.Millisecond, "server failed to become ready")
}
var testPort = "9080"
// skipIfSymlinkNotValid skips the test if the given path is not a valid symlink
func skipIfSymlinkNotValid(t *testing.T, path string) {
t.Helper()
info, err := os.Lstat(path)
if err != nil {
t.Skipf("symlink test skipped: cannot stat %s: %v", path, err)
}
if info.Mode()&os.ModeSymlink == 0 {
t.Skipf("symlink test skipped: %s is not a symlink (git may not support symlinks on this platform)", path)
}
}
// escapeMetricLabel escapes backslashes in label values for Prometheus text format
func escapeMetricLabel(s string) string {
return strings.ReplaceAll(s, "\\", "\\\\")
}
func TestMain(m *testing.M) {
// setup custom environment vars for TestOsEnv
if os.Setenv("ENV1", "value1") != nil || os.Setenv("ENV2", "value2") != nil {
fmt.Println("Failed to set environment variables for tests")
os.Exit(1)
}
os.Exit(m.Run())
}
func TestPHP(t *testing.T) {
var wg sync.WaitGroup
tester := caddytest.NewTester(t)
tester.InitServer(`
initServer(t, tester, `
{
skip_install_trust
admin localhost:2999
@@ -54,7 +105,7 @@ func TestPHP(t *testing.T) {
func TestLargeRequest(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
initServer(t, tester, `
{
skip_install_trust
admin localhost:2999
@@ -83,7 +134,7 @@ func TestLargeRequest(t *testing.T) {
func TestWorker(t *testing.T) {
var wg sync.WaitGroup
tester := caddytest.NewTester(t)
tester.InitServer(`
initServer(t, tester, `
{
skip_install_trust
admin localhost:2999
@@ -120,7 +171,7 @@ func TestGlobalAndModuleWorker(t *testing.T) {
testPortNum, _ := strconv.Atoi(testPort)
testPortTwo := strconv.Itoa(testPortNum + 1)
tester := caddytest.NewTester(t)
tester.InitServer(`
initServer(t, tester, `
{
skip_install_trust
admin localhost:2999
@@ -170,7 +221,7 @@ func TestGlobalAndModuleWorker(t *testing.T) {
func TestModuleWorkerInheritsEnv(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
initServer(t, tester, `
{
skip_install_trust
admin localhost:2999
@@ -195,7 +246,7 @@ func TestNamedModuleWorkers(t *testing.T) {
testPortNum, _ := strconv.Atoi(testPort)
testPortTwo := strconv.Itoa(testPortNum + 1)
tester := caddytest.NewTester(t)
tester.InitServer(`
initServer(t, tester, `
{
skip_install_trust
admin localhost:2999
@@ -244,7 +295,7 @@ func TestNamedModuleWorkers(t *testing.T) {
func TestEnv(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
initServer(t, tester, `
{
skip_install_trust
admin localhost:2999
@@ -275,7 +326,7 @@ func TestEnv(t *testing.T) {
func TestJsonEnv(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
initServer(t, tester, `
{
"admin": {
"listen": "localhost:2999"
@@ -358,7 +409,7 @@ func TestJsonEnv(t *testing.T) {
func TestCustomCaddyVariablesInEnv(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
initServer(t, tester, `
{
skip_install_trust
admin localhost:2999
@@ -392,7 +443,7 @@ func TestCustomCaddyVariablesInEnv(t *testing.T) {
func TestPHPServerDirective(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
initServer(t, tester, `
{
skip_install_trust
admin localhost:2999
@@ -413,7 +464,7 @@ func TestPHPServerDirective(t *testing.T) {
func TestPHPServerDirectiveDisableFileServer(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
initServer(t, tester, `
{
skip_install_trust
admin localhost:2999
@@ -438,7 +489,7 @@ func TestPHPServerDirectiveDisableFileServer(t *testing.T) {
func TestMetrics(t *testing.T) {
var wg sync.WaitGroup
tester := caddytest.NewTester(t)
tester.InitServer(`
initServer(t, tester, `
{
skip_install_trust
admin localhost:2999
@@ -489,7 +540,9 @@ func TestMetrics(t *testing.T) {
// Fetch metrics
resp, err := http.Get("http://localhost:2999/metrics")
require.NoError(t, err, "failed to fetch metrics")
defer resp.Body.Close()
t.Cleanup(func() {
require.NoError(t, resp.Body.Close())
})
// Read and parse metrics
metrics := new(bytes.Buffer)
@@ -517,7 +570,7 @@ func TestMetrics(t *testing.T) {
func TestWorkerMetrics(t *testing.T) {
var wg sync.WaitGroup
tester := caddytest.NewTester(t)
tester.InitServer(`
initServer(t, tester, `
{
skip_install_trust
admin localhost:2999
@@ -548,6 +601,7 @@ func TestWorkerMetrics(t *testing.T) {
`, "caddyfile")
workerName, _ := fastabs.FastAbs("../testdata/index.php")
workerName = escapeMetricLabel(workerName)
// Make some requests
for i := range 10 {
@@ -562,7 +616,9 @@ func TestWorkerMetrics(t *testing.T) {
// Fetch metrics
resp, err := http.Get("http://localhost:2999/metrics")
require.NoError(t, err, "failed to fetch metrics")
defer resp.Body.Close()
t.Cleanup(func() {
require.NoError(t, resp.Body.Close())
})
// Read and parse metrics
metrics := new(bytes.Buffer)
@@ -615,7 +671,7 @@ func TestWorkerMetrics(t *testing.T) {
func TestNamedWorkerMetrics(t *testing.T) {
var wg sync.WaitGroup
tester := caddytest.NewTester(t)
tester.InitServer(`
initServer(t, tester, `
{
skip_install_trust
admin localhost:2999
@@ -654,7 +710,9 @@ func TestNamedWorkerMetrics(t *testing.T) {
// Fetch metrics
resp, err := http.Get("http://localhost:2999/metrics")
require.NoError(t, err, "failed to fetch metrics")
defer resp.Body.Close()
t.Cleanup(func() {
require.NoError(t, resp.Body.Close())
})
// Read and parse metrics
metrics := new(bytes.Buffer)
@@ -708,7 +766,7 @@ func TestNamedWorkerMetrics(t *testing.T) {
func TestAutoWorkerConfig(t *testing.T) {
var wg sync.WaitGroup
tester := caddytest.NewTester(t)
tester.InitServer(`
initServer(t, tester, `
{
skip_install_trust
admin localhost:2999
@@ -731,6 +789,7 @@ func TestAutoWorkerConfig(t *testing.T) {
`, "caddyfile")
workerName, _ := fastabs.FastAbs("../testdata/index.php")
workerName = escapeMetricLabel(workerName)
// Make some requests
for i := range 10 {
@@ -745,7 +804,9 @@ func TestAutoWorkerConfig(t *testing.T) {
// Fetch metrics
resp, err := http.Get("http://localhost:2999/metrics")
require.NoError(t, err, "failed to fetch metrics")
defer resp.Body.Close()
t.Cleanup(func() {
require.NoError(t, resp.Body.Close())
})
// Read and parse metrics
metrics := new(bytes.Buffer)
@@ -804,8 +865,9 @@ func TestAllDefinedServerVars(t *testing.T) {
expectedBody = strings.ReplaceAll(expectedBody, "{documentRoot}", documentRoot)
expectedBody = strings.ReplaceAll(expectedBody, "\r\n", "\n")
expectedBody = strings.ReplaceAll(expectedBody, "{testPort}", testPort)
expectedBody = strings.ReplaceAll(expectedBody, documentRoot+"/", documentRoot+string(filepath.Separator))
tester := caddytest.NewTester(t)
tester.InitServer(`
initServer(t, tester, `
{
skip_install_trust
admin localhost:2999
@@ -837,7 +899,7 @@ func TestAllDefinedServerVars(t *testing.T) {
func TestPHPIniConfiguration(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
initServer(t, tester, `
{
skip_install_trust
admin localhost:2999
@@ -865,7 +927,7 @@ func TestPHPIniConfiguration(t *testing.T) {
func TestPHPIniBlockConfiguration(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
initServer(t, tester, `
{
skip_install_trust
admin localhost:2999
@@ -904,10 +966,8 @@ func testSingleIniConfiguration(tester *caddytest.Tester, key string, value stri
}
func TestOsEnv(t *testing.T) {
os.Setenv("ENV1", "value1")
os.Setenv("ENV2", "value2")
tester := caddytest.NewTester(t)
tester.InitServer(`
initServer(t, tester, `
{
skip_install_trust
admin localhost:2999
@@ -937,7 +997,7 @@ func TestOsEnv(t *testing.T) {
func TestMaxWaitTime(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
initServer(t, tester, `
{
skip_install_trust
admin localhost:2999
@@ -978,7 +1038,7 @@ func TestMaxWaitTime(t *testing.T) {
func TestMaxWaitTimeWorker(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
initServer(t, tester, `
{
skip_install_trust
admin localhost:2999
@@ -1050,16 +1110,18 @@ func TestMaxWaitTimeWorker(t *testing.T) {
func getStatusCode(url string, t *testing.T) int {
req, err := http.NewRequest("GET", url, nil)
require.NoError(t, err)
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
require.NoError(t, resp.Body.Close())
return resp.StatusCode
}
func TestMultiWorkersMetrics(t *testing.T) {
var wg sync.WaitGroup
tester := caddytest.NewTester(t)
tester.InitServer(`
initServer(t, tester, `
{
skip_install_trust
admin localhost:2999
@@ -1111,7 +1173,9 @@ func TestMultiWorkersMetrics(t *testing.T) {
// Fetch metrics
resp, err := http.Get("http://localhost:2999/metrics")
require.NoError(t, err, "failed to fetch metrics")
defer resp.Body.Close()
t.Cleanup(func() {
require.NoError(t, resp.Body.Close())
})
// Read and parse metrics
metrics := new(bytes.Buffer)
@@ -1166,7 +1230,7 @@ func TestMultiWorkersMetrics(t *testing.T) {
func TestDisabledMetrics(t *testing.T) {
var wg sync.WaitGroup
tester := caddytest.NewTester(t)
tester.InitServer(`
initServer(t, tester, `
{
skip_install_trust
admin localhost:2999
@@ -1217,7 +1281,9 @@ func TestDisabledMetrics(t *testing.T) {
// Fetch metrics
resp, err := http.Get("http://localhost:2999/metrics")
require.NoError(t, err, "failed to fetch metrics")
defer resp.Body.Close()
t.Cleanup(func() {
require.NoError(t, resp.Body.Close())
})
// Read and parse metrics
metrics := new(bytes.Buffer)
@@ -1244,7 +1310,7 @@ func TestDisabledMetrics(t *testing.T) {
func TestWorkerRestart(t *testing.T) {
var wg sync.WaitGroup
tester := caddytest.NewTester(t)
tester.InitServer(`
initServer(t, tester, `
{
skip_install_trust
admin localhost:2999
@@ -1276,7 +1342,9 @@ func TestWorkerRestart(t *testing.T) {
resp, err := http.Get("http://localhost:2999/metrics")
require.NoError(t, err, "failed to fetch metrics")
defer resp.Body.Close()
t.Cleanup(func() {
require.NoError(t, resp.Body.Close())
})
// Read and parse metrics
metrics := new(bytes.Buffer)
@@ -1344,7 +1412,7 @@ func TestWorkerRestart(t *testing.T) {
func TestWorkerMatchDirective(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
initServer(t, tester, `
{
skip_install_trust
admin localhost:2999
@@ -1377,7 +1445,7 @@ func TestWorkerMatchDirective(t *testing.T) {
func TestWorkerMatchDirectiveWithMultipleWorkers(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
initServer(t, tester, `
{
skip_install_trust
admin localhost:2999
@@ -1418,7 +1486,7 @@ func TestWorkerMatchDirectiveWithMultipleWorkers(t *testing.T) {
func TestWorkerMatchDirectiveWithoutFileServer(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
initServer(t, tester, `
{
skip_install_trust
admin localhost:2999
@@ -1451,7 +1519,7 @@ func TestWorkerMatchDirectiveWithoutFileServer(t *testing.T) {
func TestDd(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
initServer(t, tester, `
{
skip_install_trust
admin localhost:2999
@@ -1475,7 +1543,7 @@ func TestDd(t *testing.T) {
func TestLog(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
initServer(t, tester, `
{
skip_install_trust
admin localhost:2999
@@ -1505,6 +1573,7 @@ func TestLog(t *testing.T) {
func TestSymlinkWorkerPaths(t *testing.T) {
cwd, _ := os.Getwd()
publicDir := filepath.Join(cwd, "..", "testdata", "symlinks", "public")
skipIfSymlinkNotValid(t, publicDir)
t.Run("NeighboringWorkerScript", func(t *testing.T) {
// Scenario: neighboring worker script
@@ -1512,7 +1581,7 @@ func TestSymlinkWorkerPaths(t *testing.T) {
// When I execute `frankenphp php-server --listen localhost:8080 -w index.php` from `public`
// Then I expect to see the worker script executed successfully
tester := caddytest.NewTester(t)
tester.InitServer(`
initServer(t, tester, `
{
skip_install_trust
admin localhost:2999
@@ -1542,7 +1611,7 @@ func TestSymlinkWorkerPaths(t *testing.T) {
// When I execute `frankenphp --listen localhost:8080 -w nested/index.php` from `public`
// Then I expect to see the worker script executed successfully
tester := caddytest.NewTester(t)
tester.InitServer(`
initServer(t, tester, `
{
skip_install_trust
admin localhost:2999
@@ -1572,7 +1641,7 @@ func TestSymlinkWorkerPaths(t *testing.T) {
// When I execute `frankenphp --listen localhost:8080 -w public/index.php` from the root folder
// Then I expect to see the worker script executed successfully
tester := caddytest.NewTester(t)
tester.InitServer(`
initServer(t, tester, `
{
skip_install_trust
admin localhost:2999
@@ -1606,7 +1675,7 @@ func TestSymlinkWorkerPaths(t *testing.T) {
// When I execute `frankenphp --listen localhost:8080 -w public/index.php -r public` from the root folder
// Then I expect to see the worker script executed successfully
tester := caddytest.NewTester(t)
tester.InitServer(`
initServer(t, tester, `
{
skip_install_trust
admin localhost:2999
@@ -1640,11 +1709,12 @@ func TestSymlinkResolveRoot(t *testing.T) {
cwd, _ := os.Getwd()
testDir := filepath.Join(cwd, "..", "testdata", "symlinks", "test")
publicDir := filepath.Join(cwd, "..", "testdata", "symlinks", "public")
skipIfSymlinkNotValid(t, publicDir)
t.Run("ResolveRootSymlink", func(t *testing.T) {
// Tests that resolve_root_symlink directive works correctly
tester := caddytest.NewTester(t)
tester.InitServer(`
initServer(t, tester, `
{
skip_install_trust
admin localhost:2999
@@ -1672,7 +1742,7 @@ func TestSymlinkResolveRoot(t *testing.T) {
t.Run("NoResolveRootSymlink", func(t *testing.T) {
// Tests that symlinks are preserved when resolve_root_symlink is false (non-worker mode)
tester := caddytest.NewTester(t)
tester.InitServer(`
initServer(t, tester, `
{
skip_install_trust
admin localhost:2999
@@ -1698,11 +1768,12 @@ func TestSymlinkResolveRoot(t *testing.T) {
func TestSymlinkWorkerBehavior(t *testing.T) {
cwd, _ := os.Getwd()
publicDir := filepath.Join(cwd, "..", "testdata", "symlinks", "public")
skipIfSymlinkNotValid(t, publicDir)
t.Run("WorkerScriptFailsWithoutWorkerMode", func(t *testing.T) {
// Tests that accessing a worker-only script without configuring it as a worker actually results in an error
tester := caddytest.NewTester(t)
tester.InitServer(`
initServer(t, tester, `
{
skip_install_trust
admin localhost:2999
@@ -1726,7 +1797,7 @@ func TestSymlinkWorkerBehavior(t *testing.T) {
t.Run("MultipleRequests", func(t *testing.T) {
// Tests that symlinked workers handle multiple requests correctly
tester := caddytest.NewTester(t)
tester.InitServer(`
initServer(t, tester, `
{
skip_install_trust
admin localhost:2999

View File

@@ -0,0 +1,5 @@
//go:build !nobrotli
package main
import _ "github.com/dunglas/caddy-cbrotli"

View File

@@ -5,7 +5,6 @@ import (
// 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"

View File

@@ -1,19 +1,19 @@
module github.com/dunglas/frankenphp/caddy
go 1.25.4
go 1.26.0
replace github.com/dunglas/frankenphp => ../
retract v1.0.0-rc.1 // Human error
require (
github.com/caddyserver/caddy/v2 v2.10.2
github.com/caddyserver/certmagic v0.25.1
github.com/caddyserver/caddy/v2 v2.11.2
github.com/caddyserver/certmagic v0.25.2
github.com/dunglas/caddy-cbrotli v1.0.1
github.com/dunglas/frankenphp v1.11.1
github.com/dunglas/mercure v0.21.7
github.com/dunglas/mercure/caddy v0.21.7
github.com/dunglas/vulcain/caddy v1.2.1
github.com/dunglas/frankenphp v1.12.0
github.com/dunglas/mercure v0.21.11
github.com/dunglas/mercure/caddy v0.21.11
github.com/dunglas/vulcain/caddy v1.4.0
github.com/prometheus/client_golang v1.23.2
github.com/spf13/cobra v1.10.2
github.com/stretchr/testify v1.11.1
@@ -23,27 +23,29 @@ require github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca
require (
cel.dev/expr v0.25.1 // indirect
cloud.google.com/go/auth v0.18.1 // indirect
cloud.google.com/go/auth v0.18.2 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect
dario.cat/mergo v1.0.2 // indirect
filippo.io/edwards25519 v1.1.0 // indirect
filippo.io/bigmod v0.1.0 // indirect
filippo.io/edwards25519 v1.2.0 // indirect
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect
github.com/BurntSushi/toml v1.6.0 // indirect
github.com/DeRuina/timberjack v1.3.9 // indirect
github.com/KimMachineGun/automemlimit v0.7.5 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.4.0 // indirect
github.com/Masterminds/sprig/v3 v3.3.0 // indirect
github.com/MauriceGit/skiplist v0.0.0-20211105230623-77f5c8d3e145 // indirect
github.com/MicahParks/jwkset v0.11.0 // indirect
github.com/MicahParks/keyfunc/v3 v3.7.0 // indirect
github.com/RoaringBitmap/roaring/v2 v2.14.4 // indirect
github.com/MicahParks/keyfunc/v3 v3.8.0 // indirect
github.com/RoaringBitmap/roaring/v2 v2.14.5 // indirect
github.com/alecthomas/chroma/v2 v2.23.1 // indirect
github.com/antlr4-go/antlr/v4 v4.13.1 // 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.24.4 // indirect
github.com/caddyserver/zerossl v0.1.4 // indirect
github.com/caddyserver/zerossl v0.1.5 // indirect
github.com/ccoveille/go-safecast/v2 v2.0.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash v1.1.0 // indirect
@@ -60,20 +62,20 @@ require (
github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/dunglas/httpsfv v1.1.0 // indirect
github.com/dunglas/skipfilter v1.0.0 // indirect
github.com/dunglas/vulcain v1.2.1 // indirect
github.com/dunglas/vulcain v1.4.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/e-dant/watcher/watcher-go v0.0.0-20260104182512-c28e9078050a // indirect
github.com/e-dant/watcher v0.0.0-20260223030516-06f84a1314be // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/getkin/kin-openapi v0.133.0 // indirect
github.com/go-chi/chi/v5 v5.2.4 // indirect
github.com/go-chi/chi/v5 v5.2.5 // indirect
github.com/go-jose/go-jose/v3 v3.0.4 // indirect
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.22.4 // indirect
github.com/go-openapi/swag/jsonname v0.25.4 // indirect
github.com/go-openapi/jsonpointer v0.22.5 // indirect
github.com/go-openapi/swag/jsonname v0.25.5 // indirect
github.com/go-sql-driver/mysql v1.9.3 // indirect
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
github.com/gofrs/uuid v4.4.0+incompatible // indirect
@@ -82,17 +84,17 @@ require (
github.com/golang/protobuf v1.5.4 // indirect
github.com/golang/snappy v1.0.0 // indirect
github.com/google/brotli/go/cbrotli v1.1.0 // indirect
github.com/google/cel-go v0.26.1 // indirect
github.com/google/certificate-transparency-go v1.3.2 // indirect
github.com/google/cel-go v0.27.0 // indirect
github.com/google/certificate-transparency-go v1.3.3 // indirect
github.com/google/go-tpm v0.9.8 // indirect
github.com/google/go-tspi v0.3.0 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect
github.com/googleapis/gax-go/v2 v2.16.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect
github.com/googleapis/gax-go/v2 v2.17.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.27.6 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
github.com/huandu/xstrings v1.5.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
@@ -100,7 +102,7 @@ require (
github.com/jackc/pgx/v5 v5.8.0 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/klauspost/compress v1.18.3 // indirect
github.com/klauspost/compress v1.18.4 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/libdns/libdns v1.1.1 // indirect
@@ -110,7 +112,7 @@ require (
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/maypok86/otter/v2 v2.3.0 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/mholt/acmez/v3 v3.1.4 // indirect
github.com/mholt/acmez/v3 v3.1.6 // indirect
github.com/miekg/dns v1.1.72 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-ps v1.0.0 // indirect
@@ -123,12 +125,13 @@ require (
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/perimeterx/marshmallow v1.1.5 // indirect
github.com/pires/go-proxyproto v0.9.2 // indirect
github.com/pires/go-proxyproto v0.11.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/procfs v0.19.2 // indirect
github.com/prometheus/otlptranslator v1.0.0 // indirect
github.com/prometheus/procfs v0.20.1 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.59.0 // indirect
github.com/rs/cors v1.11.1 // indirect
@@ -138,8 +141,8 @@ require (
github.com/shopspring/decimal v1.4.0 // indirect
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
github.com/sirupsen/logrus v1.9.4 // indirect
github.com/slackhq/nebula v1.9.7 // indirect
github.com/smallstep/certificates v0.29.0 // indirect
github.com/slackhq/nebula v1.10.3 // indirect
github.com/smallstep/certificates v0.30.0-rc3 // indirect
github.com/smallstep/cli-utils v0.12.2 // indirect
github.com/smallstep/linkedca v0.25.0 // indirect
github.com/smallstep/nosql v0.7.0 // indirect
@@ -150,7 +153,6 @@ require (
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/spf13/viper v1.21.0 // indirect
github.com/stoewer/go-strcase v1.3.1 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect
github.com/tailscale/tscert v0.0.0-20251216020129-aea342f6d747 // indirect
@@ -168,45 +170,58 @@ require (
github.com/zeebo/blake3 v0.2.4 // indirect
go.etcd.io/bbolt v1.4.3 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 // indirect
go.opentelemetry.io/contrib/propagators/autoprop v0.64.0 // indirect
go.opentelemetry.io/contrib/propagators/aws v1.39.0 // indirect
go.opentelemetry.io/contrib/propagators/b3 v1.39.0 // indirect
go.opentelemetry.io/contrib/propagators/jaeger v1.39.0 // indirect
go.opentelemetry.io/contrib/propagators/ot v1.39.0 // indirect
go.opentelemetry.io/otel v1.39.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 // indirect
go.opentelemetry.io/otel/metric v1.39.0 // indirect
go.opentelemetry.io/otel/sdk v1.39.0 // indirect
go.opentelemetry.io/otel/trace v1.39.0 // indirect
go.opentelemetry.io/contrib/bridges/prometheus v0.66.0 // indirect
go.opentelemetry.io/contrib/exporters/autoexport v0.66.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.66.0 // indirect
go.opentelemetry.io/contrib/propagators/autoprop v0.66.0 // indirect
go.opentelemetry.io/contrib/propagators/aws v1.41.0 // indirect
go.opentelemetry.io/contrib/propagators/b3 v1.41.0 // indirect
go.opentelemetry.io/contrib/propagators/jaeger v1.41.0 // indirect
go.opentelemetry.io/contrib/propagators/ot v1.41.0 // indirect
go.opentelemetry.io/otel v1.41.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.17.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.17.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.41.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.41.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.41.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.41.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.41.0 // indirect
go.opentelemetry.io/otel/exporters/prometheus v0.63.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.17.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.41.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.41.0 // indirect
go.opentelemetry.io/otel/log v0.17.0 // indirect
go.opentelemetry.io/otel/metric v1.41.0 // indirect
go.opentelemetry.io/otel/sdk v1.41.0 // indirect
go.opentelemetry.io/otel/sdk/log v0.17.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.41.0 // indirect
go.opentelemetry.io/otel/trace v1.41.0 // indirect
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
go.step.sm/crypto v0.76.0 // indirect
go.step.sm/crypto v0.76.2 // indirect
go.uber.org/automaxprocs v1.6.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.1 // indirect
go.uber.org/zap/exp v0.3.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.47.0 // indirect
golang.org/x/crypto/x509roots/fallback v0.0.0-20260113154411-7d0074ccc6f1 // indirect
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect
golang.org/x/mod v0.32.0 // indirect
golang.org/x/net v0.49.0 // indirect
golang.org/x/oauth2 v0.34.0 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/crypto/x509roots/fallback v0.0.0-20260213171211-a408498e5541 // indirect
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect
golang.org/x/mod v0.33.0 // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/oauth2 v0.35.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.40.0 // indirect
golang.org/x/term v0.39.0 // indirect
golang.org/x/text v0.33.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/term v0.40.0 // indirect
golang.org/x/text v0.34.0 // indirect
golang.org/x/time v0.14.0 // indirect
golang.org/x/tools v0.41.0 // indirect
google.golang.org/api v0.263.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect
google.golang.org/grpc v1.78.0 // indirect
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.6.0 // indirect
golang.org/x/tools v0.42.0 // indirect
google.golang.org/api v0.269.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
google.golang.org/grpc v1.79.2 // indirect
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.6.1 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
howett.net/plist v1.0.1 // indirect
)

View File

@@ -1,29 +1,35 @@
cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
cloud.google.com/go v0.121.6 h1:waZiuajrI28iAf40cWgycWNgaXPO06dupuS+sgibK6c=
cloud.google.com/go v0.121.6/go.mod h1:coChdst4Ea5vUpiALcYKXEpR1S9ZgXbhEzzMcMR66vI=
cloud.google.com/go/auth v0.18.1 h1:IwTEx92GFUo2pJ6Qea0EU3zYvKnTAeRCODxfA/G5UWs=
cloud.google.com/go/auth v0.18.1/go.mod h1:GfTYoS9G3CWpRA3Va9doKN9mjPGRS+v41jmZAhBzbrA=
cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=
cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM=
cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M=
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc=
cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU=
cloud.google.com/go/kms v1.24.0 h1:SWltUuoPhTdv9q/P0YEAWQfoYT32O5HdfPgTiWMvrH8=
cloud.google.com/go/kms v1.24.0/go.mod h1:QDH3z2SJ50lfNOE8EokKC1G40i7I0f8xTMCoiptcb5g=
cloud.google.com/go/longrunning v0.7.0 h1:FV0+SYF1RIj59gyoWDRi45GiYUMM3K1qO51qoboQT1E=
cloud.google.com/go/longrunning v0.7.0/go.mod h1:ySn2yXmjbK9Ba0zsQqunhDkYi0+9rlXIwnoAf+h+TPY=
cloud.google.com/go/kms v1.25.0 h1:gVqvGGUmz0nYCmtoxWmdc1wli2L1apgP8U4fghPGSbQ=
cloud.google.com/go/kms v1.25.0/go.mod h1:XIdHkzfj0bUO3E+LvwPg+oc7s58/Ns8Nd8Sdtljihbk=
cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8=
cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk=
code.pfad.fr/check v1.1.0 h1:GWvjdzhSEgHvEHe2uJujDcpmZoySKuHQNrZMfzfO0bE=
code.pfad.fr/check v1.1.0/go.mod h1:NiUH13DtYsb7xp5wll0U4SXx7KhXQVCtRgdC96IPfoM=
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
filippo.io/bigmod v0.1.0 h1:UNzDk7y9ADKST+axd9skUpBQeW7fG2KrTZyOE4uGQy8=
filippo.io/bigmod v0.1.0/go.mod h1:OjOXDNlClLblvXdwgFFOQFJEocLhhtai8vGLy0JCZlI=
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M=
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/DeRuina/timberjack v1.3.9 h1:6UXZ1I7ExPGTX/1UNYawR58LlOJUHKBPiYC7WQ91eBo=
github.com/DeRuina/timberjack v1.3.9/go.mod h1:RLoeQrwrCGIEF8gO5nV5b/gMD0QIy7bzQhBUgpp1EqE=
github.com/KimMachineGun/automemlimit v0.7.5 h1:RkbaC0MwhjL1ZuBKunGDjE/ggwAX43DwZrJqVwyveTk=
github.com/KimMachineGun/automemlimit v0.7.5/go.mod h1:QZxpHaGOQoYvFhv/r4u3U0JTC2ZcOwbSr11UZF46UBM=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
@@ -36,12 +42,12 @@ github.com/MauriceGit/skiplist v0.0.0-20211105230623-77f5c8d3e145 h1:1yw6O62BReQ
github.com/MauriceGit/skiplist v0.0.0-20211105230623-77f5c8d3e145/go.mod h1:877WBceefKn14QwVVn4xRFUsHsZb9clICgdeTj4XsUg=
github.com/MicahParks/jwkset v0.11.0 h1:yc0zG+jCvZpWgFDFmvs8/8jqqVBG9oyIbmBtmjOhoyQ=
github.com/MicahParks/jwkset v0.11.0/go.mod h1:U2oRhRaLgDCLjtpGL2GseNKGmZtLs/3O7p+OZaL5vo0=
github.com/MicahParks/keyfunc/v3 v3.7.0 h1:pdafUNyq+p3ZlvjJX1HWFP7MA3+cLpDtg69U3kITJGM=
github.com/MicahParks/keyfunc/v3 v3.7.0/go.mod h1:z66bkCviwqfg2YUp+Jcc/xRE9IXLcMq6DrgV/+Htru0=
github.com/MicahParks/keyfunc/v3 v3.8.0 h1:Hx2dgIjAXGk9slakM6rV9BOeaWDPEXXZ4Us8guNBfds=
github.com/MicahParks/keyfunc/v3 v3.8.0/go.mod h1:z66bkCviwqfg2YUp+Jcc/xRE9IXLcMq6DrgV/+Htru0=
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/RoaringBitmap/roaring/v2 v2.14.4 h1:4aKySrrg9G/5oRtJ3TrZLObVqxgQ9f1znCRBwEwjuVw=
github.com/RoaringBitmap/roaring/v2 v2.14.4/go.mod h1:oMvV6omPWr+2ifRdeZvVJyaz+aoEUopyv5iH0u/+wbY=
github.com/RoaringBitmap/roaring/v2 v2.14.5 h1:ckd0o545JqDPeVJDgeFoaM21eBixUnlWfYgjE5VnyWw=
github.com/RoaringBitmap/roaring/v2 v2.14.5/go.mod h1:eq4wdNXxtJIS/oikeCzdX1rBzek7ANzbth041hrU8Q4=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
@@ -89,12 +95,12 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bits-and-blooms/bitset v1.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoGXLwLQOZE=
github.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/caddyserver/caddy/v2 v2.10.2 h1:g/gTYjGMD0dec+UgMw8SnfmJ3I9+M2TdvoRL/Ovu6U8=
github.com/caddyserver/caddy/v2 v2.10.2/go.mod h1:TXLQHx+ev4HDpkO6PnVVHUbL6OXt6Dfe7VcIBdQnPL0=
github.com/caddyserver/certmagic v0.25.1 h1:4sIKKbOt5pg6+sL7tEwymE1x2bj6CHr80da1CRRIPbY=
github.com/caddyserver/certmagic v0.25.1/go.mod h1:VhyvndxtVton/Fo/wKhRoC46Rbw1fmjvQ3GjHYSQTEY=
github.com/caddyserver/zerossl v0.1.4 h1:CVJOE3MZeFisCERZjkxIcsqIH4fnFdlYWnPYeFtBHRw=
github.com/caddyserver/zerossl v0.1.4/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4=
github.com/caddyserver/caddy/v2 v2.11.2 h1:iOlpsSiSKqEW+SIXrcZsZ/NO74SzB/ycqqvAIEfIm64=
github.com/caddyserver/caddy/v2 v2.11.2/go.mod h1:ASNYYmKhIVWWMGPfNxclI5DqKEgU3FhmL+6NZWzQEag=
github.com/caddyserver/certmagic v0.25.2 h1:D7xcS7ggX/WEY54x0czj7ioTkmDWKIgxtIi2OcQclUc=
github.com/caddyserver/certmagic v0.25.2/go.mod h1:llW/CvsNmza8S6hmsuggsZeiX+uS27dkqY27wDIuBWg=
github.com/caddyserver/zerossl v0.1.5 h1:dkvOjBAEEtY6LIGAHei7sw2UgqSD6TrWweXpV7lvEvE=
github.com/caddyserver/zerossl v0.1.5/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4=
github.com/ccoveille/go-safecast/v2 v2.0.0 h1:+5eyITXAUj3wMjad6cRVJKGnC7vDS55zk0INzJagub0=
github.com/ccoveille/go-safecast/v2 v2.0.0/go.mod h1:JIYA4CAR33blIDuE6fSwCp2sz1oOBahXnvmdBhOAABs=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
@@ -146,23 +152,25 @@ github.com/dunglas/caddy-cbrotli v1.0.1 h1:mkg7EB1GmoyfBt3kY3mq4o/0bfnBeq7ZLQjmV
github.com/dunglas/caddy-cbrotli v1.0.1/go.mod h1:uXABy3tjy1FABF+3JWKVh1ajFvIO/kfpwHaeZGSBaAY=
github.com/dunglas/httpsfv v1.1.0 h1:Jw76nAyKWKZKFrpMMcL76y35tOpYHqQPzHQiwDvpe54=
github.com/dunglas/httpsfv v1.1.0/go.mod h1:zID2mqw9mFsnt7YC3vYQ9/cjq30q41W+1AnDwH8TiMg=
github.com/dunglas/mercure v0.21.7 h1:QtxQr9IMqJdlLxpsCjdBoDkJm4r4HXIEQCLl5obzI+o=
github.com/dunglas/mercure v0.21.7/go.mod h1:jze6G0/ha0j/YJnAbQRcMLE58/LoRogquwaRQPGjOuA=
github.com/dunglas/mercure/caddy v0.21.7 h1:+Mx9zzQPnGsF3Jwwp7hFXravkv5TUWqjifZFJ6yArQk=
github.com/dunglas/mercure/caddy v0.21.7/go.mod h1:VOrsyc4RJf8a9Ucr/cLjiODr6nd/BinLi02dOvyWXi4=
github.com/dunglas/mercure v0.21.11 h1:4Sd/Q77j8uh9SI5D9ZMg5sePlWs336+9CKxDQC1FV34=
github.com/dunglas/mercure v0.21.11/go.mod h1:WPMgfqonUiO1qB+W8Tya63Ngag9ZwplGMXSOy8P/uMg=
github.com/dunglas/mercure/caddy v0.21.11 h1:WnasC7EiqBPAB0CpBEPrm7vLiuL7o3BOVmfGDghnyVM=
github.com/dunglas/mercure/caddy v0.21.11/go.mod h1:MlGm4jbpBV+9nizn03PDejTEM916z3WDP9zO/Yw8OYQ=
github.com/dunglas/skipfilter v1.0.0 h1:JG9SgGg4n6BlFwuTYzb9RIqjH7PfwszvWehanrYWPF4=
github.com/dunglas/skipfilter v1.0.0/go.mod h1:ryhr8j7CAHSjzeN7wI6YEuwoArQ3OQmRqWWVCEAfb9w=
github.com/dunglas/vulcain v1.2.1 h1:pkPwvIfoa/xmWSVUyhntbIKT+XO2VFMyhLKv1gA61O8=
github.com/dunglas/vulcain v1.2.1/go.mod h1:Qv1hxHP8rMDp6ZrpQQPSOh1OibuAYScNNCL/46sCRXU=
github.com/dunglas/vulcain/caddy v1.2.1 h1:Bh5awZ9WoNqsBv7OZfs1SktJqRgJaF5avI5oDmxs6lI=
github.com/dunglas/vulcain/caddy v1.2.1/go.mod h1:8QrmLTfURmW2VgjTR6Gb9a53FrZjspFQfX5FTy/f6dw=
github.com/dunglas/vulcain v1.4.0 h1:uGMTLKmw53yJNKBwCtD3GOmnmGw4SfsIqYfb3NEKvbA=
github.com/dunglas/vulcain v1.4.0/go.mod h1:WJjUJ/anaMlV4JWUCLNTNkviPFQL817yaVW5ErfiaMY=
github.com/dunglas/vulcain/caddy v1.4.0 h1:u377qYQwDKRA2/CcZ7yIbRehuY+AjQM5xpkIyynsn1c=
github.com/dunglas/vulcain/caddy v1.4.0/go.mod h1:qu6VV33pP42D8tIZDURE0ngt2MBR9OE34lCeMBo8caM=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/e-dant/watcher/watcher-go v0.0.0-20260104182512-c28e9078050a h1:e/m9m8cJgjzw2Ol7tKTu4B/lM5F3Ym7ryKI+oyw0T8Y=
github.com/e-dant/watcher/watcher-go v0.0.0-20260104182512-c28e9078050a/go.mod h1:sVUOkwtftoj71nnJRG2S0oWNfXFdKpz/M9vK0z06nmM=
github.com/e-dant/watcher v0.0.0-20260223030516-06f84a1314be h1:vqHrvilasyJcnru/0Z4FoojsQJUIfXGVplte7JtupfY=
github.com/e-dant/watcher v0.0.0-20260223030516-06f84a1314be/go.mod h1:PmV4IVmBJVqT2NcfTGN4+sZ+qGe3PA0qkphAtOHeFG0=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
@@ -172,8 +180,8 @@ github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sa
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ=
github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE=
github.com/go-chi/chi/v5 v5.2.4 h1:WtFKPHwlywe8Srng8j2BhOD9312j9cGUxG1SP4V2cR4=
github.com/go-chi/chi/v5 v5.2.4/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY=
github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
@@ -183,12 +191,12 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4=
github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80=
github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI=
github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag=
github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
github.com/go-openapi/jsonpointer v0.22.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA=
github.com/go-openapi/jsonpointer v0.22.5/go.mod h1:gyUR3sCvGSWchA2sUBJGluYMbe1zazrYWIkWPjjMUY0=
github.com/go-openapi/swag/jsonname v0.25.5 h1:8p150i44rv/Drip4vWI3kGi9+4W9TdI3US3uUYSFhSo=
github.com/go-openapi/swag/jsonname v0.25.5/go.mod h1:jNqqikyiAK56uS7n8sLkdaNY/uq6+D2m2LANat09pKU=
github.com/go-openapi/testify/v2 v2.4.0 h1:8nsPrHVCWkQ4p8h1EsRVymA2XABB4OT40gcvAu+voFM=
github.com/go-openapi/testify/v2 v2.4.0/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
@@ -211,11 +219,11 @@ github.com/google/brotli/go/cbrotli v1.1.0 h1:YwHD/rwSgUSL4b2S3ZM2jnNymm+tmwKQqj
github.com/google/brotli/go/cbrotli v1.1.0/go.mod h1:nOPhAkwVliJdNTkj3gXpljmWhjc4wCaVqbMJcPKWP4s=
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/cel-go v0.26.1 h1:iPbVVEdkhTX++hpe3lzSk7D3G3QSYqLGoHOcEio+UXQ=
github.com/google/cel-go v0.26.1/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM=
github.com/google/cel-go v0.27.0 h1:e7ih85+4qVrBuqQWTW4FKSqZYokVuc3HnhH5keboFTo=
github.com/google/cel-go v0.27.0/go.mod h1:tTJ11FWqnhw5KKpnWpvW9CJC3Y9GK4EIS0WXnBbebzw=
github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg=
github.com/google/certificate-transparency-go v1.3.2 h1:9ahSNZF2o7SYMaKaXhAumVEzXB2QaayzII9C8rv7v+A=
github.com/google/certificate-transparency-go v1.3.2/go.mod h1:H5FpMUaGa5Ab2+KCYsxg6sELw3Flkl7pGZzWdBoYLXs=
github.com/google/certificate-transparency-go v1.3.3 h1:hq/rSxztSkXN2tx/3jQqF6Xc0O565UQPdHrOWvZwybo=
github.com/google/certificate-transparency-go v1.3.3/go.mod h1:iR17ZgSaXRzSa5qvjFl8TnVD5h8ky2JMVio+dzoKMgA=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
@@ -230,16 +238,16 @@ github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.11 h1:vAe81Msw+8tKUxi2Dqh/NZMz7475yUvmRIkXr4oN2ao=
github.com/googleapis/enterprise-certificate-proxy v0.3.11/go.mod h1:RFV7MUdlb7AgEq2v7FmMCfeSMCllAzWxFgRdusoGks8=
github.com/googleapis/gax-go/v2 v2.16.0 h1:iHbQmKLLZrexmb0OSsNGTeSTS0HO4YvFOG8g5E4Zd0Y=
github.com/googleapis/gax-go/v2 v2.16.0/go.mod h1:o1vfQjjNZn4+dPnRdl/4ZD7S9414Y4xA+a/6Icj6l14=
github.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8=
github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=
github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc=
github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY=
github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.6 h1:1ufTZkFXIQQ9EmgPjcIPIi2krfxG03lQ8OLoY1MJ3UM=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.6/go.mod h1:lW34nIZuQ8UDPdkon5fmfp2l3+ZkQ2me/+oecHYLOII=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
@@ -260,8 +268,8 @@ github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJS
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@@ -274,6 +282,10 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/letsencrypt/challtestsrv v1.4.2 h1:0ON3ldMhZyWlfVNYYpFuWRTmZNnyfiL9Hh5YzC3JVwU=
github.com/letsencrypt/challtestsrv v1.4.2/go.mod h1:GhqMqcSoeGpYd5zX5TgwA6er/1MbWzx/o7yuuVya+Wk=
github.com/letsencrypt/pebble/v2 v2.10.0 h1:Wq6gYXlsY6ubqI3hhxsTzdyotvfdjFBxuwYqCLCnj/U=
github.com/letsencrypt/pebble/v2 v2.10.0/go.mod h1:Sk8cmUIPcIdv2nINo+9PB4L+ZBhzY+F9A1a/h/xmWiQ=
github.com/libdns/libdns v1.1.1 h1:wPrHrXILoSHKWJKGd0EiAVmiJbFShguILTg9leS/P/U=
github.com/libdns/libdns v1.1.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
@@ -289,8 +301,8 @@ github.com/maypok86/otter/v2 v2.3.0 h1:8H8AVVFUSzJwIegKwv1uF5aGitTY+AIrtktg7OcLs
github.com/maypok86/otter/v2 v2.3.0/go.mod h1:XgIdlpmL6jYz882/CAx1E4C1ukfgDKSaw4mWq59+7l8=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mholt/acmez/v3 v3.1.4 h1:DyzZe/RnAzT3rpZj/2Ii5xZpiEvvYk3cQEN/RmqxwFQ=
github.com/mholt/acmez/v3 v3.1.4/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ=
github.com/mholt/acmez/v3 v3.1.6 h1:eGVQNObP0pBN4sxqrXeg7MYqTOWyoiYpQqITVWlrevk=
github.com/mholt/acmez/v3 v3.1.6/go.mod h1:5nTPosTGosLxF3+LU4ygbgMRFDhbAVpqMI4+a4aHLBY=
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
@@ -320,8 +332,8 @@ github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX
github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
github.com/peterbourgon/diskv/v3 v3.0.1 h1:x06SQA46+PKIUftmEujdwSEpIx8kR+M9eLYsUxeYveU=
github.com/peterbourgon/diskv/v3 v3.0.1/go.mod h1:kJ5Ny7vLdARGU3WUuy6uzO6T0nb/2gWcT1JiBvRmb5o=
github.com/pires/go-proxyproto v0.9.2 h1:H1UdHn695zUVVmB0lQ354lOWHOy6TZSpzBl3tgN0s1U=
github.com/pires/go-proxyproto v0.9.2/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
github.com/pires/go-proxyproto v0.11.0 h1:gUQpS85X/VJMdUsYyEgyn59uLJvGqPhJV5YvG68wXH4=
github.com/pires/go-proxyproto v0.11.0/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -336,8 +348,10 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/prometheus/otlptranslator v1.0.0 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEoIwkU+A6qos=
github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM=
github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
@@ -361,12 +375,12 @@ github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5I
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/slackhq/nebula v1.9.7 h1:v5u46efIyYHGdfjFnozQbRRhMdaB9Ma1SSTcUcE2lfE=
github.com/slackhq/nebula v1.9.7/go.mod h1:1+4q4wd3dDAjO8rKCttSb9JIVbklQhuJiBp5I0lbIsQ=
github.com/slackhq/nebula v1.10.3 h1:EstYj8ODEcv6T0R9X5BVq1zgWZnyU5gtPzk99QF1PMU=
github.com/slackhq/nebula v1.10.3/go.mod h1:IL5TUQm4x9IFx2kCKPYm1gP47pwd5b8QGnnBH2RHnvs=
github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 h1:unQFBIznI+VYD1/1fApl1A+9VcBk+9dcqGfnePY87LY=
github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262/go.mod h1:MyOHs9Po2fbM1LHej6sBUT8ozbxmMOFG+E+rx/GSGuc=
github.com/smallstep/certificates v0.29.0 h1:f90szTKYTW62bmCc+qE5doGqIGPVxTQb8Ba37e/K8Zs=
github.com/smallstep/certificates v0.29.0/go.mod h1:27WI0od6gu84mvE4mYQ/QZGyYwHXvhsiSRNC+y3t+mo=
github.com/smallstep/certificates v0.30.0-rc3 h1:Lx/NNJ4n+L3Pyx5NtVRGXeqviPPXTFFGLRiC1fCwU50=
github.com/smallstep/certificates v0.30.0-rc3/go.mod h1:e5/ylYYpvnjCVZz6RpyOkpTe73EGPYoL+8TZZ5EtLjI=
github.com/smallstep/cli-utils v0.12.2 h1:lGzM9PJrH/qawbzMC/s2SvgLdJPKDWKwKzx9doCVO+k=
github.com/smallstep/cli-utils v0.12.2/go.mod h1:uCPqefO29goHLGqFnwk0i8W7XJu18X3WHQFRtOm/00Y=
github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca h1:VX8L0r8vybH0bPeaIxh4NQzafKQiqvlOn8pmOXbFLO4=
@@ -401,8 +415,6 @@ github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3A
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
github.com/stoewer/go-strcase v1.3.1 h1:iS0MdW+kVTxgMoE1LAZyMiYJFKlOzLooE4MxjirtkAs=
github.com/stoewer/go-strcase v1.3.1/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo=
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=
@@ -413,7 +425,6 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
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/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
@@ -465,38 +476,66 @@ go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/bridges/prometheus v0.66.0 h1:nQlJkSnoq/O+z7Az1CjwM+IMCIKbnP7Twm3UxJYRv/Y=
go.opentelemetry.io/contrib/bridges/prometheus v0.66.0/go.mod h1:U87nfzwzcfDvqTeRnI+dBHMAmHGQf9AWqvTC2dAv8as=
go.opentelemetry.io/contrib/exporters/autoexport v0.66.0 h1:xFeFWaleVRvR9eUwh5ew+1+LYkTnqs7lvcj55TX9EbQ=
go.opentelemetry.io/contrib/exporters/autoexport v0.66.0/go.mod h1:OQHzrGFKcMdMgY646FZ0wqc9fbMlHd7h6AjU1dDqEI0=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ=
go.opentelemetry.io/contrib/propagators/autoprop v0.64.0 h1:VVrb1ErDD0Tlh/0K0rUqjky1e8AekjspTFN9sU2ekaA=
go.opentelemetry.io/contrib/propagators/autoprop v0.64.0/go.mod h1:QCsOQk+9Ep8Mkp4/aPtSzUT0dc8SaPYzBAE6o1jYuSE=
go.opentelemetry.io/contrib/propagators/aws v1.39.0 h1:IvNR8pAVGpkK1CHMjU/YE6B6TlnAPGFvogkMWRWU6wo=
go.opentelemetry.io/contrib/propagators/aws v1.39.0/go.mod h1:TUsFCERuGM4IGhJG9w+9l0nzmHUKHuaDYYNF6mtNgjY=
go.opentelemetry.io/contrib/propagators/b3 v1.39.0 h1:PI7pt9pkSnimWcp5sQhUA9OzLbc3Ba4sL+VEUTNsxrk=
go.opentelemetry.io/contrib/propagators/b3 v1.39.0/go.mod h1:5gV/EzPnfYIwjzj+6y8tbGW2PKWhcsz5e/7twptRVQY=
go.opentelemetry.io/contrib/propagators/jaeger v1.39.0 h1:Gz3yKzfMSEFzF0Vy5eIpu9ndpo4DhXMCxsLMF0OOApo=
go.opentelemetry.io/contrib/propagators/jaeger v1.39.0/go.mod h1:2D/cxxCqTlrday0rZrPujjg5aoAdqk1NaNyoXn8FJn8=
go.opentelemetry.io/contrib/propagators/ot v1.39.0 h1:vKTve1W/WKPVp1fzJamhCDDECt+5upJJ65bPyWoddGg=
go.opentelemetry.io/contrib/propagators/ot v1.39.0/go.mod h1:FH5VB2N19duNzh1Q8ks6CsZFyu3LFhNLiA9lPxyEkvU=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 h1:in9O8ESIOlwJAEGTkkf34DesGRAc/Pn8qJ7k3r/42LM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0/go.mod h1:Rp0EXBm5tfnv0WL+ARyO/PHBEaEAT8UUHQ6AGJcSq6c=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.66.0 h1:PnV4kVnw0zOmwwFkAzCN5O07fw1YOIQor120zrh0AVo=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.66.0/go.mod h1:ofAwF4uinaf8SXdVzzbL4OsxJ3VfeEg3f/F6CeF49/Y=
go.opentelemetry.io/contrib/propagators/autoprop v0.66.0 h1:gy5EF1w9wU6uUTaaWNa335yWn41jxkFg6OM9JXkbOxw=
go.opentelemetry.io/contrib/propagators/autoprop v0.66.0/go.mod h1:wc0aFc/8Th/UybDUy21U05lO1ENoTBzWdjaNpYlCkSE=
go.opentelemetry.io/contrib/propagators/aws v1.41.0 h1:XfxrpJSi+57+c0R5hgf2PJ9bCxLI4JwHJ/LR3GZKtXY=
go.opentelemetry.io/contrib/propagators/aws v1.41.0/go.mod h1:Oj8RSiDVWc1EmiuP/jd1bc98639awR/XKXEA/zY2Jns=
go.opentelemetry.io/contrib/propagators/b3 v1.41.0 h1:yzplYIx9maUG/KIq6YhLm2jXOFP+2fdiXGYmubV7l1M=
go.opentelemetry.io/contrib/propagators/b3 v1.41.0/go.mod h1:7wqcPkVIx1LaxMD5LwqvQ4OUXjusQGOnD/hrGBl6rws=
go.opentelemetry.io/contrib/propagators/jaeger v1.41.0 h1:uw+ghxLS0Wb98XfAhgJVQvAmmNJzT7jE30wJ4sKBrWs=
go.opentelemetry.io/contrib/propagators/jaeger v1.41.0/go.mod h1:Og4snN98wfeGoKm516W5+v0orAqwRRB5TAOJetk/HdM=
go.opentelemetry.io/contrib/propagators/ot v1.41.0 h1:KwJHGOfIowrKcVBfgPOSddvtNdpz29QbYC9GkS1rfvc=
go.opentelemetry.io/contrib/propagators/ot v1.41.0/go.mod h1:nDbpzwxrpp7OKQ8BdMoiZSQPw7+aam2ucMKyf8mSB48=
go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c=
go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.17.0 h1:6SRrIZrFLFVkktXaO0OUTweDdxNveqxczTsk3XUVQX8=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.17.0/go.mod h1:Nx2rIwEusIh/KFV8UrjjB87BfVn+daJ/lWCA0CkxAtY=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.17.0 h1:GcSx2UgcMuQEu0vHq823xR5LCN3WqEx5yKhqDkv1pwY=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.17.0/go.mod h1:ctNT8t8Vzx9sb1oWAozighT3guWorr8xdCboBvkT5yg=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.41.0 h1:VO3BL6OZXRQ1yQc8W6EVfJzINeJ35BkiHx4MYfoQf44=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.41.0/go.mod h1:qRDnJ2nv3CQXMK2HUd9K9VtvedsPAce3S+/4LZHjX/s=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.41.0 h1:MMrOAN8H1FrvDyq9UJ4lu5/+ss49Qgfgb7Zpm0m8ABo=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.41.0/go.mod h1:Na+2NNASJtF+uT4NxDe0G+NQb+bUgdPDfwxY/6JmS/c=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.41.0 h1:ao6Oe+wSebTlQ1OEht7jlYTzQKE+pnx/iNywFvTbuuI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.41.0/go.mod h1:u3T6vz0gh/NVzgDgiwkgLxpsSF6PaPmo2il0apGJbls=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.41.0 h1:mq/Qcf28TWz719lE3/hMB4KkyDuLJIvgJnFGcd0kEUI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.41.0/go.mod h1:yk5LXEYhsL2htyDNJbEq7fWzNEigeEdV5xBF/Y+kAv0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.41.0 h1:inYW9ZhgqiDqh6BioM7DVHHzEGVq76Db5897WLGZ5Go=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.41.0/go.mod h1:Izur+Wt8gClgMJqO/cZ8wdeeMryJ/xxiOVgFSSfpDTY=
go.opentelemetry.io/otel/exporters/prometheus v0.63.0 h1:OLo1FNb0pBZykLqbKRZolKtGZd0Waqlr240YdMEnhhg=
go.opentelemetry.io/otel/exporters/prometheus v0.63.0/go.mod h1:8yeQAdhrK5xsWuFehO13Dk/Xb9FuhZoVpJfpoNCfJnw=
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.17.0 h1:fXdXhpH9CmoaFc0Pf9o0wV05eMjjrVruGgYbo2GQ4L4=
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.17.0/go.mod h1:e5RGV+Yz5dheUnMWadfZ146MQ4XhwG2XVGS/5gQVQZA=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.41.0 h1:8+lzlbtX0QZ2TfILr7utn3YBipxmIjPHuHgcI1hDLI4=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.41.0/go.mod h1:sYzlrHkIULlZBHvhA0wqbR8tHt23pv4eJ87fINn4/Tc=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.41.0 h1:61oRQmYGMW7pXmFjPg1Muy84ndqMxQ6SH2L8fBG8fSY=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.41.0/go.mod h1:c0z2ubK4RQL+kSDuuFu9WnuXimObon3IiKjJf4NACvU=
go.opentelemetry.io/otel/log v0.17.0 h1:blZWM4y7n+KSa9OywwGWyBMPpeVoCl/NCw+jMps8afM=
go.opentelemetry.io/otel/log v0.17.0/go.mod h1:VXhjKYep6/laSgf/tjdh2SMAt18Z9XotBFBO0jxSE24=
go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ=
go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps=
go.opentelemetry.io/otel/sdk v1.41.0 h1:YPIEXKmiAwkGl3Gu1huk1aYWwtpRLeskpV+wPisxBp8=
go.opentelemetry.io/otel/sdk v1.41.0/go.mod h1:ahFdU0G5y8IxglBf0QBJXgSe7agzjE4GiTJ6HT9ud90=
go.opentelemetry.io/otel/sdk/log v0.17.0 h1:stWOgJB8bWieSlX4VO+gD7BrRZ/Dh1H/u7115amleGE=
go.opentelemetry.io/otel/sdk/log v0.17.0/go.mod h1:LQKPUyHraLka2sRvNQ5+W456+sElomqR7VWpOnOefZg=
go.opentelemetry.io/otel/sdk/log/logtest v0.17.0 h1:Z4S9W5piCH88itCkWDtX5ppRgO0UTkLXVK/6tPOMM2w=
go.opentelemetry.io/otel/sdk/log/logtest v0.17.0/go.mod h1:d9iIX/BwLfu1BTPxO0wi4ucyCenCckfuf9LC0aJDjqM=
go.opentelemetry.io/otel/sdk/metric v1.41.0 h1:siZQIYBAUd1rlIWQT2uCxWJxcCO7q3TriaMlf08rXw8=
go.opentelemetry.io/otel/sdk/metric v1.41.0/go.mod h1:HNBuSvT7ROaGtGI50ArdRLUnvRTRGniSUZbxiWxSO8Y=
go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0=
go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis=
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
go.step.sm/crypto v0.76.0 h1:K23BSaeoiY7Y5dvvijTeYC9EduDBetNwQYMBwMhi1aA=
go.step.sm/crypto v0.76.0/go.mod h1:PXYJdKkK8s+GHLwLguFaLxHNAFsFL3tL1vSBrYfey5k=
go.step.sm/crypto v0.76.2 h1:JJ/yMcs/rmcCAwlo+afrHjq74XBFRTJw5B2y4Q4Z4c4=
go.step.sm/crypto v0.76.2/go.mod h1:m6KlB/HzIuGFep0UWI5e0SYi38UxpoKeCg6qUaHV6/Q=
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
@@ -520,19 +559,19 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/crypto/x509roots/fallback v0.0.0-20260113154411-7d0074ccc6f1 h1:EBHQuS9qI8xJ96+YRgVV2ahFLUYbWpt1rf3wPfXN2wQ=
golang.org/x/crypto/x509roots/fallback v0.0.0-20260113154411-7d0074ccc6f1/go.mod h1:MEIPiCnxvQEjA4astfaKItNwEVZA5Ki+3+nyGbJ5N18=
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU=
golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/crypto/x509roots/fallback v0.0.0-20260213171211-a408498e5541 h1:FmKxj9ocLKn45jiR2jQMwCVhDvaK7fKQFzfuT9GvyK8=
golang.org/x/crypto/x509roots/fallback v0.0.0-20260213171211-a408498e5541/go.mod h1:+UoQFNBq2p2wO+Q6ddVtYc25GZ6VNdOMyyrd4nrqrKs=
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0=
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
@@ -541,10 +580,10 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -570,8 +609,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -581,8 +620,8 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
@@ -592,8 +631,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -602,31 +641,29 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/api v0.263.0 h1:UFs7qn8gInIdtk1ZA6eXRXp5JDAnS4x9VRsRVCeKdbk=
google.golang.org/api v0.263.0/go.mod h1:fAU1xtNNisHgOF5JooAs8rRaTkl2rT3uaoNGo9NS3R8=
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217 h1:GvESR9BIyHUahIb0NcTum6itIWtdoglGX+rnGxm2934=
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:yJ2HH4EHEDTd3JiLmhds6NkJ17ITVYOdV3m3VKOnws0=
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M=
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.6.0 h1:6Al3kEFFP9VJhRz3DID6quisgPnTeZVr4lep9kkxdPA=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.6.0/go.mod h1:QLvsjh0OIR0TYBeiu2bkWGTJBUNQ64st52iWj/yA93I=
google.golang.org/api v0.269.0 h1:qDrTOxKUQ/P0MveH6a7vZ+DNHxJQjtGm/uvdbdGXCQg=
google.golang.org/api v0.269.0/go.mod h1:N8Wpcu23Tlccl0zSHEkcAZQKDLdquxK+l9r2LkwAauE=
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM=
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM=
google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 h1:tu/dtnW1o3wfaxCOjSLn5IRX4YDcJrtlpzYkhHhGaC4=
google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171/go.mod h1:M5krXqk4GhBKvB596udGL3UyjL4I1+cTbK0orROM9ng=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU=
google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.6.1 h1:/WILD1UcXj/ujCxgoL/DvRgt2CP3txG8+FwkUbb9110=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.6.1/go.mod h1:YNKnb2OAApgYn2oYY47Rn7alMr1zWjb2U8Q0aoGWiNc=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=

View File

@@ -2,12 +2,17 @@
package caddy
type mercureContext struct {
}
import (
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
)
func (f *FrankenPHPModule) configureHotReload(_ *FrankenPHPApp) error {
return nil
type mercureContext struct {
}
func (f *FrankenPHPModule) assignMercureHub(_ caddy.Context) {
}
func createMercureRoute() (caddyhttp.Route, error) {
return caddyhttp.Route{}, nil
}

View File

@@ -3,10 +3,15 @@
package caddy
import (
"encoding/json"
"errors"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/dunglas/frankenphp"
"github.com/dunglas/mercure"
mercureCaddy "github.com/dunglas/mercure/caddy"
"os"
)
func init() {
@@ -22,8 +27,7 @@ func (f *FrankenPHPModule) assignMercureHub(ctx caddy.Context) {
return
}
opt := frankenphp.WithMercureHub(f.mercureHub)
f.mercureHubRequestOption = &opt
f.requestOptions = append(f.requestOptions, frankenphp.WithMercureHub(f.mercureHub))
for i, wc := range f.Workers {
wc.mercureHub = f.mercureHub
@@ -32,3 +36,36 @@ func (f *FrankenPHPModule) assignMercureHub(ctx caddy.Context) {
f.Workers[i] = wc
}
}
func createMercureRoute() (caddyhttp.Route, error) {
mercurePublisherJwtKey := os.Getenv("MERCURE_PUBLISHER_JWT_KEY")
if mercurePublisherJwtKey == "" {
return caddyhttp.Route{}, errors.New(`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 == "" {
return caddyhttp.Route{}, errors.New(`The "MERCURE_SUBSCRIBER_JWT_KEY" environment variable must be set to use the Mercure.rocks hub`)
}
mercureRoute := caddyhttp.Route{
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(
mercureCaddy.Mercure{
PublisherJWT: mercureCaddy.JWTConfig{
Alg: os.Getenv("MERCURE_PUBLISHER_JWT_ALG"),
Key: mercurePublisherJwtKey,
},
SubscriberJWT: mercureCaddy.JWTConfig{
Alg: os.Getenv("MERCURE_SUBSCRIBER_JWT_ALG"),
Key: mercureSubscriberJwtKey,
},
},
"handler",
"mercure",
nil,
),
},
}
return mercureRoute, nil
}

View File

@@ -6,6 +6,7 @@ import (
"log/slog"
"net/http"
"path/filepath"
"runtime"
"slices"
"strconv"
"strings"
@@ -21,8 +22,6 @@ import (
"github.com/dunglas/frankenphp/internal/fastabs"
)
var serverHeader = []string{"FrankenPHP Caddy"}
// FrankenPHPModule represents the "php_server" and "php" directives in the Caddyfile
// they are responsible for forwarding requests to FrankenPHP via "ServeHTTP"
//
@@ -50,7 +49,7 @@ type FrankenPHPModule struct {
preparedEnv frankenphp.PreparedEnv
preparedEnvNeedsReplacement bool
logger *slog.Logger
mercureHubRequestOption *frankenphp.RequestOption
requestOptions []frankenphp.RequestOption
}
// CaddyModule returns the Caddy module information.
@@ -107,8 +106,7 @@ func (f *FrankenPHPModule) Provision(ctx caddy.Context) error {
} else {
f.Root = filepath.Join(frankenphp.EmbeddedAppPath, defaultDocumentRoot)
var rrs bool
f.ResolveRootSymlink = &rrs
f.ResolveRootSymlink = new(false)
}
} else if frankenphp.EmbeddedAppPath != "" && filepath.IsLocal(f.Root) {
f.Root = filepath.Join(frankenphp.EmbeddedAppPath, f.Root)
@@ -118,9 +116,19 @@ func (f *FrankenPHPModule) Provision(ctx caddy.Context) error {
f.SplitPath = []string{".php"}
}
if opt, err := frankenphp.WithRequestSplitPath(f.SplitPath); err == nil {
f.requestOptions = append(f.requestOptions, opt)
} else {
f.requestOptions = append(f.requestOptions, opt)
}
if f.ResolveRootSymlink == nil {
rrs := true
f.ResolveRootSymlink = &rrs
f.ResolveRootSymlink = new(true)
}
// Always pre-compute absolute file names for fallback matching
for i := range f.Workers {
f.Workers[i].absFileName, _ = fastabs.FastAbs(f.Workers[i].FileName)
}
if !needReplacement(f.Root) {
@@ -138,15 +146,25 @@ func (f *FrankenPHPModule) Provision(ctx caddy.Context) error {
f.resolvedDocumentRoot = root
// Also resolve symlinks in worker file paths when resolve_root_symlink is true
// Resolve symlinks in worker file paths
for i, wc := range f.Workers {
if !filepath.IsAbs(wc.FileName) {
continue
if filepath.IsAbs(wc.FileName) {
resolvedPath, _ := filepath.EvalSymlinks(wc.FileName)
f.Workers[i].FileName = resolvedPath
f.Workers[i].absFileName = resolvedPath
}
resolvedPath, _ := filepath.EvalSymlinks(wc.FileName)
f.Workers[i].FileName = resolvedPath
}
}
// Pre-compute relative match paths for all workers (requires resolved document root)
docRootWithSep := f.resolvedDocumentRoot + string(filepath.Separator)
for i := range f.Workers {
if strings.HasPrefix(f.Workers[i].absFileName, docRootWithSep) {
f.Workers[i].matchRelPath = filepath.ToSlash(f.Workers[i].absFileName[len(f.resolvedDocumentRoot):])
}
}
f.requestOptions = append(f.requestOptions, frankenphp.WithRequestResolvedDocumentRoot(f.resolvedDocumentRoot))
}
if f.preparedEnv == nil {
@@ -161,6 +179,10 @@ func (f *FrankenPHPModule) Provision(ctx caddy.Context) error {
}
}
if !f.preparedEnvNeedsReplacement {
f.requestOptions = append(f.requestOptions, frankenphp.WithRequestPreparedEnv(f.preparedEnv))
}
if err := f.configureHotReload(fapp); err != nil {
return err
}
@@ -176,34 +198,32 @@ func needReplacement(s string) bool {
// ServeHTTP implements caddyhttp.MiddlewareHandler.
func (f *FrankenPHPModule) ServeHTTP(w http.ResponseWriter, r *http.Request, _ caddyhttp.Handler) error {
ctx := r.Context()
origReq := ctx.Value(caddyhttp.OriginalRequestCtxKey).(http.Request)
repl := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
var (
documentRootOption frankenphp.RequestOption
documentRoot string
)
documentRoot := f.resolvedDocumentRoot
if f.resolvedDocumentRoot == "" {
opts := make([]frankenphp.RequestOption, 0, len(f.requestOptions)+4)
opts = append(opts, f.requestOptions...)
if documentRoot == "" {
documentRoot = repl.ReplaceKnown(f.Root, "")
if documentRoot == "" && frankenphp.EmbeddedAppPath != "" {
documentRoot = frankenphp.EmbeddedAppPath
}
// If we do not have a resolved document root, then we cannot resolve the symlink of our cwd because it may
// resolve to a different directory than the one we are currently in.
// This is especially important if there are workers running.
documentRootOption = frankenphp.WithRequestDocumentRoot(documentRoot, false)
} else {
documentRoot = f.resolvedDocumentRoot
documentRootOption = frankenphp.WithRequestResolvedDocumentRoot(documentRoot)
opts = append(opts, frankenphp.WithRequestDocumentRoot(documentRoot, false))
}
env := f.preparedEnv
if f.preparedEnvNeedsReplacement {
env = make(frankenphp.PreparedEnv, len(f.Env))
env := make(frankenphp.PreparedEnv, len(f.Env))
for k, v := range f.preparedEnv {
env[k] = repl.ReplaceKnown(v, "")
}
opts = append(opts, frankenphp.WithRequestPreparedEnv(env))
}
workerName := ""
@@ -214,32 +234,16 @@ func (f *FrankenPHPModule) ServeHTTP(w http.ResponseWriter, r *http.Request, _ c
}
}
// TODO: set caddyhttp.ServerHeader when https://github.com/caddyserver/caddy/pull/7338 will be released
w.Header()["Server"] = serverHeader
var err error
if f.mercureHubRequestOption == nil {
err = frankenphp.ServeHTTP(
w,
r,
documentRootOption,
frankenphp.WithRequestSplitPath(f.SplitPath),
frankenphp.WithRequestPreparedEnv(env),
frankenphp.WithOriginalRequest(&origReq),
frankenphp.WithWorkerName(workerName),
)
} else {
err = frankenphp.ServeHTTP(
w,
r,
documentRootOption,
frankenphp.WithRequestSplitPath(f.SplitPath),
frankenphp.WithRequestPreparedEnv(env),
frankenphp.WithOriginalRequest(&origReq),
frankenphp.WithWorkerName(workerName),
*f.mercureHubRequestOption,
)
}
err := frankenphp.ServeHTTP(
w,
r,
append(
opts,
frankenphp.WithOriginalRequest(new(ctx.Value(caddyhttp.OriginalRequestCtxKey).(http.Request))),
frankenphp.WithWorkerName(workerName),
)...,
)
if err != nil {
return caddyhttp.Error(http.StatusInternalServerError, err)
@@ -467,8 +471,7 @@ func parsePhpServer(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error)
if phpsrv.Root == "" {
phpsrv.Root = filepath.Join(frankenphp.EmbeddedAppPath, defaultDocumentRoot)
fsrv.Root = phpsrv.Root
rrs := false
phpsrv.ResolveRootSymlink = &rrs
phpsrv.ResolveRootSymlink = new(false)
} else if filepath.IsLocal(fsrv.Root) {
phpsrv.Root = filepath.Join(frankenphp.EmbeddedAppPath, phpsrv.Root)
fsrv.Root = phpsrv.Root
@@ -488,7 +491,13 @@ func parsePhpServer(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error)
if indexFile != "off" {
dirRedir := false
dirIndex := "{http.request.uri.path}/" + indexFile
// On Windows, first_exist_fallback doesn't work correctly because
// glob is skipped and patterns are returned as-is without checking existence.
// Use first_exist instead to ensure all files are checked.
tryPolicy := "first_exist_fallback"
if runtime.GOOS == "windows" {
tryPolicy = "first_exist"
}
// if tryFiles wasn't overridden, use a reasonable default
if len(tryFiles) == 0 {

View File

@@ -10,8 +10,6 @@ import (
"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"
@@ -113,7 +111,7 @@ func cmdPHPServer(fs caddycmd.Flags) (int, error) {
}
if _, err := os.Stat("Caddyfile"); err == nil {
config, _, err := caddycmd.LoadConfig("Caddyfile", "caddyfile")
config, _, _, err := caddycmd.LoadConfig("Caddyfile", "caddyfile")
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
@@ -253,33 +251,9 @@ func cmdPHPServer(fs caddycmd.Flags) (int, error) {
}
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,
),
},
mercureRoute, err := createMercureRoute()
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
subroute.Routes = append(caddyhttp.RouteList{mercureRoute}, subroute.Routes...)

View File

@@ -2,6 +2,7 @@ package caddy
import (
"net/http"
"path"
"path/filepath"
"strconv"
@@ -43,6 +44,8 @@ type workerConfig struct {
options []frankenphp.WorkerOption
requestOptions []frankenphp.RequestOption
absFileName string
matchRelPath string // pre-computed relative URL path for fast matching
}
func unmarshalWorker(d *caddyfile.Dispenser) (workerConfig, error) {
@@ -171,15 +174,28 @@ func (wc *workerConfig) inheritEnv(env map[string]string) {
}
func (wc *workerConfig) matchesPath(r *http.Request, documentRoot string) bool {
// try to match against a pattern if one is assigned
if len(wc.MatchPath) != 0 {
return (caddyhttp.MatchPath)(wc.MatchPath).Match(r)
}
// if there is no pattern, try to match against the actual path (in the public directory)
fullScriptPath, _ := fastabs.FastAbs(documentRoot + "/" + r.URL.Path)
absFileName, _ := fastabs.FastAbs(wc.FileName)
// fast path: compare the request URL path against the pre-computed relative path
if wc.matchRelPath != "" {
reqPath := r.URL.Path
if reqPath == wc.matchRelPath {
return true
}
return fullScriptPath == absFileName
// ensure leading slash for relative paths (see #2166)
if reqPath == "" || reqPath[0] != '/' {
reqPath = "/" + reqPath
}
return path.Clean(reqPath) == wc.matchRelPath
}
// fallback when documentRoot is dynamic (contains placeholders)
fullPath, _ := fastabs.FastAbs(filepath.Join(documentRoot, r.URL.Path))
return fullPath == wc.absFileName
}

302
cgi.go
View File

@@ -1,15 +1,15 @@
package frankenphp
// #cgo nocallback frankenphp_register_bulk
// #cgo nocallback frankenphp_register_variables_from_request_info
// #cgo nocallback frankenphp_register_server_vars
// #cgo nocallback frankenphp_register_variable_safe
// #cgo nocallback frankenphp_register_single
// #cgo noescape frankenphp_register_bulk
// #cgo noescape frankenphp_register_variables_from_request_info
// #cgo nocallback frankenphp_register_known_variable
// #cgo nocallback frankenphp_init_persistent_string
// #cgo noescape frankenphp_register_server_vars
// #cgo noescape frankenphp_register_variable_safe
// #cgo noescape frankenphp_register_single
// #include <php_variables.h>
// #cgo noescape frankenphp_register_known_variable
// #cgo noescape frankenphp_init_persistent_string
// #include "frankenphp.h"
// #include <php_variables.h>
import "C"
import (
"context"
@@ -18,50 +18,26 @@ import (
"net/http"
"path/filepath"
"strings"
"unicode/utf8"
"unsafe"
"github.com/dunglas/frankenphp/internal/phpheaders"
"golang.org/x/text/language"
"golang.org/x/text/search"
)
// Protocol versions, in Apache mod_ssl format: https://httpd.apache.org/docs/current/mod/mod_ssl.html
// Note that these are slightly different from SupportedProtocols in caddytls/config.go
var tlsProtocolStrings = map[uint16]string{
tls.VersionTLS10: "TLSv1",
tls.VersionTLS11: "TLSv1.1",
tls.VersionTLS12: "TLSv1.2",
tls.VersionTLS13: "TLSv1.3",
}
// Known $_SERVER keys
var knownServerKeys = []string{
"CONTENT_LENGTH",
"DOCUMENT_ROOT",
"DOCUMENT_URI",
"GATEWAY_INTERFACE",
"HTTP_HOST",
"HTTPS",
"PATH_INFO",
"PHP_SELF",
"REMOTE_ADDR",
"REMOTE_HOST",
"REMOTE_PORT",
"REQUEST_SCHEME",
"SCRIPT_FILENAME",
"SCRIPT_NAME",
"SERVER_NAME",
"SERVER_PORT",
"SERVER_PROTOCOL",
"SERVER_SOFTWARE",
"SSL_PROTOCOL",
"SSL_CIPHER",
"AUTH_TYPE",
"REMOTE_IDENT",
"CONTENT_TYPE",
"PATH_TRANSLATED",
"QUERY_STRING",
"REMOTE_USER",
"REQUEST_METHOD",
"REQUEST_URI",
// cStringHTTPMethods caches C string versions of common HTTP methods
// to avoid allocations in pinCString on every request.
var cStringHTTPMethods = map[string]*C.char{
"GET": C.CString("GET"),
"HEAD": C.CString("HEAD"),
"POST": C.CString("POST"),
"PUT": C.CString("PUT"),
"DELETE": C.CString("DELETE"),
"CONNECT": C.CString("CONNECT"),
"OPTIONS": C.CString("OPTIONS"),
"TRACE": C.CString("TRACE"),
"PATCH": C.CString("PATCH"),
}
// computeKnownVariables returns a set of CGI environment variables for the request.
@@ -70,7 +46,6 @@ var knownServerKeys = []string{
// Inspired by https://github.com/caddyserver/caddy/blob/master/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go
func addKnownVariablesToServer(fc *frankenPHPContext, trackVarsArray *C.zval) {
request := fc.request
keys := mainThread.knownServerKeys
// Separate remote IP and port; more lenient than net.SplitHostPort
var ip, port string
if idx := strings.LastIndex(request.RemoteAddr, ":"); idx > -1 {
@@ -81,27 +56,25 @@ func addKnownVariablesToServer(fc *frankenPHPContext, trackVarsArray *C.zval) {
}
// Remove [] from IPv6 addresses
ip = strings.Replace(ip, "[", "", 1)
ip = strings.Replace(ip, "]", "", 1)
if len(ip) > 0 && ip[0] == '[' {
ip = ip[1 : len(ip)-1]
}
var https, sslProtocol, sslCipher, rs string
var rs, https, sslProtocol *C.zend_string
var sslCipher string
if request.TLS == nil {
rs = "http"
https = ""
sslProtocol = ""
rs = C.frankenphp_strings.httpLowercase
https = C.frankenphp_strings.empty
sslProtocol = C.frankenphp_strings.empty
sslCipher = ""
} else {
rs = "https"
https = "on"
rs = C.frankenphp_strings.httpsLowercase
https = C.frankenphp_strings.on
// and pass the protocol details in a manner compatible with apache's mod_ssl
// and pass the protocol details in a manner compatible with Apache's mod_ssl
// (which is why these have an SSL_ prefix and not TLS_).
if v, ok := tlsProtocolStrings[request.TLS.Version]; ok {
sslProtocol = v
} else {
sslProtocol = ""
}
sslProtocol = tlsProtocol(request.TLS.Version)
if request.TLS.CipherSuite != 0 {
sslCipher = tls.CipherSuiteName(request.TLS.CipherSuite)
@@ -121,9 +94,9 @@ func addKnownVariablesToServer(fc *frankenPHPContext, trackVarsArray *C.zval) {
// 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
switch rs {
case "https":
case C.frankenphp_strings.httpsLowercase:
reqPort = "443"
case "http":
case C.frankenphp_strings.httpLowercase:
reqPort = "80"
}
}
@@ -135,62 +108,62 @@ func addKnownVariablesToServer(fc *frankenPHPContext, trackVarsArray *C.zval) {
if fc.originalRequest != nil {
requestURI = fc.originalRequest.URL.RequestURI()
} else {
requestURI = request.URL.RequestURI()
requestURI = fc.requestURI
}
C.frankenphp_register_bulk(
trackVarsArray,
packCgiVariable(keys["REMOTE_ADDR"], ip),
packCgiVariable(keys["REMOTE_HOST"], ip),
packCgiVariable(keys["REMOTE_PORT"], port),
packCgiVariable(keys["DOCUMENT_ROOT"], fc.documentRoot),
packCgiVariable(keys["PATH_INFO"], fc.pathInfo),
packCgiVariable(keys["PHP_SELF"], request.URL.Path),
packCgiVariable(keys["DOCUMENT_URI"], fc.docURI),
packCgiVariable(keys["SCRIPT_FILENAME"], fc.scriptFilename),
packCgiVariable(keys["SCRIPT_NAME"], fc.scriptName),
packCgiVariable(keys["HTTPS"], https),
packCgiVariable(keys["SSL_PROTOCOL"], sslProtocol),
packCgiVariable(keys["REQUEST_SCHEME"], rs),
packCgiVariable(keys["SERVER_NAME"], reqHost),
packCgiVariable(keys["SERVER_PORT"], serverPort),
// Variables defined in CGI 1.1 spec
// Some variables are unused but cleared explicitly to prevent
// the parent environment from interfering.
// These values can not be overridden
packCgiVariable(keys["CONTENT_LENGTH"], contentLength),
packCgiVariable(keys["GATEWAY_INTERFACE"], "CGI/1.1"),
packCgiVariable(keys["SERVER_PROTOCOL"], request.Proto),
packCgiVariable(keys["SERVER_SOFTWARE"], "FrankenPHP"),
packCgiVariable(keys["HTTP_HOST"], request.Host),
// These values are always empty but must be defined:
packCgiVariable(keys["AUTH_TYPE"], ""),
packCgiVariable(keys["REMOTE_IDENT"], ""),
// Request uri of the original request
packCgiVariable(keys["REQUEST_URI"], requestURI),
packCgiVariable(keys["SSL_CIPHER"], sslCipher),
)
requestPath := ensureLeadingSlash(request.URL.Path)
// These values are already present in the SG(request_info), so we'll register them from there
C.frankenphp_register_variables_from_request_info(
trackVarsArray,
keys["CONTENT_TYPE"],
keys["PATH_TRANSLATED"],
keys["QUERY_STRING"],
keys["REMOTE_USER"],
keys["REQUEST_METHOD"],
)
}
C.frankenphp_register_server_vars(trackVarsArray, C.frankenphp_server_vars{
// approximate total length to avoid array re-hashing:
// 28 CGI vars + headers + environment
total_num_vars: C.size_t(28 + len(request.Header) + len(fc.env) + lengthOfEnv),
func packCgiVariable(key *C.zend_string, value string) C.ht_key_value_pair {
return C.ht_key_value_pair{key, toUnsafeChar(value), C.size_t(len(value))}
// CGI vars with variable values
remote_addr: toUnsafeChar(ip),
remote_addr_len: C.size_t(len(ip)),
remote_host: toUnsafeChar(ip),
remote_host_len: C.size_t(len(ip)),
remote_port: toUnsafeChar(port),
remote_port_len: C.size_t(len(port)),
document_root: toUnsafeChar(fc.documentRoot),
document_root_len: C.size_t(len(fc.documentRoot)),
path_info: toUnsafeChar(fc.pathInfo),
path_info_len: C.size_t(len(fc.pathInfo)),
php_self: toUnsafeChar(requestPath),
php_self_len: C.size_t(len(requestPath)),
document_uri: toUnsafeChar(fc.docURI),
document_uri_len: C.size_t(len(fc.docURI)),
script_filename: toUnsafeChar(fc.scriptFilename),
script_filename_len: C.size_t(len(fc.scriptFilename)),
script_name: toUnsafeChar(fc.scriptName),
script_name_len: C.size_t(len(fc.scriptName)),
server_name: toUnsafeChar(reqHost),
server_name_len: C.size_t(len(reqHost)),
server_port: toUnsafeChar(serverPort),
server_port_len: C.size_t(len(serverPort)),
content_length: toUnsafeChar(contentLength),
content_length_len: C.size_t(len(contentLength)),
server_protocol: toUnsafeChar(request.Proto),
server_protocol_len: C.size_t(len(request.Proto)),
http_host: toUnsafeChar(request.Host),
http_host_len: C.size_t(len(request.Host)),
request_uri: toUnsafeChar(requestURI),
request_uri_len: C.size_t(len(requestURI)),
ssl_cipher: toUnsafeChar(sslCipher),
ssl_cipher_len: C.size_t(len(sslCipher)),
// CGI vars with known values
request_scheme: rs, // "http" or "https"
ssl_protocol: sslProtocol, // values from tlsProtocol
https: https, // "on" or empty
})
}
func addHeadersToServer(ctx context.Context, request *http.Request, trackVarsArray *C.zval) {
for field, val := range request.Header {
if k := mainThread.commonHeaders[field]; k != nil {
if k := commonHeaders[field]; k != nil {
v := strings.Join(val, ", ")
C.frankenphp_register_single(k, toUnsafeChar(v), C.size_t(len(v)), trackVarsArray)
C.frankenphp_register_known_variable(k, toUnsafeChar(v), C.size_t(len(v)), trackVarsArray)
continue
}
@@ -209,8 +182,8 @@ func addPreparedEnvToServer(fc *frankenPHPContext, trackVarsArray *C.zval) {
fc.env = nil
}
//export go_register_variables
func go_register_variables(threadIndex C.uintptr_t, trackVarsArray *C.zval) {
//export go_register_server_variables
func go_register_server_variables(threadIndex C.uintptr_t, trackVarsArray *C.zval) {
thread := phpThreads[threadIndex]
fc := thread.frankenPHPContext()
@@ -249,27 +222,67 @@ func splitCgiPath(fc *frankenPHPContext) {
// TODO: is it possible to delay this and avoid saving everything in the context?
// SCRIPT_FILENAME is the absolute path of SCRIPT_NAME
fc.scriptFilename = sanitizedPathJoin(fc.documentRoot, fc.scriptName)
fc.worker = getWorkerByPath(fc.scriptFilename)
fc.worker = workersByPath[fc.scriptFilename]
}
// splitPos returns the index where path should
// be split based on SplitPath.
var splitSearchNonASCII = search.New(language.Und, search.IgnoreCase)
// splitPos returns the index where path should be split based on splitPath.
// example: if splitPath is [".php"]
// "/path/to/script.php/some/path": ("/path/to/script.php", "/some/path")
//
// 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(path string, splitPath []string) int {
if len(splitPath) == 0 {
return 0
}
lowerPath := strings.ToLower(path)
pathLen := len(path)
// We are sure that split strings are all ASCII-only and lower-case because of validation and normalization in WithRequestSplitPath
for _, split := range splitPath {
if idx := strings.Index(lowerPath, strings.ToLower(split)); idx > -1 {
return idx + len(split)
splitLen := len(split)
for i := 0; i < pathLen; i++ {
if path[i] >= utf8.RuneSelf {
if _, end := splitSearchNonASCII.IndexString(path, split); end > -1 {
return end
}
break
}
if i+splitLen > pathLen {
continue
}
match := true
for j := 0; j < splitLen; j++ {
c := path[i+j]
if c >= utf8.RuneSelf {
if _, end := splitSearchNonASCII.IndexString(path, split); end > -1 {
return end
}
break
}
if 'A' <= c && c <= 'Z' {
c += 'a' - 'A'
}
if c != split[j] {
match = false
break
}
}
if match {
return i + splitLen
}
}
}
return -1
}
@@ -286,7 +299,11 @@ func go_update_request_info(threadIndex C.uintptr_t, info *C.sapi_request_info)
return nil
}
info.request_method = thread.pinCString(request.Method)
if m, ok := cStringHTTPMethods[request.Method]; ok {
info.request_method = m
} else {
info.request_method = thread.pinCString(request.Method)
}
info.query_string = thread.pinCString(request.URL.RawQuery)
info.content_length = C.zend_long(request.ContentLength)
@@ -298,7 +315,7 @@ func go_update_request_info(threadIndex C.uintptr_t, info *C.sapi_request_info)
info.path_translated = thread.pinCString(sanitizedPathJoin(fc.documentRoot, fc.pathInfo)) // See: http://www.oreilly.com/openbook/cgi/ch02_04.html
}
info.request_uri = thread.pinCString(request.URL.RequestURI())
info.request_uri = thread.pinCString(fc.requestURI)
info.proto_num = C.int(request.ProtoMajor*1000 + request.ProtoMinor)
@@ -340,7 +357,40 @@ func sanitizedPathJoin(root, reqPath string) string {
const separator = string(filepath.Separator)
func toUnsafeChar(s string) *C.char {
sData := unsafe.StringData(s)
return (*C.char)(unsafe.Pointer(sData))
func ensureLeadingSlash(path string) string {
if path == "" || path[0] == '/' {
return path
}
return "/" + path
}
// toUnsafeChar returns a *C.char pointing at the backing bytes the Go string.
// If C does not store the string, it may be passed directly in a Cgo call (most efficient).
// If C stores the string, it must be pinned explicitly instead (inefficient).
// C may never modify the string.
func toUnsafeChar(s string) *C.char {
return (*C.char)(unsafe.Pointer(unsafe.StringData(s)))
}
// initialize a global zend_string that must never be freed and is ignored by GC
func newPersistentZendString(str string) *C.zend_string {
return C.frankenphp_init_persistent_string(toUnsafeChar(str), C.size_t(len(str)))
}
// Protocol versions, in Apache mod_ssl format: https://httpd.apache.org/docs/current/mod/mod_ssl.html
// Note that these are slightly different from SupportedProtocols in caddytls/config.go
func tlsProtocol(proto uint16) *C.zend_string {
switch proto {
case tls.VersionTLS10:
return C.frankenphp_strings.tls1
case tls.VersionTLS11:
return C.frankenphp_strings.tls11
case tls.VersionTLS12:
return C.frankenphp_strings.tls12
case tls.VersionTLS13:
return C.frankenphp_strings.tls13
default:
return C.frankenphp_strings.empty
}
}

210
cgi_test.go Normal file
View File

@@ -0,0 +1,210 @@
package frankenphp
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func TestEnsureLeadingSlash(t *testing.T) {
t.Parallel()
tests := []struct {
input string
expected string
}{
{"/index.php", "/index.php"},
{"index.php", "/index.php"},
{"/", "/"},
{"", ""},
{"/path/to/script.php", "/path/to/script.php"},
{"path/to/script.php", "/path/to/script.php"},
{"/index.php/path/info", "/index.php/path/info"},
{"index.php/path/info", "/index.php/path/info"},
}
for _, tt := range tests {
t.Run(tt.input+"-"+tt.expected, func(t *testing.T) {
t.Parallel()
assert.Equal(t, tt.expected, ensureLeadingSlash(tt.input), "ensureLeadingSlash(%q)", tt.input)
})
}
}
func TestSplitPos(t *testing.T) {
tests := []struct {
name string
path string
splitPath []string
wantPos int
}{
{
name: "simple php extension",
path: "/path/to/script.php",
splitPath: []string{".php"},
wantPos: 19,
},
{
name: "php extension with path info",
path: "/path/to/script.php/some/path",
splitPath: []string{".php"},
wantPos: 19,
},
{
name: "case insensitive match",
path: "/path/to/script.PHP",
splitPath: []string{".php"},
wantPos: 19,
},
{
name: "mixed case match",
path: "/path/to/script.PhP/info",
splitPath: []string{".php"},
wantPos: 19,
},
{
name: "no match",
path: "/path/to/script.txt",
splitPath: []string{".php"},
wantPos: -1,
},
{
name: "empty split path",
path: "/path/to/script.php",
splitPath: []string{},
wantPos: 0,
},
{
name: "multiple split paths first match",
path: "/path/to/script.php",
splitPath: []string{".php", ".phtml"},
wantPos: 19,
},
{
name: "multiple split paths second match",
path: "/path/to/script.phtml",
splitPath: []string{".php", ".phtml"},
wantPos: 21,
},
// Unicode case-folding tests (security fix for GHSA-g966-83w7-6w38)
// U+023A (Ⱥ) lowercases to U+2C65 (ⱥ), which has different UTF-8 byte length
// Ⱥ: 2 bytes (C8 BA), ⱥ: 3 bytes (E2 B1 A5)
{
name: "unicode path with case-folding length expansion",
path: "/ȺȺȺȺshell.php",
splitPath: []string{".php"},
wantPos: 18, // correct position in original string
},
{
name: "unicode path with extension after expansion chars",
path: "/ȺȺȺȺshell.php/path/info",
splitPath: []string{".php"},
wantPos: 18,
},
{
name: "unicode in filename with multiple php occurrences",
path: "/ȺȺȺȺshell.php.txt.php",
splitPath: []string{".php"},
wantPos: 18, // should match first .php, not be confused by byte offset shift
},
{
name: "unicode case insensitive extension",
path: "/ȺȺȺȺshell.PHP",
splitPath: []string{".php"},
wantPos: 18,
},
{
name: "unicode in middle of path",
path: "/path/Ⱥtest/script.php",
splitPath: []string{".php"},
wantPos: 23, // Ⱥ is 2 bytes, so path is 23 bytes total, .php ends at byte 23
},
{
name: "unicode only in directory not filename",
path: "/Ⱥ/script.php",
splitPath: []string{".php"},
wantPos: 14,
},
// Additional Unicode characters that expand when lowercased
// U+0130 (İ - Turkish capital I with dot) lowercases to U+0069 + U+0307
{
name: "turkish capital I with dot",
path: "/İtest.php",
splitPath: []string{".php"},
wantPos: 11,
},
// Ensure standard ASCII still works correctly
{
name: "ascii only path with case variation",
path: "/PATH/TO/SCRIPT.PHP/INFO",
splitPath: []string{".php"},
wantPos: 19,
},
{
name: "path at root",
path: "/index.php",
splitPath: []string{".php"},
wantPos: 10,
},
{
name: "extension in middle of filename",
path: "/test.php.bak",
splitPath: []string{".php"},
wantPos: 9,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotPos := splitPos(tt.path, tt.splitPath)
assert.Equal(t, tt.wantPos, gotPos, "splitPos(%q, %v)", tt.path, tt.splitPath)
// Verify that the split produces valid substrings
if gotPos > 0 && gotPos <= len(tt.path) {
scriptName := tt.path[:gotPos]
pathInfo := tt.path[gotPos:]
// The script name should end with one of the split extensions (case-insensitive)
hasValidEnding := false
for _, split := range tt.splitPath {
if strings.HasSuffix(strings.ToLower(scriptName), split) {
hasValidEnding = true
break
}
}
assert.True(t, hasValidEnding, "script name %q should end with one of %v", scriptName, tt.splitPath)
// Original path should be reconstructable
assert.Equal(t, tt.path, scriptName+pathInfo, "path should be reconstructable from split parts")
}
})
}
}
// TestSplitPosUnicodeSecurityRegression specifically tests the vulnerability
// described in GHSA-g966-83w7-6w38 where Unicode case-folding caused
// incorrect SCRIPT_NAME/PATH_INFO splitting
func TestSplitPosUnicodeSecurityRegression(t *testing.T) {
// U+023A: Ⱥ (UTF-8: C8 BA). Lowercase is ⱥ (UTF-8: E2 B1 A5), longer in bytes.
path := "/ȺȺȺȺshell.php.txt.php"
split := []string{".php"}
pos := splitPos(path, split)
// The vulnerable code would return 22 (computed on lowercased string)
// The correct code should return 18 (position in original string)
expectedPos := strings.Index(path, ".php") + len(".php")
assert.Equal(t, expectedPos, pos, "split position should match first .php in original string")
assert.Equal(t, 18, pos, "split position should be 18, not 22")
if pos > 0 && pos <= len(path) {
scriptName := path[:pos]
pathInfo := path[pos:]
assert.Equal(t, "/ȺȺȺȺshell.php", scriptName, "script name should be the path up to first .php")
assert.Equal(t, ".txt.php", pathInfo, "path info should be the remainder after first .php")
}
}

6
cgo.go
View File

@@ -1,9 +1,11 @@
package frankenphp
// #cgo darwin pkg-config: libxml-2.0
// #cgo CFLAGS: -Wall -Werror
// #cgo unix CFLAGS: -Wall -Werror
// #cgo linux CFLAGS: -D_GNU_SOURCE
// #cgo LDFLAGS: -lphp -lm -lutil
// #cgo unix LDFLAGS: -lphp -lm -lutil
// #cgo linux LDFLAGS: -ldl -lresolv
// #cgo darwin LDFLAGS: -Wl,-rpath,/usr/local/lib -liconv -ldl
// #cgo windows CFLAGS: -D_WINDOWS -DWINDOWS=1 -DZEND_WIN32=1 -DPHP_WIN32=1 -DWIN32 -D_MBCS -D_USE_MATH_DEFINES -DNDebug -DNDEBUG -DZEND_DEBUG=0 -DZTS=1 -DFD_SETSIZE=256
// #cgo windows LDFLAGS: -lpthreadVC3
import "C"

29
cli.go Normal file
View File

@@ -0,0 +1,29 @@
package frankenphp
// #include "frankenphp.h"
import "C"
import "unsafe"
// ExecuteScriptCLI executes the PHP script passed as parameter.
// It returns the exit status code of the script.
func ExecuteScriptCLI(script string, args []string) int {
// Ensure extensions are registered before CLI execution
registerExtensions()
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])), false))
}
func ExecutePHPCode(phpCode string) int {
// Ensure extensions are registered before CLI execution
registerExtensions()
cCode := C.CString(phpCode)
defer C.free(unsafe.Pointer(cCode))
return int(C.frankenphp_execute_script_cli(cCode, 0, nil, true))
}

55
cli_test.go Normal file
View File

@@ -0,0 +1,55 @@
package frankenphp_test
import (
"errors"
"log"
"os"
"os/exec"
"testing"
"github.com/dunglas/frankenphp"
"github.com/stretchr/testify/assert"
)
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)
var exitError *exec.ExitError
if errors.As(err, &exitError) {
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 TestExecuteCLICode(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", "-r", "echo 'Hello World';")
stdoutStderr, err := cmd.CombinedOutput()
assert.NoError(t, err)
stdoutStderrStr := string(stdoutStderr)
assert.Equal(t, stdoutStderrStr, `Hello World`)
}
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))
}

View File

@@ -28,14 +28,17 @@ type frankenPHPContext struct {
pathInfo string
scriptName string
scriptFilename string
requestURI string
ctx context.Context
// Whether the request is already closed by us
isDone bool
responseWriter http.ResponseWriter
handlerParameters any
handlerReturn any
responseWriter http.ResponseWriter
responseController *http.ResponseController
handlerParameters any
handlerReturn any
done chan any
startedAt time.Time
@@ -82,6 +85,8 @@ func newFrankenPHPContext(responseWriter http.ResponseWriter, r *http.Request, o
splitCgiPath(fc)
}
fc.requestURI = r.URL.RequestURI()
return fc, nil
}

View File

@@ -1,7 +1,7 @@
# syntax=docker/dockerfile:1
#checkov:skip=CKV_DOCKER_2
#checkov:skip=CKV_DOCKER_3
FROM golang:1.25-alpine
FROM golang:1.26-alpine
ENV GOTOOLCHAIN=local
ENV CFLAGS="-ggdb3"

View File

@@ -1,7 +1,7 @@
# syntax=docker/dockerfile:1
#checkov:skip=CKV_DOCKER_2
#checkov:skip=CKV_DOCKER_3
FROM golang:1.25
FROM golang:1.26
ENV GOTOOLCHAIN=local
ENV CFLAGS="-ggdb3"

View File

@@ -11,7 +11,7 @@ variable "PHP_VERSION" {
}
variable "GO_VERSION" {
default = "1.25"
default = "1.26"
}
variable "BASE_FINGERPRINT" {

View File

@@ -96,6 +96,7 @@ You can also explicitly configure FrankenPHP using the [global option](https://c
num_threads <num_threads> # Sets the number of PHP threads to start. Default: 2x the number of available CPUs.
max_threads <num_threads> # Limits the number of additional PHP threads that can be started at runtime. Default: num_threads. Can be set to 'auto'.
max_wait_time <duration> # Sets the maximum time a request may wait for a free PHP thread before timing out. Default: disabled.
max_idle_time <duration> # Sets the maximum time an autoscaled thread may be idle before being deactivated. Default: 5s.
php_ini <key> <value> # Set a php.ini directive. Can be used several times to set multiple directives.
worker {
file <path> # Sets the path to the worker script.

View File

@@ -256,15 +256,16 @@ COPY "$PATH_TO_CADDYFILE" /etc/caddy/Caddyfile
# Copy frankenphp and necessary libs
COPY --from=builder /usr/local/bin/frankenphp /usr/local/bin/frankenphp
COPY --from=builder --chown=nonroot:nonroot /usr/local/lib/php/extensions /usr/local/lib/php/extensions
COPY --from=builder /usr/local/lib/php/extensions /usr/local/lib/php/extensions
COPY --from=builder /tmp/libs /usr/lib
# Copy php.ini configuration files
COPY --from=builder /usr/local/etc/php/conf.d /usr/local/etc/php/conf.d
COPY --from=builder /usr/local/etc/php/php.ini-production /usr/local/etc/php/php.ini
# Create necessary caddy dirs
# These dirs also need to be writable in case of a read-only root filesystem
# Caddy data dirs — must be writable for nonroot, even on a read-only root filesystem
ENV XDG_CONFIG_HOME=/config \
XDG_DATA_HOME=/data
COPY --from=builder --chown=nonroot:nonroot /data/caddy /data/caddy
COPY --from=builder --chown=nonroot:nonroot /config/caddy /config/caddy

220
docs/es/CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,220 @@
# Contribuir
## Compilar PHP
### Con Docker (Linux)
Construya la imagen Docker de desarrollo:
```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
```
La imagen contiene las herramientas de desarrollo habituales (Go, GDB, Valgrind, Neovim...) y utiliza las siguientes ubicaciones de configuración de PHP:
- php.ini: `/etc/frankenphp/php.ini` Se proporciona un archivo php.ini con ajustes preestablecidos de desarrollo por defecto.
- archivos de configuración adicionales: `/etc/frankenphp/php.d/*.ini`
- extensiones php: `/usr/lib/frankenphp/modules/`
Si su versión de Docker es inferior a 23.0, la construcción fallará debido a un [problema de patrón](https://github.com/moby/moby/pull/42676) en `.dockerignore`. Agregue los directorios a `.dockerignore`:
```patch
!testdata/*.php
!testdata/*.txt
+!caddy
+!internal
```
### Sin Docker (Linux y macOS)
[Siga las instrucciones para compilar desde las fuentes](compile.md) y pase la bandera de configuración `--debug`.
## Ejecutar la suite de pruebas
```console
go test -tags watcher -race -v ./...
```
## Módulo Caddy
Construir Caddy con el módulo FrankenPHP:
```console
cd caddy/frankenphp/
go build -tags watcher,brotli,nobadger,nomysql,nopgx
cd ../../
```
Ejecutar Caddy con el módulo FrankenPHP:
```console
cd testdata/
../caddy/frankenphp/frankenphp run
```
El servidor está configurado para escuchar en la dirección `127.0.0.1:80`:
> [!NOTE]
>
> Si está usando Docker, deberá enlazar el puerto 80 del contenedor o ejecutar desde dentro del contenedor.
```console
curl -vk http://127.0.0.1/phpinfo.php
```
## Servidor de prueba mínimo
Construir el servidor de prueba mínimo:
```console
cd internal/testserver/
go build
cd ../../
```
Iniciar el servidor de prueba:
```console
cd testdata/
../internal/testserver/testserver
```
El servidor está configurado para escuchar en la dirección `127.0.0.1:8080`:
```console
curl -v http://127.0.0.1:8080/phpinfo.php
```
## Construir localmente las imágenes Docker
Mostrar el plan de compilación:
```console
docker buildx bake -f docker-bake.hcl --print
```
Construir localmente las imágenes FrankenPHP para amd64:
```console
docker buildx bake -f docker-bake.hcl --pull --load --set "*.platform=linux/amd64"
```
Construir localmente las imágenes FrankenPHP para arm64:
```console
docker buildx bake -f docker-bake.hcl --pull --load --set "*.platform=linux/arm64"
```
Construir desde cero las imágenes FrankenPHP para arm64 y amd64 y subirlas a Docker Hub:
```console
docker buildx bake -f docker-bake.hcl --pull --no-cache --push
```
## Depurar errores de segmentación con las compilaciones estáticas
1. Descargue la versión de depuración del binario FrankenPHP desde GitHub o cree su propia compilación estática incluyendo símbolos de depuración:
```console
docker buildx bake \
--load \
--set static-builder.args.DEBUG_SYMBOLS=1 \
--set "static-builder.platform=linux/amd64" \
static-builder
docker cp $(docker create --name static-builder-musl dunglas/frankenphp:static-builder-musl):/go/src/app/dist/frankenphp-linux-$(uname -m) frankenphp
```
2. Reemplace su versión actual de `frankenphp` por el ejecutable de depuración de FrankenPHP.
3. Inicie FrankenPHP como de costumbre (alternativamente, puede iniciar FrankenPHP directamente con GDB: `gdb --args frankenphp run`).
4. Adjunte el proceso con GDB:
```console
gdb -p `pidof frankenphp`
```
5. Si es necesario, escriba `continue` en el shell de GDB.
6. Haga que FrankenPHP falle.
7. Escriba `bt` en el shell de GDB.
8. Copie la salida.
## Depurar errores de segmentación en GitHub Actions
1. Abrir `.github/workflows/tests.yml`
2. Activar los símbolos de depuración de la biblioteca PHP:
```patch
- uses: shivammathur/setup-php@v2
# ...
env:
phpts: ts
+ debug: true
```
3. Activar `tmate` para conectarse al contenedor:
```patch
- name: Set CGO flags
run: echo "CGO_CFLAGS=$(php-config --includes)" >> "$GITHUB_ENV"
+ - 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
```
4. Conectarse al contenedor.
5. Abrir `frankenphp.go`.
6. Activar `cgosymbolizer`:
```patch
- //_ "github.com/ianlancetaylor/cgosymbolizer"
+ _ "github.com/ianlancetaylor/cgosymbolizer"
```
7. Descargar el módulo: `go get`.
8. Dentro del contenedor, puede usar GDB y similares:
```console
go test -tags watcher -c -ldflags=-w
gdb --args frankenphp.test -test.run ^MyTest$
```
9. Cuando el error esté corregido, revierta todos los cambios.
## Recursos diversos para el desarrollo
- [Integración de PHP en uWSGI](https://github.com/unbit/uwsgi/blob/master/plugins/php/php_plugin.c)
- [Integración de PHP en NGINX Unit](https://github.com/nginx/unit/blob/master/src/nxt_php_sapi.c)
- [Integración de PHP en Go (go-php)](https://github.com/deuill/go-php)
- [Integración de PHP en Go (GoEmPHP)](https://github.com/mikespook/goemphp)
- [Integración de PHP en C++](https://gist.github.com/paresy/3cbd4c6a469511ac7479aa0e7c42fea7)
- [Extending and Embedding PHP por Sara Golemon](https://books.google.fr/books?id=zMbGvK17_tYC&pg=PA254&lpg=PA254#v=onepage&q&f=false)
- [¿Qué es TSRMLS_CC, exactamente?](http://blog.golemon.com/2006/06/what-heck-is-tsrmlscc-anyway.html)
- [Integración de PHP en Mac](https://gist.github.com/jonnywang/61427ffc0e8dde74fff40f479d147db4)
- [Bindings SDL](https://pkg.go.dev/github.com/veandco/go-sdl2@v0.4.21/sdl#Main)
## Recursos relacionados con Docker
- [Definición del archivo Bake](https://docs.docker.com/build/customize/bake/file-definition/)
- [`docker buildx build`](https://docs.docker.com/engine/reference/commandline/buildx_build/)
## Comando útil
```console
apk add strace util-linux gdb
strace -e 'trace=!futex,epoll_ctl,epoll_pwait,tgkill,rt_sigreturn' -p 1
```
## Traducir la documentación
Para traducir la documentación y el sitio a un nuevo idioma, siga estos pasos:
1. Cree un nuevo directorio con el código ISO de 2 caracteres del idioma en el directorio `docs/` de este repositorio.
2. Copie todos los archivos `.md` de la raíz del directorio `docs/` al nuevo directorio (siempre use la versión en inglés como fuente de traducción, ya que siempre está actualizada).
3. Copie los archivos `README.md` y `CONTRIBUTING.md` del directorio raíz al nuevo directorio.
4. Traduzca el contenido de los archivos, pero no cambie los nombres de los archivos, tampoco traduzca las cadenas que comiencen por `> [!` (es un marcado especial para GitHub).
5. Cree una Pull Request con las traducciones.
6. En el [repositorio del sitio](https://github.com/dunglas/frankenphp-website/tree/main), copie y traduzca los archivos de traducción en los directorios `content/`, `data/` y `i18n/`.
7. Traduzca los valores en el archivo YAML creado.
8. Abra una Pull Request en el repositorio del sitio.

162
docs/es/README.md Normal file
View File

@@ -0,0 +1,162 @@
# FrankenPHP: el servidor de aplicaciones PHP moderno, escrito en Go
<h1 align="center"><a href="https://frankenphp.dev"><img src="../../frankenphp.png" alt="FrankenPHP" width="600"></a></h1>
FrankenPHP es un servidor de aplicaciones moderno para PHP construido sobre el servidor web [Caddy](https://caddyserver.com/).
FrankenPHP otorga superpoderes a tus aplicaciones PHP gracias a sus características de vanguardia: [_Early Hints_](early-hints.md), [modo worker](worker.md), [funcionalidades en tiempo real](mercure.md), HTTPS automático, soporte para HTTP/2 y HTTP/3...
FrankenPHP funciona con cualquier aplicación PHP y hace que tus proyectos Laravel y Symfony sean más rápidos que nunca gracias a sus integraciones oficiales con el modo worker.
FrankenPHP también puede usarse como una biblioteca Go autónoma que permite integrar PHP en cualquier aplicación usando `net/http`.
Descubre más detalles sobre este servidor de aplicaciones en la grabación de esta conferencia dada en el Forum PHP 2022:
<a href="https://dunglas.dev/2022/10/frankenphp-the-modern-php-app-server-written-in-go/"><img src="https://dunglas.dev/wp-content/uploads/2022/10/frankenphp.png" alt="Diapositivas" width="600"></a>
## Para Comenzar
En Windows, usa [WSL](https://learn.microsoft.com/es-es/windows/wsl/) para ejecutar FrankenPHP.
### Script de instalación
Puedes copiar esta línea en tu terminal para instalar automáticamente
una versión adaptada a tu plataforma:
```console
curl https://frankenphp.dev/install.sh | sh
```
### Binario autónomo
Proporcionamos binarios estáticos de FrankenPHP para desarrollo, para Linux y macOS,
conteniendo [PHP 8.4](https://www.php.net/releases/8.4/es.php) y la mayoría de las extensiones PHP populares.
[Descargar FrankenPHP](https://github.com/php/frankenphp/releases)
**Instalación de extensiones:** Las extensiones más comunes están incluidas. No es posible instalar más.
### Paquetes rpm
Nuestros mantenedores proponen paquetes rpm para todos los sistemas que usan `dnf`. Para instalar, ejecuta:
```console
sudo dnf install https://rpm.henderkes.com/static-php-1-0.noarch.rpm
sudo dnf module enable php-zts:static-8.4 # 8.2-8.5 disponibles
sudo dnf install frankenphp
```
**Instalación de extensiones:** `sudo dnf install php-zts-<extension>`
Para extensiones no disponibles por defecto, usa [PIE](https://github.com/php/pie):
```console
sudo dnf install pie-zts
sudo pie-zts install asgrim/example-pie-extension
```
### Paquetes deb
Nuestros mantenedores proponen paquetes deb para todos los sistemas que usan `apt`. Para instalar, ejecuta:
```console
sudo curl -fsSL https://key.henderkes.com/static-php.gpg -o /usr/share/keyrings/static-php.gpg && \
echo "deb [signed-by=/usr/share/keyrings/static-php.gpg] https://deb.henderkes.com/ stable main" | sudo tee /etc/apt/sources.list.d/static-php.list && \
sudo apt update
sudo apt install frankenphp
```
**Instalación de extensiones:** `sudo apt install php-zts-<extension>`
Para extensiones no disponibles por defecto, usa [PIE](https://github.com/php/pie):
```console
sudo apt install pie-zts
sudo pie-zts install asgrim/example-pie-extension
```
### Docker
Las [imágenes Docker](https://frankenphp.dev/docs/es/docker/) también están disponibles:
```console
docker run -v .:/app/public \
-p 80:80 -p 443:443 -p 443:443/udp \
dunglas/frankenphp
```
Ve a `https://localhost`, ¡listo!
> [!TIP]
>
> No intentes usar `https://127.0.0.1`. Usa `https://localhost` y acepta el certificado auto-firmado.
> Usa [la variable de entorno `SERVER_NAME`](config.md#variables-de-entorno) para cambiar el dominio a usar.
### Homebrew
FrankenPHP también está disponible como paquete [Homebrew](https://brew.sh) para macOS y Linux.
Para instalarlo:
```console
brew install dunglas/frankenphp/frankenphp
```
**Instalación de extensiones:** Usa [PIE](https://github.com/php/pie).
### Uso
Para servir el contenido del directorio actual, ejecuta:
```console
frankenphp php-server
```
También puedes ejecutar scripts en línea de comandos con:
```console
frankenphp php-cli /ruta/a/tu/script.php
```
Para los paquetes deb y rpm, también puedes iniciar el servicio systemd:
```console
sudo systemctl start frankenphp
```
## Documentación
- [El modo clásico](classic.md)
- [El modo worker](worker.md)
- [Soporte para Early Hints (código de estado HTTP 103)](early-hints.md)
- [Tiempo real](mercure.md)
- [Hot reloading](https://frankenphp.dev/docs/hot-reload/)
- [Registro de actividad](https://frankenphp.dev/docs/logging/)
- [Servir eficientemente archivos estáticos grandes](x-sendfile.md)
- [Configuración](config.md)
- [Escribir extensiones PHP en Go](extensions.md)
- [Imágenes Docker](docker.md)
- [Despliegue en producción](production.md)
- [Optimización del rendimiento](performance.md)
- [Crear aplicaciones PHP **autónomas**, auto-ejecutables](embed.md)
- [Crear una compilación estática](static.md)
- [Compilar desde las fuentes](compile.md)
- [Monitoreo de FrankenPHP](metrics.md)
- [Integración con WordPress](https://frankenphp.dev/docs/wordpress/)
- [Integración con Laravel](laravel.md)
- [Problemas conocidos](known-issues.md)
- [Aplicación de demostración (Symfony) y benchmarks](https://github.com/dunglas/frankenphp-demo)
- [Documentación de la biblioteca Go](https://pkg.go.dev/github.com/dunglas/frankenphp)
- [Contribuir y depurar](CONTRIBUTING.md)
## Ejemplos y esqueletos
- [Symfony](https://github.com/dunglas/symfony-docker)
- [API Platform](https://api-platform.com/docs/distribution/)
- [Laravel](laravel.md)
- [Sulu](https://sulu.io/blog/running-sulu-with-frankenphp)
- [WordPress](https://github.com/StephenMiracle/frankenwp)
- [Drupal](https://github.com/dunglas/frankenphp-drupal)
- [Joomla](https://github.com/alexandreelise/frankenphp-joomla)
- [TYPO3](https://github.com/ochorocho/franken-typo3)
- [Magento2](https://github.com/ekino/frankenphp-magento2)

11
docs/es/classic.md Normal file
View File

@@ -0,0 +1,11 @@
# Usando el Modo Clásico
Sin ninguna configuración adicional, FrankenPHP opera en modo clásico. En este modo, FrankenPHP funciona como un servidor PHP tradicional, sirviendo directamente archivos PHP. Esto lo convierte en un reemplazo directo para PHP-FPM o Apache con mod_php.
Al igual que Caddy, FrankenPHP acepta un número ilimitado de conexiones y utiliza un [número fijo de hilos](config.md#caddyfile-config) para atenderlas. La cantidad de conexiones aceptadas y en cola está limitada únicamente por los recursos disponibles del sistema.
El *pool* de hilos de PHP opera con un número fijo de hilos inicializados al inicio, comparable al modo estático de PHP-FPM. También es posible permitir que los hilos [escale automáticamente en tiempo de ejecución](performance.md#max_threads), similar al modo dinámico de PHP-FPM.
Las conexiones en cola esperarán indefinidamente hasta que un hilo de PHP esté disponible para atenderlas. Para evitar esto, puedes usar la configuración `max_wait_time` en la [configuración global de FrankenPHP](config.md#caddyfile-config) para limitar la duración que una petición puede esperar por un hilo de PHP libre antes de ser rechazada.
Adicionalmente, puedes establecer un [tiempo límite de escritura razonable en Caddy](https://caddyserver.com/docs/caddyfile/options#timeouts).
Cada instancia de Caddy iniciará solo un *pool* de hilos de FrankenPHP, el cual será compartido entre todos los bloques `php_server`.

133
docs/es/compile.md Normal file
View File

@@ -0,0 +1,133 @@
# Compilar desde fuentes
Este documento explica cómo crear un binario de FrankenPHP que cargará PHP como una biblioteca dinámica.
Esta es la forma recomendada.
Alternativamente, también se pueden crear [compilaciones estáticas y mayormente estáticas](static.md).
## Instalar PHP
FrankenPHP es compatible con PHP 8.2 y versiones superiores.
### Con Homebrew (Linux y Mac)
La forma más sencilla de instalar una versión de libphp compatible con FrankenPHP es usar los paquetes ZTS proporcionados por [Homebrew PHP](https://github.com/shivammathur/homebrew-php).
Primero, si no lo ha hecho ya, instale [Homebrew](https://brew.sh).
Luego, instale la variante ZTS de PHP, Brotli (opcional, para soporte de compresión) y watcher (opcional, para detección de cambios en archivos):
```console
brew install shivammathur/php/php-zts brotli watcher
brew link --overwrite --force shivammathur/php/php-zts
```
### Compilando PHP
Alternativamente, puede compilar PHP desde las fuentes con las opciones necesarias para FrankenPHP siguiendo estos pasos.
Primero, [obtenga las fuentes de PHP](https://www.php.net/downloads.php) y extráigalas:
```console
tar xf php-*
cd php-*/
```
Luego, ejecute el script `configure` con las opciones necesarias para su plataforma.
Las siguientes banderas de `./configure` son obligatorias, pero puede agregar otras, por ejemplo, para compilar extensiones o características adicionales.
#### Linux
```console
./configure \
--enable-embed \
--enable-zts \
--disable-zend-signals \
--enable-zend-max-execution-timers
```
#### Mac
Use el gestor de paquetes [Homebrew](https://brew.sh/) para instalar las dependencias requeridas y opcionales:
```console
brew install libiconv bison brotli re2c pkg-config watcher
echo 'export PATH="/opt/homebrew/opt/bison/bin:$PATH"' >> ~/.zshrc
```
Luego ejecute el script de configuración:
```console
./configure \
--enable-embed \
--enable-zts \
--disable-zend-signals \
--with-iconv=/opt/homebrew/opt/libiconv/
```
#### Compilar PHP
Finalmente, compile e instale PHP:
```console
make -j"$(getconf _NPROCESSORS_ONLN)"
sudo make install
```
## Instalar dependencias opcionales
Algunas características de FrankenPHP dependen de dependencias opcionales del sistema que deben instalarse.
Alternativamente, estas características pueden deshabilitarse pasando etiquetas de compilación al compilador Go.
| Característica | Dependencia | Etiqueta de compilación para deshabilitarla |
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------- | -------------------------------------------- |
| Compresión Brotli | [Brotli](https://github.com/google/brotli) | nobrotli |
| Reiniciar workers al cambiar archivos | [Watcher C](https://github.com/e-dant/watcher/tree/release/watcher-c) | nowatcher |
| [Mercure](mercure.md) | [Biblioteca Mercure Go](https://pkg.go.dev/github.com/dunglas/mercure) (instalada automáticamente, licencia AGPL) | nomercure |
## Compilar la aplicación Go
Ahora puede construir el binario final.
### Usando xcaddy
La forma recomendada es usar [xcaddy](https://github.com/caddyserver/xcaddy) para compilar FrankenPHP.
`xcaddy` también permite agregar fácilmente [módulos personalizados de Caddy](https://caddyserver.com/docs/modules/) y extensiones de FrankenPHP:
```console
CGO_ENABLED=1 \
XCADDY_GO_BUILD_FLAGS="-ldflags='-w -s' -tags=nobadger,nomysql,nopgx" \
CGO_CFLAGS=$(php-config --includes) \
CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)" \
xcaddy build \
--output frankenphp \
--with github.com/dunglas/frankenphp/caddy \
--with github.com/dunglas/mercure/caddy \
--with github.com/dunglas/vulcain/caddy \
--with github.com/dunglas/caddy-cbrotli
# Agregue módulos adicionales de Caddy y extensiones de FrankenPHP aquí
# opcionalmente, si desea compilar desde sus fuentes de frankenphp:
# --with github.com/dunglas/frankenphp=$(pwd) \
# --with github.com/dunglas/frankenphp/caddy=$(pwd)/caddy
```
> [!TIP]
>
> Si está usando musl libc (predeterminado en Alpine Linux) y Symfony,
> es posible que deba aumentar el tamaño de pila predeterminado.
> De lo contrario, podría obtener errores como `PHP Fatal error: Maximum call stack size of 83360 bytes reached during compilation. Try splitting expression`
>
> Para hacerlo, cambie la variable de entorno `XCADDY_GO_BUILD_FLAGS` a algo como:
> `XCADDY_GO_BUILD_FLAGS=$'-ldflags "-w -s -extldflags \'-Wl,-z,stack-size=0x80000\'"'`
> (cambie el valor del tamaño de pila según las necesidades de su aplicación).
### Sin xcaddy
Alternativamente, es posible compilar FrankenPHP sin `xcaddy` usando directamente el comando `go`:
```console
curl -L https://github.com/php/frankenphp/archive/refs/heads/main.tar.gz | tar xz
cd frankenphp-main/caddy/frankenphp
CGO_CFLAGS=$(php-config --includes) CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)" go build -tags=nobadger,nomysql,nopgx
```

349
docs/es/config.md Normal file
View File

@@ -0,0 +1,349 @@
# Configuración
FrankenPHP, Caddy así como los módulos [Mercure](mercure.md) y [Vulcain](https://vulcain.rocks) pueden configurarse usando [los formatos soportados por Caddy](https://caddyserver.com/docs/getting-started#your-first-config).
El formato más común es el `Caddyfile`, que es un formato de texto simple y legible.
Por defecto, FrankenPHP buscará un `Caddyfile` en el directorio actual.
Puede especificar una ruta personalizada con la opción `-c` o `--config`.
Un `Caddyfile` mínimo para servir una aplicación PHP se muestra a continuación:
```caddyfile
# El nombre de host al que responder
localhost
# Opcionalmente, el directorio desde el que servir archivos, por defecto es el directorio actual
#root public/
php_server
```
Un `Caddyfile` más avanzado que habilita más características y proporciona variables de entorno convenientes está disponible [en el repositorio de FrankenPHP](https://github.com/php/frankenphp/blob/main/caddy/frankenphp/Caddyfile),
y con las imágenes de Docker.
PHP en sí puede configurarse [usando un archivo `php.ini`](https://www.php.net/manual/es/configuration.file.php).
Dependiendo de su método de instalación, FrankenPHP y el intérprete de PHP buscarán archivos de configuración en las ubicaciones descritas a continuación.
## Docker
FrankenPHP:
- `/etc/frankenphp/Caddyfile`: el archivo de configuración principal
- `/etc/frankenphp/Caddyfile.d/*.caddyfile`: archivos de configuración adicionales que se cargan automáticamente
PHP:
- `php.ini`: `/usr/local/etc/php/php.ini` (no se proporciona ningún `php.ini` por defecto)
- archivos de configuración adicionales: `/usr/local/etc/php/conf.d/*.ini`
- extensiones de PHP: `/usr/local/lib/php/extensions/no-debug-zts-<YYYYMMDD>/`
- Debe copiar una plantilla oficial proporcionada por el proyecto PHP:
```dockerfile
FROM dunglas/frankenphp
# Producción:
RUN cp $PHP_INI_DIR/php.ini-production $PHP_INI_DIR/php.ini
# O desarrollo:
RUN cp $PHP_INI_DIR/php.ini-development $PHP_INI_DIR/php.ini
```
## Paquetes RPM y Debian
FrankenPHP:
- `/etc/frankenphp/Caddyfile`: el archivo de configuración principal
- `/etc/frankenphp/Caddyfile.d/*.caddyfile`: archivos de configuración adicionales que se cargan automáticamente
PHP:
- `php.ini`: `/etc/php-zts/php.ini` (se proporciona un archivo `php.ini` con ajustes de producción por defecto)
- archivos de configuración adicionales: `/etc/php-zts/conf.d/*.ini`
## Binario estático
FrankenPHP:
- En el directorio de trabajo actual: `Caddyfile`
PHP:
- `php.ini`: El directorio en el que se ejecuta `frankenphp run` o `frankenphp php-server`, luego `/etc/frankenphp/php.ini`
- archivos de configuración adicionales: `/etc/frankenphp/php.d/*.ini`
- extensiones de PHP: no pueden cargarse, debe incluirlas en el binario mismo
- copie uno de `php.ini-production` o `php.ini-development` proporcionados [en las fuentes de PHP](https://github.com/php/php-src/).
## Configuración de Caddyfile
Las directivas `php_server` o `php` [de HTTP](https://caddyserver.com/docs/caddyfile/concepts#directives) pueden usarse dentro de los bloques de sitio para servir su aplicación PHP.
Ejemplo mínimo:
```caddyfile
localhost {
# Habilitar compresión (opcional)
encode zstd br gzip
# Ejecutar archivos PHP en el directorio actual y servir activos
php_server
}
```
También puede configurar explícitamente FrankenPHP usando la [opción global](https://caddyserver.com/docs/caddyfile/concepts#global-options) `frankenphp`:
```caddyfile
{
frankenphp {
num_threads <num_threads> # Establece el número de hilos de PHP para iniciar. Por defecto: 2x el número de CPUs disponibles.
max_threads <num_threads> # Limita el número de hilos de PHP adicionales que pueden iniciarse en tiempo de ejecución. Por defecto: num_threads. Puede establecerse como 'auto'.
max_wait_time <duration> # Establece el tiempo máximo que una solicitud puede esperar por un hilo de PHP libre antes de agotar el tiempo de espera. Por defecto: deshabilitado.
php_ini <key> <value> # Establece una directiva php.ini. Puede usarse varias veces para establecer múltiples directivas.
worker {
file <path> # Establece la ruta al script del worker.
num <num> # Establece el número de hilos de PHP para iniciar, por defecto es 2x el número de CPUs disponibles.
env <key> <value> # Establece una variable de entorno adicional con el valor dado. Puede especificarse más de una vez para múltiples variables de entorno.
watch <path> # Establece la ruta para observar cambios en archivos. Puede especificarse más de una vez para múltiples rutas.
name <name> # Establece el nombre del worker, usado en logs y métricas. Por defecto: ruta absoluta del archivo del worker
max_consecutive_failures <num> # Establece el número máximo de fallos consecutivos antes de que el worker se considere no saludable, -1 significa que el worker siempre se reiniciará. Por defecto: 6.
}
}
}
# ...
```
Alternativamente, puede usar la forma corta de una línea de la opción `worker`:
```caddyfile
{
frankenphp {
worker <file> <num>
}
}
# ...
```
También puede definir múltiples workers si sirve múltiples aplicaciones en el mismo servidor:
```caddyfile
app.example.com {
root /path/to/app/public
php_server {
root /path/to/app/public # permite un mejor almacenamiento en caché
worker index.php <num>
}
}
other.example.com {
root /path/to/other/public
php_server {
root /path/to/other/public
worker index.php <num>
}
}
# ...
```
Usar la directiva `php_server` es generalmente lo que necesita,
pero si necesita un control total, puede usar la directiva de bajo nivel `php`.
La directiva `php` pasa toda la entrada a PHP, en lugar de verificar primero si
es un archivo PHP o no. Lea más sobre esto en la [página de rendimiento](performance.md#try_files).
Usar la directiva `php_server` es equivalente a esta configuración:
```caddyfile
route {
# Agrega barra final para solicitudes de directorio
@canonicalPath {
file {path}/index.php
not path */
}
redir @canonicalPath {path}/ 308
# Si el archivo solicitado no existe, intenta archivos índice
@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
}
```
Las directivas `php_server` y `php` tienen las siguientes opciones:
```caddyfile
php_server [<matcher>] {
root <directory> # Establece la carpeta raíz del sitio. Por defecto: directiva `root`.
split_path <delim...> # Establece las subcadenas para dividir la URI en dos partes. La primera subcadena coincidente se usará para dividir la "información de ruta" del path. La primera parte se sufija con la subcadena coincidente y se asumirá como el nombre del recurso real (script CGI). La segunda parte se establecerá como PATH_INFO para que el script la use. Por defecto: `.php`
resolve_root_symlink false # Desactiva la resolución del directorio `root` a su valor real evaluando un enlace simbólico, si existe (habilitado por defecto).
env <key> <value> # Establece una variable de entorno adicional con el valor dado. Puede especificarse más de una vez para múltiples variables de entorno.
file_server off # Desactiva la directiva incorporada file_server.
worker { # Crea un worker específico para este servidor. Puede especificarse más de una vez para múltiples workers.
file <path> # Establece la ruta al script del worker, puede ser relativa a la raíz de php_server
num <num> # Establece el número de hilos de PHP para iniciar, por defecto es 2x el número de CPUs disponibles.
name <name> # Establece el nombre para el worker, usado en logs y métricas. Por defecto: ruta absoluta del archivo del worker. Siempre comienza con m# cuando se define en un bloque php_server.
watch <path> # Establece la ruta para observar cambios en archivos. Puede especificarse más de una vez para múltiples rutas.
env <key> <value> # Establece una variable de entorno adicional con el valor dado. Puede especificarse más de una vez para múltiples variables de entorno. Las variables de entorno para este worker también se heredan del php_server padre, pero pueden sobrescribirse aquí.
match <path> # hace coincidir el worker con un patrón de ruta. Anula try_files y solo puede usarse en la directiva php_server.
}
worker <other_file> <num> # También puede usar la forma corta como en el bloque global frankenphp.
}
```
### Observando cambios en archivos
Dado que los workers solo inician su aplicación una vez y la mantienen en memoria, cualquier cambio
en sus archivos PHP no se reflejará inmediatamente.
Los workers pueden reiniciarse al cambiar archivos mediante la directiva `watch`.
Esto es útil para entornos de desarrollo.
```caddyfile
{
frankenphp {
worker {
file /path/to/app/public/worker.php
watch
}
}
}
```
Esta función se utiliza frecuentemente en combinación con [hot reload](hot-reload.md).
Si el directorio `watch` no está especificado, retrocederá a `./**/*.{env,php,twig,yaml,yml}`,
lo cual vigila todos los archivos `.env`, `.php`, `.twig`, `.yaml` y `.yml` en el directorio y subdirectorios
donde se inició el proceso de FrankenPHP. También puede especificar uno o más directorios mediante un
[patrón de nombres de ficheros de shell](https://pkg.go.dev/path/filepath#Match):
```caddyfile
{
frankenphp {
worker {
file /path/to/app/public/worker.php
watch /path/to/app # observa todos los archivos en todos los subdirectorios de /path/to/app
watch /path/to/app/*.php # observa archivos que terminan en .php en /path/to/app
watch /path/to/app/**/*.php # observa archivos PHP en /path/to/app y subdirectorios
watch /path/to/app/**/*.{php,twig} # observa archivos PHP y Twig en /path/to/app y subdirectorios
}
}
}
```
- El patrón `**` significa observación recursiva
- Los directorios también pueden ser relativos (al lugar donde se inicia el proceso de FrankenPHP)
- Si tiene múltiples workers definidos, todos ellos se reiniciarán cuando cambie un archivo
- Tenga cuidado al observar archivos que se crean en tiempo de ejecución (como logs) ya que podrían causar reinicios no deseados de workers.
El observador de archivos se basa en [e-dant/watcher](https://github.com/e-dant/watcher).
## Coincidencia del worker con una ruta
En aplicaciones PHP tradicionales, los scripts siempre se colocan en el directorio público.
Esto también es cierto para los scripts de workers, que se tratan como cualquier otro script PHP.
Si desea colocar el script del worker fuera del directorio público, puede hacerlo mediante la directiva `match`.
La directiva `match` es una alternativa optimizada a `try_files` solo disponible dentro de `php_server` y `php`.
El siguiente ejemplo siempre servirá un archivo en el directorio público si está presente
y de lo contrario reenviará la solicitud al worker que coincida con el patrón de ruta.
```caddyfile
{
frankenphp {
php_server {
worker {
file /path/to/worker.php # el archivo puede estar fuera de la ruta pública
match /api/* # todas las solicitudes que comiencen con /api/ serán manejadas por este worker
}
}
}
}
```
## Variables de entorno
Las siguientes variables de entorno pueden usarse para inyectar directivas de Caddy en el `Caddyfile` sin modificarlo:
- `SERVER_NAME`: cambia [las direcciones en las que escuchar](https://caddyserver.com/docs/caddyfile/concepts#addresses), los nombres de host proporcionados también se usarán para el certificado TLS generado
- `SERVER_ROOT`: cambia el directorio raíz del sitio, por defecto es `public/`
- `CADDY_GLOBAL_OPTIONS`: inyecta [opciones globales](https://caddyserver.com/docs/caddyfile/options)
- `FRANKENPHP_CONFIG`: inyecta configuración bajo la directiva `frankenphp`
Al igual que en FPM y SAPIs CLI, las variables de entorno se exponen por defecto en la superglobal `$_SERVER`.
El valor `S` de [la directiva `variables_order` de PHP](https://www.php.net/manual/en/ini.core.php#ini.variables-order) siempre es equivalente a `ES` independientemente de la ubicación de `E` en otro lugar de esta directiva.
## Configuración de PHP
Para cargar [archivos de configuración adicionales de PHP](https://www.php.net/manual/en/configuration.file.php#configuration.file.scan),
puede usarse la variable de entorno `PHP_INI_SCAN_DIR`.
Cuando se establece, PHP cargará todos los archivos con la extensión `.ini` presentes en los directorios dados.
También puede cambiar la configuración de PHP usando la directiva `php_ini` en el `Caddyfile`:
```caddyfile
{
frankenphp {
php_ini memory_limit 256M
# o
php_ini {
memory_limit 256M
max_execution_time 15
}
}
}
```
### Deshabilitar HTTPS
Por defecto, FrankenPHP habilitará automáticamente HTTPS para todos los nombres de host, incluyendo `localhost`.
Si desea deshabilitar HTTPS (por ejemplo en un entorno de desarrollo), puede establecer la variable de entorno `SERVER_NAME` a `http://` o `:80`:
Alternativamente, puede usar todos los otros métodos descritos en la [documentación de Caddy](https://caddyserver.com/docs/automatic-https#activation).
Si desea usar HTTPS con la dirección IP `127.0.0.1` en lugar del nombre de host `localhost`, lea la sección de [problemas conocidos](known-issues.md#using-https127001-with-docker).
### Dúplex completo (HTTP/1)
Al usar HTTP/1.x, puede ser deseable habilitar el modo dúplex completo para permitir escribir una respuesta antes de que se haya leído todo el cuerpo
(por ejemplo: [Mercure](mercure.md), WebSocket, Eventos enviados por el servidor, etc.).
Esta es una configuración opcional que debe agregarse a las opciones globales en el `Caddyfile`:
```caddyfile
{
servers {
enable_full_duplex
}
}
```
> [!CAUTION]
>
> Habilitar esta opción puede causar que clientes HTTP/1.x antiguos que no soportan dúplex completo se bloqueen.
> Esto también puede configurarse usando la configuración de entorno `CADDY_GLOBAL_OPTIONS`:
```sh
CADDY_GLOBAL_OPTIONS="servers {
enable_full_duplex
}"
```
Puede encontrar más información sobre esta configuración en la [documentación de Caddy](https://caddyserver.com/docs/caddyfile/options#enable-full-duplex).
## Habilitar el modo de depuración
Al usar la imagen de Docker, establezca la variable de entorno `CADDY_GLOBAL_OPTIONS` a `debug` para habilitar el modo de depuración:
```console
docker run -v $PWD:/app/public \
-e CADDY_GLOBAL_OPTIONS=debug \
-p 80:80 -p 443:443 -p 443:443/udp \
dunglas/frankenphp
```

283
docs/es/docker.md Normal file
View File

@@ -0,0 +1,283 @@
# Construir una imagen Docker personalizada
Las [imágenes Docker de FrankenPHP](https://hub.docker.com/r/dunglas/frankenphp) están basadas en [imágenes oficiales de PHP](https://hub.docker.com/_/php/).
Se proporcionan variantes para Debian y Alpine Linux en arquitecturas populares.
Se recomiendan las variantes de Debian.
Se proporcionan variantes para PHP 8.2, 8.3, 8.4 y 8.5.
Las etiquetas siguen este patrón: `dunglas/frankenphp:<versión-frankenphp>-php<versión-php>-<sistema-operativo>`
- `<versión-frankenphp>` y `<versión-php>` son los números de versión de FrankenPHP y PHP respectivamente, que van desde versiones principales (ej. `1`), menores (ej. `1.2`) hasta versiones de parche (ej. `1.2.3`).
- `<sistema-operativo>` es `trixie` (para Debian Trixie), `bookworm` (para Debian Bookworm) o `alpine` (para la última versión estable de Alpine).
[Explorar etiquetas](https://hub.docker.com/r/dunglas/frankenphp/tags).
## Cómo usar las imágenes
Cree un archivo `Dockerfile` en su proyecto:
```dockerfile
FROM dunglas/frankenphp
COPY . /app/public
```
Luego, ejecute estos comandos para construir y ejecutar la imagen Docker:
```console
docker build -t my-php-app .
docker run -it --rm --name my-running-app my-php-app
```
## Cómo ajustar la configuración
Para mayor comodidad, se proporciona en la imagen un [archivo `Caddyfile` predeterminado](https://github.com/php/frankenphp/blob/main/caddy/frankenphp/Caddyfile) que contiene variables de entorno útiles.
## Cómo instalar más extensiones de PHP
El script [`docker-php-extension-installer`](https://github.com/mlocati/docker-php-extension-installer) está disponible en la imagen base.
Agregar extensiones adicionales de PHP es sencillo:
```dockerfile
FROM dunglas/frankenphp
# agregue extensiones adicionales aquí:
RUN install-php-extensions \
pdo_mysql \
gd \
intl \
zip \
opcache
```
## Cómo instalar más módulos de Caddy
FrankenPHP está construido sobre Caddy, y todos los [módulos de Caddy](https://caddyserver.com/docs/modules/) pueden usarse con FrankenPHP.
La forma más fácil de instalar módulos personalizados de Caddy es usar [xcaddy](https://github.com/caddyserver/xcaddy):
```dockerfile
FROM dunglas/frankenphp:builder AS builder
# Copie xcaddy en la imagen del constructor
COPY --from=caddy:builder /usr/bin/xcaddy /usr/bin/xcaddy
# CGO debe estar habilitado para construir FrankenPHP
RUN CGO_ENABLED=1 \
XCADDY_SETCAP=1 \
XCADDY_GO_BUILD_FLAGS="-ldflags='-w -s' -tags=nobadger,nomysql,nopgx" \
CGO_CFLAGS=$(php-config --includes) \
CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)" \
xcaddy build \
--output /usr/local/bin/frankenphp \
--with github.com/dunglas/frankenphp=./ \
--with github.com/dunglas/frankenphp/caddy=./caddy/ \
--with github.com/dunglas/caddy-cbrotli \
# Mercure y Vulcain están incluidos en la compilación oficial, pero puede eliminarlos si lo desea
--with github.com/dunglas/mercure/caddy \
--with github.com/dunglas/vulcain/caddy
# Agregue módulos adicionales de Caddy aquí
FROM dunglas/frankenphp AS runner
# Reemplace el binario oficial por el que contiene sus módulos personalizados
COPY --from=builder /usr/local/bin/frankenphp /usr/local/bin/frankenphp
```
La imagen `builder` proporcionada por FrankenPHP contiene una versión compilada de `libphp`.
Se proporcionan [imágenes de constructor](https://hub.docker.com/r/dunglas/frankenphp/tags?name=builder) para todas las versiones de FrankenPHP y PHP, tanto para Debian como para Alpine.
> [!TIP]
>
> Si está usando Alpine Linux y Symfony,
> es posible que deba [aumentar el tamaño de pila predeterminado](compile.md#using-xcaddy).
## Habilitar el modo Worker por defecto
Establezca la variable de entorno `FRANKENPHP_CONFIG` para iniciar FrankenPHP con un script de worker:
```dockerfile
FROM dunglas/frankenphp
# ...
ENV FRANKENPHP_CONFIG="worker ./public/index.php"
```
## Usar un volumen en desarrollo
Para desarrollar fácilmente con FrankenPHP, monte el directorio de su host que contiene el código fuente de la aplicación como un volumen en el contenedor Docker:
```console
docker run -v $PWD:/app/public -p 80:80 -p 443:443 -p 443:443/udp --tty my-php-app
```
> [!TIP]
>
> La opción `--tty` permite tener logs legibles en lugar de logs en formato JSON.
Con Docker Compose:
```yaml
# compose.yaml
services:
php:
image: dunglas/frankenphp
# descomente la siguiente línea si desea usar un Dockerfile personalizado
#build: .
# descomente la siguiente línea si desea ejecutar esto en un entorno de producción
# restart: always
ports:
- "80:80" # HTTP
- "443:443" # HTTPS
- "443:443/udp" # HTTP/3
volumes:
- ./:/app/public
- caddy_data:/data
- caddy_config:/config
# comente la siguiente línea en producción, permite tener logs legibles en desarrollo
tty: true
# Volúmenes necesarios para los certificados y configuración de Caddy
volumes:
caddy_data:
caddy_config:
```
## Ejecutar como usuario no root
FrankenPHP puede ejecutarse como usuario no root en Docker.
Aquí hay un ejemplo de `Dockerfile` que hace esto:
```dockerfile
FROM dunglas/frankenphp
ARG USER=appuser
RUN \
# Use "adduser -D ${USER}" para distribuciones basadas en alpine
useradd ${USER}; \
# Agregar capacidad adicional para enlazar a los puertos 80 y 443
setcap CAP_NET_BIND_SERVICE=+eip /usr/local/bin/frankenphp; \
# Dar acceso de escritura a /config/caddy y /data/caddy
chown -R ${USER}:${USER} /config/caddy /data/caddy
USER ${USER}
```
### Ejecutar sin capacidades
Incluso cuando se ejecuta sin root, FrankenPHP necesita la capacidad `CAP_NET_BIND_SERVICE` para enlazar el servidor web en puertos privilegiados (80 y 443).
Si expone FrankenPHP en un puerto no privilegiado (1024 y superior), es posible ejecutar el servidor web como usuario no root, y sin necesidad de ninguna capacidad:
```dockerfile
FROM dunglas/frankenphp
ARG USER=appuser
RUN \
# Use "adduser -D ${USER}" para distribuciones basadas en alpine
useradd ${USER}; \
# Eliminar la capacidad predeterminada
setcap -r /usr/local/bin/frankenphp; \
# Dar acceso de escritura a /config/caddy y /data/caddy
chown -R ${USER}:${USER} /config/caddy /data/caddy
USER ${USER}
```
Luego, establezca la variable de entorno `SERVER_NAME` para usar un puerto no privilegiado.
Ejemplo: `:8000`
## Actualizaciones
Las imágenes Docker se construyen:
- Cuando se etiqueta una nueva versión
- Diariamente a las 4 am UTC, si hay nuevas versiones de las imágenes oficiales de PHP disponibles
## Endurecimiento de Imágenes
Para reducir aún más la superficie de ataque y el tamaño de tus imágenes Docker de FrankenPHP, también es posible construirlas sobre una imagen
[Google distroless](https://github.com/GoogleContainerTools/distroless) o
[Docker hardened](https://www.docker.com/products/hardened-images).
> [!WARNING]
> Estas imágenes base mínimas no incluyen un shell ni gestor de paquetes, lo que hace que la depuración sea más difícil.
> Por lo tanto, se recomiendan solo para producción si la seguridad es una alta prioridad.
Cuando agregues extensiones PHP adicionales, necesitarás una etapa de construcción intermedia:
```dockerfile
FROM dunglas/frankenphp AS builder
# Agregar extensiones PHP adicionales aquí
RUN install-php-extensions pdo_mysql pdo_pgsql #...
# Copiar bibliotecas compartidas de frankenphp y todas las extensiones instaladas a una ubicación temporal
# También puedes hacer este paso manualmente analizando la salida de ldd del binario frankenphp y cada archivo .so de extensión
RUN apt-get update && apt-get install -y libtree && \
EXT_DIR="$(php -r 'echo ini_get("extension_dir");')" && \
FRANKENPHP_BIN="$(which frankenphp)"; \
LIBS_TMP_DIR="/tmp/libs"; \
mkdir -p "$LIBS_TMP_DIR"; \
for target in "$FRANKENPHP_BIN" $(find "$EXT_DIR" -maxdepth 2 -type f -name "*.so"); do \
libtree -pv "$target" | sed 's/.*── \(.*\) \[.*/\1/' | grep -v "^$target" | while IFS= read -r lib; do \
[ -z "$lib" ] && continue; \
base=$(basename "$lib"); \
destfile="$LIBS_TMP_DIR/$base"; \
if [ ! -f "$destfile" ]; then \
cp "$lib" "$destfile"; \
fi; \
done; \
done
# Imagen base distroless de Debian, asegúrate de que sea la misma versión de Debian que la imagen base
FROM gcr.io/distroless/base-debian13
# Alternativa de imagen endurecida de Docker
# FROM dhi.io/debian:13
# Ubicación de tu aplicación y Caddyfile que se copiará al contenedor
ARG PATH_TO_APP="."
ARG PATH_TO_CADDYFILE="./Caddyfile"
# Copiar tu aplicación en /app
# Para mayor endurecimiento asegúrate de que solo las rutas escribibles sean propiedad del usuario nonroot
COPY --chown=nonroot:nonroot "$PATH_TO_APP" /app
COPY "$PATH_TO_CADDYFILE" /etc/caddy/Caddyfile
# Copiar frankenphp y bibliotecas necesarias
COPY --from=builder /usr/local/bin/frankenphp /usr/local/bin/frankenphp
COPY --from=builder /usr/local/lib/php/extensions /usr/local/lib/php/extensions
COPY --from=builder /tmp/libs /usr/lib
# Copiar archivos de configuración php.ini
COPY --from=builder /usr/local/etc/php/conf.d /usr/local/etc/php/conf.d
COPY --from=builder /usr/local/etc/php/php.ini-production /usr/local/etc/php/php.ini
# Directorios de datos de Caddy — deben ser escribibles para nonroot, incluso en un sistema de archivos raíz de solo lectura
ENV XDG_CONFIG_HOME=/config \
XDG_DATA_HOME=/data
COPY --from=builder --chown=nonroot:nonroot /data/caddy /data/caddy
COPY --from=builder --chown=nonroot:nonroot /config/caddy /config/caddy
USER nonroot
WORKDIR /app
# punto de entrada para ejecutar frankenphp con el Caddyfile proporcionado
ENTRYPOINT ["/usr/local/bin/frankenphp", "run", "-c", "/etc/caddy/Caddyfile"]
```
## Versiones de desarrollo
Las versiones de desarrollo están disponibles en el [repositorio Docker `dunglas/frankenphp-dev`](https://hub.docker.com/repository/docker/dunglas/frankenphp-dev).
Se activa una nueva compilación cada vez que se envía un commit a la rama principal del repositorio de GitHub.
Las etiquetas `latest*` apuntan a la cabeza de la rama `main`.
También están disponibles etiquetas de la forma `sha-<hash-del-commit-git>`.

21
docs/es/early-hints.md Normal file
View File

@@ -0,0 +1,21 @@
# Early Hints (Pistas Tempranas)
FrankenPHP soporta nativamente el [código de estado 103 Early Hints](https://developer.chrome.com/blog/early-hints/).
El uso de Early Hints puede mejorar el tiempo de carga de sus páginas web hasta en un 30%.
```php
<?php
header('Link: </style.css>; rel=preload; as=style');
headers_send(103);
// sus algoritmos lentos y consultas SQL 🤪
echo <<<'HTML'
<!DOCTYPE html>
<title>Hola FrankenPHP</title>
<link rel="stylesheet" href="style.css">
HTML;
```
Early Hints están soportados tanto en el modo normal como en el modo [worker](worker.md).

144
docs/es/embed.md Normal file
View File

@@ -0,0 +1,144 @@
# Aplicaciones PHP como Binarios Autónomos
FrankenPHP tiene la capacidad de incrustar el código fuente y los activos de aplicaciones PHP en un binario estático y autónomo.
Gracias a esta característica, las aplicaciones PHP pueden distribuirse como binarios autónomos que incluyen la aplicación en sí, el intérprete de PHP y Caddy, un servidor web de nivel de producción.
Obtenga más información sobre esta característica [en la presentación realizada por Kévin en SymfonyCon 2023](https://dunglas.dev/2023/12/php-and-symfony-apps-as-standalone-binaries/).
Para incrustar aplicaciones Laravel, [lea esta entrada específica de documentación](laravel.md#laravel-apps-as-standalone-binaries).
## Preparando su Aplicación
Antes de crear el binario autónomo, asegúrese de que su aplicación esté lista para ser incrustada.
Por ejemplo, probablemente querrá:
- Instalar las dependencias de producción de la aplicación
- Volcar el autoload
- Activar el modo de producción de su aplicación (si lo hay)
- Eliminar archivos innecesarios como `.git` o pruebas para reducir el tamaño de su binario final
Por ejemplo, para una aplicación Symfony, puede usar los siguientes comandos:
```console
# Exportar el proyecto para deshacerse de .git/, etc.
mkdir $TMPDIR/my-prepared-app
git archive HEAD | tar -x -C $TMPDIR/my-prepared-app
cd $TMPDIR/my-prepared-app
# Establecer las variables de entorno adecuadas
echo APP_ENV=prod > .env.local
echo APP_DEBUG=0 >> .env.local
# Eliminar las pruebas y otros archivos innecesarios para ahorrar espacio
# Alternativamente, agregue estos archivos con el atributo export-ignore en su archivo .gitattributes
rm -Rf tests/
# Instalar las dependencias
composer install --ignore-platform-reqs --no-dev -a
# Optimizar .env
composer dump-env prod
```
### Personalizar la Configuración
Para personalizar [la configuración](config.md), puede colocar un archivo `Caddyfile` así como un archivo `php.ini`
en el directorio principal de la aplicación a incrustar (`$TMPDIR/my-prepared-app` en el ejemplo anterior).
## Crear un Binario para Linux
La forma más fácil de crear un binario para Linux es usar el constructor basado en Docker que proporcionamos.
1. Cree un archivo llamado `static-build.Dockerfile` en el repositorio de su aplicación:
```dockerfile
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder-gnu
# Si tiene la intención de ejecutar el binario en sistemas musl-libc, use static-builder-musl en su lugar
# Copie su aplicación
WORKDIR /go/src/app/dist/app
COPY . .
# Construya el binario estático
WORKDIR /go/src/app/
RUN EMBED=dist/app/ ./build-static.sh
```
> [!CAUTION]
>
> Algunos archivos `.dockerignore` (por ejemplo, el [`.dockerignore` predeterminado de Symfony Docker](https://github.com/dunglas/symfony-docker/blob/main/.dockerignore))
> ignorarán el directorio `vendor/` y los archivos `.env`. Asegúrese de ajustar o eliminar el archivo `.dockerignore` antes de la construcción.
2. Construya:
```console
docker build -t static-app -f static-build.Dockerfile .
```
3. Extraiga el binario:
```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
```
El binario resultante es el archivo llamado `my-app` en el directorio actual.
## Crear un Binario para Otros Sistemas Operativos
Si no desea usar Docker o desea construir un binario para macOS, use el script de shell que proporcionamos:
```console
git clone https://github.com/php/frankenphp
cd frankenphp
EMBED=/path/to/your/app ./build-static.sh
```
El binario resultante es el archivo llamado `frankenphp-<os>-<arch>` en el directorio `dist/`.
## Usar el Binario
¡Listo! El archivo `my-app` (o `dist/frankenphp-<os>-<arch>` en otros sistemas operativos) contiene su aplicación autónoma.
Para iniciar la aplicación web, ejecute:
```console
./my-app php-server
```
Si su aplicación contiene un [script worker](worker.md), inicie el worker con algo como:
```console
./my-app php-server --worker public/index.php
```
Para habilitar HTTPS (se crea automáticamente un certificado de Let's Encrypt), HTTP/2 y HTTP/3, especifique el nombre de dominio a usar:
```console
./my-app php-server --domain localhost
```
También puede ejecutar los scripts CLI de PHP incrustados en su binario:
```console
./my-app php-cli bin/console
```
## Extensiones de PHP
Por defecto, el script construirá las extensiones requeridas por el archivo `composer.json` de su proyecto, si existe.
Si el archivo `composer.json` no existe, se construirán las extensiones predeterminadas, como se documenta en [la entrada de compilaciones estáticas](static.md).
Para personalizar las extensiones, use la variable de entorno `PHP_EXTENSIONS`.
## Personalizar la Compilación
[Lea la documentación de compilación estática](static.md) para ver cómo personalizar el binario (extensiones, versión de PHP, etc.).
## Distribuir el Binario
En Linux, el binario creado se comprime usando [UPX](https://upx.github.io).
En Mac, para reducir el tamaño del archivo antes de enviarlo, puede comprimirlo.
Recomendamos `xz`.

View File

@@ -0,0 +1,172 @@
# Extension Workers
Los Extension Workers permiten que tu [extensión FrankenPHP](https://frankenphp.dev/docs/extensions/) gestione un pool dedicado de hilos PHP para ejecutar tareas en segundo plano, manejar eventos asíncronos o implementar protocolos personalizados. Útil para sistemas de colas, listeners de eventos, programadores, etc.
## Registrando el Worker
### Registro Estático
Si no necesitas hacer que el worker sea configurable por el usuario (ruta de script fija, número fijo de hilos), simplemente puedes registrar el worker en la función `init()`.
```go
package myextension
import (
"github.com/dunglas/frankenphp"
"github.com/dunglas/frankenphp/caddy"
)
// Manejador global para comunicarse con el pool de workers
var worker frankenphp.Workers
func init() {
// Registrar el worker cuando se carga el módulo.
worker = caddy.RegisterWorkers(
"my-internal-worker", // Nombre único
"worker.php", // Ruta del script (relativa a la ejecución o absoluta)
2, // Número fijo de hilos
// Hooks de ciclo de vida opcionales
frankenphp.WithWorkerOnServerStartup(func() {
// Lógica de configuración global...
}),
)
}
```
### En un Módulo Caddy (Configurable por el usuario)
Si planeas compartir tu extensión (como una cola genérica o listener de eventos), deberías envolverla en un módulo Caddy. Esto permite a los usuarios configurar la ruta del script y el número de hilos a través de su `Caddyfile`. Esto requiere implementar la interfaz `caddy.Provisioner` y analizar el Caddyfile ([ver un ejemplo](https://github.com/dunglas/frankenphp-queue/blob/989120d394d66dd6c8e2101cac73dd622fade334/caddy.go)).
### En una Aplicación Go Pura (Embedding)
Si estás [embebiendo FrankenPHP en una aplicación Go estándar sin caddy](https://pkg.go.dev/github.com/dunglas/frankenphp#example-ServeHTTP), puedes registrar extension workers usando `frankenphp.WithExtensionWorkers` al inicializar las opciones.
## Interactuando con Workers
Una vez que el pool de workers está activo, puedes enviar tareas a él. Esto se puede hacer dentro de [funciones nativas exportadas a PHP](https://frankenphp.dev/docs/extensions/#writing-the-extension), o desde cualquier lógica Go como un programador cron, un listener de eventos (MQTT, Kafka), o cualquier otra goroutine.
### Modo Sin Cabeza: `SendMessage`
Usa `SendMessage` para pasar datos sin procesar directamente a tu script worker. Esto es ideal para colas o comandos simples.
#### Ejemplo: Una Extensión de Cola Asíncrona
```go
// #include <Zend/zend_types.h>
import "C"
import (
"context"
"unsafe"
"github.com/dunglas/frankenphp"
)
//export_php:function my_queue_push(mixed $data): bool
func my_queue_push(data *C.zval) bool {
// 1. Asegurar que el worker esté listo
if worker == nil {
return false
}
// 2. Enviar al worker en segundo plano
_, err := worker.SendMessage(
context.Background(), // Contexto estándar de Go
unsafe.Pointer(data), // Datos para pasar al worker
nil, // http.ResponseWriter opcional
)
return err == nil
}
```
### Emulación HTTP: `SendRequest`
Usa `SendRequest` si tu extensión necesita invocar un script PHP que espera un entorno web estándar (poblando `$_SERVER`, `$_GET`, etc.).
```go
// #include <Zend/zend_types.h>
import "C"
import (
"net/http"
"net/http/httptest"
"unsafe"
"github.com/dunglas/frankenphp"
)
//export_php:function my_worker_http_request(string $path): string
func my_worker_http_request(path *C.zend_string) unsafe.Pointer {
// 1. Preparar la solicitud y el grabador
url := frankenphp.GoString(unsafe.Pointer(path))
req, _ := http.NewRequest("GET", url, http.NoBody)
rr := httptest.NewRecorder()
// 2. Enviar al worker
if err := worker.SendRequest(rr, req); err != nil {
return nil
}
// 3. Devolver la respuesta capturada
return frankenphp.PHPString(rr.Body.String(), false)
}
```
## Script Worker
El script worker PHP se ejecuta en un bucle y puede manejar tanto mensajes sin procesar como solicitudes HTTP.
```php
<?php
// Manejar tanto mensajes sin procesar como solicitudes HTTP en el mismo bucle
$handler = function ($payload = null) {
// Caso 1: Modo Mensaje
if ($payload !== null) {
return "Payload recibido: " . $payload;
}
// Caso 2: Modo HTTP (las superglobales estándar de PHP están pobladas)
echo "Hola desde la página: " . $_SERVER['REQUEST_URI'];
};
while (frankenphp_handle_request($handler)) {
gc_collect_cycles();
}
```
## Hooks de Ciclo de Vida
FrankenPHP proporciona hooks para ejecutar código Go en puntos específicos del ciclo de vida.
| Tipo de Hook | Nombre de Opción | Firma | Contexto y Caso de Uso |
| :----------- | :--------------------------- | :------------------- | :-------------------------------------------------------------------------- |
| **Server** | `WithWorkerOnServerStartup` | `func()` | Configuración global. Se ejecuta **Una vez**. Ejemplo: Conectar a NATS/Redis. |
| **Server** | `WithWorkerOnServerShutdown` | `func()` | Limpieza global. Se ejecuta **Una vez**. Ejemplo: Cerrar conexiones compartidas. |
| **Thread** | `WithWorkerOnReady` | `func(threadID int)` | Configuración por hilo. Llamado cuando un hilo inicia. Recibe el ID del hilo. |
| **Thread** | `WithWorkerOnShutdown` | `func(threadID int)` | Limpieza por hilo. Recibe el ID del hilo. |
### Ejemplo
```go
package myextension
import (
"fmt"
"github.com/dunglas/frankenphp"
frankenphpCaddy "github.com/dunglas/frankenphp/caddy"
)
func init() {
workerHandle = frankenphpCaddy.RegisterWorkers(
"my-worker", "worker.php", 2,
// Inicio del Servidor (Global)
frankenphp.WithWorkerOnServerStartup(func() {
fmt.Println("Extension: Servidor iniciando...")
}),
// Hilo Listo (Por Hilo)
// Nota: La función acepta un entero que representa el ID del hilo
frankenphp.WithWorkerOnReady(func(id int) {
fmt.Printf("Extension: Hilo worker #%d está listo.\n", id)
}),
)
}
```

893
docs/es/extensions.md Normal file
View File

@@ -0,0 +1,893 @@
# Escribir Extensiones PHP en Go
Con FrankenPHP, puedes **escribir extensiones PHP en Go**, lo que te permite crear **funciones nativas de alto rendimiento** que pueden ser llamadas directamente desde PHP. Tus aplicaciones pueden aprovechar cualquier biblioteca Go existente o nueva, así como el famoso modelo de concurrencia de **goroutines directamente desde tu código PHP**.
Escribir extensiones PHP típicamente se hace en C, pero también es posible escribirlas en otros lenguajes con un poco de trabajo adicional. Las extensiones PHP te permiten aprovechar el poder de lenguajes de bajo nivel para extender las funcionalidades de PHP, por ejemplo, añadiendo funciones nativas o optimizando operaciones específicas.
Gracias a los módulos de Caddy, puedes escribir extensiones PHP en Go e integrarlas muy rápidamente en FrankenPHP.
## Dos Enfoques
FrankenPHP proporciona dos formas de crear extensiones PHP en Go:
1. **Usando el Generador de Extensiones** - El enfoque recomendado que genera todo el código repetitivo necesario para la mayoría de los casos de uso, permitiéndote enfocarte en escribir tu código Go.
2. **Implementación Manual** - Control total sobre la estructura de la extensión para casos de uso avanzados.
Comenzaremos con el enfoque del generador ya que es la forma más fácil de empezar, luego mostraremos la implementación manual para aquellos que necesitan un control completo.
## Usando el Generador de Extensiones
FrankenPHP incluye una herramienta que te permite **crear una extensión PHP** usando solo Go. **No necesitas escribir código C** ni usar CGO directamente: FrankenPHP también incluye una **API de tipos públicos** para ayudarte a escribir tus extensiones en Go sin tener que preocuparte por **la manipulación de tipos entre PHP/C y Go**.
> [!TIP]
> Si quieres entender cómo se pueden escribir extensiones en Go desde cero, puedes leer la sección de implementación manual a continuación que demuestra cómo escribir una extensión PHP en Go sin usar el generador.
Ten en cuenta que esta herramienta **no es un generador de extensiones completo**. Está diseñada para ayudarte a escribir extensiones simples en Go, pero no proporciona las características más avanzadas de las extensiones PHP. Si necesitas escribir una extensión más **compleja y optimizada**, es posible que necesites escribir algo de código C o usar CGO directamente.
### Requisitos Previos
Como se cubre en la sección de implementación manual a continuación, necesitas [obtener las fuentes de PHP](https://www.php.net/downloads.php) y crear un nuevo módulo Go.
#### Crear un Nuevo Módulo y Obtener las Fuentes de PHP
El primer paso para escribir una extensión PHP en Go es crear un nuevo módulo Go. Puedes usar el siguiente comando para esto:
```console
go mod init ejemplo.com/ejemplo
```
El segundo paso es [obtener las fuentes de PHP](https://www.php.net/downloads.php) para los siguientes pasos. Una vez que las tengas, descomprímelas en el directorio de tu elección, no dentro de tu módulo Go:
```console
tar xf php-*
```
### Escribiendo la Extensión
Todo está listo para escribir tu función nativa en Go. Crea un nuevo archivo llamado `stringext.go`. Nuestra primera función tomará una cadena como argumento, el número de veces para repetirla, un booleano para indicar si invertir la cadena, y devolverá la cadena resultante. Esto debería verse así:
```go
package ejemplo
// #include <Zend/zend_types.h>
import "C"
import (
"strings"
"unsafe"
"github.com/dunglas/frankenphp"
)
//export_php:function repeat_this(string $str, int $count, bool $reverse): string
func repeat_this(s *C.zend_string, count int64, reverse bool) unsafe.Pointer {
str := frankenphp.GoString(unsafe.Pointer(s))
result := strings.Repeat(str, int(count))
if reverse {
runes := []rune(result)
for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
runes[i], runes[j] = runes[j], runes[i]
}
result = string(runes)
}
return frankenphp.PHPString(result, false)
}
```
Hay dos cosas importantes a tener en cuenta aquí:
- Un comentario de directiva `//export_php:function` define la firma de la función en PHP. Así es como el generador sabe cómo generar la función PHP con los parámetros y tipo de retorno correctos;
- La función debe devolver un `unsafe.Pointer`. FrankenPHP proporciona una API para ayudarte con la manipulación de tipos entre C y Go.
Mientras que el primer punto se explica por sí mismo, el segundo puede ser más difícil de entender. Profundicemos en la manipulación de tipos en la siguiente sección.
### Manipulación de Tipos
Aunque algunos tipos de variables tienen la misma representación en memoria entre C/PHP y Go, algunos tipos requieren más lógica para ser usados directamente. Esta es quizá la parte más difícil cuando se trata de escribir extensiones porque requiere entender los internos del motor Zend y cómo se almacenan las variables internamente en PHP.
Esta tabla resume lo que necesitas saber:
| Tipo PHP | Tipo Go | Conversión directa | Helper de C a Go | Helper de Go a C | Soporte para Métodos de Clase |
|---------------------|--------------------------------|---------------------|---------------------------------------|----------------------------------------|-------------------------------|
| `int` | `int64` | ✅ | - | - | ✅ |
| `?int` | `*int64` | ✅ | - | - | ✅ |
| `float` | `float64` | ✅ | - | - | ✅ |
| `?float` | `*float64` | ✅ | - | - | ✅ |
| `bool` | `bool` | ✅ | - | - | ✅ |
| `?bool` | `*bool` | ✅ | - | - | ✅ |
| `string`/`?string` | `*C.zend_string` | ❌ | `frankenphp.GoString()` | `frankenphp.PHPString()` | ✅ |
| `array` | `frankenphp.AssociativeArray` | ❌ | `frankenphp.GoAssociativeArray()` | `frankenphp.PHPAssociativeArray()` | ✅ |
| `array` | `map[string]any` | ❌ | `frankenphp.GoMap()` | `frankenphp.PHPMap()` | ✅ |
| `array` | `[]any` | ❌ | `frankenphp.GoPackedArray()` | `frankenphp.PHPPackedArray()` | ✅ |
| `mixed` | `any` | ❌ | `GoValue()` | `PHPValue()` | ❌ |
| `callable` | `*C.zval` | ❌ | - | frankenphp.CallPHPCallable() | ❌ |
| `object` | `struct` | ❌ | _Aún no implementado_ | _Aún no implementado_ | ❌ |
> [!NOTE]
>
> Esta tabla aún no es exhaustiva y se completará a medida que la API de tipos de FrankenPHP se vuelva más completa.
>
> Para métodos de clase específicamente, los tipos primitivos y los arrays están actualmente soportados. Los objetos aún no pueden usarse como parámetros de métodos o tipos de retorno.
Si te refieres al fragmento de código de la sección anterior, puedes ver que se usan helpers para convertir el primer parámetro y el valor de retorno. El segundo y tercer parámetro de nuestra función `repeat_this()` no necesitan ser convertidos ya que la representación en memoria de los tipos subyacentes es la misma para C y Go.
#### Trabajando con Arrays
FrankenPHP proporciona soporte nativo para arrays PHP a través de `frankenphp.AssociativeArray` o conversión directa a un mapa o slice.
`AssociativeArray` representa un [mapa hash](https://es.wikipedia.org/wiki/Tabla_hash) compuesto por un campo `Map: map[string]any` y un campo opcional `Order: []string` (a diferencia de los "arrays asociativos" de PHP, los mapas de Go no están ordenados).
Si no se necesita orden o asociación, también es posible convertir directamente a un slice `[]any` o un mapa no ordenado `map[string]any`.
**Creando y manipulando arrays en Go:**
```go
package ejemplo
// #include <Zend/zend_types.h>
import "C"
import (
"unsafe"
"github.com/dunglas/frankenphp"
)
// export_php:function process_data_ordered(array $input): array
func process_data_ordered_map(arr *C.zend_array) unsafe.Pointer {
// Convertir array asociativo PHP a Go manteniendo el orden
associativeArray, err := frankenphp.GoAssociativeArray[any](unsafe.Pointer(arr))
if err != nil {
// manejar error
}
// iterar sobre las entradas en orden
for _, key := range associativeArray.Order {
value, _ = associativeArray.Map[key]
// hacer algo con key y value
}
// devolver un array ordenado
// si 'Order' no está vacío, solo se respetarán los pares clave-valor en 'Order'
return frankenphp.PHPAssociativeArray[string](frankenphp.AssociativeArray[string]{
Map: map[string]string{
"clave1": "valor1",
"clave2": "valor2",
},
Order: []string{"clave1", "clave2"},
})
}
// export_php:function process_data_unordered(array $input): array
func process_data_unordered_map(arr *C.zend_array) unsafe.Pointer {
// Convertir array asociativo PHP a un mapa Go sin mantener el orden
// ignorar el orden será más eficiente
goMap, err := frankenphp.GoMap[any](unsafe.Pointer(arr))
if err != nil {
// manejar error
}
// iterar sobre las entradas sin un orden específico
for key, value := range goMap {
// hacer algo con key y value
}
// devolver un array no ordenado
return frankenphp.PHPMap(map[string]string {
"clave1": "valor1",
"clave2": "valor2",
})
}
// export_php:function process_data_packed(array $input): array
func process_data_packed(arr *C.zend_array) unsafe.Pointer {
// Convertir array empaquetado PHP a Go
goSlice, err := frankenphp.GoPackedArray(unsafe.Pointer(arr))
if err != nil {
// manejar error
}
// iterar sobre el slice en orden
for index, value := range goSlice {
// hacer algo con index y value
}
// devolver un array empaquetado
return frankenphp.PHPPackedArray([]string{"valor1", "valor2", "valor3"})
}
```
**Características clave de la conversión de arrays:**
- **Pares clave-valor ordenados** - Opción para mantener el orden del array asociativo
- **Optimizado para múltiples casos** - Opción para prescindir del orden para un mejor rendimiento o convertir directamente a un slice
- **Detección automática de listas** - Al convertir a PHP, detecta automáticamente si el array debe ser una lista empaquetada o un mapa hash
- **Arrays Anidados** - Los arrays pueden estar anidados y convertirán automáticamente todos los tipos soportados (`int64`, `float64`, `string`, `bool`, `nil`, `AssociativeArray`, `map[string]any`, `[]any`)
- **Objetos no soportados** - Actualmente, solo se pueden usar tipos escalares y arrays como valores. Proporcionar un objeto resultará en un valor `null` en el array PHP.
##### Métodos Disponibles: Empaquetados y Asociativos
- `frankenphp.PHPAssociativeArray(arr frankenphp.AssociativeArray) unsafe.Pointer` - Convertir a un array PHP ordenado con pares clave-valor
- `frankenphp.PHPMap(arr map[string]any) unsafe.Pointer` - Convertir un mapa a un array PHP no ordenado con pares clave-valor
- `frankenphp.PHPPackedArray(slice []any) unsafe.Pointer` - Convertir un slice a un array PHP empaquetado con solo valores indexados
- `frankenphp.GoAssociativeArray(arr unsafe.Pointer, ordered bool) frankenphp.AssociativeArray` - Convertir un array PHP a un `AssociativeArray` de Go ordenado (mapa con orden)
- `frankenphp.GoMap(arr unsafe.Pointer) map[string]any` - Convertir un array PHP a un mapa Go no ordenado
- `frankenphp.GoPackedArray(arr unsafe.Pointer) []any` - Convertir un array PHP a un slice Go
- `frankenphp.IsPacked(zval *C.zend_array) bool` - Verificar si un array PHP está empaquetado (solo indexado) o es asociativo (pares clave-valor)
### Trabajando con Callables
FrankenPHP proporciona una forma de trabajar con callables de PHP usando el helper `frankenphp.CallPHPCallable`. Esto te permite llamar a funciones o métodos de PHP desde código Go.
Para mostrar esto, creemos nuestra propia función `array_map()` que toma un callable y un array, aplica el callable a cada elemento del array, y devuelve un nuevo array con los resultados:
```go
// export_php:function my_array_map(array $data, callable $callback): array
func my_array_map(arr *C.zend_array, callback *C.zval) unsafe.Pointer {
goSlice, err := frankenphp.GoPackedArray[any](unsafe.Pointer(arr))
if err != nil {
panic(err)
}
result := make([]any, len(goSlice))
for index, value := range goSlice {
result[index] = frankenphp.CallPHPCallable(unsafe.Pointer(callback), []interface{}{value})
}
return frankenphp.PHPPackedArray(result)
}
```
Observa cómo usamos `frankenphp.CallPHPCallable()` para llamar al callable de PHP pasado como parámetro. Esta función toma un puntero al callable y un array de argumentos, y devuelve el resultado de la ejecución del callable. Puedes usar la sintaxis de callable a la que estás acostumbrado:
```php
<?php
$result = my_array_map([1, 2, 3], function($x) { return $x * 2; });
// $result será [2, 4, 6]
$result = my_array_map(['hola', 'mundo'], 'strtoupper');
// $result será ['HOLA', 'MUNDO']
```
### Declarando una Clase Nativa de PHP
El generador soporta la declaración de **clases opacas** como estructuras Go, que pueden usarse para crear objetos PHP. Puedes usar el comentario de directiva `//export_php:class` para definir una clase PHP. Por ejemplo:
```go
package ejemplo
//export_php:class User
type UserStruct struct {
Name string
Age int
}
```
#### ¿Qué son las Clases Opaque?
Las **clases opacas** son clases donde la estructura interna (propiedades) está oculta del código PHP. Esto significa:
- **Sin acceso directo a propiedades**: No puedes leer o escribir propiedades directamente desde PHP (`$user->name` no funcionará)
- **Interfaz solo de métodos** - Todas las interacciones deben pasar a través de los métodos que defines
- **Mejor encapsulación** - La estructura de datos interna está completamente controlada por el código Go
- **Seguridad de tipos** - Sin riesgo de que el código PHP corrompa el estado interno con tipos incorrectos
- **API más limpia** - Obliga a diseñar una interfaz pública adecuada
Este enfoque proporciona una mejor encapsulación y evita que el código PHP corrompa accidentalmente el estado interno de tus objetos Go. Todas las interacciones con el objeto deben pasar a través de los métodos que defines explícitamente.
#### Añadiendo Métodos a las Clases
Dado que las propiedades no son directamente accesibles, **debes definir métodos** para interactuar con tus clases opacas. Usa la directiva `//export_php:method` para definir el comportamiento:
```go
package ejemplo
// #include <Zend/zend_types.h>
import "C"
import (
"unsafe"
"github.com/dunglas/frankenphp"
)
//export_php:class User
type UserStruct struct {
Name string
Age int
}
//export_php:method User::getName(): string
func (us *UserStruct) GetUserName() unsafe.Pointer {
return frankenphp.PHPString(us.Name, false)
}
//export_php:method User::setAge(int $age): void
func (us *UserStruct) SetUserAge(age int64) {
us.Age = int(age)
}
//export_php:method User::getAge(): int
func (us *UserStruct) GetUserAge() int64 {
return int64(us.Age)
}
//export_php:method User::setNamePrefix(string $prefix = "User"): void
func (us *UserStruct) SetNamePrefix(prefix *C.zend_string) {
us.Name = frankenphp.GoString(unsafe.Pointer(prefix)) + ": " + us.Name
}
```
#### Parámetros Nulos
El generador soporta parámetros nulos usando el prefijo `?` en las firmas de PHP. Cuando un parámetro es nulo, se convierte en un puntero en tu función Go, permitiéndote verificar si el valor era `null` en PHP:
```go
package ejemplo
// #include <Zend/zend_types.h>
import "C"
import (
"unsafe"
"github.com/dunglas/frankenphp"
)
//export_php:method User::updateInfo(?string $name, ?int $age, ?bool $active): void
func (us *UserStruct) UpdateInfo(name *C.zend_string, age *int64, active *bool) {
// Verificar si se proporcionó name (no es null)
if name != nil {
us.Name = frankenphp.GoString(unsafe.Pointer(name))
}
// Verificar si se proporcionó age (no es null)
if age != nil {
us.Age = int(*age)
}
// Verificar si se proporcionó active (no es null)
if active != nil {
us.Active = *active
}
}
```
**Puntos clave sobre parámetros nulos:**
- **Tipos primitivos nulos** (`?int`, `?float`, `?bool`) se convierten en punteros (`*int64`, `*float64`, `*bool`) en Go
- **Strings nulos** (`?string`) permanecen como `*C.zend_string` pero pueden ser `nil`
- **Verificar `nil`** antes de desreferenciar valores de puntero
- **`null` de PHP se convierte en `nil` de Go** - cuando PHP pasa `null`, tu función Go recibe un puntero `nil`
> [!CAUTION]
>
> Actualmente, los métodos de clase tienen las siguientes limitaciones. **Los objetos no están soportados** como tipos de parámetro o tipos de retorno. **Los arrays están completamente soportados** para ambos parámetros y tipos de retorno. Tipos soportados: `string`, `int`, `float`, `bool`, `array`, y `void` (para tipo de retorno). **Los tipos de parámetros nulos están completamente soportados** para todos los tipos escalares (`?string`, `?int`, `?float`, `?bool`).
Después de generar la extensión, podrás usar la clase y sus métodos en PHP. Ten en cuenta que **no puedes acceder a las propiedades directamente**:
```php
<?php
$user = new User();
// ✅ Esto funciona - usando métodos
$user->setAge(25);
echo $user->getName(); // Salida: (vacío, valor por defecto)
echo $user->getAge(); // Salida: 25
$user->setNamePrefix("Empleado");
// ✅ Esto también funciona - parámetros nulos
$user->updateInfo("John", 30, true); // Todos los parámetros proporcionados
$user->updateInfo("Jane", null, false); // Age es null
$user->updateInfo(null, 25, null); // Name y active son null
// ❌ Esto NO funcionará - acceso directo a propiedades
// echo $user->name; // Error: No se puede acceder a la propiedad privada
// $user->age = 30; // Error: No se puede acceder a la propiedad privada
```
Este diseño asegura que tu código Go tenga control completo sobre cómo se accede y modifica el estado del objeto, proporcionando una mejor encapsulación y seguridad de tipos.
### Declarando Constantes
El generador soporta exportar constantes Go a PHP usando dos directivas: `//export_php:const` para constantes globales y `//export_php:classconst` para constantes de clase. Esto te permite compartir valores de configuración, códigos de estado y otras constantes entre código Go y PHP.
#### Constantes Globales
Usa la directiva `//export_php:const` para crear constantes globales de PHP:
```go
package ejemplo
//export_php:const
const MAX_CONNECTIONS = 100
//export_php:const
const API_VERSION = "1.2.3"
//export_php:const
const STATUS_OK = iota
//export_php:const
const STATUS_ERROR = iota
```
#### Constantes de Clase
Usa la directiva `//export_php:classconst ClassName` para crear constantes que pertenecen a una clase PHP específica:
```go
package ejemplo
//export_php:classconst User
const STATUS_ACTIVE = 1
//export_php:classconst User
const STATUS_INACTIVE = 0
//export_php:classconst User
const ROLE_ADMIN = "admin"
//export_php:classconst Order
const STATE_PENDING = iota
//export_php:classconst Order
const STATE_PROCESSING = iota
//export_php:classconst Order
const STATE_COMPLETED = iota
```
Las constantes de clase son accesibles usando el ámbito del nombre de clase en PHP:
```php
<?php
// Constantes globales
echo MAX_CONNECTIONS; // 100
echo API_VERSION; // "1.2.3"
// Constantes de clase
echo User::STATUS_ACTIVE; // 1
echo User::ROLE_ADMIN; // "admin"
echo Order::STATE_PENDING; // 0
```
La directiva soporta varios tipos de valores incluyendo strings, enteros, booleanos, floats y constantes iota. Cuando se usa `iota`, el generador asigna automáticamente valores secuenciales (0, 1, 2, etc.). Las constantes globales se vuelven disponibles en tu código PHP como constantes globales, mientras que las constantes de clase tienen alcance a sus respectivas clases usando visibilidad pública. Cuando se usan enteros, se soportan diferentes notaciones posibles (binario, hexadecimal, octal) y se vuelcan tal cual en el archivo stub de PHP.
Puedes usar constantes tal como estás acostumbrado en el código Go. Por ejemplo, tomemos la función `repeat_this()` que declaramos anteriormente y cambiemos el último argumento a un entero:
```go
package ejemplo
// #include <Zend/zend_types.h>
import "C"
import (
"strings"
"unsafe"
"github.com/dunglas/frankenphp"
)
//export_php:const
const STR_REVERSE = iota
//export_php:const
const STR_NORMAL = iota
//export_php:classconst StringProcessor
const MODE_LOWERCASE = 1
//export_php:classconst StringProcessor
const MODE_UPPERCASE = 2
//export_php:function repeat_this(string $str, int $count, int $mode): string
func repeat_this(s *C.zend_string, count int64, mode int) unsafe.Pointer {
str := frankenphp.GoString(unsafe.Pointer(s))
result := strings.Repeat(str, int(count))
if mode == STR_REVERSE {
// invertir la cadena
}
if mode == STR_NORMAL {
// no hacer nada, solo para mostrar la constante
}
return frankenphp.PHPString(result, false)
}
//export_php:class StringProcessor
type StringProcessorStruct struct {
// campos internos
}
//export_php:method StringProcessor::process(string $input, int $mode): string
func (sp *StringProcessorStruct) Process(input *C.zend_string, mode int64) unsafe.Pointer {
str := frankenphp.GoString(unsafe.Pointer(input))
switch mode {
case MODE_LOWERCASE:
str = strings.ToLower(str)
case MODE_UPPERCASE:
str = strings.ToUpper(str)
}
return frankenphp.PHPString(str, false)
}
```
### Usando Espacios de Nombres
El generador soporta organizar las funciones, clases y constantes de tu extensión PHP bajo un espacio de nombres usando la directiva `//export_php:namespace`. Esto ayuda a evitar conflictos de nombres y proporciona una mejor organización para la API de tu extensión.
#### Declarando un Espacio de Nombres
Usa la directiva `//export_php:namespace` al inicio de tu archivo Go para colocar todos los símbolos exportados bajo un espacio de nombres específico:
```go
//export_php:namespace Mi\Extensión
package ejemplo
import (
"unsafe"
"github.com/dunglas/frankenphp"
)
//export_php:function hello(): string
func hello() string {
return "Hola desde el espacio de nombres Mi\\Extensión!"
}
//export_php:class User
type UserStruct struct {
// campos internos
}
//export_php:method User::getName(): string
func (u *UserStruct) GetName() unsafe.Pointer {
return frankenphp.PHPString("John Doe", false)
}
//export_php:const
const STATUS_ACTIVE = 1
```
#### Usando la Extensión con Espacio de Nombres en PHP
Cuando se declara un espacio de nombres, todas las funciones, clases y constantes se colocan bajo ese espacio de nombres en PHP:
```php
<?php
echo Mi\Extensión\hello(); // "Hola desde el espacio de nombres Mi\Extensión!"
$user = new Mi\Extensión\User();
echo $user->getName(); // "John Doe"
echo Mi\Extensión\STATUS_ACTIVE; // 1
```
#### Notas Importantes
- Solo se permite **una** directiva de espacio de nombres por archivo. Si se encuentran múltiples directivas de espacio de nombres, el generador devolverá un error.
- El espacio de nombres se aplica a **todos** los símbolos exportados en el archivo: funciones, clases, métodos y constantes.
- Los nombres de espacios de nombres siguen las convenciones de espacios de nombres de PHP usando barras invertidas (`\`) como separadores.
- Si no se declara un espacio de nombres, los símbolos se exportan al espacio de nombres global como de costumbre.
### Generando la Extensión
Aquí es donde ocurre la magia, y tu extensión ahora puede ser generada. Puedes ejecutar el generador con el siguiente comando:
```console
GEN_STUB_SCRIPT=php-src/build/gen_stub.php frankenphp extension-init mi_extensión.go
```
> [!NOTE]
> No olvides establecer la variable de entorno `GEN_STUB_SCRIPT` a la ruta del archivo `gen_stub.php` en las fuentes de PHP que descargaste anteriormente. Este es el mismo script `gen_stub.php` mencionado en la sección de implementación manual.
Si todo salió bien, se debería haber creado un nuevo directorio llamado `build`. Este directorio contiene los archivos generados para tu extensión, incluyendo el archivo `mi_extensión.go` con los stubs de funciones PHP generadas.
### Integrando la Extensión Generada en FrankenPHP
Nuestra extensión ahora está lista para ser compilada e integrada en FrankenPHP. Para hacerlo, consulta la documentación de [compilación de FrankenPHP](compile.md) para aprender cómo compilar FrankenPHP. Agrega el módulo usando la bandera `--with`, apuntando a la ruta de tu módulo:
```console
CGO_ENABLED=1 \
XCADDY_GO_BUILD_FLAGS="-ldflags='-w -s' -tags=nobadger,nomysql,nopgx" \
CGO_CFLAGS=$(php-config --includes) \
CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)" \
xcaddy build \
--output frankenphp \
--with github.com/mi-cuenta/mi-módulo/build
```
Ten en cuenta que apuntas al subdirectorio `/build` que se creó durante el paso de generación. Sin embargo, esto no es obligatorio: también puedes copiar los archivos generados a tu directorio de módulo y apuntar a él directamente.
### Probando tu Extensión Generada
Puedes crear un archivo PHP para probar las funciones y clases que has creado. Por ejemplo, crea un archivo `index.php` con el siguiente contenido:
```php
<?php
// Usando constantes globales
var_dump(repeat_this('Hola Mundo', 5, STR_REVERSE));
// Usando constantes de clase
$processor = new StringProcessor();
echo $processor->process('Hola Mundo', StringProcessor::MODE_LOWERCASE); // "hola mundo"
echo $processor->process('Hola Mundo', StringProcessor::MODE_UPPERCASE); // "HOLA MUNDO"
```
Una vez que hayas integrado tu extensión en FrankenPHP como se demostró en la sección anterior, puedes ejecutar este archivo de prueba usando `./frankenphp php-server`, y deberías ver tu extensión funcionando.
## Implementación Manual
Si quieres entender cómo funcionan las extensiones o necesitas un control total sobre tu extensión, puedes escribirlas manualmente. Este enfoque te da control completo pero requiere más código repetitivo.
### Función Básica
Veremos cómo escribir una extensión PHP simple en Go que define una nueva función nativa. Esta función será llamada desde PHP y desencadenará una goroutine que registra un mensaje en los logs de Caddy. Esta función no toma ningún parámetro y no devuelve nada.
#### Definir la Función Go
En tu módulo, necesitas definir una nueva función nativa que será llamada desde PHP. Para esto, crea un archivo con el nombre que desees, por ejemplo, `extension.go`, y agrega el siguiente código:
```go
package ejemplo
// #include "extension.h"
import "C"
import (
"log/slog"
"unsafe"
"github.com/dunglas/frankenphp"
)
func init() {
frankenphp.RegisterExtension(unsafe.Pointer(&C.ext_module_entry))
}
//export go_print_something
func go_print_something() {
go func() {
slog.Info("¡Hola desde una goroutine!")
}()
}
```
La función `frankenphp.RegisterExtension()` simplifica el proceso de registro de la extensión manejando la lógica interna de registro de PHP. La función `go_print_something` usa la directiva `//export` para indicar que será accesible en el código C que escribiremos, gracias a CGO.
En este ejemplo, nuestra nueva función desencadenará una goroutine que registra un mensaje en los logs de Caddy.
#### Definir la Función PHP
Para permitir que PHP llame a nuestra función, necesitamos definir una función PHP correspondiente. Para esto, crearemos un archivo stub, por ejemplo, `extension.stub.php`, que contendrá el siguiente código:
```php
<?php
/** @generate-class-entries */
function go_print(): void {}
```
Este archivo define la firma de la función `go_print()`, que será llamada desde PHP. La directiva `@generate-class-entries` permite a PHP generar automáticamente entradas de funciones para nuestra extensión.
Esto no se hace manualmente, sino usando un script proporcionado en las fuentes de PHP (asegúrate de ajustar la ruta al script `gen_stub.php` según dónde se encuentren tus fuentes de PHP):
```bash
php ../php-src/build/gen_stub.php extension.stub.php
```
Este script generará un archivo llamado `extension_arginfo.h` que contiene la información necesaria para que PHP sepa cómo definir y llamar a nuestra función.
#### Escribir el Puente entre Go y C
Ahora, necesitamos escribir el puente entre Go y C. Crea un archivo llamado `extension.h` en el directorio de tu módulo con el siguiente contenido:
```c
#ifndef _EXTENSION_H
#define _EXTENSION_H
#include <php.h>
extern zend_module_entry ext_module_entry;
#endif
```
A continuación, crea un archivo llamado `extension.c` que realizará los siguientes pasos:
- Incluir los encabezados de PHP;
- Declarar nuestra nueva función nativa de PHP `go_print()`;
- Declarar los metadatos de la extensión.
Comencemos incluyendo los encabezados requeridos:
```c
#include <php.h>
#include "extension.h"
#include "extension_arginfo.h"
// Contiene símbolos exportados por Go
#include "_cgo_export.h"
```
Luego definimos nuestra función PHP como una función de lenguaje nativo:
```c
PHP_FUNCTION(go_print)
{
ZEND_PARSE_PARAMETERS_NONE();
go_print_something();
}
zend_module_entry ext_module_entry = {
STANDARD_MODULE_HEADER,
"ext_go",
ext_functions, /* Funciones */
NULL, /* MINIT */
NULL, /* MSHUTDOWN */
NULL, /* RINIT */
NULL, /* RSHUTDOWN */
NULL, /* MINFO */
"0.1.1",
STANDARD_MODULE_PROPERTIES
};
```
En este caso, nuestra función no toma parámetros y no devuelve nada. Simplemente llama a la función Go que definimos anteriormente, exportada usando la directiva `//export`.
Finalmente, definimos los metadatos de la extensión en una estructura `zend_module_entry`, como su nombre, versión y propiedades. Esta información es necesaria para que PHP reconozca y cargue nuestra extensión. Ten en cuenta que `ext_functions` es un array de punteros a las funciones PHP que definimos, y fue generado automáticamente por el script `gen_stub.php` en el archivo `extension_arginfo.h`.
El registro de la extensión es manejado automáticamente por la función `RegisterExtension()` de FrankenPHP que llamamos en nuestro código Go.
### Uso Avanzado
Ahora que sabemos cómo crear una extensión PHP básica en Go, compliquemos nuestro ejemplo. Ahora crearemos una función PHP que tome una cadena como parámetro y devuelva su versión en mayúsculas.
#### Definir el Stub de la Función PHP
Para definir la nueva función PHP, modificaremos nuestro archivo `extension.stub.php` para incluir la nueva firma de la función:
```php
<?php
/** @generate-class-entries */
/**
* Convierte una cadena a mayúsculas.
*
* @param string $string La cadena a convertir.
* @return string La versión en mayúsculas de la cadena.
*/
function go_upper(string $string): string {}
```
> [!TIP]
> ¡No descuides la documentación de tus funciones! Es probable que compartas tus stubs de extensión con otros desarrolladores para documentar cómo usar tu extensión y qué características están disponibles.
Al regenerar el archivo stub con el script `gen_stub.php`, el archivo `extension_arginfo.h` debería verse así:
```c
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_go_upper, 0, 1, IS_STRING, 0)
ZEND_ARG_TYPE_INFO(0, string, IS_STRING, 0)
ZEND_END_ARG_INFO()
ZEND_FUNCTION(go_upper);
static const zend_function_entry ext_functions[] = {
ZEND_FE(go_upper, arginfo_go_upper)
ZEND_FE_END
};
```
Podemos ver que la función `go_upper` está definida con un parámetro de tipo `string` y un tipo de retorno `string`.
#### Manipulación de Tipos entre Go y PHP/C
Tu función Go no puede aceptar directamente una cadena PHP como parámetro. Necesitas convertirla a una cadena Go. Afortunadamente, FrankenPHP proporciona funciones helper para manejar la conversión entre cadenas PHP y cadenas Go, similar a lo que vimos en el enfoque del generador.
El archivo de encabezado sigue siendo simple:
```c
#ifndef _EXTENSION_H
#define _EXTENSION_H
#include <php.h>
extern zend_module_entry ext_module_entry;
#endif
```
Ahora podemos escribir el puente entre Go y C en nuestro archivo `extension.c`. Pasaremos la cadena PHP directamente a nuestra función Go:
```c
PHP_FUNCTION(go_upper)
{
zend_string *str;
ZEND_PARSE_PARAMETERS_START(1, 1)
Z_PARAM_STR(str)
ZEND_PARSE_PARAMETERS_END();
zend_string *result = go_upper(str);
RETVAL_STR(result);
}
```
Puedes aprender más sobre `ZEND_PARSE_PARAMETERS_START` y el análisis de parámetros en la página dedicada del [Libro de Internals de PHP](https://www.phpinternalsbook.com/php7/extensions_design/php_functions.html#parsing-parameters-zend-parse-parameters). Aquí, le decimos a PHP que nuestra función toma un parámetro obligatorio de tipo `string` como `zend_string`. Luego pasamos esta cadena directamente a nuestra función Go y devolvemos el resultado usando `RETVAL_STR`.
Solo queda una cosa por hacer: implementar la función `go_upper` en Go.
#### Implementar la Función Go
Nuestra función Go tomará un `*C.zend_string` como parámetro, lo convertirá a una cadena Go usando la función helper de FrankenPHP, lo procesará y devolverá el resultado como un nuevo `*C.zend_string`. Las funciones helper manejan toda la complejidad de gestión de memoria y conversión por nosotros.
```go
package ejemplo
// #include <Zend/zend_types.h>
import "C"
import (
"unsafe"
"strings"
"github.com/dunglas/frankenphp"
)
//export go_upper
func go_upper(s *C.zend_string) *C.zend_string {
str := frankenphp.GoString(unsafe.Pointer(s))
upper := strings.ToUpper(str)
return (*C.zend_string)(frankenphp.PHPString(upper, false))
}
```
Este enfoque es mucho más limpio y seguro que la gestión manual de memoria.
Las funciones helper de FrankenPHP manejan la conversión entre el formato `zend_string` de PHP y las cadenas Go automáticamente.
El parámetro `false` en `PHPString()` indica que queremos crear una nueva cadena no persistente (liberada al final de la solicitud).
> [!TIP]
>
> En este ejemplo, no realizamos ningún manejo de errores, pero siempre debes verificar que los punteros no sean `nil` y que los datos sean válidos antes de usarlos en tus funciones Go.
### Integrando la Extensión en FrankenPHP
Nuestra extensión ahora está lista para ser compilada e integrada en FrankenPHP. Para hacerlo, consulta la documentación de [compilación de FrankenPHP](compile.md) para aprender cómo compilar FrankenPHP. Agrega el módulo usando la bandera `--with`, apuntando a la ruta de tu módulo:
```console
CGO_ENABLED=1 \
XCADDY_GO_BUILD_FLAGS="-ldflags='-w -s' -tags=nobadger,nomysql,nopgx" \
CGO_CFLAGS=$(php-config --includes) \
CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)" \
xcaddy build \
--output frankenphp \
--with github.com/mi-cuenta/mi-módulo
```
¡Eso es todo! Tu extensión ahora está integrada en FrankenPHP y puede ser usada en tu código PHP.
### Probando tu Extensión
Después de integrar tu extensión en FrankenPHP, puedes crear un archivo `index.php` con ejemplos para las funciones que has implementado:
```php
<?php
// Probar función básica
go_print();
// Probar función avanzada
echo go_upper("hola mundo") . "\n";
```
Ahora puedes ejecutar FrankenPHP con este archivo usando `./frankenphp php-server`, y deberías ver tu extensión funcionando.

31
docs/es/github-actions.md Normal file
View File

@@ -0,0 +1,31 @@
# Usando GitHub Actions
Este repositorio construye y despliega la imagen Docker en [Docker Hub](https://hub.docker.com/r/dunglas/frankenphp) en
cada pull request aprobado o en tu propio fork una vez configurado.
## Configurando GitHub Actions
En la configuración del repositorio, bajo secrets, agrega los siguientes secretos:
- `REGISTRY_LOGIN_SERVER`: El registro Docker a usar (ej. `docker.io`).
- `REGISTRY_USERNAME`: El nombre de usuario para iniciar sesión en el registro (ej. `dunglas`).
- `REGISTRY_PASSWORD`: La contraseña para iniciar sesión en el registro (ej. una clave de acceso).
- `IMAGE_NAME`: El nombre de la imagen (ej. `dunglas/frankenphp`).
## Construyendo y Subiendo la Imagen
1. Crea un Pull Request o haz push a tu fork.
2. GitHub Actions construirá la imagen y ejecutará cualquier prueba.
3. Si la construcción es exitosa, la imagen será subida al registro usando la etiqueta `pr-x`, donde `x` es el número del PR.
## Desplegando la Imagen
1. Una vez que el Pull Request sea fusionado, GitHub Actions ejecutará nuevamente las pruebas y construirá una nueva imagen.
2. Si la construcción es exitosa, la etiqueta `main` será actualizada en el registro Docker.
## Lanzamientos (Releases)
1. Crea una nueva etiqueta (tag) en el repositorio.
2. GitHub Actions construirá la imagen y ejecutará cualquier prueba.
3. Si la construcción es exitosa, la imagen será subida al registro usando el nombre de la etiqueta como etiqueta (ej. se crearán `v1.2.3` y `v1.2`).
4. La etiqueta `latest` también será actualizada.

139
docs/es/hot-reload.md Normal file
View File

@@ -0,0 +1,139 @@
# Hot reload
FrankenPHP incluye una función de **hot reload** integrada diseñada para mejorar significativamente la experiencia del desarrollador.
![Mercure](../hot-reload.png)
Esta función proporciona un flujo de trabajo similar a **Hot Module Replacement (HMR)** encontrado en herramientas modernas de JavaScript (como Vite o webpack).
En lugar de actualizar manualmente el navegador después de cada cambio de archivo (código PHP, plantillas, archivos JavaScript y CSS...),
FrankenPHP actualiza el contenido en tiempo real.
La Hot Reload funciona de forma nativa con WordPress, Laravel, Symfony y cualquier otra aplicación o framework PHP.
Cuando está activada, FrankenPHP vigila el directorio de trabajo actual en busca de cambios en el sistema de archivos.
Cuando se modifica un archivo, envía una actualización [Mercure](mercure.md) al navegador.
Dependiendo de la configuración, el navegador:
- **Transformará el DOM** (preservando la posición de desplazamiento y el estado de los inputs) si [Idiomorph](https://github.com/bigskysoftware/idiomorph) está cargado.
- **Recargará la página** (recarga en vivo estándar) si Idiomorph no está presente.
## Configuración
Para habilitar la Hot Reload, active Mercure y luego agregue la subdirectiva `hot_reload` a la directiva `php_server` en su `Caddyfile`.
> [!WARNING]
> Esta función está destinada **únicamente a entornos de desarrollo**.
> No active `hot_reload` en producción, ya que vigilar el sistema de archivos implica una sobrecarga de rendimiento y expone endpoints internos.
```caddyfile
localhost
mercure {
anonymous
}
root public/
php_server {
hot_reload
}
```
Por omisión, FrankenPHP vigilará todos los archivos en el directorio de trabajo actual que coincidan con este patrón glob: `./**/*.{css,env,gif,htm,html,jpg,jpeg,js,mjs,php,png,svg,twig,webp,xml,yaml,yml}`
Es posible establecer explícitamente los archivos a vigilar usando la sintaxis glob:
```caddyfile
localhost
mercure {
anonymous
}
root public/
php_server {
hot_reload src/**/*{.php,.js} config/**/*.yaml
}
```
Use la forma larga para especificar el tema de Mercure a utilizar, así como qué directorios o archivos vigilar, proporcionando rutas a la opción `hot_reload`:
```caddyfile
localhost
mercure {
anonymous
}
root public/
php_server {
hot_reload {
topic hot-reload-topic
watch src/**/*.php
watch assets/**/*.{ts,json}
watch templates/
watch public/css/
}
}
```
## Integración Lado-Cliente
Mientras el servidor detecta los cambios, el navegador necesita suscribirse a estos eventos para actualizar la página.
FrankenPHP expone la URL del Mercure Hub a utilizar para suscribirse a los cambios de archivos a través de la variable de entorno `$_SERVER['FRANKENPHP_HOT_RELOAD']`.
Una biblioteca JavaScript de conveniencia, [frankenphp-hot-reload](https://www.npmjs.com/package/frankenphp-hot-reload), también está disponible para manejar la lógica lado-cliente.
Para usarla, agregue lo siguiente a su diseño principal:
```php
<!DOCTYPE html>
<title>FrankenPHP Hot Reload</title>
<?php if (isset($_SERVER['FRANKENPHP_HOT_RELOAD'])): ?>
<meta name="frankenphp-hot-reload:url" content="<?=$_SERVER['FRANKENPHP_HOT_RELOAD']?>">
<script src="https://cdn.jsdelivr.net/npm/idiomorph"></script>
<script src="https://cdn.jsdelivr.net/npm/frankenphp-hot-reload/+esm" type="module"></script>
<?php endif ?>
```
La biblioteca se suscribirá automáticamente al hub de Mercure, obtendrá la URL actual en segundo plano cuando se detecte un cambio en un archivo y transformará el DOM.
Está disponible como un paquete [npm](https://www.npmjs.com/package/frankenphp-hot-reload) y en [GitHub](https://github.com/dunglas/frankenphp-hot-reload).
Alternativamente, puede implementar su propia lógica lado-cliente suscribiéndose directamente al hub de Mercure usando la clase nativa de JavaScript `EventSource`.
### Modo Worker
Si está ejecutando su aplicación en [Modo Worker](https://frankenphp.dev/docs/worker/), el script de su aplicación permanece en memoria.
Esto significa que los cambios en su código PHP no se reflejarán inmediatamente, incluso si el navegador se recarga.
Para la mejor experiencia de desarrollador, debe combinar `hot_reload` con [la subdirectiva `watch` en la directiva `worker`](config.md#watching-for-file-changes).
- `hot_reload`: actualiza el **navegador** cuando los archivos cambian
- `worker.watch`: reinicia el worker cuando los archivos cambian
```caddy
localhost
mercure {
anonymous
}
root public/
php_server {
hot_reload
worker {
file /path/to/my_worker.php
watch
}
}
```
### Funcionamiento
1. **Vigilancia**: FrankenPHP monitorea el sistema de archivos en busca de modificaciones usando [la biblioteca `e-dant/watcher`](https://github.com/e-dant/watcher) internamente (contribuimos con el binding de Go).
2. **Reinicio (Modo Worker)**: si `watch` está habilitado en la configuración del worker, el worker de PHP se reinicia para cargar el nuevo código.
3. **Envío**: se envía una carga útil JSON que contiene la lista de archivos modificados al [hub de Mercure](https://mercure.rocks) integrado.
4. **Recepción**: El navegador, escuchando a través de la biblioteca JavaScript, recibe el evento de Mercure.
5. **Actualización**:
- Si se detecta **Idiomorph**, obtiene el contenido actualizado y transforma el HTML actual para que coincida con el nuevo estado, aplicando los cambios al instante sin perder el estado.
- De lo contrario, se llama a `window.location.reload()` para recargar la página.

147
docs/es/known-issues.md Normal file
View File

@@ -0,0 +1,147 @@
# Problemas Conocidos
## Extensiones PHP no Soportadas
Las siguientes extensiones se sabe que no son compatibles con FrankenPHP:
| Nombre | Razón | Alternativas |
| ----------------------------------------------------------------------------------------------------------- | ------------------- | -------------------------------------------------------------------------------------------------------------------- |
| [imap](https://www.php.net/manual/es/imap.installation.php) | No es thread-safe | [javanile/php-imap2](https://github.com/javanile/php-imap2), [webklex/php-imap](https://github.com/Webklex/php-imap) |
| [newrelic](https://docs.newrelic.com/docs/apm/agents/php-agent/getting-started/introduction-new-relic-php/) | No es thread-safe | - |
## Extensiones PHP con Errores
Las siguientes extensiones tienen errores conocidos y comportamientos inesperados cuando se usan con FrankenPHP:
| Nombre | Problema |
| ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [ext-openssl](https://www.php.net/manual/es/book.openssl.php) | Cuando se usa musl libc, la extensión OpenSSL puede fallar bajo cargas pesadas. El problema no ocurre cuando se usa la más popular GNU libc. Este error está [siendo rastreado por PHP](https://github.com/php/php-src/issues/13648). |
## get_browser
La función [get_browser()](https://www.php.net/manual/es/function.get-browser.php) parece funcionar mal después de un tiempo. Una solución es almacenar en caché (por ejemplo, con [APCu](https://www.php.net/manual/es/book.apcu.php)) los resultados por User Agent, ya que son estáticos.
## Binario Autónomo e Imágenes Docker Basadas en Alpine
Los binarios completamente estáticos y las imágenes Docker basadas en Alpine (`dunglas/frankenphp:*-alpine`) usan [musl libc](https://musl.libc.org/) en lugar de [glibc](https://www.etalabs.net/compare_libcs.html), para mantener un tamaño de binario más pequeño.
Esto puede llevar a algunos problemas de compatibilidad.
En particular, la bandera glob `GLOB_BRACE` [no está disponible](https://www.php.net/manual/es/function.glob.php).
Se recomienda usar la variante GNU del binario estático y las imágenes Docker basadas en Debian si encuentras problemas.
## Usar `https://127.0.0.1` con Docker
Por defecto, FrankenPHP genera un certificado TLS para `localhost`.
Es la opción más fácil y recomendada para el desarrollo local.
Si realmente deseas usar `127.0.0.1` como host en su lugar, es posible configurarlo para generar un certificado para él estableciendo el nombre del servidor en `127.0.0.1`.
Desafortunadamente, esto no es suficiente al usar Docker debido a [su sistema de red](https://docs.docker.com/network/).
Obtendrás un error TLS similar a `curl: (35) LibreSSL/3.3.6: error:1404B438:SSL routines:ST_CONNECT:tlsv1 alert internal error`.
Si estás usando Linux, una solución es usar [el controlador de red host](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
```
El controlador de red host no está soportado en Mac y Windows. En estas plataformas, tendrás que adivinar la dirección IP del contenedor e incluirla en los nombres del servidor.
Ejecuta `docker network inspect bridge` y busca la clave `Containers` para identificar la última dirección IP actualmente asignada bajo la clave `IPv4Address`, y incrementa en uno. Si no hay contenedores en ejecución, la primera dirección IP asignada suele ser `172.17.0.2`.
Luego, incluye esto en la variable de entorno `SERVER_NAME`:
```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]
>
> Asegúrate de reemplazar `172.17.0.3` con la IP que se asignará a tu contenedor.
Ahora deberías poder acceder a `https://127.0.0.1` desde la máquina host.
Si no es así, inicia FrankenPHP en modo depuración para intentar identificar el problema:
```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
```
## Scripts de Composer que Referencian `@php`
Los [scripts de Composer](https://getcomposer.org/doc/articles/scripts.md) pueden querer ejecutar un binario PHP para algunas tareas, por ejemplo, en [un proyecto Laravel](laravel.md) para ejecutar `@php artisan package:discover --ansi`. Esto [actualmente falla](https://github.com/php/frankenphp/issues/483#issuecomment-1899890915) por dos razones:
- Composer no sabe cómo llamar al binario de FrankenPHP;
- Composer puede agregar configuraciones de PHP usando la bandera `-d` en el comando, que FrankenPHP aún no soporta.
Como solución alternativa, podemos crear un script de shell en `/usr/local/bin/php` que elimine los parámetros no soportados y luego llame a FrankenPHP:
```bash
#!/usr/bin/env bash
args=("$@")
index=0
for i in "$@"
do
if [ "$i" == "-d" ]; then
unset 'args[$index]'
unset 'args[$index+1]'
fi
index=$((index+1))
done
/usr/local/bin/frankenphp php-cli ${args[@]}
```
Luego, establece la variable de entorno `PHP_BINARY` a la ruta de nuestro script `php` y ejecuta Composer:
```console
export PHP_BINARY=/usr/local/bin/php
composer install
```
## Solución de Problemas de TLS/SSL con Binarios Estáticos
Al usar los binarios estáticos, puedes encontrar los siguientes errores relacionados con TLS, por ejemplo, al enviar correos electrónicos usando STARTTLS:
```text
No se puede conectar con STARTTLS: stream_socket_enable_crypto(): La operación SSL falló con el código 5. Mensajes de error de OpenSSL:
error:80000002:librería del sistema::No existe el archivo o el directorio
error:80000002:librería del sistema::No existe el archivo o el directorio
error:80000002:librería del sistema::No existe el archivo o el directorio
error:0A000086:rutinas de SSL::falló la verificación del certificado
```
Dado que el binario estático no incluye certificados TLS, necesitas indicar a OpenSSL la ubicación de tu instalación local de certificados CA.
Inspecciona la salida de [`openssl_get_cert_locations()`](https://www.php.net/manual/es/function.openssl-get-cert-locations.php),
para encontrar dónde deben instalarse los certificados CA y guárdalos en esa ubicación.
> [!CAUTION]
>
> Los contextos web y CLI pueden tener configuraciones diferentes.
> Asegúrate de ejecutar `openssl_get_cert_locations()` en el contexto adecuado.
[Los certificados CA extraídos de Mozilla pueden descargarse del sitio de cURL](https://curl.se/docs/caextract.html).
Alternativamente, muchas distribuciones, incluyendo Debian, Ubuntu y Alpine, proporcionan paquetes llamados `ca-certificates` que contienen estos certificados.
También es posible usar `SSL_CERT_FILE` y `SSL_CERT_DIR` para indicar a OpenSSL dónde buscar los certificados CA:
```console
# Establecer variables de entorno de certificados TLS
export SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt
export SSL_CERT_DIR=/etc/ssl/certs
```

214
docs/es/laravel.md Normal file
View File

@@ -0,0 +1,214 @@
# Laravel
## Docker
Servir una aplicación web [Laravel](https://laravel.com) con FrankenPHP es tan fácil como montar el proyecto en el directorio `/app` de la imagen Docker oficial.
Ejecuta este comando desde el directorio principal de tu aplicación Laravel:
```console
docker run -p 80:80 -p 443:443 -p 443:443/udp -v $PWD:/app dunglas/frankenphp
```
¡Y listo!
## Instalación Local
Alternativamente, puedes ejecutar tus proyectos Laravel con FrankenPHP desde tu máquina local:
1. [Descarga el binario correspondiente a tu sistema](../#binario-autónomo)
2. Agrega la siguiente configuración a un archivo llamado `Caddyfile` en el directorio raíz de tu proyecto Laravel:
```caddyfile
{
frankenphp
}
# El nombre de dominio de tu servidor
localhost {
# Establece el directorio web raíz en public/
root public/
# Habilita la compresión (opcional)
encode zstd br gzip
# Ejecuta archivos PHP desde el directorio public/ y sirve los assets
php_server {
try_files {path} index.php
}
}
```
3. Inicia FrankenPHP desde el directorio raíz de tu proyecto Laravel: `frankenphp run`
## Laravel Octane
Octane se puede instalar a través del gestor de paquetes Composer:
```console
composer require laravel/octane
```
Después de instalar Octane, puedes ejecutar el comando Artisan `octane:install`, que instalará el archivo de configuración de Octane en tu aplicación:
```console
php artisan octane:install --server=frankenphp
```
El servidor Octane se puede iniciar mediante el comando Artisan `octane:frankenphp`.
```console
php artisan octane:frankenphp
```
El comando `octane:frankenphp` puede tomar las siguientes opciones:
- `--host`: La dirección IP a la que el servidor debe enlazarse (por defecto: `127.0.0.1`)
- `--port`: El puerto en el que el servidor debe estar disponible (por defecto: `8000`)
- `--admin-port`: El puerto en el que el servidor de administración debe estar disponible (por defecto: `2019`)
- `--workers`: El número de workers que deben estar disponibles para manejar solicitudes (por defecto: `auto`)
- `--max-requests`: El número de solicitudes a procesar antes de recargar el servidor (por defecto: `500`)
- `--caddyfile`: La ruta al archivo `Caddyfile` de FrankenPHP (por defecto: [Caddyfile de plantilla en Laravel Octane](https://github.com/laravel/octane/blob/2.x/src/Commands/stubs/Caddyfile))
- `--https`: Habilita HTTPS, HTTP/2 y HTTP/3, y genera y renueva certificados automáticamente
- `--http-redirect`: Habilita la redirección de HTTP a HTTPS (solo se habilita si se pasa --https)
- `--watch`: Recarga automáticamente el servidor cuando se modifica la aplicación
- `--poll`: Usa la sondea del sistema de archivos mientras se observa para vigilar archivos a través de una red
- `--log-level`: Registra mensajes en o por encima del nivel de registro especificado, usando el registrador nativo de Caddy
> [!TIP]
> Para obtener registros JSON estructurados (útil al usar soluciones de análisis de registros), pasa explícitamente la opción `--log-level`.
Consulta también [cómo usar Mercure con Octane](#soporte-para-mercure).
Aprende más sobre [Laravel Octane en su documentación oficial](https://laravel.com/docs/octane).
## Aplicaciones Laravel como Binarios Autónomos
Usando [la característica de incrustación de aplicaciones de FrankenPHP](embed.md), es posible distribuir aplicaciones Laravel
como binarios autónomos.
Sigue estos pasos para empaquetar tu aplicación Laravel como un binario autónomo para Linux:
1. Crea un archivo llamado `static-build.Dockerfile` en el repositorio de tu aplicación:
```dockerfile
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder-gnu
# Si tienes intención de ejecutar el binario en sistemas musl-libc, usa static-builder-musl en su lugar
# Copia tu aplicación
WORKDIR /go/src/app/dist/app
COPY . .
# Elimina las pruebas y otros archivos innecesarios para ahorrar espacio
# Alternativamente, agrega estos archivos a un archivo .dockerignore
RUN rm -Rf tests/
# Copia el archivo .env
RUN cp .env.example .env
# Cambia APP_ENV y APP_DEBUG para que estén listos para producción
RUN sed -i'' -e 's/^APP_ENV=.*/APP_ENV=production/' -e 's/^APP_DEBUG=.*/APP_DEBUG=false/' .env
# Realiza otros cambios en tu archivo .env si es necesario
# Instala las dependencias
RUN composer install --ignore-platform-reqs --no-dev -a
# Compila el binario estático
WORKDIR /go/src/app/
RUN EMBED=dist/app/ ./build-static.sh
```
> [!CAUTION]
>
> Algunos archivos `.dockerignore`
> ignorarán el directorio `vendor/` y los archivos `.env`. Asegúrate de ajustar o eliminar el archivo `.dockerignore` antes de la compilación.
2. Compila:
```console
docker build -t static-laravel-app -f static-build.Dockerfile .
```
3. Extrae el binario:
```console
docker cp $(docker create --name static-laravel-app-tmp static-laravel-app):/go/src/app/dist/frankenphp-linux-x86_64 frankenphp ; docker rm static-laravel-app-tmp
```
4. Rellena las cachés:
```console
frankenphp php-cli artisan optimize
```
5. Ejecuta las migraciones de la base de datos (si las hay):
```console
frankenphp php-cli artisan migrate
```
6. Genera la clave secreta de la aplicación:
```console
frankenphp php-cli artisan key:generate
```
7. Inicia el servidor:
```console
frankenphp php-server
```
¡Tu aplicación ya está lista!
Aprende más sobre las opciones disponibles y cómo compilar binarios para otros sistemas operativos en la documentación de [incrustación de aplicaciones](embed.md).
### Cambiar la Ruta de Almacenamiento
Por defecto, Laravel almacena los archivos subidos, cachés, registros, etc. en el directorio `storage/` de la aplicación.
Esto no es adecuado para aplicaciones incrustadas, ya que cada nueva versión se extraerá en un directorio temporal diferente.
Establece la variable de entorno `LARAVEL_STORAGE_PATH` (por ejemplo, en tu archivo `.env`) o llama al método `Illuminate\Foundation\Application::useStoragePath()` para usar un directorio fuera del directorio temporal.
### Soporte para Mercure
[Mercure](https://mercure.rocks) es una excelente manera de agregar capacidades en tiempo real a tus aplicaciones Laravel.
FrankenPHP incluye [soporte para Mercure integrado](mercure.md).
Si no estás usando [Octane](#laravel-octane), consulta [la entrada de documentación de Mercure](mercure.md).
Si estás usando Octane, puedes habilitar el soporte para Mercure agregando las siguientes líneas a tu archivo `config/octane.php`:
```php
// ...
return [
// ...
'mercure' => [
'anonymous' => true,
'publisher_jwt' => '!CambiaEstaClaveSecretaJWTDelHubMercure!',
'subscriber_jwt' => '!CambiaEstaClaveSecretaJWTDelHubMercure!',
],
];
```
Puedes usar [todas las directivas soportadas por Mercure](https://mercure.rocks/docs/hub/config#directives) en este array.
Para publicar y suscribirte a actualizaciones, recomendamos usar la biblioteca [Laravel Mercure Broadcaster](https://github.com/mvanduijker/laravel-mercure-broadcaster).
Alternativamente, consulta [la documentación de Mercure](mercure.md) para hacerlo en PHP y JavaScript puros.
### Ejecutar Octane con Binarios Autónomos
¡Incluso es posible empaquetar aplicaciones Laravel Octane como binarios autónomos!
Para hacerlo, [instala Octane correctamente](#laravel-octane) y sigue los pasos descritos en [la sección anterior](#aplicaciones-laravel-como-binarios-autónomos).
Luego, para iniciar FrankenPHP en modo worker a través de Octane, ejecuta:
```console
PATH="$PWD:$PATH" frankenphp php-cli artisan octane:frankenphp
```
> [!CAUTION]
>
> Para que el comando funcione, el binario autónomo **debe** llamarse `frankenphp`
> porque Octane necesita un programa llamado `frankenphp` disponible en la ruta.

71
docs/es/logging.md Normal file
View File

@@ -0,0 +1,71 @@
# Registro de actividad
FrankenPHP se integra perfectamente con [el sistema de registro de Caddy](https://caddyserver.com/docs/logging).
Puede registrar mensajes usando funciones estándar de PHP o aprovechar la función dedicada `frankenphp_log()` para capacidades avanzadas de registro estructurado.
## `frankenphp_log()`
La función `frankenphp_log()` le permite emitir registros estructurados directamente desde su aplicación PHP,
facilitando la ingesta en plataformas como Datadog, Grafana Loki o Elastic, así como el soporte para OpenTelemetry.
Internamente, `frankenphp_log()` envuelve [el paquete `log/slog` de Go](https://pkg.go.dev/log/slog) para proporcionar funciones avanzadas de registro.
Estos registros incluyen el nivel de gravedad y datos de contexto opcionales.
```php
function frankenphp_log(string $message, int $level = FRANKENPHP_LOG_LEVEL_INFO, array $context = []): void
```
### Parámetros
- **`message`**: El string del mensaje de registro.
- **`level`**: El nivel de gravedad del registro. Puede ser cualquier entero arbitrario. Se proporcionan constantes de conveniencia para niveles comunes: `FRANKENPHP_LOG_LEVEL_DEBUG` (`-4`), `FRANKENPHP_LOG_LEVEL_INFO` (`0`), `FRANKENPHP_LOG_LEVEL_WARN` (`4`) y `FRANKENPHP_LOG_LEVEL_ERROR` (`8`)). Por omisión es `FRANKENPHP_LOG_LEVEL_INFO`.
- **`context`**: Un array asociativo de datos adicionales para incluir en la entrada del registro.
### Ejemplo
```php
<?php
// Registrar un mensaje informativo simple
frankenphp_log("¡Hola desde FrankenPHP!");
// Registrar una advertencia con datos de contexto
frankenphp_log(
"Uso de memoria alto",
FRANKENPHP_LOG_LEVEL_WARN,
[
'uso_actual' => memory_get_usage(),
'uso_pico' => memory_get_peak_usage(),
],
);
```
Al ver los registros (por ejemplo, mediante `docker compose logs`), la salida aparecerá como JSON estructurado:
```json
{"level":"info","ts":1704067200,"logger":"frankenphp","msg":"¡Hola desde FrankenPHP!"}
{"level":"warn","ts":1704067200,"logger":"frankenphp","msg":"Uso de memoria alto","uso_actual":10485760,"uso_pico":12582912}
```
## `error_log()`
FrankenPHP también permite el registro mediante la función estándar `error_log()`. Si el parámetro `$message_type` es `4` (SAPI),
estos mensajes se redirigen al registrador de Caddy.
Por omisión, los mensajes enviados a través de `error_log()` se tratan como texto no estructurado.
Son útiles para la compatibilidad con aplicaciones o bibliotecas existentes que dependen de la biblioteca estándar de PHP.
### Uso
```php
error_log("Fallo en la conexión a la base de datos", 4);
```
Esto aparecerá en los registros de Caddy, a menudo con un prefijo que indica que se originó desde PHP.
> [!TIP]
> Para una mejor observabilidad en entornos de producción, prefiera `frankenphp_log()`
> ya que permite filtrar registros por nivel (Depuración, Error, etc.)
> y consultar campos específicos en su infraestructura de registro.

151
docs/es/mercure.md Normal file
View File

@@ -0,0 +1,151 @@
# Tiempo Real
¡FrankenPHP incluye un hub [Mercure](https://mercure.rocks) integrado!
Mercure te permite enviar eventos en tiempo real a todos los dispositivos conectados: recibirán un evento JavaScript al instante.
¡Es una alternativa conveniente a WebSockets que es simple de usar y es soportada nativamente por todos los navegadores web modernos!
![Mercure](../mercure-hub.png)
## Habilitando Mercure
El soporte para Mercure está deshabilitado por defecto.
Aquí tienes un ejemplo mínimo de un `Caddyfile` que habilita tanto FrankenPHP como el hub Mercure:
```caddyfile
# El nombre de host al que responder
localhost
mercure {
# La clave secreta usada para firmar los tokens JWT para los publicadores
publisher_jwt !CambiaEstaClaveSecretaJWTDelHubMercure!
# Cuando se establece publisher_jwt, ¡también debes establecer subscriber_jwt!
subscriber_jwt !CambiaEstaClaveSecretaJWTDelHubMercure!
# Permite suscriptores anónimos (sin JWT)
anonymous
}
root public/
php_server
```
> [!TIP]
>
> El [`Caddyfile` de ejemplo](https://github.com/php/frankenphp/blob/main/caddy/frankenphp/Caddyfile)
> proporcionado por [las imágenes Docker](docker.md) ya incluye una configuración comentada de Mercure
> con variables de entorno convenientes para configurarlo.
>
> Descomenta la sección Mercure en `/etc/frankenphp/Caddyfile` para habilitarla.
## Suscribiéndose a Actualizaciones
Por defecto, el hub Mercure está disponible en la ruta `/.well-known/mercure` de tu servidor FrankenPHP.
Para suscribirte a actualizaciones, usa la clase nativa [`EventSource`](https://developer.mozilla.org/es/docs/Web/API/EventSource) de JavaScript:
```html
<!-- public/index.html -->
<!doctype html>
<title>Ejemplo Mercure</title>
<script>
const eventSource = new EventSource("/.well-known/mercure?topic=mi-tema");
eventSource.onmessage = function (event) {
console.log("Nuevo mensaje:", event.data);
};
</script>
```
## Publicando Actualizaciones
### Usando `mercure_publish()`
FrankenPHP proporciona una función conveniente `mercure_publish()` para publicar actualizaciones en el hub Mercure integrado:
```php
<?php
// public/publish.php
$updateID = mercure_publish('mi-tema', json_encode(['clave' => 'valor']));
// Escribir en los registros de FrankenPHP
error_log("actualización $updateID publicada", 4);
```
La firma completa de la función es:
```php
/**
* @param string|string[] $topics
*/
function mercure_publish(string|array $topics, string $data = '', bool $private = false, ?string $id = null, ?string $type = null, ?int $retry = null): string {}
```
### Usando `file_get_contents()`
Para enviar una actualización a los suscriptores conectados, envía una solicitud POST autenticada al hub Mercure con los parámetros `topic` y `data`:
```php
<?php
// public/publish.php
const JWT_SECRET = '!ChangeThisMercureHubJWTSecretKey!'; // Debe ser la misma que mercure.publisher_jwt en Caddyfile
$updateID = file_get_contents('https://localhost/.well-known/mercure', context: stream_context_create(['http' => [
'method' => 'POST',
'header' => "Content-type: application/x-www-form-urlencoded\r\nAuthorization: Bearer " . JWT,
'content' => http_build_query([
'topic' => 'mi-tema',
'data' => json_encode(['clave' => 'valor']),
]),
]]));
// Escribir en los registros de FrankenPHP
error_log("actualización $updateID publicada", 4);
```
La clave pasada como parámetro de la opción `mercure.publisher_jwt` en el `Caddyfile` debe usarse para firmar el token JWT usado en el encabezado `Authorization`.
El JWT debe incluir un reclamo `mercure` con un permiso `publish` para los temas a los que deseas publicar.
Consulta [la documentación de Mercure](https://mercure.rocks/spec#publishers) sobre autorización.
Para generar tus propios tokens, puedes usar [este enlace de jwt.io](https://www.jwt.io/#token=eyJhbGciOiJIUzI1NiJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdfX0.PXwpfIGng6KObfZlcOXvcnWCJOWTFLtswGI5DZuWSK4),
pero para aplicaciones en producción, se recomienda usar tokens de corta duración generados dinámicamente usando una biblioteca [JWT](https://www.jwt.io/libraries?programming_language=php) confiable.
### Usando Symfony Mercure
Alternativamente, puedes usar el [Componente Symfony Mercure](https://symfony.com/components/Mercure), una biblioteca PHP independiente.
Esta biblioteca maneja la generación de JWT, la publicación de actualizaciones así como la autorización basada en cookies para los suscriptores.
Primero, instala la biblioteca usando Composer:
```console
composer require symfony/mercure lcobucci/jwt
```
Luego, puedes usarla de la siguiente manera:
```php
<?php
// public/publish.php
require __DIR__ . '/../vendor/autoload.php';
const JWT_SECRET = '!CambiaEstaClaveSecretaJWTDelHubMercure!'; // Debe ser la misma que mercure.publisher_jwt en Caddyfile
// Configurar el proveedor de tokens JWT
$jwFactory = new \Symfony\Component\Mercure\Jwt\LcobucciFactory(JWT_SECRET);
$provider = new \Symfony\Component\Mercure\Jwt\FactoryTokenProvider($jwFactory, publish: ['*']);
$hub = new \Symfony\Component\Mercure\Hub('https://localhost/.well-known/mercure', $provider);
// Serializar la actualización y enviarla al hub, que la transmitirá a los clientes
$updateID = $hub->publish(new \Symfony\Component\Mercure\Update('mi-tema', json_encode(['clave' => 'valor'])));
// Escribir en los registros de FrankenPHP
error_log("actualización $updateID publicada", 4);
```
Mercure también es soportado nativamente por:
- [Laravel](laravel.md#soporte-para-mercure)
- [Symfony](https://symfony.com/doc/current/mercure.html)
- [API Platform](https://api-platform.com/docs/core/mercure/)

17
docs/es/metrics.md Normal file
View File

@@ -0,0 +1,17 @@
# Métricas
Cuando las [métricas de Caddy](https://caddyserver.com/docs/metrics) están habilitadas, FrankenPHP expone las siguientes métricas:
- `frankenphp_total_threads`: El número total de hilos PHP.
- `frankenphp_busy_threads`: El número de hilos PHP procesando actualmente una solicitud (los workers en ejecución siempre consumen un hilo).
- `frankenphp_queue_depth`: El número de solicitudes regulares en cola
- `frankenphp_total_workers{worker="[nombre_worker]"}`: El número total de workers.
- `frankenphp_busy_workers{worker="[nombre_worker]"}`: El número de workers procesando actualmente una solicitud.
- `frankenphp_worker_request_time{worker="[nombre_worker]"}`: El tiempo dedicado al procesamiento de solicitudes por todos los workers.
- `frankenphp_worker_request_count{worker="[nombre_worker]"}`: El número de solicitudes procesadas por todos los workers.
- `frankenphp_ready_workers{worker="[nombre_worker]"}`: El número de workers que han llamado a `frankenphp_handle_request` al menos una vez.
- `frankenphp_worker_crashes{worker="[nombre_worker]"}`: El número de veces que un worker ha terminado inesperadamente.
- `frankenphp_worker_restarts{worker="[nombre_worker]"}`: El número de veces que un worker ha sido reiniciado deliberadamente.
- `frankenphp_worker_queue_depth{worker="[nombre_worker]"}`: El número de solicitudes en cola.
Para las métricas de los workers, el marcador de posición `[nombre_worker]` es reemplazado por el nombre del worker en el Caddyfile; de lo contrario, se usará la ruta absoluta del archivo del worker.

187
docs/es/performance.md Normal file
View File

@@ -0,0 +1,187 @@
# Rendimiento
Por defecto, FrankenPHP intenta ofrecer un buen compromiso entre rendimiento y facilidad de uso.
Sin embargo, es posible mejorar sustancialmente el rendimiento usando una configuración adecuada.
## Número de Hilos y Workers
Por defecto, FrankenPHP inicia 2 veces más hilos y workers (en modo worker) que el número de CPUs disponibles.
Los valores apropiados dependen en gran medida de cómo está escrita tu aplicación, qué hace y tu hardware.
Recomendamos encarecidamente cambiar estos valores. Para una mejor estabilidad del sistema, se recomienda que `num_threads` x `memory_limit` < `memoria_disponible`.
Para encontrar los valores correctos, es mejor ejecutar pruebas de carga que simulen tráfico real.
[k6](https://k6.io) y [Gatling](https://gatling.io) son buenas herramientas para esto.
Para configurar el número de hilos, usa la opción `num_threads` de las directivas `php_server` y `php`.
Para cambiar el número de workers, usa la opción `num` de la sección `worker` de la directiva `frankenphp`.
### `max_threads`
Aunque siempre es mejor saber exactamente cómo será tu tráfico, las aplicaciones reales tienden a ser más impredecibles.
La configuración `max_threads` [configuración](config.md#caddyfile-config) permite a FrankenPHP generar automáticamente hilos adicionales en tiempo de ejecución hasta el límite especificado.
`max_threads` puede ayudarte a determinar cuántos hilos necesitas para manejar tu tráfico y puede hacer que el servidor sea más resiliente a picos de latencia.
Si se establece en `auto`, el límite se estimará en función del `memory_limit` en tu `php.ini`. Si no puede hacerlo,
`auto` se establecerá por defecto en 2x `num_threads`. Ten en cuenta que `auto` puede subestimar fuertemente el número de hilos necesarios.
`max_threads` es similar a [pm.max_children](https://www.php.net/manual/es/install.fpm.configuration.php#pm.max-children) de PHP FPM. La principal diferencia es que FrankenPHP usa hilos en lugar de procesos y los delega automáticamente entre diferentes scripts de worker y el 'modo clásico' según sea necesario.
## Modo Worker
Habilitar [el modo worker](worker.md) mejora drásticamente el rendimiento,
pero tu aplicación debe adaptarse para ser compatible con este modo:
debes crear un script de worker y asegurarte de que la aplicación no tenga fugas de memoria.
## No Usar musl
La variante Alpine Linux de las imágenes Docker oficiales y los binarios predeterminados que proporcionamos usan [la libc musl](https://musl.libc.org).
Se sabe que PHP es [más lento](https://gitlab.alpinelinux.org/alpine/aports/-/issues/14381) cuando usa esta biblioteca C alternativa en lugar de la biblioteca GNU tradicional,
especialmente cuando se compila en modo ZTS (thread-safe), que es requerido para FrankenPHP. La diferencia puede ser significativa en un entorno con muchos hilos.
Además, [algunos errores solo ocurren cuando se usa musl](https://github.com/php/php-src/issues?q=sort%3Aupdated-desc+is%3Aissue+is%3Aopen+label%3ABug+musl).
En entornos de producción, recomendamos usar FrankenPHP vinculado a glibc, compilado con un nivel de optimización adecuado.
Esto se puede lograr usando las imágenes Docker de Debian, usando los paquetes de nuestros mantenedores [.deb](https://debs.henderkes.com) o [.rpm](https://rpms.henderkes.com), o [compilando FrankenPHP desde las fuentes](compile.md).
## Configuración del Runtime de Go
FrankenPHP está escrito en Go.
En general, el runtime de Go no requiere ninguna configuración especial, pero en ciertas circunstancias,
una configuración específica mejora el rendimiento.
Probablemente quieras establecer la variable de entorno `GODEBUG` en `cgocheck=0` (el valor predeterminado en las imágenes Docker de FrankenPHP).
Si ejecutas FrankenPHP en contenedores (Docker, Kubernetes, LXC...) y limitas la memoria disponible para los contenedores,
establece la variable de entorno `GOMEMLIMIT` en la cantidad de memoria disponible.
Para más detalles, [la página de documentación de Go dedicada a este tema](https://pkg.go.dev/runtime#hdr-Environment_Variables) es una lectura obligada para aprovechar al máximo el runtime.
## `file_server`
Por defecto, la directiva `php_server` configura automáticamente un servidor de archivos para
servir archivos estáticos (assets) almacenados en el directorio raíz.
Esta característica es conveniente, pero tiene un costo.
Para deshabilitarla, usa la siguiente configuración:
```caddyfile
php_server {
file_server off
}
```
## `try_files`
Además de los archivos estáticos y los archivos PHP, `php_server` también intentará servir los archivos de índice de tu aplicación
y los índices de directorio (`/ruta/` -> `/ruta/index.php`). Si no necesitas índices de directorio,
puedes deshabilitarlos definiendo explícitamente `try_files` de esta manera:
```caddyfile
php_server {
try_files {path} index.php
root /ruta/a/tu/app # agregar explícitamente la raíz aquí permite un mejor almacenamiento en caché
}
```
Esto puede reducir significativamente el número de operaciones de archivo innecesarias.
Un enfoque alternativo con 0 operaciones innecesarias de sistema de archivos sería usar en su lugar la directiva `php` y separar
los archivos de PHP por ruta. Este enfoque funciona bien si toda tu aplicación es servida por un solo archivo de entrada.
Un ejemplo de [configuración](config.md#caddyfile-config) que sirve archivos estáticos detrás de una carpeta `/assets` podría verse así:
```caddyfile
route {
@assets {
path /assets/*
}
# todo lo que está detrás de /assets es manejado por el servidor de archivos
file_server @assets {
root /ruta/a/tu/app
}
# todo lo que no está en /assets es manejado por tu archivo index o worker PHP
rewrite index.php
php {
root /ruta/a/tu/app # agregar explícitamente la raíz aquí permite un mejor almacenamiento en caché
}
}
```
## Marcadores de Posición (Placeholders)
Puedes usar [marcadores de posición](https://caddyserver.com/docs/conventions#placeholders) en las directivas `root` y `env`.
Sin embargo, esto evita el almacenamiento en caché de estos valores y conlleva un costo significativo de rendimiento.
Si es posible, evita los marcadores de posición en estas directivas.
## `resolve_root_symlink`
Por defecto, si la raíz del documento es un enlace simbólico, se resuelve automáticamente por FrankenPHP (esto es necesario para que PHP funcione correctamente).
Si la raíz del documento no es un enlace simbólico, puedes deshabilitar esta característica.
```caddyfile
php_server {
resolve_root_symlink false
}
```
Esto mejorará el rendimiento si la directiva `root` contiene [marcadores de posición](https://caddyserver.com/docs/conventions#placeholders).
La ganancia será negligible en otros casos.
## Registros (Logs)
El registro es obviamente muy útil, pero, por definición,
requiere operaciones de E/S y asignaciones de memoria, lo que reduce considerablemente el rendimiento.
Asegúrate de [establecer el nivel de registro](https://caddyserver.com/docs/caddyfile/options#log) correctamente,
y registra solo lo necesario.
## Rendimiento de PHP
FrankenPHP usa el intérprete oficial de PHP.
Todas las optimizaciones de rendimiento habituales relacionadas con PHP se aplican con FrankenPHP.
En particular:
- verifica que [OPcache](https://www.php.net/manual/es/book.opcache.php) esté instalado, habilitado y correctamente configurado
- habilita [optimizaciones del autoload de Composer](https://getcomposer.org/doc/articles/autoloader-optimization.md)
- asegúrate de que la caché `realpath` sea lo suficientemente grande para las necesidades de tu aplicación
- usa [preloading](https://www.php.net/manual/es/opcache.preloading.php)
Para más detalles, lee [la entrada de documentación dedicada de Symfony](https://symfony.com/doc/current/performance.html)
(la mayoría de los consejos son útiles incluso si no usas Symfony).
## Dividiendo el Pool de Hilos
Es común que las aplicaciones interactúen con servicios externos lentos, como una
API que tiende a ser poco confiable bajo alta carga o que consistentemente tarda 10+ segundos en responder.
En tales casos, puede ser beneficioso dividir el pool de hilos para tener pools "lentos" dedicados.
Esto evita que los endpoints lentos consuman todos los recursos/hilos del servidor y
limita la concurrencia de solicitudes hacia el endpoint lento, similar a un
pool de conexiones.
```caddyfile
{
frankenphp {
max_threads 100 # máximo 100 hilos compartidos por todos los workers
}
}
ejemplo.com {
php_server {
root /app/public # la raíz de tu aplicación
worker index.php {
match /endpoint-lento/* # todas las solicitudes con la ruta /endpoint-lento/* son manejadas por este pool de hilos
num 10 # mínimo 10 hilos para solicitudes que coincidan con /endpoint-lento/*
}
worker index.php {
match * # todas las demás solicitudes son manejadas por separado
num 20 # mínimo 20 hilos para otras solicitudes, incluso si los endpoints lentos comienzan a colgarse
}
}
}
```
En general, también es aconsejable manejar endpoints muy lentos de manera asíncrona, utilizando mecanismos relevantes como colas de mensajes.

144
docs/es/production.md Normal file
View File

@@ -0,0 +1,144 @@
# Despliegue en Producción
En este tutorial, aprenderemos cómo desplegar una aplicación PHP en un único servidor usando Docker Compose.
Si estás usando Symfony, consulta la documentación "[Despliegue en producción](https://github.com/dunglas/symfony-docker/blob/main/docs/production.md)" del proyecto Symfony Docker (que usa FrankenPHP).
Si estás usando API Platform (que también usa FrankenPHP), consulta [la documentación de despliegue del framework](https://api-platform.com/docs/deployment/).
## Preparando tu Aplicación
Primero, crea un archivo `Dockerfile` en el directorio raíz de tu proyecto PHP:
```dockerfile
FROM dunglas/frankenphp
# Asegúrate de reemplazar "tu-dominio.ejemplo.com" por tu nombre de dominio
ENV SERVER_NAME=tu-dominio.ejemplo.com
# Si quieres deshabilitar HTTPS, usa este valor en su lugar:
#ENV SERVER_NAME=:80
# Si tu proyecto no usa el directorio "public" como raíz web, puedes establecerlo aquí:
# ENV SERVER_ROOT=web/
# Habilitar configuración de producción de PHP
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
# Copiar los archivos PHP de tu proyecto en el directorio público
COPY . /app/public
# Si usas Symfony o Laravel, necesitas copiar todo el proyecto en su lugar:
#COPY . /app
```
Consulta "[Construyendo una Imagen Docker Personalizada](docker.md)" para más detalles y opciones,
y para aprender cómo personalizar la configuración, instalar extensiones PHP y módulos de Caddy.
Si tu proyecto usa Composer,
asegúrate de incluirlo en la imagen Docker e instalar tus dependencias.
Luego, agrega un archivo `compose.yaml`:
```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
# Volúmenes necesarios para los certificados y configuración de Caddy
volumes:
caddy_data:
caddy_config:
```
> [!NOTE]
>
> Los ejemplos anteriores están destinados a uso en producción.
> En desarrollo, es posible que desees usar un volumen, una configuración PHP diferente y un valor diferente para la variable de entorno `SERVER_NAME`.
>
> Consulta el proyecto [Symfony Docker](https://github.com/dunglas/symfony-docker)
> (que usa FrankenPHP) para un ejemplo más avanzado usando imágenes multi-etapa,
> Composer, extensiones PHP adicionales, etc.
Finalmente, si usas Git, haz commit de estos archivos y haz push.
## Preparando un Servidor
Para desplegar tu aplicación en producción, necesitas un servidor.
En este tutorial, usaremos una máquina virtual proporcionada por DigitalOcean, pero cualquier servidor Linux puede funcionar.
Si ya tienes un servidor Linux con Docker instalado, puedes saltar directamente a [la siguiente sección](#configurando-un-nombre-de-dominio).
De lo contrario, usa [este enlace de afiliado](https://m.do.co/c/5d8aabe3ab80) para obtener $200 de crédito gratuito, crea una cuenta y luego haz clic en "Crear un Droplet".
Luego, haz clic en la pestaña "Marketplace" bajo la sección "Elegir una imagen" y busca la aplicación llamada "Docker".
Esto aprovisionará un servidor Ubuntu con las últimas versiones de Docker y Docker Compose ya instaladas.
Para fines de prueba, los planes más económicos serán suficientes.
Para un uso real en producción, probablemente querrás elegir un plan en la sección "uso general" que se adapte a tus necesidades.
![Desplegando FrankenPHP en DigitalOcean con Docker](digitalocean-droplet.png)
Puedes mantener los valores predeterminados para otras configuraciones o ajustarlos según tus necesidades.
No olvides agregar tu clave SSH o crear una contraseña y luego presionar el botón "Finalizar y crear".
Luego, espera unos segundos mientras se aprovisiona tu Droplet.
Cuando tu Droplet esté listo, usa SSH para conectarte:
```console
ssh root@<ip-del-droplet>
```
## Configurando un Nombre de Dominio
En la mayoría de los casos, querrás asociar un nombre de dominio a tu sitio.
Si aún no tienes un nombre de dominio, deberás comprar uno a través de un registrador.
Luego, crea un registro DNS de tipo `A` para tu nombre de dominio que apunte a la dirección IP de tu servidor:
```dns
tu-dominio.ejemplo.com. IN A 207.154.233.113
```
Ejemplo con el servicio de Dominios de DigitalOcean ("Redes" > "Dominios"):
![Configurando DNS en DigitalOcean](../digitalocean-dns.png)
> [!NOTE]
>
> Let's Encrypt, el servicio utilizado por defecto por FrankenPHP para generar automáticamente un certificado TLS, no soporta el uso de direcciones IP puras. Usar un nombre de dominio es obligatorio para usar Let's Encrypt.
## Despliegue
Copia tu proyecto en el servidor usando `git clone`, `scp` o cualquier otra herramienta que se ajuste a tus necesidades.
Si usas GitHub, es posible que desees usar [una clave de despliegue](https://docs.github.com/es/free-pro-team@latest/developers/overview/managing-deploy-keys#deploy-keys).
Las claves de despliegue también son [soportadas por GitLab](https://docs.gitlab.com/ee/user/project/deploy_keys/).
Ejemplo con Git:
```console
git clone git@github.com:<usuario>/<nombre-proyecto>.git
```
Ve al directorio que contiene tu proyecto (`<nombre-proyecto>`) e inicia la aplicación en modo producción:
```console
docker compose up --wait
```
Tu servidor está en funcionamiento y se ha generado automáticamente un certificado HTTPS para ti.
Ve a `https://tu-dominio.ejemplo.com` y ¡disfruta!
> [!CAUTION]
>
> Docker puede tener una capa de caché, asegúrate de tener la compilación correcta para cada despliegue o vuelve a compilar tu proyecto con la opción `--no-cache` para evitar problemas de caché.
## Despliegue en Múltiples Nodos
Si deseas desplegar tu aplicación en un clúster de máquinas, puedes usar [Docker Swarm](https://docs.docker.com/engine/swarm/stack-deploy/),
que es compatible con los archivos Compose proporcionados.
Para desplegar en Kubernetes, consulta [el gráfico Helm proporcionado con API Platform](https://api-platform.com/docs/deployment/kubernetes/), que usa FrankenPHP.

160
docs/es/static.md Normal file
View File

@@ -0,0 +1,160 @@
# Crear una Compilación Estática
En lugar de usar una instalación local de la biblioteca PHP,
es posible crear una compilación estática o mayormente estática de FrankenPHP gracias al excelente [proyecto static-php-cli](https://github.com/crazywhalecc/static-php-cli) (a pesar de su nombre, este proyecto soporta todas las SAPI, no solo CLI).
Con este método, un único binario portátil contendrá el intérprete de PHP, el servidor web Caddy y FrankenPHP.
Los ejecutables nativos completamente estáticos no requieren dependencias y pueden ejecutarse incluso en la imagen Docker [`scratch`](https://docs.docker.com/build/building/base-images/#create-a-minimal-base-image-using-scratch).
Sin embargo, no pueden cargar extensiones PHP dinámicas (como Xdebug) y tienen algunas limitaciones porque usan la libc musl.
Los binarios mayormente estáticos solo requieren `glibc` y pueden cargar extensiones dinámicas.
Cuando sea posible, recomendamos usar compilaciones mayormente estáticas basadas en glibc.
FrankenPHP también soporta [incrustar la aplicación PHP en el binario estático](embed.md).
## Linux
Proporcionamos imágenes Docker para compilar binarios Linux estáticos:
### Compilación Completamente Estática Basada en musl
Para un binario completamente estático que se ejecuta en cualquier distribución Linux sin dependencias pero que no soporta carga dinámica de extensiones:
```console
docker buildx bake --load static-builder-musl
docker cp $(docker create --name static-builder-musl dunglas/frankenphp:static-builder-musl):/go/src/app/dist/frankenphp-linux-$(uname -m) frankenphp ; docker rm static-builder-musl
```
Para un mejor rendimiento en escenarios altamente concurrentes, considera usar el asignador [mimalloc](https://github.com/microsoft/mimalloc).
```console
docker buildx bake --load --set static-builder-musl.args.MIMALLOC=1 static-builder-musl
```
### Compilación Mayormente Estática Basada en glibc (Con Soporte para Extensiones Dinámicas)
Para un binario que soporta la carga dinámica de extensiones PHP mientras tiene las extensiones seleccionadas compiladas estáticamente:
```console
docker buildx bake --load static-builder-gnu
docker cp $(docker create --name static-builder-gnu dunglas/frankenphp:static-builder-gnu):/go/src/app/dist/frankenphp-linux-$(uname -m) frankenphp ; docker rm static-builder-gnu
```
Este binario soporta todas las versiones de glibc 2.17 y superiores, pero no se ejecuta en sistemas basados en musl (como Alpine Linux).
El binario resultante (mayormente estático excepto por `glibc`) se llama `frankenphp` y está disponible en el directorio actual.
Si deseas compilar el binario estático sin Docker, consulta las instrucciones para macOS, que también funcionan para Linux.
### Extensiones Personalizadas
Por omisión, se compilan las extensiones PHP más populares.
Para reducir el tamaño del binario y disminuir la superficie de ataque, puedes elegir la lista de extensiones a compilar usando el ARG de Docker `PHP_EXTENSIONS`.
Por ejemplo, ejecuta el siguiente comando para compilar solo la extensión `opcache`:
```console
docker buildx bake --load --set static-builder-musl.args.PHP_EXTENSIONS=opcache,pdo_sqlite static-builder-musl
# ...
```
Para agregar bibliotecas que habiliten funcionalidades adicionales a las extensiones que has habilitado, puedes pasar el ARG de Docker `PHP_EXTENSION_LIBS`:
```console
docker buildx bake \
--load \
--set static-builder-musl.args.PHP_EXTENSIONS=gd \
--set static-builder-musl.args.PHP_EXTENSION_LIBS=libjpeg,libwebp \
static-builder-musl
```
### Módulos Adicionales de Caddy
Para agregar módulos adicionales de Caddy o pasar otros argumentos a [xcaddy](https://github.com/caddyserver/xcaddy), usa el ARG de Docker `XCADDY_ARGS`:
```console
docker buildx bake \
--load \
--set static-builder-musl.args.XCADDY_ARGS="--with github.com/darkweak/souin/plugins/caddy --with github.com/dunglas/caddy-cbrotli --with github.com/dunglas/mercure/caddy --with github.com/dunglas/vulcain/caddy" \
static-builder-musl
```
En este ejemplo, agregamos el módulo de caché HTTP [Souin](https://souin.io) para Caddy, así como los módulos [cbrotli](https://github.com/dunglas/caddy-cbrotli), [Mercure](https://mercure.rocks) y [Vulcain](https://vulcain.rocks).
> [!TIP]
>
> Los módulos cbrotli, Mercure y Vulcain están incluidos por omisión si `XCADDY_ARGS` está vacío o no está configurado.
> Si personalizas el valor de `XCADDY_ARGS`, debes incluirlos explícitamente si deseas que estén incluidos.
Consulta también cómo [personalizar la compilación](#personalizando-la-compilación).
### Token de GitHub
Si alcanzas el límite de tasa de la API de GitHub, establece un Token de Acceso Personal de GitHub en una variable de entorno llamada `GITHUB_TOKEN`:
```console
GITHUB_TOKEN="xxx" docker --load buildx bake static-builder-musl
# ...
```
## macOS
Ejecuta el siguiente script para crear un binario estático para macOS (debes tener [Homebrew](https://brew.sh/) instalado):
```console
git clone https://github.com/php/frankenphp
cd frankenphp
./build-static.sh
```
Nota: este script también funciona en Linux (y probablemente en otros Unix) y es usado internamente por las imágenes Docker que proporcionamos.
## Personalizando la Compilación
Las siguientes variables de entorno pueden pasarse a `docker build` y al script `build-static.sh` para personalizar la compilación estática:
- `FRANKENPHP_VERSION`: la versión de FrankenPHP a usar
- `PHP_VERSION`: la versión de PHP a usar
- `PHP_EXTENSIONS`: las extensiones PHP a compilar ([lista de extensiones soportadas](https://static-php.dev/en/guide/extensions.html))
- `PHP_EXTENSION_LIBS`: bibliotecas adicionales a compilar que añaden funcionalidades a las extensiones
- `XCADDY_ARGS`: argumentos a pasar a [xcaddy](https://github.com/caddyserver/xcaddy), por ejemplo para agregar módulos adicionales de Caddy
- `EMBED`: ruta de la aplicación PHP a incrustar en el binario
- `CLEAN`: cuando está establecido, libphp y todas sus dependencias se compilan desde cero (sin caché)
- `NO_COMPRESS`: no comprimir el binario resultante usando UPX
- `DEBUG_SYMBOLS`: cuando está establecido, los símbolos de depuración no se eliminarán y se añadirán al binario
- `MIMALLOC`: (experimental, solo Linux) reemplaza mallocng de musl por [mimalloc](https://github.com/microsoft/mimalloc) para mejorar el rendimiento. Solo recomendamos usar esto para compilaciones orientadas a musl; para glibc, preferimos deshabilitar esta opción y usar [`LD_PRELOAD`](https://microsoft.github.io/mimalloc/overrides.html) cuando ejecutes tu binario.
- `RELEASE`: (solo para mantenedores) cuando está establecido, el binario resultante se subirá a GitHub
## Extensiones
Con los binarios basados en glibc o macOS, puedes cargar extensiones PHP dinámicamente. Sin embargo, estas extensiones deberán ser compiladas con soporte ZTS.
Dado que la mayoría de los gestores de paquetes no ofrecen actualmente versiones ZTS de sus extensiones, tendrás que compilarlas tú mismo.
Para esto, puedes compilar y ejecutar el contenedor Docker `static-builder-gnu`, acceder a él y compilar las extensiones con `./configure --with-php-config=/go/src/app/dist/static-php-cli/buildroot/bin/php-config`.
Pasos de ejemplo para [la extensión Xdebug](https://xdebug.org):
```console
docker build -t gnu-ext -f static-builder-gnu.Dockerfile --build-arg FRANKENPHP_VERSION=1.0 .
docker create --name static-builder-gnu -it gnu-ext /bin/sh
docker start static-builder-gnu
docker exec -it static-builder-gnu /bin/sh
cd /go/src/app/dist/static-php-cli/buildroot/bin
git clone https://github.com/xdebug/xdebug.git && cd xdebug
source scl_source enable devtoolset-10
../phpize
./configure --with-php-config=/go/src/app/dist/static-php-cli/buildroot/bin/php-config
make
exit
docker cp static-builder-gnu:/go/src/app/dist/static-php-cli/buildroot/bin/xdebug/modules/xdebug.so xdebug-zts.so
docker cp static-builder-gnu:/go/src/app/dist/frankenphp-linux-$(uname -m) ./frankenphp
docker stop static-builder-gnu
docker rm static-builder-gnu
docker rmi gnu-ext
```
Esto creará `frankenphp` y `xdebug-zts.so` en el directorio actual.
Si mueves `xdebug-zts.so` a tu directorio de extensiones, agrega `zend_extension=xdebug-zts.so` a tu php.ini y ejecuta FrankenPHP, cargará Xdebug.

59
docs/es/wordpress.md Normal file
View File

@@ -0,0 +1,59 @@
# WordPress
Ejecute [WordPress](https://wordpress.org/) con FrankenPHP para disfrutar de una pila moderna y de alto rendimiento con HTTPS automático, HTTP/3 y compresión Zstandard.
## Instalación Mínima
1. [Descargue WordPress](https://wordpress.org/download/)
2. Extraiga el archivo ZIP y abra una terminal en el directorio extraído
3. Ejecute:
```console
frankenphp php-server
```
4. Vaya a `http://localhost/wp-admin/` y siga las instrucciones de instalación
5. ¡Listo!
Para una configuración lista para producción, prefiera usar `frankenphp run` con un `Caddyfile` como este:
```caddyfile
example.com
php_server
encode zstd br gzip
log
```
## Hot Reload
Para usar la función de [Hot reload](hot-reload.md) con WordPress, active [Mercure](mercure.md) y agregue la subdirectiva `hot_reload` a la directiva `php_server` en su `Caddyfile`:
```caddyfile
localhost
mercure {
anonymous
}
php_server {
hot_reload
}
```
Luego, agregue el código necesario para cargar las bibliotecas JavaScript en el archivo `functions.php` de su tema de WordPress:
```php
function hot_reload() {
?>
<?php if (isset($_SERVER['FRANKENPHP_HOT_RELOAD'])): ?>
<meta name="frankenphp-hot-reload:url" content="<?=$_SERVER['FRANKENPHP_HOT_RELOAD']?>">
<script src="https://cdn.jsdelivr.net/npm/idiomorph"></script>
<script src="https://cdn.jsdelivr.net/npm/frankenphp-hot-reload/+esm" type="module"></script>
<?php endif ?>
<?php
}
add_action('wp_head', 'hot_reload');
```
Finalmente, ejecute `frankenphp run` desde el directorio raíz de WordPress.

192
docs/es/worker.md Normal file
View File

@@ -0,0 +1,192 @@
# Usando los Workers de FrankenPHP
Inicia tu aplicación una vez y manténla en memoria.
FrankenPHP gestionará las peticiones entrantes en unos pocos milisegundos.
## Iniciando Scripts de Worker
### Docker
Establece el valor de la variable de entorno `FRANKENPHP_CONFIG` a `worker /ruta/a/tu/script/worker.php`:
```console
docker run \
-e FRANKENPHP_CONFIG="worker /app/ruta/a/tu/script/worker.php" \
-v $PWD:/app \
-p 80:80 -p 443:443 -p 443:443/udp \
dunglas/frankenphp
```
### Binario Autónomo
Usa la opción `--worker` del comando `php-server` para servir el contenido del directorio actual usando un worker:
```console
frankenphp php-server --worker /ruta/a/tu/script/worker.php
```
Si tu aplicación PHP está [incrustada en el binario](embed.md), puedes agregar un `Caddyfile` personalizado en el directorio raíz de la aplicación.
Será usado automáticamente.
También es posible [reiniciar el worker al detectar cambios en archivos](config.md#watching-for-file-changes) con la opción `--watch`.
El siguiente comando activará un reinicio si algún archivo que termine en `.php` en el directorio `/ruta/a/tu/app/` o sus subdirectorios es modificado:
```console
frankenphp php-server --worker /ruta/a/tu/script/worker.php --watch="/ruta/a/tu/app/**/*.php"
```
Esta función se utiliza frecuentemente en combinación con [hot reloading](hot-reload.md).
## Symfony Runtime
> [!TIP]
> La siguiente sección es necesaria solo para versiones anteriores a Symfony 7.4, donde se introdujo soporte nativo para el modo worker de FrankenPHP.
El modo worker de FrankenPHP es soportado por el [Componente Runtime de Symfony](https://symfony.com/doc/current/components/runtime.html).
Para iniciar cualquier aplicación Symfony en un worker, instala el paquete FrankenPHP de [PHP Runtime](https://github.com/php-runtime/runtime):
```console
composer require runtime/frankenphp-symfony
```
Inicia tu servidor de aplicación definiendo la variable de entorno `APP_RUNTIME` para usar el Runtime de FrankenPHP Symfony:
```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
Consulta [la documentación dedicada](laravel.md#laravel-octane).
## Aplicaciones Personalizadas
El siguiente ejemplo muestra cómo crear tu propio script de worker sin depender de una biblioteca de terceros:
```php
<?php
// public/index.php
// Prevenir la terminación del script de worker cuando una conexión de cliente se interrumpe
ignore_user_abort(true);
// Iniciar tu aplicación
require __DIR__.'/vendor/autoload.php';
$myApp = new \App\Kernel();
$myApp->boot();
// Manejador fuera del bucle para mejor rendimiento (menos trabajo)
$handler = static function () use ($myApp) {
try {
// Llamado cuando se recibe una petición,
// las superglobales, php://input y similares se reinician
echo $myApp->handle($_GET, $_POST, $_COOKIE, $_FILES, $_SERVER);
} catch (\Throwable $exception) {
// `set_exception_handler` se llama solo cuando el script de worker termina,
// lo cual puede no ser lo que esperas, así que captura y maneja excepciones aquí
(new \MyCustomExceptionHandler)->handleException($exception);
}
};
$maxRequests = (int)($_SERVER['MAX_REQUESTS'] ?? 0);
for ($nbRequests = 0; !$maxRequests || $nbRequests < $maxRequests; ++$nbRequests) {
$keepRunning = \frankenphp_handle_request($handler);
// Haz algo después de enviar la respuesta HTTP
$myApp->terminate();
// Llama al recolector de basura para reducir las posibilidades de que se active en medio de la generación de una página
gc_collect_cycles();
if (!$keepRunning) break;
}
// Limpieza
$myApp->shutdown();
```
Luego, inicia tu aplicación y usa la variable de entorno `FRANKENPHP_CONFIG` para configurar tu worker:
```console
docker run \
-e FRANKENPHP_CONFIG="worker ./public/index.php" \
-v $PWD:/app \
-p 80:80 -p 443:443 -p 443:443/udp \
dunglas/frankenphp
```
Por omisión, se inician 2 workers por CPU.
También puedes configurar el número de workers a iniciar:
```console
docker run \
-e FRANKENPHP_CONFIG="worker ./public/index.php 42" \
-v $PWD:/app \
-p 80:80 -p 443:443 -p 443:443/udp \
dunglas/frankenphp
```
### Reiniciar el Worker Después de un Número Determinado de Peticiones
Como PHP no fue diseñado originalmente para procesos de larga duración, aún hay muchas bibliotecas y códigos heredados que generan fugas de memoria.
Una solución para usar este tipo de código en modo worker es reiniciar el script de worker después de procesar un cierto número de peticiones:
El fragmento de worker anterior permite configurar un número máximo de peticiones a manejar estableciendo una variable de entorno llamada `MAX_REQUESTS`.
### Reiniciar Workers Manualmente
Aunque es posible reiniciar workers [al detectar cambios en archivos](config.md#watching-for-file-changes), también es posible reiniciar todos los workers
de manera controlada a través de la [API de administración de Caddy](https://caddyserver.com/docs/api). Si el admin está habilitado en tu
[Caddyfile](config.md#caddyfile-config), puedes activar el endpoint de reinicio con una simple petición POST como esta:
```console
curl -X POST http://localhost:2019/frankenphp/workers/restart
```
### Fallos en Workers
Si un script de worker falla con un código de salida distinto de cero, FrankenPHP lo reiniciará con una estrategia de retroceso exponencial.
Si el script de worker permanece activo más tiempo que el último retroceso * 2,
no penalizará al script de worker y lo reiniciará nuevamente.
Sin embargo, si el script de worker continúa fallando con un código de salida distinto de cero en un corto período de tiempo
(por ejemplo, tener un error tipográfico en un script), FrankenPHP fallará con el error: `too many consecutive failures`.
El número de fallos consecutivos puede configurarse en tu [Caddyfile](config.md#caddyfile-config) con la opción `max_consecutive_failures`:
```caddyfile
frankenphp {
worker {
# ...
max_consecutive_failures 10
}
}
```
## Comportamiento de las Superglobales
Las [superglobales de PHP](https://www.php.net/manual/es/language.variables.superglobals.php) (`$_SERVER`, `$_ENV`, `$_GET`...)
se comportan de la siguiente manera:
- antes de la primera llamada a `frankenphp_handle_request()`, las superglobales contienen valores vinculados al script de worker en sí
- durante y después de la llamada a `frankenphp_handle_request()`, las superglobales contienen valores generados a partir de la petición HTTP procesada; cada llamada a `frankenphp_handle_request()` cambia los valores de las superglobales
Para acceder a las superglobales del script de worker dentro de la retrollamada, debes copiarlas e importar la copia en el ámbito de la retrollamada:
```php
<?php
// Copia la superglobal $_SERVER del worker antes de la primera llamada a frankenphp_handle_request()
$workerServer = $_SERVER;
$handler = static function () use ($workerServer) {
var_dump($_SERVER); // $_SERVER vinculado a la petición
var_dump($workerServer); // $_SERVER del script de worker
};
// ...
```

71
docs/es/x-sendfile.md Normal file
View File

@@ -0,0 +1,71 @@
# Sirviendo archivos estáticos grandes de manera eficiente (`X-Sendfile`/`X-Accel-Redirect`)
Normalmente, los archivos estáticos pueden ser servidos directamente por el servidor web,
pero a veces es necesario ejecutar código PHP antes de enviarlos:
control de acceso, estadísticas, encabezados HTTP personalizados...
Desafortunadamente, usar PHP para servir archivos estáticos grandes es ineficiente en comparación con
el uso directo del servidor web (sobrecarga de memoria, rendimiento reducido...).
FrankenPHP permite delegar el envío de archivos estáticos al servidor web
**después** de ejecutar código PHP personalizado.
Para hacerlo, tu aplicación PHP simplemente necesita definir un encabezado HTTP personalizado
que contenga la ruta del archivo a servir. FrankenPHP se encarga del resto.
Esta funcionalidad es conocida como **`X-Sendfile`** para Apache y **`X-Accel-Redirect`** para NGINX.
En los siguientes ejemplos, asumimos que el directorio raíz del proyecto es `public/`
y que queremos usar PHP para servir archivos almacenados fuera del directorio `public/`,
desde un directorio llamado `private-files/`.
## Configuración
Primero, agrega la siguiente configuración a tu `Caddyfile` para habilitar esta funcionalidad:
```patch
root public/
# ...
+ # Necesario para Symfony, Laravel y otros proyectos que usan el componente Symfony HttpFoundation
+ request_header X-Sendfile-Type x-accel-redirect
+ request_header X-Accel-Mapping ../private-files=/private-files
+
+ intercept {
+ @accel header X-Accel-Redirect *
+ handle_response @accel {
+ root private-files/
+ rewrite * {resp.header.X-Accel-Redirect}
+ method * GET
+
+ # Elimina el encabezado X-Accel-Redirect establecido por PHP para mayor seguridad
+ header -X-Accel-Redirect
+
+ file_server
+ }
+ }
php_server
```
## PHP puro
Establece la ruta relativa del archivo (desde `private-files/`) como valor del encabezado `X-Accel-Redirect`:
```php
header('X-Accel-Redirect: file.txt');
```
## Proyectos que usan el componente Symfony HttpFoundation (Symfony, Laravel, Drupal...)
Symfony HttpFoundation [soporta nativamente esta funcionalidad](https://symfony.com/doc/current/components/http_foundation.html#serving-files).
Determinará automáticamente el valor correcto para el encabezado `X-Accel-Redirect` y lo agregará a la respuesta.
```php
use Symfony\Component\HttpFoundation\BinaryFileResponse;
BinaryFileResponse::trustXSendfileTypeHeader();
$response = new BinaryFileResponse(__DIR__.'/../private-files/file.txt');
// ...
```

172
docs/extension-workers.md Normal file
View File

@@ -0,0 +1,172 @@
# Extension Workers
Extension Workers enable your [FrankenPHP extension](https://frankenphp.dev/docs/extensions/) to manage a dedicated pool of PHP threads for executing background tasks, handling asynchronous events, or implementing custom protocols. Useful for queue systems, event listeners, schedulers, etc.
## Registering the Worker
### Static Registration
If you don't need to make the worker configurable by the user (fixed script path, fixed number of threads), you can simply register the worker in the `init()` function.
```go
package myextension
import (
"github.com/dunglas/frankenphp"
"github.com/dunglas/frankenphp/caddy"
)
// Global handle to communicate with the worker pool
var worker frankenphp.Workers
func init() {
// Register the worker when the module is loaded.
worker = caddy.RegisterWorkers(
"my-internal-worker", // Unique name
"worker.php", // Script path (relative to execution or absolute)
2, // Fixed Thread count
// Optional Lifecycle Hooks
frankenphp.WithWorkerOnServerStartup(func() {
// Global setup logic...
}),
)
}
```
### In a Caddy Module (Configurable by the user)
If you plan to share your extension (like a generic queue or event listener), you should wrap it in a Caddy module. This allows users to configure the script path and thread count via their `Caddyfile`. This requires implementing the `caddy.Provisioner` interface and parsing the Caddyfile ([see an example](https://github.com/dunglas/frankenphp-queue/blob/989120d394d66dd6c8e2101cac73dd622fade334/caddy.go)).
### In a Pure Go Application (Embedding)
If you are [embedding FrankenPHP in a standard Go application without caddy](https://pkg.go.dev/github.com/dunglas/frankenphp#example-ServeHTTP), you can register extension workers using `frankenphp.WithExtensionWorkers` when initializing options.
## Interacting with Workers
Once the worker pool is active, you can dispatch tasks to it. This can be done inside [native functions exported to PHP](https://frankenphp.dev/docs/extensions/#writing-the-extension), or from any Go logic such as a cron scheduler, an event listener (MQTT, Kafka), or a any other goroutine.
### Headless Mode : `SendMessage`
Use `SendMessage` to pass raw data directly to your worker script. This is ideal for queues or simple commands.
#### Example: An Async Queue Extension
```go
// #include <Zend/zend_types.h>
import "C"
import (
"context"
"unsafe"
"github.com/dunglas/frankenphp"
)
//export_php:function my_queue_push(mixed $data): bool
func my_queue_push(data *C.zval) bool {
// 1. Ensure worker is ready
if worker == nil {
return false
}
// 2. Dispatch to the background worker
_, err := worker.SendMessage(
context.Background(), // Standard Go context
unsafe.Pointer(data), // Data to pass to the worker
nil, // Optional http.ResponseWriter
)
return err == nil
}
```
### HTTP Emulation :`SendRequest`
Use `SendRequest` if your extension needs to invoke a PHP script that expects a standard web environment (populating `$_SERVER`, `$_GET`, etc.).
```go
// #include <Zend/zend_types.h>
import "C"
import (
"net/http"
"net/http/httptest"
"unsafe"
"github.com/dunglas/frankenphp"
)
//export_php:function my_worker_http_request(string $path): string
func my_worker_http_request(path *C.zend_string) unsafe.Pointer {
// 1. Prepare the request and recorder
url := frankenphp.GoString(unsafe.Pointer(path))
req, _ := http.NewRequest("GET", url, http.NoBody)
rr := httptest.NewRecorder()
// 2. Dispatch to the worker
if err := worker.SendRequest(rr, req); err != nil {
return nil
}
// 3. Return the captured response
return frankenphp.PHPString(rr.Body.String(), false)
}
```
## Worker Script
The PHP worker script runs in a loop and can handle both raw messages and HTTP requests.
```php
<?php
// Handle both raw messages and HTTP requests in the same loop
$handler = function ($payload = null) {
// Case 1: Message Mode
if ($payload !== null) {
return "Received payload: " . $payload;
}
// Case 2: HTTP Mode (standard PHP superglobals are populated)
echo "Hello from page: " . $_SERVER['REQUEST_URI'];
};
while (frankenphp_handle_request($handler)) {
gc_collect_cycles();
}
```
## Lifecycle Hooks
FrankenPHP provides hooks to execute Go code at specific points in the lifecycle.
| Hook Type | Option Name | Signature | Context & Use Case |
| :--------- | :--------------------------- | :------------------- | :--------------------------------------------------------------------- |
| **Server** | `WithWorkerOnServerStartup` | `func()` | Global setup. Run **Once**. Example: Connect to NATS/Redis. |
| **Server** | `WithWorkerOnServerShutdown` | `func()` | Global cleanup. Run **Once**. Example: Close shared connections. |
| **Thread** | `WithWorkerOnReady` | `func(threadID int)` | Per-thread setup. Called when a thread starts. Receives the Thread ID. |
| **Thread** | `WithWorkerOnShutdown` | `func(threadID int)` | Per-thread cleanup. Receives the Thread ID. |
### Example
```go
package myextension
import (
"fmt"
"github.com/dunglas/frankenphp"
frankenphpCaddy "github.com/dunglas/frankenphp/caddy"
)
func init() {
workerHandle = frankenphpCaddy.RegisterWorkers(
"my-worker", "worker.php", 2,
// Server Startup (Global)
frankenphp.WithWorkerOnServerStartup(func() {
fmt.Println("Extension: Server starting up...")
}),
// Thread Ready (Per Thread)
// Note: The function accepts an integer representing the Thread ID
frankenphp.WithWorkerOnReady(func(id int) {
fmt.Printf("Extension: Worker thread #%d is ready.\n", id)
}),
)
}
```

View File

@@ -5,7 +5,7 @@ However, it is possible to substantially improve performance using an appropriat
## Number of Threads and Workers
By default, FrankenPHP starts 2 times more threads and workers (in worker mode) than the available numbers of CPU.
By default, FrankenPHP starts 2 times more threads and workers (in worker mode) than the available number of CPU cores.
The appropriate values depend heavily on how your application is written, what it does, and your hardware.
We strongly recommend changing these values. For best system stability, it is recommended to have `num_threads` x `memory_limit` < `available_memory`.
@@ -45,6 +45,8 @@ In production environments, we recommend using FrankenPHP linked against glibc,
This can be achieved by using the Debian Docker images, using [our maintainers .deb, .rpm, or .apk packages](https://pkgs.henderkes.com), or by [compiling FrankenPHP from sources](compile.md).
For leaner or more secure containers, you may want to consider [a hardened Debian image](docker.md#hardening-images) rather than Alpine.
## Go Runtime Configuration
FrankenPHP is written in Go.
@@ -87,6 +89,18 @@ php_server {
```
This can significantly reduce the number of unnecessary file operations.
A worker equivalent of the previous configuration would be:
```caddyfile
route {
php_server { # use "php" instead of "php_server" if you don't need the file server at all
root /root/to/your/app
worker /path/to/worker.php {
match * # send all requests directly to the worker
}
}
}
```
An alternate approach with 0 unnecessary file system operations would be to instead use the `php` directive and split
files from PHP by path. This approach works well if your entire application is served by one entry file.
@@ -164,22 +178,18 @@ limits the concurrency of requests going towards the slow endpoint, similar to a
connection pool.
```caddyfile
{
frankenphp {
max_threads 100 # max 100 threads shared by all workers
}
}
example.com {
php_server {
root /app/public # the root of your application
worker index.php {
match /slow-endpoint/* # all requests with path /slow-endpoint/* are handled by this thread pool
num 10 # minimum 10 threads for requests matching /slow-endpoint/*
num 1 # minimum 1 threads for requests matching /slow-endpoint/*
max_threads 20 # allow up to 20 threads for requests matching /slow-endpoint/*, if needed
}
worker index.php {
match * # all other requests are handled separately
num 20 # minimum 20 threads for other requests, even if the slow endppoints start hanging
num 1 # minimum 1 threads for other requests, even if the slow endpoints start hanging
max_threads 20 # allow up to 20 threads for other requests, if needed
}
}
}

View File

@@ -99,6 +99,7 @@ function sanitizeMarkdown(string $markdown): string
$fileToTranslate = $argv;
array_shift($fileToTranslate);
$fileToTranslate = array_map(fn($filename) => trim($filename), $fileToTranslate);
$apiKey = $_SERVER['GEMINI_API_KEY'] ?? $_ENV['GEMINI_API_KEY'] ?? '';
if (!$apiKey) {
echo 'Enter gemini api key ($GEMINI_API_KEY): ';

View File

@@ -72,9 +72,6 @@ The following example shows how to create your own worker script without relying
<?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';

View File

@@ -17,7 +17,15 @@ import (
"time"
)
// EmbeddedAppPath contains the path of the embedded PHP application (empty if none)
// EmbeddedAppPath contains the path of the embedded PHP application (empty if none).
// It can be set at build time using -ldflags to override the default extraction path:
//
// go build -ldflags "-X github.com/dunglas/frankenphp.EmbeddedAppPath=/app" ...
//
// When set, the embedded app is extracted to this fixed path instead of a temp
// directory with a checksum suffix. This is useful when the app contains
// pre-compiled artifacts (e.g. OPcache file cache) that reference absolute paths
// and need a predictable extraction location.
var EmbeddedAppPath string
//go:embed app.tar
@@ -32,14 +40,14 @@ func init() {
return
}
appPath := filepath.Join(os.TempDir(), "frankenphp_"+string(embeddedAppChecksum))
if err := untar(appPath); err != nil {
_ = os.RemoveAll(appPath)
panic(err)
if EmbeddedAppPath == "" {
EmbeddedAppPath = filepath.Join(os.TempDir(), "frankenphp_"+string(embeddedAppChecksum))
}
EmbeddedAppPath = appPath
if err := untar(EmbeddedAppPath); err != nil {
_ = os.RemoveAll(EmbeddedAppPath)
panic(err)
}
}
// untar reads the tar file from r and writes it into dir.

115
env.go
View File

@@ -1,116 +1,37 @@
package frankenphp
// #cgo nocallback frankenphp_init_persistent_string
// #cgo noescape frankenphp_init_persistent_string
// #include "frankenphp.h"
// #include <Zend/zend_API.h>
// #include "types.h"
import "C"
import (
"os"
"strings"
"unsafe"
)
func initializeEnv() map[string]*C.zend_string {
env := os.Environ()
envMap := make(map[string]*C.zend_string, len(env))
var lengthOfEnv = 0
for _, envVar := range env {
//export go_init_os_env
func go_init_os_env(mainThreadEnv *C.zend_array) {
fullEnv := os.Environ()
lengthOfEnv = len(fullEnv)
for _, envVar := range fullEnv {
key, val, _ := strings.Cut(envVar, "=")
envMap[key] = C.frankenphp_init_persistent_string(toUnsafeChar(val), C.size_t(len(val)))
}
return envMap
}
// get the main thread env or the thread specific env
func getSandboxedEnv(thread *phpThread) map[string]*C.zend_string {
if thread.sandboxedEnv != nil {
return thread.sandboxedEnv
}
return mainThread.sandboxedEnv
}
func clearSandboxedEnv(thread *phpThread) {
if thread.sandboxedEnv == nil {
return
}
for key, val := range thread.sandboxedEnv {
valInMainThread, ok := mainThread.sandboxedEnv[key]
if !ok || val != valInMainThread {
C.free(unsafe.Pointer(val))
}
}
thread.sandboxedEnv = nil
}
// if an env var already exists, it needs to be freed
func removeEnvFromThread(thread *phpThread, key string) {
valueInThread, existsInThread := thread.sandboxedEnv[key]
if !existsInThread {
return
}
valueInMainThread, ok := mainThread.sandboxedEnv[key]
if !ok || valueInThread != valueInMainThread {
C.free(unsafe.Pointer(valueInThread))
}
delete(thread.sandboxedEnv, key)
}
// copy the main thread env to the thread specific env
func cloneSandboxedEnv(thread *phpThread) {
if thread.sandboxedEnv != nil {
return
}
thread.sandboxedEnv = make(map[string]*C.zend_string, len(mainThread.sandboxedEnv))
for key, value := range mainThread.sandboxedEnv {
thread.sandboxedEnv[key] = value
zkey := newPersistentZendString(key)
zStr := newPersistentZendString(val)
C.__hash_update_string__(mainThreadEnv, zkey, zStr)
}
}
//export go_putenv
func go_putenv(threadIndex C.uintptr_t, str *C.char, length C.int) C.bool {
thread := phpThreads[threadIndex]
envString := C.GoStringN(str, length)
cloneSandboxedEnv(thread)
func go_putenv(name *C.char, nameLen C.int, val *C.char, valLen C.int) C.bool {
goName := C.GoStringN(name, nameLen)
// Check if '=' is present in the string
if key, val, found := strings.Cut(envString, "="); found {
removeEnvFromThread(thread, key)
thread.sandboxedEnv[key] = C.frankenphp_init_persistent_string(toUnsafeChar(val), C.size_t(len(val)))
return os.Setenv(key, val) == nil
if val == nil {
// If no "=" is present, unset the environment variable
return C.bool(os.Unsetenv(goName) == nil)
}
// No '=', unset the environment variable
removeEnvFromThread(thread, envString)
return os.Unsetenv(envString) == nil
}
//export go_getfullenv
func go_getfullenv(threadIndex C.uintptr_t, trackVarsArray *C.zval) {
thread := phpThreads[threadIndex]
env := getSandboxedEnv(thread)
for key, val := range env {
C.add_assoc_str_ex(trackVarsArray, toUnsafeChar(key), C.size_t(len(key)), val)
}
}
//export go_getenv
func go_getenv(threadIndex C.uintptr_t, name *C.char) (C.bool, *C.zend_string) {
thread := phpThreads[threadIndex]
// Get the environment variable value
envValue, exists := getSandboxedEnv(thread)[C.GoString(name)]
if !exists {
// Environment variable does not exist
return false, nil // Return 0 to indicate failure
}
return true, envValue // Return 1 to indicate success
goVal := C.GoStringN(val, valLen)
return C.bool(os.Setenv(goName, goVal) == nil)
}

2
ext.go
View File

@@ -1,6 +1,6 @@
package frankenphp
//#include "frankenphp.h"
// #include "frankenphp.h"
import "C"
import (
"sync"

View File

@@ -1,15 +1,21 @@
#include "frankenphp.h"
#include <SAPI.h>
#include <Zend/zend_alloc.h>
#include <Zend/zend_exceptions.h>
#include <Zend/zend_interfaces.h>
#include <Zend/zend_types.h>
#include <errno.h>
#include <ext/session/php_session.h>
#include <ext/spl/spl_exceptions.h>
#include <ext/standard/head.h>
#ifdef HAVE_PHP_SESSION
#include <ext/session/php_session.h>
#endif
#include <inttypes.h>
#include <php.h>
#ifdef PHP_WIN32
#include <config.w32.h>
#else
#include <php_config.h>
#endif
#include <php_ini.h>
#include <php_main.h>
#include <php_output.h>
@@ -20,7 +26,9 @@
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#ifndef ZEND_WIN32
#include <unistd.h>
#endif
#if defined(__linux__)
#include <sys/prctl.h>
#elif defined(__FreeBSD__) || defined(__OpenBSD__)
@@ -41,7 +49,7 @@ ZEND_TSRMLS_CACHE_DEFINE()
*
* @see https://github.com/DataDog/dd-trace-php/pull/3169 for an example
*/
static const char *MODULES_TO_RELOAD[] = {"filter", "session", NULL};
static const char *MODULES_TO_RELOAD[] = {"filter", NULL};
frankenphp_version frankenphp_get_version() {
return (frankenphp_version){
@@ -71,48 +79,19 @@ frankenphp_config frankenphp_get_config() {
}
bool should_filter_var = 0;
bool original_user_abort_setting = 0;
frankenphp_interned_strings_t frankenphp_strings = {0};
HashTable *main_thread_env = NULL;
__thread uintptr_t thread_index;
__thread bool is_worker_thread = false;
__thread zval *os_environment = NULL;
__thread HashTable *worker_ini_snapshot = NULL;
/* Session user handler names (same structure as PS(mod_user_names)).
* In PHP 8.2, mod_user_names is a union with .name.ps_* access.
* In PHP 8.3+, mod_user_names is a direct struct with .ps_* access. */
typedef struct {
zval ps_open;
zval ps_close;
zval ps_read;
zval ps_write;
zval ps_destroy;
zval ps_gc;
zval ps_create_sid;
zval ps_validate_sid;
zval ps_update_timestamp;
} session_user_handlers;
/* Macro to access PS(mod_user_names) handlers across PHP versions */
#if PHP_VERSION_ID >= 80300
#define PS_MOD_USER_NAMES(handler) PS(mod_user_names).handler
#else
#define PS_MOD_USER_NAMES(handler) PS(mod_user_names).name.handler
#endif
#define FOR_EACH_SESSION_HANDLER(op) \
op(ps_open); \
op(ps_close); \
op(ps_read); \
op(ps_write); \
op(ps_destroy); \
op(ps_gc); \
op(ps_create_sid); \
op(ps_validate_sid); \
op(ps_update_timestamp)
__thread session_user_handlers *worker_session_handlers_snapshot = NULL;
__thread HashTable *sandboxed_env = NULL;
void frankenphp_update_local_thread_context(bool is_worker) {
is_worker_thread = is_worker;
/* workers should keep running if the user aborts the connection */
PG(ignore_user_abort) = is_worker ? 1 : original_user_abort_setting;
}
static void frankenphp_update_request_context() {
@@ -155,6 +134,13 @@ static void frankenphp_reset_super_globals() {
zval *files = &PG(http_globals)[TRACK_VARS_FILES];
zval_ptr_dtor_nogc(files);
memset(files, 0, sizeof(*files));
/* $_SESSION must be explicitly deleted from the symbol table.
* Unlike other superglobals, $_SESSION is stored in EG(symbol_table)
* with a reference to PS(http_session_vars). The session RSHUTDOWN
* only decrements the refcount but doesn't remove it from the symbol
* table, causing data to leak between requests. */
zend_hash_str_del(&EG(symbol_table), "_SESSION", sizeof("_SESSION") - 1);
}
zend_end_try();
@@ -211,165 +197,52 @@ static void frankenphp_release_temporary_streams() {
ZEND_HASH_FOREACH_END();
}
/* Destructor for INI snapshot hash table entries */
static void frankenphp_ini_snapshot_dtor(zval *zv) {
zend_string_release((zend_string *)Z_PTR_P(zv));
}
/* Save the current state of modified INI entries.
* This captures INI values set by the framework before the worker loop. */
static void frankenphp_snapshot_ini(void) {
if (worker_ini_snapshot != NULL) {
return; /* Already snapshotted */
}
if (EG(modified_ini_directives) == NULL) {
/* Allocate empty table to mark as snapshotted */
ALLOC_HASHTABLE(worker_ini_snapshot);
zend_hash_init(worker_ini_snapshot, 0, NULL, frankenphp_ini_snapshot_dtor,
0);
return;
}
uint32_t num_modified = zend_hash_num_elements(EG(modified_ini_directives));
ALLOC_HASHTABLE(worker_ini_snapshot);
zend_hash_init(worker_ini_snapshot, num_modified, NULL,
frankenphp_ini_snapshot_dtor, 0);
zend_ini_entry *ini_entry;
ZEND_HASH_FOREACH_PTR(EG(modified_ini_directives), ini_entry) {
if (ini_entry->value) {
zend_hash_add_ptr(worker_ini_snapshot, ini_entry->name,
zend_string_copy(ini_entry->value));
}
}
ZEND_HASH_FOREACH_END();
}
/* Restore INI values to the state captured by frankenphp_snapshot_ini().
* - Entries in snapshot with changed values: restore to snapshot value
* - Entries not in snapshot: restore to startup default */
static void frankenphp_restore_ini(void) {
if (worker_ini_snapshot == NULL || EG(modified_ini_directives) == NULL) {
return;
}
zend_ini_entry *ini_entry;
zend_string *snapshot_value;
zend_string *entry_name;
/* Collect entries to restore to default in a separate array.
* We cannot call zend_restore_ini_entry() during iteration because
* it calls zend_hash_del() on EG(modified_ini_directives). */
uint32_t max_entries = zend_hash_num_elements(EG(modified_ini_directives));
zend_string **entries_to_restore =
max_entries ? emalloc(max_entries * sizeof(zend_string *)) : NULL;
size_t restore_count = 0;
ZEND_HASH_FOREACH_STR_KEY_PTR(EG(modified_ini_directives), entry_name,
ini_entry) {
snapshot_value = zend_hash_find_ptr(worker_ini_snapshot, entry_name);
if (snapshot_value == NULL) {
/* Entry was not in snapshot: collect for restore to startup default */
entries_to_restore[restore_count++] = zend_string_copy(entry_name);
} else if (!zend_string_equals(ini_entry->value, snapshot_value)) {
/* Entry was in snapshot but value changed: restore to snapshot value.
* zend_alter_ini_entry() does not delete from modified_ini_directives. */
zend_alter_ini_entry(entry_name, snapshot_value, PHP_INI_USER,
PHP_INI_STAGE_RUNTIME);
}
/* else: Entry in snapshot with same value, nothing to do */
}
ZEND_HASH_FOREACH_END();
/* Now restore entries to default outside of iteration */
for (size_t i = 0; i < restore_count; i++) {
zend_restore_ini_entry(entries_to_restore[i], PHP_INI_STAGE_RUNTIME);
zend_string_release(entries_to_restore[i]);
}
if (entries_to_restore) {
efree(entries_to_restore);
}
}
/* Save session user handlers set before the worker loop.
* This allows frameworks to define custom session handlers that persist. */
static void frankenphp_snapshot_session_handlers(void) {
if (worker_session_handlers_snapshot != NULL) {
return; /* Already snapshotted */
}
/* Check if session module is loaded */
if (zend_hash_str_find_ptr(&module_registry, "session",
sizeof("session") - 1) == NULL) {
return; /* Session module not available */
}
/* Check if user session handlers are defined */
if (Z_ISUNDEF(PS_MOD_USER_NAMES(ps_open))) {
return; /* No user handlers to snapshot */
}
worker_session_handlers_snapshot = emalloc(sizeof(session_user_handlers));
/* Copy each handler zval with incremented reference count */
#define SNAPSHOT_HANDLER(h) \
if (!Z_ISUNDEF(PS_MOD_USER_NAMES(h))) { \
ZVAL_COPY(&worker_session_handlers_snapshot->h, &PS_MOD_USER_NAMES(h)); \
} else { \
ZVAL_UNDEF(&worker_session_handlers_snapshot->h); \
}
FOR_EACH_SESSION_HANDLER(SNAPSHOT_HANDLER);
#undef SNAPSHOT_HANDLER
}
/* Restore session user handlers from snapshot after RSHUTDOWN freed them. */
static void frankenphp_restore_session_handlers(void) {
if (worker_session_handlers_snapshot == NULL) {
return;
}
/* Restore each handler zval.
* Session RSHUTDOWN already freed the handlers via zval_ptr_dtor and set
* them to UNDEF, so we don't need to destroy them again. We simply copy
* from the snapshot (which holds its own reference). */
#define RESTORE_HANDLER(h) \
if (!Z_ISUNDEF(worker_session_handlers_snapshot->h)) { \
ZVAL_COPY(&PS_MOD_USER_NAMES(h), &worker_session_handlers_snapshot->h); \
}
FOR_EACH_SESSION_HANDLER(RESTORE_HANDLER);
#undef RESTORE_HANDLER
}
/* Free worker state when the worker script terminates. */
static void frankenphp_cleanup_worker_state(void) {
/* Free INI snapshot */
if (worker_ini_snapshot != NULL) {
zend_hash_destroy(worker_ini_snapshot);
FREE_HASHTABLE(worker_ini_snapshot);
worker_ini_snapshot = NULL;
}
/* Free session handlers snapshot */
if (worker_session_handlers_snapshot != NULL) {
#define FREE_HANDLER(h) \
if (!Z_ISUNDEF(worker_session_handlers_snapshot->h)) { \
zval_ptr_dtor(&worker_session_handlers_snapshot->h); \
}
FOR_EACH_SESSION_HANDLER(FREE_HANDLER);
#undef FREE_HANDLER
efree(worker_session_handlers_snapshot);
worker_session_handlers_snapshot = NULL;
}
#ifdef HAVE_PHP_SESSION
/* Reset session state between worker requests, preserving user handlers.
* Based on php_rshutdown_session_globals() + php_rinit_session_globals(). */
static void frankenphp_reset_session_state(void) {
if (PS(session_status) == php_session_active) {
php_session_flush(1);
}
if (!Z_ISUNDEF(PS(http_session_vars))) {
zval_ptr_dtor(&PS(http_session_vars));
ZVAL_UNDEF(&PS(http_session_vars));
}
if (PS(mod_data) || PS(mod_user_implemented)) {
zend_try { PS(mod)->s_close(&PS(mod_data)); }
zend_end_try();
}
if (PS(id)) {
zend_string_release_ex(PS(id), 0);
PS(id) = NULL;
}
if (PS(session_vars)) {
zend_string_release_ex(PS(session_vars), 0);
PS(session_vars) = NULL;
}
/* PS(mod_user_class_name) and PS(mod_user_names) are preserved */
#if PHP_VERSION_ID >= 80300
if (PS(session_started_filename)) {
zend_string_release(PS(session_started_filename));
PS(session_started_filename) = NULL;
PS(session_started_lineno) = 0;
}
#endif
PS(session_status) = php_session_none;
PS(in_save_handler) = 0;
PS(set_handler) = 0;
PS(mod_data) = NULL;
PS(mod_user_is_open) = 0;
PS(define_sid) = 1;
}
#endif
/* Adapted from php_request_shutdown */
static void frankenphp_worker_request_shutdown() {
@@ -386,6 +259,10 @@ static void frankenphp_worker_request_shutdown() {
}
}
#ifdef HAVE_PHP_SESSION
frankenphp_reset_session_state();
#endif
/* Shutdown output layer (send the set HTTP headers, cleanup output handlers,
* etc.) */
zend_try { php_output_deactivate(); }
@@ -405,19 +282,13 @@ bool frankenphp_shutdown_dummy_request(void) {
return false;
}
/* Snapshot INI and session handlers BEFORE shutdown.
* The framework has set these up before the worker loop, and we want
* to preserve them. Session RSHUTDOWN will free the handlers. */
frankenphp_snapshot_ini();
frankenphp_snapshot_session_handlers();
frankenphp_worker_request_shutdown();
return true;
}
PHPAPI void get_full_env(zval *track_vars_array) {
go_getfullenv(thread_index, track_vars_array);
void get_full_env(zval *track_vars_array) {
zend_hash_copy(Z_ARR_P(track_vars_array), main_thread_env, NULL);
}
/* Adapted from php_request_startup() */
@@ -466,12 +337,6 @@ static int frankenphp_worker_request_startup() {
frankenphp_reset_super_globals();
/* Restore INI values changed during the previous request back to their
* snapshot state (captured in frankenphp_shutdown_dummy_request).
* This ensures framework settings persist while request-level changes
* are reset. */
frankenphp_restore_ini();
const char **module_name;
zend_module_entry *module;
for (module_name = MODULES_TO_RELOAD; *module_name; module_name++) {
@@ -481,12 +346,6 @@ static int frankenphp_worker_request_startup() {
module->request_startup_func(module->type, module->module_number);
}
}
/* Restore session handlers AFTER session RINIT.
* Session RSHUTDOWN frees mod_user_names callbacks, so we must restore
* them before user code runs. This must happen after RINIT because
* session RINIT may reset some state. */
frankenphp_restore_session_handlers();
}
zend_catch { retval = FAILURE; }
zend_end_try();
@@ -526,39 +385,72 @@ PHP_FUNCTION(frankenphp_putenv) {
RETURN_FALSE;
}
if (go_putenv(thread_index, setting, (int)setting_len)) {
RETURN_TRUE;
} else {
RETURN_FALSE;
if (setting_len == 0 || setting[0] == '=') {
zend_argument_value_error(1, "must have a valid syntax");
RETURN_THROWS();
}
if (sandboxed_env == NULL) {
sandboxed_env = zend_array_dup(main_thread_env);
}
/* cut at null byte to stay consistent with regular putenv */
char *null_pos = memchr(setting, '\0', setting_len);
if (null_pos != NULL) {
setting_len = null_pos - setting;
}
/* cut the string at the first '=' */
char *eq_pos = memchr(setting, '=', setting_len);
bool success = true;
/* no '=' found, delete the variable */
if (eq_pos == NULL) {
success = go_putenv(setting, (int)setting_len, NULL, 0);
if (success) {
zend_hash_str_del(sandboxed_env, setting, setting_len);
}
RETURN_BOOL(success);
}
size_t name_len = eq_pos - setting;
size_t value_len =
(setting_len > name_len + 1) ? (setting_len - name_len - 1) : 0;
success = go_putenv(setting, (int)name_len, eq_pos + 1, (int)value_len);
if (success) {
zval val = {0};
ZVAL_STRINGL(&val, eq_pos + 1, value_len);
zend_hash_str_update(sandboxed_env, setting, name_len, &val);
}
RETURN_BOOL(success);
} /* }}} */
/* {{{ Call go's getenv to prevent race conditions */
/* {{{ Get the env from the sandboxed environment */
PHP_FUNCTION(frankenphp_getenv) {
char *name = NULL;
size_t name_len = 0;
zend_string *name = NULL;
bool local_only = 0;
ZEND_PARSE_PARAMETERS_START(0, 2)
Z_PARAM_OPTIONAL
Z_PARAM_STRING_OR_NULL(name, name_len)
Z_PARAM_STR_OR_NULL(name)
Z_PARAM_BOOL(local_only)
ZEND_PARSE_PARAMETERS_END();
if (!name) {
array_init(return_value);
get_full_env(return_value);
HashTable *ht = sandboxed_env ? sandboxed_env : main_thread_env;
if (!name) {
RETURN_ARR(zend_array_dup(ht));
return;
}
struct go_getenv_return result = go_getenv(thread_index, name);
if (result.r0) {
// Return the single environment variable as a string
RETVAL_STR(result.r1);
zval *env_val = zend_hash_find(ht, name);
if (env_val && Z_TYPE_P(env_val) == IS_STRING) {
zend_string *str = Z_STR_P(env_val);
zend_string_addref(str);
RETVAL_STR(str);
} else {
// Environment variable does not exist
RETVAL_FALSE;
}
} /* }}} */
@@ -681,6 +573,11 @@ PHP_FUNCTION(frankenphp_handle_request) {
if (zend_call_function(&fci, &fcc) == SUCCESS && Z_TYPE(retval) != IS_UNDEF) {
callback_ret = &retval;
/* pass NULL instead of the NULL zval as return value */
if (Z_TYPE(retval) == IS_NULL) {
callback_ret = NULL;
}
}
/*
@@ -831,14 +728,6 @@ static zend_module_entry frankenphp_module = {
TOSTRING(FRANKENPHP_VERSION),
STANDARD_MODULE_PROPERTIES};
static void frankenphp_request_shutdown() {
if (is_worker_thread) {
frankenphp_cleanup_worker_state();
}
php_request_shutdown((void *)0);
frankenphp_free_request_context();
}
static int frankenphp_startup(sapi_module_struct *sapi_module) {
php_import_environment_variables = get_full_env;
@@ -919,72 +808,53 @@ static inline void frankenphp_register_trusted_var(zend_string *z_key,
}
}
void frankenphp_register_single(zend_string *z_key, char *value, size_t val_len,
zval *track_vars_array) {
HashTable *ht = Z_ARRVAL_P(track_vars_array);
frankenphp_register_trusted_var(z_key, value, val_len, ht);
}
/* Register known $_SERVER variables in bulk to avoid cgo overhead */
void frankenphp_register_bulk(
zval *track_vars_array, ht_key_value_pair remote_addr,
ht_key_value_pair remote_host, ht_key_value_pair remote_port,
ht_key_value_pair document_root, ht_key_value_pair path_info,
ht_key_value_pair php_self, ht_key_value_pair document_uri,
ht_key_value_pair script_filename, ht_key_value_pair script_name,
ht_key_value_pair https, ht_key_value_pair ssl_protocol,
ht_key_value_pair request_scheme, ht_key_value_pair server_name,
ht_key_value_pair server_port, ht_key_value_pair content_length,
ht_key_value_pair gateway_interface, ht_key_value_pair server_protocol,
ht_key_value_pair server_software, ht_key_value_pair http_host,
ht_key_value_pair auth_type, ht_key_value_pair remote_ident,
ht_key_value_pair request_uri, ht_key_value_pair ssl_cipher) {
void frankenphp_register_server_vars(zval *track_vars_array,
frankenphp_server_vars vars) {
HashTable *ht = Z_ARRVAL_P(track_vars_array);
frankenphp_register_trusted_var(remote_addr.key, remote_addr.val,
remote_addr.val_len, ht);
frankenphp_register_trusted_var(remote_host.key, remote_host.val,
remote_host.val_len, ht);
frankenphp_register_trusted_var(remote_port.key, remote_port.val,
remote_port.val_len, ht);
frankenphp_register_trusted_var(document_root.key, document_root.val,
document_root.val_len, ht);
frankenphp_register_trusted_var(path_info.key, path_info.val,
path_info.val_len, ht);
frankenphp_register_trusted_var(php_self.key, php_self.val, php_self.val_len,
ht);
frankenphp_register_trusted_var(document_uri.key, document_uri.val,
document_uri.val_len, ht);
frankenphp_register_trusted_var(script_filename.key, script_filename.val,
script_filename.val_len, ht);
frankenphp_register_trusted_var(script_name.key, script_name.val,
script_name.val_len, ht);
frankenphp_register_trusted_var(https.key, https.val, https.val_len, ht);
frankenphp_register_trusted_var(ssl_protocol.key, ssl_protocol.val,
ssl_protocol.val_len, ht);
frankenphp_register_trusted_var(ssl_cipher.key, ssl_cipher.val,
ssl_cipher.val_len, ht);
frankenphp_register_trusted_var(request_scheme.key, request_scheme.val,
request_scheme.val_len, ht);
frankenphp_register_trusted_var(server_name.key, server_name.val,
server_name.val_len, ht);
frankenphp_register_trusted_var(server_port.key, server_port.val,
server_port.val_len, ht);
frankenphp_register_trusted_var(content_length.key, content_length.val,
content_length.val_len, ht);
frankenphp_register_trusted_var(gateway_interface.key, gateway_interface.val,
gateway_interface.val_len, ht);
frankenphp_register_trusted_var(server_protocol.key, server_protocol.val,
server_protocol.val_len, ht);
frankenphp_register_trusted_var(server_software.key, server_software.val,
server_software.val_len, ht);
frankenphp_register_trusted_var(http_host.key, http_host.val,
http_host.val_len, ht);
frankenphp_register_trusted_var(auth_type.key, auth_type.val,
auth_type.val_len, ht);
frankenphp_register_trusted_var(remote_ident.key, remote_ident.val,
remote_ident.val_len, ht);
frankenphp_register_trusted_var(request_uri.key, request_uri.val,
request_uri.val_len, ht);
zend_hash_extend(ht, vars.total_num_vars, 0);
// update values with variable strings
#define FRANKENPHP_REGISTER_VAR(name) \
frankenphp_register_trusted_var(frankenphp_strings.name, vars.name, \
vars.name##_len, ht)
FRANKENPHP_REGISTER_VAR(remote_addr);
FRANKENPHP_REGISTER_VAR(remote_host);
FRANKENPHP_REGISTER_VAR(remote_port);
FRANKENPHP_REGISTER_VAR(document_root);
FRANKENPHP_REGISTER_VAR(path_info);
FRANKENPHP_REGISTER_VAR(php_self);
FRANKENPHP_REGISTER_VAR(document_uri);
FRANKENPHP_REGISTER_VAR(script_filename);
FRANKENPHP_REGISTER_VAR(script_name);
FRANKENPHP_REGISTER_VAR(ssl_cipher);
FRANKENPHP_REGISTER_VAR(server_name);
FRANKENPHP_REGISTER_VAR(server_port);
FRANKENPHP_REGISTER_VAR(content_length);
FRANKENPHP_REGISTER_VAR(server_protocol);
FRANKENPHP_REGISTER_VAR(http_host);
FRANKENPHP_REGISTER_VAR(request_uri);
#undef FRANKENPHP_REGISTER_VAR
/* update values with hard-coded zend_strings */
zval zv;
ZVAL_STR(&zv, frankenphp_strings.cgi11);
zend_hash_update_ind(ht, frankenphp_strings.gateway_interface, &zv);
ZVAL_STR(&zv, frankenphp_strings.frankenphp);
zend_hash_update_ind(ht, frankenphp_strings.server_software, &zv);
ZVAL_STR(&zv, vars.request_scheme);
zend_hash_update_ind(ht, frankenphp_strings.request_scheme, &zv);
ZVAL_STR(&zv, vars.ssl_protocol);
zend_hash_update_ind(ht, frankenphp_strings.ssl_protocol, &zv);
ZVAL_STR(&zv, vars.https);
zend_hash_update_ind(ht, frankenphp_strings.https, &zv);
/* update values with always empty strings */
ZVAL_EMPTY_STRING(&zv);
zend_hash_update_ind(ht, frankenphp_strings.auth_type, &zv);
zend_hash_update_ind(ht, frankenphp_strings.remote_ident, &zv);
}
/** Create an immutable zend_string that lasts for the whole process **/
@@ -999,7 +869,22 @@ zend_string *frankenphp_init_persistent_string(const char *string, size_t len) {
return z_string;
}
static void
/* initialize all hard-coded zend_strings once per process */
static void frankenphp_init_interned_strings(void) {
if (frankenphp_strings.remote_addr != NULL) {
return; /* already initialized */
}
#define F_INITIALIZE_FIELD(name, str) \
frankenphp_strings.name = \
frankenphp_init_persistent_string(str, sizeof(str) - 1);
FRANKENPHP_INTERNED_STRINGS_LIST(F_INITIALIZE_FIELD)
#undef F_INITIALIZE_FIELD
}
/* Register variables from SG(request_info) into $_SERVER */
static inline void
frankenphp_register_variable_from_request_info(zend_string *zKey, char *value,
bool must_be_present,
zval *track_vars_array) {
@@ -1012,23 +897,31 @@ frankenphp_register_variable_from_request_info(zend_string *zKey, char *value,
}
}
void frankenphp_register_variables_from_request_info(
zval *track_vars_array, zend_string *content_type,
zend_string *path_translated, zend_string *query_string,
zend_string *auth_user, zend_string *request_method) {
static void
frankenphp_register_variables_from_request_info(zval *track_vars_array) {
frankenphp_register_variable_from_request_info(
content_type, (char *)SG(request_info).content_type, true,
frankenphp_strings.content_type, (char *)SG(request_info).content_type,
true, track_vars_array);
frankenphp_register_variable_from_request_info(
frankenphp_strings.path_translated,
(char *)SG(request_info).path_translated, false, track_vars_array);
frankenphp_register_variable_from_request_info(
frankenphp_strings.query_string, SG(request_info).query_string, true,
track_vars_array);
frankenphp_register_variable_from_request_info(
path_translated, (char *)SG(request_info).path_translated, false,
frankenphp_strings.remote_user, (char *)SG(request_info).auth_user, false,
track_vars_array);
frankenphp_register_variable_from_request_info(
query_string, SG(request_info).query_string, true, track_vars_array);
frankenphp_register_variable_from_request_info(
auth_user, (char *)SG(request_info).auth_user, false, track_vars_array);
frankenphp_register_variable_from_request_info(
request_method, (char *)SG(request_info).request_method, false,
track_vars_array);
frankenphp_strings.request_method,
(char *)SG(request_info).request_method, false, track_vars_array);
}
/* Only hard-coded keys may be registered this way */
void frankenphp_register_known_variable(zend_string *z_key, char *value,
size_t val_len,
zval *track_vars_array) {
frankenphp_register_trusted_var(z_key, value, val_len,
Z_ARRVAL_P(track_vars_array));
}
/* variables with user-defined keys must be registered safely
@@ -1061,34 +954,17 @@ static inline void register_server_variable_filtered(const char *key,
static void frankenphp_register_variables(zval *track_vars_array) {
/* https://www.php.net/manual/en/reserved.variables.server.php */
/* In CGI mode, we consider the environment to be a part of the server
* variables.
/* In CGI mode, the environment is part of the $_SERVER variables.
* $_SERVER and $_ENV should only contain values from the original
* environment, not values added though putenv
*/
zend_hash_copy(Z_ARR_P(track_vars_array), main_thread_env, NULL);
/* in non-worker mode we import the os environment regularly */
if (!is_worker_thread) {
get_full_env(track_vars_array);
// php_import_environment_variables(track_vars_array);
go_register_variables(thread_index, track_vars_array);
return;
}
/* import CGI variables from the request context in go */
go_register_server_variables(thread_index, track_vars_array);
/* In worker mode we cache the os environment */
if (os_environment == NULL) {
os_environment = malloc(sizeof(zval));
if (os_environment == NULL) {
php_error(E_ERROR, "Failed to allocate memory for os_environment");
return;
}
array_init(os_environment);
get_full_env(os_environment);
// php_import_environment_variables(os_environment);
}
zend_hash_copy(Z_ARR_P(track_vars_array), Z_ARR_P(os_environment),
(copy_ctor_func_t)zval_add_ref);
go_register_variables(thread_index, track_vars_array);
/* Some variables are already present in SG(request_info) */
frankenphp_register_variables_from_request_info(track_vars_array);
}
static void frankenphp_log_message(const char *message, int syslog_type_int) {
@@ -1096,10 +972,12 @@ static void frankenphp_log_message(const char *message, int syslog_type_int) {
}
static char *frankenphp_getenv(const char *name, size_t name_len) {
struct go_getenv_return result = go_getenv(thread_index, (char *)name);
HashTable *ht = sandboxed_env ? sandboxed_env : main_thread_env;
if (result.r0) {
return result.r1->val;
zval *env_val = zend_hash_str_find(ht, name, name_len);
if (env_val && Z_TYPE_P(env_val) == IS_STRING) {
zend_string *str = Z_STR_P(env_val);
return ZSTR_VAL(str);
}
return NULL;
@@ -1185,6 +1063,7 @@ static void *php_thread(void *arg) {
}
static void *php_main(void *arg) {
#ifndef ZEND_WIN32
/*
* SIGPIPE must be masked in non-Go threads:
* https://pkg.go.dev/os/signal#hdr-Go_programs_that_use_cgo_or_SWIG
@@ -1197,6 +1076,7 @@ static void *php_main(void *arg) {
perror("failed to block SIGPIPE");
exit(EXIT_FAILURE);
}
#endif
set_thread_name("php-main");
@@ -1214,6 +1094,32 @@ static void *php_main(void *arg) {
sapi_startup(&frankenphp_sapi_module);
/* TODO: adapted from https://github.com/php/php-src/pull/16958, remove when
* merged. */
#ifdef PHP_WIN32
{
const DWORD flags = GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT;
HMODULE module;
/* Use a larger buffer to support long module paths on Windows. */
wchar_t filename[32768];
if (GetModuleHandleExW(flags, (LPCWSTR)&frankenphp_sapi_module, &module)) {
const DWORD filename_capacity = (DWORD)_countof(filename);
DWORD len = GetModuleFileNameW(module, filename, filename_capacity);
if (len > 0 && len < filename_capacity) {
wchar_t *slash = wcsrchr(filename, L'\\');
if (slash) {
*slash = L'\0';
if (!SetDllDirectoryW(filename)) {
fprintf(stderr, "Warning: SetDllDirectoryW failed (error %lu)\n",
GetLastError());
}
}
}
}
}
#endif
#ifdef ZEND_MAX_EXECUTION_TIMERS
/* overwrite php.ini with custom user settings */
char *php_ini_overrides = go_get_custom_php_ini(false);
@@ -1227,6 +1133,8 @@ static void *php_main(void *arg) {
frankenphp_sapi_module.ini_entries = php_ini_overrides;
}
frankenphp_init_interned_strings();
frankenphp_sapi_module.startup(&frankenphp_sapi_module);
/* check if a default filter is set in php.ini and only filter if
@@ -1234,6 +1142,14 @@ static void *php_main(void *arg) {
char *default_filter;
cfg_get_string("filter.default", &default_filter);
should_filter_var = default_filter != NULL;
original_user_abort_setting = PG(ignore_user_abort);
/* take a snapshot of the environment for sandboxing */
if (main_thread_env == NULL) {
main_thread_env = pemalloc(sizeof(HashTable), 1);
zend_hash_init(main_thread_env, 8, NULL, NULL, 1);
go_init_os_env(main_thread_env);
}
go_frankenphp_main_thread_is_ready();
@@ -1281,7 +1197,8 @@ static int frankenphp_request_startup() {
return SUCCESS;
}
frankenphp_request_shutdown();
php_request_shutdown((void *)0);
frankenphp_free_request_context();
return FAILURE;
}
@@ -1307,16 +1224,16 @@ int frankenphp_execute_script(char *file_name) {
zend_catch { status = EG(exit_status); }
zend_end_try();
// free the cached os environment before shutting down the script
if (os_environment != NULL) {
zval_ptr_dtor(os_environment);
free(os_environment);
os_environment = NULL;
}
zend_destroy_file_handle(&file_handle);
frankenphp_request_shutdown();
/* Reset the sandboxed environment */
if (sandboxed_env != NULL) {
zend_hash_release(sandboxed_env);
sandboxed_env = NULL;
}
php_request_shutdown((void *)0);
frankenphp_free_request_context();
return status;
}
@@ -1491,7 +1408,7 @@ static zend_module_entry **modules = NULL;
static int modules_len = 0;
static int (*original_php_register_internal_extensions_func)(void) = NULL;
PHPAPI int register_internal_extensions(void) {
int register_internal_extensions(void) {
if (original_php_register_internal_extensions_func != NULL &&
original_php_register_internal_extensions_func() != SUCCESS) {
return FAILURE;

View File

@@ -14,10 +14,10 @@ package frankenphp
// #include <stdlib.h>
// #include <stdint.h>
// #include "frankenphp.h"
// #include <php_variables.h>
// #include <zend_llist.h>
// #include <SAPI.h>
// #include "frankenphp.h"
import "C"
import (
"bytes"
@@ -275,6 +275,10 @@ func Init(options ...Option) error {
maxWaitTime = opt.maxWaitTime
if opt.maxIdleTime > 0 {
maxIdleTime = opt.maxIdleTime
}
workerThreadCount, err := calculateMaxThreads(opt)
if err != nil {
Shutdown()
@@ -411,7 +415,7 @@ func ServeHTTP(responseWriter http.ResponseWriter, request *http.Request, opts .
}
//export go_ub_write
func go_ub_write(threadIndex C.uintptr_t, cBuf *C.char, length C.int) (C.size_t, C.bool) {
func go_ub_write(threadIndex C.uintptr_t, cBuf *C.char, length C.size_t) (C.size_t, C.bool) {
thread := phpThreads[threadIndex]
fc := thread.frankenPHPContext()
@@ -484,11 +488,11 @@ func go_apache_request_headers(threadIndex C.uintptr_t) (*C.go_string, C.size_t)
return sd, C.size_t(len(fc.request.Header))
}
func addHeader(ctx context.Context, fc *frankenPHPContext, cString *C.char, length C.int) {
key, val := splitRawHeader(cString, int(length))
func addHeader(ctx context.Context, fc *frankenPHPContext, h *C.sapi_header_struct) {
key, val := splitRawHeader(h.header, int(h.header_len))
if key == "" {
if fc.logger.Enabled(ctx, slog.LevelDebug) {
fc.logger.LogAttrs(ctx, slog.LevelDebug, "invalid header", slog.String("header", C.GoStringN(cString, length)))
fc.logger.LogAttrs(ctx, slog.LevelDebug, "invalid header", slog.String("header", C.GoStringN(h.header, C.int(h.header_len))))
}
return
@@ -548,7 +552,7 @@ func go_write_headers(threadIndex C.uintptr_t, status C.int, headers *C.zend_lli
for current != nil {
h := (*C.sapi_header_struct)(unsafe.Pointer(&(current.data)))
addHeader(fc.ctx, fc, h.header, C.int(h.header_len))
addHeader(fc.ctx, fc, h)
current = current.next
}
@@ -593,12 +597,14 @@ func go_sapi_flush(threadIndex C.uintptr_t) bool {
return true
}
if err := http.NewResponseController(fc.responseWriter).Flush(); err != nil {
if fc.responseController == nil {
fc.responseController = http.NewResponseController(fc.responseWriter)
}
if err := fc.responseController.Flush(); err != nil {
if globalLogger.Enabled(fc.ctx, slog.LevelWarn) {
globalLogger.LogAttrs(fc.ctx, slog.LevelWarn, "the current responseWriter is not a flusher, if you are not using a custom build, please report this issue", slog.Any("error", err))
}
}
return false
}
@@ -658,34 +664,28 @@ func getLogger(threadIndex C.uintptr_t) (*slog.Logger, context.Context) {
func go_log(threadIndex C.uintptr_t, message *C.char, level C.int) {
logger, ctx := getLogger(threadIndex)
m := C.GoString(message)
le := syslogLevelInfo
if level >= C.int(syslogLevelEmerg) && level <= C.int(syslogLevelDebug) {
le = syslogLevel(level)
}
var slogLevel slog.Level
switch le {
case syslogLevelEmerg, syslogLevelAlert, syslogLevelCrit, syslogLevelErr:
if logger.Enabled(ctx, slog.LevelError) {
logger.LogAttrs(ctx, slog.LevelError, m, slog.String("syslog_level", le.String()))
}
slogLevel = slog.LevelError
case syslogLevelWarn:
if logger.Enabled(ctx, slog.LevelWarn) {
logger.LogAttrs(ctx, slog.LevelWarn, m, slog.String("syslog_level", le.String()))
}
slogLevel = slog.LevelWarn
case syslogLevelDebug:
if logger.Enabled(ctx, slog.LevelDebug) {
logger.LogAttrs(ctx, slog.LevelDebug, m, slog.String("syslog_level", le.String()))
}
slogLevel = slog.LevelDebug
default:
if logger.Enabled(ctx, slog.LevelInfo) {
logger.LogAttrs(ctx, slog.LevelInfo, m, slog.String("syslog_level", le.String()))
}
slogLevel = slog.LevelInfo
}
if !logger.Enabled(ctx, slogLevel) {
return
}
logger.LogAttrs(ctx, slogLevel, C.GoString(message), slog.String("syslog_level", le.String()))
}
//export go_log_attrs
@@ -728,30 +728,6 @@ func go_is_context_done(threadIndex C.uintptr_t) C.bool {
return C.bool(phpThreads[threadIndex].frankenPHPContext().isDone)
}
// ExecuteScriptCLI executes the PHP script passed as parameter.
// It returns the exit status code of the script.
func ExecuteScriptCLI(script string, args []string) int {
// Ensure extensions are registered before CLI execution
registerExtensions()
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])), false))
}
func ExecutePHPCode(phpCode string) int {
// Ensure extensions are registered before CLI execution
registerExtensions()
cCode := C.CString(phpCode)
defer C.free(unsafe.Pointer(cCode))
return int(C.frankenphp_execute_script_cli(cCode, 0, nil, true))
}
func convertArgs(args []string) (C.int, []*C.char) {
argc := C.int(len(args))
argv := make([]*C.char, argc)
@@ -780,6 +756,9 @@ func resetGlobals() {
globalCtx = context.Background()
globalLogger = slog.Default()
workers = nil
workersByName = nil
workersByPath = nil
watcherIsEnabled = false
maxIdleTime = defaultMaxIdleTime
globalMu.Unlock()
}

View File

@@ -1,6 +1,46 @@
#ifndef _FRANKENPHP_H
#define _FRANKENPHP_H
#ifdef _WIN32
// Define this to prevent windows.h from including legacy winsock.h
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
// Explicitly include Winsock2 BEFORE windows.h
#include <windows.h>
#include <winerror.h>
#include <winsock2.h>
#include <ws2tcpip.h>
// Fix for missing IntSafe functions (LongLongAdd) when building with Clang
#ifdef __clang__
#ifndef INTSAFE_E_ARITHMETIC_OVERFLOW
#define INTSAFE_E_ARITHMETIC_OVERFLOW ((HRESULT)0x80070216L)
#endif
#ifndef LongLongAdd
static inline HRESULT LongLongAdd(LONGLONG llAugend, LONGLONG llAddend,
LONGLONG *pllResult) {
if (__builtin_add_overflow(llAugend, llAddend, pllResult)) {
return INTSAFE_E_ARITHMETIC_OVERFLOW;
}
return S_OK;
}
#endif
#ifndef LongLongSub
static inline HRESULT LongLongSub(LONGLONG llMinuend, LONGLONG llSubtrahend,
LONGLONG *pllResult) {
if (__builtin_sub_overflow(llMinuend, llSubtrahend, pllResult)) {
return INTSAFE_E_ARITHMETIC_OVERFLOW;
}
return S_OK;
}
#endif
#endif
#endif
#include <Zend/zend_modules.h>
#include <Zend/zend_types.h>
#include <stdbool.h>
@@ -17,11 +57,96 @@ typedef struct go_string {
char *data;
} go_string;
typedef struct ht_key_value_pair {
zend_string *key;
char *val;
size_t val_len;
} ht_key_value_pair;
typedef struct frankenphp_server_vars {
size_t total_num_vars;
char *remote_addr;
size_t remote_addr_len;
char *remote_host;
size_t remote_host_len;
char *remote_port;
size_t remote_port_len;
char *document_root;
size_t document_root_len;
char *path_info;
size_t path_info_len;
char *php_self;
size_t php_self_len;
char *document_uri;
size_t document_uri_len;
char *script_filename;
size_t script_filename_len;
char *script_name;
size_t script_name_len;
char *server_name;
size_t server_name_len;
char *server_port;
size_t server_port_len;
char *content_length;
size_t content_length_len;
char *server_protocol;
size_t server_protocol_len;
char *http_host;
size_t http_host_len;
char *request_uri;
size_t request_uri_len;
char *ssl_cipher;
size_t ssl_cipher_len;
zend_string *request_scheme;
zend_string *ssl_protocol;
zend_string *https;
} frankenphp_server_vars;
/**
* Cached interned strings for memory and performance benefits
* Add more hard-coded strings here if needed
*/
#define FRANKENPHP_INTERNED_STRINGS_LIST(X) \
X(remote_addr, "REMOTE_ADDR") \
X(remote_host, "REMOTE_HOST") \
X(remote_port, "REMOTE_PORT") \
X(document_root, "DOCUMENT_ROOT") \
X(path_info, "PATH_INFO") \
X(php_self, "PHP_SELF") \
X(document_uri, "DOCUMENT_URI") \
X(script_filename, "SCRIPT_FILENAME") \
X(script_name, "SCRIPT_NAME") \
X(https, "HTTPS") \
X(httpsLowercase, "https") \
X(httpLowercase, "http") \
X(ssl_protocol, "SSL_PROTOCOL") \
X(request_scheme, "REQUEST_SCHEME") \
X(server_name, "SERVER_NAME") \
X(server_port, "SERVER_PORT") \
X(content_length, "CONTENT_LENGTH") \
X(server_protocol, "SERVER_PROTOCOL") \
X(http_host, "HTTP_HOST") \
X(request_uri, "REQUEST_URI") \
X(ssl_cipher, "SSL_CIPHER") \
X(server_software, "SERVER_SOFTWARE") \
X(frankenphp, "FrankenPHP") \
X(gateway_interface, "GATEWAY_INTERFACE") \
X(cgi11, "CGI/1.1") \
X(auth_type, "AUTH_TYPE") \
X(remote_ident, "REMOTE_IDENT") \
X(content_type, "CONTENT_TYPE") \
X(path_translated, "PATH_TRANSLATED") \
X(query_string, "QUERY_STRING") \
X(remote_user, "REMOTE_USER") \
X(request_method, "REQUEST_METHOD") \
X(tls1, "TLSv1") \
X(tls11, "TLSv1.1") \
X(tls12, "TLSv1.2") \
X(tls13, "TLSv1.3") \
X(on, "on") \
X(empty, "")
typedef struct frankenphp_interned_strings_t {
#define F_DEFINE_STRUCT_FIELD(name, str) zend_string *name;
FRANKENPHP_INTERNED_STRINGS_LIST(F_DEFINE_STRUCT_FIELD)
#undef F_DEFINE_STRUCT_FIELD
} frankenphp_interned_strings_t;
extern frankenphp_interned_strings_t frankenphp_strings;
typedef struct frankenphp_version {
unsigned char major_version;
@@ -50,32 +175,17 @@ void frankenphp_update_local_thread_context(bool is_worker);
int frankenphp_execute_script_cli(char *script, int argc, char **argv,
bool eval);
void frankenphp_register_variables_from_request_info(
zval *track_vars_array, zend_string *content_type,
zend_string *path_translated, zend_string *query_string,
zend_string *auth_user, zend_string *request_method);
void frankenphp_register_known_variable(zend_string *z_key, char *value,
size_t val_len, zval *track_vars_array);
void frankenphp_register_variable_safe(char *key, char *var, size_t val_len,
zval *track_vars_array);
void frankenphp_register_server_vars(zval *track_vars_array,
frankenphp_server_vars vars);
zend_string *frankenphp_init_persistent_string(const char *string, size_t len);
int frankenphp_reset_opcache(void);
int frankenphp_get_current_memory_limit();
void frankenphp_register_single(zend_string *z_key, char *value, size_t val_len,
zval *track_vars_array);
void frankenphp_register_bulk(
zval *track_vars_array, ht_key_value_pair remote_addr,
ht_key_value_pair remote_host, ht_key_value_pair remote_port,
ht_key_value_pair document_root, ht_key_value_pair path_info,
ht_key_value_pair php_self, ht_key_value_pair document_uri,
ht_key_value_pair script_filename, ht_key_value_pair script_name,
ht_key_value_pair https, ht_key_value_pair ssl_protocol,
ht_key_value_pair request_scheme, ht_key_value_pair server_name,
ht_key_value_pair server_port, ht_key_value_pair content_length,
ht_key_value_pair gateway_interface, ht_key_value_pair server_protocol,
ht_key_value_pair server_software, ht_key_value_pair http_host,
ht_key_value_pair auth_type, ht_key_value_pair remote_ident,
ht_key_value_pair request_uri, ht_key_value_pair ssl_cipher);
void register_extensions(zend_module_entry **m, int len);
#endif

BIN
frankenphp.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

View File

@@ -27,6 +27,7 @@ import (
"strings"
"sync"
"testing"
"time"
"github.com/dunglas/frankenphp"
"github.com/dunglas/frankenphp/internal/fastabs"
@@ -136,6 +137,12 @@ func TestMain(m *testing.M) {
slog.SetDefault(slog.New(slog.DiscardHandler))
}
// setup custom environment var for TestWorkerHasOSEnvironmentVariableInSERVER
if os.Setenv("CUSTOM_OS_ENV_VARIABLE", "custom_env_variable_value") != nil {
fmt.Println("Failed to set environment variable for tests")
os.Exit(1)
}
os.Exit(m.Run())
}
@@ -690,11 +697,11 @@ func TestFailingWorker(t *testing.T) {
assert.Error(t, err, "should return an immediate error if workers fail on startup")
}
func TestEnv(t *testing.T) {
testEnv(t, &testOptions{nbParallelRequests: 1})
func TestEnv_module(t *testing.T) {
testEnv(t, &testOptions{nbParallelRequests: 1, phpIni: map[string]string{"variables_order": "EGPCS"}})
}
func TestEnvWorker(t *testing.T) {
testEnv(t, &testOptions{nbParallelRequests: 1, workerScript: "env/test-env.php"})
func TestEnv_worker(t *testing.T) {
testEnv(t, &testOptions{nbParallelRequests: 1, workerScript: "env/test-env.php", phpIni: map[string]string{"variables_order": "EGPCS"}})
}
// testEnv cannot be run in parallel due to https://github.com/golang/go/issues/63567
@@ -709,7 +716,7 @@ func testEnv(t *testing.T, opts *testOptions) {
stdoutStderr, err := cmd.CombinedOutput()
if err != nil {
// php is not installed or other issue, use the hardcoded output below:
stdoutStderr = []byte("Set MY_VAR successfully.\nMY_VAR = HelloWorld\nUnset MY_VAR successfully.\nMY_VAR is unset.\nMY_VAR set to empty successfully.\nMY_VAR = \nUnset NON_EXISTING_VAR successfully.\n")
stdoutStderr = []byte("Set MY_VAR successfully.\nMY_VAR = HelloWorld\nMY_VAR not found in $_ENV.\nMY_VAR not found in $_SERVER.\nUnset MY_VAR successfully.\nMY_VAR is unset.\nMY_VAR set to empty successfully.\nMY_VAR = \nUnset NON_EXISTING_VAR successfully.\nInvalid value was not inserted.\n")
}
assert.Equal(t, string(stdoutStderr), body)
@@ -779,40 +786,6 @@ func testFileUpload(t *testing.T, opts *testOptions) {
}, 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)
var exitError *exec.ExitError
if errors.As(err, &exitError) {
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 TestExecuteCLICode(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", "-r", "echo 'Hello World';")
stdoutStderr, err := cmd.CombinedOutput()
assert.NoError(t, err)
stdoutStderrStr := string(stdoutStderr)
assert.Equal(t, stdoutStderrStr, `Hello World`)
}
func ExampleServeHTTP() {
if err := frankenphp.Init(); err != nil {
panic(err)
@@ -827,15 +800,6 @@ func ExampleServeHTTP() {
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) {
require.NoError(b, frankenphp.Init())
b.Cleanup(frankenphp.Shutdown)
@@ -1120,8 +1084,8 @@ func TestSessionHandlerReset_worker(t *testing.T) {
assert.Contains(t, body1Str, "session.save_handler=user")
// Request 2: Start session without setting a custom handler
// After the fix: session.save_handler should be reset to "files"
// and session_start() should work normally
// The user handler from request 1 is preserved (mod_user_names persist),
// so session_start() should work without crashing.
resp2, err := http.Get(ts.URL + "/session-handler.php?action=start_without_handler")
assert.NoError(t, err)
body2, _ := io.ReadAll(resp2.Body)
@@ -1129,13 +1093,9 @@ func TestSessionHandlerReset_worker(t *testing.T) {
body2Str := string(body2)
// session.save_handler should be reset to "files" (default)
assert.Contains(t, body2Str, "save_handler_before=files",
"session.save_handler INI should be reset to 'files' between requests.\nResponse: %s", body2Str)
// session_start() should succeed
// session_start() should succeed (handlers are preserved)
assert.Contains(t, body2Str, "SESSION_START_RESULT=true",
"session_start() should succeed after INI reset.\nResponse: %s", body2Str)
"session_start() should succeed.\nResponse: %s", body2Str)
// No errors or exceptions should occur
assert.NotContains(t, body2Str, "ERROR:",
@@ -1151,39 +1111,6 @@ func TestSessionHandlerReset_worker(t *testing.T) {
})
}
func TestIniLeakBetweenRequests_worker(t *testing.T) {
runTest(t, func(_ func(http.ResponseWriter, *http.Request), ts *httptest.Server, i int) {
// Request 1: Change INI values
resp1, err := http.Get(ts.URL + "/ini-leak.php?action=change_ini")
assert.NoError(t, err)
body1, _ := io.ReadAll(resp1.Body)
_ = resp1.Body.Close()
assert.Contains(t, string(body1), "INI_CHANGED")
// Request 2: Check if INI values leaked from request 1
resp2, err := http.Get(ts.URL + "/ini-leak.php?action=check_ini")
assert.NoError(t, err)
body2, _ := io.ReadAll(resp2.Body)
_ = resp2.Body.Close()
body2Str := string(body2)
t.Logf("Response: %s", body2Str)
// If INI values leak, this test will fail
assert.Contains(t, body2Str, "NO_LEAKS",
"INI values should not leak between requests.\nResponse: %s", body2Str)
assert.NotContains(t, body2Str, "LEAKS_DETECTED",
"INI leaks detected.\nResponse: %s", body2Str)
}, &testOptions{
workerScript: "ini-leak.php",
nbWorkers: 1,
nbParallelRequests: 1,
realServer: true,
})
}
func TestSessionHandlerPreLoopPreserved_worker(t *testing.T) {
runTest(t, func(_ func(http.ResponseWriter, *http.Request), ts *httptest.Server, i int) {
// Request 1: Check that the pre-loop session handler is preserved
@@ -1233,52 +1160,108 @@ func TestSessionHandlerPreLoopPreserved_worker(t *testing.T) {
})
}
func TestIniPreLoopPreserved_worker(t *testing.T) {
func TestSessionNoLeakBetweenRequests_worker(t *testing.T) {
runTest(t, func(_ func(http.ResponseWriter, *http.Request), ts *httptest.Server, i int) {
// Request 1: Check that pre-loop INI values are present
resp1, err := http.Get(ts.URL + "/worker-with-ini.php?action=check")
// Client A: Set a secret value in session
clientA := &http.Client{}
resp1, err := clientA.Get(ts.URL + "/session-leak.php?action=set&value=secret_A&client_id=clientA")
assert.NoError(t, err)
body1, _ := io.ReadAll(resp1.Body)
_ = resp1.Body.Close()
body1Str := string(body1)
t.Logf("Request 1 response: %s", body1Str)
assert.Contains(t, body1Str, "precision=8",
"Pre-loop precision should be 8")
assert.Contains(t, body1Str, "display_errors=0",
"Pre-loop display_errors should be 0")
assert.Contains(t, body1Str, "PRELOOP_INI_PRESERVED",
"Pre-loop INI values should be preserved")
t.Logf("Client A set session: %s", body1Str)
assert.Contains(t, body1Str, "SESSION_SET")
assert.Contains(t, body1Str, "secret=secret_A")
// Request 2: Change INI values during request
resp2, err := http.Get(ts.URL + "/worker-with-ini.php?action=change_ini")
// Client B: Check that session is empty (no cookie, should not see Client A's data)
clientB := &http.Client{}
resp2, err := clientB.Get(ts.URL + "/session-leak.php?action=check_empty")
assert.NoError(t, err)
body2, _ := io.ReadAll(resp2.Body)
_ = resp2.Body.Close()
body2Str := string(body2)
t.Logf("Request 2 response: %s", body2Str)
assert.Contains(t, body2Str, "INI_CHANGED")
assert.Contains(t, body2Str, "precision=5",
"INI should be changed during request")
t.Logf("Client B check empty: %s", body2Str)
assert.Contains(t, body2Str, "SESSION_CHECK")
assert.Contains(t, body2Str, "SESSION_EMPTY=true",
"Client B should have empty session, not see Client A's data.\nResponse: %s", body2Str)
assert.NotContains(t, body2Str, "secret_A",
"Client A's secret should not leak to Client B.\nResponse: %s", body2Str)
// Request 3: Check that pre-loop INI values are restored
resp3, err := http.Get(ts.URL + "/worker-with-ini.php?action=check")
// Client C: Read session without cookie (should also be empty)
clientC := &http.Client{}
resp3, err := clientC.Get(ts.URL + "/session-leak.php?action=get")
assert.NoError(t, err)
body3, _ := io.ReadAll(resp3.Body)
_ = resp3.Body.Close()
body3Str := string(body3)
t.Logf("Request 3 response: %s", body3Str)
assert.Contains(t, body3Str, "precision=8",
"Pre-loop precision should be restored to 8.\nResponse: %s", body3Str)
assert.Contains(t, body3Str, "display_errors=0",
"Pre-loop display_errors should be restored to 0.\nResponse: %s", body3Str)
assert.Contains(t, body3Str, "PRELOOP_INI_PRESERVED",
"Pre-loop INI values should be restored after request changes.\nResponse: %s", body3Str)
t.Logf("Client C get session: %s", body3Str)
assert.Contains(t, body3Str, "SESSION_READ")
assert.Contains(t, body3Str, "secret=NOT_FOUND",
"Client C should not find any secret.\nResponse: %s", body3Str)
assert.Contains(t, body3Str, "client_id=NOT_FOUND",
"Client C should not find any client_id.\nResponse: %s", body3Str)
}, &testOptions{
workerScript: "worker-with-ini.php",
workerScript: "session-leak.php",
nbWorkers: 1,
nbParallelRequests: 1,
realServer: true,
})
}
func TestSessionNoLeakAfterExit_worker(t *testing.T) {
runTest(t, func(_ func(http.ResponseWriter, *http.Request), ts *httptest.Server, i int) {
// Client A: Set a secret value in session and call exit(1)
clientA := &http.Client{}
resp1, err := clientA.Get(ts.URL + "/session-leak.php?action=set_and_exit&value=exit_secret&client_id=exitClient")
assert.NoError(t, err)
body1, _ := io.ReadAll(resp1.Body)
_ = resp1.Body.Close()
body1Str := string(body1)
t.Logf("Client A set and exit: %s", body1Str)
// The response may be incomplete due to exit(1)
assert.Contains(t, body1Str, "BEFORE_EXIT")
// Client B: Check that session is empty (should not see Client A's data)
// Retry until the worker has restarted after exit(1)
clientB := &http.Client{}
var body2Str string
assert.Eventually(t, func() bool {
resp2, err := clientB.Get(ts.URL + "/session-leak.php?action=check_empty")
if err != nil {
return false
}
body2, _ := io.ReadAll(resp2.Body)
_ = resp2.Body.Close()
body2Str = string(body2)
return strings.Contains(body2Str, "SESSION_CHECK")
}, 2*time.Second, 10*time.Millisecond, "Worker did not restart in time after exit(1)")
t.Logf("Client B check empty after exit: %s", body2Str)
assert.Contains(t, body2Str, "SESSION_EMPTY=true",
"Client B should have empty session after Client A's exit(1).\nResponse: %s", body2Str)
assert.NotContains(t, body2Str, "exit_secret",
"Client A's secret should not leak to Client B after exit(1).\nResponse: %s", body2Str)
// Client C: Try to read session (should also be empty)
clientC := &http.Client{}
resp3, err := clientC.Get(ts.URL + "/session-leak.php?action=get")
assert.NoError(t, err)
body3, _ := io.ReadAll(resp3.Body)
_ = resp3.Body.Close()
body3Str := string(body3)
t.Logf("Client C get session after exit: %s", body3Str)
assert.Contains(t, body3Str, "SESSION_READ")
assert.Contains(t, body3Str, "secret=NOT_FOUND",
"Client C should not find any secret after exit(1).\nResponse: %s", body3Str)
}, &testOptions{
workerScript: "session-leak.php",
nbWorkers: 1,
nbParallelRequests: 1,
realServer: true,

19
go.mod
View File

@@ -1,17 +1,18 @@
module github.com/dunglas/frankenphp
go 1.25.4
go 1.26.0
retract v1.0.0-rc.1 // Human error
require (
github.com/Masterminds/sprig/v3 v3.3.0
github.com/dunglas/mercure v0.21.7
github.com/e-dant/watcher/watcher-go v0.0.0-20260104182512-c28e9078050a
github.com/dunglas/mercure v0.21.11
github.com/e-dant/watcher v0.0.0-20260223030516-06f84a1314be
github.com/maypok86/otter/v2 v2.3.0
github.com/prometheus/client_golang v1.23.2
github.com/stretchr/testify v1.11.1
golang.org/x/net v0.49.0
golang.org/x/net v0.51.0
golang.org/x/text v0.34.0
)
require (
@@ -19,7 +20,7 @@ require (
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.4.0 // indirect
github.com/MauriceGit/skiplist v0.0.0-20211105230623-77f5c8d3e145 // indirect
github.com/RoaringBitmap/roaring/v2 v2.14.4 // indirect
github.com/RoaringBitmap/roaring/v2 v2.14.5 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bits-and-blooms/bitset v1.24.4 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
@@ -34,6 +35,7 @@ require (
github.com/gorilla/handlers v1.5.2 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/huandu/xstrings v1.5.0 // indirect
github.com/klauspost/compress v1.18.4 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
@@ -43,7 +45,7 @@ require (
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/procfs v0.19.2 // indirect
github.com/prometheus/procfs v0.20.1 // indirect
github.com/rogpeppe/go-internal v1.13.1 // indirect
github.com/rs/cors v1.11.1 // indirect
github.com/sagikazarmark/locafero v0.12.0 // indirect
@@ -58,9 +60,8 @@ require (
go.etcd.io/bbolt v1.4.3 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.47.0 // indirect
golang.org/x/sys v0.40.0 // indirect
golang.org/x/text v0.33.0 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/sys v0.41.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

36
go.sum
View File

@@ -8,8 +8,8 @@ github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
github.com/MauriceGit/skiplist v0.0.0-20211105230623-77f5c8d3e145 h1:1yw6O62BReQ+uA1oyk9XaQTvLhcoHWmoQAgXmDFXpIY=
github.com/MauriceGit/skiplist v0.0.0-20211105230623-77f5c8d3e145/go.mod h1:877WBceefKn14QwVVn4xRFUsHsZb9clICgdeTj4XsUg=
github.com/RoaringBitmap/roaring/v2 v2.14.4 h1:4aKySrrg9G/5oRtJ3TrZLObVqxgQ9f1znCRBwEwjuVw=
github.com/RoaringBitmap/roaring/v2 v2.14.4/go.mod h1:oMvV6omPWr+2ifRdeZvVJyaz+aoEUopyv5iH0u/+wbY=
github.com/RoaringBitmap/roaring/v2 v2.14.5 h1:ckd0o545JqDPeVJDgeFoaM21eBixUnlWfYgjE5VnyWw=
github.com/RoaringBitmap/roaring/v2 v2.14.5/go.mod h1:eq4wdNXxtJIS/oikeCzdX1rBzek7ANzbth041hrU8Q4=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bits-and-blooms/bitset v1.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoGXLwLQOZE=
@@ -18,12 +18,12 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dunglas/mercure v0.21.7 h1:QtxQr9IMqJdlLxpsCjdBoDkJm4r4HXIEQCLl5obzI+o=
github.com/dunglas/mercure v0.21.7/go.mod h1:jze6G0/ha0j/YJnAbQRcMLE58/LoRogquwaRQPGjOuA=
github.com/dunglas/mercure v0.21.11 h1:4Sd/Q77j8uh9SI5D9ZMg5sePlWs336+9CKxDQC1FV34=
github.com/dunglas/mercure v0.21.11/go.mod h1:WPMgfqonUiO1qB+W8Tya63Ngag9ZwplGMXSOy8P/uMg=
github.com/dunglas/skipfilter v1.0.0 h1:JG9SgGg4n6BlFwuTYzb9RIqjH7PfwszvWehanrYWPF4=
github.com/dunglas/skipfilter v1.0.0/go.mod h1:ryhr8j7CAHSjzeN7wI6YEuwoArQ3OQmRqWWVCEAfb9w=
github.com/e-dant/watcher/watcher-go v0.0.0-20260104182512-c28e9078050a h1:e/m9m8cJgjzw2Ol7tKTu4B/lM5F3Ym7ryKI+oyw0T8Y=
github.com/e-dant/watcher/watcher-go v0.0.0-20260104182512-c28e9078050a/go.mod h1:sVUOkwtftoj71nnJRG2S0oWNfXFdKpz/M9vK0z06nmM=
github.com/e-dant/watcher v0.0.0-20260223030516-06f84a1314be h1:vqHrvilasyJcnru/0Z4FoojsQJUIfXGVplte7JtupfY=
github.com/e-dant/watcher v0.0.0-20260223030516-06f84a1314be/go.mod h1:PmV4IVmBJVqT2NcfTGN4+sZ+qGe3PA0qkphAtOHeFG0=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
@@ -46,8 +46,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -74,8 +74,8 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
@@ -108,16 +108,16 @@ go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

74
install.ps1 Normal file
View File

@@ -0,0 +1,74 @@
#Requires -Version 5.1
<#
.SYNOPSIS
Downloads and installs the latest FrankenPHP release for Windows.
.DESCRIPTION
This script downloads the latest FrankenPHP Windows release from GitHub
and extracts it to the specified directory (~\.frankenphp by default).
Usage as a one-liner:
irm https://github.com/php/frankenphp/raw/refs/heads/main/install.ps1 | iex
Custom install directory:
$env:FRANKENPHP_INSTALL = 'C:\frankenphp'; irm https://github.com/php/frankenphp/raw/refs/heads/main/install.ps1 | iex
#>
$ErrorActionPreference = "Stop"
if ($env:FRANKENPHP_INSTALL) {
$BinDir = $env:FRANKENPHP_INSTALL
} else {
$BinDir = Join-Path $HOME ".frankenphp"
}
Write-Host "Querying latest FrankenPHP release..." -ForegroundColor Cyan
try {
$release = Invoke-RestMethod -Uri "https://api.github.com/repos/php/frankenphp/releases/latest"
} catch {
Write-Host "Could not query GitHub releases: $_" -ForegroundColor Red
exit 1
}
$asset = $release.assets | Where-Object { $_.name -match "Win32-vs17-x64\.zip$" } | Select-Object -First 1
if (-not $asset) {
Write-Host "Could not find a Windows release asset." -ForegroundColor Red
Write-Host "Check https://github.com/php/frankenphp/releases for available downloads." -ForegroundColor Red
exit 1
}
Write-Host "Downloading $($asset.name)..." -ForegroundColor Cyan
$tmpZip = Join-Path $env:TEMP "frankenphp-windows-$PID.zip"
try {
Invoke-WebRequest -Uri $asset.browser_download_url -OutFile $tmpZip
} catch {
Write-Host "Download failed: $_" -ForegroundColor Red
exit 1
}
Write-Host "Extracting to $BinDir..." -ForegroundColor Cyan
if (-not (Test-Path $BinDir)) {
New-Item -ItemType Directory -Path $BinDir -Force | Out-Null
}
try {
Expand-Archive -Force -Path $tmpZip -DestinationPath $BinDir
} finally {
Remove-Item $tmpZip -Force -ErrorAction SilentlyContinue
}
Write-Host ""
Write-Host "FrankenPHP downloaded successfully to $BinDir" -ForegroundColor Green
# Check if the directory is in PATH
$inPath = $env:PATH -split ";" | Where-Object { $_ -eq $BinDir -or $_ -eq "$BinDir\" }
if (-not $inPath) {
Write-Host "Add $BinDir to your PATH to use frankenphp.exe globally:" -ForegroundColor Yellow
Write-Host " [Environment]::SetEnvironmentVariable('PATH', `"$BinDir;`" + [Environment]::GetEnvironmentVariable('PATH', 'User'), 'User')" -ForegroundColor Gray
}
Write-Host ""
Write-Host "If you like FrankenPHP, please give it a star on GitHub: https://github.com/php/frankenphp" -ForegroundColor Cyan

View File

@@ -16,7 +16,7 @@ ARCH=$(uname -m)
GNU=""
if ! command -v curl >/dev/null 2>&1; then
echo "Please install curl to download FrankenPHP"
echo "Please install curl to download FrankenPHP"
exit 1
fi
@@ -126,9 +126,41 @@ Darwin*)
;;
esac
;;
Windows | MINGW64_NT*)
echo "❗ Use WSL to run FrankenPHP on Windows: https://learn.microsoft.com/windows/wsl/"
exit 1
CYGWIN_NT* | MSYS_NT* | MINGW*)
if ! command -v unzip >/dev/null 2>&1 && ! command -v powershell.exe >/dev/null 2>&1; then
echo "❗ Please install unzip or ensure PowerShell is available to extract FrankenPHP"
exit 1
fi
WIN_ASSET=$(curl -s https://api.github.com/repos/php/frankenphp/releases/latest |
grep -o '"name": *"frankenphp-[^"]*-Win32-vs17-x64\.zip"' | head -1 |
sed 's/"name": *"//;s/"//')
if [ -z "${WIN_ASSET}" ]; then
echo "❗ Could not find a Windows release asset"
echo "❗ Check https://github.com/php/frankenphp/releases for available downloads"
exit 1
fi
echo "📦 Downloading ${bold}FrankenPHP${normal} for Windows (x64):"
TMPZIP="/tmp/frankenphp-windows-$$.zip"
curl -L --progress-bar "https://github.com/php/frankenphp/releases/latest/download/${WIN_ASSET}" -o "${TMPZIP}"
echo "📂 Extracting to ${italic}${BIN_DIR}${normal}..."
if command -v unzip >/dev/null 2>&1; then
unzip -o -q "${TMPZIP}" -d "${BIN_DIR}"
else
powershell.exe -Command "Expand-Archive -Force -Path '$(cygpath -w "${TMPZIP}")' -DestinationPath '$(cygpath -w "${BIN_DIR}")'"
fi
rm -f "${TMPZIP}"
echo
echo "🥳 FrankenPHP downloaded successfully to ${italic}${BIN_DIR}${normal}"
echo "🔧 Add ${italic}$(cygpath -w "${BIN_DIR}")${normal} to your Windows PATH to use ${italic}frankenphp.exe${normal} globally."
echo
echo "⭐ If you like FrankenPHP, please give it a star on GitHub: ${italic}https://github.com/php/frankenphp${normal}"
exit 0
;;
*)
THE_ARCH_BIN=""

View File

@@ -1,3 +1,5 @@
//go:build unix
package cpu
// #include <time.h>

View File

@@ -5,7 +5,7 @@ import (
)
// ProbeCPUs fallback that always determines that the CPU limits are not reached
func ProbeCPUs(probeTime time.Duration, maxCPUUsage float64, abort chan struct{}) bool {
func ProbeCPUs(probeTime time.Duration, _ float64, abort chan struct{}) bool {
select {
case <-abort:
return false

View File

@@ -75,7 +75,7 @@ func (s *IntegrationTestSuite) createGoModule(sourceFile string) (string, error)
goModContent := fmt.Sprintf(`module %s
go 1.25
go 1.26
require github.com/dunglas/frankenphp v0.0.0

View File

@@ -92,7 +92,7 @@ func (pp *ParameterParser) generateParamParsing(params []phpParameter, requiredC
}
var builder strings.Builder
builder.WriteString(fmt.Sprintf(" ZEND_PARSE_PARAMETERS_START(%d, %d)", requiredCount, len(params)))
_, _ = fmt.Fprintf(&builder, " ZEND_PARSE_PARAMETERS_START(%d, %d)", requiredCount, len(params))
optionalStarted := false
for _, param := range params {

View File

@@ -16,7 +16,7 @@ func (pfg *PHPFuncGenerator) generate(fn phpFunction) string {
paramInfo := pfg.paramParser.analyzeParameters(fn.Params)
funcName := NamespacedName(pfg.namespace, fn.Name)
builder.WriteString(fmt.Sprintf("PHP_FUNCTION(%s)\n{\n", funcName))
_, _ = fmt.Fprintf(&builder, "PHP_FUNCTION(%s)\n{\n", funcName)
if decl := pfg.paramParser.generateParamDeclarations(fn.Params); decl != "" {
builder.WriteString(decl + "\n")

View File

@@ -3,6 +3,7 @@ package extgen
import (
"os"
"path/filepath"
"runtime"
"testing"
"github.com/stretchr/testify/assert"
@@ -68,6 +69,10 @@ func TestWriteFile(t *testing.T) {
assert.NoError(t, err, "Failed to stat file")
expectedMode := os.FileMode(0644)
if runtime.GOOS == "windows" {
expectedMode = os.FileMode(0666)
}
assert.Equal(t, expectedMode, info.Mode().Perm(), "writeFile() wrong permissions")
})
}

View File

@@ -9,5 +9,6 @@ import (
// FastAbs can't be optimized on Windows because the
// syscall.FullPath function takes an input.
func FastAbs(path string) (string, error) {
return filepath.Abs(path)
// Normalize forward slashes to backslashes for Windows compatibility
return filepath.Abs(filepath.FromSlash(path))
}

View File

@@ -6,8 +6,9 @@ import (
"os"
"path/filepath"
)
var (
wd string
wd string
wderr error
)
@@ -19,7 +20,7 @@ func init() {
}
canonicalWD, err := filepath.EvalSymlinks(wd)
if err != nil {
if err == nil {
wd = canonicalWD
}
}
@@ -38,4 +39,3 @@ func FastAbs(path string) (string, error) {
return filepath.Join(wd, path), nil
}

View File

@@ -4,40 +4,71 @@ import "C"
import (
"slices"
"sync"
"sync/atomic"
"time"
)
type State string
type State int
const (
// lifecycle States of a thread
Reserved State = "reserved"
Booting State = "booting"
BootRequested State = "boot requested"
ShuttingDown State = "shutting down"
Done State = "done"
Reserved State = iota
Booting
BootRequested
ShuttingDown
Done
// these States are 'stable' and safe to transition from at any time
Inactive State = "inactive"
Ready State = "ready"
Inactive
Ready
// States necessary for restarting workers
Restarting State = "restarting"
Yielding State = "yielding"
Restarting
Yielding
// States necessary for transitioning between different handlers
TransitionRequested State = "transition requested"
TransitionInProgress State = "transition in progress"
TransitionComplete State = "transition complete"
TransitionRequested
TransitionInProgress
TransitionComplete
)
func (s State) String() string {
switch s {
case Reserved:
return "reserved"
case Booting:
return "booting"
case BootRequested:
return "boot requested"
case ShuttingDown:
return "shutting down"
case Done:
return "done"
case Inactive:
return "inactive"
case Ready:
return "ready"
case Restarting:
return "restarting"
case Yielding:
return "yielding"
case TransitionRequested:
return "transition requested"
case TransitionInProgress:
return "transition in progress"
case TransitionComplete:
return "transition complete"
default:
return "unknown"
}
}
type ThreadState struct {
currentState State
mu sync.RWMutex
subscribers []stateSubscriber
// how long threads have been waiting in stable states
waitingSince time.Time
isWaiting bool
// how long threads have been waiting in stable states (unix ms, 0 = not waiting)
waitingSince atomic.Int64
}
type stateSubscriber struct {
@@ -74,7 +105,7 @@ func (ts *ThreadState) CompareAndSwap(compareTo State, swapTo State) bool {
}
func (ts *ThreadState) Name() string {
return string(ts.Get())
return ts.Get().String()
}
func (ts *ThreadState) Get() State {
@@ -97,29 +128,28 @@ func (ts *ThreadState) notifySubscribers(nextState State) {
return
}
var newSubscribers []stateSubscriber
// notify subscribers to the state change
n := 0
for _, sub := range ts.subscribers {
if !slices.Contains(sub.states, nextState) {
newSubscribers = append(newSubscribers, sub)
ts.subscribers[n] = sub
n++
continue
}
close(sub.ch)
}
ts.subscribers = newSubscribers
ts.subscribers = ts.subscribers[:n]
}
// block until the thread reaches a certain state
// WaitFor blocks until the thread reaches a certain state
func (ts *ThreadState) WaitFor(states ...State) {
ts.mu.Lock()
if slices.Contains(states, ts.currentState) {
ts.mu.Unlock()
return
}
sub := stateSubscriber{
states: states,
ch: make(chan struct{}),
@@ -129,7 +159,7 @@ func (ts *ThreadState) WaitFor(states ...State) {
<-sub.ch
}
// safely request a state change from a different goroutine
// RequestSafeStateChange safely requests a state change from a different goroutine
func (ts *ThreadState) RequestSafeStateChange(nextState State) bool {
ts.mu.Lock()
switch ts.currentState {
@@ -156,39 +186,27 @@ func (ts *ThreadState) RequestSafeStateChange(nextState State) bool {
// MarkAsWaiting hints that the thread reached a stable state and is waiting for requests or shutdown
func (ts *ThreadState) MarkAsWaiting(isWaiting bool) {
ts.mu.Lock()
if isWaiting {
ts.isWaiting = true
ts.waitingSince = time.Now()
ts.waitingSince.Store(time.Now().UnixMilli())
} else {
ts.isWaiting = false
ts.waitingSince.Store(0)
}
ts.mu.Unlock()
}
// IsInWaitingState returns true if a thread is waiting for a request or shutdown
func (ts *ThreadState) IsInWaitingState() bool {
ts.mu.RLock()
isWaiting := ts.isWaiting
ts.mu.RUnlock()
return isWaiting
return ts.waitingSince.Load() != 0
}
// WaitTime returns the time since the thread is waiting in a stable state in ms
func (ts *ThreadState) WaitTime() int64 {
ts.mu.RLock()
waitTime := int64(0)
if ts.isWaiting {
waitTime = time.Now().UnixMilli() - ts.waitingSince.UnixMilli()
since := ts.waitingSince.Load()
if since == 0 {
return 0
}
ts.mu.RUnlock()
return waitTime
return time.Now().UnixMilli() - since
}
func (ts *ThreadState) SetWaitTime(t time.Time) {
ts.mu.Lock()
ts.waitingSince = t
ts.mu.Unlock()
ts.waitingSince.Store(t.UnixMilli())
}

View File

@@ -1,6 +1,7 @@
#ifndef _EXTENSIONS_H
#define _EXTENSIONS_H
#include "../../frankenphp.h"
#include <php.h>
extern zend_module_entry module1_entry;

View File

@@ -1,3 +1,4 @@
#include "extension.h"
#include <php.h>
#include <zend_exceptions.h>

View File

@@ -1,13 +1,14 @@
package testext
// #cgo darwin pkg-config: libxml-2.0
// #cgo CFLAGS: -Wall -Werror
// #cgo CFLAGS: -I/usr/local/include -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 unix CFLAGS: -Wall -Werror
// #cgo unix CFLAGS: -I/usr/local/include -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 linux CFLAGS: -D_GNU_SOURCE
// #cgo darwin CFLAGS: -I/opt/homebrew/include
// #cgo LDFLAGS: -L/usr/local/lib -L/usr/lib -lphp -lm -lutil
// #cgo unix LDFLAGS: -L/usr/local/lib -L/usr/lib -lphp -lm -lutil
// #cgo linux LDFLAGS: -ldl -lresolv
// #cgo darwin LDFLAGS: -Wl,-rpath,/usr/local/lib -L/opt/homebrew/lib -L/opt/homebrew/opt/libiconv/lib -liconv -ldl
// #cgo windows CFLAGS: -D_WINDOWS -DWINDOWS=1 -DZEND_WIN32=1 -DPHP_WIN32=1 -DWIN32 -D_MBCS -D_USE_MATH_DEFINES -DNDebug -DNDEBUG -DZEND_DEBUG=0 -DZTS=1 -DFD_SETSIZE=256
// #include "extension.h"
import "C"
import (

View File

@@ -13,7 +13,6 @@ func main() {
ctx := context.Background()
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
if err := frankenphp.Init(frankenphp.WithContext(ctx), frankenphp.WithLogger(logger)); err != nil {
panic(err)
}

View File

@@ -11,6 +11,8 @@ import (
"github.com/e-dant/watcher/watcher-go"
)
const sep = string(filepath.Separator)
type pattern struct {
patternGroup *PatternGroup
value string
@@ -39,8 +41,11 @@ func (p *pattern) parse() (err error) {
p.value = absPattern
volumeName := filepath.VolumeName(p.value)
p.value = strings.TrimPrefix(p.value, volumeName)
// then we split the pattern to determine where the directory ends and the pattern starts
splitPattern := strings.Split(absPattern, string(filepath.Separator))
splitPattern := strings.Split(p.value, sep)
patternWithoutDir := ""
for i, part := range splitPattern {
isFilename := i == len(splitPattern)-1 && strings.Contains(part, ".")
@@ -57,11 +62,15 @@ func (p *pattern) parse() (err error) {
// now we split the pattern according to the recursive '**' syntax
p.parsedValues = strings.Split(patternWithoutDir, "**")
for i, pp := range p.parsedValues {
p.parsedValues[i] = strings.Trim(pp, string(filepath.Separator))
p.parsedValues[i] = strings.Trim(pp, sep)
}
// remove the trailing separator and add leading separator
p.value = string(filepath.Separator) + strings.Trim(p.value, string(filepath.Separator))
// remove the trailing separator and add leading separator (except on Windows)
if volumeName == "" {
p.value = sep + strings.Trim(p.value, sep)
} else {
p.value = volumeName + sep + strings.Trim(p.value, sep)
}
// try to canonicalize the path
canonicalPattern, err := filepath.EvalSymlinks(p.value)
@@ -123,7 +132,7 @@ func (p *pattern) isValidPattern(fileName string) bool {
}
// remove the directory path and separator from the filename
fileNameWithoutDir := strings.TrimPrefix(strings.TrimPrefix(fileName, p.value), string(filepath.Separator))
fileNameWithoutDir := strings.TrimPrefix(strings.TrimPrefix(fileName, p.value), sep)
// if the pattern has size 1 we can match it directly against the filename
if len(p.parsedValues) == 1 {
@@ -134,12 +143,12 @@ func (p *pattern) isValidPattern(fileName string) bool {
}
func (p *pattern) matchPatterns(fileName string) bool {
partsToMatch := strings.Split(fileName, string(filepath.Separator))
partsToMatch := strings.Split(fileName, sep)
cursor := 0
// if there are multiple parsedValues due to '**' we need to match them individually
for i, pattern := range p.parsedValues {
patternSize := strings.Count(pattern, string(filepath.Separator)) + 1
patternSize := strings.Count(pattern, sep) + 1
// if we are at the last pattern we will start matching from the end of the filename
if i == len(p.parsedValues)-1 {
@@ -157,7 +166,7 @@ func (p *pattern) matchPatterns(fileName string) bool {
}
cursor = j
subPattern := strings.Join(partsToMatch[j:j+patternSize], string(filepath.Separator))
subPattern := strings.Join(partsToMatch[j:j+patternSize], sep)
if matchCurlyBracePattern(pattern, subPattern) {
cursor = j + patternSize - 1

View File

@@ -4,6 +4,7 @@ package watcher
import (
"path/filepath"
"strings"
"testing"
"github.com/e-dant/watcher/watcher-go"
@@ -11,16 +12,42 @@ import (
"github.com/stretchr/testify/require"
)
func normalizePath(t *testing.T, path string) string {
t.Helper()
if filepath.Separator == '/' {
return path
}
path = filepath.FromSlash(path)
if strings.HasPrefix(path, "\\") {
path = "C:\\" + path[1:]
}
return path
}
func newPattern(t *testing.T, value string) pattern {
t.Helper()
p := pattern{value: normalizePath(t, value)}
require.NoError(t, p.parse())
return p
}
func TestDisallowOnEventTypeBiggerThan3(t *testing.T) {
w := pattern{value: "/some/path"}
require.NoError(t, w.parse())
t.Parallel()
w := newPattern(t, "/some/path")
assert.False(t, w.allowReload(&watcher.Event{PathName: "/some/path/watch-me.php", EffectType: watcher.EffectTypeOwner}))
}
func TestDisallowOnPathTypeBiggerThan2(t *testing.T) {
w := pattern{value: "/some/path"}
require.NoError(t, w.parse())
t.Parallel()
w := newPattern(t, "/some/path")
assert.False(t, w.allowReload(&watcher.Event{PathName: "/some/path/watch-me.php", PathType: watcher.PathTypeSymLink}))
}
@@ -59,7 +86,7 @@ func TestValidRecursiveDirectories(t *testing.T) {
data := []struct {
pattern string
dir string
file string
}{
{"/path", "/path/file.php"},
{"/path", "/path/subpath/file.php"},
@@ -77,7 +104,7 @@ func TestValidRecursiveDirectories(t *testing.T) {
t.Run(d.pattern, func(t *testing.T) {
t.Parallel()
shouldMatch(t, d.pattern, d.dir)
assertPatternMatch(t, d.pattern, d.file)
})
}
}
@@ -98,7 +125,7 @@ func TestInvalidRecursiveDirectories(t *testing.T) {
t.Run(d.pattern, func(t *testing.T) {
t.Parallel()
shouldNotMatch(t, d.pattern, d.dir)
assertPatternNotMatch(t, d.pattern, d.dir)
})
}
}
@@ -122,7 +149,7 @@ func TestValidNonRecursiveFilePatterns(t *testing.T) {
t.Run(d.pattern, func(t *testing.T) {
t.Parallel()
shouldMatch(t, d.pattern, d.dir)
assertPatternMatch(t, d.pattern, d.dir)
})
}
}
@@ -145,7 +172,7 @@ func TestInValidNonRecursiveFilePatterns(t *testing.T) {
t.Run(d.pattern, func(t *testing.T) {
t.Parallel()
shouldNotMatch(t, d.pattern, d.dir)
assertPatternNotMatch(t, d.pattern, d.dir)
})
}
}
@@ -170,7 +197,7 @@ func TestValidRecursiveFilePatterns(t *testing.T) {
t.Run(d.pattern, func(t *testing.T) {
t.Parallel()
shouldMatch(t, d.pattern, d.dir)
assertPatternMatch(t, d.pattern, d.dir)
})
}
}
@@ -198,7 +225,7 @@ func TestInvalidRecursiveFilePatterns(t *testing.T) {
t.Run(d.pattern, func(t *testing.T) {
t.Parallel()
shouldNotMatch(t, d.pattern, d.dir)
assertPatternNotMatch(t, d.pattern, d.dir)
})
}
}
@@ -225,13 +252,14 @@ func TestValidDirectoryPatterns(t *testing.T) {
t.Run(d.pattern, func(t *testing.T) {
t.Parallel()
shouldMatch(t, d.pattern, d.dir)
assertPatternMatch(t, d.pattern, d.dir)
})
}
}
func TestInvalidDirectoryPatterns(t *testing.T) {
t.Parallel()
data := []struct {
pattern string
dir string
@@ -254,15 +282,17 @@ func TestInvalidDirectoryPatterns(t *testing.T) {
t.Run(d.pattern, func(t *testing.T) {
t.Parallel()
shouldNotMatch(t, d.pattern, d.dir)
assertPatternNotMatch(t, d.pattern, d.dir)
})
}
}
func TestValidCurlyBracePatterns(t *testing.T) {
t.Parallel()
data := []struct {
pattern string
dir string
file string
}{
{"/path/*.{php}", "/path/file.php"},
{"/path/*.{php,twig}", "/path/file.php"},
@@ -282,12 +312,14 @@ func TestValidCurlyBracePatterns(t *testing.T) {
t.Run(d.pattern, func(t *testing.T) {
t.Parallel()
shouldMatch(t, d.pattern, d.dir)
assertPatternMatch(t, d.pattern, d.file)
})
}
}
func TestInvalidCurlyBracePatterns(t *testing.T) {
t.Parallel()
data := []struct {
pattern string
dir string
@@ -306,52 +338,52 @@ func TestInvalidCurlyBracePatterns(t *testing.T) {
t.Run(d.pattern, func(t *testing.T) {
t.Parallel()
shouldNotMatch(t, d.pattern, d.dir)
assertPatternNotMatch(t, d.pattern, d.dir)
})
}
}
func TestAnAssociatedEventTriggersTheWatcher(t *testing.T) {
w := pattern{value: "/**/*.php"}
require.NoError(t, w.parse())
t.Parallel()
w := newPattern(t, "/**/*.php")
w.events = make(chan eventHolder)
e := &watcher.Event{PathName: "/path/temporary_file", AssociatedPathName: "/path/file.php"}
e := &watcher.Event{PathName: normalizePath(t, "/path/temporary_file"), AssociatedPathName: normalizePath(t, "/path/file.php")}
go w.handle(e)
assert.Equal(t, e, (<-w.events).event)
}
func relativeDir(t *testing.T, relativePath string) string {
t.Helper()
dir, err := filepath.Abs("./" + relativePath)
assert.NoError(t, err)
return dir
}
func hasDir(t *testing.T, p string, dir string) {
t.Helper()
w := pattern{value: p}
require.NoError(t, w.parse())
w := newPattern(t, p)
assert.Equal(t, dir, w.value)
assert.Equal(t, normalizePath(t, dir), w.value)
}
func shouldMatch(t *testing.T, p string, fileName string) {
func assertPatternMatch(t *testing.T, p, fileName string) {
t.Helper()
w := pattern{value: p}
require.NoError(t, w.parse())
w := newPattern(t, p)
assert.True(t, w.allowReload(&watcher.Event{PathName: fileName}))
assert.True(t, w.allowReload(&watcher.Event{PathName: normalizePath(t, fileName)}))
}
func shouldNotMatch(t *testing.T, p string, fileName string) {
func assertPatternNotMatch(t *testing.T, p, fileName string) {
t.Helper()
w := pattern{value: p}
require.NoError(t, w.parse())
w := newPattern(t, p)
assert.False(t, w.allowReload(&watcher.Event{PathName: fileName}))
assert.False(t, w.allowReload(&watcher.Event{PathName: normalizePath(t, fileName)}))
}

View File

@@ -3,6 +3,7 @@
package frankenphp
// #include <stdint.h>
// #include "frankenphp.h"
// #include <php.h>
import "C"
import (

View File

@@ -11,7 +11,7 @@ import (
const (
StopReasonCrash = iota
StopReasonRestart
//StopReasonShutdown
StopReasonBootFailure // worker crashed before reaching frankenphp_handle_request
)
type StopReason int
@@ -125,10 +125,14 @@ func (m *PrometheusMetrics) StopWorker(name string, reason StopReason) {
}
m.totalWorkers.WithLabelValues(name).Dec()
m.readyWorkers.WithLabelValues(name).Dec()
// only decrement readyWorkers if the worker actually reached frankenphp_handle_request
if reason != StopReasonBootFailure {
m.readyWorkers.WithLabelValues(name).Dec()
}
switch reason {
case StopReasonCrash:
case StopReasonCrash, StopReasonBootFailure:
m.workerCrashes.WithLabelValues(name).Inc()
case StopReasonRestart:
m.workerRestarts.WithLabelValues(name).Inc()

View File

@@ -30,6 +30,7 @@ type opt struct {
metrics Metrics
phpIni map[string]string
maxWaitTime time.Duration
maxIdleTime time.Duration
}
type workerOpt struct {
@@ -156,6 +157,15 @@ func WithMaxWaitTime(maxWaitTime time.Duration) Option {
}
}
// WithMaxIdleTime configures the max time an autoscaled thread may be idle before being deactivated.
func WithMaxIdleTime(maxIdleTime time.Duration) Option {
return func(o *opt) error {
o.maxIdleTime = maxIdleTime
return nil
}
}
// WithWorkerEnv sets environment variables for the worker
func WithWorkerEnv(env map[string]string) WorkerOption {
return func(w *workerOpt) error {

View File

@@ -10,6 +10,8 @@ command_background="yes"
capabilities="^cap_net_bind_service"
pidfile="/run/frankenphp/frankenphp.pid"
start_stop_daemon_args="--chdir /var/lib/frankenphp"
respawn_delay=3
respawn_max=10
depend() {
need net

View File

@@ -12,6 +12,8 @@ ExecStartPre=/usr/bin/frankenphp validate --config /etc/frankenphp/Caddyfile
ExecStart=/usr/bin/frankenphp run --environ --config /etc/frankenphp/Caddyfile
ExecReload=/usr/bin/frankenphp reload --config /etc/frankenphp/Caddyfile
WorkingDirectory=/var/lib/frankenphp
Restart=on-failure
RestartSec=3s
TimeoutStopSec=5s
LimitNOFILE=1048576
LimitNPROC=512

View File

@@ -12,6 +12,8 @@ ExecStartPre=/usr/bin/frankenphp validate --config /etc/frankenphp/Caddyfile
ExecStart=/usr/bin/frankenphp run --environ --config /etc/frankenphp/Caddyfile
ExecReload=/usr/bin/frankenphp reload --config /etc/frankenphp/Caddyfile
WorkingDirectory=/var/lib/frankenphp
Restart=on-failure
RestartSec=3s
TimeoutStopSec=5s
LimitNOFILE=1048576
LimitNPROC=512

View File

@@ -1,11 +1,9 @@
package frankenphp
// #cgo nocallback frankenphp_new_main_thread
// #cgo nocallback frankenphp_init_persistent_string
// #cgo noescape frankenphp_new_main_thread
// #cgo noescape frankenphp_init_persistent_string
// #include <php_variables.h>
// #include "frankenphp.h"
// #include <php_variables.h>
import "C"
import (
"log/slog"
@@ -20,19 +18,17 @@ import (
// represents the main PHP thread
// the thread needs to keep running as long as all other threads are running
type phpMainThread struct {
state *state.ThreadState
done chan struct{}
numThreads int
maxThreads int
phpIni map[string]string
commonHeaders map[string]*C.zend_string
knownServerKeys map[string]*C.zend_string
sandboxedEnv map[string]*C.zend_string
state *state.ThreadState
done chan struct{}
numThreads int
maxThreads int
phpIni map[string]string
}
var (
phpThreads []*phpThread
mainThread *phpMainThread
phpThreads []*phpThread
mainThread *phpMainThread
commonHeaders map[string]*C.zend_string
)
// initPHPThreads starts the main PHP thread,
@@ -40,12 +36,11 @@ var (
// and reserves a fixed number of possible PHP threads
func initPHPThreads(numThreads int, numMaxThreads int, phpIni map[string]string) (*phpMainThread, error) {
mainThread = &phpMainThread{
state: state.NewThreadState(),
done: make(chan struct{}),
numThreads: numThreads,
maxThreads: numMaxThreads,
phpIni: phpIni,
sandboxedEnv: initializeEnv(),
state: state.NewThreadState(),
done: make(chan struct{}),
numThreads: numThreads,
maxThreads: numMaxThreads,
phpIni: phpIni,
}
// initialize the first thread
@@ -113,15 +108,11 @@ func (mainThread *phpMainThread) start() error {
mainThread.state.WaitFor(state.Ready)
// cache common request headers as zend_strings (HTTP_ACCEPT, HTTP_USER_AGENT, etc.)
mainThread.commonHeaders = make(map[string]*C.zend_string, len(phpheaders.CommonRequestHeaders))
for key, phpKey := range phpheaders.CommonRequestHeaders {
mainThread.commonHeaders[key] = C.frankenphp_init_persistent_string(C.CString(phpKey), C.size_t(len(phpKey)))
}
// cache $_SERVER keys as zend_strings (SERVER_PROTOCOL, SERVER_SOFTWARE, etc.)
mainThread.knownServerKeys = make(map[string]*C.zend_string, len(knownServerKeys))
for _, phpKey := range knownServerKeys {
mainThread.knownServerKeys[phpKey] = C.frankenphp_init_persistent_string(toUnsafeChar(phpKey), C.size_t(len(phpKey)))
if commonHeaders == nil {
commonHeaders = make(map[string]*C.zend_string, len(phpheaders.CommonRequestHeaders))
for key, phpKey := range phpheaders.CommonRequestHeaders {
commonHeaders[key] = newPersistentZendString(phpKey)
}
}
return nil
@@ -198,6 +189,9 @@ func go_get_custom_php_ini(disableTimeouts C.bool) *C.char {
// Pass the php.ini overrides to PHP before startup
// TODO: if needed this would also be possible on a per-thread basis
var overrides strings.Builder
// 32 is an over-estimate for php.ini settings
overrides.Grow(len(mainThread.phpIni) * 32)
for k, v := range mainThread.phpIni {
overrides.WriteString(k)
overrides.WriteByte('=')

View File

@@ -101,9 +101,9 @@ func TestTransitionThreadsWhileDoingRequests(t *testing.T) {
numThreads := 10
numRequestsPerThread := 100
worker1Path := testDataPath + "/transition-worker-1.php"
worker1Path := filepath.Join(testDataPath, "transition-worker-1.php")
worker1Name := "worker-1"
worker2Path := testDataPath + "/transition-worker-2.php"
worker2Path := filepath.Join(testDataPath, "transition-worker-2.php")
worker2Name := "worker-2"
assert.NoError(t, Init(
@@ -185,8 +185,12 @@ func TestFinishBootingAWorkerScript(t *testing.T) {
func TestReturnAnErrorIf2WorkersHaveTheSameFileName(t *testing.T) {
workers = []*worker{}
workersByName = map[string]*worker{}
workersByPath = map[string]*worker{}
w, err1 := newWorker(workerOpt{fileName: testDataPath + "/index.php"})
workers = append(workers, w)
workersByName[w.name] = w
workersByPath[w.fileName] = w
_, err2 := newWorker(workerOpt{fileName: testDataPath + "/index.php"})
assert.NoError(t, err1)
@@ -195,8 +199,12 @@ func TestReturnAnErrorIf2WorkersHaveTheSameFileName(t *testing.T) {
func TestReturnAnErrorIf2ModuleWorkersHaveTheSameName(t *testing.T) {
workers = []*worker{}
workersByName = map[string]*worker{}
workersByPath = map[string]*worker{}
w, err1 := newWorker(workerOpt{fileName: testDataPath + "/index.php", name: "workername"})
workers = append(workers, w)
workersByName[w.name] = w
workersByPath[w.fileName] = w
_, err2 := newWorker(workerOpt{fileName: testDataPath + "/hello.php", name: "workername"})
assert.NoError(t, err1)
@@ -240,9 +248,9 @@ func allPossibleTransitions(worker1Path string, worker2Path string) []func(*phpT
thread.boot()
}
},
func(thread *phpThread) { convertToWorkerThread(thread, getWorkerByPath(worker1Path)) },
func(thread *phpThread) { convertToWorkerThread(thread, workersByPath[worker1Path]) },
convertToInactiveThread,
func(thread *phpThread) { convertToWorkerThread(thread, getWorkerByPath(worker2Path)) },
func(thread *phpThread) { convertToWorkerThread(thread, workersByPath[worker2Path]) },
convertToInactiveThread,
}
}

View File

@@ -16,16 +16,15 @@ import (
// identified by the index in the phpThreads slice
type phpThread struct {
runtime.Pinner
threadIndex int
requestChan chan *frankenPHPContext
drainChan chan struct{}
handlerMu sync.Mutex
handler threadHandler
state *state.ThreadState
sandboxedEnv map[string]*C.zend_string
threadIndex int
requestChan chan *frankenPHPContext
drainChan chan struct{}
handlerMu sync.RWMutex
handler threadHandler
state *state.ThreadState
}
// interface that defines how the callbacks from the C thread should be handled
// threadHandler defines how the callbacks from the C thread should be handled
type threadHandler interface {
name() string
beforeScriptExecution() string
@@ -66,9 +65,12 @@ func (thread *phpThread) boot() {
// shutdown the underlying PHP thread
func (thread *phpThread) shutdown() {
if !thread.state.RequestSafeStateChange(state.ShuttingDown) {
// already shutting down or done
// already shutting down or done, wait for the C thread to finish
thread.state.WaitFor(state.Done, state.Reserved)
return
}
close(thread.drainChan)
thread.state.WaitFor(state.Done)
thread.drainChan = make(chan struct{})
@@ -79,17 +81,19 @@ func (thread *phpThread) shutdown() {
}
}
// change the thread handler safely
// setHandler changes the thread handler safely
// must be called from outside the PHP thread
func (thread *phpThread) setHandler(handler threadHandler) {
thread.handlerMu.Lock()
defer thread.handlerMu.Unlock()
if !thread.state.RequestSafeStateChange(state.TransitionRequested) {
// no state change allowed == shutdown or done
return
}
close(thread.drainChan)
thread.state.WaitFor(state.TransitionInProgress)
thread.handler = handler
thread.drainChan = make(chan struct{})
@@ -120,9 +124,10 @@ func (thread *phpThread) context() context.Context {
}
func (thread *phpThread) name() string {
thread.handlerMu.Lock()
thread.handlerMu.RLock()
name := thread.handler.name()
thread.handlerMu.Unlock()
thread.handlerMu.RUnlock()
return name
}
@@ -133,6 +138,7 @@ func (thread *phpThread) pinString(s string) *C.char {
if sData == nil {
return nil
}
thread.Pin(sData)
return (*C.char)(unsafe.Pointer(sData))

View File

@@ -1,11 +1,14 @@
package frankenphp
import (
"errors"
"log/slog"
"net/http"
"path/filepath"
"strings"
"sync"
"sync/atomic"
"unicode/utf8"
"github.com/dunglas/frankenphp/internal/fastabs"
)
@@ -14,6 +17,8 @@ import (
type RequestOption func(h *frankenPHPContext) error
var (
ErrInvalidSplitPath = errors.New("split path contains non-ASCII characters")
documentRootCache sync.Map
documentRootCacheLen atomic.Uint32
)
@@ -71,15 +76,40 @@ func WithRequestResolvedDocumentRoot(documentRoot string) RequestOption {
// actual resource (CGI script) name, and the second piece will be set to
// PATH_INFO for the CGI script to use.
//
// Split paths can only contain ASCII characters.
// Comparison is case-insensitive.
//
// 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 {
func WithRequestSplitPath(splitPath []string) (RequestOption, error) {
var b strings.Builder
for i, split := range splitPath {
b.Grow(len(split))
for j := 0; j < len(split); j++ {
c := split[j]
if c >= utf8.RuneSelf {
return nil, ErrInvalidSplitPath
}
if 'A' <= c && c <= 'Z' {
b.WriteByte(c + 'a' - 'A')
} else {
b.WriteByte(c)
}
}
splitPath[i] = b.String()
b.Reset()
}
return func(o *frankenPHPContext) error {
o.splitPath = splitPath
return nil
}
}, nil
}
type PreparedEnv = map[string]string
@@ -128,7 +158,7 @@ func WithRequestLogger(logger *slog.Logger) RequestOption {
func WithWorkerName(name string) RequestOption {
return func(o *frankenPHPContext) error {
if name != "" {
o.worker = getWorkerByName(name)
o.worker = workersByName[name]
}
return nil

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