2 -- Copyright (C) 2012 Florian Zeitz
3 -- Copyright (C) 2014 Daurnimator
5 -- This project is MIT/X11 licensed. Please see the
6 -- COPYING file in the source package for more information.
9 local t_concat
= table.concat
;
11 local http
= require
"net.http";
12 local frames
= require
"net.websocket.frames";
13 local base64
= require
"util.encodings".base64
;
14 local sha1
= require
"util.hashes".sha1
;
15 local random_bytes
= require
"util.random".bytes
;
16 local timer
= require
"util.timer";
17 local log = require
"util.logger".init
"websocket";
19 local close_timeout
= 3; -- Seconds to wait after sending close frame until closing connection.
21 local websockets
= {};
23 local websocket_listeners
= {};
24 function websocket_listeners
.ondisconnect(conn
, err
)
25 local s
= websockets
[conn
];
26 websockets
[conn
] = nil;
28 timer
.stop(s
.close_timer
);
32 if s
.close_code
== nil and s
.onerror
then s
:onerror(err
); end
33 if s
.onclose
then s
:onclose(s
.close_code
, s
.close_message
or err
); end
36 function websocket_listeners
.ondetach(conn
)
37 websockets
[conn
] = nil;
40 local function fail(s
, code
, reason
)
41 log("warn", "WebSocket connection failed, closing. %d %s", code
, reason
);
42 s
:close(code
, reason
);
47 function websocket_listeners
.onincoming(conn
, buffer
, err
) -- luacheck: ignore 212/err
48 local s
= websockets
[conn
];
49 s
.readbuffer
= s
.readbuffer
..buffer
;
51 local frame
, len
= frames
.parse(s
.readbuffer
);
52 if frame
== nil then break end
53 s
.readbuffer
= s
.readbuffer
:sub(len
+1);
55 log("debug", "Websocket received frame: opcode=%0x, %i bytes", frame
.opcode
, #frame
.data
);
58 if frame
.RSV1
or frame
.RSV2
or frame
.RSV3
then -- Reserved bits non zero
59 return fail(s
, 1002, "Reserved bits not zero");
62 if frame
.opcode
< 0x8 then
63 local databuffer
= s
.databuffer
;
64 if frame
.opcode
== 0x0 then -- Continuation frames
65 if not databuffer
then
66 return fail(s
, 1002, "Unexpected continuation frame");
68 databuffer
[#databuffer
+1] = frame
.data
;
69 elseif frame
.opcode
== 0x1 or frame
.opcode
== 0x2 then -- Text or Binary frame
71 return fail(s
, 1002, "Continuation frame expected");
73 databuffer
= {type=frame
.opcode
, frame
.data
};
74 s
.databuffer
= databuffer
;
76 return fail(s
, 1002, "Reserved opcode");
81 s
:onmessage(t_concat(databuffer
), databuffer
.type);
85 if frame
.length
> 125 then -- Control frame with too much payload
86 return fail(s
, 1002, "Payload too large");
87 elseif not frame
.FIN
then -- Fragmented control frame
88 return fail(s
, 1002, "Fragmented control frame");
90 if frame
.opcode
== 0x8 then -- Close request
91 if frame
.length
== 1 then
92 return fail(s
, 1002, "Close frame with payload, but too short for status code");
94 local status_code
, message
= frames
.parse_close(frame
.data
);
95 if status_code
== nil then
97 1005 is a reserved value and MUST NOT be set as a status code in a
98 Close control frame by an endpoint. It is designated for use in
99 applications expecting a status code to indicate that no status
100 code was actually present.
103 elseif status_code
< 1000 then
104 return fail(s
, 1002, "Closed with invalid status code");
105 elseif ((status_code
> 1003 and status_code
< 1007) or status_code
> 1011) and status_code
< 3000 then
106 return fail(s
, 1002, "Closed with reserved status code");
108 s
.close_code
, s
.close_message
= status_code
, message
;
111 elseif frame
.opcode
== 0x9 then -- Ping frame
113 frame
.MASK
= true; -- RFC 6455 6.1.5: If the data is being sent by the client, the frame(s) MUST be masked
114 conn
:write(frames
.build(frame
));
115 elseif frame
.opcode
== 0xA then -- Pong frame
116 log("debug", "Received unexpected pong frame: %s", frame
.data
);
118 return fail(s
, 1002, "Reserved opcode");
125 local websocket_methods
= {};
126 local function close_timeout_cb(now
, timerid
, s
) -- luacheck: ignore 212/now 212/timerid
128 log("warn", "Close timeout waiting for server to close, closing manually.");
131 function websocket_methods
:close(code
, reason
)
132 if self
.readyState
< 2 then
134 log("debug", "closing WebSocket with code %i: %s" , code
, reason
);
136 local conn
= self
.conn
;
137 conn
:write(frames
.build_close(code
, reason
, true));
138 -- Do not close socket straight away, wait for acknowledgement from server.
139 self
.close_timer
= timer
.add_task(close_timeout
, close_timeout_cb
, self
);
140 elseif self
.readyState
== 2 then
141 log("debug", "tried to close a closing WebSocket, closing the raw socket.");
143 if self
.close_timer
then
144 timer
.stop(self
.close_timer
);
145 self
.close_timer
= nil;
147 local conn
= self
.conn
;
150 log("debug", "tried to close a closed WebSocket, ignoring.");
153 function websocket_methods
:send(data
, opcode
)
154 if self
.readyState
< 1 then
155 return nil, "WebSocket not open yet, unable to send data.";
156 elseif self
.readyState
>= 2 then
157 return nil, "WebSocket closed, unable to send data.";
159 if opcode
== "text" or opcode
== nil then
161 elseif opcode
== "binary" then
166 MASK
= true; -- RFC 6455 6.1.5: If the data is being sent by the client, the frame(s) MUST be masked
168 data
= tostring(data
);
170 log("debug", "WebSocket sending frame: opcode=%0x, %i bytes", frame
.opcode
, #frame
.data
);
171 return self
.conn
:write(frames
.build(frame
));
174 local websocket_metatable
= {
175 __index
= websocket_methods
;
178 local function connect(url
, ex
, listeners
)
182 The request MUST include a header field with the name
183 |Sec-WebSocket-Key|. The value of this header field MUST be a
184 nonce consisting of a randomly selected 16-byte value that has
185 been base64-encoded (see Section 4 of [RFC4648]). The nonce
186 MUST be selected randomly for each connection.
188 local key
= base64
.encode(random_bytes(16));
190 -- Either a single protocol string or an array of protocol strings.
191 local protocol
= ex
.protocol
;
192 if type(protocol
) == "string" then
193 protocol
= { protocol
, [protocol
] = true };
194 elseif type(protocol
) == "table" and protocol
[1] then
195 for _
, v
in ipairs(protocol
) do
203 ["Upgrade"] = "websocket";
204 ["Connection"] = "Upgrade";
205 ["Sec-WebSocket-Key"] = key
;
206 ["Sec-WebSocket-Protocol"] = protocol
and t_concat(protocol
, ", ");
207 ["Sec-WebSocket-Version"] = "13";
208 ["Sec-WebSocket-Extensions"] = ex
.extensions
;
211 for k
,v
in pairs(ex
.headers
) do
216 local s
= setmetatable({
228 onopen
= listeners
.onopen
;
229 onclose
= listeners
.onclose
;
230 onmessage
= listeners
.onmessage
;
231 onerror
= listeners
.onerror
;
232 }, websocket_metatable
);
234 local http_url
= url
:gsub("^(ws)", "http");
235 local http_req
= http
.request(http_url
, { -- luacheck: ignore 211/http_req
239 insecure
= ex
.insecure
;
240 }, function(b
, c
, r
, http_req
)
242 or r
.headers
["connection"]:lower() ~= "upgrade"
243 or r
.headers
["upgrade"] ~= "websocket"
244 or r
.headers
["sec-websocket-accept"] ~= base64
.encode(sha1(key
.. "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"))
245 or (protocol
and not protocol
[r
.headers
["sec-websocket-protocol"]]
)
248 log("warn", "WebSocket connection to %s failed: %s", url
, b
);
249 if s
.onerror
then s
:onerror("connecting-failed"); end
253 s
.protocol
= r
.headers
["sec-websocket-protocol"];
255 -- Take possession of socket from http
256 local conn
= http_req
.conn
;
259 websockets
[conn
] = s
;
260 conn
:setlistener(websocket_listeners
);
262 log("debug", "WebSocket connected successfully to %s", url
);
264 if s
.onopen
then s
:onopen(); end
265 websocket_listeners
.onincoming(conn
, b
);