Laravel WebSockets: Subscribing to private channels does not work

Hillcow picture Hillcow · Nov 12, 2019 · Viewed 7.5k times · Source

Software:

In websockets.php (complete file) I have my local_cert and local_pk setup with my certificates. If I leave this option blank I cannot even connect. I also have set verify_peerto false, because if I don't I cannot connect either.

broadcasting.php:

'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'),
            'host' => '127.0.0.1',
            'port' => 6001,
            'scheme' => 'https',
            'curl_options' => [
                CURLOPT_SSL_VERIFYHOST => 0,
                CURLOPT_SSL_VERIFYPEER => 0,
            ]
        ],
    ],

If I get rid of the curl options I get an empty Broadcast exception like described here.

bootstrap.js:

window.Pusher = require('pusher-js');
window.Echo = new Echo({
    broadcaster: 'pusher',
    key: '7d23096ae0ab2d02d220',
    wsHost: window.location.hostname,
    wsPort: 6001,
    wssPort: 6001,
    encrypted: true,
    disableStats: true,
    auth: {
        headers: {
            'X-CSRF-TOKEN': window.App.csrfToken,
        },
    },
})

This is all I get from the logs after running php artisan websockets:serve:

New connection opened for app key 7d23096ae0ab2d02d220.
Connection id 49092664.114416323 sending message {"event":"pusher:connection_established","data":"{\"socket_id\":\"49092664.114416323\",\"activity_timeout\":30}"}

What I should get is messages about listening / joining channels and sending messages etc. But all of that does not work at the moment. I have things like:

Echo.private('notifications.' + this.user.id)
                .listen('UserNotificationSent', (e) => {
                    console.log(e)
                })

Events: UserNotificationSent.php for example.

Of course internally I have everything else setup as well: channels with auth, etc. Everything worked locally on my machine on a lower Laravel version (5.4). But I recently updated to 5.8 and deployed to a server and now I struggle with this.

I also opened an issue on github.

IMPORTANT UPDATE This is actually not due to the deployment, I have the same problem on my local setup. What is interesting is that listening to channels via Echo.channel() works, however, .private() is not working. On Github (link above) I came across a guy who has the exact same problem. We did not find a solution yet.

Answer

senty picture senty · Nov 16, 2019

It happens because of the port 6001 is reserved in nginx on live server (explanation at the bottom). I needed to use reverse-proxy on nginx to make it work - and used port 6002 for websockets in live server.

In nginx (upon request, I added the full nginx code):

server {

  #The nginx domain configurations
  root /var/www/laravel/public;
  index index.html index.htm index.php index.nginx-debian.html;
  server_name example.com www.example.com;

  #WHAT YOU NEED IS FROM HERE...
  location / {
      try_files $uri $uri/ /index.php?$query_string;

      # "But why port 6000, 6002 and 433? Scroll at the bottom"

      proxy_pass                          http://127.0.0.1:6001;
      proxy_set_header Host               $host;
      proxy_set_header X-Real-IP          $remote_addr;

      proxy_set_header X-Forwarded-For    $proxy_add_x_forwarded_for;
      proxy_set_header X-Forwarded-Proto  https;
      proxy_set_header X-VerifiedViaNginx yes;
      proxy_read_timeout                  60;
      proxy_connect_timeout               60;
      proxy_redirect                      off;

      # Specific for websockets: force the use of HTTP/1.1 and set the Upgrade header
      proxy_http_version 1.1;
      proxy_set_header Upgrade $http_upgrade;
      proxy_set_header Connection 'upgrade';
      proxy_set_header Host $host;
      proxy_cache_bypass $http_upgrade;
 }
 #..UNTIL HERE - The rest are classic nginx config and certbot

 #The default Laravel nginx config
 location ~ \.php$ {
      try_files $uri =404;
      fastcgi_split_path_info ^(.+\.php)(/.+)$;
      fastcgi_pass unix:/run/php/php7.2-fpm.sock;
      fastcgi_index index.php;
      fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
      include fastcgi_params;
 }

 #SSL by certbot
 listen [::]:443 ssl ipv6only=on; # managed by Certbot
 listen 443 ssl; # managed by Certbot

 ssl                         on;
 ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; # managed by Certbot
 ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; # managed by Certbot
 include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
 ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot

 ssl_session_cache           shared:SSL:30m;
 ssl_protocols               TLSv1.1 TLSv1.2;

 # Diffie Hellmann performance improvements
 ssl_ecdh_curve              secp384r1;
}

Everything that connects to your domain over TLS will be proxied to a local service on port 6001, in plain text. This offloads all the TLS (and certificate management) to Nginx, keeping your websocket server configuration as clean and simple as possible.

This also makes automation via Let’s Encrypt a lot easier, as there are already implementations that will manage the certificate configuration in your Nginx and reload them when needed. - Source - Mattias Geniar

Echo setup:

let isProduction = process.env.MIX_WS_CONNECT_PRODUCTION === 'true';

Vue.prototype.Echo = new LaravelEcho({
    broadcaster: 'pusher',
    key: process.env.MIX_PUSHER_APP_KEY,
    wssHost: window.location.hostname,
    wssPort: isProduction ? 6002 : 6001,
    wsHost: window.location.hostname,
    wsPort: isProduction ? 6002 : 6001,
    disableStats: false,
    encrypted: isProduction,
    enabledTransports: ['ws', 'wss'],
    disabledTransports: ['sockjs', 'xhr_polling', 'xhr_streaming']
});

In websockets.php

'apps' => [
    [
        'id' => env('MIX_PUSHER_APP_ID'),
        'name' => env('APP_NAME'),
        'key' => env('MIX_PUSHER_APP_KEY'),
        'secret' => env('MIX_PUSHER_APP_SECRET'),
        'enable_client_messages' => false,
        'enable_statistics' => true,
    ],
],

// I kept them null but I use LetsEncrypt for SSL certs too.
'ssl' => [
   'local_cert' => null,
   'local_pk' => null,
   'passphrase' => null,
]

And broadcasting.php

'pusher' => [
     'driver' => 'pusher',
     'key' => env('MIX_PUSHER_APP_KEY'),
     'secret' => env('MIX_PUSHER_APP_SECRET'),
     'app_id' => env('MIX_PUSHER_APP_ID'),
     'options' => [
         'cluster' => env('MIX_PUSHER_APP_CLUSTER'),
         'encrypted' => env('MIX_WS_CONNECT_PRODUCTION'),
         'host' => '127.0.0.1',
         'port' => env('MIX_WS_CONNECT_PRODUCTION') ? 6002 : 6001,
         'scheme' => 'http'
     ],
 ],

This was my full cycle that made it work. Hope it helps.


Quoting from Alex Bouma's explanation:

"But why port 6000, 6002 and 433, what a mess!?"

I hear ya! Let me explain a bit, it will hopefully all make sense afterwards.

Here is the thing, opening an port on your server can only be done by only one application at a time (technically that is not true, but let's keep it simple here). So if we would let NGINX listen on port 6001 we cannot start our websockets server also on port 6001 since it will conflict with NGINX and the other way around, therefore we let NGINX listen on port 6002 and let it proxy (NGINX is a reverse proxy after all) all that traffic to port 6001 (the websockets server) over plain http. Stripping away the SSL so the websockets server has no need to know how to handle SSL.

So NGINX will handle all the SSL magic and forward the traffic in plain http to port 6001 on your server where the websockets server is listening for requests.

The reason we are not configuring any SSL in the websockets.php config and we define the scheme in our broadcasting.php as http and use port 6001 is to bypass NGINX and directly communicate with the websockets server locally without needing SSL which faster (and easier to configure and maintain).