From dcca712d967dbb677e17c9688fa53e8d637d813c Mon Sep 17 00:00:00 2001 From: daurnimator Date: Wed, 23 Dec 2015 21:28:38 +1100 Subject: [PATCH 01/31] http/websocket: WIP - Imported code from prosody - Started refactoring Has WIP negotiation code at bottom of module --- http/websocket.lua | 459 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 459 insertions(+) create mode 100644 http/websocket.lua diff --git a/http/websocket.lua b/http/websocket.lua new file mode 100644 index 00000000..0c75519f --- /dev/null +++ b/http/websocket.lua @@ -0,0 +1,459 @@ +--[[ +Uses code from prosody's net/websocket +Some portions Copyright (C) 2012 Florian Zeitz +]] + +local basexx = require "basexx" +local spack = string.pack or require "compat53.string".pack +local sunpack = string.unpack or require "compat53.string".unpack +local unpack = table.unpack or unpack -- luacheck: ignore 113 +local cqueues = require "cqueues" +local monotime = cqueues.monotime +local uri_patts = require "lpeg_patterns.uri" +local rand = require "openssl.rand" +local digest = require "openssl.digest" +local new_headers = require "http.headers".new +local bit = require "http.bit" +local http_request = require "http.request" + +-- Seconds to wait after sending close frame until closing connection. +local close_timeout = 3 + +-- a nonce consisting of a randomly selected 16-byte value that has been base64-encoded +local function new_key() + return basexx.to_base64(rand.bytes(16)) +end + +local magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" + +local function base64_sha1(str) + return basexx.to_base64(digest.new("sha1"):final(str)) +end + +-- XORs the string `str` with a 32bit key +local function apply_mask(str, key) + assert(#key == 4) + local data = {} + for i = 1, #str do + local key_index = (i-1)%4 + 1 + data[i] = string.char(bit.bxor(key[key_index], str:byte(i))) + end + return table.concat(data) +end + +local function build_frame(desc) + local data = desc.data or "" + + assert(desc.opcode and desc.opcode >= 0 and desc.opcode <= 0xF, "Invalid WebSocket opcode") + if desc.opcode >= 0x8 then + -- RFC 6455 5.5 + assert(#data <= 125, "WebSocket control frames MUST have a payload length of 125 bytes or less.") + end + + local b1 = bit.bor(desc.opcode, + desc.FIN and 0x80 or 0, + desc.RSV1 and 0x40 or 0, + desc.RSV2 and 0x20 or 0, + desc.RSV3 and 0x10 or 0) + + local b2 = #data + local length_extra + if b2 <= 125 then -- 7-bit length + length_extra = "" + elseif b2 <= 0xFFFF then -- 2-byte length + b2 = 126 + length_extra = spack(">I2", #data) + else -- 8-byte length + b2 = 127 + length_extra = spack(">I8", #data) + end + + local key = "" + if desc.MASK then + local key_a = desc.key + if key_a then + key = string.char(unpack(key_a, 1, 4)) + else + key = rand.bytes(4) + key_a = {key:byte(1,4)} + end + b2 = bit.bor(b2, 0x80) + data = apply_mask(data, key_a) + end + + return string.char(b1, b2) .. length_extra .. key .. data +end + +local function build_close(code, message, mask) + local data = spack(">I2", code) + if message then + assert(#message<=123, "Close reason must be <=123 bytes") + data = data .. message + end + return build_frame { + opcode = 0x8; + FIN = true; + MASK = mask; + data = data; + } +end + +local function read_frame(sock, deadline) + local frame do + local first_2, err, errno = sock:xread(2, deadline and (deadline-monotime())) + if not first_2 then + return nil, err, errno + end + local byte1, byte2 = first_2:byte(1, 2) + frame = { + FIN = bit.band(byte1, 0x80) ~= 0; + RSV1 = bit.band(byte1, 0x40) ~= 0; + RSV2 = bit.band(byte1, 0x20) ~= 0; + RSV3 = bit.band(byte1, 0x10) ~= 0; + opcode = bit.band(byte1, 0x0F); + + MASK = bit.band(byte2, 0x80) ~= 0; + length = bit.band(byte2, 0x7F); + + data = nil; + } + end + + if frame.length == 126 then + local length, err, errno = sock:xread(2, deadline and (deadline-monotime())) + if not length then + return nil, err, errno + end + frame.length = sunpack(">I2", length) + elseif frame.length == 127 then + local length, err, errno = sock:xread(8, deadline and (deadline-monotime())) + if not length then + return nil, err, errno + end + frame.length = sunpack(">I8", length) + end + + if frame.MASK then + local key, err, errno = sock:xread(4, deadline and (deadline-monotime())) + if not key then + return nil, err, errno + end + frame.key = { key:byte(1, 4) } + end + + do + local data, err, errno = sock:xread(frame.length, deadline and (deadline-monotime())) + if data == nil then + return nil, err, errno + end + + if frame.MASK then + frame.data = apply_mask(data, frame.key) + else + frame.data = data + end + end + + return frame +end + +local function parse_close(data) + local code, message + if #data >= 2 then + code = sunpack(">I2", data) + if #data > 2 then + message = data:sub(3) + end + end + return code, message +end + +local function read_loop(sock, on_data, on_close) + local code, reason = 1000, nil + local databuffer, databuffer_type + while true do + local frame, err, errno = read_frame(sock) + if frame == nil then + return nil, err, errno + end + + -- Error cases + if frame.RSV1 or frame.RSV2 or frame.RSV3 then -- Reserved bits non zero + code, reason = 1002, "Reserved bits not zero" + break + end + + if frame.opcode < 0x8 then + if frame.opcode == 0x0 then -- Continuation frames + if not databuffer then + code, reason = 1002, "Unexpected continuation frame" + break + end + databuffer[#databuffer+1] = frame.data + elseif frame.opcode == 0x1 or frame.opcode == 0x2 then -- Text or Binary frame + if databuffer then + code, reason = 1002, "Continuation frame expected" + break + end + databuffer = { frame.data } + if frame.opcode == 0x1 then + databuffer_type = "text" + else + databuffer_type = "binary" + end + else + code, reason = 1002, "Reserved opcode" + break + end + if frame.FIN then + on_data(databuffer_type, table.concat(databuffer)) + databuffer, databuffer_type = nil, nil + end + else -- Control frame + if frame.length > 125 then -- Control frame with too much payload + code, reason = 1002, "Payload too large" + break + elseif not frame.FIN then -- Fragmented control frame + code, reason = 1002, "Fragmented control frame" + break + end + if frame.opcode == 0x8 then -- Close request + if frame.length == 1 then + code, reason = 1002, "Close frame with payload, but too short for status code" + break + end + local status_code, message = parse_close(frame.data) + if status_code == nil then + --[[ RFC 6455 7.4.1 + 1005 is a reserved value and MUST NOT be set as a status code in a + Close control frame by an endpoint. It is designated for use in + applications expecting a status code to indicate that no status + code was actually present. + ]] + status_code = 1005 + elseif status_code < 1000 then + code, reason = 1002, "Closed with invalid status code" + break + elseif ((status_code > 1003 and status_code < 1007) or status_code > 1011) and status_code < 3000 then + code, reason = 1002, "Closed with reserved status code" + break + end + code, reason = 1000, nil + on_close(status_code, message) + break + elseif frame.opcode == 0x9 then -- Ping frame + frame.opcode = 0xA + frame.MASK = true; -- RFC 6455 6.1.5: If the data is being sent by the client, the frame(s) MUST be masked + sock:write(build_frame(frame)) + elseif frame.opcode == 0xA then -- luacheck: ignore 542 + -- Received unexpected pong frame + else + code, reason = 1002, "Reserved opcode" + break + end + end + end + + if sock:xwrite(build_close(code, reason, true), "n") then + -- Do not close socket straight away, wait for acknowledgement from server. + cqueues.poll(sock, close_timeout) + end + + sock:close() + + return true +end + + +local function new_from_uri_t(uri_t, protocols) + local scheme = assert(uri_t.scheme, "URI missing scheme") + assert(scheme == "ws" or scheme == "wss", "scheme not websocket") + local headers = new_headers() + headers:append("connection", "upgrade") + headers:append("upgrade", "websocket") + headers:append("sec-websocket-key", new_key(), true) + if protocols then + --[[ The request MAY include a header field with the name + |Sec-WebSocket-Protocol|. If present, this value indicates one + or more comma-separated subprotocol the client wishes to speak, + ordered by preference. The elements that comprise this value + MUST be non-empty strings with characters in the range U+0021 to + U+007E not including separator characters as defined in + [RFC2616] and MUST all be unique strings. ]] + -- TODO: protocol validation + headers:append("sec-websocket-protocol", table.concat(protocols, ",")) + end + headers:append("sec-websocket-version", "13") + local req = http_request.new_from_uri_t(uri_t, headers) + return req +end + +local function new_from_uri(uri, ...) + local uri_t = assert(uri_patts.uri:match(uri), "invalid URI") + uri_t.scheme = uri_t.scheme or "ws" -- default to ws + return new_from_uri_t(uri_t, ...) +end + +do + local function has(list, val) + for i=1, list.n do + if list[i]:lower() == val then + return true + end + end + return false + end + local function has_any(list1, list2) + for i=1, list2.n do + if has(list1, list2[i]) then + return true + end + end + return false + end + + -- trim12 from http://lua-users.org/wiki/StringTrim + local function trim(s) + local from = s:match"^%s*()" + return from > #s and "" or s:match(".*%S", from) + end + + local req = new_from_uri("ws://echo.websocket.org") + local stream = req:new_stream() + assert(stream:write_headers(req.headers, false)) + local headers = assert(stream:get_headers()) + -- TODO: redirects + if headers:get(":status") == "101" + --[[ If the response lacks an |Upgrade| header field or the |Upgrade| + header field contains a value that is not an ASCII case- + insensitive match for the value "websocket", the client MUST + _Fail the WebSocket Connection_.]] + and headers:get("upgrade"):lower() == "websocket" + --[[ If the response lacks a |Connection| header field or the + |Connection| header field doesn't contain a token that is an + ASCII case-insensitive match for the value "Upgrade", the client + MUST _Fail the WebSocket Connection_.]] + and has(headers:get_split_as_sequence("connection"), "upgrade") + --[[ If the response lacks a |Sec-WebSocket-Accept| header field or + the |Sec-WebSocket-Accept| contains a value other than the + base64-encoded SHA-1 of the concatenation of the |Sec-WebSocket- + Key| (as a string, not base64-decoded) with the string "258EAFA5- + E914-47DA-95CA-C5AB0DC85B11" but ignoring any leading and + trailing whitespace, the client MUST _Fail the WebSocket + Connection_.]] + and trim(headers:get("sec-websocket-accept")) == base64_sha1(trim(req.headers:get("sec-websocket-key"))..magic) + --[[ If the response includes a |Sec-WebSocket-Extensions| header + field and this header field indicates the use of an extension + that was not present in the client's handshake (the server has + indicated an extension not requested by the client), the client + MUST _Fail the WebSocket Connection_.]] + -- For now, we don't support any extensions + and headers:get_split_as_sequence("sec-websocket-extensions").n == 0 + --[[ If the response includes a |Sec-WebSocket-Protocol| header field + and this header field indicates the use of a subprotocol that was + not present in the client's handshake (the server has indicated a + subprotocol not requested by the client), the client MUST _Fail + the WebSocket Connection_.]] + and (not headers:has("sec-websocket-protocol") + or has_any(headers:get_split_as_sequence("sec-websocket-protocol"), req.headers:get_split_as_sequence("sec-websocket-protocol"))) + then + -- Success! + print(stream) + local sock = stream.connection:take_socket() + print(sock) + + local function send(data, opcode) + -- if self.readyState < 1 then + -- return nil, "WebSocket not open yet, unable to send data." + -- elseif self.readyState >= 2 then + -- return nil, "WebSocket closed, unable to send data." + -- end + if opcode == "text" or opcode == nil then + opcode = 0x1 + elseif opcode == "binary" then + opcode = 0x2; + end + return sock:xwrite(build_frame{ + FIN = true; + MASK = true; -- RFC 6455 6.1.5: If the data is being sent by the client, the frame(s) MUST be masked + opcode = opcode; + data = tostring(data); + }, "n") + end + + + -- function websocket_methods:close(code, reason) + -- if self.readyState < 2 then + -- code = code or 1000; + -- log("debug", "closing WebSocket with code %i: %s" , code , tostring(reason)); + -- self.readyState = 2; + -- local handler = self.handler; + -- handler:write(frames.build_close(code, reason, true)); + -- -- Do not close socket straight away, wait for acknowledgement from server. + -- self.close_timer = timer.add_task(close_timeout, close_timeout_cb, self); + -- elseif self.readyState == 2 then + -- log("debug", "tried to close a closing WebSocket, closing the raw socket."); + -- -- Stop timer + -- if self.close_timer then + -- timer.stop(self.close_timer); + -- self.close_timer = nil; + -- end + -- local handler = self.handler; + -- handler:close(); + -- else + -- log("debug", "tried to close a closed WebSocket, ignoring."); + -- end + -- end + + local new_fifo = require "fifo" + local cc = require "cqueues.condition" + local cond = cc.new() + local q = new_fifo() + q:setempty(function() + cond:wait() + return q:pop() + end) + local cq = cqueues.new() + cq:wrap(function() + local ok, err = read_loop(sock, function(type, data) + q:push({type, data}) + cond:signal(1) + end, print) + if not ok then + error(err) + end + end) + local function get_next(f) + local ob = f:pop() + local type, data = ob[1], ob[2] + return type, data + end + local function each() + return get_next, q + end + + + cq:wrap(function() + for type, data in each() do + print("QWEWE", type, data) + end + end) + cq:wrap(function() + send("foo") + cqueues.sleep(1) + send("bar") + send("bar", "binary") + end) + assert(cq:loop()) + else + print("FAIL") + headers:dump() + end +end + +return { + new_from_uri_t = new_from_uri_t; + new_from_uri = new_from_uri; +} + + From a8388bca18bd99c3f24a511894d3820706f483c1 Mon Sep 17 00:00:00 2001 From: daurnimator Date: Sun, 27 Dec 2015 01:49:23 +1100 Subject: [PATCH 02/31] http/websocket: WIP Refactor into a websocket "object" --- http/websocket.lua | 456 +++++++++++++++++++++++++-------------------- 1 file changed, 253 insertions(+), 203 deletions(-) diff --git a/http/websocket.lua b/http/websocket.lua index 0c75519f..6f5eb5f3 100644 --- a/http/websocket.lua +++ b/http/websocket.lua @@ -1,6 +1,14 @@ --[[ -Uses code from prosody's net/websocket -Some portions Copyright (C) 2012 Florian Zeitz +WebSocket module + +Specified in RFC-6455 + +Design criteria: + - Client API must work without an event loop + - Borrow from the Browser Javascript WebSocket API when sensible + - server-side API should mirror client-side API + +This code is partially based on MIT/X11 code Copyright (C) 2012 Florian Zeitz ]] local basexx = require "basexx" @@ -9,27 +17,40 @@ local sunpack = string.unpack or require "compat53.string".unpack local unpack = table.unpack or unpack -- luacheck: ignore 113 local cqueues = require "cqueues" local monotime = cqueues.monotime +local ce = require "cqueues.errno" local uri_patts = require "lpeg_patterns.uri" local rand = require "openssl.rand" local digest = require "openssl.digest" -local new_headers = require "http.headers".new local bit = require "http.bit" local http_request = require "http.request" --- Seconds to wait after sending close frame until closing connection. -local close_timeout = 3 +local websocket_methods = { + -- Max seconds to wait after sending close frame until closing connection + close_timeout = 3; +} + +local websocket_mt = { + __name = "http.websocket"; + __index = websocket_methods; +} + +local magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" -- a nonce consisting of a randomly selected 16-byte value that has been base64-encoded local function new_key() return basexx.to_base64(rand.bytes(16)) end -local magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" - local function base64_sha1(str) return basexx.to_base64(digest.new("sha1"):final(str)) end +-- trim12 from http://lua-users.org/wiki/StringTrim +local function trim(s) + local from = s:match"^%s*()" + return from > #s and "" or s:match(".*%S", from) +end + -- XORs the string `str` with a 32bit key local function apply_mask(str, key) assert(#key == 4) @@ -90,7 +111,7 @@ local function build_close(code, message, mask) assert(#message<=123, "Close reason must be <=123 bytes") data = data .. message end - return build_frame { + return { opcode = 0x8; FIN = true; MASK = mask; @@ -168,32 +189,100 @@ local function parse_close(data) return code, message end -local function read_loop(sock, on_data, on_close) - local code, reason = 1000, nil +function websocket_methods:send_frame(frame, timeout) + local ok, err, errno = self.socket:xwrite(build_frame(frame), "n", timeout) + if not ok then + return nil, err, errno + end + if frame.opcode == 0x8 then + self.readyState = 2 + end + return true +end + +function websocket_methods:send(data, opcode) + if self.readyState >= 2 then + return nil, "WebSocket closed, unable to send data", ce.EPIPE + end + assert(type(data) == "string") + if opcode == "text" or opcode == nil then + opcode = 0x1 + elseif opcode == "binary" then + opcode = 0x2; + end + return self:send_frame({ + FIN = true; + --[[ RFC 6455 + 5.1: A server MUST NOT mask any frames that it sends to the client + 6.1.5: If the data is being sent by the client, the frame(s) MUST be masked]] + MASK = self.type == "client"; + opcode = opcode; + data = data; + }) +end + +local function close_helper(self, code, reason, deadline) + if self.readyState == 3 then + return nil, ce.strerror(ce.EPIPE), ce.EPIPE + end + + if self.readyState < 2 then + local close_frame = build_close(code, reason, true) + -- ignore failure + self:send_frame(close_frame, deadline and deadline-monotime()) + end + + -- Do not close socket straight away, wait for acknowledgement from server + local read_deadline = monotime() + self.close_timeout + if deadline then + read_deadline = math.min(read_deadline, deadline) + end + while not self.got_close_code do + if not self:read(read_deadline-monotime()) then + break + end + end + + self.socket:shutdown() + cqueues.poll() + cqueues.poll() + self.socket:close() + + self.readyState = 3 + + return nil, reason, ce.ENOMSG +end + +function websocket_methods:close(code, reason, timeout) + local deadline = timeout and (monotime()+timeout) + code = code or 1000 + close_helper(self, code, reason, deadline) + return true +end + +function websocket_methods:read(timeout) + local deadline = timeout and (monotime()+timeout) local databuffer, databuffer_type while true do - local frame, err, errno = read_frame(sock) + local frame, err, errno = read_frame(self.socket, deadline and (deadline-monotime())) if frame == nil then return nil, err, errno end -- Error cases if frame.RSV1 or frame.RSV2 or frame.RSV3 then -- Reserved bits non zero - code, reason = 1002, "Reserved bits not zero" - break + return close_helper(self, 1002, "Reserved bits not zero", deadline) end if frame.opcode < 0x8 then if frame.opcode == 0x0 then -- Continuation frames if not databuffer then - code, reason = 1002, "Unexpected continuation frame" - break + return close_helper(self, 1002, "Unexpected continuation frame", deadline) end databuffer[#databuffer+1] = frame.data elseif frame.opcode == 0x1 or frame.opcode == 0x2 then -- Text or Binary frame if databuffer then - code, reason = 1002, "Continuation frame expected" - break + return close_helper(self, 1002, "Continuation frame expected", deadline) end databuffer = { frame.data } if frame.opcode == 0x1 then @@ -202,25 +291,20 @@ local function read_loop(sock, on_data, on_close) databuffer_type = "binary" end else - code, reason = 1002, "Reserved opcode" - break + return close_helper(self, 1002, "Reserved opcode", deadline) end if frame.FIN then - on_data(databuffer_type, table.concat(databuffer)) - databuffer, databuffer_type = nil, nil + return table.concat(databuffer), databuffer_type end else -- Control frame if frame.length > 125 then -- Control frame with too much payload - code, reason = 1002, "Payload too large" - break + return close_helper(self, 1002, "Payload too large", deadline) elseif not frame.FIN then -- Fragmented control frame - code, reason = 1002, "Fragmented control frame" - break + return close_helper(self, 1002, "Fragmented control frame", deadline) end if frame.opcode == 0x8 then -- Close request if frame.length == 1 then - code, reason = 1002, "Close frame with payload, but too short for status code" - break + return close_helper(self, 1002, "Close frame with payload, but too short for status code", deadline) end local status_code, message = parse_close(frame.data) if status_code == nil then @@ -228,232 +312,198 @@ local function read_loop(sock, on_data, on_close) 1005 is a reserved value and MUST NOT be set as a status code in a Close control frame by an endpoint. It is designated for use in applications expecting a status code to indicate that no status - code was actually present. - ]] + code was actually present.]] status_code = 1005 elseif status_code < 1000 then - code, reason = 1002, "Closed with invalid status code" - break + return close_helper(self, 1002, "Closed with invalid status code", deadline) elseif ((status_code > 1003 and status_code < 1007) or status_code > 1011) and status_code < 3000 then - code, reason = 1002, "Closed with reserved status code" - break + return close_helper(self, 1002, "Closed with reserved status code", deadline) end - code, reason = 1000, nil - on_close(status_code, message) - break + self.got_close_code = status_code + self.got_close_message = message + return close_helper(self, status_code, message, deadline) elseif frame.opcode == 0x9 then -- Ping frame frame.opcode = 0xA - frame.MASK = true; -- RFC 6455 6.1.5: If the data is being sent by the client, the frame(s) MUST be masked - sock:write(build_frame(frame)) + --[[ RFC 6455 + 5.1: A server MUST NOT mask any frames that it sends to the client + 6.1.5: If the data is being sent by the client, the frame(s) MUST be masked]] + frame.MASK = self.type == "client"; + if not self:send_frame(frame, deadline and (deadline-monotime())) then + return close_helper(self, 1002, "Pong failed", deadline) + end elseif frame.opcode == 0xA then -- luacheck: ignore 542 -- Received unexpected pong frame else - code, reason = 1002, "Reserved opcode" - break + return close_helper(self, 1002, "Reserved opcode", deadline) end end end +end - if sock:xwrite(build_close(code, reason, true), "n") then - -- Do not close socket straight away, wait for acknowledgement from server. - cqueues.poll(sock, close_timeout) - end - - sock:close() - - return true +function websocket_methods:each() + return function(self) -- luacheck: ignore 432 + return self:read() + end, self end +local function new(type) + assert(type == "client") + local self = setmetatable({ + socket = nil; + type = type; + readyState = 0; + got_close_code = nil; + got_close_reason = nil; + key = nil; + protocols = nil; + request = nil; + }, websocket_mt) + return self +end local function new_from_uri_t(uri_t, protocols) local scheme = assert(uri_t.scheme, "URI missing scheme") assert(scheme == "ws" or scheme == "wss", "scheme not websocket") - local headers = new_headers() - headers:append("connection", "upgrade") - headers:append("upgrade", "websocket") - headers:append("sec-websocket-key", new_key(), true) + local self = new("client") + self.request = http_request.new_from_uri_t(uri_t) + self.request.version = 1.1 + self.request.headers:append("connection", "upgrade") + self.request.headers:append("upgrade", "websocket") + self.key = new_key() + self.request.headers:append("sec-websocket-key", self.key, true) + self.request.headers:append("sec-websocket-version", "13") if protocols then --[[ The request MAY include a header field with the name - |Sec-WebSocket-Protocol|. If present, this value indicates one + Sec-WebSocket-Protocol. If present, this value indicates one or more comma-separated subprotocol the client wishes to speak, - ordered by preference. The elements that comprise this value + ordered by preference. The elements that comprise this value MUST be non-empty strings with characters in the range U+0021 to U+007E not including separator characters as defined in - [RFC2616] and MUST all be unique strings. ]] + [RFC2616] and MUST all be unique strings.]] -- TODO: protocol validation - headers:append("sec-websocket-protocol", table.concat(protocols, ",")) + self.protocols = protocols + self.request.headers:append("sec-websocket-protocol", table.concat(protocols, ",")) end - headers:append("sec-websocket-version", "13") - local req = http_request.new_from_uri_t(uri_t, headers) - return req + return self end local function new_from_uri(uri, ...) local uri_t = assert(uri_patts.uri:match(uri), "invalid URI") - uri_t.scheme = uri_t.scheme or "ws" -- default to ws return new_from_uri_t(uri_t, ...) end -do - local function has(list, val) - for i=1, list.n do - if list[i]:lower() == val then - return true - end - end - return false +local function handle_websocket_response(self, headers, stream) + assert(self.type == "client" and self.readyState == 0) + + if stream.connection.version < 1 or stream.connection.version >= 2 then + return nil, "websockets only supported with HTTP 1.x", ce.EINVAL end - local function has_any(list1, list2) - for i=1, list2.n do - if has(list1, list2[i]) then - return true - end - end - return false + + --[[ If the status code received from the server is not 101, the + client handles the response per HTTP [RFC2616] procedures. In + particular, the client might perform authentication if it + receives a 401 status code; the server might redirect the client + using a 3xx status code (but clients are not required to follow + them), etc.]] + if headers:get(":status") ~= "101" then + return nil, "status code not 101", ce.EINVAL end - -- trim12 from http://lua-users.org/wiki/StringTrim - local function trim(s) - local from = s:match"^%s*()" - return from > #s and "" or s:match(".*%S", from) + --[[ If the response lacks an Upgrade header field or the Upgrade + header field contains a value that is not an ASCII case- + insensitive match for the value "websocket", the client MUST + Fail the WebSocket Connection]] + local upgrade = headers:get("upgrade") + if not upgrade or upgrade:lower() ~= "websocket" then + return nil, "upgrade header not websocket", ce.EINVAL end - local req = new_from_uri("ws://echo.websocket.org") - local stream = req:new_stream() - assert(stream:write_headers(req.headers, false)) - local headers = assert(stream:get_headers()) - -- TODO: redirects - if headers:get(":status") == "101" - --[[ If the response lacks an |Upgrade| header field or the |Upgrade| - header field contains a value that is not an ASCII case- - insensitive match for the value "websocket", the client MUST - _Fail the WebSocket Connection_.]] - and headers:get("upgrade"):lower() == "websocket" - --[[ If the response lacks a |Connection| header field or the - |Connection| header field doesn't contain a token that is an - ASCII case-insensitive match for the value "Upgrade", the client - MUST _Fail the WebSocket Connection_.]] - and has(headers:get_split_as_sequence("connection"), "upgrade") - --[[ If the response lacks a |Sec-WebSocket-Accept| header field or - the |Sec-WebSocket-Accept| contains a value other than the - base64-encoded SHA-1 of the concatenation of the |Sec-WebSocket- - Key| (as a string, not base64-decoded) with the string "258EAFA5- - E914-47DA-95CA-C5AB0DC85B11" but ignoring any leading and - trailing whitespace, the client MUST _Fail the WebSocket - Connection_.]] - and trim(headers:get("sec-websocket-accept")) == base64_sha1(trim(req.headers:get("sec-websocket-key"))..magic) - --[[ If the response includes a |Sec-WebSocket-Extensions| header - field and this header field indicates the use of an extension - that was not present in the client's handshake (the server has - indicated an extension not requested by the client), the client - MUST _Fail the WebSocket Connection_.]] - -- For now, we don't support any extensions - and headers:get_split_as_sequence("sec-websocket-extensions").n == 0 - --[[ If the response includes a |Sec-WebSocket-Protocol| header field - and this header field indicates the use of a subprotocol that was - not present in the client's handshake (the server has indicated a - subprotocol not requested by the client), the client MUST _Fail - the WebSocket Connection_.]] - and (not headers:has("sec-websocket-protocol") - or has_any(headers:get_split_as_sequence("sec-websocket-protocol"), req.headers:get_split_as_sequence("sec-websocket-protocol"))) - then - -- Success! - print(stream) - local sock = stream.connection:take_socket() - print(sock) - - local function send(data, opcode) - -- if self.readyState < 1 then - -- return nil, "WebSocket not open yet, unable to send data." - -- elseif self.readyState >= 2 then - -- return nil, "WebSocket closed, unable to send data." - -- end - if opcode == "text" or opcode == nil then - opcode = 0x1 - elseif opcode == "binary" then - opcode = 0x2; - end - return sock:xwrite(build_frame{ - FIN = true; - MASK = true; -- RFC 6455 6.1.5: If the data is being sent by the client, the frame(s) MUST be masked - opcode = opcode; - data = tostring(data); - }, "n") + --[[ If the response lacks a Connection header field or the + Connection header field doesn't contain a token that is an + ASCII case-insensitive match for the value "Upgrade", the client + MUST Fail the WebSocket Connection]] + local has_connection_upgrade = false + local connection_header = headers:get_split_as_sequence("connection") + for i=1, connection_header.n do + if connection_header[i]:lower() == "upgrade" then + has_connection_upgrade = true + break end + end + if not has_connection_upgrade then + return nil, "connection header doesn't contain upgrade", ce.EINVAL + end + + --[[ If the response lacks a Sec-WebSocket-Accept header field or + the Sec-WebSocket-Accept contains a value other than the + base64-encoded SHA-1 of the concatenation of the Sec-WebSocket- + Key (as a string, not base64-decoded) with the string "258EAFA5- + E914-47DA-95CA-C5AB0DC85B11" but ignoring any leading and + trailing whitespace, the client MUST Fail the WebSocket Connection]] + local sec_websocket_accept = headers:get("sec-websocket-accept") + if sec_websocket_accept == nil or + trim(sec_websocket_accept) ~= base64_sha1(self.key .. magic) + then + return nil, "sec-websocket-accept header incorrect", ce.EINVAL + end + --[[ If the response includes a Sec-WebSocket-Extensions header field and + this header field indicates the use of an extension that was not present + in the client's handshake (the server has indicated an extension not + requested by the client), the client MUST Fail the WebSocket Connection]] + -- For now, we don't support any extensions + if headers:has("sec-websocket-extensions") then + return nil, "extensions not supported", ce.EINVAL + end - -- function websocket_methods:close(code, reason) - -- if self.readyState < 2 then - -- code = code or 1000; - -- log("debug", "closing WebSocket with code %i: %s" , code , tostring(reason)); - -- self.readyState = 2; - -- local handler = self.handler; - -- handler:write(frames.build_close(code, reason, true)); - -- -- Do not close socket straight away, wait for acknowledgement from server. - -- self.close_timer = timer.add_task(close_timeout, close_timeout_cb, self); - -- elseif self.readyState == 2 then - -- log("debug", "tried to close a closing WebSocket, closing the raw socket."); - -- -- Stop timer - -- if self.close_timer then - -- timer.stop(self.close_timer); - -- self.close_timer = nil; - -- end - -- local handler = self.handler; - -- handler:close(); - -- else - -- log("debug", "tried to close a closed WebSocket, ignoring."); - -- end - -- end - - local new_fifo = require "fifo" - local cc = require "cqueues.condition" - local cond = cc.new() - local q = new_fifo() - q:setempty(function() - cond:wait() - return q:pop() - end) - local cq = cqueues.new() - cq:wrap(function() - local ok, err = read_loop(sock, function(type, data) - q:push({type, data}) - cond:signal(1) - end, print) - if not ok then - error(err) + --[[ If the response includes a Sec-WebSocket-Protocol header field and + this header field indicates the use of a subprotocol that was not present + in the client's handshake (the server has indicated a subprotocol not + requested by the client), the client MUST Fail the WebSocket Connection]] + if headers:has("sec-websocket-protocol") then + local has_matching_protocol = false + if self.protocols then + local swps = headers:get_split_as_sequence("sec-websocket-protocol") + for i=1, swps.n do + local p1 = swps[i]:lower() + for _, p2 in ipairs(self.protocols) do + if p1 == p2 then + has_matching_protocol = true + break + end + end + if has_matching_protocol then + break + end end - end) - local function get_next(f) - local ob = f:pop() - local type, data = ob[1], ob[2] - return type, data end - local function each() - return get_next, q + if not has_matching_protocol then + return nil, "unexpected protocol", ce.EINVAL end + end + -- Success! + self.socket = assert(stream.connection:take_socket()) + self.readyState = 1 - cq:wrap(function() - for type, data in each() do - print("QWEWE", type, data) - end - end) - cq:wrap(function() - send("foo") - cqueues.sleep(1) - send("bar") - send("bar", "binary") - end) - assert(cq:loop()) - else - print("FAIL") - headers:dump() + return true +end + +function websocket_methods:connect(timeout) + assert(self.type == "client" and self.readyState == 0) + local headers, stream, errno = self.request:go(timeout) + if not headers then + return nil, stream, errno end + return handle_websocket_response(self, headers, stream) end return { new_from_uri_t = new_from_uri_t; new_from_uri = new_from_uri; -} - + build_frame = build_frame; + read_frame = read_frame; + build_close = build_close; + parse_close = parse_close; +} From d239acaa897f6f2b2a1e6d236e9d657cce99211d Mon Sep 17 00:00:00 2001 From: daurnimator Date: Wed, 20 Jan 2016 22:06:03 +1100 Subject: [PATCH 03/31] http/websocket: WIP; client side working --- http/websocket.lua | 36 +++++++++++++++++------------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/http/websocket.lua b/http/websocket.lua index 6f5eb5f3..5655ef28 100644 --- a/http/websocket.lua +++ b/http/websocket.lua @@ -200,7 +200,7 @@ function websocket_methods:send_frame(frame, timeout) return true end -function websocket_methods:send(data, opcode) +function websocket_methods:send(data, opcode, timeout) if self.readyState >= 2 then return nil, "WebSocket closed, unable to send data", ce.EPIPE end @@ -218,7 +218,7 @@ function websocket_methods:send(data, opcode) MASK = self.type == "client"; opcode = opcode; data = data; - }) + }, timeout) end local function close_helper(self, code, reason, deadline) @@ -285,15 +285,16 @@ function websocket_methods:read(timeout) return close_helper(self, 1002, "Continuation frame expected", deadline) end databuffer = { frame.data } - if frame.opcode == 0x1 then - databuffer_type = "text" - else - databuffer_type = "binary" - end + databuffer_type = frame.opcode else return close_helper(self, 1002, "Reserved opcode", deadline) end if frame.FIN then + if databuffer_type == 0x1 then + databuffer_type = "text" + elseif databuffer_type == 0x2 then + databuffer_type = "binary" + end return table.concat(databuffer), databuffer_type end else -- Control frame @@ -367,8 +368,8 @@ local function new_from_uri_t(uri_t, protocols) local self = new("client") self.request = http_request.new_from_uri_t(uri_t) self.request.version = 1.1 - self.request.headers:append("connection", "upgrade") self.request.headers:append("upgrade", "websocket") + self.request.headers:append("connection", "upgrade") self.key = new_key() self.request.headers:append("sec-websocket-key", self.key, true) self.request.headers:append("sec-websocket-version", "13") @@ -392,6 +393,8 @@ local function new_from_uri(uri, ...) return new_from_uri_t(uri_t, ...) end +--[[ Takes a response to a websocket upgrade request, +and attempts to complete a websocket connection]] local function handle_websocket_response(self, headers, stream) assert(self.type == "client" and self.readyState == 0) @@ -460,19 +463,13 @@ local function handle_websocket_response(self, headers, stream) this header field indicates the use of a subprotocol that was not present in the client's handshake (the server has indicated a subprotocol not requested by the client), the client MUST Fail the WebSocket Connection]] - if headers:has("sec-websocket-protocol") then + local protocol = headers:get("sec-websocket-protocol") + if protocol then local has_matching_protocol = false if self.protocols then - local swps = headers:get_split_as_sequence("sec-websocket-protocol") - for i=1, swps.n do - local p1 = swps[i]:lower() - for _, p2 in ipairs(self.protocols) do - if p1 == p2 then - has_matching_protocol = true - break - end - end - if has_matching_protocol then + for _, p2 in ipairs(self.protocols) do + if protocol:lower() == p2 then + has_matching_protocol = true break end end @@ -483,6 +480,7 @@ local function handle_websocket_response(self, headers, stream) end -- Success! + assert(self.socket == nil, "websocket:connect called twice") self.socket = assert(stream.connection:take_socket()) self.readyState = 1 From af6f537b8eaec7ff18a734bb97ee2264a0adf464 Mon Sep 17 00:00:00 2001 From: daurnimator Date: Wed, 20 Jan 2016 22:07:41 +1100 Subject: [PATCH 04/31] doc/index: Document websocket module --- doc/index.md | 66 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/doc/index.md b/doc/index.md index 9cab6ed3..adcd41fc 100644 --- a/doc/index.md +++ b/doc/index.md @@ -908,6 +908,72 @@ Returns the time in HTTP preferred date format (See [RFC 7231 section 7.1.1.1](h Current version of lua-http as a string. +## http.websocket + + +### `new_from_uri(uri)` {#http.websocket.new_from_uri} + +Creates a new `http.websocket` object of type `"client"` from the given URI. + + +### `websocket.close_timeout` {#http.websocket.close_timeout} + +Amount of time (in seconds) to wait between sending a close frame and actually closing the connection. +Defaults to `3` seconds. + + +### `websocket:connect(timeout)` {#http.websocket:connect} + +Try to connect to a websocket server. + + +### `websocket:read(timeout)` {#http.websocket:read} + +Reads and returns the next data frame plus its opcode. +Any ping frames received while reading will be responded to. + +The opcode `0x1` will be returned as `"text"` and `0x2` will be returned as `"binary"`. + + +### `websocket:each()` {#http.websocket:each} + +Iterator over [`websocket:read()`](#http.websocket:read). + + +### `websocket:send_frame(frame, timeout)` {#http.websocket:send_frame} + +Low level function to send a raw frame. + + +### `websocket:send(data, opcode, timeout)` {#http.websocket:send} + +Send the given `data` as a data frame. + + - `data` should be a string + - `opcode` can be a numeric opcode, `"text"` or `"binary"`. If `nil`, defaults to a text frame + + +### `websocket:close(code, reason, timeout)` {#http.websocket:close} + +Closes the websocket connection. + + - `code` defaults to `1000` + - `reason` is an optional string + + +### Example + +```lua +local websocket = require "http.websocket" +local ws = websocket.new_from_uri("wss://echo.websocket.org") +assert(ws:connect()) +assert(ws:send("koo-eee!")) +local data = assert(ws:read()) +assert(data == "koo-eee!") +assert(ws:close()) +``` + + ## http.zlib An abstraction layer over the various lua zlib libraries. From 320c399f4d05918e10f9f92bc30c9caa3dbd7332 Mon Sep 17 00:00:00 2001 From: daurnimator Date: Wed, 20 Jan 2016 22:08:29 +1100 Subject: [PATCH 05/31] examples/websocket_client: Add websocket example that streams BTC info from coinbase --- examples/websocket_client.lua | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 examples/websocket_client.lua diff --git a/examples/websocket_client.lua b/examples/websocket_client.lua new file mode 100644 index 00000000..055c3503 --- /dev/null +++ b/examples/websocket_client.lua @@ -0,0 +1,20 @@ +--[[ +Example of websocket client usage + + - Connects to the coinbase feed. + - Sends a subscribe message + - Prints off 5 messages + - Close the socket and clean up. +]] + +local json = require "cjson" +local websocket = require "http.websocket" + +local ws = websocket.new_from_uri("ws://ws-feed.exchange.coinbase.com") +assert(ws:connect()) +assert(ws:send(json.encode({type = "subscribe", product_id = "BTC-USD"}))) +for _=1, 5 do + local data = assert(ws:read()) + print(data) +end +assert(ws:close()) From 8174bbf81696c39e1854c63b816270d47479922d Mon Sep 17 00:00:00 2001 From: daurnimator Date: Tue, 16 Feb 2016 11:24:35 +1100 Subject: [PATCH 06/31] http/request: Expose new_from_uri_t --- http/request.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/http/request.lua b/http/request.lua index e200a38b..571ff5fa 100644 --- a/http/request.lua +++ b/http/request.lua @@ -346,6 +346,7 @@ function request_methods:go(timeout) end return { + new_from_uri_t = new_from_uri_t; new_from_uri = new_from_uri; new_connect = new_connect; new_from_stream = new_from_stream; From 6a313c976fecb2d49915e22d1b3d76fd8e2ed64a Mon Sep 17 00:00:00 2001 From: daurnimator Date: Tue, 16 Feb 2016 11:37:00 +1100 Subject: [PATCH 07/31] rockspec: Add websocket module --- http-scm-0.rockspec | 1 + 1 file changed, 1 insertion(+) diff --git a/http-scm-0.rockspec b/http-scm-0.rockspec index 7b9db7df..f00e3a64 100644 --- a/http-scm-0.rockspec +++ b/http-scm-0.rockspec @@ -41,6 +41,7 @@ build = { ["http.tls"] = "http/tls.lua"; ["http.util"] = "http/util.lua"; ["http.version"] = "http/version.lua"; + ["http.websocket"] = "http/websocket.lua"; ["http.zlib"] = "http/zlib.lua"; ["http.compat.prosody"] = "http/compat/prosody.lua"; }; From 6fb88bb9f385c07d0b6abe1b551d9846b469fb39 Mon Sep 17 00:00:00 2001 From: daurnimator Date: Tue, 15 Mar 2016 03:02:23 +1100 Subject: [PATCH 08/31] http/websocket: Rename websocket:read() to websocket:receive() Matches better with 'send'. Didn't want to change to read/write as those should have the same signature as lua file handles (and hence not take timeouts) --- doc/index.md | 6 +++--- examples/websocket_client.lua | 2 +- http/websocket.lua | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/doc/index.md b/doc/index.md index adcd41fc..1f46739b 100644 --- a/doc/index.md +++ b/doc/index.md @@ -927,7 +927,7 @@ Defaults to `3` seconds. Try to connect to a websocket server. -### `websocket:read(timeout)` {#http.websocket:read} +### `websocket:receive(timeout)` {#http.websocket:receive} Reads and returns the next data frame plus its opcode. Any ping frames received while reading will be responded to. @@ -937,7 +937,7 @@ The opcode `0x1` will be returned as `"text"` and `0x2` will be returned as `"bi ### `websocket:each()` {#http.websocket:each} -Iterator over [`websocket:read()`](#http.websocket:read). +Iterator over [`websocket:receive()`](#http.websocket:receive). ### `websocket:send_frame(frame, timeout)` {#http.websocket:send_frame} @@ -968,7 +968,7 @@ local websocket = require "http.websocket" local ws = websocket.new_from_uri("wss://echo.websocket.org") assert(ws:connect()) assert(ws:send("koo-eee!")) -local data = assert(ws:read()) +local data = assert(ws:receive()) assert(data == "koo-eee!") assert(ws:close()) ``` diff --git a/examples/websocket_client.lua b/examples/websocket_client.lua index 055c3503..cc0cee93 100644 --- a/examples/websocket_client.lua +++ b/examples/websocket_client.lua @@ -14,7 +14,7 @@ local ws = websocket.new_from_uri("ws://ws-feed.exchange.coinbase.com") assert(ws:connect()) assert(ws:send(json.encode({type = "subscribe", product_id = "BTC-USD"}))) for _=1, 5 do - local data = assert(ws:read()) + local data = assert(ws:receive()) print(data) end assert(ws:close()) diff --git a/http/websocket.lua b/http/websocket.lua index 5655ef28..124d5cfe 100644 --- a/http/websocket.lua +++ b/http/websocket.lua @@ -238,7 +238,7 @@ local function close_helper(self, code, reason, deadline) read_deadline = math.min(read_deadline, deadline) end while not self.got_close_code do - if not self:read(read_deadline-monotime()) then + if not self:receive(read_deadline-monotime()) then break end end @@ -260,7 +260,7 @@ function websocket_methods:close(code, reason, timeout) return true end -function websocket_methods:read(timeout) +function websocket_methods:receive(timeout) local deadline = timeout and (monotime()+timeout) local databuffer, databuffer_type while true do @@ -343,7 +343,7 @@ end function websocket_methods:each() return function(self) -- luacheck: ignore 432 - return self:read() + return self:receive() end, self end From 8f0150f2929541a91dcde832dadc18216621758a Mon Sep 17 00:00:00 2001 From: daurnimator Date: Tue, 15 Mar 2016 14:22:47 +1100 Subject: [PATCH 09/31] http/websocket: Add protocol validation. No longer check protocols case-insensitively Copy the passed protocols array so that the caller can safely modify afterwards --- http/websocket.lua | 39 +++++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/http/websocket.lua b/http/websocket.lua index 124d5cfe..ea49f761 100644 --- a/http/websocket.lua +++ b/http/websocket.lua @@ -375,15 +375,26 @@ local function new_from_uri_t(uri_t, protocols) self.request.headers:append("sec-websocket-version", "13") if protocols then --[[ The request MAY include a header field with the name - Sec-WebSocket-Protocol. If present, this value indicates one - or more comma-separated subprotocol the client wishes to speak, - ordered by preference. The elements that comprise this value - MUST be non-empty strings with characters in the range U+0021 to - U+007E not including separator characters as defined in - [RFC2616] and MUST all be unique strings.]] - -- TODO: protocol validation - self.protocols = protocols - self.request.headers:append("sec-websocket-protocol", table.concat(protocols, ",")) + Sec-WebSocket-Protocol. If present, this value indicates one + or more comma-separated subprotocol the client wishes to speak, + ordered by preference. The elements that comprise this value + MUST be non-empty strings with characters in the range U+0021 to + U+007E not including separator characters as defined in + [RFC2616] and MUST all be unique strings.]] + local n_protocols = #protocols + -- Copy the passed 'protocols' array so that caller is allowed to modify + local protocols_copy = {} + for i=1, n_protocols do + local v = protocols[i] + if protocols_copy[v] then + error("duplicate protocol") + end + assert(v:match("^[\33\35-\39\42\43\45\46\48-\57\65-\90\94-\122\124\126\127]+$"), "invalid protocol") + protocols_copy[v] = true + protocols_copy[i] = v + end + self.protocols = protocols_copy + self.request.headers:append("sec-websocket-protocol", table.concat(protocols_copy, ",", 1, n_protocols)) end return self end @@ -465,15 +476,7 @@ local function handle_websocket_response(self, headers, stream) requested by the client), the client MUST Fail the WebSocket Connection]] local protocol = headers:get("sec-websocket-protocol") if protocol then - local has_matching_protocol = false - if self.protocols then - for _, p2 in ipairs(self.protocols) do - if protocol:lower() == p2 then - has_matching_protocol = true - break - end - end - end + local has_matching_protocol = self.protocols and self.protocols[protocol] if not has_matching_protocol then return nil, "unexpected protocol", ce.EINVAL end From 68b3c39fb659dc012a8eb0f95a7d2cd510ec12e3 Mon Sep 17 00:00:00 2001 From: daurnimator Date: Tue, 15 Mar 2016 15:44:03 +1100 Subject: [PATCH 10/31] http/websocket: Move protocol validation to it's own function --- http/websocket.lua | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/http/websocket.lua b/http/websocket.lua index ea49f761..4abfb327 100644 --- a/http/websocket.lua +++ b/http/websocket.lua @@ -51,6 +51,12 @@ local function trim(s) return from > #s and "" or s:match(".*%S", from) end +--[[ this value MUST be non-empty strings with characters in the range U+0021 +to U+007E not including separator characters as defined in [RFC2616] ]] +local function validate_protocol(p) + return p:match("^[\33\35-\39\42\43\45\46\48-\57\65-\90\94-\122\124\126\127]+$") +end + -- XORs the string `str` with a 32bit key local function apply_mask(str, key) assert(#key == 4) @@ -389,7 +395,7 @@ local function new_from_uri_t(uri_t, protocols) if protocols_copy[v] then error("duplicate protocol") end - assert(v:match("^[\33\35-\39\42\43\45\46\48-\57\65-\90\94-\122\124\126\127]+$"), "invalid protocol") + assert(validate_protocol(v), "invalid protocol") protocols_copy[v] = true protocols_copy[i] = v end From c29951547255bc4f33bea7e4018dab4febdb9b71 Mon Sep 17 00:00:00 2001 From: daurnimator Date: Tue, 15 Mar 2016 16:16:44 +1100 Subject: [PATCH 11/31] http/websocket: Expose protocol selected --- http/websocket.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/http/websocket.lua b/http/websocket.lua index 4abfb327..5eaf1f08 100644 --- a/http/websocket.lua +++ b/http/websocket.lua @@ -362,6 +362,7 @@ local function new(type) got_close_code = nil; got_close_reason = nil; key = nil; + protocol = nil; protocols = nil; request = nil; }, websocket_mt) @@ -491,7 +492,9 @@ local function handle_websocket_response(self, headers, stream) -- Success! assert(self.socket == nil, "websocket:connect called twice") self.socket = assert(stream.connection:take_socket()) + self.request = nil self.readyState = 1 + self.protocol = protocol return true end From 57da1858177fc5c085b6b4ef584dbb8a77821a43 Mon Sep 17 00:00:00 2001 From: daurnimator Date: Tue, 15 Mar 2016 16:17:06 +1100 Subject: [PATCH 12/31] http/websocket: Add initial server support --- http/websocket.lua | 114 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 112 insertions(+), 2 deletions(-) diff --git a/http/websocket.lua b/http/websocket.lua index 5eaf1f08..66a114c4 100644 --- a/http/websocket.lua +++ b/http/websocket.lua @@ -22,6 +22,7 @@ local uri_patts = require "lpeg_patterns.uri" local rand = require "openssl.rand" local digest = require "openssl.digest" local bit = require "http.bit" +local new_headers = require "http.headers".new local http_request = require "http.request" local websocket_methods = { @@ -354,7 +355,7 @@ function websocket_methods:each() end local function new(type) - assert(type == "client") + assert(type == "client" or type == "server") local self = setmetatable({ socket = nil; type = type; @@ -364,7 +365,10 @@ local function new(type) key = nil; protocol = nil; protocols = nil; + -- only used by client: request = nil; + -- only used by server: + stream = nil; }, websocket_mt) return self end @@ -508,10 +512,116 @@ function websocket_methods:connect(timeout) return handle_websocket_response(self, headers, stream) end +-- Given an incoming HTTP1 request, attempts to upgrade it to a websocket connection +local function new_from_stream(headers, stream) + assert(stream.connection.type == "server") + + if stream.connection.version < 1 or stream.connection.version >= 2 then + return nil, "websockets only supported with HTTP 1.x", ce.EINVAL + end + + local upgrade = headers:get("upgrade") + if not upgrade or upgrade:lower() ~= "websocket" then + return nil, "upgrade header not websocket", ce.EINVAL + end + + local has_connection_upgrade = false + local connection_header = headers:get_split_as_sequence("connection") + for i=1, connection_header.n do + if connection_header[i]:lower() == "upgrade" then + has_connection_upgrade = true + break + end + end + if not has_connection_upgrade then + return nil, "connection header doesn't contain upgrade", ce.EINVAL + end + + local key = trim(headers:get("sec-websocket-key")) + if not key then + return nil, "missing sec-websocket-key", ce.EINVAL + end + + if headers:get("sec-websocket-version") ~= "13" then + return nil, "unsupported sec-websocket-version" + end + + local protocols_available + if headers:has("sec-websocket-protocol") then + local client_protocols = headers:get_split_as_sequence("sec-websocket-protocol") + --[[ The request MAY include a header field with the name + Sec-WebSocket-Protocol. If present, this value indicates one + or more comma-separated subprotocol the client wishes to speak, + ordered by preference. The elements that comprise this value + MUST be non-empty strings with characters in the range U+0021 to + U+007E not including separator characters as defined in + [RFC2616] and MUST all be unique strings.]] + protocols_available = {} + for i, protocol in ipairs(client_protocols) do + protocol = trim(protocol) + if protocols_available[protocol] then + return nil, "duplicate protocol", ce.EINVAL + end + if not validate_protocol(protocol) then + return nil, "invalid protocol", ce.EINVAL + end + protocols_available[protocol] = true + protocols_available[i] = protocol + end + end + + local self = new("server") + self.key = key + self.protocols = protocols_available + self.stream = stream + return self +end + +function websocket_methods:accept(protocols, response_headers) + assert(self.type == "server" and self.readyState == 0) + + if response_headers == nil then + response_headers = new_headers() + end + response_headers:upsert(":status", "101") + response_headers:upsert("upgrade", "websocket") + response_headers:upsert("connection", "upgrade") + response_headers:upsert("sec-websocket-accept", base64_sha1(self.key .. magic)) + + local chosen_protocol + if self.protocols then + if protocols then + for _, protocol in ipairs(protocols) do + if self.protocols[protocol] then + chosen_protocol = protocol + break + end + end + end + if not chosen_protocol then + return nil, "no matching protocol", ce.EPROTONOSUPPORT + end + response_headers:upsert("sec-websocket-protocol", chosen_protocol) + end + + do + local ok, err, errno = self.stream:write_headers(response_headers, false) + if not ok then + return ok, err, errno + end + end + + self.socket = assert(self.stream.connection:take_socket()) + self.stream = nil + self.readyState = 1 + self.protocol = chosen_protocol + return true +end + return { new_from_uri_t = new_from_uri_t; new_from_uri = new_from_uri; - + new_from_stream = new_from_stream; build_frame = build_frame; read_frame = read_frame; build_close = build_close; From 31889a24c49e083aa5ff9f23c577eb85e6940be4 Mon Sep 17 00:00:00 2001 From: daurnimator Date: Thu, 24 Mar 2016 17:25:31 +1100 Subject: [PATCH 13/31] http/websocket: Don't mask close frames from server --- http/websocket.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/http/websocket.lua b/http/websocket.lua index 66a114c4..c1c1fa12 100644 --- a/http/websocket.lua +++ b/http/websocket.lua @@ -234,7 +234,7 @@ local function close_helper(self, code, reason, deadline) end if self.readyState < 2 then - local close_frame = build_close(code, reason, true) + local close_frame = build_close(code, reason, self.type == "client") -- ignore failure self:send_frame(close_frame, deadline and deadline-monotime()) end From a4cc9a93480a1cfe77ec175331c99dfa7ce5d7b9 Mon Sep 17 00:00:00 2001 From: daurnimator Date: Thu, 24 Mar 2016 17:47:21 +1100 Subject: [PATCH 14/31] spec/websocket_spec: Start work on tests --- spec/websocket_spec.lua | 62 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 spec/websocket_spec.lua diff --git a/spec/websocket_spec.lua b/spec/websocket_spec.lua new file mode 100644 index 00000000..287ea674 --- /dev/null +++ b/spec/websocket_spec.lua @@ -0,0 +1,62 @@ +describe("http.websocket module's internal functions work", function() + local websocket = require "http.websocket" + it("build_frame works for simple cases", function() + -- Examples from RFC 6455 Section 5.7 + + -- A single-frame unmasked text message + assert.same(string.char(0x81,0x05,0x48,0x65,0x6c,0x6c,0x6f), websocket.build_frame { + FIN = true; + MASK = false; + opcode = 0x1; + data = "Hello"; + }) + + -- A single-frame masked text message + assert.same(string.char(0x81,0x85,0x37,0xfa,0x21,0x3d,0x7f,0x9f,0x4d,0x51,0x58), websocket.build_frame { + FIN = true; + MASK = true; + key = {0x37,0xfa,0x21,0x3d}; + opcode = 0x1; + data = "Hello"; + }) + end) + it("build_frame validates opcode", function() + assert.has.errors(function() + websocket.build_frame { opcode = -1; } + end) + assert.has.errors(function() + websocket.build_frame { opcode = 16; } + end) + end) + it("build_frame validates data length", function() + assert.has.errors(function() + websocket.build_frame { + opcode = 0x8; + data = ("f"):rep(200); + } + end) + end) + it("build_close works for common case", function() + assert.same({ + opcode = 0x8; + FIN = true; + MASK = false; + data = "\3\232"; + }, websocket.build_close(1000, nil, false)) + + assert.same({ + opcode = 0x8; + FIN = true; + MASK = false; + data = "\3\232error"; + }, websocket.build_close(1000, "error", false)) + end) + it("build_close validates string length", function() + assert.has.errors(function() websocket.build_close(1000, ("f"):rep(200), false) end) + end) + it("parse_close works", function() + assert.same({nil, nil}, {websocket.parse_close ""}) + assert.same({1000, nil}, {websocket.parse_close "\3\232"}) + assert.same({1000, "error"}, {websocket.parse_close "\3\232error"}) + end) +end) From 0fa3a0678d4884d04b966d7cd56de1b7833e5799 Mon Sep 17 00:00:00 2001 From: daurnimator Date: Thu, 24 Mar 2016 18:06:05 +1100 Subject: [PATCH 15/31] http/websocket: Expose 'new' function for testing --- http/websocket.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/http/websocket.lua b/http/websocket.lua index c1c1fa12..1482a35b 100644 --- a/http/websocket.lua +++ b/http/websocket.lua @@ -622,6 +622,8 @@ return { new_from_uri_t = new_from_uri_t; new_from_uri = new_from_uri; new_from_stream = new_from_stream; + + new = new; build_frame = build_frame; read_frame = read_frame; build_close = build_close; From 191bd3896f408d2fb21f16b082948dcfefc07a13 Mon Sep 17 00:00:00 2001 From: daurnimator Date: Thu, 24 Mar 2016 18:20:45 +1100 Subject: [PATCH 16/31] http/websocket: Update readyState as close happens --- http/websocket.lua | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/http/websocket.lua b/http/websocket.lua index 1482a35b..fcfe87e9 100644 --- a/http/websocket.lua +++ b/http/websocket.lua @@ -237,6 +237,7 @@ local function close_helper(self, code, reason, deadline) local close_frame = build_close(code, reason, self.type == "client") -- ignore failure self:send_frame(close_frame, deadline and deadline-monotime()) + self.readyState = 2 end -- Do not close socket straight away, wait for acknowledgement from server @@ -250,12 +251,13 @@ local function close_helper(self, code, reason, deadline) end end - self.socket:shutdown() - cqueues.poll() - cqueues.poll() - self.socket:close() - - self.readyState = 3 + if self.readyState < 3 then + self.readyState = 3 + self.socket:shutdown() + cqueues.poll() + cqueues.poll() + self.socket:close() + end return nil, reason, ce.ENOMSG end From a6c0680ccecc5194398fd0d02f17563a90478408 Mon Sep 17 00:00:00 2001 From: daurnimator Date: Thu, 24 Mar 2016 18:07:19 +1100 Subject: [PATCH 17/31] spec/websocket_spec: Add test of send, receive, close --- spec/websocket_spec.lua | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/spec/websocket_spec.lua b/spec/websocket_spec.lua index 287ea674..a6c241f0 100644 --- a/spec/websocket_spec.lua +++ b/spec/websocket_spec.lua @@ -1,3 +1,4 @@ +local TEST_TIMEOUT = 2 describe("http.websocket module's internal functions work", function() local websocket = require "http.websocket" it("build_frame works for simple cases", function() @@ -60,3 +61,43 @@ describe("http.websocket module's internal functions work", function() assert.same({1000, "error"}, {websocket.parse_close "\3\232error"}) end) end) +describe("http.websocket module two sided tests", function() + local websocket = require "http.websocket" + local cs = require "cqueues.socket" + local cqueues = require "cqueues" + local function assert_loop(cq, timeout) + local ok, err, _, thd = cq:loop(timeout) + if not ok then + if thd then + err = debug.traceback(thd, err) + end + error(err, 2) + end + end + local function new_pair() + local c, s = cs.pair() + local client = websocket.new("client") + client.socket = c + client.readyState = 1 + local server = websocket.new("server") + server.socket = s + server.readyState = 1 + return client, server + end + it("works with a socketpair", function() + local cq = cqueues.new() + local c, s = new_pair() + cq:wrap(function() + assert(c:send("hello")) + assert.same("world", c:receive()) + assert(c:close()) + end) + cq:wrap(function() + assert.same("hello", s:receive()) + assert(s:send("world")) + assert(s:close()) + end) + assert_loop(cq, TEST_TIMEOUT) + assert.truthy(cq:empty()) + end) +end) From aa6fd7aa0292f8e7764c22054b31d038dacc7aec Mon Sep 17 00:00:00 2001 From: daurnimator Date: Thu, 24 Mar 2016 18:21:20 +1100 Subject: [PATCH 18/31] http/websocket: Return code as provided by called in close_helper It won't conflict with `err, errno` idiom, as codes are always >1000 --- http/websocket.lua | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/http/websocket.lua b/http/websocket.lua index fcfe87e9..c16b6fdf 100644 --- a/http/websocket.lua +++ b/http/websocket.lua @@ -259,7 +259,7 @@ local function close_helper(self, code, reason, deadline) self.socket:close() end - return nil, reason, ce.ENOMSG + return nil, reason, code end function websocket_methods:close(code, reason, timeout) @@ -331,6 +331,9 @@ function websocket_methods:receive(timeout) end self.got_close_code = status_code self.got_close_message = message + --[[ RFC 6455 5.5.1 + When sending a Close frame in response, the endpoint typically + echos the status code it received.]] return close_helper(self, status_code, message, deadline) elseif frame.opcode == 0x9 then -- Ping frame frame.opcode = 0xA From 0e2662053bc6ca721794ec3d5c96d10061db01d7 Mon Sep 17 00:00:00 2001 From: daurnimator Date: Thu, 24 Mar 2016 18:50:08 +1100 Subject: [PATCH 19/31] http/websocket: Fix bug where reserved flags could never be set bit.bor in certain setups doesn't take >2 arguments --- http/websocket.lua | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/http/websocket.lua b/http/websocket.lua index c16b6fdf..55e17c3a 100644 --- a/http/websocket.lua +++ b/http/websocket.lua @@ -78,11 +78,19 @@ local function build_frame(desc) assert(#data <= 125, "WebSocket control frames MUST have a payload length of 125 bytes or less.") end - local b1 = bit.bor(desc.opcode, - desc.FIN and 0x80 or 0, - desc.RSV1 and 0x40 or 0, - desc.RSV2 and 0x20 or 0, - desc.RSV3 and 0x10 or 0) + local b1 = desc.opcode + if desc.FIN then + b1 = bit.bor(b1, 0x80) + end + if desc.RSV1 then + b1 = bit.bor(b1, 0x40) + end + if desc.RSV2 then + b1 = bit.bor(b1, 0x20) + end + if desc.RSV3 then + b1 = bit.bor(b1, 0x10) + end local b2 = #data local length_extra From d8eec3546620c92d9add4e38d5d0fccf766b8aa9 Mon Sep 17 00:00:00 2001 From: daurnimator Date: Thu, 24 Mar 2016 18:54:38 +1100 Subject: [PATCH 20/31] spec/websocket_spec: Add test of reserved flags --- spec/websocket_spec.lua | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/spec/websocket_spec.lua b/spec/websocket_spec.lua index a6c241f0..69269467 100644 --- a/spec/websocket_spec.lua +++ b/spec/websocket_spec.lua @@ -100,4 +100,25 @@ describe("http.websocket module two sided tests", function() assert_loop(cq, TEST_TIMEOUT) assert.truthy(cq:empty()) end) + for _, flag in ipairs{"RSV1", "RSV2", "RSV3"} do + it("fails correctly on "..flag.." flag set", function() + local cq = cqueues.new() + local c, s = new_pair() + cq:wrap(function() + assert(c:send_frame({ + opcode = 1; + [flag] = true; + })) + assert(c:close()) + end) + cq:wrap(function() + local ok, _, errno = s:receive() + assert.same(nil, ok) + assert.same(1002, errno) + assert(s:close()) + end) + assert_loop(cq, TEST_TIMEOUT) + assert.truthy(cq:empty()) + end) + end end) From 38896c97b67c302311027470597d1586114167d1 Mon Sep 17 00:00:00 2001 From: daurnimator Date: Thu, 24 Mar 2016 19:02:32 +1100 Subject: [PATCH 21/31] http/websocket: Keep databuffer in websocket object so that state is kept over transient errors Also helps in case of multiple users of :receive; though locking is still probably needed in read_frame --- http/websocket.lua | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/http/websocket.lua b/http/websocket.lua index 55e17c3a..59600a07 100644 --- a/http/websocket.lua +++ b/http/websocket.lua @@ -279,7 +279,6 @@ end function websocket_methods:receive(timeout) local deadline = timeout and (monotime()+timeout) - local databuffer, databuffer_type while true do local frame, err, errno = read_frame(self.socket, deadline and (deadline-monotime())) if frame == nil then @@ -293,26 +292,30 @@ function websocket_methods:receive(timeout) if frame.opcode < 0x8 then if frame.opcode == 0x0 then -- Continuation frames - if not databuffer then + if not self.databuffer then return close_helper(self, 1002, "Unexpected continuation frame", deadline) end - databuffer[#databuffer+1] = frame.data + self.databuffer[#self.databuffer+1] = frame.data elseif frame.opcode == 0x1 or frame.opcode == 0x2 then -- Text or Binary frame - if databuffer then + if self.databuffer then return close_helper(self, 1002, "Continuation frame expected", deadline) end - databuffer = { frame.data } - databuffer_type = frame.opcode + self.databuffer = { frame.data } + self.databuffer_type = frame.opcode else return close_helper(self, 1002, "Reserved opcode", deadline) end if frame.FIN then + local databuffer_type = self.databuffer_type + self.databuffer_type = nil if databuffer_type == 0x1 then databuffer_type = "text" elseif databuffer_type == 0x2 then databuffer_type = "binary" end - return table.concat(databuffer), databuffer_type + local databuffer = table.concat(self.databuffer) + self.databuffer = nil + return databuffer, databuffer_type end else -- Control frame if frame.length > 125 then -- Control frame with too much payload @@ -373,6 +376,8 @@ local function new(type) socket = nil; type = type; readyState = 0; + databuffer = nil; + databuffer_type = nil; got_close_code = nil; got_close_reason = nil; key = nil; From d5f1aa65ba9738a69f6712e6afcea9c60827125c Mon Sep 17 00:00:00 2001 From: daurnimator Date: Fri, 25 Mar 2016 21:52:29 +1100 Subject: [PATCH 22/31] http/websocket: Don't allow use of send or receive when not in correct state --- http/websocket.lua | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/http/websocket.lua b/http/websocket.lua index 59600a07..f2af2ee4 100644 --- a/http/websocket.lua +++ b/http/websocket.lua @@ -205,6 +205,9 @@ local function parse_close(data) end function websocket_methods:send_frame(frame, timeout) + if self.readyState < 1 or self.readyState > 2 then + return nil, ce.strerror(ce.EPIPE), ce.EPIPE + end local ok, err, errno = self.socket:xwrite(build_frame(frame), "n", timeout) if not ok then return nil, err, errno @@ -278,6 +281,9 @@ function websocket_methods:close(code, reason, timeout) end function websocket_methods:receive(timeout) + if self.readyState < 1 or self.readyState > 2 then + return nil, ce.strerror(ce.EPIPE), ce.EPIPE + end local deadline = timeout and (monotime()+timeout) while true do local frame, err, errno = read_frame(self.socket, deadline and (deadline-monotime())) From 4ab41ddbbfbd5b1607e3b3beeb9e298b8e1961a9 Mon Sep 17 00:00:00 2001 From: daurnimator Date: Sat, 26 Mar 2016 00:37:20 +1100 Subject: [PATCH 23/31] spec/websocket_spec: Rename variable --- spec/websocket_spec.lua | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/spec/websocket_spec.lua b/spec/websocket_spec.lua index 69269467..dae957e5 100644 --- a/spec/websocket_spec.lua +++ b/spec/websocket_spec.lua @@ -76,13 +76,13 @@ describe("http.websocket module two sided tests", function() end local function new_pair() local c, s = cs.pair() - local client = websocket.new("client") - client.socket = c - client.readyState = 1 - local server = websocket.new("server") - server.socket = s - server.readyState = 1 - return client, server + local ws_client = websocket.new("client") + ws_client.socket = c + ws_client.readyState = 1 + local ws_server = websocket.new("server") + ws_server.socket = s + ws_server.readyState = 1 + return ws_client, ws_server end it("works with a socketpair", function() local cq = cqueues.new() From 4f0515f039d02144fe0bb37e6287576125093ae1 Mon Sep 17 00:00:00 2001 From: daurnimator Date: Sat, 26 Mar 2016 00:38:48 +1100 Subject: [PATCH 24/31] spec/websocket_spec: Add test that goes via url constructor and full negotiation --- spec/websocket_spec.lua | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/spec/websocket_spec.lua b/spec/websocket_spec.lua index dae957e5..b098bc70 100644 --- a/spec/websocket_spec.lua +++ b/spec/websocket_spec.lua @@ -62,6 +62,7 @@ describe("http.websocket module's internal functions work", function() end) end) describe("http.websocket module two sided tests", function() + local server = require "http.server" local websocket = require "http.websocket" local cs = require "cqueues.socket" local cqueues = require "cqueues" @@ -121,4 +122,34 @@ describe("http.websocket module two sided tests", function() assert.truthy(cq:empty()) end) end + it("works when using url constructor", function() + local cq = cqueues.new() + local s = server.listen { + host = "localhost"; + port = 0; + } + assert(s:listen()) + local _, host, port = s:localname() + cq:wrap(function() + s:run(function (stream) + local headers = assert(stream:get_headers()) + s:pause() + local ws = websocket.new_from_stream(headers, stream) + assert(ws:accept()) + assert(ws:close()) + end) + s:close() + end) + cq:wrap(function() + local ws = websocket.new_from_uri_t { + scheme = "ws"; + host = host; + port = port; + } + assert(ws:connect()) + assert(ws:close()) + end) + assert_loop(cq, TEST_TIMEOUT) + assert.truthy(cq:empty()) + end) end) From 81d21c47b1b57a570c6bd679a1ea3cca67d5e750 Mon Sep 17 00:00:00 2001 From: daurnimator Date: Sat, 26 Mar 2016 01:02:30 +1100 Subject: [PATCH 25/31] spec/websocket_spec: Add test that passes protocols --- spec/websocket_spec.lua | 36 +++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/spec/websocket_spec.lua b/spec/websocket_spec.lua index b098bc70..78f1cf21 100644 --- a/spec/websocket_spec.lua +++ b/spec/websocket_spec.lua @@ -63,6 +63,7 @@ describe("http.websocket module's internal functions work", function() end) describe("http.websocket module two sided tests", function() local server = require "http.server" + local util = require "http.util" local websocket = require "http.websocket" local cs = require "cqueues.socket" local cqueues = require "cqueues" @@ -122,7 +123,7 @@ describe("http.websocket module two sided tests", function() assert.truthy(cq:empty()) end) end - it("works when using url constructor", function() + it("works when using uri string constructor", function() local cq = cqueues.new() local s = server.listen { host = "localhost"; @@ -141,12 +142,41 @@ describe("http.websocket module two sided tests", function() s:close() end) cq:wrap(function() - local ws = websocket.new_from_uri_t { + local ws = websocket.new_from_uri("ws://"..util.to_authority(host, port, "ws")); + assert(ws:connect()) + assert(ws:close()) + end) + assert_loop(cq, TEST_TIMEOUT) + assert.truthy(cq:empty()) + end) + it("works when using uri table constructor and protocols", function() + local cq = cqueues.new() + local s = server.listen { + host = "localhost"; + port = 0; + } + assert(s:listen()) + local _, host, port = s:localname() + cq:wrap(function() + s:run(function (stream) + local headers = assert(stream:get_headers()) + s:pause() + local ws = websocket.new_from_stream(headers, stream) + assert(ws:accept({"my awesome-protocol", "foo"})) + -- Should prefer client protocol preference + assert.same("foo", ws.protocol) + assert(ws:close()) + end) + s:close() + end) + cq:wrap(function() + local ws = websocket.new_from_uri_t({ scheme = "ws"; host = host; port = port; - } + }, {"foo", "my-awesome-protocol", "bar"}) assert(ws:connect()) + assert.same("foo", ws.protocol) assert(ws:close()) end) assert_loop(cq, TEST_TIMEOUT) From afbc8c87fb8d9d9d2eb7a5155a9183f7bd2fa0f4 Mon Sep 17 00:00:00 2001 From: daurnimator Date: Sat, 26 Mar 2016 01:09:54 +1100 Subject: [PATCH 26/31] doc/index: Document websocket.new_from_stream --- doc/index.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/doc/index.md b/doc/index.md index 1f46739b..96e379c0 100644 --- a/doc/index.md +++ b/doc/index.md @@ -916,6 +916,16 @@ Current version of lua-http as a string. Creates a new `http.websocket` object of type `"client"` from the given URI. +### `new_from_stream(headers, stream)` {#http.websocket.new_from_stream} + +Attempts to create a new `http.websocket` object of type `"server"` from the given request headers and stream. + + - [`headers`](#http.headers) should be headers of a suspected websocket upgrade request from a HTTP 1 client. + - [`stream`](#http.h1_stream) should be a live HTTP 1 stream of the `"server"` type. + +This function does **not** have side effects, and is hence okay to use tentatively. + + ### `websocket.close_timeout` {#http.websocket.close_timeout} Amount of time (in seconds) to wait between sending a close frame and actually closing the connection. From 8433ccc228a0f0b8cdfc13d14d110c3bf17f4947 Mon Sep 17 00:00:00 2001 From: daurnimator Date: Sat, 26 Mar 2016 01:16:03 +1100 Subject: [PATCH 27/31] doc/index: Document protocols argument to websocket.new_from_uri --- doc/index.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/index.md b/doc/index.md index 96e379c0..b99eec4f 100644 --- a/doc/index.md +++ b/doc/index.md @@ -911,10 +911,12 @@ Current version of lua-http as a string. ## http.websocket -### `new_from_uri(uri)` {#http.websocket.new_from_uri} +### `new_from_uri(uri, protocols)` {#http.websocket.new_from_uri} Creates a new `http.websocket` object of type `"client"` from the given URI. + - `protocols` (optional) should be a lua table containing a sequence of protocols to send to the server + ### `new_from_stream(headers, stream)` {#http.websocket.new_from_stream} From d8332c733ef2cb0f5fc319a20dac4d3600c0194a Mon Sep 17 00:00:00 2001 From: daurnimator Date: Sat, 26 Mar 2016 01:17:51 +1100 Subject: [PATCH 28/31] http/websocket: Add timeout argument to websocket:accept() --- http/websocket.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/http/websocket.lua b/http/websocket.lua index f2af2ee4..8994ff57 100644 --- a/http/websocket.lua +++ b/http/websocket.lua @@ -601,7 +601,7 @@ local function new_from_stream(headers, stream) return self end -function websocket_methods:accept(protocols, response_headers) +function websocket_methods:accept(protocols, response_headers, timeout) assert(self.type == "server" and self.readyState == 0) if response_headers == nil then @@ -629,7 +629,7 @@ function websocket_methods:accept(protocols, response_headers) end do - local ok, err, errno = self.stream:write_headers(response_headers, false) + local ok, err, errno = self.stream:write_headers(response_headers, false, timeout) if not ok then return ok, err, errno end From 086dad4606b82e1cd7f915e912832214ab1038f4 Mon Sep 17 00:00:00 2001 From: daurnimator Date: Sat, 26 Mar 2016 01:20:16 +1100 Subject: [PATCH 29/31] http/websocket: Remove headers argument to websocket:accept() Too complex for a first attempt. I don't like mutating non-self arguments --- http/websocket.lua | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/http/websocket.lua b/http/websocket.lua index 8994ff57..25dccbf3 100644 --- a/http/websocket.lua +++ b/http/websocket.lua @@ -601,12 +601,10 @@ local function new_from_stream(headers, stream) return self end -function websocket_methods:accept(protocols, response_headers, timeout) +function websocket_methods:accept(protocols, timeout) assert(self.type == "server" and self.readyState == 0) - if response_headers == nil then - response_headers = new_headers() - end + local response_headers = new_headers() response_headers:upsert(":status", "101") response_headers:upsert("upgrade", "websocket") response_headers:upsert("connection", "upgrade") From f99d064e7688f0c2cda57006e5124e8f2ad85294 Mon Sep 17 00:00:00 2001 From: daurnimator Date: Sat, 26 Mar 2016 01:21:15 +1100 Subject: [PATCH 30/31] doc/index: Give 'when' example in websocket:connect --- doc/index.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/index.md b/doc/index.md index b99eec4f..b5ea7a4b 100644 --- a/doc/index.md +++ b/doc/index.md @@ -936,7 +936,9 @@ Defaults to `3` seconds. ### `websocket:connect(timeout)` {#http.websocket:connect} -Try to connect to a websocket server. +Connect to a websocket server. + +Usually called after a successful [`new_from_uri`](#http.websocket.new_from_uri) ### `websocket:receive(timeout)` {#http.websocket:receive} From f456f75c0ca6be4e4198e7fd0945782fc0bf54bb Mon Sep 17 00:00:00 2001 From: daurnimator Date: Sat, 26 Mar 2016 01:21:30 +1100 Subject: [PATCH 31/31] doc/index: Document websocket:accept() --- doc/index.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/doc/index.md b/doc/index.md index b5ea7a4b..b99d1b63 100644 --- a/doc/index.md +++ b/doc/index.md @@ -934,6 +934,15 @@ Amount of time (in seconds) to wait between sending a close frame and actually c Defaults to `3` seconds. +### `websocket:accept(protocols, timeout)` {#http.websocket:accept} + +Completes negotiation with a websocket client. + + - `protocols` (optional) should be a lua table containing a sequence of protocols to to allow from the client + +Usually called after a successful [`new_from_stream`](#http.websocket.new_from_stream) + + ### `websocket:connect(timeout)` {#http.websocket:connect} Connect to a websocket server.