Search code examples
laravelamazon-web-servicessslwebsocketlaravel-websockets

Deploy Laravel Websockets to Amazon EC2 Instance with AWS Certificate Manager to handle SSL


I developed a real time chat app with Laravel Websockets/Websockets API on localhost + Self Signed Certificate to use a SSL in local development. Everything works up to this point. I uploaded everything to the test server + setup supervisord to run both queue and websockets:serve (except the self signed certificate). Since we used AWS Certificate Manager to setup SSL, there is no copy of private key and certificate anywhere. Because of that, I am unable to run websockets in test server.

Below is my setup (very basic)

/etc/httpd/conf.d/project.conf

<VirtualHost *:80>
    ServerAdmin [email protected]
    DocumentRoot "/var/www/html/project/core/public"
<Directory "/var/www/html/project/core">
        Options Indexes FollowSymLinks Includes ExecCGI
        AllowOverride All
        Order allow,deny
        Allow from all
        Require all granted
</Directory>
    ErrorLog "logs/project/errors.log"
    CustomLog "logs/project/access.log" common
</VirtualHost>

/etc/supervisor.conf

[program:laravel-queue]
command=php /var/www/html/project/core/artisan queue:work --sleep=3 --tries=3
process_name=%(program_name)s
numprocs=1      
autostart=true  
autorestart=true  
startsecs=10    
startretries=3      
user=ec2-user
redirect_stderr=true      
stdout_logfile=/var/www/html/project/core/laravel-queue.log

[program:laravel-websockets]
command=php /var/www/html/project/core/artisan websockets:serve --host=127.0.0.1
process_name=%(program_name)s
numprocs=1      
autostart=true  
autorestart=true  
startsecs=10    
startretries=3      
user=ec2-user
redirect_stderr=true      
stdout_logfile=/var/www/html/project/core/laravel-websockets.log

broadcasting.php

'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,
            'encrypted' => true,
            'host' => '127.0.0.1',
            'port' => env('LARAVEL_WEBSOCKETS_PORT'),
            'scheme' => 'https',
            'curl_options' => [
                CURLOPT_SSL_VERIFYHOST => 0,
                CURLOPT_SSL_VERIFYPEER => 0,
            ],
        ],
    ],

websockets.php

'ssl' => [
    'local_cert' => env('LARAVEL_WEBSOCKETS_SSL_LOCAL_CERT', null),
    'local_pk' => env('LARAVEL_WEBSOCKETS_SSL_LOCAL_PK', null),
    'passphrase' => env('LARAVEL_WEBSOCKETS_SSL_PASSPHRASE', null),

.env

PUSHER_APP_ID=12345 
PUSHER_APP_KEY=ABCDEFG 
PUSHER_APP_SECRET=HIJKLMNOP 
PUSHER_APP_CLUSTER=mt1

LARAVEL_WEBSOCKETS_SSL_LOCAL_CERT= 
LARAVEL_WEBSOCKETS_SSL_LOCAL_PK= 
LARAVEL_WEBSOCKETS_SSL_PASSPHRASE="" 
LARAVEL_WEBSOCKETS_PORT=6001 
MIX_LARAVEL_WEBSOCKETS_PORT="${LARAVEL_WEBSOCKETS_PORT}"

Echo Initialization

const Echo = new initEcho({
    broadcaster: "pusher",
    key: process.env.MIX_PUSHER_APP_KEY,
    cluster: process.env.MIX_PUSHER_APP_CLUSTER,
    wsHost: window.location.hostname,
    wsPort: process.env.MIX_LARAVEL_WEBSOCKETS_PORT,
    wssPort: process.env.MIX_LARAVEL_WEBSOCKETS_PORT,
    forceTLS: true,
    encrypted: true,
    enabledTransports: ["ws", "wss"],
    auth: {
        headers: {
            Authorization:
                "Bearer " + access_token,
            Accept: "application/json",
        },
    },
});

Echo.connector.pusher.connection.strategy.transports.ws.transport.manager.livesLeft =
    Infinity;

Echo.connector.pusher.connection.strategy.transports.wss.transport.manager.livesLeft =
    Infinity;

Echo.connector.pusher.connection.bind("state_change", function (states) {
    // state change
});

const channel = Echo.join("chat-message")
    .here(() => {
        console.log("chat channel");
    })
    .joining((event) => {
        // console.log({ event }, "joining");
    })
    .leaving((event) => {
        // console.log({ event }, "leaving");
    })
    .listenForWhisper("typing", (event) => {
        // console.log({ event }, "listenForWhisper");
    })
    .listen(".chat-message", (event) => {
        // console.log({ event }, "listen");
    });

EDIT

A lot of SO Q&A said something about setting up Application Load Balancers, SSL Termination, etc (Don't really know about this) so we tried to do the following based on this aws document and changed 443 to 6001 (ws port number) and protocol to either http or https. Still the same issue.

UPDATE We also tried to do this aws add HTTPS listener. Still the same unable to listen to port.

My knowledge with AWS and its services is very limited (I was not the one to setup AWS ec2 and ACM) so if the solution is within aws, please teach me as simple as possible.


Solution

  • I was able to work the code after almost 2 weeks. With this setup, you no longer need to add the private key and certificate in .env file.

    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'),
            'useTLS' => true,
            #### ADD HERE
            'encrypted' => false,
            'host' => '127.0.0.1',
            'port' => env('LARAVEL_WEBSOCKETS_PORT'),
            'scheme' => 'http',
            ### ADD HERE
        ],
    ],
    

    bootstrap.js

    const Echo = new Echo({
        broadcaster: "pusher",
        key: process.env.MIX_PUSHER_APP_KEY,
        cluster: process.env.MIX_PUSHER_APP_CLUSTER,
        wsHost: window.location.hostname,
        // This is important
        wsPort: 80,
        wssPort: 443,
        forceTLS: true,
        encrypted: true,
        enabledTransports: ["ws", "wss"],
        // This is important
    
        // optional if you are using jwt
        auth: {
            headers: {
                Authorization:
                    "Bearer " + access_token,
                Accept: "application/json",
            },
        },
        // optional if you are using jwt
    });
    
    
    // Socket will try to reconnect indefinetly with this. If not added, echo will only try to reconnect twice as default
    Echo.connector.pusher.connection.strategy.transports.ws.transport.manager.livesLeft =
        Infinity;
    
    Echo.connector.pusher.connection.strategy.transports.wss.transport.manager.livesLeft =
        Infinity;
    // Socket will try to reconnect indefinetly with this. If not added, echo will only try to reconnect twice as default
    

    Use this Apache Proxy Pass if you are using the Custom WebSocket Handlers

    ProxyRequests off
    ProxyVia on
    RewriteEngine On
    
    RewriteEngine On
    RewriteCond %{HTTP:Connection} Upgrade [NC]
    RewriteCond %{HTTP:Upgrade} websocket [NC]
    RewriteRule /(.*) ws://127.0.0.1:6001/$1 [P,L]
    
    ProxyPass               /ws/chat http://127.0.0.1:6001/ws/chat
    ProxyPassReverse        /ws/chat http://127.0.0.1:6001/ws/chat