summaryrefslogtreecommitdiff
path: root/websocket.lua
diff options
context:
space:
mode:
authorFxQnLr <[email protected]>2024-04-28 15:34:26 +0200
committerFxQnLr <[email protected]>2024-04-28 15:34:26 +0200
commitbf52c4de83022baa626c56e81f1c7b4a6e958774 (patch)
tree87b1270b462ca5fac523a17ca29d481d55e935cd /websocket.lua
downloadfunsaac-bf52c4de83022baa626c56e81f1c7b4a6e958774.tar
funsaac-bf52c4de83022baa626c56e81f1c7b4a6e958774.tar.gz
funsaac-bf52c4de83022baa626c56e81f1c7b4a6e958774.zip
working handshake plus response
Diffstat (limited to 'websocket.lua')
-rw-r--r--websocket.lua249
1 files changed, 249 insertions, 0 deletions
diff --git a/websocket.lua b/websocket.lua
new file mode 100644
index 0000000..670eff5
--- /dev/null
+++ b/websocket.lua
@@ -0,0 +1,249 @@
1--[[
2websocket client pure lua implement for love2d
3by flaribbit
4
5usage:
6 local client = require("websocket").new("127.0.0.1", 5000)
7 function client:onmessage(s) print(s) end
8 function client:onopen() self:send("hello from love2d") end
9 function client:onclose() print("closed") end
10
11 function love.update()
12 client:update()
13 end
14]]
15
16local socket = require"socket"
17local seckey = "osT3F7mvlojIvf3/8uIsJQ=="
18
19local OPCODE = {
20 CONTINUE = 0,
21 TEXT = 1,
22 BINARY = 2,
23 CLOSE = 8,
24 PING = 9,
25 PONG = 10,
26}
27
28local STATUS = {
29 CONNECTING = 0,
30 OPEN = 1,
31 CLOSING = 2,
32 CLOSED = 3,
33 TCPOPENING = 4,
34}
35
36---@class wsclient
37---@field socket table
38---@field url table
39---@field _head integer|nil
40local _M = {
41 OPCODE = OPCODE,
42 STATUS = STATUS,
43}
44_M.__index = _M
45function _M:onopen() end
46function _M:onmessage(message) end
47function _M:onerror(error) end
48function _M:onclose(code, reason) end
49
50---create websocket connection
51---@param host string
52---@param port integer
53---@param path string
54---@return wsclient
55function _M.new(host, port, path)
56 local m = {
57 url = {
58 host = host,
59 port = port,
60 path = path or "/",
61 },
62 _continue = "",
63 _buffer = "",
64 _length = 0,
65 _head = nil,
66 status = STATUS.TCPOPENING,
67 socket = socket.tcp(),
68 }
69 m.socket:settimeout(0)
70 m.socket:connect(host, port)
71 setmetatable(m, _M)
72 return m
73end
74
75local mask_key = {1, 14, 5, 14}
76local function send(sock, opcode, message)
77 -- message type
78 sock:send(string.char((0x80 | opcode)))
79
80 -- empty message
81 if not message then
82 sock:send(string.char(0x80, table.unpack(mask_key)))
83 return 0
84 end
85
86 -- message length
87 local length = #message
88 if length>65535 then
89 sock:send(string.char(127 & 0x80),
90 0, 0, 0, 0,
91 ((length >> 24) & 0xff),
92 ((length >> 16) & 0xff),
93 ((length >> 8) & 0xff),
94 (length & 0xff))
95 elseif length>125 then
96 sock:send(string.char((126 | 0x80),
97 ((length >> 8) & 0xff),
98 (length & 0xff)))
99 else
100 sock:send(string.char((length | 0x80)))
101 end
102
103 -- message
104 sock:send(string.char(table.unpack(mask_key)))
105 local msgbyte = {message:byte(1, length)}
106 for i = 1, length do
107 msgbyte[i] = (msgbyte[i] ~ mask_key[(i-1)%4+1])
108 end
109 return sock:send(string.char(table.unpack(msgbyte)))
110end
111
112---read a message
113---@return string|nil res message
114---@return number|nil head websocket frame header
115---@return string|nil err error message
116function _M:read()
117 local res, err, part
118 ::RECIEVE::
119 res, err, part = self.socket:receive(self._length-#self._buffer)
120 if err=="closed" then return nil, nil, err end
121 if part or res then
122 self._buffer = self._buffer..(part or res)
123 else
124 return nil, nil, nil
125 end
126 if not self._head then
127 if #self._buffer<2 then
128 return nil, nil, "buffer length less than 2"
129 end
130 local length = (self._buffer:byte(2) & 0x7f)
131 if length==126 then
132 if self._length==2 then self._length = 4 goto RECIEVE end
133 if #self._buffer<4 then
134 return nil, nil, "buffer length less than 4"
135 end
136 local b1, b2 = self._buffer:byte(3, 4)
137 self._length = (b1 << 8) + b2
138 elseif length==127 then
139 if self._length==2 then self._length = 10 goto RECIEVE end
140 if #self._buffer<10 then
141 return nil, nil, "buffer length less than 10"
142 end
143 local b5, b6, b7, b8 = self._buffer:byte(7, 10)
144 self._length = (b5 << 24) + (b6 << 16) + (b7 << 8) + b8
145 else
146 self._length = length
147 end
148 self._head, self._buffer = self._buffer:byte(1), ""
149 if length>0 then goto RECIEVE end
150 end
151 if #self._buffer>=self._length then
152 local ret, head = self._buffer, self._head
153 self._length, self._buffer, self._head = 2, "", nil
154 return ret, head, nil
155 else
156 return nil, nil, "buffer length less than "..self._length
157 end
158end
159
160---send a message
161---@param message string
162function _M:send(message)
163 send(self.socket, OPCODE.TEXT, message)
164end
165
166---send a ping message
167---@param message string
168function _M:ping(message)
169 send(self.socket, OPCODE.PING, message)
170end
171
172---send a pong message (no need)
173---@param message any
174function _M:pong(message)
175 send(self.socket, OPCODE.PONG, message)
176end
177
178---update client status
179function _M:update()
180 local sock = self.socket
181 if self.status==STATUS.TCPOPENING then
182 local url = self.url
183 local _, err = sock:connect(url.host, url.port)
184 self._length = self._length+1
185 if err=="already connected" then
186 sock:send(
187"GET "..url.path.." HTTP/1.1\r\n"..
188"Host: "..url.host..":"..url.port.."\r\n"..
189"Connection: Upgrade\r\n"..
190"Upgrade: websocket\r\n"..
191"Sec-WebSocket-Version: 13\r\n"..
192"Sec-WebSocket-Key: "..seckey.."\r\n\r\n")
193 self.status = STATUS.CONNECTING
194 self._length = 2
195 elseif self._length>600 then
196 self:onerror("connection failed")
197 self.status = STATUS.CLOSED
198 end
199 elseif self.status==STATUS.CONNECTING then
200 local res = sock:receive("*l")
201 if res then
202 repeat res = sock:receive("*l") until res==""
203 self:onopen()
204 self.status = STATUS.OPEN
205 end
206 elseif self.status==STATUS.OPEN or self.status==STATUS.CLOSING then
207 while true do
208 local res, head, err = self:read()
209 if err=="closed" then
210 self.status = STATUS.CLOSED
211 return
212 elseif res==nil then
213 return
214 end
215 local opcode = (head & 0x0f)
216 local fin = (head & 0x80)==0x80
217 if opcode==OPCODE.CLOSE then
218 if res~="" then
219 local code = (res:byte(1) << 8) + res:byte(2)
220 self:onclose(code, res:sub(3))
221 else
222 self:onclose(1005, "")
223 end
224 sock:close()
225 self.status = STATUS.CLOSED
226 elseif opcode==OPCODE.PING then self:pong(res)
227 elseif opcode==OPCODE.CONTINUE then
228 self._continue = self._continue..res
229 if fin then self:onmessage(self._continue) end
230 else
231 if fin then self:onmessage(res) else self._continue = res end
232 end
233 end
234 end
235end
236
237---close websocket connection
238---@param code integer|nil
239---@param message string|nil
240function _M:close(code, message)
241 if code and message then
242 send(self.socket, OPCODE.CLOSE, string.char((code >> 8), (code & 0xff))..message)
243 else
244 send(self.socket, OPCODE.CLOSE, nil)
245 end
246 self.status = STATUS.CLOSING
247end
248
249return _M