owntone / owntone-server

Linux/FreeBSD DAAP (iTunes) and MPD audio server with support for AirPlay 1 and 2 speakers (multiroom), Apple Remote (and compatibles), Chromecast, Spotify and internet radio.
https://owntone.github.io/owntone-server
GNU General Public License v2.0
2.07k stars 236 forks source link

SSL option, or configurable websocket protocol #664

Closed tylerball closed 5 years ago

tylerball commented 5 years ago

forked-daapd is working great for me, the music solution I've been looking for after trying many.

I've been experimenting with using traefik to reverse proxy to my forked-daapd instance so I can serve it behind https to the outside world. This works fine except for the websocket connection, which is hardcoded with ws:// and is blocked by mixed-content restrictions. Would it be possible to make this configureable? I took a shot at it but I'm not a C developer and I'm clearly missing something.

chme commented 5 years ago

The websocket protocol is hard coded in the javascript part of forked-daapd. In order to connect with wss:// you would need: https://github.com/chme/forked-daapd/commit/08584a87f76b1cf949e39ed7529c72cbcad35c79 (plus rebuilding the web interface with nodejs).

With this change, the web interface should connect with wss:// to the reverse proxy and the reverse proxy would still connect with ws:// to forked-daapd (similar how it works with http/https).

I did start working on support for https/wss directly in forked-daapd (e. g. with self signed certificates) in: https://github.com/chme/forked-daapd/commits/https. If i remember correctly it did work fine, but is in need of some polishing.

tylerball commented 5 years ago

Ah the rebuilding step is what I was missing.

if (window.location.protocol == 'https:') {
  protocol = 'wss://'
}

This seems like a way better approach without the need to add configuration. Would you accept a PR with just this change?

chme commented 5 years ago

Yes, the change is fine. Feel free to open a PR.

chme commented 5 years ago

Merged with #669

Porco-Rosso commented 5 years ago

@tylerball I am trying to accomplish a similar setup. Could you share your reverse proxy setup? I keep getting reconnecting-websocket.js:227 WebSocket connection to 'wss://XXXXXXX.XX:3688/' failed: WebSocket is closed before the connection is established"

Did you proxy ports 3688 and 3689 separately?

GuyKh commented 4 years ago

I have the same problem and the same question Getting the same error

@tylerball I am trying to accomplish a similar setup. Could you share your reverse proxy setup? I keep getting reconnecting-websocket.js:227 WebSocket connection to 'wss://XXXXXXX.XX:3688/' failed: WebSocket is closed before the connection is established"

Did you proxy ports 3688 and 3689 separately?

Were you able to solve this somehow?

Porco-Rosso commented 4 years ago

Nope, I am still hoping something comes up. I spent quite some time trying different things in nginx to no avail.

GuyKh commented 4 years ago

@Porco-Rosso - I'm actually using Traefik.

I had the problem you're having, then forwarded the 3688 port to the Traefik host on the router. Make sure you did that. This lead me to another error: WebSocket connection to 'wss://xxxxxxxx.xxx:3688/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED

I tried also forwarding this port directly from the router to the host with forked-daapd - but same result

Also tried using https://www.websocket.org/echo.html and other sites - still getting the same result

I'm still clueless


Actually - made some progress:

Now, I'm getting: WebSocket connection to 'wss://xxxxxxx:3688/' failed: Error during WebSocket handshake: Unexpected response code: 404

I tried running NMAP to the daapd host and I see only this port is open 3689/tcp open rendezvous, no 3688

peterneutron commented 4 years ago

@chme I really tried everything to use NGINX as a reverse proxy to access forked-daapd via HTTPS. The web interface is working fine but i can't get the websocket connection to work. Does anybody have a example configuration file or something.

larrybolt commented 4 years ago

I've tried and failed too. Maybe just running the web UI separately so you can change the WebSocket endpoint?

peterneutron commented 4 years ago

@larrybolt When proxying with NGINX the webinterface tries to connect to

and this always fails with SSL_ERROR_RX_RECORD_TOO_LONG according to my console. Trying with websocat also yields WebSocketError: TLS failure. I just can"t make any sense of it. Appending my .conf for reference.


server {
    listen 443 ssl;
    server_name example.com;

    allow x.x.x.x/xx;
    deny all;

    ssl_certificate /etc/letsencrypt/live/***/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/***/privkey.pem;
    #include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

    ssl_session_timeout 1d;
    ssl_session_cache shared:MozSSL:10m;
    ssl_session_tickets off;

    ssl_protocols TLSv1.3;
    ssl_prefer_server_ciphers off;

    ssl_stapling on;
    ssl_stapling_verify on;
    ssl_trusted_certificate /etc/letsencrypt/chain.pem;

    location / {
       proxy_pass http://localhost:3689;
       proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
       proxy_set_header Host $host;
       proxy_set_header X-Real-IP $remote_addr;
       proxy_http_version 1.1;
       proxy_set_header Upgrade $http_upgrade;
       proxy_set_header Connection "upgrade";
       }
  }
p3ck commented 3 years ago

@chme Do you have an example config that shows how to make this work?

p3ck commented 3 years ago

I figured it out... This may not be 100% right but it works for me...

server {
    server_name music.example.com;

    location / {
        proxy_pass http://music.home.example.com:3689;
        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 $scheme;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }

    listen 443 ssl; # managed by Certbot
    ssl_certificate /etc/letsencrypt/live/media.example.com/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/media.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

}

server {
    server_name music.example.com;

    location / {
        proxy_pass http://music.home.example.com:3688;
        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 $scheme;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
    listen 33688 ssl; # managed by Certbot
    ssl_certificate /etc/letsencrypt/live/media.example.com/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/media.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
}

server {
    if ($host = music.example.com) {
        return 301 https://$host$request_uri;
    } # managed by Certbot

    listen 80;
    server_name music.example.com;
    return 404; # managed by Certbot

}

On my firewall I forward port 443 to port 80 internally and 3688 to port 33688 internally. Need to use a different port since I'm running nginx and forked-daapd on the same host.

p3ck commented 3 years ago

also - add geo based auth...

geo $authentication {
    default "Authentication required";
    127.0.0.1 "off";
    192.168.1.0/24 "off";
}

server {
    server_name music.example.com;

    location / {
        auth_basic $authentication;
        auth_basic_user_file /etc/nginx/.htpasswd;
  .
  .
Porco-Rosso commented 3 years ago

Thanks for the contribution @p3ck, will give it a try sometime soon.

peterneutron commented 3 years ago

Thank you @p3ck for pointing me in the right direction. I also figured out how to make it work without using a port redirect.

  1. Patching src/websocket.c to bind the websocket to localhost only by adding info.protocols = "127.0.0.1";
@@ -388,6 +388,7 @@
   memset(&info, 0, sizeof(info));
   info.port = websocket_port;
   info.protocols = protocols;
+  info.iface = "127.0.0.1";
   if (!cfg_getbool(cfg_getsec(cfg, "general"), "ipv6"))
     info.options |= LWS_SERVER_OPTION_DISABLE_IPV6;
   info.gid = -1;

Then I used @p3ck nginx configuration provided above, but now you can bind nginx to the same websocket port as forked-daapd on your local network interface and redirect it to localhost.

server {
    listen X.X.X.X:3688 ssl;
    server_name music.example.com;

    location / {
        proxy_pass http://127.0.0.1:3688;
        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 $scheme;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }

    ssl_certificate /etc/letsencrypt/live/media.example.com/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/media.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
}
  1. @chme Maybe it is possible to add an option to the configuration file to use a custom info.protocols for the websocket.

Edit:

  1. To clarify a bit on why this is necessary. Per default forked-daapd binds its websocket to any interface. Therefore we cannot bind nginx to the same port which is a problem because when using SSL the web interface expects wss:// and not ws:// and that cannot work because the connection isn't upgraded by nginx because it cannot listen on the same port. When setting nginx to a different port like 1234 it also doesn't work because the webinterface expects the websocket to be the one set in the configuration file and not the one from nginx. I hope this was understandable as i'am not a native speaker.
peterneutron commented 3 years ago

I created a draft pull request #1143.

ErrorSource commented 2 years ago

Sorry for digging out this older issue. Maybe someone is interested in reverse proxy solution using apache2.

Here's my working owntone/apache2 config:

owntone.conf (restart of owntone required)

    # Websocket interface to bind listener to (e.g. "eth0"). Default is
    # disabled, which means listen on all interfaces.
    websocket_interface = "127.0.0.1"

apache2/ports.conf

<IfModule ssl_module>
    Listen 443
    # you have to bind port 3688 explicitly to an IP; not 0.0.0.0
    Listen 10.1.1.30:3688
</IfModule>

apache2/sites-enabled/vhost-owntone.conf

<VirtualHost *:80>
    ServerName owntone.domain.xy

    RewriteEngine On
    RewriteCond %{REMOTE_ADDR} !^127\.0\.0\.1$
    RewriteRule ^/(.*) https://owntone.domain.xy/$1 [R=301,L]
</VirtualHost>

<IfModule mod_ssl.c>
<VirtualHost *:443>
    ServerName owntone.domain.xy

    ProxyPreserveHost On
    ProxyPass / http://127.0.0.1:3689/
    ProxyPassReverse / http://127.0.0.1:3689/

    SSLEngine on
    SSLCertificateFile /path/to/your/fullchain.pem
    SSLCertificateKeyFile /path/to/your/privkey.pem
</VirtualHost>
</IfModule>

<IfModule mod_ssl.c>
<VirtualHost *:3688>
    ServerName owntone.domain.xy

    <IfModule mod_alias.c>
        RedirectMatch 403 favicon.ico
    </IfModule>

    RewriteEngine on
    RewriteCond %{HTTP:Upgrade} websocket [NC]
    RewriteRule /(.*) ws://127.0.0.1:3688/$1 [P,L]
    RewriteRule /(.*) http://127.0.0.1:3688/$1 [P,L]

    SSLEngine on
    SSLCertificateFile /path/to/your/fullchain.pem
    SSLCertificateKeyFile /path/to/your/privkey.pem
</VirtualHost>
</IfModule>

And of course port has to be permitted for localhost in firewall-settings.

Hope, i haven't forgotten anything.