beyondcode / laravel-websockets

Websockets for Laravel. Done right.
https://beyondco.de/docs/laravel-websockets
MIT License
5.07k stars 613 forks source link

Help with deployment on Laravel Forge w/ let's encrypt #30

Closed eldair closed 5 years ago

eldair commented 5 years ago

Hello,

Could someone help with how to configure the app to work on laravel forge w/ let's encrypt? The app would host it's own websocket, so it's not a separate subdomain.

sudden-break commented 5 years ago

I have the same situation. I got it working on local with my self signed certs from valet, but can't get it working on my forge server. I also tried to open port 6001 on the forge firewall setup, but: "WebSocket connection to 'wss://xxx:6001/app/a8f4975fa8b8dd5da1ca?protocol=7&client=js&version=4.3.1&flash=false' failed: WebSocket opening handshake timed out"

coolcodemy commented 5 years ago

Disclosure, I am a RunCloud owner. Since I saw this package, we have modified our nginx config to allow a user to add their own proxy setting. In this case, Let's Encrypt will work on port 443 which proxying port 6001. I think you can suggest forge to allow this kind of modification.

pimski commented 5 years ago

Hey @Eldair and @sudden-break

I'm still trying to get things working locally (windows + homestead). I got a similar error as you guys. So I tried to connect to my local homestead server on port 6001 using telnet. This didn't work. However, if I run the sockets server on my "external" ip address I am able to connect:

artisan websockets:serve --host=192.168.10.10

I am getting other errors now, but that's inside php code, so getting closer... Also, I don't know the implications of running it this way since I'm not a server specialist. So I'll have to read up on server stuff I guess :-)

eldair commented 5 years ago

I think that the easiest solution would be to replace forge's current let's encrypt system with certbot, which would let us have a permanent cert. path instead of the current (ever changing) one.

stayallive commented 5 years ago

I think the easiest method is to add a new host to forge (socket.yourdomain.tld for example) which has it's own cert and add the nginx rules to it to proxy the traffic from your server running on port 6001 (plain, no https) and let nginx do all the https stuff: https://docs.beyondco.de/laravel-websockets/1.0/basic-usage/ssl.html#usage-with-a-reverse-proxy-like-nginx

The webroot for this webhost can be empty since you will be replacing it with a reverse proxy config (serving the websockets server) instead of serving content from the webroot, it also does not have to point to your apps webroot.

Replae the location / in the Forge nginx config for that new domain/subdomain with (assuming you are running the websocket server on port 6001 which should be the default):

  location / {
    proxy_pass             http://127.0.0.1:6001;
    proxy_set_header Host  $host;
    proxy_read_timeout     60;
    proxy_connect_timeout  60;
    proxy_redirect         off;

    # Allow the use of websockets
    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;
  }

This way Forge can manage the certificate itself and you don't have to worry about it 👍 (and also don't have to open any special ports on the server).

Your app can use 127.0.0.1:6001 to connect (plain, no https) to the server to submit it's broadcasts (throught the HTTP API) and your app can use socket.yourdomain.tld:443 (with ssl) from Pusher.JS/Laravel Echo.

acacha commented 5 years ago

Hello,

One question: how can I test I'have configured Reverse Proxy ok? For example with following config:

$ cat /etc/nginx/sites-enabled/00_socket.scool.cat
server {
  listen        443 ssl;
  listen        [::]:443 ssl;
  server_name   socket.scool.cat;

  # Start the SSL configurations
  # ssl                  on;
  ssl_certificate      /etc/nginx/ssl/scool.cat/412609/server.crt;
  ssl_certificate_key  /etc/nginx/ssl/scool.cat/412609/server.key;

  location / {
    proxy_pass             http://127.0.0.1:6001;
    proxy_set_header Host  $host;
    proxy_read_timeout     60;
    proxy_connect_timeout  60;
    proxy_redirect         off;

    # Allow the use of websockets
    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;
  }
}

If i visit:

https://socket.scool.cat/

thows my a 502 Bad Gateway error.

vesper8 commented 5 years ago

@stayallive thank you for your explanation.. but honestly my head is spinning trying to wrap my head around all this! I think your solution seems the best and cleanest.. but can you please see if I understand correctly

currently on my dev, using laravel valet with the --secure flag

I have 3 apps

1 socket.tld.dev which handles the websocket, this has a .crt and .key in the websockets.php config in order to make it work

2 restapi.tld.dev which is the api for my front-end, and can broadcast to the front-end, via the dedicated socket app

3 vue-frontend.tld.dev which is my front-end, this uses host: socket.tld.dev and connects over ssl

all 3 sites were https through Valet and it works great

now I tried to move all of this to my forge server and I can't connect. I realized the websocket ssl config was still pointing to my dev's .crt/.key so that was surely problem #1, and problem #2 is that I probably need to open up port 6001 first

But then I came here and saw your solution and it seems great since I may not have to do either of those things above.. worry about certs and open up a port

I already set up my 3 subdomains and made them all https through letsencrypt

Now is it my understanding that you're suggesting I set up a 4th subdomain, http only, and use the nginx block you provided to proxy to my internal port 6001 ?

Does this 4th domain need to be https with let's encrypt?

And then inside my socket.tld websocket's config I can leave the ssl config blank ?

And then in my vue-frontend I would set the host to https of this 4th domain?

This way all my domains are still https, and I can access my debug dashboard on the socket.tld, but the websocket config itself is not "SSL" thanks to the proxy trick?

stayallive commented 5 years ago

@vesper8 you're almost there. The socket.tld.dev app you have should be it's own subdomain (with https) with that nginx block I mentioned earlier.

You will also need to run the php artisan websockets:serve command in Supervisor using the Daemons tab on your Forge server dashboard.

This way the websockets server runs on 127.0.0.1:6001 on your Forge box (do not configure SSL in your websockets.php file for the sockets server).

Then any app on your server (a server side app) can talk to your websockets server on http://127.0.0.1:6001 (no https) or socket.tld.dev:443 (with ssl). The internal non-http path will be slightly faster since it doesn not go via the internet but only local network.

Your front-end clients should connect to socket.tld.dev:443 (with http).

That should be it 👍

A reverse proxy... proxies traffic, you might already guessed that but a default https host listens on port 443 and what that proxy_pass http://127.0.0.1:6001; does in the nginx config is pass any traffic on to http://127.0.0.1:6001 which is your local php artisan websockets:serve. So nginx handles the SSL/TLS stuff and passes un-encrypted content through to your websockets server which handles the connections. This way port 6001 does not need to be exposed and your websockets server does not have to handle the SSL/TLS handshakes itself.

Hope this makes more sense now for everyone 👍

stayallive commented 5 years ago

@acacha if your are getting a 502 the proxy_pass failed. Make sure you have the php artisan websockets:serve running and it's running on 127.0.0.1 and port 6001. The config looks fine at a glance.

acacha commented 5 years ago

@stayallive no matter if php artisan websockets:serve is executed or no the 502 bad gateway error persists...

Let's suppose my Nginx proxy config is correct so maybe it's a package configuration error:

$ cat /home/forge/scool.cat/config/websockets.php 
<?php

use App\LaravelWebSockets\ConfigAppProvider;

return [

    /*
     * This package comes with multi tenancy out of the box. Here you can
     * configure the different apps that can use the webSockets server.
     *
     * Optionally you can disable client events so clients cannot send
     * messages to each other via the webSockets.
     */
    // IMPORTANT SEE NEXT VARIABLE -> We use custom ConfigAppProvider.
    'apps' => [],

    /*
     * This class is responsible for finding the apps. The default provider
     * will use the apps defined in this config file.
     *
     * You can create a custom provider by implementing the
     * `AppProvider` interface.
     */
    'app_provider' => ConfigAppProvider::class,

    /*
     * This array contains the hosts of which you want to allow incoming requests.
     * Leave this empty if you want to accept requests from all hosts.
     */
    'allowed_origins' => [
        //
    ],

    /*
     * The maximum request size in kilobytes that is allowed for an incoming WebSocket request.
     */
    'max_request_size_in_kb' => 250,

    /*
     * This path will be used to register the necessary routes for the package.
     */
    'path' => 'laravel-websockets',

    'statistics' => [
        /*
         * This model will be used to store the statistics of the WebSocketsServer.
         * The only requirement is that the model should extend
         * `WebSocketsStatisticsEntry` provided by this package.
         */
        'model' => \BeyondCode\LaravelWebSockets\Statistics\Models\WebSocketsStatisticsEntry::class,

        /*
         * Here you can specify the interval in seconds at which statistics should be logged.
         */
        'interval_in_seconds' => 60,

        /*
         * When the clean-command is executed, all recorded statistics older than
         * the number of days specified here will be deleted.
         */
        'delete_statistics_older_than_days' => 60,
    ],

    /*
     * Define the optional SSL context for your WebSocket connections.
     * You can see all available options at: http://php.net/manual/en/context.ssl.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('WEBSOCKETS_LOCAL_CERT','/home/sergi/.valet/Certificates/scool.test.crt'),

        /*
         * Path to local private key file on filesystem in case of separate files for
         * certificate (local_cert) and private key.
         */
        'local_pk' => env('WEBSOCKETS_LOCAL_PK','/home/sergi/.valet/Certificates/scool.test.key'),

        /*
         * Passphrase for your local_cert file.
         */
        'passphrase' => null,

        'verify_peer' => env('WEBSOCKETS_VERIFY_PEER',false),

    ],
];

It coulb be really useful to see a complete working configuration....

acacha commented 5 years ago

Current Envirnment values:

$ php artisan tinker
Psy Shell v0.9.9 (PHP 7.2.13-1+ubuntu18.04.1+deb.sury.org+1 — cli) by Justin Hileman
>>> env('WEBSOCKETS_LOCAL_CERT')
=> "/etc/nginx/ssl/scool.cat/412609/server.crt"
>>> env('WEBSOCKETS_LOCAL_PK')
=> "/etc/nginx/ssl/scool.cat/412609/server.key"
>>> env('WEBSOCKETS_VERIFY_PEER')
=> true

So using current SSL certificates for Laravel Forge site.

An any errors when running:

$ /usr/bin/php /home/forge/scool.cat/artisan websockets:serve --verbose
Starting the WebSocket server on port 6001...
stayallive commented 5 years ago

Again, you should not add the certificate in websockets.php 👍

Let nginx handle the SSL, run the websockets server as a plain one.

You are doing a proxy_pass http://127.0.0.1:6001; but that will fail if your websockets server expects a https connection.

acacha commented 5 years ago

Oh thanks I'm sure I've tried this combination but not at least no more 502 gateway errors now. Thanks! Let met try it in deep and If it works i will try to put here my full config for anybody like me with similar problems.

acacha commented 5 years ago

Working on it but now I have a 504 timeout error instead of 502 Bad gateway:

curl https://socket.scool.cat/
<html>
<head><title>504 Gateway Time-out</title></head>
<body>
<center><h1>504 Gateway Time-out</h1></center>
<hr><center>nginx/1.15.6</center>
</body>
</html>
acacha commented 5 years ago

Also not working with Laravel Echo:

Laravel Echo config:

window.Echo = new Echo({
  broadcaster: 'pusher',
  key: '6f627646afb1261d5b50',
  wsHost: 'socket.scool.cat',
  disablestats: true,
  encrypted: true
})

Chrome Console error:

app.js:38721 WebSocket connection to 'wss://socket.scool.cat/app/6f627646afb1261d5b50?protocol=7&client=js&version=4.3.1&flash=false' failed: WebSocket is closed before the connection is established.
acacha commented 5 years ago

More info:https://github.com/beyondcode/laravel-websockets-docs/pull/1

acacha commented 5 years ago

Sorry 504 Gateway error solved:

Changed in /etc/nginx/sites-available/00_socket.scool.cat:

  location / {
    proxy_pass             https://127.0.0.1:6001;
....

to http://127.0.0.1:6001; (without s)

vesper8 commented 5 years ago

@stayallive thank you very much for your help! I have it all working perfectly now using your solution!

I did end up creating a 4th subdomain for the proxy pass, because the socket.tld domain still needs to be accessible for the sole purpose of accessing the debug dashboard

stayallive commented 5 years ago

@vesper8 ah yeah, ofcourse! Happy you got it working 🤘

hadlow commented 5 years ago

Hello!

I seem to be having trouble with the same thing. I am trying to setup the reverse proxy as explained in these posts, and am getting a 502 error. I have created another Site in Forge, with the domain socket.domain.tld and this is the nginx config for that site:

include forge-conf/socket.domain.com/before/*;

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name socket.domain.com;

    ssl on;
    ssl_certificate /etc/nginx/ssl/socket.domain.com/471603/server.crt;
    ssl_certificate_key /etc/nginx/ssl/socket.domain.com/471603/server.key;

    include forge-conf/socket.domain.com/server/*;

    location / {
        proxy_pass             http://127.0.0.1:6001;
        proxy_read_timeout     60;
        proxy_connect_timeout  60;
        proxy_redirect         off;

        # Allow the use of websockets
        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;
    }
}

include forge-conf/socket.domain.com/after/*;

In my config/websockets.php file, the local_cert and local_pk are both set to null.

My pusher connection config in config/broadcasting.php looks like this:

'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' => env('WS_HOST', '127.0.0.1'),
                'port' => env('WS_PORT', '6001'),
                'scheme' => env('WS_SCHEME', 'http'),
                'encrypted' => env('WS_ENCRYPTED', 'false'),
            ],
        ],

With my .env file looking like:

WS_HOST=127.0.0.1
WS_POST=6001
WS_SCHEME='https'
WS_ENCRYPTED=true

My Echo config:

window.Echo = new Echo({
            broadcaster: 'pusher',
            key: 'PUSHE_KEY',
            encrypted: true,
            wsHost: socket.domain.com,
            wsPort: 443,
            wssPort: 443,
            disableStats: true,
            authEndpoint: environment.url + '/broadcasting/auth',
            auth: {
                headers: {
                    Authorization: 'Bearer ' + this.authService.getBearer(),
                },
            },
        });

And the exact error I get in the Chrome console:

WebSocket connection to 'wss://socket.domain.com/app/b7c07386f4d71d126430?protocol=7&client=js&version=4.3.1&flash=false' failed: Error during WebSocket handshake: Unexpected response code: 502

Oh, and I have the php artisan websockets:serve command setup as a Forge Daemon.

Any pointers on this will be more than appreciated, thanks in advance

jafar-albadarneh commented 5 years ago

@stayallive since a lot of us is experiencing the same problem, i think it would be great if there is a blog post / gist that goes through an end-to-end configuration to get it working properly. I've been digging in such issue for almost a week now.

Appreciated!

stayallive commented 5 years ago

@jafar-albadarneh how about: https://alex.bouma.me/installing-laravel-websockets-on-forge/

hadlow commented 5 years ago

Thanks @stayallive ! Your guide helped me get it working 🎉

jafar-albadarneh commented 5 years ago

@stayallive You made my day! Working Great now. Thanks a lot

tshafer commented 5 years ago

Your comments helped me a lot @stayallive Thank you.

rymesaint commented 1 year ago

@jafar-albadarneh how about: https://alex.bouma.me/installing-laravel-websockets-on-forge/

Thank you you helped me a lot