Compare commits

...

169 Commits

Author SHA1 Message Date
Kévin Dunglas
e118b9f681 fix: potential crash when using apache_request_headers() 2024-02-01 00:14:33 +01:00
Kévin Dunglas
ab7ce9cb18 ci: temporary fix for Buildx crash 2024-01-31 16:29:45 +01:00
Kévin Dunglas
5afde55ebf ci: debug Docker buildx crash 2024-01-31 15:59:54 +01:00
Kévin Dunglas
da63e700b0 ci: fix scheduled Docker images build (ctd) 2024-01-31 15:21:03 +01:00
Kévin Dunglas
5a8e5f9518 feat: add apache_response_headers() function (#530) 2024-01-31 12:34:30 +01:00
Kévin Dunglas
3d9f344a50 docs: Docker image updates and tags 2024-01-30 22:07:33 +01:00
Kévin Dunglas
175b9a0296 feat: add Brotli compression support (#524) 2024-01-30 18:33:59 +01:00
Kévin Dunglas
65c8720250 ci: push dev Docker images to a dedicated repository 2024-01-30 18:17:02 +01:00
Kévin Dunglas
59dd04234f ci: fix scheduled Docker images build 2024-01-30 15:05:24 +01:00
Kévin Dunglas
36a6daa8ba docs: use tabs instead of spaces in Dockerfiles 2024-01-29 17:17:34 +01:00
Kévin Dunglas
85b36acce1 chore: comment logs directive 2024-01-29 17:17:12 +01:00
Kévin Dunglas
d9618ac917 chore: bump deps 2024-01-29 17:17:12 +01:00
Kévin Dunglas
be08c1e717 docs: run as non-root in Docker containers 2024-01-29 14:40:56 +01:00
Kévin Dunglas
62d53253d1 ci: add a bug report form 2024-01-29 14:08:16 +01:00
Leo Cavalcante
f35e276ee6 docs: fix worker mode Docker example (#516) 2024-01-29 14:06:27 +01:00
Leo Cavalcante
a736a5a2ce fix: adding extra libs to the static build (#517) 2024-01-29 13:59:46 +01:00
Rob Landers
c71e55551c add an issue template (#515) 2024-01-27 16:04:01 +01:00
Kévin Dunglas
7454826d4b ci: use cgocheck2 when possible 2024-01-23 23:14:24 +01:00
Kévin Dunglas
49baf02035 feat: add go_apache_request_headers() 2024-01-23 23:14:24 +01:00
Pierre du Plessis
a92d774742 fix: check for custom php.ini before checking for the Caddyfile (#498) 2024-01-22 22:26:20 +01:00
Pierre du Plessis
effb5805f1 feat: allow custom php.ini file in embedded apps (#497) 2024-01-22 14:01:27 +01:00
Pierre du Plessis
5d32f32639 feat: option to start a Mercure.rocks hub when running php-server (#489)
---------

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2024-01-22 13:35:19 +01:00
Pierre du Plessis
bccbaafc84 feat: load custom Caddyfile from embedded app if it exists (#494)
---------

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2024-01-22 13:31:12 +01:00
Pierre du Plessis
6f108a4203 fix: do not extract embedded app on every execution (#488)
* Do not extract embedded app on every execution

* Add app_checksum.txt to .dockerignore

* Rename embeddedAppHash to embeddedAppChecksum

* Remove check for empty directory
2024-01-21 18:13:08 +01:00
Kévin Dunglas
3bdd6fd072 docs: improved rendering of Laravel Octane options (#479) 2024-01-20 13:31:32 +01:00
Eduardo Rocha
183451a13f Add note about musl libc (#485)
* Add note about musl libc

* Update known-issues.md

* Update known-issues.md

---------

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2024-01-20 13:29:40 +01:00
Jochen
2ef7762068 docs: PHP configuration guide (#486)
* docs: php configuration guide

* Update config.md

---------

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2024-01-20 13:19:45 +01:00
Kévin Dunglas
b624a13430 docs: improve HTTPS documentation (#480) 2024-01-20 11:49:16 +01:00
Kévin Dunglas
00b1d0e4b6 docs: add production deployment guide (#471)
* docs: update README and add guide for deploying FrankenPHP on Digital Ocean

* Update docs/deploy-frankenphp-digital-ocean.md

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>

* Update docs/deploy-frankenphp-digital-ocean.md

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>

* rewrite

* add images

* linter

* missing dot

---------

Co-authored-by: Trusted97 <benux9700@gmail.com>
Co-authored-by: Gianluca <37697178+Trusted97@users.noreply.github.com>
2024-01-12 16:53:50 +01:00
Kévin Dunglas
d4c313f3db ci: fix static binary copy (#451)
* ci: fix static binary copy

* Update static.yaml

Co-authored-by: Rob Landers <landers.robert@gmail.com>

* fix

* fix

* fix

---------

Co-authored-by: Rob Landers <landers.robert@gmail.com>
2024-01-08 12:03:36 +01:00
Mr Alexandre J-S William ELISÉ
f19c153d06 docs: update worker.md to prevent infinite loop (#455)
* Update worker.md to prevent infinite loop

Quick fix typo `<=` rather than `>=` which prevents infinite loop and get out of the loop when MAX_REQUESTS is reached.

* Update worker.md to fix impossible evaluation

Fix impossible case where if MAX_REQUESTS is not defined the rest of the condition is not evaluated as it is an && operator as mentioned by https://github.com/dunglas/frankenphp/discussions/453#discussioncomment-7981535

* Update worker.md change to for loop and remove handler from the loop

Change to for loop and remove handler from the loop based on the feedback https://github.com/dunglas/frankenphp/pull/455#discussion_r1438918811
2024-01-08 08:17:08 +01:00
naoki kuroda
3692818d0c perf: optimize cookie handling in request (#459)
Signed-off-by: nnnkkk7 <kurodanaoki0711pana@gmail.com>
2024-01-08 08:14:49 +01:00
Rob Landers
7830aae549 Ensure we don't modify a shared env (#452)
* Ensure we don't modify a shared env

* Update worker.go

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>

* Update worker.go

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>

---------

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2023-12-31 20:02:11 +01:00
Rob Landers
2055142904 apply just an optimization (#450) 2023-12-30 11:21:21 +01:00
Kévin Dunglas
9b9957f5cf chore: prepare release 1.0.3 2023-12-29 22:50:56 +01:00
Rob Landers
5bda50cbd7 Fix memory leak (#442)
* do not use caddy context

* ensure all handles are cleaned up

* do not export types

* just panic when double deleting a handle

* set the minimal capacity

* remove micro-opt

* move handle cleanup to just before we return from serveHttp

* ensure we clean up cli scripts

* handle cgi-mode free

* micro-optimization: set initial capacity
2023-12-28 00:16:19 +01:00
Kévin Dunglas
11711bb6da chore: prepare release 1.0.2 2023-12-25 17:12:41 +01:00
Kévin Dunglas
497876443c ci: fix static image inspect 2023-12-25 10:20:28 +01:00
Kévin Dunglas
94776119bd ci: fix static image inspect 2023-12-25 02:02:34 +01:00
Kévin Dunglas
229cb9e3e6 ci: fix static tagged builds 2023-12-24 12:35:55 +01:00
Kévin Dunglas
67fdefc416 ci: build Linux aarch64 binaries (#432) 2023-12-24 11:35:59 +01:00
Kévin Dunglas
e962f4394f ci: fix Docker builds 2023-12-24 11:34:48 +01:00
Kévin Dunglas
0054b92115 docs: link to TYPO3 skeleton 2023-12-23 21:39:48 +01:00
Kévin Dunglas
34e4ef1e84 ci: fix scheduled builds 2023-12-23 15:02:09 +01:00
Kévin Dunglas
9bf991ca88 ci: add igbinary extension and LZ4 support to static builds 2023-12-23 14:22:06 +01:00
Kévin Dunglas
64672a267a chore: bump deps (#429) 2023-12-22 16:54:05 +01:00
Kévin Dunglas
f16cb0b5f0 ci: allow building manually (#427) 2023-12-22 16:53:42 +01:00
Kévin Dunglas
efa49848e4 ci: remove armv6 Debian images (#431) 2023-12-22 16:45:51 +01:00
Kévin Dunglas
2eabec8c92 ci: fix static binary release 2023-12-20 19:44:48 +01:00
thecaliskan
f6873efee4 Added Laravel Octane Documentation (#422)
* Added Laravel Octane Documentation

* fixed lint

---------

Co-authored-by: Emre Çalışkan <emre.caliskan@beyn.com.tr>
2023-12-20 18:07:24 +01:00
Kévin Dunglas
47ada94c41 simplify 2023-12-20 15:53:46 +01:00
Kévin Dunglas
a571d990ec ci: add sodium to static binaries 2023-12-20 15:53:31 +01:00
Rafael Kassner
b7c8d4cd49 Add ctype and session PHP extensions to static builder 2023-12-20 00:13:27 +01:00
Kévin Dunglas
7c5f18fe3f ci: fix changelog generation 2023-12-19 14:20:34 +01:00
Kévin Dunglas
0015493543 chore: prepare release 1.0.1 2023-12-18 19:39:47 +01:00
Kévin Dunglas
3709c2a50b chore: switch to upstream C-Thread-Pool 2023-12-18 18:50:50 +01:00
Kévin Dunglas
8dd22269dc ci: downgrade to GitHub Actions artifact v1 (#406) 2023-12-18 17:16:30 +01:00
Kévin Dunglas
f71f9875ca perf: add benchmark (#392) 2023-12-18 15:35:44 +01:00
Kévin Dunglas
184b86c6e3 ci: upgrade upload-artifact action 2023-12-18 15:35:18 +01:00
dependabot[bot]
77f858d009 ci: bump actions/download-artifact from 3 to 4
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 3 to 4.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-18 13:57:28 +01:00
Kévin Dunglas
517e086786 fix: random crashes when reloading (#394) 2023-12-18 09:05:49 +01:00
Kévin Dunglas
ebd5b45dda fix: add back PostreSQL extensions on Mac (#402) 2023-12-17 23:11:24 +01:00
Kévin Dunglas
ebcd937092 fix: skip shebang in CLI mode 2023-12-17 14:40:20 +01:00
Kévin Dunglas
781e8607ae feat: include debug symbols in CGO code 2023-12-17 13:12:12 +01:00
Kévin Dunglas
2ac2e9ec38 ci: remove missed temporary LATEST env var 2023-12-17 13:11:39 +01:00
Kévin Dunglas
b2e92a673a ci: ignore GitHub Actions cache errors 2023-12-17 13:08:17 +01:00
Davy Goossens
b6e2277863 feat: introduce debug symbols flag for static build (#397)
* Introduce debug symbols flag

* Add DEBUG_SYMBOLS customization to the docs
2023-12-17 10:33:37 +01:00
Dave Heineman
9410418c5c docs: use the correct link to the docker tags 2023-12-15 12:30:25 +01:00
Kévin Dunglas
1da3dbc93a docs: xcaddy build flags (#385) 2023-12-14 22:45:09 +01:00
Kévin Dunglas
fd6e28df2a ci: improve linker flags (#383) 2023-12-14 21:50:09 +01:00
Kévin Dunglas
efa8b243c9 chore: improve automaxprocs logs 2023-12-14 21:49:11 +01:00
Kévin Dunglas
e3e2ab3612 ci: add missing Go package on Mac (#381) 2023-12-13 22:34:37 +01:00
Kévin Dunglas
9046b97fa5 ci: rebuild Docker images (if necessary) and binaries every night (#379) 2023-12-13 22:34:04 +01:00
Kévin Dunglas
cb02ce4783 ci: add Docker tags containing the minor and major PHP version (#373) 2023-12-13 09:06:48 +01:00
dependabot[bot]
9b09be22be chore: bump github.com/stretchr/testify from 1.8.1 to 1.8.4
Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.8.1 to 1.8.4.
- [Release notes](https://github.com/stretchr/testify/releases)
- [Commits](https://github.com/stretchr/testify/compare/v1.8.1...v1.8.4)

---
updated-dependencies:
- dependency-name: github.com/stretchr/testify
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-13 00:45:24 +01:00
dependabot[bot]
1564c762ee chore(caddy): bump github.com/dunglas/mercure/caddy in /caddy
Bumps [github.com/dunglas/mercure/caddy](https://github.com/dunglas/mercure) from 0.15.5 to 0.15.6.
- [Release notes](https://github.com/dunglas/mercure/releases)
- [Changelog](https://github.com/dunglas/mercure/blob/main/.goreleaser.yml)
- [Commits](https://github.com/dunglas/mercure/compare/v0.15.5...v0.15.6)

---
updated-dependencies:
- dependency-name: github.com/dunglas/mercure/caddy
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-13 00:44:52 +01:00
dependabot[bot]
4e00ed1a7b chore: bump golang.org/x/net from 0.18.0 to 0.19.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.18.0 to 0.19.0.
- [Commits](https://github.com/golang/net/compare/v0.18.0...v0.19.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-13 00:44:30 +01:00
Jesse Donat
6ba945091b Removes accidental RUN 2023-12-13 00:15:55 +01:00
Kévin Dunglas
e19aa75e72 ci: setup Dependabot for the Go modules 2023-12-12 11:27:59 +01:00
Kévin Dunglas
d7e40985d2 chore: upgrade to Caddy 2.7.6 2023-12-11 16:15:16 +01:00
dependabot[bot]
50feb79326 ci: bump actions/setup-go from 4 to 5
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 4 to 5.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-11 13:21:20 +01:00
Xu Chunyang
b845670f07 Fix the broken link in README.md 2023-12-09 13:01:19 +01:00
Kévin Dunglas
72c22f3c6e ci: fix static pipeline 2023-12-09 00:41:26 +01:00
Peter Fox
d93bfc0cb9 docs: enable symlinks following for Laravel app (#350)
Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2023-12-08 23:47:19 +01:00
Francis Lavoie
b9eae571e5 docs: Caddyfile config adjustments (#345) 2023-12-08 19:09:20 +01:00
Kévin Dunglas
72d983ec5e docs: better embed docs 2023-12-08 15:53:37 +01:00
Kévin Dunglas
d427f55298 ci: fix release script 2023-12-07 17:36:27 +01:00
Kévin Dunglas
5e946fc02a chore: prepare release 1.0.0 2023-12-07 16:25:12 +01:00
Kévin Dunglas
ad1c4500d3 ci(docker): only mark tagged versions as latest 2023-12-07 14:23:45 +01:00
Kévin Dunglas
5c9d7d3f6d docs: link to the website 2023-12-07 00:31:04 +01:00
Kévin Dunglas
58597bfeab ci: better tags for static-builder Docker image 2023-12-07 00:30:21 +01:00
Kévin Dunglas
5235cb9ae1 docs: embed 2023-12-06 23:30:50 +01:00
Kévin Dunglas
e3361c2b3f feat(caddy): allow to start workers using the php-server built-in command (#339) 2023-12-06 18:18:16 +01:00
Kévin Dunglas
dee84ed906 feat: define needed environment variables in static-builder.Dockerfile (#338) 2023-12-06 15:56:40 +01:00
Kévin Dunglas
537f899939 feat: use tar to embed apps (#333) 2023-12-03 19:17:36 +01:00
Kévin Dunglas
7536b89815 feat: allow running embedded CLI scripts 2023-12-03 19:17:17 +01:00
Kévin Dunglas
49d81c4ea2 ci: fix typo 2023-12-03 16:03:17 +01:00
Kévin Dunglas
ee38702846 ci: revert broken Docker push changes 2023-12-03 15:55:12 +01:00
Kévin Dunglas
dd1af53432 ci: fix Docker push 2023-12-03 15:27:31 +01:00
Kévin Dunglas
8664f8f4bc ci: fix static build workflow 2023-12-02 16:01:47 +01:00
Kévin Dunglas
6509cddd2a feat: embed PHP apps into the FrankenPHP binary 2023-12-02 15:40:51 +01:00
Shyim
bb931c99ff remove sqlite3 requirement on darwin, fixes #330 2023-12-02 15:40:37 +01:00
dependabot[bot]
f1ccb0a9c5 ci: bump docker/setup-qemu-action from 2 to 3
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 2 to 3.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](https://github.com/docker/setup-qemu-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: docker/setup-qemu-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-02 14:48:52 +01:00
dependabot[bot]
99e0a7038a ci: bump docker/login-action from 2 to 3
Bumps [docker/login-action](https://github.com/docker/login-action) from 2 to 3.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-02 14:48:21 +01:00
dependabot[bot]
723f40be16 ci: bump docker/setup-buildx-action from 2 to 3
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 2 to 3.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-02 14:48:05 +01:00
Kévin Dunglas
8eb1e9dea9 ci: setup Dependabot for GHA 2023-12-02 13:23:02 +01:00
Kévin Dunglas
c9bf9940d1 ci: add Super-Linter (#323) 2023-12-01 17:26:21 +01:00
Kévin Dunglas
b675d09c49 fix: release script 2023-12-01 13:39:41 +01:00
Kévin Dunglas
e9dfe4000e ci: fix release script 2023-11-30 08:47:18 +01:00
Kévin Dunglas
5a85a11c60 chore: prepare release 1.0.0-rc.4 2023-11-30 08:46:08 +01:00
Kévin Dunglas
de5de146c3 ci: add generic static build script (#322) 2023-11-30 08:07:39 +01:00
Natsuki Ikeguchi
b32e738d75 feat: Upgrade to PHP 8.3
Signed-off-by: Natsuki Ikeguchi <me@s6n.jp>
2023-11-29 10:40:24 +01:00
Kévin Dunglas
c884d26799 docs: don't stop the worker script even if a connection with a client is aborted 2023-11-22 19:14:51 +01:00
Kévin Dunglas
b4aa8038ff feat: detect when worker crashes or terminates normally (#315) 2023-11-21 23:15:07 +01:00
Kévin Dunglas
2e72b50d10 ci: add Apple Silicon build script (#313) 2023-11-19 17:06:18 +01:00
Kévin Dunglas
39b4f75365 ci: re-enable tests with PHP 8.3 (#306) 2023-11-17 11:03:21 +01:00
Kévin Dunglas
d80cc88ae1 chore: prepare release 1.0.0-rc.3 2023-11-16 16:08:03 +01:00
Kévin Dunglas
2c583bcb16 feat: add support for input filters 2023-11-16 15:14:54 +01:00
Kévin Dunglas
aa1d968dcf refactor: faster $_SERVER variables creation 2023-11-16 14:40:52 +01:00
Kévin Dunglas
ca76e3a763 ci: disable tests with 8.3 shared library 2023-11-16 14:06:51 +01:00
Kévin Dunglas
b56b47d187 ci: improve static build steps (#305) 2023-11-16 14:06:34 +01:00
Kévin Dunglas
4c70ae285a chore: bump deps 2023-11-10 19:44:59 +01:00
Kévin Dunglas
3d2c9b6f6f chore: add missing file 2023-11-10 18:39:29 +01:00
Kévin Dunglas
12791636ee chore: add an echo server benchmark 2023-11-10 17:10:16 +01:00
Mr Alexandre J-S William ELISÉ
981f954cf2 Add Joomla on FrankenPHP Example
Hi, FrankenPHP Team,
This pull request is a suggestion to add Joomla on FrankenPHP as it works too. I tried it and made a docker image for it based on your frankenphp-worpress repo. If you don't mind, please add the Joomla version too on your README.md as an example, besides Wordpress and Drupal.
2023-11-10 15:05:33 +01:00
Kévin Dunglas
0266175df8 ci: PHP 8.3 RC6 Docker images 2023-11-10 15:05:16 +01:00
Kévin Dunglas
9d56f2530a docs: add custom Caddy modules 2023-11-07 10:48:37 +01:00
Kévin Dunglas
4d2cd5f715 perf: write the response body directly from the memory allocated by PHP 2023-11-07 08:34:18 +01:00
Kévin Dunglas
a4938102e1 perf: read the request body directly in the memory allocated by PHP 2023-11-07 08:34:04 +01:00
Irvin Capagcuan
bb1ed8b212 Update docker.md
Fixed typo.
2023-11-06 22:39:03 +01:00
Kennedy Tedesco
e7bd54cc00 Chore: remove duplicated code in populateEnv() 2023-11-01 15:51:00 +01:00
DubbleClick
ea2c7c2895 feat: add ext-ldap and ext-sysvsem to static builder, add PHP_EXTENSION_LIBS to enhance extensions (#203)
* add ldap and sysvsem extension to static-builder

* add lib support for static-builder to enhance enabled extensions (such as libjpeg for gd)

* disable opcache-jit

* include bz2, order alphabetically

* Update static.md

---------

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2023-11-01 15:50:19 +01:00
Jacob Dreesen
8475ad9bc8 docs: fix some more typos (#285)
* Fix some typos in the docs

* Fix some more typos
2023-10-31 03:42:39 +01:00
Jacob Dreesen
35d4075c50 Fix typo in the docs 2023-10-31 01:17:41 +01:00
Kévin Dunglas
255dd4b6d6 chore: prepare release 1.0.0-rc.2 2023-10-30 23:04:59 +01:00
Kévin Dunglas
ce1b02b1bd fix: retract RC1 of the Caddy module 2023-10-30 22:17:01 +01:00
Kévin Dunglas
c1b8c5df5f docs: improve workers docs (#281)
* docs: improve workers docs

* improvements
2023-10-30 22:16:09 +01:00
Antoine Lamirault
43722a9dbe docs: update static-php URLs (#277) 2023-10-28 14:17:18 +02:00
Kévin Dunglas
385b47ee3b chore: bump deps 2023-10-27 07:29:06 +02:00
Kévin Dunglas
90caf2701a fix: healthcheck URL 2023-10-27 07:27:48 +02:00
Kévin Dunglas
e13e394700 feat: minor improvements to the default Caddyfile 2023-10-26 08:45:52 +02:00
Mathieu De Zutter
6874e44ddd doc: get_browser performance 2023-10-23 23:33:34 +02:00
hs son
2a205b16ce docs: add dockerignore issue in lower docekr version to CONTRIBUTING.md 2023-10-17 16:35:24 +02:00
Kévin Dunglas
dfa19978f8 chore: bump OpenTelemetry-Go 2023-10-16 21:53:18 +02:00
Kévin Dunglas
120006a297 chore: bump deps 2023-10-16 21:53:18 +02:00
Antoine Lamirault
8c57f4cc85 feat: upgrade to PHP 8.3RC4 2023-10-16 21:20:22 +02:00
Kévin Dunglas
7f9c50e6f5 chore: prepare release 1.0.0-beta.2 2023-10-10 16:33:05 +02:00
Kévin Dunglas
ec35afdc7f chore: remove C-Thread-Pool/example.c (#259) 2023-10-10 15:26:03 +02:00
Kévin Dunglas
51038bbdf5 chore: log maxprocs (#257)
* chore: log maxprocs

* Update caddy/frankenphp/main.go

Co-authored-by: Ruud Kamphuis <ruudk@users.noreply.github.com>

---------

Co-authored-by: Ruud Kamphuis <ruudk@users.noreply.github.com>
2023-10-09 15:16:22 +02:00
Kévin Dunglas
c615fe0087 feat: add experimental CLI support (#239)
* feat: add CLI support

* updated

* debug

* fix tests

* Caddy php-cli command

* use thread

* $_SERVER and input streams support

* Update frankenphp.c

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

---------

Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2023-10-09 14:38:15 +02:00
Ruud Kamphuis
af3ed6e26d fix: disable automaxprocs logging
See https://github.com/uber-go/automaxprocs/issues/18

Fixes #251
2023-10-09 12:03:58 +02:00
Ruud Kamphuis
669a0175f3 Fix highlighting in config docs 2023-10-09 10:37:40 +02:00
shangyuanchun
52caf17a40 fix: to solve php versions like: 9.0 / 9.1 2023-10-09 09:23:50 +02:00
Kévin Dunglas
e193a374d3 freat: set thread name to frankenphp 2023-10-06 19:15:04 +02:00
Kévin Dunglas
8b46d79560 test: refactor YAML files 2023-10-06 00:24:15 +02:00
DubbleClick
9bf24b7d88 fix(static): add start-group and end-group around php-config libs (#241)
don't be dependent on php not messing up when compiling the embed sapi
2023-10-05 17:53:43 +02:00
Kévin Dunglas
c624971fa7 feat(caddy): add command to start a PHP server (#238)
* feat(caddy): a command to start a PHP server

* docs and l shortcut

* fix some bugs and support for compression

* cleanup

* enable compression by default

* add -a shortcut

* refactor

* const

* docs
2023-10-05 14:56:04 +02:00
DubbleClick
b4780b6495 docs: config for multiple workers (#240) 2023-10-05 11:45:38 +02:00
Kévin Dunglas
b04326ee83 chore: better env vars in default Caddyfile (#237) 2023-10-03 18:02:03 +02:00
Kévin Dunglas
5874072d46 docs: Laravel example (#231) 2023-10-03 17:32:17 +02:00
Kévin Dunglas
7d41aa5243 docs: ext-imap isn't supported 2023-10-03 16:13:52 +02:00
Kévin Dunglas
2d91a606fd feat(caddy): php_server simplified directive (#235)
* feat(caddy): php_server simplified directive

* fix typo

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>

* fix

* cleanup

* Update config.md

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

* feat: automatically serve static files

* file_server off

* fix tests

* fix config

* fix tests in Docker

* debug

* fix

---------

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2023-10-03 15:44:55 +02:00
Kévin Dunglas
69416cc0d6 test: prove that Fibers work if not calling Go 2023-09-28 19:04:57 +02:00
Kévin Dunglas
29be0c0cdb docs: update skeleton list [skip ci] 2023-09-28 15:49:41 +02:00
Kévin Dunglas
eca8cc7350 fix(caddy): ModuleInfo.New() warning 2023-09-27 19:40:48 +02:00
Sergey Kamenskiy
f8e7b161b5 docs: explain php.ini configuration in docker 2023-09-25 21:32:44 +02:00
Kévin Dunglas
374c581ee5 ci: fix release workflow name 2023-09-25 18:47:52 +02:00
Kévin Dunglas
53a7f64984 fix(docker): increase thread stack size on Alpine (#223) 2023-09-25 18:16:48 +02:00
Oyebanji Jacob Mayowa
3f9cda365f fix typo in config.md (#221) 2023-09-23 20:09:12 +02:00
78 changed files with 5143 additions and 3257 deletions

View File

@@ -1,79 +0,0 @@
version: 2.1
orbs:
gh: circleci/github-cli@2.2.0
jobs:
release_mac_arm:
macos:
xcode: 14.3.1
environment:
HOMEBREW_NO_AUTO_UPDATE: 1
steps:
- checkout
- run: brew install --formula go automake cmake composer
- run:
name: Clone static-php-cli
command: git clone --depth 1 https://github.com/crazywhalecc/static-php-cli
- restore_cache:
keys:
- spc-{{ checksum "static-php-cli/composer.json" }}
- run:
name: Install static-php-cli and fetch libraries sources
working_directory: static-php-cli/
command: |
composer install --no-dev -a
./bin/spc fetch --with-php=8.2 -A
- save_cache:
key: spc-{{ checksum "static-php-cli/composer.json" }}
paths:
- static-php-cli/downloads/
- static-php-cli/vendor/
- run:
working_directory: static-php-cli/
name: Build libphp.a
command: ./bin/spc build --enable-zts --build-embed "bcmath,calendar,ctype,curl,dba,dom,exif,filter,fileinfo,gd,iconv,intl,mbstring,mbregex,mysqli,mysqlnd,opcache,openssl,pcntl,pdo,pdo_mysql,pdo_pgsql,pdo_sqlite,pgsql,phar,posix,readline,redis,session,simplexml,sockets,sqlite3,tokenizer,xml,xmlreader,xmlwriter,zip,zlib,apcu"
- run:
working_directory: static-php-cli/
name: Set CGO flags
command: |
if [ -z "$CIRCLE_TAG" ]; then export FRANKENPHP_VERSION=$CIRCLE_SHA1; else export FRANKENPHP_VERSION=${CIRCLE_TAG:1}; fi
echo "export CGO_CFLAGS='-DFRANKENPHP_VERSION=$FRANKENPHP_VERSION $(./buildroot/bin/php-config --includes | sed s#-I/#-I$PWD/buildroot/#g)'" >> "$BASH_ENV"
echo "export CGO_LDFLAGS='-framework CoreFoundation -framework SystemConfiguration $(./buildroot/bin/php-config --ldflags) $(./buildroot/bin/php-config --libs)'" >> "$BASH_ENV"
echo "export PHP_VERSION='$(./buildroot/bin/php-config --version)'" >> "$BASH_ENV"
echo "export FRANKENPHP_VERSION='$FRANKENPHP_VERSION'" >> "$BASH_ENV"
- restore_cache:
keys:
- go-mod-v4-{{ checksum "caddy/go.sum" }}
- run:
name: Build FrankenPHP
working_directory: caddy/frankenphp/
command: |
go env
go build -buildmode=pie -tags "cgo netgo osusergo static_build" -ldflags "-linkmode=external -extldflags -static-pie -w -s -X 'github.com/caddyserver/caddy/v2.CustomVersion=FrankenPHP $FRANKENPHP_VERSION PHP $PHP_VERSION Caddy'" -o frankenphp-mac-arm64
./frankenphp-mac-arm64 version
- gh/setup
- run:
name: Upload asset
working_directory: caddy/frankenphp/
command: |
if [ -n "$CIRCLE_TAG" ]; then
gh release upload $CIRCLE_TAG frankenphp-mac-arm64 --repo dunglas/frankenphp --clobber
fi
- store_artifacts:
path: caddy/frankenphp/frankenphp-mac-arm64
destination: frankenphp-mac-arm64
- save_cache:
key: go-mod-v4-{{ checksum "caddy/go.sum" }}
paths:
- "~/go/pkg/mod"
workflows:
release:
jobs:
- release_mac_arm:
filters:
branches:
ignore: /.*/
tags:
only: /^v.*/

View File

@@ -8,3 +8,7 @@
!**/*.c
!**/*.h
!testdata/*.php
!testdata/*.txt
!build-static.sh
!app.tar
!app_checksum.txt

1
.github/FUNDING.yml vendored
View File

@@ -1 +0,0 @@
github: dunglas

78
.github/ISSUE_TEMPLATE/bug_report.yaml vendored Normal file
View File

@@ -0,0 +1,78 @@
---
name: Bug Report
description: File a bug report
labels: [bug]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
- type: textarea
id: what-happened
attributes:
label: What happened?
description: |
Tell us what you do, what you get and what you expected.
Provide us with some step-by-step instructions to reproduce the issue.
validations:
required: true
- type: dropdown
id: build
attributes:
label: Build Type
description: What build of FrankenPHP do you use?
options:
- Docker (Debian Bookworm)
- Docker (Alpine)
- Official static build
- Standalone binary
- Custom (tell us more in the description)
default: 0
validations:
required: true
- type: dropdown
id: worker
attributes:
label: Worker Mode
description: Does the problem happen only when using the worker mode?
options:
- "Yes"
- "No"
default: 0
validations:
required: true
- type: dropdown
id: os
attributes:
label: Operating System
description: What operating system are you executing FrankenPHP with?
options:
- GNU/Linux
- macOS
- Other (tell us more in the description)
default: 0
validations:
required: true
- type: dropdown
id: arch
attributes:
label: CPU Architecture
description: What CPU architecture are you using?
options:
- x86_64
- Apple Silicon
- x86
- aarch64
- Other (tell us more in the description)
default: 0
validations:
required: true
- type: textarea
id: logs
attributes:
label: Relevant log output
description: |
Please copy and paste any relevant log output.
This will be automatically formatted into code,
so no need for backticks.
render: shell

View File

@@ -0,0 +1,20 @@
---
name: Feature Request
description: Suggest an idea for this project
labels: [enhancement]
body:
- type: textarea
id: description
attributes:
label: Describe you feature request
value: |
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is.
Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions
or features you've considered.

31
.github/dependabot.yaml vendored Normal file
View File

@@ -0,0 +1,31 @@
---
version: 2
updates:
-
package-ecosystem: gomod
directory: /
schedule:
interval: weekly
commit-message:
prefix: chore
-
package-ecosystem: gomod
directory: /caddy
schedule:
interval: weekly
commit-message:
prefix: chore(caddy)
# These packages must be in sync with versions
# used by github.com/caddyserver/caddy/v2
ignore:
-
dependency-name: github.com/google/cel-go
-
dependency-name: github.com/quic-go/*
-
package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
commit-message:
prefix: ci

258
.github/workflows/docker.yaml vendored Normal file
View File

@@ -0,0 +1,258 @@
---
name: Build Docker images
on:
pull_request:
branches:
- main
push:
branches:
- main
tags:
- v*.*.*
workflow_dispatch:
inputs:
version:
description: 'FrankenPHP version'
required: false
type: string
schedule:
- cron: '0 4 * * *'
env:
IMAGE_NAME: ${{ (github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && inputs.version) || startsWith(github.ref, 'refs/tags/')) && 'dunglas/frankenphp' || 'dunglas/frankenphp-dev' }}
LATEST: ${{ (github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && inputs.version) || startsWith(github.ref, 'refs/tags/')) && '0' || '1' }}
jobs:
prepare:
runs-on: ubuntu-latest
outputs:
# Push if it's a scheduled job, a tag, or if we're committing to the main branch
push: ${{ (github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && inputs.version) || startsWith(github.ref, 'refs/tags/') || (github.ref == 'refs/heads/main' && github.event_name != 'pull_request')) && true || false }}
variants: ${{ steps.matrix.outputs.variants }}
platforms: ${{ steps.matrix.outputs.platforms }}
metadata: ${{ steps.matrix.outputs.metadata }}
php_version: ${{ steps.check.outputs.php_version }}
php82_version: ${{ steps.check.outputs.php82_version }}
php83_version: ${{ steps.check.outputs.php83_version }}
skip: ${{ steps.check.outputs.skip }}
ref: ${{ steps.check.outputs.ref || (github.event_name == 'workflow_dispatch' && inputs.version) || '' }}
steps:
-
name: Check PHP versions
id: check
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
PHP_82_LATEST=$(skopeo inspect docker://docker.io/library/php:8.2 --override-os linux --override-arch amd64 | jq -r '.Env[] | select(test("^PHP_VERSION=")) | sub("^PHP_VERSION="; "")')
PHP_83_LATEST=$(skopeo inspect docker://docker.io/library/php:8.3 --override-os linux --override-arch amd64 | jq -r '.Env[] | select(test("^PHP_VERSION=")) | sub("^PHP_VERSION="; "")')
{
echo php_version="${PHP_83_LATEST},${PHP_82_LATEST}"
echo php82_version="${PHP_82_LATEST//./-}"
echo php83_version="${PHP_83_LATEST//./-}"
} >> "${GITHUB_OUTPUT}"
# Check if the Docker images must be rebuilt
if [[ "${GITHUB_EVENT_NAME}" != "schedule" ]]; then
echo skip=false >> "${GITHUB_OUTPUT}"
exit 0
fi
FRANKENPHP_82_LATEST=$(skopeo inspect docker://docker.io/dunglas/frankenphp:latest-php8.2 --override-os linux --override-arch amd64 | jq -r '.Env[] | select(test("^PHP_VERSION=")) | sub("^PHP_VERSION="; "")')
FRANKENPHP_83_LATEST=$(skopeo inspect docker://docker.io/dunglas/frankenphp:latest-php8.3 --override-os linux --override-arch amd64 | jq -r '.Env[] | select(test("^PHP_VERSION=")) | sub("^PHP_VERSION="; "")')
if [[ "${FRANKENPHP_82_LATEST}" == "${PHP_82_LATEST}" ]] && [[ "${FRANKENPHP_83_LATEST}" == "${PHP_83_LATEST}" ]]; then
echo skip=true >> "${GITHUB_OUTPUT}"
exit 0
fi
{
echo ref="$(gh release view --repo dunglas/frankenphp --json tagName --jq '.tagName')"
echo skip=false
} >> "${GITHUB_OUTPUT}"
-
uses: actions/checkout@v4
if: ${{ !fromJson(steps.check.outputs.skip) }}
with:
ref: ${{ steps.check.outputs.ref }}
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
version: latest
-
name: Create variants matrix
if: ${{ !fromJson(steps.check.outputs.skip) }}
id: matrix
run: |
METADATA="$(docker buildx bake --print | jq -c)"
{
echo metadata="${METADATA}"
echo variants="$(jq -c '.group.default.targets|map(sub("runner-|builder-"; ""))|unique' <<< "${METADATA}")"
echo platforms="$(jq -c 'first(.target[]) | .platforms' <<< "${METADATA}")"
} >> "${GITHUB_OUTPUT}"
env:
SHA: ${{ github.sha }}
VERSION: ${{ (github.ref_type == 'tag' && github.ref_name) || steps.check.outputs.ref || github.sha }}
PHP_VERSION: ${{ steps.check.outputs.php_version }}
build:
runs-on: ubuntu-latest
needs:
- prepare
if: ${{ !fromJson(needs.prepare.outputs.skip) }}
strategy:
fail-fast: false
matrix:
variant: ${{ fromJson(needs.prepare.outputs.variants) }}
platform: ${{ fromJson(needs.prepare.outputs.platforms) }}
include:
-
race: ""
qemu: true
-
platform: linux/amd64
qemu: false
race: "-race" # The Go race detector is only supported on amd64
-
platform: linux/386
qemu: false
exclude:
# arm/v6 is only available for Alpine: https://github.com/docker-library/golang/issues/502
-
variant: php-${{ needs.prepare.outputs.php82_version }}-bookworm
platform: linux/arm/v6
-
variant: php-${{ needs.prepare.outputs.php83_version }}-bookworm
platform: linux/arm/v6
steps:
-
uses: actions/checkout@v4
with:
ref: ${{ needs.prepare.outputs.ref }}
-
name: Set up QEMU
if: matrix.qemu
uses: docker/setup-qemu-action@v3
with:
platforms: ${{ matrix.platform }}
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
platforms: ${{ matrix.platform }}
version: latest
-
name: Login to DockerHub
if: fromJson(needs.prepare.outputs.push)
uses: docker/login-action@v3
with:
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
-
name: Build
id: build
uses: docker/bake-action@v4
with:
pull: true
load: ${{ !fromJson(needs.prepare.outputs.push) }}
targets: |
builder-${{ matrix.variant }}
runner-${{ matrix.variant }}
# Remove tags to prevent "can't push tagged ref [...] by digest" error
set: |
*.tags=
*.platform=${{ matrix.platform }}
*.cache-from=type=gha,scope=${{ needs.prepare.outputs.ref || github.ref }}-${{ matrix.platform }}
*.cache-from=type=gha,scope=refs/heads/main-${{ matrix.platform }}
*.cache-to=type=gha,scope=${{ needs.prepare.outputs.ref || github.ref }}-${{ matrix.platform }},ignore-error=true
${{ fromJson(needs.prepare.outputs.push) && '*.output=type=image,name=dunglas/frankenphp,push-by-digest=true,name-canonical=true,push=true' || '' }}
env:
SHA: ${{ github.sha }}
VERSION: ${{ github.ref_type == 'tag' && github.ref_name || needs.prepare.outputs.ref || github.sha }}
PHP_VERSION: ${{ needs.prepare.outputs.php_version }}
-
# Workaround for https://github.com/actions/runner/pull/2477#issuecomment-1501003600
name: Export metadata
if: fromJson(needs.prepare.outputs.push)
run: |
mkdir -p /tmp/metadata/builder /tmp/metadata/runner
builderDigest=$(jq -r '."builder-${{ matrix.variant }}"."containerimage.digest"' <<< "${METADATA}")
touch "/tmp/metadata/builder/${builderDigest#sha256:}"
runnerDigest=$(jq -r '."runner-${{ matrix.variant }}"."containerimage.digest"' <<< "${METADATA}")
touch "/tmp/metadata/runner/${runnerDigest#sha256:}"
env:
METADATA: ${{ steps.build.outputs.metadata }}
-
name: Upload builder metadata
if: fromJson(needs.prepare.outputs.push)
uses: actions/upload-artifact@v3
with:
name: metadata-builder-${{ matrix.variant }}
path: /tmp/metadata/builder/*
if-no-files-found: error
retention-days: 1
-
name: Upload runner metadata
if: fromJson(needs.prepare.outputs.push)
uses: actions/upload-artifact@v3
with:
name: metadata-runner-${{ matrix.variant }}
path: /tmp/metadata/runner/*
if-no-files-found: error
retention-days: 1
-
name: Run tests
if: ${{ !matrix.qemu && !fromJson(needs.prepare.outputs.push) }}
run: |
docker run --platform=${{ matrix.platform }} --rm \
"$(jq -r '."builder-${{ matrix.variant }}"."containerimage.config.digest"' <<< "${METADATA}")" \
sh -c 'go test ${{ matrix.race }} -v ./... && cd caddy && go test ${{ matrix.race }} -v ./...'
env:
METADATA: ${{ steps.build.outputs.metadata }}
# Adapted from https://docs.docker.com/build/ci/github-actions/multi-platform/
push:
runs-on: ubuntu-latest
needs:
- prepare
- build
if: fromJson(needs.prepare.outputs.push)
strategy:
fail-fast: false
matrix:
variant: ${{ fromJson(needs.prepare.outputs.variants) }}
target: ['builder', 'runner']
steps:
-
name: Download metadata
uses: actions/download-artifact@v3
with:
name: metadata-${{ matrix.target }}-${{ matrix.variant }}
path: /tmp/metadata
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
# Temporary fix for https://github.com/docker/buildx/issues/2229
version: "https://github.com/jedevc/buildx.git#imagetools-resolver-copy-dupe"
-
name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
-
name: Create manifest list and push
working-directory: /tmp/metadata
run: |
set -x
# shellcheck disable=SC2046,SC2086
docker buildx imagetools create $(jq -cr '.target."${{ matrix.target }}-${{ matrix.variant }}".tags | map("-t " + .) | join(" ")' <<< ${METADATA}) \
$(printf 'dunglas/frankenphp@sha256:%s ' *)
env:
METADATA: ${{ needs.prepare.outputs.metadata }}
-
name: Inspect image
run: |
# shellcheck disable=SC2046,SC2086
docker buildx imagetools inspect $(jq -cr '.target."${{ matrix.target }}-${{ matrix.variant }}".tags | first' <<< ${METADATA})
env:
METADATA: ${{ needs.prepare.outputs.metadata }}

View File

@@ -1,188 +0,0 @@
name: Build Docker images
on:
pull_request:
branches:
- main
push:
branches:
- main
tags:
- v*.*.*
workflow_dispatch:
inputs: {}
jobs:
prepare:
runs-on: ubuntu-latest
outputs:
# Push only if it's a tag or if we're committing in the main branch
push: ${{toJson(startsWith(github.ref, 'refs/tags/') || (github.ref == 'refs/heads/main' && github.event_name != 'pull_request'))}}
variants: ${{ steps.matrix.outputs.variants }}
platforms: ${{ steps.matrix.outputs.platforms }}
metadata: ${{ steps.matrix.outputs.metadata }}
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
with:
version: latest
- name: Create variants matrix
id: matrix
run: |
METADATA=$(docker buildx bake --print | jq -c)
echo "metadata=$METADATA" >> "$GITHUB_OUTPUT"
echo "variants=$(jq -c '.group.default.targets|map(sub("runner-|builder-"; ""))|unique' <<< $METADATA)" >> "$GITHUB_OUTPUT"
echo "platforms=$(jq -c 'first(.target[]) | .platforms' <<< $METADATA)" >> "$GITHUB_OUTPUT"
env:
LATEST: '1' # TODO: unset this variable when releasing the first stable version
SHA: ${{github.sha}}
VERSION: ${{github.ref_type == 'tag' && github.ref_name || github.sha}}
build:
runs-on: ubuntu-latest
needs:
- prepare
strategy:
fail-fast: false
matrix:
variant: ${{ fromJson(needs.prepare.outputs.variants) }}
platform: ${{ fromJson(needs.prepare.outputs.platforms) }}
include:
- race: ""
qemu: true
- platform: linux/amd64
qemu: false
race: "-race" # The Go race detector is only supported on amd64
- platform: linux/386
qemu: false
steps:
- uses: actions/checkout@v4
- name: Set up QEMU
if: matrix.qemu
uses: docker/setup-qemu-action@v2
with:
platforms: ${{matrix.platform}}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
with:
platforms: ${{matrix.platform}}
version: latest
- name: Login to DockerHub
if: fromJson(needs.prepare.outputs.push)
uses: docker/login-action@v2
with:
username: ${{secrets.REGISTRY_USERNAME}}
password: ${{secrets.REGISTRY_PASSWORD}}
- name: Build
id: build
uses: docker/bake-action@v3
with:
pull: true
load: ${{!fromJson(needs.prepare.outputs.push)}}
targets: |
builder-${{matrix.variant}}
runner-${{matrix.variant}}
# Remove tags to prevent "can't push tagged ref [...] by digest" error
set: |
*.tags=
*.platform=${{matrix.platform}}
*.cache-from=type=gha,scope=${{github.ref}}-${{matrix.platform}}
*.cache-from=type=gha,scope=refs/heads/main-${{matrix.platform}}
*.cache-to=type=gha,scope=${{github.ref}}-${{matrix.platform}}
${{fromJson(needs.prepare.outputs.push) && '*.output=type=image,name=dunglas/frankenphp,push-by-digest=true,name-canonical=true,push=true' || ''}}
env:
LATEST: '1' # TODO: unset this variable when releasing the first stable version
SHA: ${{github.sha}}
VERSION: ${{github.ref_type == 'tag' && github.ref_name || github.sha}}
# Workaround for https://github.com/actions/runner/pull/2477#issuecomment-1501003600
- name: Export metadata
if: fromJson(needs.prepare.outputs.push)
run: |
mkdir -p /tmp/metadata/builder /tmp/metadata/runner
builderDigest=$(jq -r '."builder-${{matrix.variant}}"."containerimage.digest"' <<< $METADATA)
touch "/tmp/metadata/builder/${builderDigest#sha256:}"
runnerDigest=$(jq -r '."runner-${{matrix.variant}}"."containerimage.digest"' <<< $METADATA)
touch "/tmp/metadata/runner/${runnerDigest#sha256:}"
env:
METADATA: ${{steps.build.outputs.metadata}}
- name: Upload runner metadata
if: fromJson(needs.prepare.outputs.push)
uses: actions/upload-artifact@v3
with:
name: metadata-builder-${{matrix.variant}}
path: /tmp/metadata/builder/*
if-no-files-found: error
retention-days: 1
- name: Upload runner metadata
if: fromJson(needs.prepare.outputs.push)
uses: actions/upload-artifact@v3
with:
name: metadata-runner-${{matrix.variant}}
path: /tmp/metadata/runner/*
if-no-files-found: error
retention-days: 1
- name: Run tests
if: '!matrix.qemu'
continue-on-error: ${{fromJson(needs.prepare.outputs.push)}}
run: |
docker run --platform=${{matrix.platform}} --rm \
$(jq -r '."builder-${{matrix.variant}}"."containerimage.config.digest"' <<< $METADATA) \
sh -c 'go test ${{matrix.race}} -v ./... && cd caddy && go test ${{matrix.race}} -v ./...'
env:
METADATA: ${{steps.build.outputs.metadata}}
# Adapted from https://docs.docker.com/build/ci/github-actions/multi-platform/
push:
runs-on: ubuntu-latest
needs:
- prepare
- build
if: fromJson(needs.prepare.outputs.push)
strategy:
fail-fast: false
matrix:
variant: ${{ fromJson(needs.prepare.outputs.variants) }}
target: ['builder', 'runner']
steps:
- name: Download metadata
uses: actions/download-artifact@v3
with:
name: metadata-${{matrix.target}}-${{matrix.variant}}
path: /tmp/metadata
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
with:
version: latest
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{secrets.REGISTRY_USERNAME}}
password: ${{secrets.REGISTRY_PASSWORD}}
- name: Create manifest list and push
working-directory: /tmp/metadata
run: |
docker buildx imagetools create $(jq -cr '.target."${{matrix.target}}-${{matrix.variant}}".tags | map("-t " + .) | join(" ")' <<< $METADATA) \
$(printf 'dunglas/frankenphp@sha256:%s ' *)
env:
METADATA: ${{needs.prepare.outputs.metadata}}
- name: Inspect image
run: |
docker buildx imagetools inspect $(jq -cr '.target."${{matrix.target}}-${{matrix.variant}}".tags | first' <<< $METADATA)
env:
METADATA: ${{needs.prepare.outputs.metadata}}

43
.github/workflows/lint.yaml vendored Normal file
View File

@@ -0,0 +1,43 @@
---
name: Lint Code Base
on:
pull_request:
branches:
- main
push:
branches:
- main
jobs:
build:
name: Lint Code Base
runs-on: ubuntu-latest
permissions:
contents: read
packages: read
statuses: write
steps:
-
name: Checkout Code
uses: actions/checkout@v4
with:
fetch-depth: 0
-
name: Lint Code Base
uses: super-linter/super-linter@v5
env:
VALIDATE_ALL_CODEBASE: true
DEFAULT_BRANCH: main
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
LINTER_RULES_PATH: /
FILTER_REGEX_EXCLUDE: '.*C-Thread-Pool/.*'
MARKDOWN_CONFIG_FILE: .markdown-lint.yaml
VALIDATE_CPP: false
VALIDATE_JSCPD: false
VALIDATE_GO: false
VALIDATE_PHP_PHPCS: false
VALIDATE_PHP_PHPSTAN: false
VALIDATE_PHP_PSALM: false
VALIDATE_TERRAGRUNT: false

258
.github/workflows/static.yaml vendored Normal file
View File

@@ -0,0 +1,258 @@
---
name: Build binary releases
on:
pull_request:
branches:
- main
push:
branches:
- main
tags:
- v*.*.*
workflow_dispatch:
inputs:
version:
description: 'FrankenPHP version'
required: false
type: string
schedule:
- cron: '0 0 * * *'
env:
IMAGE_NAME: ${{ (github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && inputs.version) || startsWith(github.ref, 'refs/tags/')) && 'dunglas/frankenphp' || 'dunglas/frankenphp-dev' }}
LATEST: ${{ (github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && inputs.version) || startsWith(github.ref, 'refs/tags/')) && '0' || '1' }}
jobs:
prepare:
runs-on: ubuntu-latest
outputs:
push: ${{ toJson((steps.check.outputs.ref || (github.event_name == 'workflow_dispatch' && inputs.version) || startsWith(github.ref, 'refs/tags/') || (github.ref == 'refs/heads/main' && github.event_name != 'pull_request')) && true || false) }}
platforms: ${{ steps.matrix.outputs.platforms }}
metadata: ${{ steps.matrix.outputs.metadata }}
ref: ${{ steps.check.outputs.ref }}
steps:
-
name: Get version
id: check
if: github.event_name == 'schedule'
run: |
ref="${{ (github.ref_type == 'tag' && github.ref_name) || (github.event_name == 'workflow_dispatch' && inputs.version) || '' }}"
if [[ -z "${ref}" ]]; then
ref="$(gh release view --repo dunglas/frankenphp --json tagName --jq '.tagName')"
fi
echo "ref=${ref}" >> "${GITHUB_OUTPUT}"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-
uses: actions/checkout@v4
with:
ref: ${{ steps.check.outputs.ref }}
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
version: latest
-
name: Create platforms matrix
id: matrix
run: |
METADATA="$(docker buildx bake --print static-builder | jq -c)"
{
echo metadata="${METADATA}"
echo platforms="$(jq -c 'first(.target[]) | .platforms' <<< "${METADATA}")"
} >> "${GITHUB_OUTPUT}"
env:
SHA: ${{ github.sha }}
VERSION: ${{ steps.check.outputs.ref || github.sha }}
build-linux:
strategy:
fail-fast: false
matrix:
platform: ${{ fromJson(needs.prepare.outputs.platforms) }}
include:
- race: ""
qemu: true
- platform: linux/amd64
qemu: false
name: Build ${{ matrix.platform }} static binary
runs-on: ubuntu-latest
needs: [ prepare ]
steps:
-
uses: actions/checkout@v4
with:
ref: ${{ needs.prepare.outputs.ref }}
-
name: Set up QEMU
if: matrix.qemu
uses: docker/setup-qemu-action@v3
with:
platforms: ${{ matrix.platform }}
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
platforms: ${{ matrix.platform }}
version: latest
-
name: Login to DockerHub
if: ${{ fromJson(needs.prepare.outputs.push) }}
uses: docker/login-action@v3
with:
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
-
name: Build
id: build
uses: docker/bake-action@v4
with:
pull: true
load: ${{ !fromJson(needs.prepare.outputs.push) }}
targets: static-builder
set: |
*.tags=
*.platform=${{ matrix.platform }}
*.cache-from=type=gha,scope=${{ needs.prepare.outputs.ref || github.ref }}-static-builder
*.cache-from=type=gha,scope=refs/heads/main-static-builder
*.cache-to=type=gha,scope=${{ needs.prepare.outputs.ref || github.ref }}-static-builder,ignore-error=true
${{ fromJson(needs.prepare.outputs.push) && '*.output=type=image,name=dunglas/frankenphp,push-by-digest=true,name-canonical=true,push=true' || '' }}
env:
SHA: ${{ github.sha }}
VERSION: ${{ (github.ref_type == 'tag' && github.ref_name) || needs.prepare.outputs.ref || github.sha}}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-
# Workaround for https://github.com/actions/runner/pull/2477#issuecomment-1501003600
name: Export metadata
if: fromJson(needs.prepare.outputs.push)
run: |
mkdir -p /tmp/metadata
# shellcheck disable=SC2086
digest=$(jq -r '."static-builder"."containerimage.digest"' <<< ${METADATA})
touch "/tmp/metadata/${digest#sha256:}"
env:
METADATA: ${{ steps.build.outputs.metadata }}
-
name: Upload metadata
if: fromJson(needs.prepare.outputs.push)
uses: actions/upload-artifact@v3
with:
name: metadata-static-builder
path: /tmp/metadata/*
if-no-files-found: error
retention-days: 1
-
name: Copy binary
if: ${{ !fromJson(needs.prepare.outputs.push) }}
run: |
digest=$(jq -r '."static-builder"."containerimage.config.digest"' <<< "${METADATA}")
docker create --platform=${{ matrix.platform }} --name static-builder "${digest}"
docker cp "static-builder:/go/src/app/dist/${BINARY}" "${BINARY}"
env:
METADATA: ${{ steps.build.outputs.metadata }}
BINARY: frankenphp-linux-${{ matrix.platform == 'linux/amd64' && 'x86_64' || 'aarch64' }}
-
name: Upload artifact
if: ${{ !fromJson(needs.prepare.outputs.push) }}
uses: actions/upload-artifact@v3
with:
name: frankenphp-linux-${{ matrix.platform == 'linux/amd64' && 'x86_64' || 'aarch64' }}
path: frankenphp-linux-${{ matrix.platform == 'linux/amd64' && 'x86_64' || 'aarch64' }}
# Adapted from https://docs.docker.com/build/ci/github-actions/multi-platform/
push:
runs-on: ubuntu-latest
needs:
- prepare
- build-linux
if: fromJson(needs.prepare.outputs.push)
#if: fromJson(needs.prepare.outputs.push) && (needs.prepare.outputs.ref || github.ref_type == 'tag')
steps:
-
name: Download metadata
uses: actions/download-artifact@v3
with:
name: metadata-static-builder
path: /tmp/metadata
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
version: latest
-
name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
-
name: Create manifest list and push
working-directory: /tmp/metadata
run: |
# shellcheck disable=SC2046,SC2086
docker buildx imagetools create $(jq -cr '.target."static-builder".tags | map("-t " + .) | join(" ")' <<< "${METADATA}") \
$(printf 'dunglas/frankenphp@sha256:%s ' *)
env:
METADATA: ${{ needs.prepare.outputs.metadata }}
-
name: Inspect image
run: |
# shellcheck disable=SC2046,SC2086
docker buildx imagetools inspect "$(jq -cr '.target."static-builder".tags | first' <<< "${METADATA}")"
env:
METADATA: ${{ needs.prepare.outputs.metadata }}
-
name: Copy binary
run: |
tag=$(jq -cr '.target."static-builder".tags | first' <<< "${METADATA}")
docker cp "$(docker create --platform=linux/amd64 --name static-builder "${tag}"):/go/src/app/dist/frankenphp-linux-x86_64" frankenphp-linux-x86_64 ; docker rm static-builder
docker cp "$(docker create --platform=linux/arm64 --name static-builder "${tag}"):/go/src/app/dist/frankenphp-linux-aarch64" frankenphp-linux-aarch64 ; docker rm static-builder
env:
METADATA: ${{ needs.prepare.outputs.metadata }}
-
name: Upload asset
if: needs.prepare.outputs.ref || github.ref_type == 'tag'
run: gh release upload "${{ (github.ref_type == 'tag' && github.ref_name) || needs.prepare.outputs.ref }}" frankenphp-linux-x86_64 frankenphp-linux-aarch64 --repo dunglas/frankenphp --clobber
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
build-mac:
name: Build macOS x86_64 binaries
runs-on: macos-latest
needs: [ prepare ]
env:
HOMEBREW_NO_AUTO_UPDATE: 1
steps:
-
uses: actions/checkout@v4
with:
ref: ${{ needs.prepare.outputs.ref }}
-
uses: actions/setup-go@v5
with:
go-version: '1.21'
cache-dependency-path: |
go.sum
caddy/go.sum
-
name: Set FRANKENPHP_VERSION
run: |
if [ "${GITHUB_REF_TYPE}" == "tag" ]; then
export FRANKENPHP_VERSION=${GITHUB_REF_NAME:1}
elif [ "${GITHUB_EVENT_NAME}" == "schedule" ]; then
export FRANKENPHP_VERSION="${{ needs.prepare.outputs.ref }}"
else
export FRANKENPHP_VERSION=${GITHUB_SHA}
fi
echo "FRANKENPHP_VERSION=${FRANKENPHP_VERSION}" >> "${GITHUB_ENV}"
-
name: Build FrankenPHP
run: ./build-static.sh
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE: ${{ (needs.prepare.outputs.ref || github.ref_type == 'tag') && '1' || '' }}
-
name: Upload artifact
if: github.ref_type == 'branch'
uses: actions/upload-artifact@v3
with:
name: frankenphp-mac-x86_64
path: dist/frankenphp-mac-x86_64

View File

@@ -1,137 +0,0 @@
name: Build binary releases
on:
pull_request:
branches:
- main
push:
branches:
- main
tags:
- v*.*.*
workflow_dispatch:
inputs: {}
jobs:
release:
name: Build Linux x86_64 binary
runs-on: ubuntu-latest
steps:
- name: Create release
if: github.ref_type == 'tag'
uses: ncipollo/release-action@v1
with:
generateReleaseNotes: true
allowUpdates: true
omitBodyDuringUpdate: true
omitNameDuringUpdate: true
build-linux:
name: Build Linux x86_64 binary
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
with:
version: latest
- name: Build
id: build
uses: docker/bake-action@v3
with:
pull: true
load: true
targets: static-builder
set: |
*.cache-from=type=gha,scope=${{github.ref}}-static-builder
*.cache-from=type=gha,scope=refs/heads/main-static-builder
*.cache-to=type=gha,scope=${{github.ref}}-static-builder
env:
VERSION: ${{github.ref_type == 'tag' && github.ref_name || github.sha}}
SHA: ${{github.sha}}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Copy binary
run: docker cp $(docker create --name static-builder dunglas/frankenphp:static-builder):/go/src/app/caddy/frankenphp/frankenphp frankenphp-linux-x86_64 ; docker rm static-builder
- name: Upload asset
if: github.ref_type == 'tag'
uses: ncipollo/release-action@v1
with:
generateReleaseNotes: true
allowUpdates: true
omitBodyDuringUpdate: true
omitNameDuringUpdate: true
artifacts: frankenphp-linux-x86_64
- name: Upload artifact
if: github.ref_type == 'branch'
uses: actions/upload-artifact@v3
with:
path: frankenphp-linux-x86_64
build-mac:
name: Build macOS x86_64 binaries
runs-on: macos-latest
env:
HOMEBREW_NO_AUTO_UPDATE: 1
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v4
with:
repository: crazywhalecc/static-php-cli
path: static-php-cli
- name: Install missing system dependencies
run: brew install automake
- uses: actions/setup-go@v4
with:
go-version: '1.21'
cache-dependency-path: |
go.sum
caddy/go.sum
- name: Install static-php-cli dependencies
working-directory: static-php-cli/
run: composer install --no-dev -a
- name: Fetch libraries sources
working-directory: static-php-cli/
run: ./bin/spc fetch --with-php=8.2 -A
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build libphp.a
working-directory: static-php-cli/
run: ./bin/spc build --enable-zts --build-embed "bcmath,calendar,ctype,curl,dba,dom,exif,filter,fileinfo,gd,iconv,intl,mbstring,mbregex,mysqli,mysqlnd,opcache,openssl,pcntl,pdo,pdo_mysql,pdo_pgsql,pdo_sqlite,pgsql,phar,posix,readline,redis,session,simplexml,sockets,sqlite3,tokenizer,xml,xmlreader,xmlwriter,zip,zlib,apcu"
- name: Set CGO flags
working-directory: static-php-cli/
run: |
if [ "$GITHUB_REF_TYPE" == "tag" ]; then export FRANKENPHP_VERSION=${GITHUB_REF_NAME:1}; else export FRANKENPHP_VERSION=$GITHUB_SHA; fi
echo "CGO_CFLAGS=-DFRANKENPHP_VERSION=$FRANKENPHP_VERSION $(./buildroot/bin/php-config --includes | sed s#-I/#-I$PWD/buildroot/#g)" >> "$GITHUB_ENV"
echo "CGO_LDFLAGS=-framework CoreFoundation -framework SystemConfiguration $(./buildroot/bin/php-config --ldflags) $(./buildroot/bin/php-config --libs)" >> "$GITHUB_ENV"
echo "PHP_VERSION=$(./buildroot/bin/php-config --version)" >> "$GITHUB_ENV"
echo "FRANKENPHP_VERSION=$FRANKENPHP_VERSION" >> "$GITHUB_ENV"
- name: Build FrankenPHP
working-directory: caddy/frankenphp/
run: |
go build -buildmode=pie -tags "cgo netgo osusergo static_build" -ldflags "-linkmode=external -extldflags -static-pie -w -s -X 'github.com/caddyserver/caddy/v2.CustomVersion=FrankenPHP $FRANKENPHP_VERSION PHP $PHP_VERSION Caddy'" -o frankenphp-mac-x86_64
./frankenphp-mac-x86_64 version
- name: Upload asset
if: github.ref_type == 'tag'
uses: ncipollo/release-action@v1
with:
generateReleaseNotes: true
allowUpdates: true
omitBodyDuringUpdate: true
omitNameDuringUpdate: true
artifacts: caddy/frankenphp/frankenphp-mac-x86_64
- name: Upload binary
uses: actions/upload-artifact@v3
with:
path: caddy/frankenphp/frankenphp-mac-x86_64

71
.github/workflows/tests.yaml vendored Normal file
View File

@@ -0,0 +1,71 @@
---
name: Tests
on:
pull_request:
branches:
- main
push:
branches:
- main
jobs:
tests:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php-versions: ['8.2', '8.3']
env:
GOEXPERIMENT: cgocheck2
steps:
-
uses: actions/checkout@v4
-
uses: actions/setup-go@v5
with:
go-version: '1.21'
cache-dependency-path: |
go.sum
caddy/go.sum
-
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-versions }}
ini-file: development
coverage: none
tools: none
env:
phpts: ts
-
name: Set CGO flags
run: |
echo "CGO_CFLAGS=$(php-config --includes)" >> "$GITHUB_ENV"
-
name: Build
run: go build
-
name: Build testcli binary
working-directory: internal/testcli/
run: go build
-
name: Run library tests
run: go test -race -v ./...
-
name: Run Caddy module tests
working-directory: caddy/
run: go test -race -v ./...
-
name: Build the server
working-directory: caddy/frankenphp/
run: go build
-
name: Start the server
working-directory: testdata/
run: sudo ../caddy/frankenphp/frankenphp start
-
name: Run integrations tests
run: ./reload_test.sh
-
name: Lint Go code
uses: golangci/golangci-lint-action@v3
with:
version: latest

View File

@@ -1,41 +0,0 @@
name: Tests
on: [push, pull_request]
jobs:
tests:
runs-on: ubuntu-latest
strategy:
matrix:
php-versions: ['8.2', '8.3']
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v4
with:
go-version: '1.21'
cache-dependency-path: |
go.sum
caddy/go.sum
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-versions }}
ini-file: development
extensions: opcache
coverage: none
env:
phpts: ts
- name: Set CGO flags
run: echo "CGO_CFLAGS=$(php-config --includes)" >> "$GITHUB_ENV"
- name: Build
run: go build
env:
GOEXPERIMENT: cgocheck2
- name: Run library tests
run: go test -race -v ./...
- name: Run Caddy module tests
working-directory: caddy/
run: go test -race -v ./...

2
.gitignore vendored
View File

@@ -1,5 +1,7 @@
/caddy/frankenphp/frankenphp
/internal/testserver/testserver
/internal/testcli/testcli
/dist
.idea/
.vscode/
__debug_bin

6
.hadolint.yaml Normal file
View File

@@ -0,0 +1,6 @@
---
ignored:
- DL3006
- DL3008
- DL3018
- DL3022

4
.markdown-lint.yaml Normal file
View File

@@ -0,0 +1,4 @@
---
no-hard-tabs: false
MD013: false
MD033: false

View File

@@ -1,4 +1,5 @@
[![CircleCI](https://circleci.com/gh/Pithikos/C-Thread-Pool.svg?style=svg)](https://circleci.com/gh/Pithikos/C-Thread-Pool)
[![GitHub Actions](https://github.com/Pithikos/C-Thread-Pool/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/Pithikos/C-Thread-Pool/actions?query=workflow%3Atests+branch%3Amaster)
# C Thread Pool

View File

@@ -1,40 +0,0 @@
/*
* WHAT THIS EXAMPLE DOES
*
* We create a pool of 4 threads and then add 40 tasks to the pool(20 task1
* functions and 20 task2 functions). task1 and task2 simply print which thread is running them.
*
* As soon as we add the tasks to the pool, the threads will run them. It can happen that
* you see a single thread running all the tasks (highly unlikely). It is up the OS to
* decide which thread will run what. So it is not an error of the thread pool but rather
* a decision of the OS.
*
* */
#include <stdio.h>
#include <pthread.h>
#include <stdint.h>
#include "thpool.h"
void task(void *arg){
printf("Thread #%u working on %d\n", (int)pthread_self(), (int) arg);
}
int main(){
puts("Making threadpool with 4 threads");
threadpool thpool = thpool_init(4);
puts("Adding 40 tasks to threadpool");
int i;
for (i=0; i<40; i++){
thpool_add_work(thpool, task, (void*)(uintptr_t)i);
};
thpool_wait(thpool);
puts("Killing threadpool");
thpool_destroy(thpool);
return 0;
}

View File

@@ -14,6 +14,9 @@
#ifndef _POSIX_C_SOURCE
#define _POSIX_C_SOURCE 200809L
#endif
#ifndef _XOPEN_SOURCE
#define _XOPEN_SOURCE 500
#endif
#endif
#include <unistd.h>
#include <signal.h>
@@ -25,6 +28,9 @@
#if defined(__linux__)
#include <sys/prctl.h>
#endif
#if defined(__FreeBSD__) || defined(__OpenBSD__)
#include <pthread_np.h>
#endif
#include "thpool.h"
@@ -40,6 +46,13 @@
#define err(str)
#endif
#ifndef THPOOL_THREAD_NAME
#define THPOOL_THREAD_NAME thpool
#endif
#define STRINGIFY(x) #x
#define TOSTRING(x) STRINGIFY(x)
static volatile int threads_keepalive;
static volatile int threads_on_hold;
@@ -211,7 +224,7 @@ void thpool_wait(thpool_* thpool_p){
/* Destroy the threadpool */
void thpool_destroy(thpool_* thpool_p){
/* No need to destory if it's NULL */
/* No need to destroy if it's NULL */
if (thpool_p == NULL) return ;
volatile int threads_total = thpool_p->num_threads_alive;
@@ -260,7 +273,7 @@ void thpool_pause(thpool_* thpool_p) {
/* Resume all threads in threadpool */
void thpool_resume(thpool_* thpool_p) {
// resuming a single threadpool hasn't been
// implemented yet, meanwhile this supresses
// implemented yet, meanwhile this suppresses
// the warnings
(void)thpool_p;
@@ -314,7 +327,7 @@ static void thread_hold(int sig_id) {
/* What each thread is doing
*
* In principle this is an endless loop. The only time this loop gets interuppted is once
* In principle this is an endless loop. The only time this loop gets interrupted is once
* thpool_destroy() is invoked or the program exits.
*
* @param thread thread that will run this function
@@ -322,15 +335,18 @@ static void thread_hold(int sig_id) {
*/
static void* thread_do(struct thread* thread_p){
/* Set thread name for profiling and debuging */
/* Set thread name for profiling and debugging */
char thread_name[16] = {0};
snprintf(thread_name, 16, "thpool-%d", thread_p->id);
snprintf(thread_name, 16, TOSTRING(THPOOL_THREAD_NAME) "-%d", thread_p->id);
#if defined(__linux__)
/* Use prctl instead to prevent using _GNU_SOURCE flag and implicit declaration */
prctl(PR_SET_NAME, thread_name);
#elif defined(__APPLE__) && defined(__MACH__)
pthread_setname_np(thread_name);
#elif defined(__FreeBSD__) || defined(__OpenBSD__)
pthread_set_name_np(thread_p->pthread, thread_name);
#else
err("thread_do(): pthread_setname_np is not supported on this system");
#endif
@@ -520,6 +536,8 @@ static void bsem_init(bsem *bsem_p, int value) {
/* Reset semaphore to 0 */
static void bsem_reset(bsem *bsem_p) {
pthread_mutex_destroy(&(bsem_p->mutex));
pthread_cond_destroy(&(bsem_p->cond));
bsem_init(bsem_p, 0);
}

View File

@@ -1,80 +1,107 @@
# Contributing
## Compiling PHP
### With Docker (Linux)
Build the dev Docker image:
docker build -t frankenphp-dev -f dev.Dockerfile .
docker run --cap-add=SYS_PTRACE --security-opt seccomp=unconfined -p 8080:8080 -p 443:443 -v $PWD:/go/src/app -it frankenphp-dev
```console
docker build -t frankenphp-dev -f dev.Dockerfile .
docker run --cap-add=SYS_PTRACE --security-opt seccomp=unconfined -p 8080:8080 -p 443:443 -p 443:443/udp -v $PWD:/go/src/app -it frankenphp-dev
```
The image contains the usual development tools (Go, GDB, Valgrind, Neovim...).
The image contains the usual development tools (Go, GDB, Valgrind, Neovim...).
If docker version is lower than 23.0, build is failed by dockerignore [pattern issue](https://github.com/moby/moby/pull/42676). Add directories to `.dockerignore`.
```patch
!testdata/*.php
!testdata/*.txt
+!caddy
+!C-Thread-Pool
+!internal
```
### Without Docker (Linux and macOS)
[Follow the instructions to compile from sources](docs/compile.md) and pass the `--debug` configuration flag.
[Follow the instructions to compile from sources](https://frankenphp.dev/docs/compile/) and pass the `--debug` configuration flag.
## Running the test suite
go test -race -v ./...
```console
go test -race -v ./...
```
## Caddy module
Build Caddy with the FrankenPHP Caddy module:
cd caddy/frankenphp/
go build
cd ../../
```console
cd caddy/frankenphp/
go build
cd ../../
```
Run the Caddy with the FrankenPHP Caddy module:
cd testdata/
../caddy/frankenphp/frankenphp run
```console
cd testdata/
../caddy/frankenphp/frankenphp run
```
The server is listening on `127.0.0.1:8080`:
curl -vk https://localhost/phpinfo.php
```console
curl -vk https://localhost/phpinfo.php
```
## Minimal test server
Build the minimal test server:
cd internal/testserver/
go build
cd ../../
```console
cd internal/testserver/
go build
cd ../../
```
Run the test server:
cd testdata/
../internal/testserver/testserver
```console
cd testdata/
../internal/testserver/testserver
```
The server is listening on `127.0.0.1:8080`:
curl -v http://127.0.0.1:8080/phpinfo.php
```console
curl -v http://127.0.0.1:8080/phpinfo.php
```
# Building Docker Images Locally
## Building Docker Images Locally
Print bake plan:
```
```console
docker buildx bake -f docker-bake.hcl --print
```
Build FrankenPHP images for amd64 locally:
```
```console
docker buildx bake -f docker-bake.hcl --pull --load --set "*.platform=linux/amd64"
```
Build FrankenPHP images for arm64 locally:
```
```console
docker buildx bake -f docker-bake.hcl --pull --load --set "*.platform=linux/arm64"
```
Build FrankenPHP images from scratch for arm64 & amd64 and push to Docker Hub:
```
```console
docker buildx bake -f docker-bake.hcl --pull --no-cache --push
```
@@ -82,6 +109,7 @@ docker buildx bake -f docker-bake.hcl --pull --no-cache --push
1. Open `.github/workflows/tests.yml`
2. Enable PHP debug symbols
```patch
- uses: shivammathur/setup-php@v2
# ...
@@ -89,33 +117,40 @@ docker buildx bake -f docker-bake.hcl --pull --no-cache --push
phpts: ts
+ debug: true
```
3. Enable `tmate` to connect to the container
```patch
- name: Set include flags
-
name: Set CGO flags
run: echo "CGO_CFLAGS=$(php-config --includes)" >> "$GITHUB_ENV"
+ - run: |
sudo apt install gdb
+ -
+ run: |
+ sudo apt install gdb
+ mkdir -p /home/runner/.config/gdb/
+ printf "set auto-load safe-path /\nhandle SIG34 nostop noprint pass" > /home/runner/.config/gdb/gdbinit
+ - uses: mxschmitt/action-tmate@v3
+ env:
+ GOFLAGS: "-w -gcflags=all=-N -gcflags=all=-l"
+ -
+ uses: mxschmitt/action-tmate@v3
```
4. Open `frankenphp.go`
5. Enable `cgosymbolizer`
4. Connect to the container
5. Open `frankenphp.go`
6. Enable `cgosymbolizer`
```patch
- //_ "github.com/ianlancetaylor/cgosymbolizer"
+ _ "github.com/ianlancetaylor/cgosymbolizer"
```
6. Download the module: `go get`
7. In the container, you can use GDB and the like:
```sh
sudo apt install gdb
mkdir -p /home/runner/.config/gdb/
7. Download the module: `go get`
8. In the container, you can use GDB and the like:
```console
go test -c -ldflags=-w
gdb --args ./frankenphp.test -test.run ^MyTest$
```
8. When the bug is fixed, revert all these changes
9. When the bug is fixed, revert all these changes
## Misc Dev Resources
@@ -134,10 +169,9 @@ docker buildx bake -f docker-bake.hcl --pull --no-cache --push
* [Bake file definition](https://docs.docker.com/build/customize/bake/file-definition/)
* [docker buildx build](https://docs.docker.com/engine/reference/commandline/buildx_build/)
## Useful Command
```
```console
apk add strace util-linux gdb
strace -e 'trace=!futex,epoll_ctl,epoll_pwait,tgkill,rt_sigreturn' -p 1
```
```

View File

@@ -4,12 +4,12 @@ FROM php-base AS common
WORKDIR /app
RUN apt-get update && \
apt-get -y --no-install-recommends install \
mailcap \
libcap2-bin \
&& \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
apt-get -y --no-install-recommends install \
mailcap \
libcap2-bin \
&& \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
RUN set -eux; \
mkdir -p \
@@ -24,7 +24,7 @@ COPY --link caddy/frankenphp/Caddyfile /etc/caddy/Caddyfile
COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/local/bin/
CMD ["--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"]
HEALTHCHECK CMD curl -f https://localhost/healthz || exit 1
HEALTHCHECK CMD curl -f http://localhost:2019/metrics || exit 1
# See https://caddyserver.com/docs/conventions#file-locations for details
ENV XDG_CONFIG_HOME /config
@@ -46,6 +46,7 @@ LABEL org.opencontainers.image.vendor="Kévin Dunglas"
FROM common AS builder
ARG FRANKENPHP_VERSION='dev'
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
COPY --from=golang-base /usr/local/go /usr/local/go
@@ -53,30 +54,30 @@ ENV PATH /usr/local/go/bin:$PATH
# This is required to link the FrankenPHP binary to the PHP binary
RUN apt-get update && \
apt-get -y --no-install-recommends install \
libargon2-dev \
libcurl4-openssl-dev \
libonig-dev \
libreadline-dev \
libsodium-dev \
libsqlite3-dev \
libssl-dev \
libxml2-dev \
zlib1g-dev \
&& \
apt-get clean
apt-get -y --no-install-recommends install \
libargon2-dev \
libbrotli-dev \
libcurl4-openssl-dev \
libonig-dev \
libreadline-dev \
libsodium-dev \
libsqlite3-dev \
libssl-dev \
libxml2-dev \
zlib1g-dev \
&& \
apt-get clean
WORKDIR /go/src/app
COPY --link go.mod go.sum ./
RUN go mod graph | awk '{if ($1 !~ "@") print $2}' | xargs go get
RUN mkdir caddy && cd caddy
COPY --link caddy/go.mod caddy/go.sum ./caddy/
RUN cd caddy && \
go mod graph | awk '{if ($1 !~ "@") print $2}' | xargs go get
WORKDIR /go/src/app/caddy
COPY --link caddy/go.mod caddy/go.sum ./
RUN go mod graph | awk '{if ($1 !~ "@") print $2}' | xargs go get
WORKDIR /go/src/app
COPY --link *.* ./
COPY --link caddy caddy
COPY --link C-Thread-Pool C-Thread-Pool
@@ -87,11 +88,13 @@ COPY --link testdata testdata
# see https://github.com/docker-library/php/blob/master/8.2/bookworm/zts/Dockerfile#L57-L59 for PHP values
ENV CGO_LDFLAGS="-lssl -lcrypto -lreadline -largon2 -lcurl -lonig -lz $PHP_LDFLAGS" CGO_CFLAGS="-DFRANKENPHP_VERSION=$FRANKENPHP_VERSION $PHP_CFLAGS" CGO_CPPFLAGS=$PHP_CPPFLAGS
RUN cd caddy/frankenphp && \
GOBIN=/usr/local/bin go install -ldflags "-X 'github.com/caddyserver/caddy/v2.CustomVersion=FrankenPHP $FRANKENPHP_VERSION PHP $PHP_VERSION Caddy'" && \
setcap cap_net_bind_service=+ep /usr/local/bin/frankenphp && \
cp Caddyfile /etc/caddy/Caddyfile && \
frankenphp version
WORKDIR /go/src/app/caddy/frankenphp
RUN GOBIN=/usr/local/bin go install -ldflags "-w -s -X 'github.com/caddyserver/caddy/v2.CustomVersion=FrankenPHP $FRANKENPHP_VERSION PHP $PHP_VERSION Caddy'" && \
setcap cap_net_bind_service=+ep /usr/local/bin/frankenphp && \
cp Caddyfile /etc/caddy/Caddyfile && \
frankenphp version
WORKDIR /go/src/app
FROM common AS runner

View File

@@ -4,9 +4,9 @@
FrankenPHP is a modern application server for PHP built on top of the [Caddy](https://caddyserver.com/) web server.
FrankenPHP gives superpowers to your PHP apps thanks to its stunning features: [*Early Hints*](docs/early-hints.md), [worker mode](docs/worker.md), [real-time capabilities](docs/mercure.md), automatic HTTPS, HTTP/2, and HTTP/3 support...
FrankenPHP gives superpowers to your PHP apps thanks to its stunning features: [*Early Hints*](https://frankenphp.dev/docs/early-hints/), [worker mode](https://frankenphp.dev/docs/worker/), [real-time capabilities](https://frankenphp.dev/docs/mercure/), automatic HTTPS, HTTP/2, and HTTP/3 support...
FrankenPHP works with any PHP app and makes your Symfony projects faster than ever thanks to provided integration with the worker mode (Laravel Octane support coming).
FrankenPHP works with any PHP app and makes your Symfony and Laravel projects faster than ever thanks to the provided integration with the worker mode.
FrankenPHP can also be used as a standalone Go library to embed PHP in any app using `net/http`.
@@ -16,35 +16,61 @@ FrankenPHP can also be used as a standalone Go library to embed PHP in any app u
## Getting Started
### Docker
```console
docker run -v $PWD:/app/public \
-p 80:80 -p 443:443 \
-p 80:80 -p 443:443 -p 443:443/udp \
dunglas/frankenphp
```
Go to `https://localhost`, and enjoy!
If you prefer not to use Docker, we provide standalone FrankenPHP binaries for Linux and macOS
containing [PHP 8.2](https://www.php.net/releases/8.2/en.php) and most popular PHP extensions: [Download FrankenPHP](https://github.com/dunglas/frankenphp/releases)
> [!TIP]
>
> Do not attempt to use `https://127.0.0.1`. Use `localhost` and accept the self-signed certificate.
> Use the [`SERVER_NAME` environment variable](docs/config.md#environment-variables) to change the domain to use.
> Note: do not attempt to use `https://127.0.0.1`. Use `localhost` and accept the self-signed certificate. Caddy has an automatic TLS handling that auto-trusts some local-based hostnames like `localhost`, but it does not apply to IP addresses. More details [on Caddy's "automatic https" docs](https://caddyserver.com/docs/automatic-https#hostname-requirements).
### Standalone Binary
If you prefer not to use Docker, we provide standalone FrankenPHP binaries for Linux and macOS
containing [PHP 8.3](https://www.php.net/releases/8.3/en.php) and most popular PHP extensions: [Download FrankenPHP](https://github.com/dunglas/frankenphp/releases)
To serve the content of the current directory, run:
```console
./frankenphp php-server
```
You can also run command-line scripts with:
```console
./frankenphp php-cli /path/to/your/script.php
```
## Docs
* [The worker mode](docs/worker.md)
* [Early Hints support (103 HTTP status code)](docs/early-hints.md)
* [Real-time](docs/mercure.md)
* [Configuration](docs/config.md)
* [Docker images](docs/docker.md)
* [Compile from sources](docs/compile.md)
* [Create static binaries](docs/static.md)
* [The worker mode](https://frankenphp.dev/docs/worker/)
* [Early Hints support (103 HTTP status code)](https://frankenphp.dev/docs/early-hints/)
* [Real-time](https://frankenphp.dev/docs/mercure/)
* [Configuration](https://frankenphp.dev/docs/config/)
* [Docker images](https://frankenphp.dev/docs/docker/)
* [Deploy in production](docs/production.md)
* [Create **standalone**, self-executable PHP apps](https://frankenphp.dev/docs/embed/)
* [Create static binaries](https://frankenphp.dev/docs/static/)
* [Compile from sources](https://frankenphp.dev/docs/compile/)
* [Known issues](https://frankenphp.dev/docs/known-issues/)
* [Demo app (Symfony) and benchmarks](https://github.com/dunglas/frankenphp-demo)
* [Go library documentation](https://pkg.go.dev/github.com/dunglas/frankenphp)
* [Contributing and debugging](CONTRIBUTING.md)
* [Contributing and debugging](https://frankenphp.dev/docs/contributing/)
## Examples and Skeletons
* [Symfony apps](https://github.com/dunglas/frankenphp-demo)
* [Symfony](https://github.com/dunglas/symfony-docker)
* [API Platform](https://api-platform.com/docs/distribution/)
* [Laravel](https://frankenphp.dev/docs/laravel/)
* [Sulu](https://sulu.io/blog/running-sulu-with-frankenphp)
* [WordPress](https://github.com/dunglas/frankenphp-wordpress)
* [Drupal](https://github.com/dunglas/frankenphp-drupal)
* [Sulu](https://sulu.io/blog/running-sulu-with-frankenphp)
* [Joomla](https://github.com/alexandreelise/frankenphp-joomla)
* [TYPO3](https://github.com/ochorocho/franken-typo3)

View File

@@ -21,7 +21,7 @@ COPY --link caddy/frankenphp/Caddyfile /etc/caddy/Caddyfile
COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/local/bin/
CMD ["--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"]
HEALTHCHECK CMD curl -f https://localhost/healthz || exit 1
HEALTHCHECK CMD curl -f http://localhost:2019/metrics || exit 1
# See https://caddyserver.com/docs/conventions#file-locations for details
ENV XDG_CONFIG_HOME /config
@@ -43,14 +43,17 @@ LABEL org.opencontainers.image.vendor="Kévin Dunglas"
FROM common AS builder
ARG FRANKENPHP_VERSION='dev'
SHELL ["/bin/ash", "-eo", "pipefail", "-c"]
COPY --link --from=golang-base /usr/local/go /usr/local/go
ENV PATH /usr/local/go/bin:$PATH
# hadolint ignore=SC2086
RUN apk add --no-cache --virtual .build-deps \
$PHPIZE_DEPS \
argon2-dev \
brotli-dev \
coreutils \
curl-dev \
gnu-libiconv-dev \
@@ -67,11 +70,11 @@ WORKDIR /go/src/app
COPY --link go.mod go.sum ./
RUN go mod graph | awk '{if ($1 !~ "@") print $2}' | xargs go get
RUN mkdir caddy && cd caddy
COPY caddy/go.mod caddy/go.sum ./caddy/
RUN cd caddy && go mod graph | awk '{if ($1 !~ "@") print $2}' | xargs go get
WORKDIR /go/src/app/caddy
COPY caddy/go.mod caddy/go.sum ./
RUN go mod graph | awk '{if ($1 !~ "@") print $2}' | xargs go get
WORKDIR /go/src/app
COPY --link *.* ./
COPY --link caddy caddy
COPY --link C-Thread-Pool C-Thread-Pool
@@ -82,11 +85,13 @@ COPY --link testdata testdata
# see https://github.com/docker-library/php/blob/master/8.2/bookworm/zts/Dockerfile#L57-L59 for php values
ENV CGO_LDFLAGS="-lssl -lcrypto -lreadline -largon2 -lcurl -lonig -lz $PHP_LDFLAGS" CGO_CFLAGS="-DFRANKENPHP_VERSION=$FRANKENPHP_VERSION $PHP_CFLAGS" CGO_CPPFLAGS=$PHP_CPPFLAGS
RUN cd caddy/frankenphp && \
GOBIN=/usr/local/bin go install -ldflags "-X 'github.com/caddyserver/caddy/v2.CustomVersion=FrankenPHP $FRANKENPHP_VERSION PHP $PHP_VERSION Caddy'" && \
WORKDIR /go/src/app/caddy/frankenphp
RUN GOBIN=/usr/local/bin go install -ldflags "-w -s -extldflags '-Wl,-z,stack-size=0x80000' -X 'github.com/caddyserver/caddy/v2.CustomVersion=FrankenPHP $FRANKENPHP_VERSION PHP $PHP_VERSION Caddy'" && \
setcap cap_net_bind_service=+ep /usr/local/bin/frankenphp && \
frankenphp version
WORKDIR /go/src/app
FROM common AS runner

0
app.tar Normal file
View File

0
app_checksum.txt Normal file
View File

155
build-static.sh Executable file
View File

@@ -0,0 +1,155 @@
#!/bin/sh
set -o errexit
if ! type "git" > /dev/null; then
echo "The \"git\" command must be installed."
exit 1
fi
arch="$(uname -m)"
os="$(uname -s | tr '[:upper:]' '[:lower:]')"
if [ "${os}" = "darwin" ]; then
os="mac"
fi
if [ -z "${PHP_EXTENSIONS}" ]; then
if [ "${os}" = "mac" ] && [ "${arch}" = "x86_64" ]; then
# Temporary fix for https://github.com/crazywhalecc/static-php-cli/issues/280 (remove ldap)
export PHP_EXTENSIONS="apcu,bcmath,bz2,calendar,ctype,curl,dba,dom,exif,fileinfo,filter,gd,iconv,igbinary,intl,mbregex,mbstring,mysqli,mysqlnd,opcache,openssl,pcntl,pdo,pdo_mysql,pdo_pgsql,pdo_sqlite,pgsql,phar,posix,readline,redis,session,simplexml,sockets,sodium,sqlite3,sysvsem,tokenizer,xml,xmlreader,xmlwriter,zip,zlib"
else
export PHP_EXTENSIONS="apcu,bcmath,bz2,calendar,ctype,curl,dba,dom,exif,fileinfo,filter,gd,iconv,igbinary,intl,ldap,mbregex,mbstring,mysqli,mysqlnd,opcache,openssl,pcntl,pdo,pdo_mysql,pdo_pgsql,pdo_sqlite,pgsql,phar,posix,readline,redis,session,simplexml,sockets,sodium,sqlite3,sysvsem,tokenizer,xml,xmlreader,xmlwriter,zip,zlib"
fi
fi
if [ -z "${PHP_EXTENSION_LIBS}" ]; then
export PHP_EXTENSION_LIBS="bzip2,freetype,libavif,libjpeg,liblz4,libwebp,libzip"
fi
if [ -z "${PHP_VERSION}" ]; then
export PHP_VERSION="8.3"
fi
if [ -z "${FRANKENPHP_VERSION}" ]; then
FRANKENPHP_VERSION="$(git rev-parse --verify HEAD)"
export FRANKENPHP_VERSION
elif [ -d ".git/" ]; then
CURRENT_REF="$(git rev-parse --abbrev-ref HEAD)"
export CURRENT_REF
if echo "${FRANKENPHP_VERSION}" | grep -F -q "."; then
# Tag
# Trim "v" prefix if any
FRANKENPHP_VERSION=${FRANKENPHP_VERSION#v}
export FRANKENPHP_VERSION
git checkout "v${FRANKENPHP_VERSION}"
else
git checkout "${FRANKENPHP_VERSION}"
fi
fi
bin="frankenphp-${os}-${arch}"
if [ -n "${CLEAN}" ]; then
rm -Rf dist/
go clean -cache
fi
# Build libphp if ncessary
if [ -f "dist/static-php-cli/buildroot/lib/libphp.a" ]; then
cd dist/static-php-cli
else
mkdir -p dist/
cd dist/
if [ -d "static-php-cli/" ]; then
cd static-php-cli/
git pull
else
git clone --depth 1 https://github.com/crazywhalecc/static-php-cli
cd static-php-cli/
fi
if type "brew" > /dev/null; then
packages="composer"
if ! type "go" > /dev/null; then
packages="${packages} go"
fi
if [ -n "${RELEASE}" ]; then
packages="${packages} gh"
fi
# shellcheck disable=SC2086
brew install --formula --quiet ${packages}
fi
composer install --no-dev -a
if [ "${os}" = "linux" ]; then
extraOpts="--disable-opcache-jit"
fi
if [ -n "${DEBUG_SYMBOLS}" ]; then
extraOpts="${extraOpts} --no-strip"
fi
./bin/spc doctor
./bin/spc fetch --with-php="${PHP_VERSION}" --for-extensions="${PHP_EXTENSIONS}"
# the Brotli library must always be built as it is required by http://github.com/dunglas/caddy-cbrotli
# shellcheck disable=SC2086
./bin/spc build --enable-zts --build-embed ${extraOpts} "${PHP_EXTENSIONS}" --with-libs="brotli,${PHP_EXTENSION_LIBS}"
fi
CGO_CFLAGS="-DFRANKENPHP_VERSION=${FRANKENPHP_VERSION} -I${PWD}/buildroot/include/ $(./buildroot/bin/php-config --includes | sed s#-I/#-I"${PWD}"/buildroot/#g)"
if [ -n "${DEBUG_SYMBOLS}" ]; then
CGO_CFLAGS="-g ${CGO_CFLAGS}"
fi
export CGO_CFLAGS
if [ "${os}" = "mac" ]; then
export CGO_LDFLAGS="-framework CoreFoundation -framework SystemConfiguration"
fi
CGO_LDFLAGS="${CGO_LDFLAGS} ${PWD}/buildroot/lib/libbrotlicommon.a ${PWD}/buildroot/lib/libbrotlienc.a ${PWD}/buildroot/lib/libbrotlidec.a $(./buildroot/bin/php-config --ldflags) $(./buildroot/bin/php-config --libs)"
export CGO_LDFLAGS
LIBPHP_VERSION="$(./buildroot/bin/php-config --version)"
export LIBPHP_VERSION
cd ../..
# Embed PHP app, if any
if [ -n "${EMBED}" ] && [ -d "${EMBED}" ]; then
tar -cf app.tar -C "${EMBED}" .
md5 -q app.tar > app_checksum.txt
fi
if [ "${os}" = "linux" ]; then
extraExtldflags="-Wl,-z,stack-size=0x80000"
fi
if [ -z "${DEBUG_SYMBOLS}" ]; then
extraLdflags="-w -s"
fi
cd caddy/frankenphp/
go env
go build -buildmode=pie -tags "cgo netgo osusergo static_build" -ldflags "-linkmode=external -extldflags '-static-pie ${extraExtldflags}' ${extraLdflags} -X 'github.com/caddyserver/caddy/v2.CustomVersion=FrankenPHP ${FRANKENPHP_VERSION} PHP ${LIBPHP_VERSION} Caddy'" -o "../../dist/${bin}"
cd ../..
if [ -d "${EMBED}" ]; then
truncate -s 0 app.tar
truncate -s 0 app_checksum.txt
fi
"dist/${bin}" version
if [ -n "${RELEASE}" ]; then
gh release upload "v${FRANKENPHP_VERSION}" "dist/${bin}" --repo dunglas/frankenphp --clobber
fi
if [ -n "${CURRENT_REF}" ]; then
git checkout "${CURRENT_REF}"
fi

View File

@@ -4,8 +4,10 @@
package caddy
import (
"encoding/json"
"errors"
"net/http"
"path/filepath"
"strconv"
"github.com/caddyserver/caddy/v2"
@@ -13,15 +15,20 @@ import (
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/caddy/v2/modules/caddyhttp/fileserver"
"github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite"
"github.com/dunglas/frankenphp"
"go.uber.org/zap"
)
const defaultDocumentRoot = "public"
func init() {
caddy.RegisterModule(FrankenPHPApp{})
caddy.RegisterModule(FrankenPHPModule{})
httpcaddyfile.RegisterGlobalOption("frankenphp", parseGlobalOption)
httpcaddyfile.RegisterHandlerDirective("php", parseCaddyfile)
httpcaddyfile.RegisterDirective("php_server", parsePhpServer)
}
type mainPHPinterpreterKeyType int
@@ -58,7 +65,7 @@ type FrankenPHPApp struct {
func (a FrankenPHPApp) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "frankenphp",
New: func() caddy.Module { return a },
New: func() caddy.Module { return &a },
}
}
@@ -89,8 +96,6 @@ func (f *FrankenPHPApp) Start() error {
}
}
logger.Info("FrankenPHP started 🐘", zap.String("php_version", frankenphp.Version().Version))
return nil
}
@@ -165,6 +170,10 @@ func (f *FrankenPHPApp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
if wc.FileName == "" {
return errors.New(`The "file" argument must be specified`)
}
if frankenphp.EmbeddedAppPath != "" && filepath.IsLocal(wc.FileName) {
wc.FileName = filepath.Join(frankenphp.EmbeddedAppPath, wc.FileName)
}
}
f.Workers = append(f.Workers, wc)
@@ -189,7 +198,7 @@ func parseGlobalOption(d *caddyfile.Dispenser, _ interface{}) (interface{}, erro
}
type FrankenPHPModule struct {
// Root sets the root folder to the site. Default: `root` directive.
// Root sets the root folder to the site. Default: `root` directive, or the path of the public directory of the embed app it exists.
Root string `json:"root,omitempty"`
// SplitPath sets the substrings for splitting the URI into two parts. The first matching substring will be used to split the "path info" from the path. The first piece is suffixed with the matching substring and will be assumed as the actual resource (CGI script) name. The second piece will be set to PATH_INFO for the CGI script to use. Default: `.php`.
SplitPath []string `json:"split_path,omitempty"`
@@ -213,8 +222,18 @@ func (f *FrankenPHPModule) Provision(ctx caddy.Context) error {
f.logger = ctx.Logger(f)
if f.Root == "" {
f.Root = "{http.vars.root}"
if frankenphp.EmbeddedAppPath == "" {
f.Root = "{http.vars.root}"
} else {
f.Root = filepath.Join(frankenphp.EmbeddedAppPath, defaultDocumentRoot)
f.ResolveRootSymlink = false
}
} else {
if frankenphp.EmbeddedAppPath != "" && filepath.IsLocal(f.Root) {
f.Root = filepath.Join(frankenphp.EmbeddedAppPath, f.Root)
}
}
if len(f.SplitPath) == 0 {
f.SplitPath = []string{".php"}
}
@@ -224,19 +243,27 @@ func (f *FrankenPHPModule) Provision(ctx caddy.Context) error {
// ServeHTTP implements caddyhttp.MiddlewareHandler.
// TODO: Expose TLS versions as env vars, as Apache's mod_ssl: https://github.com/caddyserver/caddy/blob/master/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go#L298
func (f FrankenPHPModule) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
func (f FrankenPHPModule) ServeHTTP(w http.ResponseWriter, r *http.Request, _ caddyhttp.Handler) error {
origReq := r.Context().Value(caddyhttp.OriginalRequestCtxKey).(http.Request)
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
documentRoot := repl.ReplaceKnown(f.Root, "")
fr := frankenphp.NewRequestWithContext(r, documentRoot, f.logger)
fc, _ := frankenphp.FromContext(fr.Context())
fc.ResolveRootSymlink = f.ResolveRootSymlink
fc.SplitPath = f.SplitPath
fc.Env["REQUEST_URI"] = origReq.URL.RequestURI()
env := make(map[string]string, len(f.Env)+1)
env["REQUEST_URI"] = origReq.URL.RequestURI()
for k, v := range f.Env {
fc.Env[k] = repl.ReplaceKnown(v, "")
env[k] = repl.ReplaceKnown(v, "")
}
fr, err := frankenphp.NewRequestWithContext(
r,
frankenphp.WithRequestDocumentRoot(documentRoot, f.ResolveRootSymlink),
frankenphp.WithRequestSplitPath(f.SplitPath),
frankenphp.WithRequestEnv(env),
)
if err != nil {
return err
}
return frankenphp.ServeHTTP(w, fr)
@@ -283,12 +310,265 @@ func (f *FrankenPHPModule) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
// parseCaddyfile unmarshals tokens from h into a new Middleware.
func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
var m FrankenPHPModule
m := FrankenPHPModule{}
err := m.UnmarshalCaddyfile(h.Dispenser)
return m, err
}
// parsePhpServer parses the php_server directive, which has a similar syntax
// to the php_fastcgi directive. A line such as this:
//
// php_server
//
// is equivalent to a route consisting of:
//
// # Add trailing slash for directory requests
// @canonicalPath {
// file {path}/index.php
// not path */
// }
// redir @canonicalPath {path}/ 308
//
// # If the requested file does not exist, try index files
// @indexFiles file {
// try_files {path} {path}/index.php index.php
// split_path .php
// }
// rewrite @indexFiles {http.matchers.file.relative}
//
// # FrankenPHP!
// @phpFiles path *.php
// php @phpFiles
// file_server
//
// parsePhpServer is freely inspired from the php_fastgci directive of the Caddy server (Apache License 2.0, Matthew Holt and The Caddy Authors)
func parsePhpServer(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error) {
if !h.Next() {
return nil, h.ArgErr()
}
// set up FrankenPHP
phpsrv := FrankenPHPModule{}
// set up file server
fsrv := fileserver.FileServer{}
disableFsrv := false
// set up the set of file extensions allowed to execute PHP code
extensions := []string{".php"}
// set the default index file for the try_files rewrites
indexFile := "index.php"
// set up for explicitly overriding try_files
tryFiles := []string{}
// if the user specified a matcher token, use that
// matcher in a route that wraps both of our routes;
// either way, strip the matcher token and pass
// the remaining tokens to the unmarshaler so that
// we can gain the rest of the directive syntax
userMatcherSet, err := h.ExtractMatcherSet()
if err != nil {
return nil, err
}
// make a new dispenser from the remaining tokens so that we
// can reset the dispenser back to this point for the
// php unmarshaler to read from it as well
dispenser := h.NewFromNextSegment()
// read the subdirectives that we allow as overrides to
// the php_server shortcut
// NOTE: we delete the tokens as we go so that the php
// unmarshal doesn't see these subdirectives which it cannot handle
for dispenser.Next() {
for dispenser.NextBlock(0) {
// ignore any sub-subdirectives that might
// have the same name somewhere within
// the php passthrough tokens
if dispenser.Nesting() != 1 {
continue
}
// parse the php_server subdirectives
switch dispenser.Val() {
case "root":
if !dispenser.NextArg() {
return nil, dispenser.ArgErr()
}
phpsrv.Root = dispenser.Val()
fsrv.Root = phpsrv.Root
dispenser.DeleteN(2)
case "split":
extensions = dispenser.RemainingArgs()
dispenser.DeleteN(len(extensions) + 1)
if len(extensions) == 0 {
return nil, dispenser.ArgErr()
}
case "index":
args := dispenser.RemainingArgs()
dispenser.DeleteN(len(args) + 1)
if len(args) != 1 {
return nil, dispenser.ArgErr()
}
indexFile = args[0]
case "try_files":
args := dispenser.RemainingArgs()
dispenser.DeleteN(len(args) + 1)
if len(args) < 1 {
return nil, dispenser.ArgErr()
}
tryFiles = args
case "file_server":
args := dispenser.RemainingArgs()
dispenser.DeleteN(len(args) + 1)
if len(args) < 1 || args[0] != "off" {
return nil, dispenser.ArgErr()
}
disableFsrv = true
}
}
}
// reset the dispenser after we're done so that the frankenphp
// unmarshaler can read it from the start
dispenser.Reset()
if frankenphp.EmbeddedAppPath != "" {
if phpsrv.Root == "" {
phpsrv.Root = filepath.Join(frankenphp.EmbeddedAppPath, defaultDocumentRoot)
fsrv.Root = phpsrv.Root
phpsrv.ResolveRootSymlink = false
} else if filepath.IsLocal(fsrv.Root) {
phpsrv.Root = filepath.Join(frankenphp.EmbeddedAppPath, phpsrv.Root)
fsrv.Root = phpsrv.Root
}
}
// set up a route list that we'll append to
routes := caddyhttp.RouteList{}
// set the list of allowed path segments on which to split
phpsrv.SplitPath = extensions
// if the index is turned off, we skip the redirect and try_files
if indexFile != "off" {
// route to redirect to canonical path if index PHP file
redirMatcherSet := caddy.ModuleMap{
"file": h.JSON(fileserver.MatchFile{
TryFiles: []string{"{http.request.uri.path}/" + indexFile},
}),
"not": h.JSON(caddyhttp.MatchNot{
MatcherSetsRaw: []caddy.ModuleMap{
{
"path": h.JSON(caddyhttp.MatchPath{"*/"}),
},
},
}),
}
redirHandler := caddyhttp.StaticResponse{
StatusCode: caddyhttp.WeakString(strconv.Itoa(http.StatusPermanentRedirect)),
Headers: http.Header{"Location": []string{"{http.request.orig_uri.path}/"}},
}
redirRoute := caddyhttp.Route{
MatcherSetsRaw: []caddy.ModuleMap{redirMatcherSet},
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(redirHandler, "handler", "static_response", nil)},
}
// if tryFiles wasn't overridden, use a reasonable default
if len(tryFiles) == 0 {
tryFiles = []string{"{http.request.uri.path}", "{http.request.uri.path}/" + indexFile, indexFile}
}
// route to rewrite to PHP index file
rewriteMatcherSet := caddy.ModuleMap{
"file": h.JSON(fileserver.MatchFile{
TryFiles: tryFiles,
SplitPath: extensions,
}),
}
rewriteHandler := rewrite.Rewrite{
URI: "{http.matchers.file.relative}",
}
rewriteRoute := caddyhttp.Route{
MatcherSetsRaw: []caddy.ModuleMap{rewriteMatcherSet},
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(rewriteHandler, "handler", "rewrite", nil)},
}
routes = append(routes, redirRoute, rewriteRoute)
}
// route to actually pass requests to PHP files;
// match only requests that are for PHP files
pathList := []string{}
for _, ext := range extensions {
pathList = append(pathList, "*"+ext)
}
phpMatcherSet := caddy.ModuleMap{
"path": h.JSON(pathList),
}
// the rest of the config is specified by the user
// using the php directive syntax
dispenser.Next() // consume the directive name
err = phpsrv.UnmarshalCaddyfile(dispenser)
if err != nil {
return nil, err
}
// create the PHP route which is
// conditional on matching PHP files
phpRoute := caddyhttp.Route{
MatcherSetsRaw: []caddy.ModuleMap{phpMatcherSet},
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(phpsrv, "handler", "php", nil)},
}
routes = append(routes, phpRoute)
// create the file server route
if !disableFsrv {
fileRoute := caddyhttp.Route{
MatcherSetsRaw: []caddy.ModuleMap{},
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(fsrv, "handler", "file_server", nil)},
}
routes = append(routes, fileRoute)
}
subroute := caddyhttp.Subroute{
Routes: routes,
}
// the user's matcher is a prerequisite for ours, so
// wrap ours in a subroute and return that
if userMatcherSet != nil {
return []httpcaddyfile.ConfigValue{
{
Class: "route",
Value: caddyhttp.Route{
MatcherSetsRaw: []caddy.ModuleMap{userMatcherSet},
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(subroute, "handler", "subroute", nil)},
},
},
}, nil
}
// otherwise, return the literal subroute instead of
// individual routes, to ensure they stay together and
// are treated as a single unit, without necessarily
// creating an actual subroute in the output
return []httpcaddyfile.ConfigValue{
{
Class: "route",
Value: subroute,
},
}, nil
}
// Interface guards
var (
_ caddy.App = (*FrankenPHPApp)(nil)

View File

@@ -139,3 +139,53 @@ func TestEnv(t *testing.T) {
tester.AssertGetResponse("http://localhost:9080/env.php", http.StatusOK, "bazbar")
}
func TestPHPServerDirective(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
frankenphp
order php_server before reverse_proxy
}
localhost:9080 {
root * ../testdata
php_server
}
`, "caddyfile")
tester.AssertGetResponse("http://localhost:9080", http.StatusOK, "I am by birth a Genevese (i not set)")
tester.AssertGetResponse("http://localhost:9080/hello.txt", http.StatusOK, "Hello")
tester.AssertGetResponse("http://localhost:9080/not-found.txt", http.StatusOK, "I am by birth a Genevese (i not set)")
}
func TestPHPServerDirectiveDisableFileServer(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
frankenphp
order php_server before respond
}
localhost:9080 {
root * ../testdata
php_server {
file_server off
}
respond "Not found" 404
}
`, "caddyfile")
tester.AssertGetResponse("http://localhost:9080", http.StatusOK, "I am by birth a Genevese (i not set)")
tester.AssertGetResponse("http://localhost:9080/hello.txt", http.StatusNotFound, "Not found")
}

View File

@@ -5,28 +5,32 @@
#worker /path/to/your/worker.php
{$FRANKENPHP_CONFIG}
}
# https://caddyserver.com/docs/caddyfile/directives#sorting-algorithm
order mercure after encode
order vulcain after reverse_proxy
order php_server before file_server
order php before file_server
}
{$SERVER_NAME:localhost}
{$CADDY_EXTRA_CONFIG}
log {
# Redact the authorization query parameter that can be set by Mercure
format filter {
wrap console
fields {
uri query {
replace authorization REDACTED
}
}
}
}
route {
# Healthcheck URL
skip_log /healthz
respond /healthz 200
{$SERVER_NAME:localhost} {
#log {
# # Redact the authorization query parameter that can be set by Mercure
# format filter {
# wrap console
# fields {
# uri query {
# replace authorization REDACTED
# }
# }
# }
#}
root * public/
encode zstd br gzip
# Uncomment the following lines to enable Mercure and Vulcain modules
#mercure {
# # Transport to use (default to Bolt)
@@ -44,25 +48,7 @@ route {
#}
#vulcain
# Add trailing slash for directory requests
@canonicalPath {
file {path}/index.php
not path */
}
redir @canonicalPath {path}/ 308
{$CADDY_SERVER_EXTRA_DIRECTIVES}
# If the requested file does not exist, try index files
@indexFiles file {
try_files {path} {path}/index.php index.php
split_path .php
}
rewrite @indexFiles {http.matchers.file.relative}
# FrankenPHP!
@phpFiles path *.php
php @phpFiles
encode zstd gzip
file_server
respond 404
php_server
}

View File

@@ -1,18 +1,25 @@
// Copied from https://github.com/caddyserver/xcaddy/blob/b7fd102f41e12be4735dc77b0391823989812ce8/environment.go#L251
package main
import (
"github.com/caddyserver/caddy/v2"
caddycmd "github.com/caddyserver/caddy/v2/cmd"
_ "go.uber.org/automaxprocs"
"go.uber.org/automaxprocs/maxprocs"
"go.uber.org/zap"
// plug in Caddy modules here.
_ "github.com/caddyserver/caddy/v2/modules/standard"
_ "github.com/dunglas/caddy-cbrotli"
_ "github.com/dunglas/frankenphp/caddy"
_ "github.com/dunglas/mercure/caddy"
_ "github.com/dunglas/vulcain/caddy"
)
func main() {
undo, err := maxprocs.Set()
defer undo()
if err != nil {
caddy.Log().Warn("failed to set GOMAXPROCS", zap.Error(err))
}
caddycmd.Main()
}

View File

@@ -1,27 +1,32 @@
module github.com/dunglas/frankenphp/caddy
go 1.20
go 1.21
replace github.com/dunglas/frankenphp => ../
replace (
// some packages must match versions defined in go.mod of caddyserver/caddy/v2
github.com/caddyserver/certmagic => github.com/caddyserver/certmagic v0.19.2
github.com/google/cel-go => github.com/google/cel-go v0.15.1
github.com/quic-go/quic-go => github.com/quic-go/quic-go v0.37.6
)
retract v1.0.0-rc.1 // Human error
require (
github.com/caddyserver/caddy/v2 v2.7.4
github.com/dunglas/frankenphp v1.0.0-beta.1
github.com/dunglas/mercure/caddy v0.15.2
github.com/dunglas/vulcain/caddy v0.5.0
github.com/caddyserver/caddy/v2 v2.7.6
github.com/caddyserver/certmagic v0.20.0
github.com/dunglas/caddy-cbrotli v1.0.0
github.com/dunglas/frankenphp v1.0.3
github.com/dunglas/mercure/caddy v0.15.9
github.com/dunglas/vulcain/caddy v1.0.1
github.com/spf13/cobra v1.8.0
go.uber.org/automaxprocs v1.5.3
go.uber.org/zap v1.26.0
)
require (
filippo.io/edwards25519 v1.0.0 // indirect
github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df // indirect
github.com/micromdm/scep/v2 v2.1.0 // indirect
github.com/smallstep/go-attestation v0.4.4-0.20240109183208-413678f90935 // indirect
go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 // indirect
)
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect
github.com/BurntSushi/toml v1.3.2 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
@@ -29,57 +34,55 @@ require (
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
github.com/MauriceGit/skiplist v0.0.0-20211105230623-77f5c8d3e145 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/RoaringBitmap/roaring v1.3.0 // indirect
github.com/alecthomas/chroma/v2 v2.7.0 // indirect
github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df // indirect
github.com/RoaringBitmap/roaring v1.8.0 // indirect
github.com/alecthomas/chroma/v2 v2.12.0 // indirect
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bits-and-blooms/bitset v1.8.0 // indirect
github.com/caddyserver/certmagic v0.19.2 // indirect
github.com/bits-and-blooms/bitset v1.13.0 // indirect
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
github.com/cespare/xxhash v1.1.0 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/chzyer/readline v1.5.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect
github.com/dgraph-io/badger v1.6.2 // indirect
github.com/dgraph-io/badger/v2 v2.2007.4 // indirect
github.com/dgraph-io/ristretto v0.1.1 // indirect
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
github.com/dlclark/regexp2 v1.7.0 // indirect
github.com/dunglas/httpsfv v1.0.1 // indirect
github.com/dunglas/mercure v0.15.2 // indirect
github.com/dunglas/vulcain v0.5.0 // indirect
github.com/dlclark/regexp2 v1.10.0 // indirect
github.com/dunglas/httpsfv v1.0.2 // indirect
github.com/dunglas/mercure v0.15.9 // indirect
github.com/dunglas/vulcain v1.0.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/felixge/httpsnoop v1.0.3 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/fxamacker/cbor/v2 v2.4.0 // indirect
github.com/getkin/kin-openapi v0.118.0 // indirect
github.com/go-chi/chi v4.1.2+incompatible // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/fxamacker/cbor/v2 v2.5.0 // indirect
github.com/getkin/kin-openapi v0.122.0 // indirect
github.com/go-chi/chi/v5 v5.0.10 // indirect
github.com/go-kit/kit v0.13.0 // indirect
github.com/go-kit/log v0.2.1 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/go-logr/logr v1.2.4 // indirect
github.com/go-logr/logr v1.3.0 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.20.0 // indirect
github.com/go-openapi/swag v0.22.4 // indirect
github.com/go-openapi/jsonpointer v0.20.2 // indirect
github.com/go-openapi/swag v0.22.5 // indirect
github.com/go-sql-driver/mysql v1.7.1 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/gofrs/uuid v4.4.0+incompatible // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/golang/glog v1.1.2 // indirect
github.com/golang/mock v1.6.0 // indirect
github.com/golang/glog v1.2.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/cel-go v0.18.0 // indirect
github.com/google/certificate-transparency-go v1.1.4 // indirect
github.com/google/go-tpm v0.3.3 // indirect
github.com/google/brotli/go/cbrotli v0.0.0-20240116120200-adbc354d23af // indirect
github.com/google/cel-go v0.15.1 // indirect
github.com/google/certificate-transparency-go v1.1.7 // indirect
github.com/google/go-tpm v0.9.0 // indirect
github.com/google/go-tspi v0.3.0 // indirect
github.com/google/pprof v0.0.0-20230912144702-c363fe2c2ed8 // indirect
github.com/google/uuid v1.3.1 // indirect
github.com/gorilla/handlers v1.5.1 // indirect
github.com/gorilla/mux v1.8.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 // indirect
github.com/hashicorp/golang-lru v0.5.4 // indirect
github.com/google/pprof v0.0.0-20240125082051-42cd04596328 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/handlers v1.5.2 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.1 // indirect
github.com/hashicorp/golang-lru v1.0.2 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/huandu/xstrings v1.4.0 // indirect
github.com/imdario/mergo v0.3.16 // indirect
@@ -90,109 +93,104 @@ require (
github.com/jackc/pgio v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgproto3/v2 v2.3.2 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgtype v1.14.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect
github.com/jackc/pgtype v1.14.1 // indirect
github.com/jackc/pgx/v4 v4.18.1 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/kevburnsjr/skipfilter v0.0.1 // indirect
github.com/klauspost/compress v1.16.7 // indirect
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
github.com/klauspost/compress v1.17.5 // indirect
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
github.com/libdns/libdns v0.2.1 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/manifoldco/promptui v0.9.0 // indirect
github.com/mastercactapus/proxyprotocol v0.0.4 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/mholt/acmez v1.2.0 // indirect
github.com/micromdm/scep/v2 v2.1.0 // indirect
github.com/miekg/dns v1.1.56 // indirect
github.com/miekg/dns v1.1.58 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-ps v1.0.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/mschoch/smat v0.2.0 // indirect
github.com/onsi/ginkgo/v2 v2.12.0 // indirect
github.com/pelletier/go-toml/v2 v2.0.9 // indirect
github.com/onsi/ginkgo/v2 v2.15.0 // indirect
github.com/pelletier/go-toml/v2 v2.1.1 // indirect
github.com/perimeterx/marshmallow v1.1.5 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/client_golang v1.16.0 // indirect
github.com/prometheus/client_model v0.4.0 // indirect
github.com/prometheus/common v0.44.0 // indirect
github.com/prometheus/procfs v0.11.1 // indirect
github.com/prometheus/client_golang v1.18.0 // indirect
github.com/prometheus/client_model v0.5.0 // indirect
github.com/prometheus/common v0.46.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
github.com/quic-go/qpack v0.4.0 // indirect
github.com/quic-go/qtls-go1-20 v0.3.4 // indirect
github.com/quic-go/quic-go v0.38.1 // indirect
github.com/quic-go/quic-go v0.41.0 // indirect
github.com/rs/xid v1.5.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/shopspring/decimal v1.3.1 // indirect
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/slackhq/nebula v1.7.2 // indirect
github.com/smallstep/certificates v0.24.3-rc1 // indirect
github.com/smallstep/go-attestation v0.4.4-0.20230509120429-e17291421738 // indirect
github.com/slackhq/nebula v1.8.2 // indirect
github.com/smallstep/certificates v0.25.0 // indirect
github.com/smallstep/nosql v0.6.0 // indirect
github.com/smallstep/truststore v0.12.1 // indirect
github.com/spf13/afero v1.9.5 // indirect
github.com/spf13/cast v1.5.1 // indirect
github.com/spf13/cobra v1.7.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.16.0 // indirect
github.com/spf13/viper v1.18.2 // indirect
github.com/stoewer/go-strcase v1.3.0 // indirect
github.com/subosito/gotenv v1.4.2 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/tailscale/tscert v0.0.0-20230806124524-28a91b69a046 // indirect
github.com/tidwall/gjson v1.15.0 // indirect
github.com/tidwall/gjson v1.17.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
github.com/unrolled/secure v1.13.0 // indirect
github.com/unrolled/secure v1.14.0 // indirect
github.com/urfave/cli v1.22.14 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
github.com/yuin/goldmark v1.5.5 // indirect
github.com/yuin/goldmark v1.6.0 // indirect
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc // indirect
github.com/zeebo/blake3 v0.2.3 // indirect
go.etcd.io/bbolt v1.3.7 // indirect
go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.42.0 // indirect
go.opentelemetry.io/contrib/propagators/autoprop v0.42.0 // indirect
go.opentelemetry.io/contrib/propagators/aws v1.17.0 // indirect
go.opentelemetry.io/contrib/propagators/b3 v1.17.0 // indirect
go.opentelemetry.io/contrib/propagators/jaeger v1.17.0 // indirect
go.opentelemetry.io/contrib/propagators/ot v1.17.0 // indirect
go.opentelemetry.io/otel v1.16.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.16.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.16.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.16.0 // indirect
go.opentelemetry.io/otel/metric v1.16.0 // indirect
go.opentelemetry.io/otel/sdk v1.16.0 // indirect
go.opentelemetry.io/otel/trace v1.16.0 // indirect
go.opentelemetry.io/proto/otlp v0.19.0 // indirect
go.etcd.io/bbolt v1.3.8 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // indirect
go.opentelemetry.io/contrib/propagators/autoprop v0.45.0 // indirect
go.opentelemetry.io/contrib/propagators/aws v1.20.0 // indirect
go.opentelemetry.io/contrib/propagators/b3 v1.20.0 // indirect
go.opentelemetry.io/contrib/propagators/jaeger v1.20.0 // indirect
go.opentelemetry.io/contrib/propagators/ot v1.20.0 // indirect
go.opentelemetry.io/otel v1.21.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 // indirect
go.opentelemetry.io/otel/metric v1.21.0 // indirect
go.opentelemetry.io/otel/sdk v1.21.0 // indirect
go.opentelemetry.io/otel/trace v1.21.0 // indirect
go.opentelemetry.io/proto/otlp v1.0.0 // indirect
go.step.sm/cli-utils v0.8.0 // indirect
go.step.sm/crypto v0.35.0 // indirect
go.step.sm/linkedca v0.20.0 // indirect
go.step.sm/crypto v0.36.0 // indirect
go.step.sm/linkedca v0.20.1 // indirect
go.uber.org/mock v0.4.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.13.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/mod v0.12.0 // indirect
golang.org/x/net v0.15.0 // indirect
golang.org/x/sync v0.3.0 // indirect
golang.org/x/sys v0.12.0 // indirect
golang.org/x/term v0.12.0 // indirect
golang.org/x/text v0.13.0 // indirect
golang.org/x/tools v0.13.0 // indirect
google.golang.org/genproto v0.0.0-20230803162519-f966b187b2e5 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20230911183012-2d3300fd4832 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230911183012-2d3300fd4832 // indirect
google.golang.org/grpc v1.58.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
golang.org/x/crypto v0.18.0 // indirect
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a // indirect
golang.org/x/mod v0.14.0 // indirect
golang.org/x/net v0.20.0 // indirect
golang.org/x/sync v0.6.0 // indirect
golang.org/x/sys v0.16.0 // indirect
golang.org/x/term v0.16.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/tools v0.17.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240125205218-1f4bbc51befe // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240125205218-1f4bbc51befe // indirect
google.golang.org/grpc v1.61.0 // indirect
google.golang.org/protobuf v1.32.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
howett.net/plist v1.0.0 // indirect
howett.net/plist v1.0.1 // indirect
)

File diff suppressed because it is too large Load Diff

44
caddy/php-cli.go Normal file
View File

@@ -0,0 +1,44 @@
package caddy
import (
"errors"
"os"
"path/filepath"
caddycmd "github.com/caddyserver/caddy/v2/cmd"
"github.com/dunglas/frankenphp"
"github.com/spf13/cobra"
)
func init() {
caddycmd.RegisterCommand(caddycmd.Command{
Name: "php-cli",
Usage: "script.php [args ...]",
Short: "Runs a PHP command",
Long: `
Executes a PHP script similarly to the CLI SAPI.`,
CobraFunc: func(cmd *cobra.Command) {
cmd.DisableFlagParsing = true
cmd.RunE = caddycmd.WrapCommandFuncForCobra(cmdPHPCLI)
},
})
}
func cmdPHPCLI(fs caddycmd.Flags) (int, error) {
args := os.Args[2:]
if len(args) < 1 {
return 1, errors.New("the path to the PHP script is required")
}
if frankenphp.EmbeddedAppPath != "" {
if _, err := os.Stat(args[0]); err != nil {
args[0] = filepath.Join(frankenphp.EmbeddedAppPath, args[0])
}
}
status := frankenphp.ExecuteScriptCLI(args[0], args)
os.Exit(status)
return status, nil
}

328
caddy/php-server.go Normal file
View File

@@ -0,0 +1,328 @@
package caddy
import (
"encoding/json"
"log"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"time"
mercureModule "github.com/dunglas/mercure/caddy"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
caddycmd "github.com/caddyserver/caddy/v2/cmd"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/caddy/v2/modules/caddyhttp/encode"
"github.com/caddyserver/caddy/v2/modules/caddyhttp/fileserver"
"github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite"
"github.com/caddyserver/certmagic"
"github.com/dunglas/frankenphp"
"go.uber.org/zap"
"github.com/spf13/cobra"
)
func init() {
caddycmd.RegisterCommand(caddycmd.Command{
Name: "php-server",
Usage: "[--domain <example.com>] [--root <path>] [--listen <addr>] [--worker /path/to/worker.php<,nb-workers>] [--access-log] [--debug] [--no-compress] [--mercure]",
Short: "Spins up a production-ready PHP server",
Long: `
A simple but production-ready PHP server. Useful for quick deployments,
demos, and development.
The listener's socket address can be customized with the --listen flag.
If a domain name is specified with --domain, the default listener address
will be changed to the HTTPS port and the server will use HTTPS. If using
a public domain, ensure A/AAAA records are properly configured before
using this option.
For more advanced use cases, see https://github.com/dunglas/frankenphp/blob/main/docs/config.md`,
CobraFunc: func(cmd *cobra.Command) {
cmd.Flags().StringP("domain", "d", "", "Domain name at which to serve the files")
cmd.Flags().StringP("root", "r", "", "The path to the root of the site")
cmd.Flags().StringP("listen", "l", "", "The address to which to bind the listener")
cmd.Flags().StringArrayP("worker", "w", []string{}, "Worker script")
cmd.Flags().BoolP("access-log", "a", false, "Enable the access log")
cmd.Flags().BoolP("debug", "v", false, "Enable verbose debug logs")
cmd.Flags().BoolP("mercure", "m", false, "Enable the built-in Mercure.rocks hub")
cmd.Flags().BoolP("no-compress", "", false, "Disable Zstandard, Brotli and Gzip compression")
cmd.RunE = caddycmd.WrapCommandFuncForCobra(cmdPHPServer)
},
})
}
// cmdPHPServer is freely inspired from the file-server command of the Caddy server (Apache License 2.0, Matthew Holt and The Caddy Authors)
func cmdPHPServer(fs caddycmd.Flags) (int, error) {
caddy.TrapSignals()
domain := fs.String("domain")
root := fs.String("root")
listen := fs.String("listen")
accessLog := fs.Bool("access-log")
debug := fs.Bool("debug")
compress := !fs.Bool("no-compress")
mercure := fs.Bool("mercure")
workers, err := fs.GetStringArray("worker")
if err != nil {
panic(err)
}
var workersOption []workerConfig
if len(workers) != 0 {
workersOption = make([]workerConfig, 0, len(workers))
for _, worker := range workers {
parts := strings.SplitN(worker, ",", 2)
if frankenphp.EmbeddedAppPath != "" && filepath.IsLocal(parts[0]) {
parts[0] = filepath.Join(frankenphp.EmbeddedAppPath, parts[0])
}
var num int
if len(parts) > 1 {
num, _ = strconv.Atoi(parts[1])
}
workersOption = append(workersOption, workerConfig{FileName: parts[0], Num: num})
}
}
if frankenphp.EmbeddedAppPath != "" {
if _, err := os.Stat(filepath.Join(frankenphp.EmbeddedAppPath, "php.ini")); err == nil {
iniScanDir := os.Getenv("PHP_INI_SCAN_DIR")
if err := os.Setenv("PHP_INI_SCAN_DIR", iniScanDir+":"+frankenphp.EmbeddedAppPath); err != nil {
return caddy.ExitCodeFailedStartup, err
}
}
if _, err := os.Stat(filepath.Join(frankenphp.EmbeddedAppPath, "Caddyfile")); err == nil {
config, _, err := caddycmd.LoadConfig(filepath.Join(frankenphp.EmbeddedAppPath, "Caddyfile"), "")
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
if err = caddy.Load(config, true); err != nil {
return caddy.ExitCodeFailedStartup, err
}
select {}
}
if root == "" {
root = filepath.Join(frankenphp.EmbeddedAppPath, defaultDocumentRoot)
} else if filepath.IsLocal(root) {
root = filepath.Join(frankenphp.EmbeddedAppPath, root)
}
}
const indexFile = "index.php"
extensions := []string{"php"}
tryFiles := []string{"{http.request.uri.path}", "{http.request.uri.path}/" + indexFile, indexFile}
phpHandler := FrankenPHPModule{
Root: root,
SplitPath: extensions,
}
// route to redirect to canonical path if index PHP file
redirMatcherSet := caddy.ModuleMap{
"file": caddyconfig.JSON(fileserver.MatchFile{
Root: root,
TryFiles: []string{"{http.request.uri.path}/" + indexFile},
}, nil),
"not": caddyconfig.JSON(caddyhttp.MatchNot{
MatcherSetsRaw: []caddy.ModuleMap{
{
"path": caddyconfig.JSON(caddyhttp.MatchPath{"*/"}, nil),
},
},
}, nil),
}
redirHandler := caddyhttp.StaticResponse{
StatusCode: caddyhttp.WeakString(strconv.Itoa(http.StatusPermanentRedirect)),
Headers: http.Header{"Location": []string{"{http.request.orig_uri.path}/"}},
}
redirRoute := caddyhttp.Route{
MatcherSetsRaw: []caddy.ModuleMap{redirMatcherSet},
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(redirHandler, "handler", "static_response", nil)},
}
// route to rewrite to PHP index file
rewriteMatcherSet := caddy.ModuleMap{
"file": caddyconfig.JSON(fileserver.MatchFile{
Root: root,
TryFiles: tryFiles,
SplitPath: extensions,
}, nil),
}
rewriteHandler := rewrite.Rewrite{
URI: "{http.matchers.file.relative}",
}
rewriteRoute := caddyhttp.Route{
MatcherSetsRaw: []caddy.ModuleMap{rewriteMatcherSet},
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(rewriteHandler, "handler", "rewrite", nil)},
}
// route to actually pass requests to PHP files;
// match only requests that are for PHP files
pathList := []string{}
for _, ext := range extensions {
pathList = append(pathList, "*"+ext)
}
phpMatcherSet := caddy.ModuleMap{
"path": caddyconfig.JSON(pathList, nil),
}
// create the PHP route which is
// conditional on matching PHP files
phpRoute := caddyhttp.Route{
MatcherSetsRaw: []caddy.ModuleMap{phpMatcherSet},
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(phpHandler, "handler", "php", nil)},
}
fileRoute := caddyhttp.Route{
MatcherSetsRaw: []caddy.ModuleMap{},
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(fileserver.FileServer{Root: root}, "handler", "file_server", nil)},
}
subroute := caddyhttp.Subroute{
Routes: caddyhttp.RouteList{redirRoute, rewriteRoute, phpRoute, fileRoute},
}
if compress {
gzip, err := caddy.GetModule("http.encoders.gzip")
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
br, err := caddy.GetModule("http.encoders.br")
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
zstd, err := caddy.GetModule("http.encoders.zstd")
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
encodeRoute := caddyhttp.Route{
MatcherSetsRaw: []caddy.ModuleMap{},
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(encode.Encode{
EncodingsRaw: caddy.ModuleMap{
"zstd": caddyconfig.JSON(zstd.New(), nil),
"br": caddyconfig.JSON(br.New(), nil),
"gzip": caddyconfig.JSON(gzip.New(), nil),
},
Prefer: []string{"zstd", "br", "gzip"},
}, "handler", "encode", nil)},
}
subroute.Routes = append(caddyhttp.RouteList{encodeRoute}, subroute.Routes...)
}
if mercure {
mercurePublisherJwtKey := os.Getenv("MERCURE_PUBLISHER_JWT_KEY")
if mercurePublisherJwtKey == "" {
panic(`The "MERCURE_PUBLISHER_JWT_KEY" environment variable must be set to use the Mercure.rocks hub`)
}
mercureSubscriberJwtKey := os.Getenv("MERCURE_SUBSCRIBER_JWT_KEY")
if mercureSubscriberJwtKey == "" {
panic(`The "MERCURE_SUBSCRIBER_JWT_KEY" environment variable must be set to use the Mercure.rocks hub`)
}
mercureRoute := caddyhttp.Route{
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(
mercureModule.Mercure{
PublisherJWT: mercureModule.JWTConfig{
Alg: os.Getenv("MERCURE_PUBLISHER_JWT_ALG"),
Key: mercurePublisherJwtKey,
},
SubscriberJWT: mercureModule.JWTConfig{
Alg: os.Getenv("MERCURE_SUBSCRIBER_JWT_ALG"),
Key: mercureSubscriberJwtKey,
},
},
"handler",
"mercure",
nil,
),
},
}
subroute.Routes = append(caddyhttp.RouteList{mercureRoute}, subroute.Routes...)
}
route := caddyhttp.Route{
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(subroute, "handler", "subroute", nil)},
}
if domain != "" {
route.MatcherSetsRaw = []caddy.ModuleMap{
{
"host": caddyconfig.JSON(caddyhttp.MatchHost{domain}, nil),
},
}
}
server := &caddyhttp.Server{
ReadHeaderTimeout: caddy.Duration(10 * time.Second),
IdleTimeout: caddy.Duration(30 * time.Second),
MaxHeaderBytes: 1024 * 10,
Routes: caddyhttp.RouteList{route},
}
if listen == "" {
if domain == "" {
listen = ":80"
} else {
listen = ":" + strconv.Itoa(certmagic.HTTPSPort)
}
}
server.Listen = []string{listen}
if accessLog {
server.Logs = &caddyhttp.ServerLogConfig{}
}
httpApp := caddyhttp.App{
Servers: map[string]*caddyhttp.Server{"php": server},
}
var false bool
cfg := &caddy.Config{
Admin: &caddy.AdminConfig{
Disabled: true,
Config: &caddy.ConfigSettings{
Persist: &false,
},
},
AppsRaw: caddy.ModuleMap{
"http": caddyconfig.JSON(httpApp, nil),
"frankenphp": caddyconfig.JSON(FrankenPHPApp{Workers: workersOption}, nil),
},
}
if debug {
cfg.Logging = &caddy.Logging{
Logs: map[string]*caddy.CustomLog{
"default": {
BaseLog: caddy.BaseLog{Level: zap.DebugLevel.CapitalString()},
},
},
}
}
err = caddy.Run(cfg)
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
log.Printf("Caddy serving PHP app on %s", listen)
select {}
}

291
cgi.go
View File

@@ -1,5 +1,6 @@
package frankenphp
import "C"
import (
"crypto/tls"
"net"
@@ -8,217 +9,155 @@ import (
"strings"
)
// populateEnv returns a set of CGI environment variables for the request.
type serverKey int
const (
contentLength serverKey = iota
documentRoot
documentUri
gatewayInterface
httpHost
https
pathInfo
phpSelf
remoteAddr
remoteHost
remotePort
requestScheme
scriptFilename
scriptName
serverName
serverPort
serverProtocol
serverSoftware
sslProtocol
)
func allocServerVariable(cArr *[27]*C.char, env map[string]string, serverKey serverKey, envKey string, val string) {
if val, ok := env[envKey]; ok {
cArr[serverKey] = C.CString(val)
delete(env, envKey)
return
}
cArr[serverKey] = C.CString(val)
}
// computeKnownVariables returns a set of CGI environment variables for the request.
//
// TODO: handle this case https://github.com/caddyserver/caddy/issues/3718
// Inspired by https://github.com/caddyserver/caddy/blob/master/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go
func populateEnv(request *http.Request) error {
fc, ok := FromContext(request.Context())
if !ok {
func computeKnownVariables(request *http.Request) (cArr [27]*C.char) {
fc, fcOK := FromContext(request.Context())
if !fcOK {
panic("not a FrankenPHP request")
}
if fc.populated {
return nil
// Separate remote IP and port; more lenient than net.SplitHostPort
var ip, port string
if idx := strings.LastIndex(request.RemoteAddr, ":"); idx > -1 {
ip = request.RemoteAddr[:idx]
port = request.RemoteAddr[idx+1:]
} else {
ip = request.RemoteAddr
}
_, addrOk := fc.Env["REMOTE_ADDR"]
_, portOk := fc.Env["REMOTE_PORT"]
if !addrOk || !portOk {
// Separate remote IP and port; more lenient than net.SplitHostPort
var ip, port string
if idx := strings.LastIndex(request.RemoteAddr, ":"); idx > -1 {
ip = request.RemoteAddr[:idx]
port = request.RemoteAddr[idx+1:]
// Remove [] from IPv6 addresses
ip = strings.Replace(ip, "[", "", 1)
ip = strings.Replace(ip, "]", "", 1)
ra, raOK := fc.env["REMOTE_ADDR"]
if raOK {
cArr[remoteAddr] = C.CString(ra)
delete(fc.env, "REMOTE_ADDR")
} else {
cArr[remoteAddr] = C.CString(ip)
}
if rh, ok := fc.env["REMOTE_HOST"]; ok {
cArr[remoteHost] = C.CString(rh) // For speed, remote host lookups disabled
delete(fc.env, "REMOTE_HOST")
} else {
if raOK {
cArr[remoteHost] = C.CString(ip)
} else {
ip = request.RemoteAddr
}
// Remove [] from IPv6 addresses
ip = strings.Replace(ip, "[", "", 1)
ip = strings.Replace(ip, "]", "", 1)
if _, ok := fc.Env["REMOTE_ADDR"]; !ok {
fc.Env["REMOTE_ADDR"] = ip
}
if _, ok := fc.Env["REMOTE_HOST"]; !ok {
fc.Env["REMOTE_HOST"] = ip // For speed, remote host lookups disabled
}
if _, ok := fc.Env["REMOTE_PORT"]; !ok {
fc.Env["REMOTE_PORT"] = port
cArr[remoteHost] = cArr[remoteAddr]
}
}
if _, ok := fc.Env["DOCUMENT_ROOT"]; !ok {
// make sure file root is absolute
root, err := filepath.Abs(fc.DocumentRoot)
if err != nil {
return err
}
allocServerVariable(&cArr, fc.env, remotePort, "REMOTE_PORT", port)
allocServerVariable(&cArr, fc.env, documentRoot, "DOCUMENT_ROOT", fc.documentRoot)
allocServerVariable(&cArr, fc.env, pathInfo, "PATH_INFO", fc.pathInfo)
allocServerVariable(&cArr, fc.env, phpSelf, "PHP_SELF", request.URL.Path)
allocServerVariable(&cArr, fc.env, documentUri, "DOCUMENT_URI", fc.docURI)
allocServerVariable(&cArr, fc.env, scriptFilename, "SCRIPT_FILENAME", fc.scriptFilename)
allocServerVariable(&cArr, fc.env, scriptName, "SCRIPT_NAME", fc.scriptName)
if fc.ResolveRootSymlink {
if root, err = filepath.EvalSymlinks(root); err != nil {
return err
}
}
var rs string
if request.TLS == nil {
rs = "http"
} else {
rs = "https"
fc.Env["DOCUMENT_ROOT"] = root
}
fpath := request.URL.Path
scriptName := fpath
docURI := fpath
// split "actual path" from "path info" if configured
if splitPos := splitPos(fc, fpath); splitPos > -1 {
docURI = fpath[:splitPos]
fc.Env["PATH_INFO"] = fpath[splitPos:]
// Strip PATH_INFO from SCRIPT_NAME
scriptName = strings.TrimSuffix(scriptName, fc.Env["PATH_INFO"])
}
// SCRIPT_FILENAME is the absolute path of SCRIPT_NAME
scriptFilename := sanitizedPathJoin(fc.Env["DOCUMENT_ROOT"], scriptName)
// Ensure the SCRIPT_NAME has a leading slash for compliance with RFC3875
// Info: https://tools.ietf.org/html/rfc3875#section-4.1.13
if scriptName != "" && !strings.HasPrefix(scriptName, "/") {
scriptName = "/" + scriptName
}
if _, ok := fc.Env["PHP_SELF"]; !ok {
fc.Env["PHP_SELF"] = fpath
}
if _, ok := fc.Env["DOCUMENT_URI"]; !ok {
fc.Env["DOCUMENT_URI"] = docURI
}
if _, ok := fc.Env["SCRIPT_FILENAME"]; !ok {
fc.Env["SCRIPT_FILENAME"] = scriptFilename
}
if _, ok := fc.Env["SCRIPT_NAME"]; !ok {
fc.Env["SCRIPT_NAME"] = scriptName
}
if _, ok := fc.Env["REQUEST_SCHEME"]; !ok {
if request.TLS == nil {
fc.Env["REQUEST_SCHEME"] = "http"
if h, ok := fc.env["HTTPS"]; ok {
cArr[https] = C.CString(h)
delete(fc.env, "HTTPS")
} else {
fc.Env["REQUEST_SCHEME"] = "https"
}
}
if request.TLS != nil {
if _, ok := fc.Env["HTTPS"]; !ok {
fc.Env["HTTPS"] = "on"
cArr[https] = C.CString("on")
}
// and pass the protocol details in a manner compatible with apache's mod_ssl
// (which is why these have a SSL_ prefix and not TLS_).
_, sslProtocolOk := fc.Env["SSL_PROTOCOL"]
v, versionOk := tlsProtocolStrings[request.TLS.Version]
if !sslProtocolOk && versionOk {
fc.Env["SSL_PROTOCOL"] = v
if p, ok := fc.env["SSL_PROTOCOL"]; ok {
cArr[sslProtocol] = C.CString(p)
delete(fc.env, "SSL_PROTOCOL")
} else {
if v, ok := tlsProtocolStrings[request.TLS.Version]; ok {
cArr[sslProtocol] = C.CString(v)
}
}
}
allocServerVariable(&cArr, fc.env, requestScheme, "REQUEST_SCHEME", rs)
if fc.Env["SERVER_NAME"] == "" || fc.Env["SERVER_PORT"] == "" {
reqHost, reqPort, _ := net.SplitHostPort(request.Host)
if fc.Env["SERVER_NAME"] == "" {
fc.Env["SERVER_NAME"] = reqHost
}
if fc.Env["SERVER_PORT"] == "" {
fc.Env["SERVER_PORT"] = reqPort
}
reqHost, reqPort, _ := net.SplitHostPort(request.Host)
if fc.Env["SERVER_NAME"] == "" {
// whatever, just assume there was no port
fc.Env["SERVER_NAME"] = request.Host
}
if reqHost == "" {
// whatever, just assume there was no port
reqHost = request.Host
}
if reqPort == "" {
// compliance with the CGI specification requires that
// the SERVER_PORT variable MUST be set to the TCP/IP port number on which this request is received from the client
// even if the port is the default port for the scheme and could otherwise be omitted from a URI.
// https://tools.ietf.org/html/rfc3875#section-4.1.15
if fc.Env["SERVER_PORT"] == "" {
if fc.Env["REQUEST_SCHEME"] == "https" {
fc.Env["SERVER_PORT"] = "443"
} else {
fc.Env["SERVER_PORT"] = "80"
}
switch rs {
case "https":
reqPort = "443"
case "http":
reqPort = "80"
}
}
allocServerVariable(&cArr, fc.env, serverName, "SERVER_NAME", reqHost)
if reqPort != "" {
allocServerVariable(&cArr, fc.env, serverPort, "SERVER_PORT", reqPort)
}
// Variables defined in CGI 1.1 spec
// Some variables are unused but cleared explicitly to prevent
// the parent environment from interfering.
// We never override an entry previously set
if _, ok := fc.Env["REMOTE_IDENT"]; !ok {
fc.Env["REMOTE_IDENT"] = "" // Not used
}
if _, ok := fc.Env["AUTH_TYPE"]; !ok {
fc.Env["AUTH_TYPE"] = "" // Not used
}
if _, ok := fc.Env["CONTENT_LENGTH"]; !ok {
fc.Env["CONTENT_LENGTH"] = request.Header.Get("Content-Length")
}
if _, ok := fc.Env["CONTENT_TYPE"]; !ok {
fc.Env["CONTENT_TYPE"] = request.Header.Get("Content-Type")
}
if _, ok := fc.Env["GATEWAY_INTERFACE"]; !ok {
fc.Env["GATEWAY_INTERFACE"] = "CGI/1.1"
}
if _, ok := fc.Env["QUERY_STRING"]; !ok {
fc.Env["QUERY_STRING"] = request.URL.RawQuery
}
if _, ok := fc.Env["QUERY_STRING"]; !ok {
fc.Env["QUERY_STRING"] = request.URL.RawQuery
}
if _, ok := fc.Env["REQUEST_METHOD"]; !ok {
fc.Env["REQUEST_METHOD"] = request.Method
}
if _, ok := fc.Env["SERVER_PROTOCOL"]; !ok {
fc.Env["SERVER_PROTOCOL"] = request.Proto
}
if _, ok := fc.Env["SERVER_SOFTWARE"]; !ok {
fc.Env["SERVER_SOFTWARE"] = "FrankenPHP"
}
if _, ok := fc.Env["HTTP_HOST"]; !ok {
fc.Env["HTTP_HOST"] = request.Host // added here, since not always part of headers
}
if _, ok := fc.Env["REQUEST_URI"]; !ok {
fc.Env["REQUEST_URI"] = request.URL.RequestURI()
}
// compliance with the CGI specification requires that
// PATH_TRANSLATED should only exist if PATH_INFO is defined.
// Info: https://www.ietf.org/rfc/rfc3875 Page 14
if fc.Env["PATH_INFO"] != "" {
fc.Env["PATH_TRANSLATED"] = sanitizedPathJoin(fc.Env["DOCUMENT_ROOT"], fc.Env["PATH_INFO"]) // Info: http://www.oreilly.com/openbook/cgi/ch02_04.html
}
// These values can not be override
cArr[contentLength] = C.CString(request.Header.Get("Content-Length"))
// Add all HTTP headers to env variables
for field, val := range request.Header {
k := "HTTP_" + headerNameReplacer.Replace(strings.ToUpper(field))
if _, ok := fc.Env[k]; !ok {
fc.Env[k] = strings.Join(val, ", ")
}
}
allocServerVariable(&cArr, fc.env, gatewayInterface, "GATEWAY_INTERFACE", "CGI/1.1")
allocServerVariable(&cArr, fc.env, serverProtocol, "SERVER_PROTOCOL", request.Proto)
allocServerVariable(&cArr, fc.env, serverSoftware, "SERVER_SOFTWARE", "FrankenPHP")
allocServerVariable(&cArr, fc.env, httpHost, "HTTP_HOST", request.Host) // added here, since not always part of headers
if _, ok := fc.Env["REMOTE_USER"]; !ok {
var (
authUser string
ok bool
)
authUser, fc.authPassword, ok = request.BasicAuth()
if ok {
fc.Env["REMOTE_USER"] = authUser
}
}
fc.populated = true
return nil
return
}
// splitPos returns the index where path should
@@ -227,12 +166,12 @@ func populateEnv(request *http.Request) error {
// Adapted from https://github.com/caddyserver/caddy/blob/master/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go
// Copyright 2015 Matthew Holt and The Caddy Authors
func splitPos(fc *FrankenPHPContext, path string) int {
if len(fc.SplitPath) == 0 {
if len(fc.splitPath) == 0 {
return 0
}
lowerPath := strings.ToLower(path)
for _, split := range fc.SplitPath {
for _, split := range fc.splitPath {
if idx := strings.Index(lowerPath, strings.ToLower(split)); idx > -1 {
return idx + len(split)
}

View File

@@ -3,62 +3,64 @@ FROM golang:1.21-alpine
ENV CFLAGS="-ggdb3"
ENV PHPIZE_DEPS \
autoconf \
dpkg-dev \
file \
g++ \
gcc \
libc-dev \
make \
pkgconfig \
re2c
autoconf \
dpkg-dev \
file \
g++ \
gcc \
libc-dev \
make \
pkgconfig \
re2c
RUN apk add --no-cache \
$PHPIZE_DEPS \
argon2-dev \
curl-dev \
oniguruma-dev \
readline-dev \
libsodium-dev \
sqlite-dev \
openssl-dev \
libxml2-dev \
zlib-dev \
bison \
nss-tools \
# Dev tools \
git \
clang \
llvm \
gdb \
valgrind \
neovim \
zsh \
libtool && \
echo 'set auto-load safe-path /' > /root/.gdbinit
$PHPIZE_DEPS \
argon2-dev \
brotli-dev \
curl-dev \
oniguruma-dev \
readline-dev \
libsodium-dev \
sqlite-dev \
openssl-dev \
libxml2-dev \
zlib-dev \
bison \
nss-tools \
# Dev tools \
git \
clang \
llvm \
gdb \
valgrind \
neovim \
zsh \
libtool && \
echo 'set auto-load safe-path /' > /root/.gdbinit
RUN git clone --branch=PHP-8.2 https://github.com/php/php-src.git && \
cd php-src && \
# --enable-embed is only necessary to generate libphp.so, we don't use this SAPI directly
./buildconf --force && \
./configure \
--enable-embed \
--enable-zts \
--disable-zend-signals \
--enable-zend-max-execution-timers \
--enable-debug && \
make -j$(nproc) && \
make install && \
ldconfig /etc/ld.so.conf.d && \
cp php.ini-development /usr/local/lib/php.ini && \
echo -e "zend_extension=opcache.so\nopcache.enable=1" >> /usr/local/lib/php.ini &&\
php --version
WORKDIR /usr/local/src/php
RUN git clone --branch=PHP-8.3 https://github.com/php/php-src.git . && \
# --enable-embed is only necessary to generate libphp.so, we don't use this SAPI directly
./buildconf --force && \
./configure \
--enable-embed \
--enable-zts \
--disable-zend-signals \
--enable-zend-max-execution-timers \
--enable-debug && \
make -j"$(nproc)" && \
make install && \
ldconfig /etc/ld.so.conf.d && \
cp php.ini-development /usr/local/lib/php.ini && \
echo "zend_extension=opcache.so" >> /usr/local/lib/php.ini && \
echo "opcache.enable=1" >> /usr/local/lib/php.ini && \
php --version
WORKDIR /go/src/app
COPY . .
RUN cd caddy/frankenphp && \
go build
WORKDIR /go/src/app/caddy/frankenphp
RUN go build
WORKDIR /go/src/app
CMD [ "zsh" ]

View File

@@ -3,66 +3,69 @@ FROM golang:1.21
ENV CFLAGS="-ggdb3"
ENV PHPIZE_DEPS \
autoconf \
dpkg-dev \
file \
g++ \
gcc \
libc-dev \
make \
pkg-config \
re2c
autoconf \
dpkg-dev \
file \
g++ \
gcc \
libc-dev \
make \
pkg-config \
re2c
# hadolint ignore=DL3009
RUN apt-get update && \
apt-get -y --no-install-recommends install \
$PHPIZE_DEPS \
libargon2-dev \
libcurl4-openssl-dev \
libonig-dev \
libreadline-dev \
libsodium-dev \
libsqlite3-dev \
libssl-dev \
libxml2-dev \
zlib1g-dev \
bison \
libnss3-tools \
# Dev tools \
git \
clang \
llvm \
gdb \
valgrind \
neovim \
zsh \
libtool-bin && \
echo 'set auto-load safe-path /' > /root/.gdbinit && \
echo '* soft core unlimited' >> /etc/security/limits.conf \
&& \
apt-get clean
apt-get -y --no-install-recommends install \
$PHPIZE_DEPS \
libargon2-dev \
libbrotli-dev \
libcurl4-openssl-dev \
libonig-dev \
libreadline-dev \
libsodium-dev \
libsqlite3-dev \
libssl-dev \
libxml2-dev \
zlib1g-dev \
bison \
libnss3-tools \
# Dev tools \
git \
clang \
llvm \
gdb \
valgrind \
neovim \
zsh \
libtool-bin && \
echo 'set auto-load safe-path /' > /root/.gdbinit && \
echo '* soft core unlimited' >> /etc/security/limits.conf \
&& \
apt-get clean
RUN git clone --branch=PHP-8.2 https://github.com/php/php-src.git && \
cd php-src && \
# --enable-embed is only necessary to generate libphp.so, we don't use this SAPI directly
./buildconf --force && \
./configure \
--enable-embed \
--enable-zts \
--disable-zend-signals \
--enable-zend-max-execution-timers \
--enable-debug && \
make -j$(nproc) && \
make install && \
ldconfig && \
cp php.ini-development /usr/local/lib/php.ini && \
echo "zend_extension=opcache.so\nopcache.enable=1" >> /usr/local/lib/php.ini &&\
php --version
WORKDIR /usr/local/src/php
RUN git clone --branch=PHP-8.3 https://github.com/php/php-src.git . && \
# --enable-embed is only necessary to generate libphp.so, we don't use this SAPI directly
./buildconf --force && \
./configure \
--enable-embed \
--enable-zts \
--disable-zend-signals \
--enable-zend-max-execution-timers \
--enable-debug && \
make -j"$(nproc)" && \
make install && \
ldconfig && \
cp php.ini-development /usr/local/lib/php.ini && \
echo "zend_extension=opcache.so" >> /usr/local/lib/php.ini && \
echo "opcache.enable=1" >> /usr/local/lib/php.ini && \
php --version
WORKDIR /go/src/app
COPY . .
RUN cd caddy/frankenphp && \
go build
WORKDIR /go/src/app/caddy/frankenphp
RUN go build
WORKDIR /go/src/app
CMD [ "zsh" ]

View File

@@ -6,6 +6,10 @@ variable "VERSION" {
default = "dev"
}
variable "PHP_VERSION" {
default = "8.2,8.3"
}
variable "GO_VERSION" {
default = "1.21"
}
@@ -20,13 +24,17 @@ variable "CACHE" {
default = ""
}
variable DEFAULT_PHP_VERSION {
default = "8.3"
}
function "tag" {
params = [version, os, php-version, tgt]
result = [
version != "" ? format("%s:%s%s-php%s-%s", IMAGE_NAME, version, tgt == "builder" ? "-builder" : "", php-version, os) : "",
php-version == "8.2" && os == "bookworm" && version != "" ? format("%s:%s%s", IMAGE_NAME, version, tgt == "builder" ? "-builder" : "") : "",
php-version == "8.2" && version != "" ? format("%s:%s%s-%s", IMAGE_NAME, version, tgt == "builder" ? "-builder" : "", os) : "",
php-version == "8.2" && version == "latest" ? format("%s:%s%s", IMAGE_NAME, os, tgt == "builder" ? "-builder" : "") : "",
php-version == DEFAULT_PHP_VERSION && os == "bookworm" && version != "" ? format("%s:%s%s", IMAGE_NAME, version, tgt == "builder" ? "-builder" : "") : "",
php-version == DEFAULT_PHP_VERSION && version != "" ? format("%s:%s%s-%s", IMAGE_NAME, version, tgt == "builder" ? "-builder" : "", os) : "",
php-version == DEFAULT_PHP_VERSION && version == "latest" ? format("%s:%s%s", IMAGE_NAME, os, tgt == "builder" ? "-builder" : "") : "",
os == "bookworm" && version != "" ? format("%s:%s%s-php%s", IMAGE_NAME, version, tgt == "builder" ? "-builder" : "", php-version) : "",
]
}
@@ -55,11 +63,21 @@ function "__semver" {
result = v == {} ? [clean_tag(VERSION)] : v.prerelease == null ? ["latest", v.major, "${v.major}.${v.minor}", "${v.major}.${v.minor}.${v.patch}"] : ["${v.major}.${v.minor}.${v.patch}-${v.prerelease}"]
}
function "php_version" {
params = [v]
result = _php_version(v, regexall("(?P<major>\\d+)\\.(?P<minor>\\d+)", v)[0])
}
function "_php_version" {
params = [v, m]
result = "${m.major}.${m.minor}" == DEFAULT_PHP_VERSION ? [v, "${m.major}.${m.minor}", "${m.major}"] : [v, "${m.major}.${m.minor}"]
}
target "default" {
name = "${tgt}-php-${replace(php-version, ".", "-")}-${os}"
matrix = {
os = ["bookworm", "alpine"]
php-version = ["8.2", "8.3.0RC2"]
php-version = split(",", PHP_VERSION)
tgt = ["builder", "runner"]
}
contexts = {
@@ -69,17 +87,25 @@ target "default" {
dockerfile = os == "alpine" ? "alpine.Dockerfile" : "Dockerfile"
context = "./"
target = tgt
platforms = [
# arm/v6 is only available for Alpine: https://github.com/docker-library/golang/issues/502
platforms = os == "alpine" ? [
"linux/amd64",
"linux/386",
"linux/arm/v6",
"linux/arm/v7",
"linux/arm64",
] : [
"linux/amd64",
"linux/386",
"linux/arm/v7",
"linux/arm64"
]
tags = distinct(flatten([
LATEST ? tag("latest", os, php-version, tgt) : [],
tag(SHA == "" ? "" : "sha-${substr(SHA, 0, 7)}", os, php-version, tgt),
[for v in semver(VERSION) : tag(v, os, php-version, tgt)]
tags = distinct(flatten(
[for pv in php_version(php-version) : flatten([
LATEST ? tag("latest", os, pv, tgt) : [],
tag(SHA == "" ? "" : "sha-${substr(SHA, 0, 7)}", os, pv, tgt),
[for v in semver(VERSION) : tag(v, os, pv, tgt)]
])
]))
labels = {
"org.opencontainers.image.created" = "${timestamp()}"
@@ -97,7 +123,20 @@ target "static-builder" {
}
dockerfile = "static-builder.Dockerfile"
context = "./"
tags = ["${IMAGE_NAME}:static-builder"]
platforms = [
"linux/amd64",
"linux/arm64",
]
tags = distinct(flatten([
LATEST ? "${IMAGE_NAME}:static-builder" : "",
SHA == "" ? "" : "${IMAGE_NAME}:static-builder-sha-${substr(SHA, 0, 7)}",
[for v in semver(VERSION) : v == "latest" ? "${IMAGE_NAME}:static-builder": "${IMAGE_NAME}:static-builder-${v}"]
]))
labels = {
"org.opencontainers.image.created" = "${timestamp()}"
"org.opencontainers.image.version" = VERSION
"org.opencontainers.image.revision" = SHA
}
args = {
FRANKENPHP_VERSION = VERSION
}

View File

@@ -68,12 +68,37 @@ make -j$(sysctl -n hw.logicalcpu)
sudo make install
```
#### Compile the Go App
## Compile the Go App
You can now use the Go library and compile our Caddy build:
```
```console
curl -L https://github.com/dunglas/frankenphp/archive/refs/heads/main.tar.gz | tar x
cd frankenphp-main/caddy/frankenphp
CGO_CFLAGS=$(php-config --includes) go build
CGO_CFLAGS=$(php-config --includes) CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)" go build
```
### Using xcaddy
Alternatively, use [xcaddy](https://github.com/caddyserver/xcaddy) to compile FrankenPHP with [custom Caddy modules](https://caddyserver.com/docs/modules/):
```console
CGO_ENABLED=1 \
XCADDY_GO_BUILD_FLAGS="-ldflags '-w -s'" \
xcaddy build \
--output frankenphp \
--with github.com/dunglas/frankenphp/caddy \
--with github.com/dunglas/mercure/caddy \
--with github.com/dunglas/vulcain/caddy
# Add extra Caddy modules here
```
> [!TIP]
>
> If you're using musl libc (the default on Alpine Linux) and Symfony,
> you may need to increase the default stack size.
> Otherwise, you may get errors like `PHP Fatal error: Maximum call stack size of 83360 bytes reached during compilation. Try splitting expression`
>
> To do so, change the `XCADDY_GO_BUILD_FLAGS` environment variable to something like
> `XCADDY_GO_BUILD_FLAGS=$'-ldflags "-w -s -extldflags \'-Wl,-z,stack-size=0x80000\'"'`
> (change the value of the stack size according to your app needs).

View File

@@ -1,85 +1,157 @@
# Configuration
FrankenPHP, Caddy as well the Mercure and Vulcain modules can be configured using [the formats supported by Caddy](https://caddyserver.com/docs/getting-started#your-first-config).
FrankenPHP, Caddy as well as the Mercure and Vulcain modules can be configured using [the formats supported by Caddy](https://caddyserver.com/docs/getting-started#your-first-config).
In the Docker image, the `Caddyfile` is located at `/etc/Caddyfile`.
In the Docker image, the `Caddyfile` is located at `/etc/caddy/Caddyfile`.
You can also configure PHP using `php.ini` as usual.
In the Docker image, the `php.ini` file is located at `/usr/local/lib/php.ini`.
In the Docker image, the `php.ini` file is not present, you can create it or `COPY` manually.
## Caddy Directives
If you copy `php.ini` from `$PHP_INI_DIR/php.ini-production` or `$PHP_INI_DIR/php.ini-development`, you also must set the variable `variables_order = "EGPCS"`, because the default value for `variables_order` is `"EGPCS"`, but in `php.ini-production` and `php.ini-development` we have `"GPCS"`. And in this case, `worker` does not work properly.
To register the FrankenPHP executor, the `frankenphp` directive must be set in Caddy global options, then the `php` HTTP directive must be set under routes serving PHP scripts:
```dockerfile
FROM dunglas/frankenphp
RUN cp $PHP_INI_DIR/php.ini-production $PHP_INI_DIR/php.ini; \
sed -i 's/variables_order = "GPCS"/variables_order = "EGPCS"/' $PHP_INI_DIR/php.ini;
```
Then, you can use the `php` HTTP directive to execute PHP scripts:
## Caddyfile Config
To register the FrankenPHP executor, the `frankenphp` [global option](https://caddyserver.com/docs/caddyfile/concepts#global-options) must be set, then the `php_server` or the `php` [HTTP directives](https://caddyserver.com/docs/caddyfile/concepts#directives) may be used within the site blocks to serve your PHP app.
Minimal example:
```caddyfile
{
frankenphp
# Enable FrankenPHP
frankenphp
# Configure when the directive must be executed
order php_server before file_server
}
localhost {
route {
php {
root <directory> # Sets the root folder to the site. Default: `root` directive.
split_path <delim...> # Sets the substrings for splitting the URI into two parts. The first matching substring will be used to split the "path info" from the path. The first piece is suffixed with the matching substring and will be assumed as the actual resource (CGI script) name. The second piece will be set to PATH_INFO for the CGI script to use. Default: `.php`
resolve_root_symlink # Enables resolving the `root` directory to its actual value by evaluating a symbolic link, if one exists.
env <key> <value> # Sets an extra environment variable to the given value. Can be specified more than once for multiple environment variables.
}
}
# Enable compression (optional)
encode zstd br gzip
# Execute PHP files in the current directory and serve assets
php_server
}
```
Optionnaly, the number of threads to create and [worker scripts](worker.md) to start with the server can be specified under the global directive.
Optionally, the number of threads to create and [worker scripts](worker.md) to start with the server can be specified under the global option.
```caddyfile
{
frankenphp {
num_threads <num_threads> # Sets the number of PHP threads to start. Default: 2x the number of available CPUs.
worker {
file <path> # Sets the path to the worker script.
num <num> # Sets the number of PHP threads to start, defaults to 2x the number of available CPUs.
env <key> <value> # Sets an extra environment variable to the given value. Can be specified more than once for multiple environment variables.
}
}
frankenphp {
num_threads <num_threads> # Sets the number of PHP threads to start. Default: 2x the number of available CPUs.
worker {
file <path> # Sets the path to the worker script.
num <num> # Sets the number of PHP threads to start, defaults to 2x the number of available CPUs.
env <key> <value> # Sets an extra environment variable to the given value. Can be specified more than once for multiple environment variables.
}
}
}
# ...
```
Alternatively, the short form of the `worker` directive can also be used:
Alternatively, you may use the one-line short form of the `worker` option:
```caddyfile
{
frankenphp {
worker <file> <num>
}
frankenphp {
worker <file> <num>
}
}
# ...
```
You can also define multiple workers if you serve multiple apps on the same server:
```caddyfile
{
frankenphp {
worker /path/to/app/public/index.php <num>
worker /path/to/other/public/index.php <num>
}
}
app.example.com {
root * /path/to/app/public
php_server
}
other.example.com {
root * /path/to/other/public
php_server
}
...
```
Using the `php_server` directive is generally what you need,
but if you need full control, you can use the lower level `php` directive:
Using the `php_server` directive is equivalent to this configuration:
```caddyfile
route {
# Add trailing slash for directory requests
@canonicalPath {
file {path}/index.php
not path */
}
redir @canonicalPath {path}/ 308
# If the requested file does not exist, try index files
@indexFiles file {
try_files {path} {path}/index.php index.php
split_path .php
}
rewrite @indexFiles {http.matchers.file.relative}
# FrankenPHP!
@phpFiles path *.php
php @phpFiles
file_server
}
```
The `php_server` and the `php` directives have the following options:
```caddyfile
php_server [<matcher>] {
root <directory> # Sets the root folder to the site. Default: `root` directive.
split_path <delim...> # Sets the substrings for splitting the URI into two parts. The first matching substring will be used to split the "path info" from the path. The first piece is suffixed with the matching substring and will be assumed as the actual resource (CGI script) name. The second piece will be set to PATH_INFO for the CGI script to use. Default: `.php`
resolve_root_symlink # Enables resolving the `root` directory to its actual value by evaluating a symbolic link, if one exists.
env <key> <value> # Sets an extra environment variable to the given value. Can be specified more than once for multiple environment variables.
}
```
## Environment Variables
The following environment variables can be used to inject Caddy directives in the `Caddyfile` without modifying it:
* `SERVER_NAME` change the server name
* `SERVER_NAME`: change [the addresses on which to listen](https://caddyserver.com/docs/caddyfile/concepts#addresses), the provided hostnames will also be used for the generated TLS certificate
* `CADDY_GLOBAL_OPTIONS`: inject [global options](https://caddyserver.com/docs/caddyfile/options)
* `FRANKENPHP_CONFIG`: inject config under the `frankenphp` directive
Unlike with FPM and CLI SAPIs, environment variables are **not** exposed by default in superglobals `$_SERVER` and `$_ENV`.
To propagate environment variables to `$_SERVER` and `$_ENV`, set the `php.ini` `variables_order` directive to `EGPS`.
To propagate environment variables to `$_SERVER` and `$_ENV`, set the `php.ini` `variables_order` directive to `EGPCS`.
## PHP config
To load [additional PHP configuration files](https://www.php.net/manual/en/configuration.file.php#configuration.file.scan),
the `PHP_INI_SCAN_DIR` environment variable can be used.
When set, PHP will load all the file with the `.ini` extension present in the given directories.
## Enable the Debug Mode
When using the Docker image, set the `CADDY_DEBUG` environment variable to `debug` to enable the debug mode:
When using the Docker image, set the `CADDY_GLOBAL_OPTIONS` environment variable to `debug` to enable the debug mode:
```console
docker run -v $PWD:/app/public \
-e CADDY_GLOBAL_OPTIONS=debug \
-p 80:80 -p 443:443 \
-p 80:80 -p 443:443 -p 443:443/udp \
dunglas/frankenphp
```

BIN
docs/digitalocean-dns.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

View File

@@ -1,6 +1,6 @@
# Building Custom Docker Image
[FrankenPHP Docker images](https://hub.docker.com/r/dunglas/frankenphp) are based on [official PHP images](https://hub.docker.com/_/php/). Alpine Linux and Debian variants are provided for popular architectures. Variants for PHP 8.2 and PHP 8.3 are provided. [Browse tags](https://hub.docker.com/repository/docker/dunglas/frankenphp).
[FrankenPHP Docker images](https://hub.docker.com/r/dunglas/frankenphp) are based on [official PHP images](https://hub.docker.com/_/php/). Alpine Linux and Debian variants are provided for popular architectures. Variants for PHP 8.2 and PHP 8.3 are provided. [Browse tags](https://hub.docker.com/r/dunglas/frankenphp/tags).
## How to Use The Images
@@ -15,30 +15,65 @@ COPY . /app/public
Then, run the commands to build and run the Docker image:
```console
$ docker build -t my-php-app .
$ docker run -it --rm --name my-running-app my-php-app
docker build -t my-php-app .
docker run -it --rm --name my-running-app my-php-app
```
## How to Install More PHP Extensions
The [`docker-php-extension-installer`](https://github.com/mlocati/docker-php-extension-installer) script is provided in the base image.
Adding additional PHP extensions is straightforwardd:
Adding additional PHP extensions is straightforward:
```dockerfile
FROM dunglas/frankenphp
# add additional extensions here:
RUN install-php-extensions \
pdo_mysql \
gd \
intl \
zip \
opcache
# ...
pdo_mysql \
gd \
intl \
zip \
opcache
```
# Enabling the Worker Mode by Default
## How to Install More Caddy Modules
FrankenPHP is built on top of Caddy, and all [Caddy modules](https://caddyserver.com/docs/modules/) can be used with FrankenPHP.
The easiest way to install custom Caddy modules is to use [xcaddy](https://github.com/caddyserver/xcaddy):
```dockerfile
FROM dunglas/frankenphp:latest-builder AS builder
# Copy xcaddy in the builder image
COPY --from=caddy:builder /usr/bin/xcaddy /usr/bin/xcaddy
# CGO must be enabled to build FrankenPHP
ENV CGO_ENABLED=1 XCADDY_SETCAP=1 XCADDY_GO_BUILD_FLAGS="-ldflags '-w -s'"
RUN xcaddy build \
--output /usr/local/bin/frankenphp \
--with github.com/dunglas/frankenphp=./ \
--with github.com/dunglas/frankenphp/caddy=./caddy/ \
# Mercure and Vulcain are included in the official build, but feel free to remove them
--with github.com/dunglas/mercure/caddy \
--with github.com/dunglas/vulcain/caddy
# Add extra Caddy modules here
FROM dunglas/frankenphp AS runner
# Replace the official binary by the one contained your custom modules
COPY --from=builder /usr/local/bin/frankenphp /usr/local/bin/frankenphp
```
The `builder` image provided by FrankenPHP contains a compiled version of libphp.
[Builders images](https://hub.docker.com/r/dunglas/frankenphp/tags?name=builder) are provided for all versions of FrankenPHP and PHP, both for Alpine and Debian.
> [!TIP]
>
> If you're using Alpine Linux and Symfony,
> you may need to [increase the default stack size](compile.md#using-xcaddy).
## Enabling the Worker Mode by Default
Set the `FRANKENPHP_CONFIG` environment variable to start FrankenPHP with a worker script:
@@ -50,20 +85,22 @@ FROM dunglas/frankenphp
ENV FRANKENPHP_CONFIG="worker ./public/index.php"
```
# Using a Volume in Development
## Using a Volume in Development
To develop easily with FrankenPHP, mount the directory from your host containing the source code of the app as a volume in the Docker container:
```console
docker run -v $PWD:/app/public -p 80:80 -p 443:443 my-php-app
docker run -v $PWD:/app/public -p 80:80 -p 443:443 -p 443:443/udp --tty my-php-app
```
> ![TIP]
>
> The `--tty` option allows to have nice human-readable logs instead of JSON logs.
With Docker Compose:
```yaml
# compose.yml
version: '3.1'
# compose.yaml
services:
php:
@@ -73,8 +110,52 @@ services:
# uncomment the following line if you want to run this in a production environment
# restart: always
ports:
- 80:80
- 443:443
- "80:80" # HTTP
- "443:443" # HTTPS
- "443:443/udp" # HTTP/3
volumes:
- ./:/app/public
- caddy_data:/data
- caddy_config:/config
# comment the following line in production, it allows to have nice human-readable logs in dev
tty: true
# Volumes needed for Caddy certificates and configuration
volumes:
caddy_data:
caddy_config:
```
## Running as a Non-Root User
FrankenPHP can run as non root user in Docker.
Here is a sample `Dockerfile` doing this:
```dockerfile
FROM dunglas/frankenphp
ARG USER=www-data
USER ${USER}
RUN adduser -D ${USER} \
# Caddy requires an additional capability to bind to port 80 and 443
setcap CAP_NET_BIND_SERVICE=+eip /usr/local/bin/frankenphp
# Caddy requires write access to /data/caddy and /config/caddy
RUN chown -R ${USER}:${USER} /data/caddy && chown -R ${USER}:${USER} /config/caddy
```
## Updates
The Docker images are built:
* when a new release is tagged
* daily at 4am UTC, if new versions of the official PHP images are available
## Development Versions
Development versions are available in the [`dunglas/frankenphp-dev`](https://hub.docker.com/repository/docker/dunglas/frankenphp-dev) Docker repository.
A new build is triggered every time a commit is pushed to the main branch of the GitHub repository.
The `latest*` tags point to the head of the `main` branch.
Tags of the form `sha-<git-commit-hash>` are also available.

127
docs/embed.md Normal file
View File

@@ -0,0 +1,127 @@
# PHP Apps As Standalone Binaries
FrankenPHP has the ability to embed the source code and assets of PHP applications in a static, self-contained binary.
Thanks to this feature, PHP applications can be distributed as standalone binaries that include the application itself, the PHP interpreter and Caddy, a production-level web server.
Learn more about this feature [in the presentation made by Kévin at SymfonyCon](https://dunglas.dev/2023/12/php-and-symfony-apps-as-standalone-binaries/).
## Preparing Your App
Before creating the self-contained binary be sure that your app is ready for embedding.
For instance you likely want to:
* Install the production dependencies of the app
* Dump the autoloader
* Enable the production mode of your application (if any)
* Strip uneeded files such as `.git` or tests to reduce the size of your final binary
For instance, for a Symfony app, you can use the following commands:
```console
# Export the project to get rid of .git/, etc
mkdir $TMPDIR/my-prepared-app
git archive HEAD | tar -x -C $TMPDIR/my-prepared-app
cd $TMPDIR/my-prepared-app
# Set proper environment variables
echo APP_ENV=prod > .env.local
echo APP_DEBUG=0 >> .env.local
# Remove the tests
rm -Rf tests/
# Install the dependencies
composer install --ignore-platform-reqs --no-dev -a
# Optimize .env
composer dump-env prod
```
## Creating a Linux Binary
The easiest way to create a Linux binary is to use the Docker-based builder we provide.
1. Create a file named `static-build.Dockerfile` in the repository of your prepared app:
```dockerfile
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder
# Copy your app
WORKDIR /go/src/app/dist/app
COPY . .
# Build the static binary, be sure to select only the PHP extensions you want
WORKDIR /go/src/app/
RUN EMBED=dist/app/ \
PHP_EXTENSIONS=ctype,iconv,pdo_sqlite \
./build-static.sh
```
2. Build:
```console
docker build -t static-app -f static-build.Dockerfile .
```
3. Extract the binary
```console
docker cp $(docker create --name static-app-tmp static-app):/go/src/app/dist/frankenphp-linux-x86_64 my-app ; docker rm static-app-tmp
```
The resulting binary is the file named `my-app` in the current directory.
## Creating a Binary for Other OSes
If you don't want to use Docker, or want to build a macOS binary, use the shell script we provide:
```console
git clone https://github.com/dunglas/frankenphp
cd frankenphp
EMBED=/path/to/your/app \
PHP_EXTENSIONS=ctype,iconv,pdo_sqlite \
./build-static.sh
```
The resulting binary is the file named `frankenphp-<os>-<arch>` in the `dist/` directory.
## Using The Binary
This is it! The `my-app` file (or `dist/frankenphp-<os>-<arch>` on other OSes) contains your self-contained app!
To start the web app run:
```console
./my-app php-server
```
If your app contains a [worker script](worker.md), start the worker with something like:
```console
./my-app php-server --worker public/index.php
```
To enable HTTPS (a Let's Encrypt certificate is automatically created), HTTP/2 and HTTP/3, specify the domain name to use:
```console
./my-app php-server --domain localhost
```
You can also run the PHP CLI scripts embedded in your binary:
```console
./my-app php-cli bin/console
```
## Customizing The Build
[Read the static build documentation](static.md) to see how to customize the binary (extensions, PHP version...).
## Distributing The Binary
The created binary isn't compressed.
To reduce the size of the file before sending it, you can compress it.
We recommend `xz`.

98
docs/known-issues.md Normal file
View File

@@ -0,0 +1,98 @@
# Known Issues
## Fibers
Calling PHP functions and language constructs that themselves call [cgo](https://go.dev/blog/cgo) in [Fibers](https://www.php.net/manual/en/language.fibers.php) is known to cause crashes.
This issue [is being worked on by the Go project](https://github.com/golang/go/issues/62130).
In the meantime, one solution is not to use constructs (like `echo`) and functions (like `header()`) that delegate to Go from inside Fibers.
This code will likely crash because it uses `echo` in the Fiber:
```php
$fiber = new Fiber(function() {
echo 'In the Fiber'.PHP_EOL;
echo 'Still inside'.PHP_EOL;
});
$fiber->start();
```
Instead, return the value from the Fiber and use it outside:
```php
$fiber = new Fiber(function() {
Fiber::suspend('In the Fiber'.PHP_EOL));
Fiber::suspend('Still inside'.PHP_EOL));
});
echo $fiber->start();
echo $fiber->resume();
$fiber->resume();
```
## Unsupported PHP Extensions
The following extensions are known not to be compatible with FrankenPHP:
| Name | Reason | Alternatives |
| ----------------------------------------------------------- | --------------- | -------------------------------------------------------------------------------------------------------------------- |
| [imap](https://www.php.net/manual/en/imap.installation.php) | Not thread-safe | [javanile/php-imap2](https://github.com/javanile/php-imap2), [webklex/php-imap](https://github.com/Webklex/php-imap) |
## get_browser
The [get_browser()](https://www.php.net/manual/en/function.get-browser.php) function seems to perform badly after a while. A workaround is to cache (e.g. with APCU) the results per User Agent, as they are static.
## Standalone Binary and Alpine-based Docker Images
The standalone binary and Alpine-based docker images (`dunglas/frankenphp:*-alpine`) use [musl libc](https://musl.libc.org/) instead of [glibc and friends](https://www.etalabs.net/compare_libcs.html), to keep a smaller binary size. This may lead to some compatibility issues. In particular, the glob flag `GLOB_BRACE` is [not available](https://www.php.net/manual/en/function.glob.php)
## Using `https://127.0.0.1` with Docker
By default, FrankenPHP generates a TLS certificate for `localhost`.
It's the easiest and recommended option for local development.
If you really want to use `127.0.0.1` as a host instead, it's possible to configure it to generate a certificate for it by setting the server name to `127.0.0.1`.
Unfortunately, this is not enough when using Docker because of [its networking system](https://docs.docker.com/network/).
You will get a TLS error similar to `curl: (35) LibreSSL/3.3.6: error:1404B438:SSL routines:ST_CONNECT:tlsv1 alert internal error`.
If you're using Linux, a solution is to use [the host networking driver](https://docs.docker.com/network/network-tutorial-host/):
```console
docker run \
-e SERVER_NAME="127.0.0.1" \
-v $PWD:/app/public \
--network host \
dunglas/frankenphp
```
The host networking driver isn't supported on Mac and Windows. On these platforms, you will have to guess the IP address of the container and include it in the server names.
Run the `docker network inspect bridge` and look at the `Containers` key to identify the last currently assigned IP address under the `IPv4Address` key, and increment it by one. If no container is running, the first assigned IP address is usually `172.17.0.2`.
Then, include this in the `SERVER_NAME` environment variable:
```console
docker run \
-e SERVER_NAME="127.0.0.1, 172.17.0.3" \
-v $PWD:/app/public \
-p 80:80 -p 443:443 -p 443:443/udp \
dunglas/frankenphp
```
> ![CAUTION]
>
> Be sure to replace `172.17.0.3` with the IP that will be assigned to your container.
You should now be able to access `https://127.0.0.1` from the host machine.
If that's not the case, start FrankenPHP in debug mode to try to figure out the problem:
```console
docker run \
-e CADDY_GLOBAL_OPTIONS="debug"
-e SERVER_NAME="127.0.0.1" \
-v $PWD:/app/public \
-p 80:80 -p 443:443 -p 443:443/udp \
dunglas/frankenphp
```

78
docs/laravel.md Normal file
View File

@@ -0,0 +1,78 @@
# Laravel
## Docker
Serving a [Laravel](https://laravel.com) web application with FrankenPHP is as easy as mounting the project in the `/app` directory of the official Docker image.
Run this command from the main directory of your Laravel app:
```console
docker run -p 80:80 -p 443:443 -p 443:443/udp -v $PWD:/app dunglas/frankenphp
```
And enjoy!
## Local Installation
Alternatively, you can run your Laravel projects with FrankenPHP from your local machine:
1. [Download the binary corresponding to your system](https://github.com/dunglas/frankenphp/releases)
2. Add the following configuration to a file named `Caddyfile` in the root directory of your Laravel project:
```caddyfile
{
frankenphp
order php_server before file_server
}
# The domain name of your server
localhost {
# Set the webroot to the public/ dir
root * public/
# Enable compression (optional)
encode zstd br gzip
# Execute PHP files in the current directory and serve assets
php_server {
# Required for the public/storage/ dir
resolve_root_symlink
}
}
```
3. Start FrankenPHP from the root directory of your Laravel project: `./frankenphp run`
## Laravel Octane
Octane may be installed via the Composer package manager:
```console
composer require laravel/octane
```
After installing Octane, you may execute the `octane:install` Artisan command, which will install Octane's configuration file into your application:
```console
php artisan octane:install --server=frankenphp
```
The Octane server can be started via the `octane:start` Artisan command.
```console
php artisan octane:start
```
The `octane:start` command can take the following options:
* `--host`: The IP address the server should bind to (default: `127.0.0.1`)
* `--port`: The port the server should be available on (default: `8000`)
* `--admin-port`: The port the admin server should be available on (default: `2019`)
* `--workers`: The number of workers that should be available to handle requests (default: `auto`)
* `--max-requests`: The number of requests to process before reloading the server (default: `500`)
* `--caddyfile`: The path to the FrankenPHP `Caddyfile` file
* `--https`: Enable HTTPS, HTTP/2, and HTTP/3, and automatically generate and renew certificates
* --http-redirect : Enable HTTP to HTTPS redirection (only enabled if --https is passed)
* `--watch`: Automatically reload the server when the application is modified
* `--poll`: Use file system polling while watching in order to watch files over a network
* `--log-level`: Log messages at or above the specified log level
Learn more about [Laravel Octane in its official documentation](https://laravel.com/docs/octane).

View File

@@ -1,12 +1,12 @@
# Real-time
FrankenPHP comes with a built-in Mercure hub!
Mercure allows to push event in real-time to all the connected devices: they will receive instantly a JavaScript event.
Mercure allows to push events in real-time to all the connected devices: they will receive a JavaScript event instantly.
No JS library or SDK required!
![Mercure](https://mercure.rocks/static/main.png)
To enable the Mercure hub, update the `Caddyfile` as described [on Mercure's website](https://mercure.rocks/docs/hub/config).
To enable the Mercure hub, update the `Caddyfile` as described [on Mercure's site](https://mercure.rocks/docs/hub/config).
To push Mercure updates from your code, we recommend the [Symfony Mercure Component](https://symfony.com/components/Mercure) (you don't need the Symfony full stack framework to use it).

138
docs/production.md Normal file
View File

@@ -0,0 +1,138 @@
# Deploying in Production
In this tutorial, we will learn how to deploy a PHP application on a single server using Docker Compose.
If you're using Symfony, prefer reading the "[Deploy in production](https://github.com/dunglas/symfony-docker/blob/main/docs/production.md)" documentation entry of the Symfony Docker project (which uses FrankenPHP).
If you're using API Platform (which also uses FrankenPHP), refer to [the deployment documentation of the framework](https://api-platform.com/docs/deployment/).
## Preparing Your App
First, create a `Dockerfile` in the root directory of your PHP project:
```dockerfile
FROM dunglas/frankenphp
# Be sure to replace "your-domain-name.example.com" by your domain name
ENV SERVER_NAME=your-domain-name.example.com
# If you want to disable HTTPS, use this value instead:
#ENV SERVER_NAME=:80
# Enable PHP production settings
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
# Copy the PHP files of your project in the public directory
COPY . /app/public
# If you use Symfony or Laravel, you need to copy the whole project instead:
#COPY . /app
```
Refer to "[Building Custom Docker Image](docker.md)" for more details and options,
and to learn how to customize the configuration, install PHP extensions and Caddy modules.
If your project uses Composer,
be sure to include it in the Docker image and to install your depedencies.
Then, add a `compose.yaml` file:
```yaml
services:
php:
image: dunglas/frankenphp
restart: always
ports:
- "80:80" # HTTP
- "443:443" # HTTPS
- "443:443/udp" # HTTP/3
volumes:
- caddy_data:/data
- caddy_config:/config
# Volumes needed for Caddy certificates and configuration
volumes:
caddy_data:
caddy_config:
```
> [!NOTE]
> The previous examples are intended for production usage.
> In development, you may want to use a volume, a different PHP configuration and a different value for the `SERVER_NAME` environment variable.
>
> Take a look to the [Symfony Docker](https://github.com/dunglas/symfony-docker) project
> (which uses FrankenPHP) for a more advanced example using multi-stage images,
> Composer, extra PHP extensions, etc.
Finally, if you use Git, commit these files and push.
## Preparing a Server
To deploy your application in production, you need a server.
In this tutorial, we will use a virtual machine provided by DigitalOcean, but any Linux server can work.
If you already have a Linux server with Docker installed, you can skip straight to [the next section](#configuring-a-domain-name).
Otherwise, use [this affiliate link](https://m.do.co/c/5d8aabe3ab80) to get $200 of free credit, create an account, then click on "Create a Droplet".
Then, click on the "Marketplace" tab under the "Choose an image" section and search for the app named "Docker".
This will provision an Ubuntu server with the latest versions of Docker and Docker Compose already installed!
For test purposes, the cheapest plans will be enough.
For real production usage, you'll probably want to pick a plan in the "general purpose" section to fit your needs.
![Deploying FrankenPHP on DigitalOcean with Docker](digitalocean-droplet.png)
You can keep the defaults for other settings, or tweak them according to your needs.
Don't forget to add your SSH key or create a password then press the "Finalize and create" button.
Then, wait a few seconds while your Droplet is provisioning.
When your Droplet is ready, use SSH to connect:
```console
ssh root@<droplet-ip>
```
## Configuring a Domain Name
In most cases, you'll want to associate a domain name with your site.
If you don't own a domain name yet, you'll have to buy one through a registrar.
Then create a DNS record of type `A` for your domain name pointing to the IP address of your server:
```dns
your-domain-name.example.com. IN A 207.154.233.113
```
Example with the DigitalOcean Domains service ("Networking" > "Domains"):
![Configuring DNS on DigitalOcean](digitalocean-dns.png)
> [!NOTE]
> Let's Encrypt, the service used by default by FrankenPHP to automatically generate a TLS certificate doesn't support using bare IP addresses. Using a domain name is mandatory to use Let's Encrypt.
## Deploying
Copy your project on the server using `git clone`, `scp`, or any other tool that may fit your need.
If you use GitHub, you may want to use [a deploy key](https://docs.github.com/en/free-pro-team@latest/developers/overview/managing-deploy-keys#deploy-keys).
Deploy keys are also [supported by GitLab](https://docs.gitlab.com/ee/user/project/deploy_keys/).
Example with Git:
```console
git clone git@github.com:<username>/<project-name>.git
```
Go into the directory containing your project (`<project-name>`), and start the app in production mode:
```console
docker compose up -d --wait
```
Your server is up and running, and a HTTPS certificate has been automatically generated for you.
Go to `https://your-domain-name.example.com` and enjoy!
> [!CAUTION]
> Docker can have a cache layer, make sure you have the right build for each deployment or rebuild your project with --no-cache option to avoid cache issue.
## Deploying on Multiple Nodes
If you want to deploy your app on a cluster of machines, you can use [Docker Swarm](https://docs.docker.com/engine/swarm/stack-deploy/),
which is compatible with the provided Compose files.
To deploy on Kubernetes, take a look at [the Helm chart provided with API Platform](https://api-platform.com/docs/deployment/kubernetes/), which can be easily adapted for use with Symfony Docker.

View File

@@ -5,18 +5,20 @@ it's possible to create a static build of FrankenPHP thanks to the great [static
With this method, a single, portable, binary will contain the PHP interpreter, the Caddy web server and FrankenPHP!
FrankenPHP also supports [embedding the PHP app in the static binary](embed.md).
## Linux
We provide a Docker image to build a Linux static binary:
```console
docker buildx bake --load static-builder
docker cp $(docker create --name static-builder dunglas/frankenphp:static-builder):/go/src/app/caddy/frankenphp/frankenphp frankenphp ; docker rm static-builder
docker cp $(docker create --name static-builder dunglas/frankenphp:static-builder):/go/src/app/dist/frankenphp-linux-$(uname -m) frankenphp ; docker rm static-builder
```
The resulting static binary is named `frankenphp` and is available in the current directory.
If you want to build the static binary without Docker, take a look to the `static-builder.Dockerfile` file.
If you want to build the static binary without Docker, take a look at the macOS instructions, which also works for Linux.
### Custom Extensions
@@ -31,7 +33,17 @@ docker buildx bake --load --set static-builder.args.PHP_EXTENSIONS=opcache,pdo_s
# ...
```
See [the list of supported extensions](https://static-php-cli.zhamao.me/en/guide/extensions.html).
To add libraries enabling additional functionality to the extensions you've enabled, you can pass use the `PHP_EXTENSION_LIBS` Docker ARG:
```console
docker buildx bake \
--load \
--set static-builder.args.PHP_EXTENSIONS=gd \
--set static-builder.args.PHP_EXTENSION_LIBS=libjpeg,libwebp \
static-builder
```
See also: [customizing the build](#customizing-the-build)
### GitHub Token
@@ -44,21 +56,26 @@ GITHUB_TOKEN="xxx" docker --load buildx bake static-builder
## macOS
Run the following command to create a static binary for macOS:
Run the following script to create a static binary for macOS (you must have [Homebrew](https://brew.sh/) installed):
```console
git clone --depth=1 https://github.com/crazywhalecc/static-php-cli.git
cd static-php-cli
composer install --no-dev -a
./bin/spc doctor --auto-fix
./bin/spc fetch --with-php=8.2 -A
./bin/spc build --enable-zts --build-embed --debug "bcmath,calendar,ctype,curl,dba,dom,exif,filter,fileinfo,gd,iconv,intl,mbstring,mbregex,mysqli,mysqlnd,opcache,openssl,pcntl,pdo,pdo_mysql,pdo_pgsql,pdo_sqlite,pgsql,phar,posix,readline,redis,session,simplexml,sockets,sqlite3,tokenizer,xml,xmlreader,xmlwriter,zip,zlib,apcu"
export CGO_CFLAGS="$(./buildroot/bin/php-config --includes | sed s#-I/#-I$PWD/buildroot/#g)"
export CGO_LDFLAGS="-framework CoreFoundation -framework SystemConfiguration $(./buildroot/bin/php-config --ldflags) $(./buildroot/bin/php-config --libs)"
git clone --depth=1 https://github.com/dunglas/frankenphp.git
cd frankenphp/caddy/frankenphp
go build -buildmode=pie -tags "cgo netgo osusergo static_build" -ldflags "-linkmode=external -extldflags -static-pie"
git clone https://github.com/dunglas/frankenphp
cd frankenphp
./build-static.sh
```
See [the list of supported extensions](https://static-php-cli.zhamao.me/en/guide/extensions.html).
Note: this script also works on Linux (and probably on other Unixes), and is used internally by the Docker based static builder we provide.
## Customizing The Build
The following environment variables can be passed to `docker build` and to the `build-static.sh`
script to customize the static build:
* `FRANKENPHP_VERSION`: the version of FrankenPHP to use
* `PHP_VERSION`: the version of PHP to use
* `PHP_EXTENSIONS`: the PHP extensions to build ([list of supported extensions](https://static-php.dev/en/guide/extensions.html))
* `PHP_EXTENSION_LIBS`: extra libraries to build that add extra features to the extensions
* `EMBED`: path of the PHP application to embed in the binary
* `CLEAN`: when set, libphp and all its dependencies are built from scratch (no cache)
* `DEBUG_SYMBOLS`: when set, debug-symbols will not be stripped and will be added within the binary
* `RELEASE`: (maintainers only) when set, the resulting binary will be uploaded on GitHub

View File

@@ -3,76 +3,112 @@
Boot your application once and keep it in memory.
FrankenPHP will handle incoming requests in a few milliseconds.
## Starting Worker Scripts
### Docker
Set the value of the `FRANKENPHP_CONFIG` environment variable to `worker /path/to/your/worker/script.php`:
```console
docker run \
-e FRANKENPHP_CONFIG="worker /app/path/to/your/worker/script.php" \
-v $PWD:/app \
-p 80:80 -p 443:443 -p 443:443/udp \
dunglas/frankenphp
```
### Standalone Binary
Use the `--worker` option of the `php-server` command to serve the content of the current directory using a worker:
```console
./frankenphp php-server --worker /path/to/your/worker/script.php
```
## Symfony Runtime
The worker mode of FrankenPHP is supported by the [Symfony Runtime Component](https://symfony.com/doc/current/components/runtime.html).
To start any Symfony application in a worker, install the FrankenPHP package of [PHP Runtime](https://github.com/php-runtime/runtime):
```console
composer require runtime/frankenphp-symfony
```
Start your app server by defining the `APP_RUNTIME` environment variable to use the FrankenPHP Symfony Runtime:
```console
docker run \
-e FRANKENPHP_CONFIG="worker ./public/index.php" \
-e APP_RUNTIME=Runtime\\FrankenPhpSymfony\\Runtime \
-v $PWD:/app \
-p 80:80 -p 443:443 -p 443:443/udp \
dunglas/frankenphp
```
## Laravel Octane
See [the dedicated documentation](laravel.md#laravel-octane).
## Custom Apps
The following example shows how to create your own worker script without relying on a third-party library:
```php
<?php
// public/index.php
// Prevent worker script termination when a client connection is interrupted
ignore_user_abort(true);
// Boot your app
require __DIR__.'/vendor/autoload.php';
$myApp = new \App\Kernel();
$myApp->boot();
do {
$running = frankenphp_handle_request(function () use ($myApp) {
// Handler outside the loop for better performance (doing less work)
$handler = static function () use ($myApp) {
// Called when a request is received,
// superglobals, php://input and the like are reset
echo $myApp->handle($_GET, $_POST, $_COOKIE, $_FILES, $_SERVER);
});
};
for($nbRequests = 0, $running = true; isset($_SERVER['MAX_REQUESTS']) && ($nbRequests < ((int)$_SERVER['MAX_REQUESTS'])) && $running; ++$nbRequests) {
$running = \frankenphp_handle_request($handler);
// Do something after sending the HTTP response
$myApp->terminate();
// Call the garbage collector to reduce the chances of it being triggered in the middle of a page generation
gc_collect_cycles();
} while ($running);
}
// Cleanup
$myApp->shutdown();
```
Then, start your app and use the `FRANKENPHP_CONFIG` environment variable to configure your worker:
Then, start your app and use the `FRANKENPHP_CONFIG` environment variable to configure your worker:
```sh
```console
docker run \
-e FRANKENPHP_CONFIG="worker ./public/index.php" \
-v $PWD:/app \
-p 80:80 -p 443:443 \
-p 80:80 -p 443:443 -p 443:443/udp \
dunglas/frankenphp
```
By default, one worker per CPU is started.
You can also configure the number of workers to start:
```sh
```console
docker run \
-e FRANKENPHP_CONFIG="worker ./public/index.php 42" \
-v $PWD:/app \
-p 80:80 -p 443:443 \
dunglas/frankenphp
```
## Symfony Runtime
The worker mode of FrankenPHP is supported by the [Symfony Runtime Component](https://symfony.com/doc/current/components/runtime.html).
To start any Symfony application in a worker, install the FrankenPHP package of [PHP Runtime](https://github.com/php-runtime/runtime):
```sh
composer require runtime/frankenphp-symfony
```
Start your app server by defining the `APP_RUNTIME` environment variable to use the FrankenPHP Symfony Runtime
```sh
docker run \
-e FRANKENPHP_CONFIG="worker ./public/index.php" \
-e APP_RUNTIME=Runtime\\FrankenPhpSymfony\\Runtime \
-v $PWD:/app \
-p 80:80 -p 443:443 \
-p 80:80 -p 443:443 -p 443:443/udp \
dunglas/frankenphp
```
## Laravel Octane
### Restart the Worker After a Certain Number of Requests
Coming soon!
As PHP was not originally designed for long-running processes, there are still many libraries and legacy codes that leak memory.
A workaround to using this type of code in worker mode is to restart the worker script after processing a certain number of requests:
The previous worker snippet allows configuring a maximum number of request to handle by setting an environment variable named `MAX_REQUESTS`.

188
embed.go Normal file
View File

@@ -0,0 +1,188 @@
package frankenphp
import (
"archive/tar"
"bytes"
_ "embed"
"errors"
"fmt"
"io"
"io/fs"
"log"
"os"
"path"
"path/filepath"
"runtime"
"strings"
"time"
)
// The path of the embedded PHP application (empty if none)
var EmbeddedAppPath string
//go:embed app.tar
var embeddedApp []byte
//go:embed app_checksum.txt
var embeddedAppChecksum []byte
func init() {
if len(embeddedApp) == 0 {
// No embedded app
return
}
appPath := filepath.Join(os.TempDir(), "frankenphp_"+strings.TrimSuffix(string(embeddedAppChecksum[:]), "\n"))
if _, err := os.Stat(appPath); os.IsNotExist(err) {
if err := untar(appPath); err != nil {
os.RemoveAll(appPath)
panic(err)
}
}
EmbeddedAppPath = appPath
}
// untar reads the tar file from r and writes it into dir.
//
// Adapted from https://github.com/golang/build/blob/master/cmd/buildlet/buildlet.go
func untar(dir string) (err error) {
t0 := time.Now()
nFiles := 0
madeDir := map[string]bool{}
tr := tar.NewReader(bytes.NewReader(embeddedApp))
loggedChtimesError := false
for {
f, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return fmt.Errorf("tar error: %w", err)
}
if f.Typeflag == tar.TypeXGlobalHeader {
// golang.org/issue/22748: git archive exports
// a global header ('g') which after Go 1.9
// (for a bit?) contained an empty filename.
// Ignore it.
continue
}
rel, err := nativeRelPath(f.Name)
if err != nil {
return fmt.Errorf("tar file contained invalid name %q: %v", f.Name, err)
}
abs := filepath.Join(dir, rel)
fi := f.FileInfo()
mode := fi.Mode()
switch {
case mode.IsRegular():
// Make the directory. This is redundant because it should
// already be made by a directory entry in the tar
// beforehand. Thus, don't check for errors; the next
// write will fail with the same error.
dir := filepath.Dir(abs)
if !madeDir[dir] {
if err := os.MkdirAll(filepath.Dir(abs), mode.Perm()); err != nil {
return err
}
madeDir[dir] = true
}
if runtime.GOOS == "darwin" && mode&0111 != 0 {
// See comment in writeFile.
err := os.Remove(abs)
if err != nil && !errors.Is(err, fs.ErrNotExist) {
return err
}
}
wf, err := os.OpenFile(abs, os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode.Perm())
if err != nil {
return err
}
n, err := io.Copy(wf, tr)
if closeErr := wf.Close(); closeErr != nil && err == nil {
err = closeErr
}
if err != nil {
return fmt.Errorf("error writing to %s: %v", abs, err)
}
if n != f.Size {
return fmt.Errorf("only wrote %d bytes to %s; expected %d", n, abs, f.Size)
}
modTime := f.ModTime
if modTime.After(t0) {
// Clamp modtimes at system time. See
// golang.org/issue/19062 when clock on
// buildlet was behind the gitmirror server
// doing the git-archive.
modTime = t0
}
if !modTime.IsZero() {
if err := os.Chtimes(abs, modTime, modTime); err != nil && !loggedChtimesError {
// benign error. Gerrit doesn't even set the
// modtime in these, and we don't end up relying
// on it anywhere (the gomote push command relies
// on digests only), so this is a little pointless
// for now.
log.Printf("error changing modtime: %v (further Chtimes errors suppressed)", err)
loggedChtimesError = true // once is enough
}
}
nFiles++
case mode.IsDir():
if err := os.MkdirAll(abs, mode.Perm()); err != nil {
return err
}
madeDir[abs] = true
case mode&os.ModeSymlink != 0:
// TODO: ignore these for now. They were breaking x/build tests.
// Implement these if/when we ever have a test that needs them.
// But maybe we'd have to skip creating them on Windows for some builders
// without permissions.
default:
return fmt.Errorf("tar file entry %s contained unsupported file type %v", f.Name, mode)
}
}
return nil
}
// nativeRelPath verifies that p is a non-empty relative path
// using either slashes or the buildlet's native path separator,
// and returns it canonicalized to the native path separator.
func nativeRelPath(p string) (string, error) {
if p == "" {
return "", errors.New("path not provided")
}
if filepath.Separator != '/' && strings.Contains(p, string(filepath.Separator)) {
clean := filepath.Clean(p)
if filepath.IsAbs(clean) {
return "", fmt.Errorf("path %q is not relative", p)
}
if clean == ".." || strings.HasPrefix(clean, ".."+string(filepath.Separator)) {
return "", fmt.Errorf("path %q refers to a parent directory", p)
}
if strings.HasPrefix(p, string(filepath.Separator)) || filepath.VolumeName(clean) != "" {
// On Windows, this catches semi-relative paths like "C:" (meaning “the
// current working directory on volume C:”) and "\windows" (meaning “the
// windows subdirectory of the current drive letter”).
return "", fmt.Errorf("path %q is relative to volume", p)
}
return p, nil
}
clean := path.Clean(p)
if path.IsAbs(clean) {
return "", fmt.Errorf("path %q is not relative", p)
}
if clean == ".." || strings.HasPrefix(clean, "../") {
return "", fmt.Errorf("path %q refers to a parent directory", p)
}
canon := filepath.FromSlash(p)
if filepath.VolumeName(canon) != "" {
return "", fmt.Errorf("path %q begins with a native volume name", p)
}
return canon, nil
}

File diff suppressed because it is too large Load Diff

View File

@@ -6,24 +6,25 @@
package frankenphp
//go:generate rm -Rf C-Thread-Pool/
//go:generate git clone --branch=fix/SA_ONSTACK --depth=1 git@github.com:dunglas/C-Thread-Pool.git
//go:generate rm -Rf C-Thread-Pool/.git C-Thread-Pool/.circleci C-Thread-Pool/docs C-Thread-Pool/tests
//go:generate git clone --depth=1 git@github.com:Pithikos/C-Thread-Pool.git
//go:generate rm -Rf C-Thread-Pool/.git C-Thread-Pool/.github C-Thread-Pool/docs C-Thread-Pool/tests C-Thread-Pool/example.c
// Use PHP includes corresponding to your PHP installation by running:
//
// export CGO_CFLAGS=$(php-config --includes)
// export CGO_LDFLAGS=$(php-config --ldflags --libs)
// export CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)"
//
// We also set these flags for hardening: https://github.com/docker-library/php/blob/master/8.2/bookworm/zts/Dockerfile#L57-L59
// #cgo darwin pkg-config: libxml-2.0 sqlite3
// #cgo darwin pkg-config: libxml-2.0
// #cgo CFLAGS: -Wall -Werror -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64
// #cgo CFLAGS: -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib
// #cgo CFLAGS: -DTHREAD_NAME=frankenphp
// #cgo linux CFLAGS: -D_GNU_SOURCE
// #cgo CPPFLAGS: -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64
// #cgo darwin LDFLAGS: -L/opt/homebrew/opt/libiconv/lib -liconv
// #cgo linux LDFLAGS: -Wl,-O1
// #cgo LDFLAGS: -pie -L/usr/local/lib -L/usr/lib -lphp -lresolv -ldl -lm -lutil
// #cgo linux LDFLAGS: -Wl,-O1 -lresolv
// #cgo LDFLAGS: -pie -L/usr/local/lib -L/usr/lib -lphp -ldl -lm -lutil
// #include <stdlib.h>
// #include <stdint.h>
// #include <php_variables.h>
@@ -38,6 +39,7 @@ import (
"fmt"
"io"
"net/http"
"os"
"runtime"
"runtime/cgo"
"strconv"
@@ -50,9 +52,11 @@ import (
//_ "github.com/ianlancetaylor/cgosymbolizer"
)
type key int
type contextKeyStruct struct{}
type handleKeyStruct struct{}
var contextKey key
var contextKey = contextKeyStruct{}
var handleKey = handleKeyStruct{}
var (
InvalidRequestError = errors.New("not a FrankenPHP request")
@@ -109,41 +113,22 @@ func (l syslogLevel) String() string {
// FrankenPHPContext provides contextual information about the Request to handle.
type FrankenPHPContext struct {
// The root directory of the PHP application.
DocumentRoot string
documentRoot string
splitPath []string
env map[string]string
logger *zap.Logger
// The path in the URL will be split into two, with the first piece ending
// with the value of SplitPath. The first piece will be assumed as the
// actual resource (CGI script) name, and the second piece will be set to
// PATH_INFO for the CGI script to use.
//
// Future enhancements should be careful to avoid CVE-2019-11043,
// which can be mitigated with use of a try_files-like behavior
// that 404s if the fastcgi path info is not found.
SplitPath []string
// Path declared as root directory will be resolved to its absolute value
// after the evaluation of any symbolic links.
// Due to the nature of PHP opcache, root directory path is cached: when
// using a symlinked directory as root this could generate errors when
// symlink is changed without php-fpm being restarted; enabling this
// directive will set $_SERVER['DOCUMENT_ROOT'] to the real directory path.
ResolveRootSymlink bool
// CGI-like environment variables that will be available in $_SERVER.
// This map is populated automatically, exisiting key are never replaced.
Env map[string]string
// The logger associated with the current request
Logger *zap.Logger
populated bool
authPassword string
docURI string
pathInfo string
scriptName string
scriptFilename string
// Whether the request is already closed by us
closed sync.Once
responseWriter http.ResponseWriter
responseWriter http.ResponseWriter
exitStatus C.int
done chan interface{}
currentWorkerRequest cgo.Handle
}
@@ -158,19 +143,60 @@ func clientHasClosed(r *http.Request) bool {
}
// NewRequestWithContext creates a new FrankenPHP request context.
func NewRequestWithContext(r *http.Request, documentRoot string, l *zap.Logger) *http.Request {
if l == nil {
l = getLogger()
func NewRequestWithContext(r *http.Request, opts ...RequestOption) (*http.Request, error) {
fc := &FrankenPHPContext{
done: make(chan interface{}),
}
for _, o := range opts {
if err := o(fc); err != nil {
return nil, err
}
}
ctx := context.WithValue(r.Context(), contextKey, &FrankenPHPContext{
DocumentRoot: documentRoot,
SplitPath: []string{".php"},
Env: make(map[string]string),
Logger: l,
})
if fc.documentRoot == "" {
if EmbeddedAppPath != "" {
fc.documentRoot = EmbeddedAppPath
} else {
var err error
if fc.documentRoot, err = os.Getwd(); err != nil {
return nil, err
}
}
}
return r.WithContext(ctx)
if fc.splitPath == nil {
fc.splitPath = []string{".php"}
}
if fc.env == nil {
fc.env = make(map[string]string)
}
if fc.logger == nil {
fc.logger = getLogger()
}
if splitPos := splitPos(fc, r.URL.Path); splitPos > -1 {
fc.docURI = r.URL.Path[:splitPos]
fc.pathInfo = r.URL.Path[splitPos:]
// Strip PATH_INFO from SCRIPT_NAME
fc.scriptName = strings.TrimSuffix(r.URL.Path, fc.pathInfo)
// Ensure the SCRIPT_NAME has a leading slash for compliance with RFC3875
// Info: https://tools.ietf.org/html/rfc3875#section-4.1.13
if fc.scriptName != "" && !strings.HasPrefix(fc.scriptName, "/") {
fc.scriptName = "/" + fc.scriptName
}
}
// SCRIPT_FILENAME is the absolute path of SCRIPT_NAME
fc.scriptFilename = sanitizedPathJoin(fc.documentRoot, fc.scriptName)
c := context.WithValue(r.Context(), contextKey, fc)
c = context.WithValue(c, handleKey, Handles())
return r.WithContext(c), nil
}
// FromContext extracts the FrankenPHPContext from a context.
@@ -273,7 +299,7 @@ func Init(options ...Option) error {
config := Config()
if config.Version.MajorVersion < 8 || config.Version.MinorVersion < 2 {
if config.Version.MajorVersion < 8 || (config.Version.MajorVersion == 8 && config.Version.MinorVersion < 2) {
return InvalidPHPVersionError
}
@@ -298,7 +324,10 @@ func Init(options ...Option) error {
return err
}
logger.Debug("FrankenPHP started")
logger.Info("FrankenPHP started 🐘", zap.String("php_version", Version().Version))
if EmbeddedAppPath != "" {
logger.Info("embedded PHP app 📦", zap.String("path", EmbeddedAppPath))
}
return nil
}
@@ -313,6 +342,11 @@ func Shutdown() {
// Always reset the WaitGroup to ensure we're in a clean state
workersReadyWG = sync.WaitGroup{}
// Remove the installed app
if EmbeddedAppPath != "" {
os.RemoveAll(EmbeddedAppPath)
}
logger.Debug("FrankenPHP shut down")
}
@@ -334,12 +368,12 @@ func updateServerContext(request *http.Request, create bool, mrh C.uintptr_t) er
return InvalidRequestError
}
authUser, authPassword, ok := request.BasicAuth()
var cAuthUser, cAuthPassword *C.char
if fc.authPassword != "" {
cAuthPassword = C.CString(fc.authPassword)
if ok && authPassword != "" {
cAuthPassword = C.CString(authPassword)
}
if authUser := fc.Env["REMOTE_USER"]; authUser != "" {
if ok && authUser != "" {
cAuthUser = C.CString(authUser)
}
@@ -361,18 +395,24 @@ func updateServerContext(request *http.Request, create bool, mrh C.uintptr_t) er
cContentType = C.CString(contentType)
}
// compliance with the CGI specification requires that
// PATH_TRANSLATED should only exist if PATH_INFO is defined.
// Info: https://www.ietf.org/rfc/rfc3875 Page 14
var cPathTranslated *C.char
if pathTranslated := fc.Env["PATH_TRANSLATED"]; pathTranslated != "" {
cPathTranslated = C.CString(pathTranslated)
if fc.pathInfo != "" {
cPathTranslated = C.CString(sanitizedPathJoin(fc.documentRoot, fc.pathInfo)) // Info: http://www.oreilly.com/openbook/cgi/ch02_04.html
}
cRequestUri := C.CString(request.URL.RequestURI())
var rh cgo.Handle
if fc.responseWriter == nil {
mrh = C.uintptr_t(cgo.NewHandle(request))
h := cgo.NewHandle(request)
request.Context().Value(handleKey).(*handleList).AddHandle(h)
mrh = C.uintptr_t(h)
} else {
rh = cgo.NewHandle(request)
request.Context().Value(handleKey).(*handleList).AddHandle(rh)
}
ret := C.frankenphp_update_server_context(
@@ -408,20 +448,14 @@ func ServeHTTP(responseWriter http.ResponseWriter, request *http.Request) error
return InvalidRequestError
}
if err := populateEnv(request); err != nil {
return err
}
fc.responseWriter = responseWriter
fc.done = make(chan interface{})
rc := requestChan
// Detect if a worker is available to handle this request
if nil == fc.responseWriter {
fc.Env["FRANKENPHP_WORKER"] = "1"
} else if v, ok := workersRequestChans.Load(fc.Env["SCRIPT_FILENAME"]); ok {
fc.Env["FRANKENPHP_WORKER"] = "1"
rc = v.(chan *http.Request)
if nil != fc.responseWriter {
if v, ok := workersRequestChans.Load(fc.scriptFilename); ok {
rc = v.(chan *http.Request)
}
}
select {
@@ -440,7 +474,9 @@ func go_fetch_request() C.uintptr_t {
return 0
case r := <-requestChan:
return C.uintptr_t(cgo.NewHandle(r))
h := cgo.NewHandle(r)
r.Context().Value(handleKey).(*handleList).AddHandle(h)
return C.uintptr_t(h)
}
}
@@ -450,39 +486,35 @@ func maybeCloseContext(fc *FrankenPHPContext) {
})
}
// go_execute_script Note: only called in cgi-mode
//
//export go_execute_script
func go_execute_script(rh unsafe.Pointer) {
handle := cgo.Handle(rh)
defer handle.Delete()
request := handle.Value().(*http.Request)
fc, ok := FromContext(request.Context())
if !ok {
panic(InvalidRequestError)
}
defer maybeCloseContext(fc)
defer func() {
maybeCloseContext(fc)
request.Context().Value(handleKey).(*handleList).FreeAll()
}()
if err := updateServerContext(request, true, 0); err != nil {
panic(err)
}
if C.frankenphp_request_startup() < 0 {
panic(RequestStartupError)
}
cFileName := C.CString(fc.Env["SCRIPT_FILENAME"])
defer C.free(unsafe.Pointer(cFileName))
if C.frankenphp_execute_script(cFileName) < 0 {
// scriptFilename is freed in frankenphp_execute_script()
fc.exitStatus = C.frankenphp_execute_script(C.CString(fc.scriptFilename))
if fc.exitStatus < 0 {
panic(ScriptExecutionError)
}
C.frankenphp_clean_server_context()
C.frankenphp_request_shutdown()
}
//export go_ub_write
func go_ub_write(rh C.uintptr_t, cString *C.char, length C.int) (C.size_t, C.bool) {
func go_ub_write(rh C.uintptr_t, cBuf *C.char, length C.int) (C.size_t, C.bool) {
r := cgo.Handle(rh).Value().(*http.Request)
fc, _ := FromContext(r.Context())
@@ -495,10 +527,13 @@ func go_ub_write(rh C.uintptr_t, cString *C.char, length C.int) (C.size_t, C.boo
writer = fc.responseWriter
}
i, _ := writer.Write([]byte(C.GoStringN(cString, length)))
i, e := writer.Write(unsafe.Slice((*byte)(unsafe.Pointer(cBuf)), length))
if e != nil {
fc.logger.Error("write error", zap.Error(e))
}
if fc.responseWriter == nil {
fc.Logger.Info(writer.(*bytes.Buffer).String())
fc.logger.Info(writer.(*bytes.Buffer).String())
}
return C.size_t(i), C.bool(clientHasClosed(r))
@@ -506,36 +541,88 @@ func go_ub_write(rh C.uintptr_t, cString *C.char, length C.int) (C.size_t, C.boo
//export go_register_variables
func go_register_variables(rh C.uintptr_t, trackVarsArray *C.zval) {
var env map[string]string
r := cgo.Handle(rh).Value().(*http.Request)
env = r.Context().Value(contextKey).(*FrankenPHPContext).Env
fc := r.Context().Value(contextKey).(*FrankenPHPContext)
le := len(env) * 2
cArr := (**C.char)(C.malloc(C.size_t(le) * C.size_t(unsafe.Sizeof((*C.char)(nil)))))
defer C.free(unsafe.Pointer(cArr))
variables := unsafe.Slice(cArr, le)
le := (len(fc.env) + len(r.Header)) * 2
dynamicVariables := make([]*C.char, le)
var i int
for k, v := range env {
variables[i] = C.CString(k)
// Add all HTTP headers to env variables
for field, val := range r.Header {
k := "HTTP_" + headerNameReplacer.Replace(strings.ToUpper(field))
if _, ok := fc.env[k]; ok {
continue
}
dynamicVariables[i] = C.CString(k)
i++
variables[i] = C.CString(v)
dynamicVariables[i] = C.CString(strings.Join(val, ", "))
i++
}
C.frankenphp_register_bulk_variables(cArr, C.size_t(le), trackVarsArray)
for k, v := range fc.env {
dynamicVariables[i] = C.CString(k)
i++
for _, v := range variables {
C.free(unsafe.Pointer(v))
dynamicVariables[i] = C.CString(v)
i++
}
var dynamicVariablesPtr **C.char = nil
if le > 0 {
dynamicVariablesPtr = &dynamicVariables[0]
}
knownVariables := computeKnownVariables(r)
C.frankenphp_register_bulk_variables(&knownVariables[0], dynamicVariablesPtr, C.size_t(le), trackVarsArray)
fc.env = nil
}
//export go_apache_request_headers
func go_apache_request_headers(rh C.uintptr_t) (*C.go_string, C.size_t, C.uintptr_t) {
r := cgo.Handle(rh).Value().(*http.Request)
pinner := runtime.Pinner{}
pinnerHandle := C.uintptr_t(cgo.NewHandle(pinner))
rl := len(r.Header)
scs := unsafe.Sizeof(C.go_string{})
headers := (*C.go_string)(unsafe.Pointer(C.malloc(C.size_t(rl*2) * (C.size_t)(scs))))
header := headers
for field, val := range r.Header {
fd := unsafe.StringData(field)
pinner.Pin(fd)
*header = C.go_string{C.size_t(len(field)), (*C.char)(unsafe.Pointer(fd))}
header = (*C.go_string)(unsafe.Add(unsafe.Pointer(header), scs))
cv := strings.Join(val, ", ")
vd := unsafe.StringData(cv)
pinner.Pin(vd)
*header = C.go_string{C.size_t(len(cv)), (*C.char)(unsafe.Pointer(vd))}
header = (*C.go_string)(unsafe.Add(unsafe.Pointer(header), scs))
}
return headers, C.size_t(rl), pinnerHandle
}
//export go_apache_request_cleanup
func go_apache_request_cleanup(rh C.uintptr_t) {
h := cgo.Handle(rh)
p := h.Value().(runtime.Pinner)
p.Unpin()
h.Delete()
}
func addHeader(fc *FrankenPHPContext, cString *C.char, length C.int) {
parts := strings.SplitN(C.GoStringN(cString, length), ": ", 2)
if len(parts) != 2 {
fc.Logger.Debug("invalid header", zap.String("header", parts[0]))
fc.logger.Debug("invalid header", zap.String("header", parts[0]))
return
}
@@ -588,7 +675,7 @@ func go_sapi_flush(rh C.uintptr_t) bool {
}
if err := http.NewResponseController(fc.responseWriter).Flush(); err != nil {
fc.Logger.Error("the current responseWriter is not a flusher", zap.Error(err))
fc.logger.Error("the current responseWriter is not a flusher", zap.Error(err))
}
return false
@@ -598,7 +685,7 @@ func go_sapi_flush(rh C.uintptr_t) bool {
func go_read_post(rh C.uintptr_t, cBuf *C.char, countBytes C.size_t) (readBytes C.size_t) {
r := cgo.Handle(rh).Value().(*http.Request)
p := make([]byte, countBytes)
p := unsafe.Slice((*byte)(unsafe.Pointer(cBuf)), countBytes)
var err error
for readBytes < countBytes && err == nil {
var n int
@@ -609,11 +696,7 @@ func go_read_post(rh C.uintptr_t, cBuf *C.char, countBytes C.size_t) (readBytes
if err != nil && err != io.EOF {
// invalid Read on closed Body may happen because of https://github.com/golang/go/issues/15527
fc, _ := FromContext(r.Context())
fc.Logger.Error("error while reading the request body", zap.Error(err))
}
if readBytes != 0 {
C.memcpy(unsafe.Pointer(cBuf), unsafe.Pointer(&p[0]), C.size_t(readBytes))
fc.logger.Error("error while reading the request body", zap.Error(err))
}
return
@@ -627,16 +710,13 @@ func go_read_cookies(rh C.uintptr_t) *C.char {
if len(cookies) == 0 {
return nil
}
cookieString := make([]string, len(cookies))
for _, cookie := range r.Cookies() {
cookieString = append(cookieString, cookie.String())
cookieStrings := make([]string, len(cookies))
for i, cookie := range cookies {
cookieStrings[i] = cookie.String()
}
cCookie := C.CString(strings.Join(cookieString, "; "))
// freed in frankenphp_request_shutdown()
return cCookie
return C.CString(strings.Join(cookieStrings, "; "))
}
//export go_log
@@ -665,3 +745,30 @@ func go_log(message *C.char, level C.int) {
l.Info(m, zap.Stringer("syslog_level", syslogLevel(level)))
}
}
// ExecuteScriptCLI executes the PHP script passed as parameter.
// It returns the exit status code of the script.
func ExecuteScriptCLI(script string, args []string) int {
cScript := C.CString(script)
defer C.free(unsafe.Pointer(cScript))
argc, argv := convertArgs(args)
defer freeArgs(argv)
return int(C.frankenphp_execute_script_cli(cScript, argc, (**C.char)(unsafe.Pointer(&argv[0]))))
}
func convertArgs(args []string) (C.int, []*C.char) {
argc := C.int(len(args))
argv := make([]*C.char, argc)
for i, arg := range args {
argv[i] = C.CString(arg)
}
return argc, argv
}
func freeArgs(argv []*C.char) {
for _, arg := range argv {
C.free(unsafe.Pointer(arg))
}
}

View File

@@ -1,9 +1,9 @@
#ifndef _FRANKENPPHP_H
#define _FRANKENPPHP_H
#include <stdint.h>
#include <stdbool.h>
#include <Zend/zend_types.h>
#include <stdbool.h>
#include <stdint.h>
#ifndef FRANKENPHP_VERSION
#define FRANKENPHP_VERSION dev
@@ -11,46 +11,43 @@
#define STRINGIFY(x) #x
#define TOSTRING(x) STRINGIFY(x)
typedef struct go_string {
size_t len;
const char *data;
} go_string;
typedef struct frankenphp_version {
unsigned char major_version;
unsigned char minor_version;
unsigned char release_version;
const char *extra_version;
const char *version;
unsigned long version_id;
unsigned char major_version;
unsigned char minor_version;
unsigned char release_version;
const char *extra_version;
const char *version;
unsigned long version_id;
} frankenphp_version;
frankenphp_version frankenphp_get_version();
typedef struct frankenphp_config {
frankenphp_version version;
bool zts;
bool zend_signals;
bool zend_max_execution_timers;
frankenphp_version version;
bool zts;
bool zend_signals;
bool zend_max_execution_timers;
} frankenphp_config;
frankenphp_config frankenphp_get_config();
int frankenphp_init(int num_threads);
int frankenphp_update_server_context(
bool create,
uintptr_t current_request,
uintptr_t main_request,
bool create, uintptr_t current_request, uintptr_t main_request,
const char *request_method,
char *query_string,
zend_long content_length,
char *path_translated,
char *request_uri,
const char *content_type,
char *auth_user,
char *auth_password,
int proto_num
);
int frankenphp_worker_reset_server_context();
uintptr_t frankenphp_clean_server_context();
const char *request_method, char *query_string, zend_long content_length,
char *path_translated, char *request_uri, const char *content_type,
char *auth_user, char *auth_password, int proto_num);
int frankenphp_request_startup();
int frankenphp_execute_script(const char *file_name);
uintptr_t frankenphp_request_shutdown();
void frankenphp_register_bulk_variables(char **variables, size_t size, zval *track_vars_array);
int frankenphp_execute_script(char *file_name);
void frankenphp_register_bulk_variables(char *known_variables[27],
char **dynamic_variables, size_t size,
zval *track_vars_array);
int frankenphp_execute_script_cli(char *script, int argc, char **argv);
#endif

View File

@@ -12,3 +12,23 @@ function frankenphp_finish_request(): bool {}
* @alias frankenphp_finish_request
*/
function fastcgi_finish_request(): bool {}
function frankenphp_request_headers(): array {}
/**
* @alias frankenphp_request_headers
*/
function apache_request_headers(): array {}
/**
* @alias frankenphp_response_headers
*/
function getallheaders(): array {}
function frankenphp_response_headers(): array|bool {}
/**
* @alias frankenphp_response_headers
*/
function apache_response_headers(): array|bool {}

View File

@@ -1,29 +1,57 @@
/* This is a generated file, edit the .stub.php file instead.
* Stub hash: de4dc4063fafd8c933e3068c8349889a7ece5f03 */
* Stub hash: 467f1406e17d3b8ca67bba5ea367194e60d8dd27 */
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_frankenphp_handle_request, 0, 1, _IS_BOOL, 0)
ZEND_ARG_TYPE_INFO(0, callback, IS_CALLABLE, 0)
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_frankenphp_handle_request, 0, 1,
_IS_BOOL, 0)
ZEND_ARG_TYPE_INFO(0, callback, IS_CALLABLE, 0)
ZEND_END_ARG_INFO()
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_headers_send, 0, 0, IS_LONG, 0)
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, status, IS_LONG, 0, "200")
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, status, IS_LONG, 0, "200")
ZEND_END_ARG_INFO()
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_frankenphp_finish_request, 0, 0, _IS_BOOL, 0)
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_frankenphp_finish_request, 0, 0,
_IS_BOOL, 0)
ZEND_END_ARG_INFO()
#define arginfo_fastcgi_finish_request arginfo_frankenphp_finish_request
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_frankenphp_request_headers, 0,
0, IS_ARRAY, 0)
ZEND_END_ARG_INFO()
#define arginfo_apache_request_headers arginfo_frankenphp_request_headers
#define arginfo_getallheaders arginfo_frankenphp_request_headers
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_frankenphp_response_headers, 0,
0, MAY_BE_ARRAY | MAY_BE_BOOL)
ZEND_END_ARG_INFO()
#define arginfo_apache_response_headers arginfo_frankenphp_response_headers
ZEND_FUNCTION(frankenphp_handle_request);
ZEND_FUNCTION(headers_send);
ZEND_FUNCTION(frankenphp_finish_request);
ZEND_FUNCTION(frankenphp_request_headers);
ZEND_FUNCTION(frankenphp_response_headers);
static const zend_function_entry ext_functions[] = {
ZEND_FE(frankenphp_handle_request, arginfo_frankenphp_handle_request)
ZEND_FE(headers_send, arginfo_headers_send)
ZEND_FE(frankenphp_finish_request, arginfo_frankenphp_finish_request)
ZEND_FALIAS(fastcgi_finish_request, frankenphp_finish_request, arginfo_fastcgi_finish_request)
ZEND_FE_END
};
ZEND_FE(frankenphp_handle_request, arginfo_frankenphp_handle_request)
ZEND_FE(headers_send, arginfo_headers_send) ZEND_FE(
frankenphp_finish_request, arginfo_frankenphp_finish_request)
ZEND_FALIAS(fastcgi_finish_request, frankenphp_finish_request,
arginfo_fastcgi_finish_request)
ZEND_FE(frankenphp_request_headers,
arginfo_frankenphp_request_headers)
ZEND_FALIAS(apache_request_headers,
frankenphp_request_headers,
arginfo_apache_request_headers)
ZEND_FALIAS(getallheaders, frankenphp_response_headers,
arginfo_getallheaders)
ZEND_FE(frankenphp_response_headers,
arginfo_frankenphp_response_headers)
ZEND_FALIAS(apache_response_headers,
frankenphp_response_headers,
arginfo_apache_response_headers)
ZEND_FE_END};

View File

@@ -12,6 +12,7 @@ import (
"net/textproto"
"net/url"
"os"
"os/exec"
"strconv"
"strings"
"sync"
@@ -61,10 +62,11 @@ func runTest(t *testing.T, test func(func(http.ResponseWriter, *http.Request), *
defer frankenphp.Shutdown()
handler := func(w http.ResponseWriter, r *http.Request) {
req := frankenphp.NewRequestWithContext(r, testDataDir, nil)
if err := frankenphp.ServeHTTP(w, req); err != nil {
panic(err)
}
req, err := frankenphp.NewRequestWithContext(r, frankenphp.WithRequestDocumentRoot(testDataDir, false))
assert.NoError(t, err)
err = frankenphp.ServeHTTP(w, req)
assert.NoError(t, err)
}
var ts *httptest.Server
@@ -85,29 +87,6 @@ func runTest(t *testing.T, test func(func(http.ResponseWriter, *http.Request), *
wg.Wait()
}
func BenchmarkHelloWorld(b *testing.B) {
if err := frankenphp.Init(frankenphp.WithLogger(zap.NewNop())); err != nil {
panic(err)
}
defer frankenphp.Shutdown()
cwd, _ := os.Getwd()
testDataDir := cwd + "/testdata/"
handler := func(w http.ResponseWriter, r *http.Request) {
req := frankenphp.NewRequestWithContext(r, testDataDir, nil)
if err := frankenphp.ServeHTTP(w, req); err != nil {
panic(err)
}
}
req := httptest.NewRequest("GET", "http://example.com/index.php", nil)
w := httptest.NewRecorder()
for i := 0; i < b.N; i++ {
handler(w, req)
}
}
func TestHelloWorld_module(t *testing.T) { testHelloWorld(t, nil) }
func TestHelloWorld_worker(t *testing.T) {
testHelloWorld(t, &testOptions{workerScript: "index.php"})
@@ -140,14 +119,17 @@ func testFinishRequest(t *testing.T, opts *testOptions) {
}, opts)
}
func TestServerVariable_module(t *testing.T) { testServerVariable(t, nil) }
func TestServerVariable_module(t *testing.T) {
testServerVariable(t, nil)
}
func TestServerVariable_worker(t *testing.T) {
testServerVariable(t, &testOptions{workerScript: "server-variable.php"})
}
func testServerVariable(t *testing.T, opts *testOptions) {
runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/server-variable.php/baz/bat?foo=a&bar=b&i=%d#hash", i), nil)
req := httptest.NewRequest("POST", fmt.Sprintf("http://example.com/server-variable.php/baz/bat?foo=a&bar=b&i=%d#hash", i), strings.NewReader("foo"))
req.SetBasicAuth("kevin", "password")
req.Header.Add("Content-Type", "text/plain")
w := httptest.NewRecorder()
handler(w, req)
@@ -163,7 +145,7 @@ func testServerVariable(t *testing.T, opts *testOptions) {
assert.Contains(t, strBody, "[HTTP_AUTHORIZATION] => Basic a2V2aW46cGFzc3dvcmQ=")
assert.Contains(t, strBody, "[DOCUMENT_ROOT]")
assert.Contains(t, strBody, "[PHP_SELF] => /server-variable.php/baz/bat")
assert.Contains(t, strBody, "[CONTENT_TYPE]")
assert.Contains(t, strBody, "[CONTENT_TYPE] => text/plain")
assert.Contains(t, strBody, fmt.Sprintf("[QUERY_STRING] => foo=a&bar=b&i=%d#hash", i))
assert.Contains(t, strBody, fmt.Sprintf("[REQUEST_URI] => /server-variable.php/baz/bat?foo=a&bar=b&i=%d#hash", i))
assert.Contains(t, strBody, "[CONTENT_LENGTH]")
@@ -173,7 +155,7 @@ func testServerVariable(t *testing.T, opts *testOptions) {
assert.Contains(t, strBody, "[DOCUMENT_URI]")
assert.Contains(t, strBody, "[AUTH_TYPE]")
assert.Contains(t, strBody, "[REMOTE_IDENT]")
assert.Contains(t, strBody, "[REQUEST_METHOD] => GET")
assert.Contains(t, strBody, "[REQUEST_METHOD] => POST")
assert.Contains(t, strBody, "[SERVER_NAME] => example.com")
assert.Contains(t, strBody, "[SERVER_PROTOCOL] => HTTP/1.1")
assert.Contains(t, strBody, "[SCRIPT_FILENAME]")
@@ -195,12 +177,15 @@ func testPathInfo(t *testing.T, opts *testOptions) {
testDataDir := cwd + "/testdata/"
requestURI := r.URL.RequestURI()
rewriteRequest := frankenphp.NewRequestWithContext(r, testDataDir, nil)
rewriteRequest.URL.Path = "/server-variable.php/pathinfo"
fc, _ := frankenphp.FromContext(rewriteRequest.Context())
fc.Env["REQUEST_URI"] = requestURI
r.URL.Path = "/server-variable.php/pathinfo"
err := frankenphp.ServeHTTP(w, rewriteRequest)
rewriteRequest, err := frankenphp.NewRequestWithContext(r,
frankenphp.WithRequestDocumentRoot(testDataDir, false),
frankenphp.WithRequestEnv(map[string]string{"REQUEST_URI": requestURI}),
)
assert.NoError(t, err)
err = frankenphp.ServeHTTP(w, rewriteRequest)
assert.NoError(t, err)
}
@@ -241,6 +226,27 @@ func testHeaders(t *testing.T, opts *testOptions) {
}, opts)
}
func TestResponseHeaders_module(t *testing.T) { testResponseHeaders(t, nil) }
func TestResponseHeaders_worker(t *testing.T) {
testResponseHeaders(t, &testOptions{workerScript: "response-headers.php"})
}
func testResponseHeaders(t *testing.T, opts *testOptions) {
runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/response-headers.php?i=%d", i), nil)
w := httptest.NewRecorder()
handler(w, req)
resp := w.Result()
body, _ := io.ReadAll(resp.Body)
assert.Contains(t, string(body), "'X-Powered-By' => 'PH")
assert.Contains(t, string(body), "'Foo' => 'bar',")
assert.Contains(t, string(body), "'Foo2' => 'bar2',")
assert.Contains(t, string(body), fmt.Sprintf("'I' => '%d',", i))
assert.NotContains(t, string(body), "Invalid")
}, opts)
}
func TestInput_module(t *testing.T) { testInput(t, nil) }
func TestInput_worker(t *testing.T) { testInput(t, &testOptions{workerScript: "input.php"}) }
func testInput(t *testing.T, opts *testOptions) {
@@ -309,24 +315,18 @@ func testSession(t *testing.T, opts *testOptions) {
runTest(t, func(_ func(http.ResponseWriter, *http.Request), ts *httptest.Server, i int) {
jar, err := cookiejar.New(&cookiejar.Options{})
if err != nil {
panic(err)
}
assert.NoError(t, err)
client := &http.Client{Jar: jar}
resp1, err := client.Get(ts.URL + "/session.php")
if err != nil {
panic(err)
}
assert.NoError(t, err)
body1, _ := io.ReadAll(resp1.Body)
assert.Equal(t, "Count: 0\n", string(body1))
resp2, err := client.Get(ts.URL + "/session.php")
if err != nil {
panic(err)
}
assert.NoError(t, err)
body2, _ := io.ReadAll(resp2.Body)
assert.Equal(t, "Count: 1\n", string(body2))
@@ -557,6 +557,64 @@ func TestVersion(t *testing.T) {
assert.NotEmpty(t, v.Version, 0)
}
func TestFiberNoCgo_module(t *testing.T) { testFiberNoCgo(t, &testOptions{}) }
func TestFiberNonCgo_worker(t *testing.T) {
testFiberNoCgo(t, &testOptions{workerScript: "fiber-no-cgo.php"})
}
func testFiberNoCgo(t *testing.T, opts *testOptions) {
runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/fiber-no-cgo.php?i=%d", i), nil)
w := httptest.NewRecorder()
handler(w, req)
resp := w.Result()
body, _ := io.ReadAll(resp.Body)
assert.Equal(t, string(body), fmt.Sprintf("Fiber %d", i))
}, opts)
}
func TestRequestHeaders_module(t *testing.T) { testRequestHeaders(t, &testOptions{}) }
func TestRequestHeaders_worker(t *testing.T) {
testRequestHeaders(t, &testOptions{workerScript: "request-headers.php"})
}
func testRequestHeaders(t *testing.T, opts *testOptions) {
runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/request-headers.php?i=%d", i), nil)
req.Header.Add(strings.Clone("Content-Type"), strings.Clone("text/plain"))
req.Header.Add(strings.Clone("Frankenphp-I"), strings.Clone(strconv.Itoa(i)))
w := httptest.NewRecorder()
handler(w, req)
resp := w.Result()
body, _ := io.ReadAll(resp.Body)
assert.Contains(t, string(body), "[Content-Type] => text/plain")
assert.Contains(t, string(body), fmt.Sprintf("[Frankenphp-I] => %d", i))
}, opts)
}
func TestExecuteScriptCLI(t *testing.T) {
if _, err := os.Stat("internal/testcli/testcli"); err != nil {
t.Skip("internal/testcli/testcli has not been compiled, run `cd internal/testcli/ && go build`")
}
cmd := exec.Command("internal/testcli/testcli", "testdata/command.php", "foo", "bar")
stdoutStderr, err := cmd.CombinedOutput()
assert.Error(t, err)
if exitError, ok := err.(*exec.ExitError); ok {
assert.Equal(t, 3, exitError.ExitCode())
}
stdoutStderrStr := string(stdoutStderr)
assert.Contains(t, stdoutStderrStr, `"foo"`)
assert.Contains(t, stdoutStderrStr, `"bar"`)
assert.Contains(t, stdoutStderrStr, "From the CLI")
}
func ExampleServeHTTP() {
if err := frankenphp.Init(); err != nil {
panic(err)
@@ -564,10 +622,116 @@ func ExampleServeHTTP() {
defer frankenphp.Shutdown()
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
req := frankenphp.NewRequestWithContext(r, "/path/to/document/root", nil)
req, err := frankenphp.NewRequestWithContext(r, frankenphp.WithRequestDocumentRoot("/path/to/document/root", false))
if err != nil {
panic(err)
}
if err := frankenphp.ServeHTTP(w, req); err != nil {
panic(err)
}
})
log.Fatal(http.ListenAndServe(":8080", nil))
}
func ExampleExecuteScriptCLI() {
if len(os.Args) <= 1 {
log.Println("Usage: my-program script.php")
os.Exit(1)
}
os.Exit(frankenphp.ExecuteScriptCLI(os.Args[1], os.Args))
}
func BenchmarkHelloWorld(b *testing.B) {
if err := frankenphp.Init(frankenphp.WithLogger(zap.NewNop())); err != nil {
panic(err)
}
defer frankenphp.Shutdown()
cwd, _ := os.Getwd()
testDataDir := cwd + "/testdata/"
handler := func(w http.ResponseWriter, r *http.Request) {
req, err := frankenphp.NewRequestWithContext(r, frankenphp.WithRequestDocumentRoot(testDataDir, false))
if err != nil {
panic(err)
}
if err := frankenphp.ServeHTTP(w, req); err != nil {
panic(err)
}
}
req := httptest.NewRequest("GET", "http://example.com/index.php", nil)
w := httptest.NewRecorder()
for i := 0; i < b.N; i++ {
handler(w, req)
}
}
func BenchmarkEcho(b *testing.B) {
if err := frankenphp.Init(frankenphp.WithLogger(zap.NewNop())); err != nil {
panic(err)
}
defer frankenphp.Shutdown()
cwd, _ := os.Getwd()
testDataDir := cwd + "/testdata/"
handler := func(w http.ResponseWriter, r *http.Request) {
req, err := frankenphp.NewRequestWithContext(r, frankenphp.WithRequestDocumentRoot(testDataDir, false))
if err != nil {
panic(err)
}
if err := frankenphp.ServeHTTP(w, req); err != nil {
panic(err)
}
}
const body = `{
"squadName": "Super hero squad",
"homeTown": "Metro City",
"formed": 2016,
"secretBase": "Super tower",
"active": true,
"members": [
{
"name": "Molecule Man",
"age": 29,
"secretIdentity": "Dan Jukes",
"powers": ["Radiation resistance", "Turning tiny", "Radiation blast"]
},
{
"name": "Madame Uppercut",
"age": 39,
"secretIdentity": "Jane Wilson",
"powers": [
"Million tonne punch",
"Damage resistance",
"Superhuman reflexes"
]
},
{
"name": "Eternal Flame",
"age": 1000000,
"secretIdentity": "Unknown",
"powers": [
"Immortality",
"Heat Immunity",
"Inferno",
"Teleportation",
"Interdimensional travel"
]
}
]
}`
r := strings.NewReader(body)
req := httptest.NewRequest("POST", "http://example.com/echo.php", r)
w := httptest.NewRecorder()
for i := 0; i < b.N; i++ {
r.Reset(body)
handler(w, req)
}
}

10
go.mod
View File

@@ -1,11 +1,13 @@
module github.com/dunglas/frankenphp
go 1.20
go 1.21
retract v1.0.0-rc.1 // Human error
require (
github.com/stretchr/testify v1.8.1
github.com/stretchr/testify v1.8.4
go.uber.org/zap v1.26.0
golang.org/x/net v0.0.0-20221004154528-8021a29435af
golang.org/x/net v0.19.0
)
require (
@@ -13,7 +15,7 @@ require (
github.com/kr/pretty v0.2.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/text v0.14.0 // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

21
go.sum
View File

@@ -1,4 +1,3 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
@@ -6,27 +5,23 @@ github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
golang.org/x/net v0.0.0-20221004154528-8021a29435af h1:wv66FM3rLZGPdxpYL+ApnDe2HzHcTFta3z5nsc13wI4=
golang.org/x/net v0.0.0-20221004154528-8021a29435af/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

17
internal/testcli/main.go Normal file
View File

@@ -0,0 +1,17 @@
package main
import (
"log"
"os"
"github.com/dunglas/frankenphp"
)
func main() {
if len(os.Args) <= 1 {
log.Println("Usage: testcli script.php")
os.Exit(1)
}
os.Exit(frankenphp.ExecuteScriptCLI(os.Args[1], os.Args))
}

View File

@@ -20,12 +20,11 @@ func main() {
defer frankenphp.Shutdown()
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
cwd, err := os.Getwd()
if err != nil {
req, err := frankenphp.NewRequestWithContext(r)
if err == nil {
panic(err)
}
req := frankenphp.NewRequestWithContext(r, cwd, nil)
if err := frankenphp.ServeHTTP(w, req); err != nil {
panic(err)
}

View File

@@ -14,7 +14,12 @@ if ! type "git" > /dev/null; then
exit 1
fi
if [ $# -ne 1 ]; then
if ! type "gh" > /dev/null; then
echo "The \"gh\" command must be installed."
exit 1
fi
if [[ $# -ne 1 ]]; then
echo "Usage: ./release.sh version" >&2
exit 1
fi
@@ -37,3 +42,13 @@ git commit -S -a -m "chore: prepare release $1" || echo "skip"
git tag -s -m "Version $1" "v$1"
git tag -s -m "Version $1" "caddy/v$1"
git push --follow-tags
tags=$(git tag --list --sort=-version:refname 'v*')
previous_tag=$(awk 'NR==2 {print;exit}' <<< "${tags}")
gh release create --draft --generate-notes --latest --notes-start-tag "${previous_tag}" --verify-tag "v$1"
if [[ "$(uname -s)" = "Darwin" ]]; then
rm -Rf dist/*
FRANKENPHP_VERSION=$1 RELEASE=1 ./build-static.sh
fi

4
reload_test.sh Executable file
View File

@@ -0,0 +1,4 @@
#!/bin/bash
for ((i = 0 ; i < 100 ; i++)); do
curl --no-progress-meter -o /dev/null http://localhost:2019/config/apps/frankenphp -: --no-progress-meter -o /dev/null -H 'Cache-Control: must-revalidate' -H 'Content-Type: application/json' --data-binary '{"workers":[{"file_name":"./index.php"}]}' -X PATCH http://localhost:2019/config/apps/frankenphp
done

72
request_options.go Normal file
View File

@@ -0,0 +1,72 @@
package frankenphp
import (
"path/filepath"
"go.uber.org/zap"
)
// RequestOption instances allow to configure a FrankenPHP Request.
type RequestOption func(h *FrankenPHPContext) error
// WithRequestDocumentRoot sets the root directory of the PHP application.
// if resolveSymlink is true, oath declared as root directory will be resolved
// to its absolute value after the evaluation of any symbolic links.
// Due to the nature of PHP opcache, root directory path is cached: when
// using a symlinked directory as root this could generate errors when
// symlink is changed without PHP being restarted; enabling this
// directive will set $_SERVER['DOCUMENT_ROOT'] to the real directory path.
func WithRequestDocumentRoot(documentRoot string, resolveSymlink bool) RequestOption {
return func(o *FrankenPHPContext) error {
// make sure file root is absolute
root, err := filepath.Abs(documentRoot)
if err != nil {
return err
}
if resolveSymlink {
if root, err = filepath.EvalSymlinks(root); err != nil {
return err
}
}
o.documentRoot = root
return nil
}
}
// The path in the URL will be split into two, with the first piece ending
// with the value of SplitPath. The first piece will be assumed as the
// actual resource (CGI script) name, and the second piece will be set to
// PATH_INFO for the CGI script to use.
//
// Future enhancements should be careful to avoid CVE-2019-11043,
// which can be mitigated with use of a try_files-like behavior
// that 404s if the fastcgi path info is not found.
func WithRequestSplitPath(splitPath []string) RequestOption {
return func(o *FrankenPHPContext) error {
o.splitPath = splitPath
return nil
}
}
// WithEnv set CGI-like environment variables that will be available in $_SERVER.
// Values set with WithEnv always have priority over automatically populated values.
func WithRequestEnv(env map[string]string) RequestOption {
return func(o *FrankenPHPContext) error {
o.env = env
return nil
}
}
// WithLogger sets the logger associated with the current request
func WithRequestLogger(logger *zap.Logger) RequestOption {
return func(o *FrankenPHPContext) error {
o.logger = logger
return nil
}
}

64
smartpointer.go Normal file
View File

@@ -0,0 +1,64 @@
package frankenphp
// #include <stdlib.h>
import "C"
import (
"runtime/cgo"
"unsafe"
)
/*
FrankenPHP is fairly complex because it shuffles handles/requests/contexts
between C and Go. This simplifies the lifecycle management of per-request
structures by allowing us to hold references until the end of the request
and ensure they are always cleaned up.
*/
// PointerList A list of pointers that can be freed at a later time
type pointerList struct {
Pointers []unsafe.Pointer
}
// HandleList A list of pointers that can be freed at a later time
type handleList struct {
Handles []cgo.Handle
}
// AddHandle Call when registering a handle for the very first time
func (h *handleList) AddHandle(handle cgo.Handle) {
h.Handles = append(h.Handles, handle)
}
// AddPointer Call when creating a request-level C pointer for the very first time
func (p *pointerList) AddPointer(ptr unsafe.Pointer) {
p.Pointers = append(p.Pointers, ptr)
}
// FreeAll frees all C pointers
func (p *pointerList) FreeAll() {
for _, ptr := range p.Pointers {
C.free(ptr)
}
p.Pointers = nil // To avoid dangling pointers
}
// FreeAll frees all handles
func (h *handleList) FreeAll() {
for _, p := range h.Handles {
p.Delete()
}
}
// Pointers Get a new list of pointers
func Pointers() *pointerList {
return &pointerList{
Pointers: make([]unsafe.Pointer, 0),
}
}
// Handles Get a new list of handles
func Handles() *handleList {
return &handleList{
Handles: make([]cgo.Handle, 0, 8),
}
}

View File

@@ -1,48 +1,73 @@
# syntax=docker/dockerfile:1
FROM golang-base
ARG FRANKENPHP_VERSION='dev'
ARG PHP_VERSION='8.2'
ARG PHP_EXTENSIONS='bcmath,calendar,ctype,curl,dba,dom,exif,filter,fileinfo,gd,iconv,intl,mbstring,mbregex,mysqli,mysqlnd,opcache,openssl,pcntl,pdo,pdo_mysql,pdo_pgsql,pdo_sqlite,pgsql,phar,posix,readline,redis,session,simplexml,sockets,sqlite3,tokenizer,xml,xmlreader,xmlwriter,zip,zlib,apcu'
ARG FRANKENPHP_VERSION=''
ENV FRANKENPHP_VERSION=${FRANKENPHP_VERSION}
ARG PHP_VERSION=''
ENV PHP_VERSION=${PHP_VERSION}
ARG PHP_EXTENSIONS=''
ENV PHP_EXTENSIONS=${PHP_EXTENSIONS}
ARG PHP_EXTENSION_LIBS=''
ENV PHP_EXTENSION_LIBS=${PHP_EXTENSION_LIBS}
ARG CLEAN=''
ARG EMBED=''
SHELL ["/bin/ash", "-eo", "pipefail", "-c"]
LABEL org.opencontainers.image.title=FrankenPHP
LABEL org.opencontainers.image.description="The modern PHP app server"
LABEL org.opencontainers.image.url=https://frankenphp.dev
LABEL org.opencontainers.image.source=https://github.com/dunglas/frankenphp
LABEL org.opencontainers.image.licenses=MIT
LABEL org.opencontainers.image.vendor="Kévin Dunglas"
RUN apk update; \
apk add --no-cache \
autoconf \
automake \
bash \
binutils \
bison \
build-base \
cmake \
curl \
file \
flex \
g++ \
gcc \
git \
jq \
libgcc \
libstdc++ \
linux-headers \
m4 \
make \
php82 \
php82-common \
php82-curl \
php82-dom \
php82-mbstring \
php82-openssl \
php82-pcntl \
php82-phar \
php82-posix \
php82-sodium \
php82-tokenizer \
php82-xml \
php82-xmlwriter \
pkgconfig \
wget \
xz ; \
ln -sf /usr/bin/php82 /usr/bin/php
apk add --no-cache \
autoconf \
automake \
bash \
binutils \
bison \
build-base \
cmake \
curl \
file \
flex \
g++ \
gcc \
git \
jq \
libgcc \
libstdc++ \
libtool \
linux-headers \
m4 \
make \
pkgconfig \
wget \
xz ; \
apk add --no-cache \
--repository=https://dl-cdn.alpinelinux.org/alpine/edge/main \
--repository=https://dl-cdn.alpinelinux.org/alpine/edge/community \
php83 \
php83-common \
php83-ctype \
php83-curl \
php83-dom \
php83-mbstring \
php83-openssl \
php83-pcntl \
php83-phar \
php83-posix \
php83-session \
php83-sodium \
php83-tokenizer \
php83-xml \
php83-xmlwriter; \
ln -sf /usr/bin/php83 /usr/bin/php
# https://getcomposer.org/doc/03-cli.md#composer-allow-superuser
ENV COMPOSER_ALLOW_SUPERUSER=1
@@ -50,34 +75,17 @@ ENV PATH="${PATH}:/root/.composer/vendor/bin"
COPY --from=composer/composer:2-bin --link /composer /usr/bin/composer
WORKDIR /static-php-cli
RUN git clone --depth=1 https://github.com/crazywhalecc/static-php-cli . && \
composer install --no-cache --no-dev --classmap-authoritative
RUN --mount=type=secret,id=github-token GITHUB_TOKEN=$(cat /run/secrets/github-token) ./bin/spc download --with-php=$PHP_VERSION --all
RUN ./bin/spc build --build-embed --enable-zts "$PHP_EXTENSIONS"
ENV PATH="/static-php-cli/buildroot/bin:/static-php-cli/buildroot/usr/bin:$PATH"
WORKDIR /go/src/app
COPY go.mod go.sum ./
RUN go mod graph | awk '{if ($1 !~ "@") print $2}' | xargs go get
RUN mkdir caddy && cd caddy
COPY caddy/go.mod caddy/go.sum ./caddy/
RUN cd caddy && go mod graph | awk '{if ($1 !~ "@") print $2}' | xargs go get
WORKDIR /go/src/app/caddy
COPY caddy/go.mod caddy/go.sum ./
RUN go mod graph | awk '{if ($1 !~ "@") print $2}' | xargs go get
WORKDIR /go/src/app
COPY *.* ./
COPY caddy caddy
COPY C-Thread-Pool C-Thread-Pool
RUN cd caddy/frankenphp && \
CGO_CFLAGS="$(/static-php-cli/buildroot/bin/php-config --includes | sed s#-I/#-I/static-php-cli/buildroot/#g)" \
CGO_LDFLAGS="-DFRANKENPHP_VERSION=$FRANKENPHP_VERSION $(/static-php-cli/buildroot/bin/php-config --ldflags) $(/static-php-cli/buildroot/bin/php-config --libs | sed -e 's/-lgcc_s//g')" \
LIBPHP_VERSION="$(/static-php-cli/buildroot/bin/php-config --version)" \
go build -buildmode=pie -tags "cgo netgo osusergo static_build" -ldflags "-linkmode=external -extldflags -static-pie -s -w -X 'github.com/caddyserver/caddy/v2.CustomVersion=FrankenPHP $FRANKENPHP_VERSION PHP $LIBPHP_VERSION Caddy'" && \
./frankenphp version
RUN --mount=type=secret,id=github-token GITHUB_TOKEN=$(cat /run/secrets/github-token) ./build-static.sh

3
testdata/Caddyfile vendored
View File

@@ -23,10 +23,11 @@ http:// {
}
rewrite @indexFiles {http.matchers.file.relative}
encode zstd br gzip
# FrankenPHP!
@phpFiles path *.php
php @phpFiles
encode zstd gzip
file_server
respond 404

12
testdata/benchmark.Caddyfile vendored Normal file
View File

@@ -0,0 +1,12 @@
{
frankenphp
}
http:// {
route {
root * .
encode zstd br gzip
php_server
}
}

6
testdata/command.php vendored Normal file
View File

@@ -0,0 +1,6 @@
<?php
var_dump($argv, $_SERVER);
echo "From the CLI\n";
exit(3);

5
testdata/echo.php vendored Normal file
View File

@@ -0,0 +1,5 @@
<?php
header('Content-Type: text/plain');
echo file_get_contents('php://input');

12
testdata/fiber-no-cgo.php vendored Normal file
View File

@@ -0,0 +1,12 @@
<?php
require_once __DIR__.'/_executor.php';
return function() {
$fiber = new Fiber(function() {
Fiber::suspend('Fiber '.($_GET['i'] ?? ''));
});
echo $fiber->start();
$fiber->resume();
};

1
testdata/hello.txt vendored Normal file
View File

@@ -0,0 +1 @@
Hello

7
testdata/large-response.php vendored Normal file
View File

@@ -0,0 +1,7 @@
<?php
require_once __DIR__.'/_executor.php';
return function () {
echo str_repeat("Hey\n", 1024);
};

66
testdata/load-test.js vendored Normal file
View File

@@ -0,0 +1,66 @@
import http from 'k6/http'
import { check } from 'k6'
export const options = {
// A number specifying the number of VUs to run concurrently.
vus: 100,
// A string specifying the total duration of the test run.
duration: '30s'
// The following section contains configuration options for execution of this
// test script in Grafana Cloud.
//
// See https://grafana.com/docs/grafana-cloud/k6/get-started/run-cloud-tests-from-the-cli/
// to learn about authoring and running k6 test scripts in Grafana k6 Cloud.
//
// ext: {
// loadimpact: {
// // The ID of the project to which the test is assigned in the k6 Cloud UI.
// // By default tests are executed in default project.
// projectID: "",
// // The name of the test in the k6 Cloud UI.
// // Test runs with the same name will be grouped.
// name: "script.js"
// }
// },
// Uncomment this section to enable the use of Browser API in your tests.
//
// See https://grafana.com/docs/k6/latest/using-k6-browser/running-browser-tests/ to learn more
// about using Browser API in your test scripts.
//
// scenarios: {
// // The scenario name appears in the result summary, tags, and so on.
// // You can give the scenario any name, as long as each name in the script is unique.
// ui: {
// // Executor is a mandatory parameter for browser-based tests.
// // Shared iterations in this case tells k6 to reuse VUs to execute iterations.
// //
// // See https://grafana.com/docs/k6/latest/using-k6/scenarios/executors/ for other executor types.
// executor: 'shared-iterations',
// options: {
// browser: {
// // This is a mandatory parameter that instructs k6 to launch and
// // connect to a chromium-based browser, and use it to run UI-based
// // tests.
// type: 'chromium',
// },
// },
// },
// }
}
const payload = 'foo\n'.repeat(1000)
// The function that defines VU logic.
//
// See https://grafana.com/docs/k6/latest/examples/get-started-with-k6/ to learn more
// about authoring k6 scripts.
//
export default function () {
const res = http.post('http://localhost/echo.php', payload)
check(res, {
'is status 200': (r) => r.status === 200,
'is echoed': (r) => r.body === payload
})
}

7
testdata/request-headers.php vendored Normal file
View File

@@ -0,0 +1,7 @@
<?php
require_once __DIR__.'/_executor.php';
return function() {
print_r(apache_request_headers());
};

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

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

View File

@@ -4,7 +4,6 @@ package frankenphp
// #include "frankenphp.h"
import "C"
import (
"context"
"errors"
"fmt"
"net/http"
@@ -50,44 +49,37 @@ func startWorkers(fileName string, nbWorkers int, env map[string]string) error {
errs []error
)
if env == nil {
env = make(map[string]string, 1)
}
env["FRANKENPHP_WORKER"] = "1"
l := getLogger()
for i := 0; i < nbWorkers; i++ {
go func() {
defer shutdownWG.Done()
for {
// Create main dummy request
fc := &FrankenPHPContext{
Env: make(map[string]string, len(env)+1),
}
fc.Env["SCRIPT_FILENAME"] = absFileName
for k, v := range env {
fc.Env[k] = v
}
r, err := http.NewRequestWithContext(context.WithValue(
context.Background(),
contextKey,
fc,
), "GET", "", nil)
r, err := http.NewRequest(http.MethodGet, filepath.Base(absFileName), nil)
if err != nil {
// TODO: this should never happen, maybe can we remove this block?
m.Lock()
defer m.Unlock()
errs = append(errs, fmt.Errorf("workers %q: unable to create main worker request: %w", absFileName, err))
return
panic(err)
}
r, err = NewRequestWithContext(
r,
WithRequestDocumentRoot(filepath.Dir(absFileName), false),
WithRequestEnv(env),
)
if err != nil {
panic(err)
}
l.Debug("starting", zap.String("worker", absFileName))
if err := ServeHTTP(nil, r); err != nil {
m.Lock()
defer m.Unlock()
errs = append(errs, fmt.Errorf("workers %q: unable to start: %w", absFileName, err))
return
panic(err)
}
fc := r.Context().Value(contextKey).(*FrankenPHPContext)
if fc.currentWorkerRequest != 0 {
// Terminate the pending HTTP request handled by the worker
maybeCloseContext(fc.currentWorkerRequest.Value().(*http.Request).Context().Value(contextKey).(*FrankenPHPContext))
@@ -98,7 +90,11 @@ func startWorkers(fileName string, nbWorkers int, env map[string]string) error {
// TODO: make the max restart configurable
if _, ok := workersRequestChans.Load(absFileName); ok {
workersReadyWG.Add(1)
l.Error("unexpected termination, restarting", zap.String("worker", absFileName))
if fc.exitStatus == 0 {
l.Info("restarting", zap.String("worker", absFileName))
} else {
l.Error("unexpected termination, restarting", zap.String("worker", absFileName), zap.Int("exit_status", int(fc.exitStatus)))
}
} else {
break
}
@@ -138,7 +134,7 @@ func go_frankenphp_worker_handle_request_start(mrh C.uintptr_t) C.uintptr_t {
mainRequest := cgo.Handle(mrh).Value().(*http.Request)
fc := mainRequest.Context().Value(contextKey).(*FrankenPHPContext)
v, ok := workersRequestChans.Load(fc.Env["SCRIPT_FILENAME"])
v, ok := workersRequestChans.Load(fc.scriptFilename)
if !ok {
// Probably shutting down
return 0
@@ -148,23 +144,24 @@ func go_frankenphp_worker_handle_request_start(mrh C.uintptr_t) C.uintptr_t {
l := getLogger()
l.Debug("waiting for request", zap.String("worker", fc.Env["SCRIPT_FILENAME"]))
l.Debug("waiting for request", zap.String("worker", fc.scriptFilename))
var r *http.Request
select {
case <-done:
l.Debug("shutting down", zap.String("worker", fc.Env["SCRIPT_FILENAME"]))
l.Debug("shutting down", zap.String("worker", fc.scriptFilename))
return 0
case r = <-rc:
}
fc.currentWorkerRequest = cgo.NewHandle(r)
r.Context().Value(handleKey).(*handleList).AddHandle(fc.currentWorkerRequest)
l.Debug("request handling started", zap.String("worker", fc.Env["SCRIPT_FILENAME"]), zap.String("url", r.RequestURI))
l.Debug("request handling started", zap.String("worker", fc.scriptFilename), zap.String("url", r.RequestURI))
if err := updateServerContext(r, false, mrh); err != nil {
// Unexpected error
l.Debug("unexpected error", zap.String("worker", fc.Env["SCRIPT_FILENAME"]), zap.String("url", r.RequestURI), zap.Error(err))
l.Debug("unexpected error", zap.String("worker", fc.scriptFilename), zap.String("url", r.RequestURI), zap.Error(err))
return 0
}
@@ -179,8 +176,7 @@ func go_frankenphp_finish_request(mrh, rh C.uintptr_t, deleteHandle bool) {
fc := r.Context().Value(contextKey).(*FrankenPHPContext)
if deleteHandle {
rHandle.Delete()
r.Context().Value(handleKey).(*handleList).FreeAll()
cgo.Handle(mrh).Value().(*http.Request).Context().Value(contextKey).(*FrankenPHPContext).currentWorkerRequest = 0
}
@@ -188,10 +184,10 @@ func go_frankenphp_finish_request(mrh, rh C.uintptr_t, deleteHandle bool) {
var fields []zap.Field
if mrh == 0 {
fields = append(fields, zap.String("worker", fc.Env["SCRIPT_FILENAME"]), zap.String("url", r.RequestURI))
fields = append(fields, zap.String("worker", fc.scriptFilename), zap.String("url", r.RequestURI))
} else {
fields = append(fields, zap.String("url", r.RequestURI))
}
fc.Logger.Debug("request handling finished", fields...)
fc.logger.Debug("request handling finished", fields...)
}

View File

@@ -98,7 +98,11 @@ func ExampleServeHTTP_workers() {
defer frankenphp.Shutdown()
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
req := frankenphp.NewRequestWithContext(r, "/path/to/document/root", nil)
req, err := frankenphp.NewRequestWithContext(r, frankenphp.WithRequestDocumentRoot("/path/to/document/root", false))
if err != nil {
panic(err)
}
if err := frankenphp.ServeHTTP(w, req); err != nil {
panic(err)
}