Upload files to "chatbridge"

This commit is contained in:
Signal 2025-06-01 00:41:27 +00:00
parent ac4028bc77
commit fedcfeeb4b
2 changed files with 401 additions and 0 deletions

400
chatbridge/init.lua Normal file
View file

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