|
|
@@ -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
|