Parcourir la source

Update client2server-unified.lua with proper WebSocket handshake

- RFC 6455 WebSocket implementation
- Proper Sec-WebSocket-Key generation with base64
- SHA1 using openssl-util for handshake
- HTTP upgrade handshake check
Luis Rosales il y a 2 mois
Parent
commit
1b2df7e13d
1 fichiers modifiés avec 108 ajouts et 14 suppressions
  1. 108 14
      package/src/client2server-unified.lua

+ 108 - 14
package/src/client2server-unified.lua

@@ -131,28 +131,117 @@ function buffer.flush(send_fn)
 end
 
 -- ============================================================================
--- WEBSOCKET
+-- WEBSOCKET (RFC 6455 - Proper WebSocket)
 -- ============================================================================
 
-local ws = { sock = nil, connected = false }
+local ws = { sock = nil, connected = false, key = "" }
+
+-- Simple base64 encoder
+local function base64_encode(data)
+    local b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
+    local result = {}
+    local i = 1
+    while i <= #data do
+        local b1, b2, b3 = string.byte(data, i, i+2)
+        b2 = b2 or 0
+        b3 = b3 or 0
+        table.insert(result, string.sub(b64, math.floor(b1/4)+1, math.floor(b1/4)+1))
+        table.insert(result, string.sub(b64, ((b1%16)*4) + math.floor(b2/16)+1, ((b1%16)*4) + math.floor(b2/16)+1))
+        if i+1 > #data then table.insert(result, "=") else
+            table.insert(result, string.sub(b64, ((b2%16)*4) + math.floor(b3/64)+1, ((b2%16)*4) + math.floor(b3/64)+1))
+        end
+        if i+2 > #data then table.insert(result, "=") else
+            table.insert(result, string.sub(b64, (b3%64)+1, (b3%64)+1))
+        end
+        i = i + 3
+    end
+    return table.concat(result)
+end
+
+-- SHA1 (for WebSocket handshake)
+local function sha1_binary(data)
+    local f = io.popen("echo -n '" .. data:gsub("'", "'\\''") .. "' | openssl sha1 -binary | base64 | tr -d '\\n' 2>/dev/null")
+    if f then
+        local result = f:read("*a")
+        f:close()
+        return result:gsub("%s+$", "")
+    end
+    return ""
+end
+
+-- Compute Sec-WebSocket-Accept
+local function compute_accept(key)
+    local combined = key .. "258EAFA5-E914-47DA-95CA-C5C753455362"
+    return sha1_binary(combined)
+end
 
 function ws.connect(url)
+    local is_ssl = url:match("wss://") ~= nil
     local host = url:match("wss?://([^:/]+)")
-    local port = url:match(":%d+") or ":443"
+    local port = url:match(":(%d+)") or (is_ssl and "443" or "80")
     if not host then return nil end
-    
+
     local sock = require("socket").tcp()
     sock:settimeout(10)
-    
-    if not pcall(sock.connect, sock, host, tonumber(port:sub(2)) then return nil end
+
+    local ok, err = sock:connect(host, tonumber(port))
+    if not ok then
+        log_err("Cannot connect to " .. host .. ":" .. port .. ": " .. tostring(err))
+        return nil
+    end
+
+    -- Generate random Sec-WebSocket-Key
+    local key = ""
+    for i = 1, 16 do key = key .. string.char(math.random(32, 126)) end
+    key = base64_encode(key)
+    ws.key = key
+
+    local request = "GET /ws HTTP/1.1\r\n" ..
+            "Host: " .. host .. ":" .. port .. "\r\n" ..
+            "Upgrade: websocket\r\n" ..
+            "Connection: Upgrade\r\n" ..
+            "Sec-WebSocket-Key: " .. key .. "\r\n" ..
+            "Sec-WebSocket-Version: 13\r\n" ..
+            "Origin: http://" .. host .. "\r\n" ..
+            "\r\n"
+
+    sock:send(request)
+
+
+    -- Read response
+    local response = {}
+    sock:settimeout(5)
+    for i = 1, 20 do
+        local line = sock:receive("*l")
+        if not line or line == "" then break end
+        table.insert(response, line)
+    end
+
+    -- Check for 101 Switching Protocols
+    local ok_response = false
+    for _, line in ipairs(response) do
+        if line:match("^HTTP/.* 101") then ok_response = true end
+    end
+
+    if not ok_response then
+        log_err("WebSocket handshake failed")
+        sock:close()
+        return nil
+    end
+
     ws.sock = sock
     ws.connected = true
+    log_info("WebSocket connected to " .. host .. ":" .. port)
     return sock
 end
 
 function ws.send(data)
     if not ws.connected then buffer.add(data); return false end
-    local frame = string.format("\x81\x80%s", data)
+
+    -- WebSocket frame: FIN(1) + opcode(1) = 0x81 (text)
+    local payload = data
+    local frame = string.char(0x81) .. payload
+
     if not pcall(function() ws.sock:send(frame) end) then
         ws.connected = false
         buffer.add(data)
@@ -161,25 +250,30 @@ function ws.send(data)
     return true
 end
 
--- Read from websocket (non-blocking-ish)
 function ws.recv()
     if not ws.connected then return nil end
-    
-    -- Set non-blocking
-    ws.sock:settimeout(0.1)
-    
+
+    ws.sock:settimeout(0.5)
     local data, err = ws.sock:receive("*l")
     ws.sock:settimeout(10)
-    
+
     if err and err ~= "timeout" then
         ws.connected = false
         return nil
     end
-    
+
+    -- Strip WebSocket frame header (first byte)
+    if data and #data > 1 then
+        data = data:sub(2)
+    end
+
     return data
 end
 
 function ws.close()
+    if ws.sock and ws.connected then
+        pcall(function() ws.sock:send(string.char(0x88, 0x00)) end)
+    end
     if ws.sock then pcall(ws.sock.close, ws.sock) end
     ws.sock = nil
     ws.connected = false