Laravel websockets+Soketi at production

652 views Asked by At

My Backend and Soketi servers run in same docker environment. I configure it with http and it works fine but now I want to use these services in production with ssl.

My backend placed behind Nginx and all http connections are redirected to 443 https port. So after I change laravel-websockets(pusher), Soketi and frontend code to use https I got error from backend code: cURL error 60: SSL certificate problem: unable to get local issuer certificate. Frontend code is abble to connect to soketi and subscribe to desired private channel.

Is it a way to continue use http to communicate between Backend and Soketi placed in same docker environment? If no what should I change in my configuration to wire my backend properly with https Soketi server?

app.js(Soketi connection and subscribe is ok):

import Echo from 'laravel-echo'
import Pusher from 'pusher-js'
window.Pusher = Pusher
window.Echo = new Echo({
    broadcaster: 'pusher',
    key: import.meta.env.VITE_PUSHER_APP_KEY,
    cluster: import.meta.env.VITE_PUSHER_APP_CLUSTER,
    forceTLS: false,
    encrypted: false,
    disableStats: true,
    wsHost: window.location.hostname,
    wsPort: 6007, 
    wssPort: 6007, 
    enabledTransports: ['ws', 'wss'], 
})

Backend code(connection error: cURL error 60: SSL certificate problem: unable to get local issuer certificate (see https://curl.haxx.se/libcurl/c/libcurl-errors.html) for https://soketi:6001/apps/myapp/events?auth_key=qwerty3&auth_timestamp=1698669966&auth_version=1.0&body_md5=f0ee1708e294c27b9200b1b942b4ad1d&auth_signature=b2648472a55b21d70b2f14a1e17s7ce39bcaeff7ba6afdsb256262ed05df79d12):

class RestoreHistoryFromBlockchainJobStatus implements ShouldBroadcastNow
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    /**
     * Create a new event instance.
     *
     * @return void
     */
    public function __construct(
        public Token $token,
        public int $userId,
        public $result
    ) {}

    public function broadcastAs()
    {
        return 'RestoreHistoryFromBlockchainJobStatus';
    }

    /**
     * Get the channels the event should broadcast on.
     *
     * @return \Illuminate\Broadcasting\Channel|array
     */
    public function broadcastOn()
    {
        return new PrivateChannel('retroshooting.'.$this->userId);
    }
}

 RestoreHistoryFromBlockchainJobStatus::dispatch($this->token, $this->user->id, $action);

.env:

PUSHER_HOST=soketi
PUSHER_APP_ID=myapp
PUSHER_APP_KEY='qwerty'
PUSHER_APP_SECRET='qwerty'
PUSHER_PORT=6001
SOKETI_PORT=6007
PUSHER_APP_CLUSTER=mt1
PUSHER_SCHEME=https

docker-compose.yml:

### NGINX Server #########################################
    nginx:
      build:
        context: ./nginx
        args:
          - CHANGE_SOURCE=${CHANGE_SOURCE}
          - PHP_UPSTREAM_CONTAINER=${NGINX_PHP_UPSTREAM_CONTAINER}
          - PHP_UPSTREAM_PORT=${NGINX_PHP_UPSTREAM_PORT}
          - http_proxy
          - https_proxy
          - no_proxy
      volumes:
        - ${APP_CODE_PATH_HOST}:${APP_CODE_PATH_CONTAINER}${APP_CODE_CONTAINER_FLAG}
        - ${NGINX_HOST_LOG_PATH}:/var/log/nginx
        - ${NGINX_SITES_PATH}:/etc/nginx/sites-available
        - ${NGINX_SSL_PATH}:/etc/nginx/ssl
        - ./certbot/letsencrypt/:/var/www/letsencrypt
        - ../data/certbot/certs/:/var/certs
      ports:
        - "${NGINX_HOST_HTTP_PORT}:80"
        - "${NGINX_HOST_HTTPS_PORT}:443"
        - "${VARNISH_BACKEND_PORT}:81"
      depends_on:
        - php-fpm
      networks:
        - frontend
        - backend
### Soketi Server ##############################################
    soketi:
      build:
        context: ./soketi
      volumes:
        - ./soketi/config.json:/app/bin/config.json:ro
        - ../data/certbot/certs/:/var/certs # for websockets
      environment:
          SOKETI_DEBUG: '0'
          SOKETI_METRICS_SERVER_PORT: '${SOKETI_METRICS_SERVER_PORT}'
          SOKETI_DEFAULT_APP_ID: '${SOKETI_DEFAULT_APP_ID}'
          SOKETI_DEFAULT_APP_KEY: '${SOKETI_DEFAULT_APP_KEY}'
          SOKETI_DEFAULT_APP_SECRET: '${SOKETI_DEFAULT_APP_SECRET}'
          SOKETI_SSL_CERT: '${SOKETI_SSL_CERT}'
          SOKETI_SSL_KEY: '${SOKETI_SSL_KEY}'
          SOKETI_SSL_PASS: '${SOKETI_SSL_PASS}'
          SOKETI_SSL_CA: '${SOKETI_SSL_CA}'
      ports:
        - "${SOKETI_PORT}:6001"
        - "${SOKETI_METRICS_SERVER_PORT}:${SOKETI_METRICS_SERVER_PORT}"
      networks:
        - frontend
        - backend

broadcasting.php:

'pusher' => [
        'driver' => 'pusher',
        'key' => env('PUSHER_APP_KEY', 'app-key'),
        'secret' => env('PUSHER_APP_SECRET', 'app-secret'),
        'app_id' => env('PUSHER_APP_ID', 'app-id'),
        'options' => [
            'host' => env('PUSHER_HOST', '127.0.0.1'),
            'port' => env('PUSHER_PORT', 6001),
            'scheme' => env('PUSHER_SCHEME', 'http'),
            'encrypted' => true,
            'useTLS' => env('PUSHER_SCHEME') === 'https',
        ],
    ],

nginx.conf:

server {
    listen 80 default_server;

    server_name _;

    return 301 https://$host$request_uri;
}

server {

    listen 443 ssl;
    listen [::]:443 ssl ipv6only=on;
    ssl_certificate /var/certs/www-cert1.pem;
    ssl_certificate_key /var/certs/www-privkey1.pem;
    
    server_name my-domain.net;
    root /var/www/public;
    index index.php index.html index.htm;

    location / {
         try_files $uri $uri/ /index.php$is_args$args;
    }

    location ~ \.php$ {
        try_files $uri /index.php =404;
        fastcgi_pass php-upstream;
        fastcgi_index index.php;
        fastcgi_buffers 16 16k;
        fastcgi_buffer_size 32k;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        #fixes timeouts
        fastcgi_read_timeout 600;
        include fastcgi_params;
    }

    location ~ /\.ht {
        deny all;
    }

    location /.well-known/acme-challenge/ {
        root /var/www/letsencrypt/;
        log_not_found off;
    }

    error_log /var/log/nginx/laravel_error.log;
    access_log /var/log/nginx/laravel_access.log;
}

I tried to continue use http in my backend broadcasting(PUSHER_SCHEME=http in .env) and got error with empty response:

 Pusher error: cURL error 52: Empty reply from server (see https://curl.haxx.se/libcurl/c/libcurl-errors.html) for http://soketi:6001/apps/myapp/events?auth_key=qwerty&auth_timestamp=1698693715&auth_version=1.0&body_md5=ac1e69bdf7a6538901fa1d46&auth_signature=aec99de9ffb5sajkkldvjsv109504edad0a. {"userId":1,"exception":"[object] (Illuminate\\Broadcasting\\BroadcastException(code: 0): Pusher error: cURL error 52: Empty reply from server
1

There are 1 answers

0
RomanKovalev On

It is because pusher-php-server use curl under the hood that verify host of certificate by default. I use public certificate on my Soketi server so it fails to verify by curl when I try to connect via docker locally.

I solved it by adding client_options to pusher connection in broadcasting.php. It forces BroadcastManager to create Pusher instance with a custom GuzzleClient that takes into account 'verify' option. So full broadcasting.php pusher connection looks like:

 'connections' => [
        
        'pusher' => [
            'driver' => 'pusher',
            'key' => env('PUSHER_APP_KEY', 'app-key'),
            'secret' => env('PUSHER_APP_SECRET', 'app-secret'),
            'app_id' => env('PUSHER_APP_ID', 'app-id'),
            'options' => [
                'host' => env('PUSHER_HOST', '127.0.0.1'),
                'port' => env('PUSHER_PORT', 6001),
                'scheme' => env('PUSHER_SCHEME', 'http'),
                'encrypted' => true,
                'useTLS' => env('PUSHER_SCHEME') === 'https'
            ],
            'client_options' => [
                'verify' => false, 
            ]
        ],