Upload files to "chatbridge"
This commit is contained in:
parent
ac4028bc77
commit
fedcfeeb4b
2 changed files with 401 additions and 0 deletions
400
chatbridge/init.lua
Normal file
400
chatbridge/init.lua
Normal 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"
|
||||||
1
chatbridge/mod.conf
Normal file
1
chatbridge/mod.conf
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
name = chatbridge
|
||||||
Loading…
Add table
Add a link
Reference in a new issue