/freddie

A Mercure Hub, written in PHP (8.1 + ReactPHP)

Primary LanguagePHP

Application Coverage

Freddie

Freddie is a PHP implementation of the Mercure Hub Specification.

It is blazing fast, built on the shoulders of giants:

See what features are covered and what aren't (yet) here.

Installation

PHP 8.1 is required to run the hub.

As a standalone Mercure hub

composer create-project freddie/mercure-x freddie && cd freddie
bin/freddie

This will start a Freddie instance on 127.0.0.1:8080, with anonymous subscriptions enabled.

You can publish updates to the hub by generating a valid JWT signed with the !ChangeMe! key with HMAC SHA256 algorithm.

To change these values, see Security.

As a bundle of your existing Symfony application

composer req freddie/mercure-x

You can then start the hub by doing:

bin/console freddie:serve

You can override relevant env vars in your .env.local and services in your config/services.yaml as usual.

Then, you can inject Freddie\Hub\HubInterface in your services so that you can call $hub->publish($update), or listening to dispatched updates in a CLI context 👍

Keep in mind this only works when using the Redis transport.

⚠️ Freddie uses its own routing/authentication system (because of async / event loop).

The controllers it exposes cannot be imported in your routes.yaml, and get out of your security.yaml scope.

Usage

./bin/freddie

It will start a new Mercure hub on 127.0.0.1:8080. To change this address, use the X_LISTEN environment variable:

X_LISTEN="0.0.0.0:8000" ./bin/freddie

Security

The default JWT key is !ChangeMe! with a HS256 signature.

You can set different values by changing the environment variables (in .env.local or at the OS level): X_LISTEN, JWT_SECRET_KEY, JWT_ALGORITHM, JWT_PUBLIC_KEY and JWT_PASSPHRASE (when using RS512 or ECDSA)

Please refer to the authorization section of the Mercure specification to authenticate as a publisher and/or a subscriber.

PHP Transport (default)

By default, the hub will run as a simple event-dispatcher, in a single PHP process.

It can fit common needs for a basic usage, but using this transport prevents scalability, as opening another process won't share the same event emitter.

It's still prefectly usable as soon as :

  • You don't expect more than a few hundreds updates per second
  • Your application is served from a single server.

Redis transport

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 optionally use a load-balancer to distribute the traffic.

The official open-source version of the hub doesn't allow scaling because of concurrency restrictions on the bolt transport.

To launch the hub with the Redis transport, change the TRANSPORT_DSN environment variable:

TRANSPORT_DSN="redis://127.0.0.1:6379" ./bin/freddie

Alternatively, you can set this variable into .env.local.

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 Nginx configuration

upstream freddie {
    # Example with a single node
    server 127.0.0.1:8080;

    # Example with several nodes (they must share the same Redis instance)
    # 2 instances on 10.1.2.3
    server 10.1.2.3:8080;
    server 10.1.2.3:8081;

    # 2 instances on 10.1.2.4
    server 10.1.2.4:8080;
    server 10.1.2.4:8081;
}

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://freddie;
        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;
    }
}

Example Caddy configuration

Single node

example.com

reverse_proxy 127.0.0.1:8080

With multiple nodes

example.com

reverse_proxy 10.1.2.3:8080 10.1.2.3:8081 10.1.2.4:8080 10.1.2.4:8081

Payload limitations

⚠ There's a known limit in Framework-X which prevents request bodies to weigh more than 64 KB. At the time of writing, this limit cannot be raised due to Framework-X encapsulating HTTP Server instantiation.

Publishing bigger updates to Freddie (through HTTP, at least) could result in 400 errors.

Feature coverage

Feature Covered
JWT through Authorization header
JWT through mercureAuthorization Cookie
Allow anonymous subscribers
Alternate topics ✅️
Private updates
URI Templates for topics
HMAC SHA256 JWT signatures
RS512 JWT signatures
Environment variables configuration
Custom message IDs
Last event ID (including earliest) ✅️
Customizable event type ✅️
Customizable retry directive ✅️
CORS ❌ (configure them on your web server)
Health check endpoint ❌ (PR welcome)
Logging ❌ (PR welcome))️
Metrics ❌ (PR welcome)️
Different JWTs for subscribers / publishers ❌ (PR welcome)
Subscription API ❌️ (TODO)

Tests

This project is 100% covered with Pest tests.

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 8

You can run the following command before committing to ensure all CI requirements are successfully met:

composer ci:check

License

GNU General Public License v3.0.