diff --git a/chatbridge/init.lua b/chatbridge/init.lua new file mode 100644 index 0000000..85eef4e --- /dev/null +++ b/chatbridge/init.lua @@ -0,0 +1,400 @@ + +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" \ No newline at end of file diff --git a/chatbridge/mod.conf b/chatbridge/mod.conf new file mode 100644 index 0000000..f144587 --- /dev/null +++ b/chatbridge/mod.conf @@ -0,0 +1 @@ +name = chatbridge