Переглянути джерело

Add proper WebSocket client module for OpenWrt

- client2server-ws.lua: RFC 6455 WebSocket implementation with proper handshake
- Uses openssl-util for SHA1 (base64 encoded)
- Proper Sec-WebSocket-Key generation
- Base64 encoding without external deps
Luis Rosales 2 місяців тому
батько
коміт
ba54e7ecab
2 змінених файлів з 148 додано та 1 видалено
  1. 1 1
      package/Makefile
  2. 147 0
      package/src/client2server-ws.lua

+ 1 - 1
package/Makefile

@@ -17,7 +17,7 @@ define Package/$(PKG_NAME)
   CATEGORY:=Utilities
   TITLE:=Event forwarder for OpenWrt to central server
   DESCRIPTION:=All-in-one Lua event forwarder that monitors DHCP, WAN link, and forwards to central server via WebSocket with offline buffering
-  DEPENDS:=+lua +luasocket
+  DEPENDS:=+lua +luasocket +openssl-util
   TITLE:=Client2Server Unified
 endef
 

+ 147 - 0
package/src/client2server-ws.lua

@@ -0,0 +1,147 @@
+-- ============================================================================
+-- WEBSOCKET (RFC 6455) - Proper WebSocket implementation
+-- ============================================================================
+
+local ws = { sock = nil, connected = false, key = "" }
+
+-- Simple base64 encoder (no external deps)
+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) - uses openssl
+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 (is_ssl and "443" or "80")
+    if not host then return nil end
+
+    local sock = require("socket").tcp()
+    sock:settimeout(10)
+
+    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
+
+    -- 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)
+        return false
+    end
+    return true
+end
+
+function ws.recv()
+    if not ws.connected then return nil end
+
+    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
+end