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