From 0ec4e8df04fa2f2d32dc5d3632cab338222fd0d4 Mon Sep 17 00:00:00 2001 From: daurnimator Date: Tue, 13 Dec 2016 14:03:42 +1100 Subject: [PATCH 1/3] http/*connection*: Construct h1_connection and h2_connection objects from connection_common objects --- http/connection_common.lua | 24 +++++++++++ http/h1_connection.lua | 43 +++++++++----------- http/h2_connection.lua | 83 +++++++++++++++++--------------------- 3 files changed, 80 insertions(+), 70 deletions(-) diff --git a/http/connection_common.lua b/http/connection_common.lua index 5f7c4673..510ae819 100644 --- a/http/connection_common.lua +++ b/http/connection_common.lua @@ -2,6 +2,10 @@ local ca = require "cqueues.auxlib" local ce = require "cqueues.errno" local connection_methods = {} +local connection_mt = { + __name = "http.connection_common"; + __index = connection_methods; +} local function onerror(socket, op, why, lvl) -- luacheck: ignore 212 local err = string.format("%s: %s", op, ce.strerror(why)) @@ -24,6 +28,24 @@ local function onerror(socket, op, why, lvl) -- luacheck: ignore 212 return err, why end +-- assumes ownership of the socket +local function new_connection(socket, conn_type) + assert(socket, "must provide a socket") + if conn_type ~= "client" and conn_type ~= "server" then + error('invalid connection type. must be "client" or "server"') + end + local self = setmetatable({ + socket = socket; + type = conn_type; + version = nil; + -- A function that will be called if the connection becomes idle + onidle_ = nil; + }, connection_mt) + socket:setmode("b", "bf") + socket:onerror(onerror) + return self +end + function connection_methods:onidle_() -- luacheck: ignore 212 end @@ -69,5 +91,7 @@ end return { onerror = onerror; + new = new_connection; methods = connection_methods; + mt = connection_mt; } diff --git a/http/h1_connection.lua b/http/h1_connection.lua index d39aa4be..b668f9df 100644 --- a/http/h1_connection.lua +++ b/http/h1_connection.lua @@ -23,35 +23,27 @@ function connection_mt:__tostring() self.type, self.version) end --- assumes ownership of the socket -local function new_connection(socket, conn_type, version) - assert(socket, "must provide a socket") - if conn_type ~= "client" and conn_type ~= "server" then - error('invalid connection type. must be "client" or "server"') - end +local function new_from_common(self, version) assert(version == 1 or version == 1.1, "unsupported version") - local self = setmetatable({ - socket = socket; - type = conn_type; - version = version; + self.version = version - -- for server: streams waiting to go out - -- for client: streams waiting for a response - pipeline = new_fifo(); - -- pipeline condition is stored in stream itself + -- for server: streams waiting to go out + -- for client: streams waiting for a response + self.pipeline = new_fifo() + -- pipeline condition is stored in stream itself - -- for server: held while request being read - -- for client: held while writing request - req_locked = nil; - -- signaled when unlocked - req_cond = cc.new(); + -- for server: held while request being read + -- for client: held while writing request + -- self.req_locked = nil + -- signaled when unlocked + self.req_cond = cc.new() - -- A function that will be called if the connection becomes idle - onidle_ = nil; - }, connection_mt) - socket:setmode("b", "bf") - socket:onerror(onerror) - return self + return setmetatable(self, connection_mt) +end + +local function new_connection(socket, conn_type, version) + local self = connection_common.new(socket, conn_type) + return new_from_common(self, version) end function connection_methods:clearerr(...) @@ -417,6 +409,7 @@ function connection_methods:write_body_plain(body, timeout) end return { + new_from_common = new_from_common; new = new_connection; methods = connection_methods; mt = connection_mt; diff --git a/http/h2_connection.lua b/http/h2_connection.lua index 12ea7333..4e9e78d4 100644 --- a/http/h2_connection.lua +++ b/http/h2_connection.lua @@ -93,11 +93,7 @@ local function socket_has_preface(socket, unget, timeout) return is_h2 end -local function new_connection(socket, conn_type, settings) - if conn_type ~= "client" and conn_type ~= "server" then - error('invalid connection type. must be "client" or "server"') - end - +local function new_from_common(self, settings) local cq do -- Allocate cqueue first up, as it can throw when out of files local ok, err = pcall(cqueues.new) if not ok then @@ -107,11 +103,7 @@ local function new_connection(socket, conn_type, settings) cq = err end - socket:setvbuf("full", math.huge) -- 'infinite' buffering; no write locks needed - socket:setmode("b", "bf") -- full buffering for now; will be set to no buffering after settings sent - socket:onerror(onerror) - - local ssl = socket:checktls() + local ssl = self.socket:checktls() if ssl then local cipher = ssl:getCipherInfo() if h2_banned_ciphers[cipher.name] then @@ -119,53 +111,53 @@ local function new_connection(socket, conn_type, settings) end end - local self = setmetatable({ - socket = socket; - type = conn_type; - version = 2; -- for compat with h1_connection - - streams = setmetatable({}, {__mode="kv"}); - n_active_streams = 0; - onidle_ = nil; - stream0 = nil; -- store separately with a strong reference - need_continuation = nil; -- stream - cq = cq; - close_me = false; -- to indicate that :close() should be called after exiting the current :step() - highest_odd_stream = -1; - highest_even_stream = -2; - send_goaway_lowest = nil; - recv_goaway_lowest = nil; - recv_goaway = cc.new(); - new_streams = new_fifo(); - new_streams_cond = cc.new(); - peer_settings = default_settings; - peer_settings_cond = cc.new(); -- signaled when the peer has changed their settings - acked_settings = default_settings; - send_settings = {n = 0}; - send_settings_ack_cond = cc.new(); -- for when server ACKs our settings - send_settings_acked = 0; - peer_flow_credits = 65535; -- 5.2.1 - peer_flow_credits_increase = cc.new(); - encoding_context = nil; - decoding_context = nil; - pongs = {}; -- pending pings we've sent. keyed by opaque 8 byte payload - }, connection_mt) - self:new_stream(0) + self.socket:setvbuf("full", math.huge) -- 'infinite' buffering; no write locks needed + + self.version = 2 -- for compat with h1_connection + self.streams = setmetatable({}, {__mode="kv"}) + self.n_active_streams = 0 + self.cq = cq + self.close_me = false -- to indicate that :close() should be called after exiting the current :step() + self.highest_odd_stream = -1 + self.highest_even_stream = -2 + -- self.send_goaway_lowest = nil + -- self.recv_goaway_lowest = nil + self.recv_goaway = cc.new() + self.new_streams = new_fifo() + self.new_streams_cond = cc.new() + self.peer_settings = default_settings + self.peer_settings_cond = cc.new() -- signaled when the peer has changed their settings + self.acked_settings = default_settings + self.send_settings = {n = 0} + self.send_settings_ack_cond = cc.new() -- for when server ACKs our settings + self.send_settings_acked = 0 + self.peer_flow_credits = 65535 -- 5.2.1 + self.peer_flow_credits_increase = cc.new() self.encoding_context = hpack.new(default_settings[0x1]) self.decoding_context = hpack.new(default_settings[0x1]) + self.pongs = {} -- pending pings we've sent. keyed by opaque 8 byte payload + + setmetatable(self, connection_mt) + + self:new_stream(0) -- sets self.stream0 as a strong reference self.cq:wrap(connection_main_loop, self) if self.type == "client" then - -- fully buffered write; will be flushed when sending settings - assert(socket:xwrite(preface, "f", 0)) + -- fully buffered write; will be flushed along with settings + assert(self.socket:xwrite(preface, "f", 0)) end assert(self.stream0:write_settings_frame(false, settings or {}, 0)) - socket:setmode("b", "bn") -- writes that don't explicitly buffer will now flush the buffer + self.socket:setmode("b", "bn") -- writes that don't explicitly buffer will now flush the buffer -- note that the buffer is *not* flushed right now return self end +local function new_connection(socket, conn_type, settings) + local self = connection_common.new(socket, conn_type) + return new_from_common(self, settings) +end + function connection_methods:pollfd() return self.cq:pollfd() end @@ -543,6 +535,7 @@ end return { preface = preface; socket_has_preface = socket_has_preface; + new_from_common = new_from_common; new = new_connection; methods = connection_methods; mt = connection_mt; From 9638e61aca8d335718cdaeef829d238c3d0fd547 Mon Sep 17 00:00:00 2001 From: daurnimator Date: Tue, 13 Dec 2016 14:22:57 +1100 Subject: [PATCH 2/3] http/connection_common: Add :starttls() --- doc/index.md | 7 ++++++- http/connection_common.lua | 11 +++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/doc/index.md b/doc/index.md index b4c9fa63..8211da6e 100644 --- a/doc/index.md +++ b/doc/index.md @@ -127,9 +127,14 @@ The HTTP version number of the connection as a number. Completes the connection to the remote server using the address specified, HTTP version and any options specified in the `connection.new` constructor. The `connect` function will yield until the connection attempt finishes (success or failure) or until `timeout` is exceeded. Connecting may include DNS lookups, TLS negotiation and HTTP2 settings exchange. Returns `true` on success. On failure returns `nil` and an error message. +### `connection:starttls(ctx, timeout)` {#connection:starttls} + +Starts a TLS (Transport Layer Security) negotiation with the remote server. `ctx` should be a luaossl SSL context or a luasec SSL context. Returns `true` on success. On error, returns `nil` an error message and an error number. + + ### `connection:checktls()` {#connection:checktls} -Checks the socket for a valid Transport Layer Security connection. Returns the luaossl ssl object if the connection is secured. Returns `nil` and an error message if there is no active TLS session. Please see the [luaossl website](http://25thandclement.com/~william/projects/luaossl.html) for more information about the ssl object. +Checks the socket for a valid TLS (Transport Layer Security) connection. Returns the luaossl ssl object if the connection is secured. Returns `nil` and an error message if there is no active TLS session. Please see the [luaossl website](http://25thandclement.com/~william/projects/luaossl.html) for more information about the ssl object. ### `connection:localname()` {#connection:localname} diff --git a/http/connection_common.lua b/http/connection_common.lua index 510ae819..76d61a70 100644 --- a/http/connection_common.lua +++ b/http/connection_common.lua @@ -68,6 +68,17 @@ function connection_methods:connect(timeout) return true end +function connection_methods:starttls(ctx, timeout) + if self.socket == nil then + return nil + end + local ok, err, errno = self.socket:starttls(ctx, timeout) + if not ok then + return nil, err, errno + end + return true +end + function connection_methods:checktls() if self.socket == nil then return nil From 2919ed75748582fd32fb219c21da296313652826 Mon Sep 17 00:00:00 2001 From: daurnimator Date: Tue, 13 Dec 2016 14:36:48 +1100 Subject: [PATCH 3/3] http/client: Convert socket to a connection_common and operate on that --- http/client.lua | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/http/client.lua b/http/client.lua index a2cb479c..0a3e595a 100644 --- a/http/client.lua +++ b/http/client.lua @@ -2,9 +2,8 @@ local ca = require "cqueues.auxlib" local cs = require "cqueues.socket" local http_tls = require "http.tls" local connection_common = require "http.connection_common" -local onerror = connection_common.onerror -local new_h1_connection = require "http.h1_connection".new -local new_h2_connection = require "http.h2_connection".new +local h1_connection = require "http.h1_connection" +local h2_connection = require "http.h2_connection" local openssl_ctx = require "openssl.ssl.context" -- Create a shared 'default' TLS contexts @@ -27,8 +26,10 @@ else end default_h2_ctx:setOptions(openssl_ctx.OP_NO_TLSv1 + openssl_ctx.OP_NO_TLSv1_1) -local function negotiate(s, options, timeout) - s:onerror(onerror) +local function negotiate(self, options, timeout) + if cs.type(self) then -- passing cqueues socket + self = connection_common.new(self, "client") + end local tls = options.tls local version = options.version if tls then @@ -46,13 +47,13 @@ local function negotiate(s, options, timeout) error("Unknown HTTP version: " .. tostring(version)) end end - local ok, err, errno = s:starttls(ctx, timeout) + local ok, err, errno = self:starttls(ctx, timeout) if not ok then return nil, err, errno end end if version == nil then - local ssl = s:checktls() + local ssl = self:checktls() if ssl then if http_tls.has_alpn and ssl:getAlpnSelected() == "h2" then version = 2 @@ -65,9 +66,9 @@ local function negotiate(s, options, timeout) end end if version < 2 then - return new_h1_connection(s, "client", version) + return h1_connection.new_from_common(self, version) elseif version == 2 then - return new_h2_connection(s, "client", options.h2_settings) + return h2_connection.new_from_common(self, options.h2_settings) else error("Unknown HTTP version: " .. tostring(version)) end