ie = minetest.request_insecure_environment() if not ie then minetest.log("warn", "Chatbridge failed to access the insecure enviroment. It will be disabled.") error("!!") return end local oldrequire = require require = ie.require local socket = ie.require("socket") local mime = ie.require("mime") local bxor = bit.bxor local band = bit.band local rshift = bit.rshift local lshift = bit.lshift -- Helper function to generate a random WebSocket key (16 bytes, base64-encoded) local function generate_websocket_key() local bytes = {} for i = 1, 16 do bytes[i] = string.char(math.random(0, 255)) end return mime.b64(table.concat(bytes)) end -- Helper function to create a masked WebSocket frame for a text message local function create_websocket_frame(message, opcode) local payload = message or "" local payload_len = #payload local frame = {} -- FIN bit (1) and opcode (0x1 for text, 0xA for pong) frame[1] = string.char(0x80 + (opcode or 0x1)) -- FIN=1, specified opcode -- Mask bit (1) and payload length if payload_len <= 125 then frame[2] = string.char(0x80 + payload_len) -- Mask=1, length <= 125 elseif payload_len <= 65535 then frame[2] = string.char(0x80 + 126) -- Mask=1, length=126 (extended) frame[3] = string.char(math.floor(payload_len / 256)) -- High byte frame[4] = string.char(payload_len % 256) -- Low byte else error("Payload too large for this example") end -- Generate 4-byte masking key local mask_key = {} for i = 1, 4 do mask_key[i] = math.random(0, 255) frame[#frame + 1] = string.char(mask_key[i]) end -- Mask the payload for i = 1, payload_len do local byte = string.byte(payload, i) local masked_byte = bxor(byte, mask_key[(i - 1) % 4 + 1]) frame[#frame + 1] = string.char(masked_byte) end return table.concat(frame) end -- Helper function to parse a WebSocket frame header local function parse_websocket_header(data) if #data < 2 then return nil, nil, nil, data end -- Not enough data local first_byte = string.byte(data, 1) local second_byte = string.byte(data, 2) local fin = rshift(first_byte, 7) == 1 local opcode = band(first_byte, 0x0F) local masked = rshift(second_byte, 7) == 1 local payload_len = band(second_byte, 0x7F) local offset = 2 if payload_len == 126 then if #data < 4 then return nil, nil, nil, data end payload_len = lshift(string.byte(data, 3), 8) + string.byte(data, 4) offset = 4 elseif payload_len == 127 then return nil, nil, nil, "64-bit length not supported" end if masked then if #data < offset + 4 then return nil, nil, nil, data end offset = offset + 4 end return opcode, payload_len, offset, data end -- Helper function to print raw data in hex for debugging local function print_hex(data) if data then local hex = data:gsub(".", function(c) return string.format("%02X ", string.byte(c)) end) print("Raw data (hex): " .. hex) else print("No data received") end end -- Helper function to check for available messages using socket.select local function check_for_message(client, timeout) local readable, _, err = socket.select({client}, nil, timeout or 0) if err then return false, err end return #readable > 0, nil end -- WebSocket client class local WebSocketClient = { onmessage = function() end } WebSocketClient.__index = WebSocketClient function WebSocketClient:new(host, port) local client = socket.connect(host, port) if not client then return nil, "Connection failed" end client:settimeout(0.1) -- Short timeout for non-blocking reads local self = { client = client, buffer = "", closed = false } setmetatable(self, WebSocketClient) return self end function WebSocketClient:connect() local client = self.client local ws_key = generate_websocket_key() local handshake = table.concat({ "GET / HTTP/1.1", "Host: localhost:8001", "Upgrade: websocket", "Connection: Upgrade", "Sec-WebSocket-Key: " .. ws_key, "Sec-WebSocket-Version: 13", "", "" }, "\r\n") local _, err = client:send(handshake) if err then client:close() self.closed = true return false, "Handshake send failed: " .. err end local response = "" while true do local line, err = client:receive("*l") if not line or err then client:close() self.closed = true return false, "Handshake failed: " .. (err or "empty line") end response = response .. line .. "\r\n" if line == "" then break end end if not response:match("HTTP/1.1 101 Switching Protocols") then client:close() self.closed = true return false, "Invalid handshake response" end print("WebSocket handshake successful") return true end function WebSocketClient:send_message(message) if type(message) == "table" then message = minetest.write_json(message) end if self.closed then return false, "Connection closed" end local frame = create_websocket_frame(message) local _, err = self.client:send(frame) if err then self.client:close() self.closed = true return false, "Send failed: " .. err end print("Sent frame: " .. message) return true end function WebSocketClient:send_pong(payload) if self.closed then return false, "Connection closed" end local frame = create_websocket_frame(payload, 0xA) -- Opcode 0xA for pong local _, err = self.client:send(frame) if err then self.client:close() self.closed = true return false, "Send pong failed: " .. err end print("Sent pong") return true end function WebSocketClient:receive_message() if self.closed then return nil, "Connection closed" end local client = self.client local buffer = self.buffer local opcode, payload_len, offset, err -- Step 1: Read header while true do opcode, payload_len, offset, buffer = parse_websocket_header(buffer) if opcode then break end if err then self.client:close() self.closed = true return nil, err end local data, err = client:receive(1) if data then buffer = buffer .. data print_hex(data) elseif err == "timeout" then self.buffer = buffer return nil, nil -- Not enough data, try again later elseif err == "closed" then self.client:close() self.closed = true return nil, "Connection closed by server" else self.client:close() self.closed = true return nil, "Receive error: " .. err end end -- Step 2: Read payload while #buffer < offset + payload_len do local remaining = offset + payload_len - #buffer local data, err = client:receive(remaining) if data then buffer = buffer .. data print_hex(data) elseif err == "timeout" then self.buffer = buffer return nil, nil -- Not enough data, try again later elseif err == "closed" then self.client:close() self.closed = true return nil, "Connection closed by server" else self.client:close() self.closed = true return nil, "Receive error: " .. err end end -- Extract payload local payload = buffer:sub(offset + 1, offset + payload_len) self.buffer = buffer:sub(offset + payload_len + 1) -- Handle frame types if opcode == 0x1 then print("Received: " .. payload) return payload, opcode elseif opcode == 0x9 then print("Received ping") self:send_pong(payload) -- Respond with pong return nil, opcode -- Ping frames are not returned as messages elseif opcode == 0x8 then print("Received close frame") self:close() return nil, opcode else print("Unexpected opcode: " .. opcode) return nil, opcode end return payload, opcode end function WebSocketClient:close() if not self.closed then local close_frame = string.char(0x88, 0x02, 0x03, 0xE8) -- FIN=1, opcode=0x8, length=2, status=1000 self.client:send(close_frame) self.client:close() self.closed = true end end -- Main client loop for sending and receiving messages local client, err = WebSocketClient:new("localhost", 8001) if not client then print("Failed to create client: " .. err) return end if not client:connect() then return end client.onmessage = function(e, data) data = minetest.parse_json(data) minetest.chat_send_all(minetest.colorize("#888", "<"..data.name.."> "..data.msg)) end client:send_message({type = "online"}) minetest.register_on_chat_message(function(name, msg) client:send_message({type = "chat", sender = name, msg = msg}) end) minetest.register_globalstep(function() local has_data, err = check_for_message(client.client) if has_data then local payload, opcode = client:receive_message() if payload then if opcode == 0x1 then print("Received: " .. payload) client:onmessage(payload) elseif opcode == 0x8 then print("Received close frame") client:close() return else print("Unexpected opcode: " .. opcode) end elseif err then print("Receive error: " .. err) return end elseif err then -- print("Select error: " .. err) -- client:close() -- return end end) minetest.register_on_shutdown(function() client:close() end) require = oldrequire --local ws, err = socket.connect("localhost", 8001) -- --if not ws then -- minetest.log("Failed to open connection: "..err) -- return --end --local ws_key = "dGhlIHNhbXBsZSBub25jZQ==" -- ---- Send HTTP upgrade request --local handshake = table.concat({ -- "GET / HTTP/1.1", -- "Host: localhost:8001", -- "Upgrade: websocket", -- "Connection: Upgrade", -- "Sec-WebSocket-Key: " .. ws_key, -- "Sec-WebSocket-Version: 13", -- "", "" --}, "\r\n") --ws:send(handshake) -- ---- Receive and validate server response --local response, err = ws:receive("*l") --if not response or response ~= "HTTP/1.1 101 Switching Protocols" then -- print("Handshake failed: " .. (err or response)) -- return --end -- ---- Read headers to confirm Sec-WebSocket-Accept --while true do -- local line, err = ws:receive("*l") -- if not line or err then -- print("Error reading headers: " .. (err or "empty line")) -- break -- end -- if line == "" then break end -- End of headers -- print(line) --end -- ---- Send a simple text frame (incomplete, needs proper framing and masking) --ws:send("\x81\x04Test") -- Opcode 0x1 (text), length 4, payload "Test"