bentools / mercure-php-hub
A PHP Implementation of the Mercure Hub protocol.
Installs: 35
Dependents: 0
Suggesters: 0
Security: 0
Stars: 21
Watchers: 6
Forks: 1
Open Issues: 2
Type:project
Requires
- php: >=7.4
- ext-json: *
- ext-mbstring: *
- ext-pcntl: *
- bentools/psr7-request-matcher: ^1.1
- bentools/querystring: ^1.0
- clue/block-react: ^1.4
- clue/redis-react: ^2.4
- lcobucci/jwt: 3.3.*
- predis/predis: ^1.1
- psr/http-server-middleware: ^1.0
- psr/log: ^1.1
- ramsey/uuid: ^4.0
- react/http: ^1.0.0
- rize/uri-template: ^0.3.2
- symfony/console: ^5.2
- symfony/dotenv: ^5.2
- symfony/flex: ^1.11.0
- symfony/framework-bundle: ^5.2
- symfony/string: ^5.2
- symfony/yaml: ^5.2
Requires (Dev)
- ext-dom: *
- bentools/cartesian-product: ^1.3.1
- bentools/iterable-functions: ^1.4
- bentools/shh: ^1.0
- clue/reactphp-eventsource: dev-master#e356b73fbf54a491c37d6b571ee5245206cbdc27
- friendsofphp/php-cs-fixer: ^2.16
- pestphp/pest: ^0.3.19
- phpstan/phpstan: ^0.12.25
- phpunit/phpunit: ^9.0
- ringcentral/psr7: ^1.3
- squizlabs/php_codesniffer: ^3.5
- symfony/http-client: ^5.2
- symfony/process: ^5.2
- symfony/var-dumper: ^5.2
This package is auto-updated.
Last update: 2021-12-18 13:00:05 UTC
README
Mercure PHP Hub
This POC was a PHP implementation of the Mercure Hub Specification.
This repository will no longer be maintained, as I recently released Freddie, which is a brand new implementation leveraging Framework X (still using ReactPHP under the hood).
Installation
PHP 7.4+ (and Redis, or a Redis instance, if using the Redis transport) is required to run the hub.
composer create-project bentools/mercure-php-hub:dev-master
Usage
./bin/mercure --jwt-key=\!ChangeMe\!
You can use environment variables (UPPER_SNAKE_CASE) to replace CLI options for better convenience.
The hub will also check for the presence of a /etc/mercure/mercure.env
file,
then make use of the Symfony DotEnv component to populate variables.
Check out configuration.php for full configuration options.
Advantages and limitations
This implementation does not provide SSL nor HTTP2 termination, so you'd better put a reverse proxy in front of it.
Example with nginx:
upstream mercure { server 127.0.0.1:3000; } server { listen 443 ssl http2; listen [::]:443 ssl http2; server_name example.com; ssl_certificate /etc/ssl/certs/example.com/example.com.cert; ssl_certificate_key /etc/ssl/certs/example.com/example.com.key; ssl_ciphers EECDH+CHACHA20:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5; location /.well-known/mercure { proxy_pass http://mercure; proxy_read_timeout 24h; proxy_http_version 1.1; proxy_set_header Connection ""; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Proto $scheme; } }
By default, the hub will run as a simple event-dispatcher. It can fit common needs for a basic usage, but is not scalable (opening another process won't share the same event emitter).
On the other hand, you can launch the hub on multiple ports and/or multiple servers with a Redis transport (as soon as they share the same Redis instance) and leverage a load-balancer to distribute the traffic. This is currently not possible with the open-source Go implementation of the hub because of concurrency restrictions on the bolt transport.
upstream mercure { # 4 instances on 10.1.2.3 server 10.1.2.3:3000; server 10.1.2.3:3001; server 10.1.2.3:3002; server 10.1.2.3:3003; # 4 instances on 10.1.2.4 server 10.1.2.4:3000; server 10.1.2.4:3001; server 10.1.2.4:3002; server 10.1.2.4:3003; }
./bin/mercure --transport-url="redis://localhost" --jwt-key=\!ChangeMe\!
Benchmarks
Simulated 1 / 100 /1000 subscribers on server A, 1 publisher blasting messages on server B, Hub on server C (and D for the latter).
Implementation | Transport | Servers | Nodes | 1 subscriber | 100 subscribers | 1000 subscribers |
---|---|---|---|---|---|---|
Mercure.rocks GO Hub | Bolt | 1 | 1 | 361 / 286 | 129 / 4989 | 142 / 682 |
ReactPHP implementation | PHP | 1 | 1 | 860 / 295 | 519 / 4526 | 45 / 322 |
ReactPHP implementation | Redis (local) | 1 | 1 | 1548 / 411 | 393 / 5861 | 112 / 777 |
ReactPHP implementation | Redis (local) | 1 | 4 | 108 / 76 | 61 / 2852 | 61 / 688 |
ReactPHP implementation | Redis (shared) | 2 | 8 | 3035 / 144 | 1183 / 7864 | 708 / 2698 |
Units are POSTs (publish) / s
/ Received events / s
(total for all subscribers).
Nodes are the total number of ReactPHP open ports.
Hub was hosted on cheap server(s): 2GB / 2 CPU VPS (Ubuntu 20.04). You could probably reach a very high level of performance with better-sized servers and dedicated CPUs.
Feature coverage
Feature | Covered |
---|---|
JWT through Authorization header |
✅ |
JWT through mercureAuthorization Cookie |
✅ |
Different JWTs for subscribers / publishers | ✅ |
Allow anonymous subscribers | ✅ |
CORS | ✅ |
Private updates | ✅ |
URI Templates for topics | ✅ |
Health check endpoint | ✅ |
HMAC SHA256 JWT signatures | ✅ |
RS512 JWT signatures | ✅ |
Environment variables configuration | ✅ |
Custom message IDs | ✅ |
Last event ID | ✅️ (except: earliest on REDIS transport) |
Customizable event type | ✅️ |
Customizable retry directive |
✅️ |
Logging | ❌ (WIP)️ |
Metrics | ❌ (WIP)️ |
Subscription events | ❌️ |
Subscription API | ❌️ |
Configuration w/ config file | ❌️ |
Payload | ❌️ |
Heartbeat | ❌️ |
Forwarded / X-Forwarded-For headers |
❌️ |
Alternate topics | ❌️ |
Additional features
This implementation provides features which are not defined in the original specification.
Subscribe / Publish topic exclusions
Mercure leverages URI Templates to grant subscribe and/or publish auhorizations on an URI pattern basis:
{ "mercure": { "publish": [ "https://example.com/items/{id}" ], "subscribe": [ "https://example.com/items/{id}" ] } }
However, denying access to a specific URL matching an URI template requires you to explicitely list authorized items:
{ "mercure": { "publish": [ "https://example.com/items/1", "https://example.com/items/2", "https://example.com/items/4" ], "subscribe": [ "https://example.com/items/1", "https://example.com/items/2", "https://example.com/items/4" ] } }
When dealing with thousands of possibilities, it can quicky become a problem. The Mercure PHP Hub allows you to specify
denylists through the publish_exclude
and subscribe_exclude
keys, which accept any topic selector:
{ "mercure": { "publish": [ "https://example.com/items/{id}" ], "publish_exclude": [ "https://example.com/items/3" ], "subscribe": [ "https://example.com/items/{id}" ], "subscribe_exclude": [ "https://example.com/items/3" ] } }
Json Web Token Generator
You can generate a JWT to use on the hub from the command-line:
./bin/mercure jwt:generate
It will ask you interactively what topic selectors you want to allow/deny for publishing/subscribing, and ask you for an optional TTL.
If you want a raw output (to pipe the generated JWT for instance), use the --raw
option.
To disable interaction, you can use the following example:
./bin/mercure jwt:generate --no-interactive --publish=/foo/{id} --publish=/bar --publish-exclude=/foo/bar
It will use your JWT keys environment variables, or you can use the --jwt-key
, --publisher-jwt-key
, --subscriber-jwt-key
options.
For a full list of available options, run this:
./bin/mercure jwt:generate -h
Tests
This project is covered with Pest tests. Coverage has to be improved: feel free to contribute.
composer tests:run
Contribute
If you want to improve this project, feel free to submit PRs:
- CI will yell if you don't follow PSR-12 coding standards
- In the case of a new feature, it must come along with tests
- PHPStan analysis must pass at level 5
You can run composer ci:check
before committing to ensure all CI requirements are successfully met.
License
GNU General Public License v3.0.