mvanduijker / laravel-mercure-broadcaster Goto Github PK
View Code? Open in Web Editor NEWLaravel broadcaster for Mercure
License: MIT License
Laravel broadcaster for Mercure
License: MIT License
Sorry, not an issue. But wanted to drop a message saying we’ll done.
Haven’t tried yet, but reading code online.
I have setup mercure with the docker-compose. It used a self-signed certificate and I don't find how to allow Laravel to send the request with this package.
I have the following error
[previous exception] [object] (Symfony\\Component\\HttpClient\\Exception\\TransportException(code: 0): SSL certificate problem: unable to get local issuer certificate for \"https://localhost/.well-known/mercure\". at C:\\laragon\\www\\premo\\vendor\\symfony\\http-client\\Chunk\\ErrorChunk.php:56)
Here the docker-compose:
# docker-compose.yml
version: "3.7"
services:
mercure:
image: dunglas/mercure
restart: unless-stopped
environment:
# Uncomment the following line to disable HTTPS
# SERVER_NAME: ':1337'
MERCURE_PUBLISHER_JWT_KEY: 'ChangeMe'
MERCURE_SUBSCRIBER_JWT_KEY: 'ChangeMe'
MERCURE_EXTRA_DIRECTIVES: |-
cors_origins *
# Comment / Uncomment - the following line to enable the development mode
command: /usr/bin/caddy run -config /etc/caddy/Caddyfile.dev
ports:
- "80:80"
- "443:443"
volumes:
- caddy_data:/data
- caddy_config:/config
volumes:
caddy_data:
caddy_config:
Why I need SSL on localhost? Because otherwise the browser use Http/1.1 and I have already reached the connection limit. But with Http/2 the limit is a lot more (about 100).
Used versions for testing:
mercure: v0.10
laravel-mercure-broadcaster: v3.1.0
Possible fixes:
version freeze jwt dep?
https://github.com/lcobucci/jwt/releases/tag/3.3.3
Error messages:
HTTP 401 unauthorized
{"level":"info","ts":1606474124.5431101,"logger":"http.handlers.mercure","msg":"Topic selectors not matched or not provided","remote_addr":"x.x.x.x:46442","error":"unable to parse JWT: signature is invalid"}
trigger_error('Implicit conversion of keys from strings is deprecated. Please use InMemory or LocalFileReference classes.', E_USER_DEPRECATED);
More upgrades incoming:
Upgrading docs for incoming php jwt dep:
https://github.com/lcobucci/jwt/blob/master/docs/upgrading.md
It feels like the JWT middleware for private channels should be integrated in the library. I'm happy to contribute a PR, but I wanted to talk about the design first. I think most of the dynamic values could be configured in broadcasting.connections.mercure
, with exception of the subscription URLs. What I'd aim for here is an integration of Laravel routing, so you can pass route IDs in addition to URI patterns:
Route::middleware('mercure.auth:user.notifications,id')->get('/user/{id}', [UserController::class, 'notifications']);
Route::middleware('mercure.auth:https://my.app/users/{id}/notifications')->get('/user/{id}', [UserController::class, 'notifications']);
Route::middleware('mercure.auth:/users/{id}/notifications')->get('/user/{id}', [UserController::class, 'notifications']);
The first middleware identifier, mercure.auth:user.notifications,id
, passes the route name user.notifications
and the request parameter id
. By checking whether a route with that name exists and fetching the parameter from the request, we should be able to build the full subscription URL on behalf of the developer automatically.
In the second case, we receive a full URL with an inline parameter ({id}
). Again, we can use the route parameters to replace the parameter with the actual value from the request (I've done this once for another package and it works well).
In the third case, if the first parameter starts with a slash, we can assume it's an URI, prepend current host and scheme in front of it, then continue as for case two.
All in all, this would make it really easy to use private channels and fit well into the Laravel ecosystem. What do you think?
Hi, I try to implement your package on my app', I want to add channel (in channels.php, but i don't know if it's necessary)
With something like this :
Broadcast::channel('{user}.example', fn() => true);
When run composer install, Laravel throw me an error :
#message: "Call to undefined method Duijker\LaravelMercureBroadcaster\Broadcasting\Broadcasters\MercureBroadcaster::channel()" #code: 0 #file: "./vendor/laravel/framework/src/Illuminate/Broadcasting/BroadcastManager.php"
The "channel()" method refer to "Broadcast::channel()" methods in channels.php.
Are there alternative existing ? Or maybe i can't need to declare a channel, the package do not map model's attribute to change it state in real time ?
Do you have an example of your package implementation in Laravel from scratch ? Thanks
Hello,
This is very likely not an issue with the package but something i'm not doing right ... i've been overthinking this for some times now and would appreciate a fresh look ...
I'm not able to receive events from laravel event broadcasting, not sure why but i feel like im making things too complex. I think i'm doing it wrong.
I'm able to subscribe to event on the frontend using :
var es = new EventSource(`http://${window.location.hostname}:3030/.well-known/mercure?topic=` + encodeURIComponent(`http://expl/profile/${this.profile.id}`), { withCredentials: true });
es.addEventListener('message', (messageEvent) => {
console.log(messageEvent.data);
});
I can publish and get events datas in the browser console by using an Http request client (Insomnia, Postman, ...) but i can't get any event from laravel event broadcasting ...
I've generated my keypair following the Mercure.rocks documentation
docker-compose.yml
mercure:
image: dunglas/mercure
container_name: app-mercure
restart: unless-stopped
environment:
DEBUG: "debug"
SERVER_NAME: ':80'
# MERCURE_TRANSPORT_URL: "bolt://mercure.db"
MERCURE_PUBLISHER_JWT_KEY: |
-----BEGIN PUBLIC KEY-----
M3IBgkq.......GSzhP1............dQ==
-----END PUBLIC KEY-----
MERCURE_PUBLISHER_JWT_ALG: RS256
MERCURE_SUBSCRIBER_JWT_KEY: |
-----BEGIN PUBLIC KEY-----
M3IBgkq........hkiBA..........AQ==
-----END PUBLIC KEY-----
MERCURE_SUBSCRIBER_JWT_ALG: RS256
MERCURE_EXTRA_DIRECTIVES: |-
cors_origins http://example.web:8181
anonymous
ports:
- "3030:80"
networks:
- app-network
.env file (inlined public keys)
BROADCAST_DRIVER=mercure
....
MERCURE_SECRET_SUB="-----BEGIN PUBLIC KEY-----\nM3IBgkq......hkiBA..........AQ==\n-----END PUBLIC KEY-----\n"
MERCURE_SECRET_PUB="-----BEGIN PUBLIC KEY-----\nM3IBgkq.......GSzhP1............dQ=\n-----END PUBLIC KEY-----\n"
MERCURE_URL=http://app-mercure/.well-known/mercure
config/broadcasting.php :
'mercure' => [
'driver' => 'mercure',
'url' => env('MERCURE_URL', 'http://example.web:3030/.well-known/mercure'),
'secret_pub' => env('MERCURE_SECRET_PUB', 'aVerySecretKey'),
'secret_sub' => env('MERCURE_SECRET_SUB', 'aVerySecretKey'),
],
MercureBroadcasterAuthorizationCookie.php middleware (attached to every requests) :
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Cookie;
use Lcobucci\JWT\Configuration;
// use Lcobucci\JWT\Signer\Hmac\Sha256;
use Lcobucci\JWT\Signer;
use Lcobucci\JWT\Signer\Key\LocalFileReference;
use Lcobucci\JWT\Signer\Key\InMemory;
class MercureBroadcasterAuthorizationCookie
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle(Request $request, Closure $next)
{
/** @var Response $response */
$response = $next($request);
if (!method_exists($response, 'withCookie')) {
return $response;
}
if (!\Auth::check()) {
return $response;
}
$user = $request->user();
if (!$user->profile()->exists()) {
return $response;
}
return $response->withCookie($this->createCookie($request->user(), $request->secure()));
}
private function createCookie($user, bool $secure)
{
$subscriptions = [
sprintf("http://expl/profile/%d", $user->profile->id)
];
$jwtConfiguration = Configuration::forAsymmetricSigner(
new Signer\Rsa\Sha256(),
LocalFileReference::file(__DIR__ . '/../../../.docker/mercure/subscriber.key'),
InMemory::plainText(config('broadcasting.connections.mercure.secret_sub'))
);
$token = $jwtConfiguration->builder()
->withClaim('mercure', ['subscribe' => $subscriptions])
->getToken($jwtConfiguration->signer(), $jwtConfiguration->signingKey())
->toString();
return Cookie::make(
'mercureAuthorization',
$token,
15,
'/.well-known/mercure', // or which path you have mercure running
parse_url(config('app.url'), PHP_URL_HOST),
$secure,
true
);
}
}
Modified LaravelMercureBroadcasterServiceProvider (to use asymetric signer) :
<?php declare(strict_types = 1);
namespace Duijker\LaravelMercureBroadcaster;
use Duijker\LaravelMercureBroadcaster\Broadcasting\Broadcasters\MercureBroadcaster;
use Illuminate\Broadcasting\BroadcastManager;
use Illuminate\Support\ServiceProvider;
use Symfony\Component\Mercure\Publisher;
use Lcobucci\JWT\Configuration;
use Lcobucci\JWT\Signer;
use Lcobucci\JWT\Signer\Key\LocalFileReference;
use Lcobucci\JWT\Signer\Key\InMemory;
class LaravelMercureBroadcasterServiceProvider extends ServiceProvider
{
public function boot()
{
$jwtConfiguration = Configuration::forAsymmetricSigner(
new Signer\Rsa\Sha256(),
LocalFileReference::file(__DIR__ . '/../../../../.docker/mercure/publisher.key'),
InMemory::plainText(config('broadcasting.connections.mercure.secret_pub'))
);
$token = $jwtConfiguration->builder()
->withClaim('mercure', ['publish' => ["*"]])
->getToken($jwtConfiguration->signer(), $jwtConfiguration->signingKey())
->toString();
$this->app
->make(BroadcastManager::class)
->extend('mercure', function ($app, array $config) use ($token) {
return new MercureBroadcaster(
new Publisher(
$config['url'],
function () use ($config, $token) {
return (string) $token;
}
)
);
});
}
public function register()
{
}
}
app/Http/Kernel.php (middleware declaration) :
\App\Http\Middleware\MercureBroadcasterAuthorizationCookie::class,
Laravel event :
<?php
namespace App\Events;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use Duijker\LaravelMercureBroadcaster\Broadcasting\Channel;
use App\Models\Message;
class MessageSent implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $message;
public $dest_id;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct(Message $message, int $dest_id)
{
$this->message = $message;
$this->dest_id = $dest_id;
}
/**
* Get the channels the event should broadcast on.
*
* @return \Illuminate\Broadcasting\Channel|array
*/
public function broadcastOn()
{
return new Channel("http://expl/profile/".$this->dest_id, true);
}
}
Dispatching event in a controller :
MessageSent::dispatch($message->load('profile'), $dest_id);
Thanks a lot if someone is able to help !
Thanks @dunglas @mvanduijker for your great work 🚀
Hello,
Thanks a lot for this implementation of mercure broadcaster for laravel.
I just would like to expose an "issue" I've been fighting with while trying to use this in a project.
It looks like by default, or at least when using a pre-built authentication stack, Laravel encrypt all cookies. So for private channels using the cookie middleware, the resulting cookie gets encrypted and cannot be parsed by the Mercure hub.
This resulted in this kind of log entries on the Hub:
mercure_1 | {"level":"info","ts":1645187479.2805336,"logger":"http.handlers.mercure","msg":"Subscriber unauthorized","subscriber":{"id":"urn:uuid:05fcf9f7-8564-45f3-952b-a8abaafadd54","last_event_id":"","remote_addr":"172.21.0.1:48160"},"error":"unable to parse JWT: token contains an invalid number of segments"}
After digging a bit I found out that you can exclude cookie names from Laravel encryption by adding an App/Middleware/EncryptCookies.php
middleware and declare the cookies name to be left unencrypted in it.
<?php
namespace App\Http\Middleware;
use Illuminate\Cookie\Middleware\EncryptCookies as Middleware;
class EncryptCookies extends Middleware
{
/**
* The names of the cookies that should not be encrypted.
*
* @var array<int, string>
*/
protected $except = [
'mercureAuthorization'
];
}
This resolved the issue for me. The client can now subscribe to the private channel.
mercure_1 | {"level":"info","ts":1645189281.517158,"logger":"http.handlers.mercure","msg":"New subscriber","subscriber":{"id":"urn:uuid:5cb3e680-d9a2-4fec-ba0b-a173736024ce","last_event_id":"","remote_addr":"172.21.0.1:48188","topic_selectors":["https://vpanel/user/05a39255-a0ab-4ecd-98dc-708ab9b61ae8/direct-messages"],"topics":["https://vpanel/user/05a39255-a0ab-4ecd-98dc-708ab9b61ae8/direct-messages"]}}
Maybe this should appear somewhere in the README ?
Kind regards
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.