Skip to content

Commit

Permalink
PHP: Implement TLS 1.2 to decrypt https:// and ssl:// traffic and tra…
Browse files Browse the repository at this point in the history
…nslate it into fetch() (#1926)

Enables HTTPS requests from PHP via `file_get_contents()`, curl, and all
other networking mechanisms. This PR effectively performs a MITM attack
on the PHP instance to decrypt the outbound traffic, run the request
using `fetch()`, and then provide an encrypted response – everything as
if PHP was directly talking to the right server.

## How is it implemented?

Emscripten can be configured to stream all network traffic through a
WebSocket. `@php-wasm/node` and `wp-now` use that to access the internet
via a local WebSocket->TCP proxy, but the in-browser version of
WordPress Playground exposes no such proxy.

This PR ships a "fake" WebSocket class. Instead of starting a `ws://`
connection, it translates the raw HTTP/HTTPS bytes into a `fetch()`
call.

In case of HTTP, the raw request bytes are parsed into a Request object
with a body stream and passes it to `fetch()`. Then, as the response
status, headers, and the body arrive, they're stream-encoded as raw
response bytes and exposed as incoming WebSocket data.

In case of HTTPS, we the raw bytes are first piped through a custom
TCPConnection class as follows:

1. We generate a self-signed CA certificate and tell PHP to trust it
using the `openssl.cafile` PHP.ini option
1. We create a domain-specific child certificate and sign it with the CA
private key.
1. We start accepting raw encrypted bytes, process them as structured
TLS records, and perform the TLS handshake.
1. Encrypted tunnel is established
	* TLSConnection decrypts the encrypted outbound data sent by PHP
	* TLSConnection encrypts the unencrypted inbound data fed back to PHP

From there, the plaintext data is treated by the same HTTP<->fetch()
machinery as described in the previous paragraph.

## Implementation details

This PR ships:

* PHP.wasm bindings to pipe the outbound bytes through a `WebSocket <->
TLS <-> fetch()` pipeline.
* A subset of TLS 1.2 protocol implementation (parts of [RFC
5246](https://datatracker.ietf.org/doc/html/rfc5246), [RFC
6066](https://datatracker.ietf.org/doc/html/rfc6066.html), [RFC
4492](https://datatracker.ietf.org/doc/html/rfc4492#section-5.4), [RFC
8446](https://www.iana.org/go/rfc8446), [RFC
6070](https://www.ietf.org/rfc/rfc6070.txt))
* SSL certificate generator supporting CA certs signed certs

### TLS 1.2

* Parses all TLS record types: handshakes, alerts, application data.
* Performs the full TLS handshake required for ECDH encryption including
the necessary TLS 1.2 extensions.
* Correctly encrypts and decrypts all the post-handshake data.
* Uses `window.crypto()` for encryption.
* Only supports the `TLS1_CK_ECDHE_RSA_WITH_AES_128_GCM_SHA256` mode.
* Doesn't support multiple `ChangeCipherSpec` messages.

### SSL certificate generator

* CA certificate is generated at WASM boot (if networking is enabled)
* Host-specific certificate is generated at every request and signed
with CA private key
* Certificates are created using a custom
[ASN.1/DER](https://letsencrypt.org/docs/a-warm-welcome-to-asn1-and-der/)
encoder and a PEM exporter shipped in this PR
* Only RSA 2048 with SHA-256 supported today

## Avenues explored but not pursued

This work supersedes
#1093 where
`node-forge` was used. Here's why I'm moving to a custom TLS
implementation:

* `node-forge` runs everything synchronously and ships a lot of code.
`window.crypto` is async, faster, bundles less code, and is more
convenient than `node-forge`.
* With `node-forge`, every error made me question fundamentals like the
RSA implementation. With `window.crypto()`, I feel confident assuming
that encryption, hashing, signing etc. are implemented correctly.
* `node-forge` doesn't support TLS 1.3. Neither does this PR, but after
implementing TLS 1.2 I think adding TLS 1.3 support would be reasonably
easy

## Testing instructions

Go to the URL below and confirm you see "Hello-dolly.zip downloaded from
https://downloads.wordpress.org/plugin/hello-dolly.1.7.3.zip has this
many bytes: int(1887)"

```
http://localhost:5400/website-server/?php=8.0&wp=6.6&networking=yes&language=&multisite=no&random=f1qv1twpssr#%7B%22landingPage%22:%22/network-test.php%22,%22preferredVersions%22:%7B%22php%22:%228.0%22,%22wp%22:%22latest%22%7D,%22phpExtensionBundles%22:%5B%22kitchen-sink%22%5D,%22steps%22:%5B%7B%22step%22:%22writeFile%22,%22path%22:%22/wordpress/network-test.php%22,%22data%22:%22%3C?php%20echo%20'Hello-dolly.zip%20downloaded%20from%20https://downloads.wordpress.org/plugin/hello-dolly.1.7.3.zip%20has%20this%20many%20bytes:%20';%20var_dump(strlen(file_get_contents('https://downloads.wordpress.org/plugin/hello-dolly.1.7.3.zip')));%22%7D%5D%7D
```

From there, you could manipulate the URL in the `file_get_contents()`
call to fetch a different file, file with no CORS headers, invalid URLs
etc. Confirm that each time PHP did something sensible, e.g. displayed
the length, displayed the error message, etc. It should never just hang.

Also, confirm the newly added CI tests work as expected.

## Remaining work

- [x] Add a solid unit and E2E test suite, especially for:
    - [x] Streaming: bytes, pause, more bytes
    - [x] `fetch()` exceptions
    - [x] Slow servers
    - [x] POST requests
- [x] Add abundant docstrings to explain what's happening at each stage
- [x] Core work
- [x] Be more strict in `httpRequestToFetch` about HTTP (plaintext) vs
HTTPS (go through TLS) vs other protocols (reject connection). For
example, check ports, pay attention to parsing errors, etc.
   - [x] Rebuild all the in-browser PHP.wasm versions
   - [x] Don't run any of this code when networking is disabled
- [x] Continue using the custom handler for the Requests library to
enable direct `fetch()` calls without the
encrypt->decrypt->encrypt->decrypt overhead.
- [x] Clean it up

## Follow up work

* Caching – perhaps as a follow-up
  * Ship a precomputed CA cert and private key
  * Memoize host-specific certificates

CC @brandonpayton @bgrgicak @dmsnell @mho22
  • Loading branch information
adamziel authored Oct 23, 2024
1 parent 5e5ba3e commit 7fa40be
Show file tree
Hide file tree
Showing 66 changed files with 4,703 additions and 147 deletions.
2 changes: 1 addition & 1 deletion .github/actions/prepare-playground/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ runs:
node-version: ${{ inputs.node-version }}
- name: Cache node modules
id: cache-nodemodules
uses: actions/cache@v2
uses: actions/cache@v4
env:
cache-name: cache-node-modules
with:
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ jobs:
with:
submodules: true
- uses: ./.github/actions/prepare-playground
with:
node-version: 22
- run: node --expose-gc node_modules/nx/bin/nx affected --target=test --configuration=ci
# Most of these tests pass locally but the process is crashing
# on the CI runner.
Expand Down
102 changes: 45 additions & 57 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,168 +4,156 @@ All notable changes to this project are documented in this file by a CI job
that runs on every NPM release. The file follows the [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
format.

## [v1.0.4] (2024-10-21)
## [v1.0.4] (2024-10-21)

### Enhancements

- Support CORS proxy rate-limiting. ([#1879](https://github.com/WordPress/wordpress-playground/pull/1879))
- Support CORS proxy rate-limiting. ([#1879](https://github.com/WordPress/wordpress-playground/pull/1879))

### Blueprints

- Allow multisites to load wp-admin pages with the landingPage attribute. ([#1913](https://github.com/WordPress/wordpress-playground/pull/1913))
- Allow multisites to load wp-admin pages with the landingPage attribute. ([#1913](https://github.com/WordPress/wordpress-playground/pull/1913))

### Tools


#### GitHub integration

- Blueprints: Use `?` instead of `/` to CORS Proxy URLs. ([#1899](https://github.com/WordPress/wordpress-playground/pull/1899))
- Blueprints: Use `?` instead of `/` to CORS Proxy URLs. ([#1899](https://github.com/WordPress/wordpress-playground/pull/1899))

#### Import/Export

- Kickoff Data Liberation: Let's Build WordPress-first Data Migration Tools. ([#1888](https://github.com/WordPress/wordpress-playground/pull/1888))
- Kickoff Data Liberation: Let's Build WordPress-first Data Migration Tools. ([#1888](https://github.com/WordPress/wordpress-playground/pull/1888))

### Experiments


#### File Synchronization

- [Remote] Preserve PHP constants when saving a temporary site. ([#1911](https://github.com/WordPress/wordpress-playground/pull/1911))
- [Remote] Preserve PHP constants when saving a temporary site. ([#1911](https://github.com/WordPress/wordpress-playground/pull/1911))

### Website

- Do not display "You have no Playgrounds" message before loading the site. ([#1912](https://github.com/WordPress/wordpress-playground/pull/1912))
- Fix build error that only appeared during deployment. ([#1896](https://github.com/WordPress/wordpress-playground/pull/1896))
- Fix use of secrets on WP Cloud site. ([#1909](https://github.com/WordPress/wordpress-playground/pull/1909))
- Maintain Query API parameters on temporary Playground settings update. ([#1910](https://github.com/WordPress/wordpress-playground/pull/1910))
- Stop adding all CORS proxy files to website build. ([#1895](https://github.com/WordPress/wordpress-playground/pull/1895))
- Stop responding with default MIME type. ([#1897](https://github.com/WordPress/wordpress-playground/pull/1897))
- Stop short-circuiting web host PHP execution. ([#1898](https://github.com/WordPress/wordpress-playground/pull/1898))
- Fix progress reporting during Playground load. ([#1915](https://github.com/WordPress/wordpress-playground/pull/1915))
- Include CORS proxy with website builds. ([#1880](https://github.com/WordPress/wordpress-playground/pull/1880))
- [Blueprints] Stop escaping landingPage URLs when loading WP Admin. ([#1891](https://github.com/WordPress/wordpress-playground/pull/1891))
- Do not display "You have no Playgrounds" message before loading the site. ([#1912](https://github.com/WordPress/wordpress-playground/pull/1912))
- Fix build error that only appeared during deployment. ([#1896](https://github.com/WordPress/wordpress-playground/pull/1896))
- Fix use of secrets on WP Cloud site. ([#1909](https://github.com/WordPress/wordpress-playground/pull/1909))
- Maintain Query API parameters on temporary Playground settings update. ([#1910](https://github.com/WordPress/wordpress-playground/pull/1910))
- Stop adding all CORS proxy files to website build. ([#1895](https://github.com/WordPress/wordpress-playground/pull/1895))
- Stop responding with default MIME type. ([#1897](https://github.com/WordPress/wordpress-playground/pull/1897))
- Stop short-circuiting web host PHP execution. ([#1898](https://github.com/WordPress/wordpress-playground/pull/1898))
- Fix progress reporting during Playground load. ([#1915](https://github.com/WordPress/wordpress-playground/pull/1915))
- Include CORS proxy with website builds. ([#1880](https://github.com/WordPress/wordpress-playground/pull/1880))
- [Blueprints] Stop escaping landingPage URLs when loading WP Admin. ([#1891](https://github.com/WordPress/wordpress-playground/pull/1891))

### Bug Fixes


#### Boot Flow

- Prefer pretty permalinks like WP install does. ([#1832](https://github.com/WordPress/wordpress-playground/pull/1832))
- Prefer pretty permalinks like WP install does. ([#1832](https://github.com/WordPress/wordpress-playground/pull/1832))

### Various

- Improve self-host documentation. ([#1884](https://github.com/WordPress/wordpress-playground/pull/1884))
- Improve self-host documentation. ([#1884](https://github.com/WordPress/wordpress-playground/pull/1884))

### Contributors

The following contributors merged PRs in this release:

@adamziel @ashfame @bgrgicak @brandonpayton


## [v1.0.3] (2024-10-14)
## [v1.0.3] (2024-10-14)

### Enhancements

- Website: Remove unused React components. ([#1887](https://github.com/WordPress/wordpress-playground/pull/1887))
- Website: Remove unused React components. ([#1887](https://github.com/WordPress/wordpress-playground/pull/1887))

### Tools


#### Blueprints Builder

- Blueprints sidebar section for single-click Playground presets. ([#1759](https://github.com/WordPress/wordpress-playground/pull/1759))
- Blueprints sidebar section for single-click Playground presets. ([#1759](https://github.com/WordPress/wordpress-playground/pull/1759))

### Website

- Replace 100vh with 100dvh to fix an "unscrollable" state on mobile devices. ([#1883](https://github.com/WordPress/wordpress-playground/pull/1883))
- Use modal for Site settings form on mobile – mobile Safari l…. ([#1885](https://github.com/WordPress/wordpress-playground/pull/1885))
- Replace 100vh with 100dvh to fix an "unscrollable" state on mobile devices. ([#1883](https://github.com/WordPress/wordpress-playground/pull/1883))
- Use modal for Site settings form on mobile – mobile Safari l…. ([#1885](https://github.com/WordPress/wordpress-playground/pull/1885))

### Contributors

The following contributors merged PRs in this release:

@adamziel


## [v1.0.2] (2024-10-09)
## [v1.0.2] (2024-10-09)

### PHP WebAssembly

- PHP.wasm: Load correct php.wasm paths in the built Node.js packages. ([#1877](https://github.com/WordPress/wordpress-playground/pull/1877))
- PHP.wasm: Load correct php.wasm paths in the built Node.js packages. ([#1877](https://github.com/WordPress/wordpress-playground/pull/1877))

### Contributors

The following contributors merged PRs in this release:

@adamziel

## [v1.0.1] (2024-10-09)

## [v1.0.1] (2024-10-09)




## [v1.0.0] (2024-10-09)
## [v1.0.0] (2024-10-09)

### Blueprints

- Directory Resources. ([#1793](https://github.com/WordPress/wordpress-playground/pull/1793))
- Login step – handle passwordless autologin via a PHP mu-plugin. ([#1856](https://github.com/WordPress/wordpress-playground/pull/1856))
- Directory Resources. ([#1793](https://github.com/WordPress/wordpress-playground/pull/1793))
- Login step – handle passwordless autologin via a PHP mu-plugin. ([#1856](https://github.com/WordPress/wordpress-playground/pull/1856))

### Tools


#### Blueprints

- GitDirectoryResource. ([#1858](https://github.com/WordPress/wordpress-playground/pull/1858))
- GitDirectoryResource: Accept "/", "", "." as root paths. ([#1860](https://github.com/WordPress/wordpress-playground/pull/1860))
- GitDirectoryResource. ([#1858](https://github.com/WordPress/wordpress-playground/pull/1858))
- GitDirectoryResource: Accept "/", "", "." as root paths. ([#1860](https://github.com/WordPress/wordpress-playground/pull/1860))

#### GitHub integration

- Native git support: LsRefs(), sparseCheckout(), GitPathControl. ([#1764](https://github.com/WordPress/wordpress-playground/pull/1764))
- Native git support: LsRefs(), sparseCheckout(), GitPathControl. ([#1764](https://github.com/WordPress/wordpress-playground/pull/1764))

#### Website

- Add core-pr and gutenberg-pr Query API parameters. ([#1761](https://github.com/WordPress/wordpress-playground/pull/1761))
- Add core-pr and gutenberg-pr Query API parameters. ([#1761](https://github.com/WordPress/wordpress-playground/pull/1761))

### PHP WebAssembly

- Refreshless website deployments – load remote.html using the network-first strategy. ([#1849](https://github.com/WordPress/wordpress-playground/pull/1849))
- JSPI: Pass all unit tests, remove stale PHP builds. ([#1876](https://github.com/WordPress/wordpress-playground/pull/1876))
- [Remote] Remove the "light" PHP.wasm bundle and only ship the "kitchen-sink" build. ([#1861](https://github.com/WordPress/wordpress-playground/pull/1861))
- Refreshless website deployments – load remote.html using the network-first strategy. ([#1849](https://github.com/WordPress/wordpress-playground/pull/1849))
- JSPI: Pass all unit tests, remove stale PHP builds. ([#1876](https://github.com/WordPress/wordpress-playground/pull/1876))
- [Remote] Remove the "light" PHP.wasm bundle and only ship the "kitchen-sink" build. ([#1861](https://github.com/WordPress/wordpress-playground/pull/1861))

### Website

- Restore the single-click "Edit Settings" flow. ([#1854](https://github.com/WordPress/wordpress-playground/pull/1854))
- Restrict CORS proxy to requests from Playground website origin. ([#1865](https://github.com/WordPress/wordpress-playground/pull/1865))
- Restore the single-click "Edit Settings" flow. ([#1854](https://github.com/WordPress/wordpress-playground/pull/1854))
- Restrict CORS proxy to requests from Playground website origin. ([#1865](https://github.com/WordPress/wordpress-playground/pull/1865))

#### Documentation

- Add /release redirect to WP beta/RC blueprint. ([#1866](https://github.com/WordPress/wordpress-playground/pull/1866))
- Add /release redirect to WP beta/RC blueprint. ([#1866](https://github.com/WordPress/wordpress-playground/pull/1866))

### Internal

- Make isomorphic-git submodule use https, not ssh. ([#1863](https://github.com/WordPress/wordpress-playground/pull/1863))
- Make isomorphic-git submodule use https, not ssh. ([#1863](https://github.com/WordPress/wordpress-playground/pull/1863))

### Bug Fixes

- [CLI] Fix `isWordPressInstalled()` in CLI by inlining the auto_login.php in index.ts instead of using import ?raw. ([#1869](https://github.com/WordPress/wordpress-playground/pull/1869))
- [CLI] Fix `isWordPressInstalled()` in CLI by inlining the auto_login.php in index.ts instead of using import ?raw. ([#1869](https://github.com/WordPress/wordpress-playground/pull/1869))

### Various

- Add documentation around the GPL license and implications for contribution. ([#1776](https://github.com/WordPress/wordpress-playground/pull/1776))
- Allow installing Plugins/Themes into an arbitrary folder. ([#1803](https://github.com/WordPress/wordpress-playground/pull/1803))
- Improve documentation. ([#1862](https://github.com/WordPress/wordpress-playground/pull/1862))
- [Website] Fix "undefined" as className. ([#1870](https://github.com/WordPress/wordpress-playground/pull/1870))
- Add documentation around the GPL license and implications for contribution. ([#1776](https://github.com/WordPress/wordpress-playground/pull/1776))
- Allow installing Plugins/Themes into an arbitrary folder. ([#1803](https://github.com/WordPress/wordpress-playground/pull/1803))
- Improve documentation. ([#1862](https://github.com/WordPress/wordpress-playground/pull/1862))
- [Website] Fix "undefined" as className. ([#1870](https://github.com/WordPress/wordpress-playground/pull/1870))

### Contributors

The following contributors merged PRs in this release:

@adamziel @ajotka @bgrgicak @brandonpayton @dd32 @n8finch


## [v0.9.46] (2024-10-07)

### Enhancements
Expand Down
8 changes: 7 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
"recompile:php:web:jspi:7.2": "nx recompile-php:jspi php-wasm-web -- --PHP_VERSION=7.2 ",
"recompile:php:web:jspi:7.1": "nx recompile-php:jspi php-wasm-web -- --PHP_VERSION=7.1 ",
"recompile:php:web:jspi:7.0": "nx recompile-php:jspi php-wasm-web -- --PHP_VERSION=7.0 ",
"recompile:php:web:asyncify:all": "nx recompile-php:asyncify php-wasm-web",
"recompile:php:web:asyncify:all": "nx recompile-php:asyncify:all php-wasm-web",
"recompile:php:web:asyncify:8.3": "nx recompile-php:asyncify php-wasm-web -- --PHP_VERSION=8.3 ",
"recompile:php:web:asyncify:8.2": "nx recompile-php:asyncify php-wasm-web -- --PHP_VERSION=8.2 ",
"recompile:php:web:asyncify:8.1": "nx recompile-php:asyncify php-wasm-web -- --PHP_VERSION=8.1 ",
Expand Down
Loading

0 comments on commit 7fa40be

Please sign in to comment.