Laravel WebSockets Server with Valet and SSL

vor 1 Monat

Preamble

Thanks to Marcel Pociot et. al. for the awesome Laravel package. Thanks to DevMarketer for the step-by-step instructions.

Why, though?

So, I've wanted to try broadcasting/real-time notifications with Laravel Echo and Pusher for the longest time. But working mostly on internal applications that should not rely on external service I was SOL. After Marcel gave a great talk on "Understanding Laravel broadcasting" at Laracon Online Winter 2021 I was more than ready to plunge into the topic.

So I opened up a new terminal window, entered laravel new websockets (for creating a fresh Laravel app), and followed the step-by-step instructions in the docs available at https://beyondco.de/docs/laravel-websockets/.

I don't know where I messed up but nothing worked as expected. Turns out, it was my local setup with Valet and its self-signed SSL certificates (it's 2021, who uses HTTP anymore, am I right?)

So here's my guide on how to run your own local WebSockets server with Valet and SSL.

Let's go!

Prerequisites

I'm running Laravel Valet 2.14.1 and PHP 8.0.3

Setup

Starting from scratch, let's create a brand new Laravel project (Laravel 8.36.2 at the time of writing)

$ laravel new websockets

Fresh SSL certificate, anyone?

$ cd websockets
$ valet secure
Restarting nginx...
The [websockets.test] site has been secured with a fresh TLS certificate.

Let's change the app's URL in .env

APP_URL=https://websockets.test

We need to pull in the beyondcode/laravel-websockets package

composer require beyondcode/laravel-websockets

Now let's publish the migration (for statistics) and the config file

$ php artisan vendor:publish --provider="BeyondCode\LaravelWebSockets\WebSocketsServiceProvider" --tag="migrations"

Let's publish the websockets config file:

$ php artisan vendor:publish --provider="BeyondCode\LaravelWebSockets\WebSocketsServiceProvider" --tag="config"

Run the migrations (you should set up your database beforehand and update .env accordingly)

$ php artisan migrate

Configuration

Now that we're ready to start, let's have a look at our config files. First, we need to change BROADCAST_DRIVER in .env

...

BROADCAST_DRIVER=pusher

...

PUSHER_APP_ID=1 // this can be anything you want
PUSHER_APP_KEY=some-key // this can also be anything you want
PUSHER_APP_SECRET=some-secret // also this
PUSHER_APP_CLUSTER=mt1 // doesn't matter

"Why pusher?" you may ask yourself. Don't I want to not use external services? You're right and we are NOT using pusher. Since laravel-websockets uses the same API as pusher we're using the pusher driver.

Now, in config/broadcasting.php find connections:

'connections' => [

    'pusher' => [
        'driver' => 'pusher',
        'key' => env('PUSHER_APP_KEY'),
        'secret' => env('PUSHER_APP_SECRET'),
        'app_id' => env('PUSHER_APP_ID'),
        'options' => [
            'cluster' => env('PUSHER_APP_CLUSTER'),
            'useTLS' => true,
            'host' => '127.0.0.1', // add this
            'port' => 6001, // and this
            'encrypted' => true, // also this
            'scheme' => 'https', // and this
            'curl_options' => [ // since we're only doing stuff locally this is fine
                CURLOPT_SSL_VERIFYHOST => 0,
                CURLOPT_SSL_VERIFYPEER => 0,
            ],
        ],

Almost done! Let's have a look at config/websockets.php

'ssl' => [
    /*
     * Path to local certificate file on filesystem. It must be a PEM encoded file which
     * contains your certificate and private key. It can optionally contain the
     * certificate chain of issuers. The private key also may be contained
     * in a separate file specified by local_pk.
     */
    'local_cert' => env('LARAVEL_WEBSOCKETS_SSL_LOCAL_CERT', null),

    /*
     * Path to local private key file on filesystem in case of separate files for
     * certificate (local_cert) and private key.
     */
    'local_pk' => env('LARAVEL_WEBSOCKETS_SSL_LOCAL_PK', null),

We need to set those environment variables in our .env file for SSL to work.

In .env add the following entries:

LARAVEL_WEBSOCKETS_SSL_LOCAL_CERT=/absolute/path/to/your/crt/file
LARAVEL_WEBSOCKETS_SSL_LOCAL_PK=/absolute/path/to/your/key/file

Depending on your setup, those files will reside in ~/.config/valet/Certifificates, in my case it would look like this

LARAVEL_WEBSOCKETS_SSL_LOCAL_CERT=/Users/maze/.config/valet/Certificates/websockets.test.crt
LARAVEL_WEBSOCKETS_SSL_LOCAL_PK=/Users/maze/.config/valet/Certificates/websockets.test.key

Now we're ready to fire up the WebSockets server with

$ php artisan websockets:serve

If everything went right you should see the following output in your terminal

Starting the WebSocket server on port 6001...

Open up a browser and head to the WebSockets dashboard located at https://websockets.test/laravel-websockets, and click on Connect.

You should see a few events of type subscribed which means that the server is running. Your terminal will also be flooded with debugging messages.

Awesome right? But wait, there's more! How about we connect the frontend to popup an alert window every time an event gets dispatched? This would be super annoying to users, but for the sake of the example good enough.

Frontend

We need to pull in two NPM packages

$ npm install --save-dev laravel-echo pusher-js

Again, a Pusher package? Well, yes, same API, remember?

Now let's setup Laravel Echo in resources/js/bootstrap.js:

import Echo from 'laravel-echo';

window.Pusher = require('pusher-js');

window.Echo = new Echo({
    broadcaster: 'pusher',
    key: process.env.MIX_PUSHER_APP_KEY,
    cluster: process.env.MIX_PUSHER_APP_CLUSTER,
    forceTLS: true,
    wsHost: window.location.hostname,
    wssPort: 6001,
});

Compile your frontend assets:

$ npm run dev

Since we're using a vanilla Laravel installation we'll just add some JS to the welcome view located in resources/views/welcome.blade.php:

        <script src="{{ mix('js/app.js') }}"></script>
        <script>
            Echo.channel('home')
                .listen('Message', (e) => alert(e.message));
        </script>
    </body>
</html>

This will listen on the home channel and will alert the message of the event that broadcasts to this channel. Speaking of events, we yet need to create one, so

$ php artisan make:event Message

app/Events/Message.php should look like this

<?php

namespace App\Events;

use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class Message implements ShouldBroadcast // this needs to be added
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public $message; // our message

    public function __construct(string $message)
    {
        $this->message = $message;
    }

    public function broadcastOn()
    {
        return new Channel('home');
    }
}

This event will broadcast on the home channel after being dispatched, and Laravel Echo will listen for that event being dispatched.

Now, open a browser window and point it to the index page of our local application, located at https://websockets.test, you should see the welcome page.

In your terminal open up Laravel Tinker. We're using Tinker to quickly dispatch an event without writing a whole lot of application logic.

$ php artisan tinker
Psy Shell v0.10.8 (PHP 8.0.3 — cli) by Justin Hileman
>>>

and fire the event by hand

$ php artisan tinker
Psy Shell v0.10.8 (PHP 8.0.3 — cli) by Justin Hileman
>>> App\Events\Message::dispatch('this is awesome')

Now when you press enter, the event will be dispatched and you should see an alert pop up in your browser. We're done! Wasn't that hard, wasn't it?

Where to go from here?

You can check out the code for this example at https://github.com/mazedlx/laravel-websockets-example.

Don't forget to have a look at Pusher and Ably, they both offer generous free plans.

Also, head over to https://github.com/beyondcode/laravel-websockets and star the repo and follow Marcel Pociot on Twitter.

If you want to see a real-life example, my good friend Christoph Rumpel wrote a blog post not too long ago covering real-time notifications using the Larvel Websockets package.