Nginx als Reverse Proxy mit websocket für FHEM

Begonnen von h3llsp4wn, 16 Februar 2017, 14:37:53

Vorheriges Thema - Nächstes Thema

ebeneezer

Gut zu hören - wer hat das mit Safari am laufen und zeigt mal seine nginX Config?

9zehn75

Ich würde das gerne nochmals hochholen: hat irgendwer eine nginx-Config, die als reverse proxy auch mit iOS-Safari funktioniert?
VG, 9zehn75

FHEM seit 02.02.2016: Raspberry Pi 2, ZME_UZB1, Fibaro WallPlugs, Fibaro Fenstersensoren, Aeon Indoor Sirene, Greenwave WallPlugs, Qubino Dimmer

Loredo

Safari funktioniert nur richtig, wenn die Webseite, die Websockets benutzt, ein valides SSL Zertifikat verwendet. So eines musst du wahrscheinlich bei dir installieren und FHEM dann auch über die korrekte URL aufrufen. Geht natürlich auch mit einer eigenen privaten CA.
Hat meine Arbeit dir geholfen? ⟹ https://paypal.me/pools/c/8gDLrIWrG9

Maintainer:
FHEM-Docker Image, https://github.com/fhem, Astro(Co-Maintainer), ENIGMA2, GEOFANCY, GUEST, HP1000, Installer, LaMetric2, MSG, msgConfig, npmjs, PET, PHTV, Pushover, RESIDENTS, ROOMMATE, search, THINKINGCLEANER

dickdickerson

#63
Ich habe ähnliche Probleme (daher schreibe ich es hier rein), allerdings über HAproxy(2.1.2-alpine) als ReverseProxy. Mir ist das "Connection lost, trying a reconnect every 5 seconds." schon des öftern aufgefallen und jetzt habe ich mich mal dran gemacht das Problem zu analysieren.

Mein Aufbau sieht aktuell so aus:
Extern --> HAproxy (https:443) --> FHEM (http:8083)
alternativ auch folgendes getestet:
Extern --> HAproxy (https:443) --> FHEM (https:8084 selfsigned)

Ich habe ein gültiges Letsencrypt-Zertifikat am Frontend.

Bei mir tritt das Problem mit "attr WEBrproxy longpoll websocket" auf.

Chromium zeigt folgenden Fehler an:
WebSocket connection to 'wss://<domain>/fhem/dashboard/Dashboard?XHR=1&inform=type=status;filter=.*;since=1579958874;fmt=JSON&fw_id=1445&timestamp=1579958882521' failed: Error during WebSocket handshake: Incorrect 'Sec-WebSocket-Accept' header value
FW_longpoll @ fhemweb.js:1208


In besagter Zeile 1208 in fhemweb.js steht folgendes:
1207  if(typeof WebSocket == "function" && FW_longpollType == "websocket") {
1208    FW_pollConn = new WebSocket(loc.replace(/[#&?].*/,'')
1209                                   .replace(/^http/i, "ws")+query);
1210    FW_pollConn.onclose =
1211    FW_pollConn.onerror =
1212    FW_pollConn.onmessage = FW_doUpdate;
1213
1214  }


Meine backend-Konfig in HAproxy:
backend fhem
        acl forwarded_proto hdr_cnt(X-Forwarded-Proto) eq 0
        acl forwarded_port hdr_cnt(X-Forwarded-Port) eq 0
        option httpchk GET /fhem
        http-check expect status 200
        http-response set-header Strict-Transport-Security max-age=31536000;\ includeSubDomains
        #http-response set-header X-Frame-Options SAMEORIGIN
        #http-response set-header X-XSS-Protection 1;\ mode=block
        #http-response set-header X-Content-Type-Options nosniff
        #http-response set-header Feature-Policy \'none\'
        #http-response set-header Referrer-Policy no-referrer
        #http-response set-header Content-Type text/html;\ charset=UTF-8
        http-response del-header Server
        acl AuthOkay_FHEM http_auth(FHEM)
        timeout client 25s
        timeout connect 5s
        timeout server 25s
        timeout tunnel 1h
        timeout http-keep-alive 1s
        timeout http-request 15s
        timeout queue 30s
        timeout tarpit 60s
        #timeout client-fin 10s
        http-request auth realm FHEM if !AuthOkay_FHEM
        http-request add-header X-Forwarded-Port %[dst_port] if forwarded_port
        http-request add-header X-Forwarded-Proto https if { ssl_fc } forwarded_proto
        mode http
        server fhem <ip-fhem>:8084 ssl verify none check inter 60000 rise 6 fall 12
        #server fhem <ip-fhem>:8083 check inter 60000 rise 6 fall 12


So wie ich das sehe (zumindest zeigt mir der Browser es nicht an), kommt am Browser der Header 'Sec-WebSocket-Accept' nicht mehr an.
Ich habe schon mit tcpdump einen Dump auf dem Server wo HAproxy läuft gemacht, mit "tcpdump -nw host <ip-fhem> and '(port 8083 or 8084)'".
In dem Dump sehe ich, dass ein GET vom HAproxy zu FHEM gesendet wird und FHEM mit "HTTP/1.1 101 Switching Protocols" antwortet:

Request:
Frame 4: 843 bytes on wire (6744 bits), 843 bytes captured (6744 bits)
Raw packet data
Internet Protocol Version 4, Src: <haproxyip>, Dst: <fhemip>
Transmission Control Protocol, Src Port: 32996, Dst Port: 8083, Seq: 1, Ack: 1, Len: 791
Hypertext Transfer Protocol
    GET /fhem?XHR=1&inform=type=status;filter=;since=1579956273;fmt=JSON&fw_id=600&timestamp=1579956446194 HTTP/1.1\r\n
    host: <domain>\r\n
    connection: Upgrade\r\n
    pragma: no-cache\r\n
    cache-control: no-cache\r\n
    authorization: Basic <basicauth>\r\n
    user-agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/79.0.3945.79 Chrome/79.0.3945.79 Safari/537.36\r\n
    upgrade: websocket\r\n
    origin: https://<domain>\r\n
    sec-websocket-version: 13\r\n
    accept-encoding: gzip, deflate, br\r\n
    accept-language: de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7\r\n
    sec-websocket-key: tV5arw6WE10arEuOYdX27A==\r\n
    sec-websocket-extensions: permessage-deflate; client_max_window_bits\r\n
    x-forwarded-port: 443\r\n
    x-forwarded-proto: https\r\n
    x-forwarded-for: ::ffff:<externalip>\r\n
    \r\n
    [Full request URI: http://<domain>/fhem?XHR=1&inform=type=status;filter=;since=1579956273;fmt=JSON&fw_id=600&timestamp=1579956446194]
    [HTTP request 1/1]
    [Response in frame: 6]


Response:
Frame 6: 223 bytes on wire (1784 bits), 223 bytes captured (1784 bits)
Raw packet data
Internet Protocol Version 4, Src: <fhemip>, Dst: <haproxyip>
Transmission Control Protocol, Src Port: 8083, Dst Port: 32996, Seq: 1, Ack: 792, Len: 171
Hypertext Transfer Protocol
    HTTP/1.1 101 Switching Protocols\r\n
    Upgrade: websocket\r\n
    Connection: Upgrade\r\n
    Sec-WebSocket-Accept:Kfh9QIsMVZcl6xEPYxPHzW8SZ8w=\r\n
    X-FHEM-csrfToken: csrf_114925038332728e15\r\n
    \r\n
    [HTTP response 1/1]
    [Time since request: 0.018372000 seconds]
    [Request in frame: 4]
    [Request URI: http://<domain>/fhem?XHR=1&inform=type=status;filter=;since=1579956273;fmt=JSON&fw_id=600&timestamp=1579956446194]


Der Response scheint sich aber in Luft aufzulösen.
Ich habe andere Subdomains (Nextcloud mit Collabora) bei denen Websocket ohne Probleme funktioniert und die HAproxy-Konfig identisch ist.
Laut der Doku (https://www.haproxy.com/de/blog/websockets-load-balancing-with-haproxy/) sind meine Konfigzeilen ausreichend (Funktioniert ja auch bei Nextcloud).

Hat irgendjemand noch einen Tipp was das Problem sein könnte? Mir gehen die Ideen aus.


Edit:
Ich habe gerade mal kontrolliert, was mir die Developer-Tools von Firefox sagen. Der Response-Header wird offenbar doch zurück zum Browser gesendet. In Firefox sehe ich nämlich den Response.
Auf Github habe ich ein Issue gefunden, wo der Ersteller genau das selbe Problem mit FHEM hat, jedoch mit Traefik. (https://github.com/containous/traefik/issues/5330)
Dort ist es offenbar ein Groß/Kleinschreib-Problem vom Response-Header Sec-WebSocket-Accept selbst.

Also habe ich das mal nachgestellt:

Mit ReverseProxy:
Request:
Sec-WebSocket-Key: WhkUkJc32AK1r5P7qWsDrA==
Response:
sec-websocket-accept: Kfh9QIsMVZcl6xEPYxPHzW8SZ8w=

Ohne ReverseProxy:
Request:
Sec-WebSocket-Key: y3yVaexowsFIYghS0OiZtg==
Response:
Sec-WebSocket-Accept: OdkJmc2/FPCa4dIR7ilp3d+fHBY=

Hier ist zu sehen, dass bei der Verbindung über den HAproxy der Response-Header in Kleinbuchstaben geschrieben ist.
Laut dem Issue auf Github (https://github.com/containous/traefik/pull/5397/files/43f1c0ba380ca527c645f5067f69da1965e02e6c) sollten die Header laut RFC eigentlich case-insensitiv sein.

Ich versuche jetzt mal mit HAproxy den Header zu korrigieren. Mal schauen ob das klappt.

P.A.Trick

Zitat von: hexenmeister am 26 Mai 2019, 23:29:47
Zuerst müssen (wenn nicht bereits geschehen) docker und docker-compose installiert werden.
Die Datei "docker-compose.yml" muss in einem leeren Verzeichniss abgelegt werden.
Weitere Infos zu dem Image und Benutzung: https://github.com/linuxserver/docker-letsencrypt

docker-compose.yml:
version: "2"
services:
  letsencrypt:
    image: linuxserver/letsencrypt
    container_name: letsencrypt
    cap_add:
      - NET_ADMIN
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Europe/Berlin
      - URL=<hier die URL eintragen! Z.B. irgendwas.spdns.org>
      - SUBDOMAINS= #www,
      - VALIDATION=http
      - DNSPLUGIN=cloudflare #optional
      - DUCKDNSTOKEN=<token> #optional
      - EMAIL=<mail adresse> #optional
      - DHLEVEL=2048 #optional, (default=2048, can be set to 1024 or 4096)
      - ONLY_SUBDOMAINS=false #optional
      #- EXTRA_DOMAINS= #<extradomains> #optional
      - STAGING=false #optional
    volumes:
      - ./data/config:/config
    ports:
      - 443:443
      - 80:80   # for redirect to 443
    restart: unless-stopped


In dem Verzeichnis, wo die yml Datei liegt muss noch ein Verzeichniss data/config erstellt werden. Dort sollen nach dem ersten Container-Start ("docker-compose up" in diesem Verzeichnis ausführen) eine Menge Verzeichnisse und Dateien entstehen.
Im Verzeichnis data/config/nginx/proxy-confs existieren dann mehrere Vorlagen für verschiedenen Dienste. Fhem ist zwar nicht dabei, ist aber leicht selbst zu esrstellen. Hier ist meine Version:

    # check user agent
    if ($http_user_agent ~* '(iPhone|iPod|Opera Mini|Android.*Mobile|NetFront|PSP|BlackBerry|Windows Phone)') {
        set $ua_type "@mobile";
    }

   # default
    set $fport "8083";
    set $fmport "8084";
   
    # fhem UI
if ($uri ~ "/fhem.*"){
        set $fport "8083";
        set $fmport "8084";
    }
   
    # fhem test     8983
    if ($uri ~ "/fhem-test.*"){
        set $fport "8983";
        set $fmport "8984";
    }
   
    location / {
        access_log off;
        error_log off;
   
        # Wird fuer 'longpoll' benoetigt (z.B. bei FTUI)
        set $my_http_upgrade "";
        set $my_connection "Connection";
       
        if ($http_upgrade = "websocket") {
          set $my_http_upgrade $http_upgrade;
          set $my_connection "upgrade";
        }
       
        proxy_set_header Upgrade $my_http_upgrade;
        proxy_set_header Connection $my_connection;
       
        auth_basic "Restricted area";
        auth_basic_user_file /config/nginx/.htpasswd;
       
        # enable the next two lines for ldap auth, also customize and enable ldap.conf in the default conf
        #auth_request /auth;
        #error_page 401 =200 /login;

        include /config/nginx/proxy.conf;
        resolver 127.0.0.11 valid=30s;
        set $upstream 192.168.0.15;
        #proxy_pass http://$upstream:8080;
       
        # Gehe zu FHEMWEB wenn kein mobiler Browser
        if ($ua_type != "@mobile"){
            #proxy_pass http://$upstream:8083;
            proxy_pass http://$upstream:$fport;
        }
       
        # Gehe zu FHEMWEB smallscreen wenn mobiler Browser
        if ($ua_type = "@mobile"){
            #proxy_pass http://$upstream:8084;
            proxy_pass http://$upstream:$fmport;
        }
    }



.../config/nginx/.htpasswd muss nattürlich auch noch erstellt werden.

Hexenmeister geht das auch ohne eine valide Web Adresse, also mit localhost?
Cubietruck,RPI,QNAP Ts-419p+, FS20, FRITZ!DECT200, 7 MAX! Thermostate, 3 MAX! Fensterkontakte, Kodi, CUL V3.3, EM1000S, LW12, LD382, HUE, HM-CFG-USB-2, 1x HM-LC-SW1-FM, 2x HM-LC-SW2-FM, 2x HM-LC-Sw1PBU-FM, 3xHM-LC-Bl1PBU-FM,HM-SEC-RHS, 2xHM-SEC-SD,HM-WDS30-T-O, 3x HM-LC-Dim1TPBU-FM, RPI+AddOn