Commit cf2a18b0 authored by Marek Vavrusa's avatar Marek Vavrusa

modules/http: graphs, prometheus metrics, websocks

* http embeds modified lua-http server code that
  reuses single cqueue for all h2 client sockets,
  this is also because the API in upstream is unstable
* http embeds rickshaw for real-time graphs over
  websockets, it displays latency heatmap by default
  and can show several other metrics
* http shows a world map with pinned recently contacted
  authoritatives, where diameter represents number
  of queries sent and colour its average RTT, so
  you can see where the queries are going
* http now exports several endpoints and websockets:
  /stats for statistics in JSON, and /metrics for
  metrics in Prometheus text format
parent 7ed94014
...@@ -10,6 +10,17 @@ min = minute ...@@ -10,6 +10,17 @@ min = minute
hour = 60 * minute hour = 60 * minute
day = 24 * hour day = 24 * hour
-- Logging
function panic(fmt, ...)
error(string.format('error: '..fmt, ...))
end
function warn(fmt, ...)
io.stderr:write(string.format(fmt..'\n', ...))
end
function log(fmt, ...)
print(string.format(fmt, ...))
end
-- Resolver bindings -- Resolver bindings
kres = require('kres') kres = require('kres')
trust_anchors = require('trust_anchors') trust_anchors = require('trust_anchors')
......
...@@ -28,6 +28,7 @@ for starters? ...@@ -28,6 +28,7 @@ for starters?
http = { http = {
host = 'localhost', host = 'localhost',
port = 8053, port = 8053,
geoip = 'GeoLite2-City.mmdb' -- Optional
} }
} }
...@@ -71,6 +72,40 @@ the outputs of following: ...@@ -71,6 +72,40 @@ the outputs of following:
openssl req -new -key mykey.key -out csr.pem openssl req -new -key mykey.key -out csr.pem
openssl req -x509 -days 90 -key mykey.key -in csr.pem -out mycert.crt openssl req -x509 -days 90 -key mykey.key -in csr.pem -out mycert.crt
Built-in services
^^^^^^^^^^^^^^^^^
The HTTP module has several built-in services to use.
.. csv-table::
:header: "Endpoint", "Service", "Description"
"``/stats``", "Statistics/metrics", "Exported metrics in JSON."
"``/metrics``", "Prometheus metrics", "Exported metrics for Prometheus_"
"``/feed``", "Most frequent queries", "List of most frequent queries in JSON."
Enabling Prometheus metrics endpoint
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The module exposes ``/metrics`` endpoint that serves internal metrics in Prometheus_ text format.
You can use it out of the box:
.. code-block:: bash
$ curl -k https://localhost:8053/metrics | tail
# TYPE latency histogram
latency_bucket{le=10} 2.000000
latency_bucket{le=50} 2.000000
latency_bucket{le=100} 2.000000
latency_bucket{le=250} 2.000000
latency_bucket{le=500} 2.000000
latency_bucket{le=1000} 2.000000
latency_bucket{le=1500} 2.000000
latency_bucket{le=+Inf} 2.000000
latency_count 2.000000
latency_sum 11.000000
How to expose services over HTTP How to expose services over HTTP
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
...@@ -142,3 +177,9 @@ Dependencies ...@@ -142,3 +177,9 @@ Dependencies
* `lua-http <https://github.com/daurnimator/lua-http>`_ available in LuaRocks * `lua-http <https://github.com/daurnimator/lua-http>`_ available in LuaRocks
``$ luarocks install --server=http://luarocks.org/dev http`` ``$ luarocks install --server=http://luarocks.org/dev http``
* `mmdblua <https://github.com/daurnimator/mmdblua>`_ available in LuaRocks
``$ luarocks install --server=http://luarocks.org/dev mmdblua``
.. _Prometheus: https://prometheus.io
\ No newline at end of file
...@@ -8,6 +8,7 @@ local server = require('http.server') ...@@ -8,6 +8,7 @@ local server = require('http.server')
local headers = require('http.headers') local headers = require('http.headers')
local websocket = require('http.websocket') local websocket = require('http.websocket')
local x509, pkey = require('openssl.x509'), require('openssl.pkey') local x509, pkey = require('openssl.x509'), require('openssl.pkey')
local has_mmdb, mmdb = pcall(require, 'mmdb')
-- Module declaration -- Module declaration
local cq = cqueues.new() local cq = cqueues.new()
...@@ -31,89 +32,54 @@ local function pgload(relpath) ...@@ -31,89 +32,54 @@ local function pgload(relpath)
fp:close() fp:close()
-- Guess content type -- Guess content type
local ext = relpath:match('[^\\.]+$') local ext = relpath:match('[^\\.]+$')
return {'/'..relpath, mime_types[ext] or 'text', data} return {'/'..relpath, mime_types[ext] or 'text', data, 86400}
end end
-- Preloaded static assets -- Preloaded static assets
local pages = { local pages = {
pgload('favicon.ico'),
pgload('rickshaw.min.css'),
pgload('kresd.js'), pgload('kresd.js'),
pgload('datamaps.world.min.js'), pgload('datamaps.world.min.js'),
pgload('topojson.js'), pgload('topojson.js'),
pgload('jquery.js'), pgload('jquery.js'),
pgload('epoch.css'), pgload('rickshaw.min.js'),
pgload('epoch.js'),
pgload('favicon.ico'),
pgload('d3.js'), pgload('d3.js'),
} }
-- Serve preloaded root page -- Serve preloaded root page
local function serve_root() local function serve_root()
local _, mime_root, mime_data = unpack(pgload('main.tpl')) local data = pgload('main.tpl')[3]
mime_data = mime_data data = data
:gsub('{{ title }}', 'kresd @ '..hostname()) :gsub('{{ title }}', 'kresd @ '..hostname())
:gsub('{{ host }}', hostname()) :gsub('{{ host }}', hostname())
return function (h, stream) return function (h, stream)
-- Return index page -- Render snippets
local rsnippets = {} local rsnippets = {}
for _,v in pairs(M.snippets) do for _,v in pairs(M.snippets) do
table.insert(rsnippets, string.format('<h2>%s</h2>\n%s', v[1], v[2])) table.insert(rsnippets, string.format('<h2>%s</h2>\n%s', v[1], v[2]))
end end
local data = mime_data -- Return index page
return data
:gsub('{{ secure }}', stream:checktls() and 'true' or 'false') :gsub('{{ secure }}', stream:checktls() and 'true' or 'false')
:gsub('{{ snippets }}', table.concat(rsnippets, '\n')) :gsub('{{ snippets }}', table.concat(rsnippets, '\n'))
local hsend = headers.new()
hsend:append(':status', '200')
hsend:append('content/type', mime_root)
assert(stream:write_headers(hsend, false))
assert(stream:write_chunk(data, true))
-- Push assets
-- local path, mime, data = unpack(pages[1])
-- local hpush = headers.new()
-- hpush:append(':scheme', h:get(':scheme'))
-- hpush:append(':method', 'GET')
-- hpush:append(':authority', h:get(':authority'))
-- hpush:append(':path', path)
-- local nstream = stream:push_promise(hpush)
-- hpush = headers.new()
-- hpush:append(':status', '200')
-- hpush:append('content/type', mime)
-- print('pushing', path)
-- assert(nstream:write_headers(hpush, false))
-- assert(nstream:write_chunk(data, true))
-- Do not send anything else
return false
end end
end end
-- Load dependent modules
if not stats then modules.load('stats') end
-- Function to sort frequency list
local function stream_stats(h, ws)
local ok, prev = true, stats.list()
while ok do
-- Get current snapshot
local cur, stats_dt = stats.list(), {}
for k,v in pairs(cur) do
stats_dt[k] = v - (prev[k] or 0)
end
prev = cur
-- Publish stats updates periodically
local push = tojson({stats=stats_dt})
ok = ws:send(push)
cqueues.sleep(0.5)
end
ws:close()
end
-- Export HTTP service endpoints -- Export HTTP service endpoints
M.endpoints = { M.endpoints = {
['/'] = {'text/html', serve_root()}, ['/'] = {'text/html', serve_root()},
['/stats'] = {'application/json', stats.list, stream_stats},
['/feed'] = {'application/json', stats.frequent},
} }
-- Export static pages
for _, pg in ipairs(pages) do for _, pg in ipairs(pages) do
local path, mime, data = unpack(pg) local path, mime, data, ttl = unpack(pg)
M.endpoints[path] = {mime, data} M.endpoints[path] = {mime, data, nil, ttl}
end
-- Export built-in prometheus interface
for k, v in pairs(require('prometheus')) do
M.endpoints[k] = v
end end
-- Export HTTP service page snippets -- Export HTTP service page snippets
...@@ -140,11 +106,15 @@ local function serve_get(h, stream) ...@@ -140,11 +106,15 @@ local function serve_get(h, stream)
if type(data) == 'table' then data = tojson(data) end if type(data) == 'table' then data = tojson(data) end
if not mime or type(data) ~= 'string' then if not mime or type(data) ~= 'string' then
hsend:append(':status', '404') hsend:append(':status', '404')
assert(stream:write_headers(hsend, false)) assert(stream:write_headers(hsend, true))
else else
-- Serve content type appropriately -- Serve content type appropriately
hsend:append(':status', '200') hsend:append(':status', '200')
hsend:append('content/type', mime) hsend:append('content-type', mime)
local ttl = entry and entry[4]
if ttl then
hsend:append('cache-control', string.format('max-age=%d', ttl))
end
assert(stream:write_headers(hsend, false)) assert(stream:write_headers(hsend, false))
assert(stream:write_chunk(data, true)) assert(stream:write_chunk(data, true))
end end
...@@ -174,6 +144,7 @@ local function route(endpoints) ...@@ -174,6 +144,7 @@ local function route(endpoints)
if cb then if cb then
cb(h, ws) cb(h, ws)
end end
ws:close()
return return
-- Handle HTTP method appropriately -- Handle HTTP method appropriately
elseif m == 'GET' then elseif m == 'GET' then
...@@ -182,13 +153,9 @@ local function route(endpoints) ...@@ -182,13 +153,9 @@ local function route(endpoints)
-- Method is not supported -- Method is not supported
local hsend = headers.new() local hsend = headers.new()
hsend:append(':status', '500') hsend:append(':status', '500')
assert(stream:write_headers(hsend, false)) assert(stream:write_headers(hsend, true))
end end
stream:shutdown() stream:shutdown()
-- Close multiplexed HTTP/2 connection only when empty
if connection.version < 2 or connection.new_streams:length() == 0 then
connection:shutdown()
end
end end
end end
...@@ -282,18 +249,18 @@ function M.interface(host, port, endpoints, crtfile, keyfile) ...@@ -282,18 +249,18 @@ function M.interface(host, port, endpoints, crtfile, keyfile)
end end
-- Check loaded certificate -- Check loaded certificate
if not crt or not key then if not crt or not key then
error(string.format('failed to load certificate "%s" - %s', crtfile, err or 'error')) panic('failed to load certificate "%s" - %s', crtfile, err or 'error')
end end
end end
-- Create TLS context and start listening -- Create TLS context and start listening
local s, err = server.listen { local s, err = server.listen {
host = host, host = host,
port = port, port = port,
tls = crt ~= nil, client_timeout = 5,
ctx = crt and tlscontext(crt, key), ctx = crt and tlscontext(crt, key),
} }
if not s then if not s then
error(string.format('failed to listen on %s#%d: %s', host, port, err)) panic('failed to listen on %s#%d: %s', host, port, err)
end end
-- Compose server handler -- Compose server handler
local routes = route(endpoints) local routes = route(endpoints)
...@@ -307,7 +274,7 @@ function M.interface(host, port, endpoints, crtfile, keyfile) ...@@ -307,7 +274,7 @@ function M.interface(host, port, endpoints, crtfile, keyfile)
local _, expiry = crt:getLifetime() local _, expiry = crt:getLifetime()
expiry = math.max(0, expiry - (os.time() - 3 * 24 * 3600)) expiry = math.max(0, expiry - (os.time() - 3 * 24 * 3600))
event.after(expiry, function (ev) event.after(expiry, function (ev)
print('[http] refreshed ephemeral certificate') log('[http] refreshed ephemeral certificate')
crt, key = updatecert(crtfile, keyfile) crt, key = updatecert(crtfile, keyfile)
s.ctx = tlscontext(crt, key) s.ctx = tlscontext(crt, key)
end) end)
...@@ -321,21 +288,21 @@ function M.deinit() ...@@ -321,21 +288,21 @@ function M.deinit()
end end
-- @function Configure module -- @function Configure module
local ffi = require('ffi')
function M.config(conf) function M.config(conf)
conf = conf or {} conf = conf or {}
assert(type(conf) == 'table', 'config { host = "...", port = 443, cert = "...", key = "..." }') assert(type(conf) == 'table', 'config { host = "...", port = 443, cert = "...", key = "..." }')
-- Configure web interface for resolver -- Configure web interface for resolver
if not conf.port then conf.port = 8053 end if not conf.port then conf.port = 8053 end
if not conf.host then conf.host = 'localhost' end if not conf.host then conf.host = 'localhost' end
if conf.geoip and has_mmdb then M.geoip = mmdb.open(conf.geoip) end
M.interface(conf.host, conf.port, M.endpoints, conf.cert, conf.key) M.interface(conf.host, conf.port, M.endpoints, conf.cert, conf.key)
-- TODO: configure DNS/HTTP(s) interface -- TODO: configure DNS/HTTP(s) interface
if M.ev then return end if M.ev then return end
-- Schedule both I/O activity notification and timeouts -- Schedule both I/O activity notification and timeouts
local poll_step local poll_step
poll_step = function (ev, status, events) poll_step = function ()
local ok, err, _, co = cq:step(0) local ok, err, _, co = cq:step(0)
if not ok then print('[http]', err, debug.traceback(co)) end if not ok then warn('[http] error: %s %s', err, debug.traceback(co)) end
-- Reschedule timeout or create new one -- Reschedule timeout or create new one
local timeout = cq:timeout() local timeout = cq:timeout()
if timeout then if timeout then
......
http_SOURCES := http.lua http_SOURCES := http.lua prometheus.lua
http_INSTALL := $(wildcard modules/http/static/*) http_INSTALL := $(wildcard modules/http/static/*) \
modules/http/http/h2_connection.lua \
modules/http/http/server.lua
$(call make_lua_module,http) $(call make_lua_module,http)
The MIT License (MIT)
Copyright (c) 2015-2016 Daurnimator
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
\ No newline at end of file
Embedded unstable APIs from https://github.com/daurnimator/lua-http under MIT license, see LICENSE.
ChangeLog:
* Marek Vavrusa <marek@vavrusa.com>:
- Modified h2_connection to reuse current cqueue context
\ No newline at end of file
This diff is collapsed.
local cqueues = require "cqueues"
local monotime = cqueues.monotime
local cs = require "cqueues.socket"
local cc = require "cqueues.condition"
local ce = require "cqueues.errno"
local h1_connection = require "http.h1_connection"
local h2_connection = require "http.h2_connection"
local http_tls = require "http.tls"
local pkey = require "openssl.pkey"
local x509 = require "openssl.x509"
local name = require "openssl.x509.name"
local altname = require "openssl.x509.altname"
local hang_timeout = 0.03
local function onerror(socket, op, why, lvl) -- luacheck: ignore 212
if why == ce.EPIPE or why == ce.ETIMEDOUT then
return why
end
return string.format("%s: %s", op, ce.strerror(why)), why
end
-- Sense for TLS or SSL client hello
-- returns `true`, `false` or `nil, err`
local function is_tls_client_hello(socket, timeout)
-- reading for 6 bytes should be safe, as no HTTP version
-- has a valid client request shorter than 6 bytes
local first_bytes, err, errno = socket:xread(6, timeout)
if first_bytes == nil then
return nil, err or ce.EPIPE, errno
end
local use_tls = not not (
first_bytes:match("^\22\3...\1") or -- TLS
first_bytes:match("^[\128-\255][\9-\255]\1") -- SSLv2
)
local ok
ok, errno = socket:unget(first_bytes)
if not ok then
return nil, onerror(socket, "unget", errno, 2)
end
return use_tls
end
-- Wrap a bare cqueues socket in an HTTP connection of a suitable version
-- Starts TLS if necessary
-- this function *should never throw*
local function wrap_socket(self, socket, deadline)
socket:setmode("b", "b")
socket:onerror(onerror)
local use_tls = self.tls
if use_tls == nil then
local err, errno
use_tls, err, errno = is_tls_client_hello(socket, deadline and (deadline-monotime()))
if use_tls == nil then
return nil, err, errno
end
end
local is_h2 -- tri-state
if use_tls then
local ok, err, errno = socket:starttls(self.ctx, deadline and (deadline-monotime()))
if not ok then
return nil, err, errno
end
local ssl = socket:checktls()
if ssl and http_tls.has_alpn then
local proto = ssl:getAlpnSelected()
if proto == "h2" then
is_h2 = true
elseif proto == nil or proto == "http/1.1" then
is_h2 = false
else
return nil, "unexpected ALPN protocol: " .. proto
end
end
end
-- Still not sure if incoming connection is an HTTP1 or HTTP2 connection
-- Need to sniff for the h2 connection preface to find out for sure
if is_h2 == nil then
local err, errno
is_h2, err, errno = h2_connection.socket_has_preface(socket, true, deadline and (deadline-monotime()))
if is_h2 == nil then
return nil, err, errno
end
end
local conn, err, errno
if is_h2 then
conn, err, errno = h2_connection.new(socket, "server", nil, deadline and (deadline-monotime()))
else
conn, err, errno = h1_connection.new(socket, "server", 1.1)
end
if not conn then
return nil, err, errno
end
return conn, is_h2
end
-- this function *should never throw*
local function handle_client(conn, on_stream)
while true do
local stream, err, errno = conn:get_next_incoming_stream()
if stream == nil then
if (err == ce.EPIPE or errno == ce.ECONNRESET or errno == ce.ENOTCONN)
and (conn.socket == nil or conn.socket:pending() == 0) then
break
else
return nil, err, errno
end
end
on_stream(stream)
end
-- wait for streams to complete?
return true
end
-- Prefer whichever comes first
local function alpn_select(ssl, protos) -- luacheck: ignore 212
for _, proto in ipairs(protos) do
if proto == "h2" or proto == "http/1.1" then
return proto
end
end
return nil
end
-- create a new self signed cert
local function new_ctx(host)
local ctx = http_tls.new_server_context()
if ctx.setAlpnSelect then
ctx:setAlpnSelect(alpn_select)
end
local crt = x509.new()
-- serial needs to be unique or browsers will show uninformative error messages
crt:setSerial(os.time())
-- use the host we're listening on as canonical name
local dn = name.new()
dn:add("CN", host)
crt:setSubject(dn)
local alt = altname.new()
alt:add("DNS", host)
crt:setSubjectAlt(alt)
-- lasts for 10 years
crt:setLifetime(os.time(), os.time()+86400*3650)
-- can't be used as a CA
crt:setBasicConstraints{CA=false}
crt:setBasicConstraintsCritical(true)
-- generate a new private/public key pair
local key = pkey.new()
crt:setPublicKey(key)
crt:sign(key)
assert(ctx:setPrivateKey(key))
assert(ctx:setCertificate(crt))
return ctx
end
local server_methods = {
max_concurrent = math.huge;
client_timeout = 10;
}
local server_mt = {
__name = "http.server";
__index = server_methods;
}
function server_mt:__tostring()
return string.format("http.server{socket=%s;n_connections=%d}",
tostring(self.socket), self.n_connections)
end
--[[ Creates a new server object
Takes a table of options:
- `.socket`: A cqueues socket object
- `.tls`: `nil`: allow both tls and non-tls connections
- `true`: allows tls connections only
- `false`: allows non-tls connections only
- `.ctx`: an `openssl.ssl.context` object to use for tls connections
- ` `nil`: a self-signed context will be generated
- `.max_concurrent`: Maximum number of connections to allow live at a time (default: infinity)
- `.client_timeout`: Timeout (in seconds) to wait for client to send first bytes and/or complete TLS handshake (default: 10)
]]
local function new_server(tbl)
local socket = assert(tbl.socket)
-- Return errors rather than throwing
socket:onerror(function(s, op, why, lvl) -- luacheck: ignore 431 212
return why
end)
return setmetatable({
socket = socket;
tls = tbl.tls;
ctx = tbl.ctx;
max_concurrent = tbl.max_concurrent;
n_connections = 0;
pause_cond = cc.new();
paused = true;
connection_done = cc.new(); -- signalled when connection has been closed
client_timeout = tbl.client_timeout;
}, server_mt)
end
--[[
Extra options:
- `.family`: protocol family
- `.host`: address to bind to (required if not `.path`)
- `.port`: port to bind to (optional if tls isn't `nil`, in which case defaults to 80 for `.tls == false` or 443 if `.tls == true`)
- `.path`: path to UNIX socket (required if not `.host`)
- `.v6only`: allow ipv6 only (no ipv4-mapped-ipv6)
- `.mode`: fchmod or chmod socket after creating UNIX domain socket
- `.mask`: set and restore umask when binding UNIX domain socket
- `.unlink`: unlink socket path before binding?
- `.reuseaddr`: turn on SO_REUSEADDR flag?
- `.reuseport`: turn on SO_REUSEPORT flag?
]]
local function listen(tbl)
local tls = tbl.tls
local host = tbl.host
local path = tbl.path
assert(host or path, "need host or path")
local port = tbl.port
if host and port == nil then
if tls == true then
port = "443"
elseif tls == false then
port = "80"
else
error("need port")
end
end
local ctx = tbl.ctx
if ctx == nil and tls ~= false then
if host then
ctx = new_ctx(host)
else
error("Custom OpenSSL context required when using a UNIX domain socket")
end
end
local s = assert(cs.listen{
family = tbl.family;
host = host;
port = port;
path = path;
mode = tbl.mode;
mask = tbl.mask;
unlink = tbl.unlink;
reuseaddr = tbl.reuseaddr;
reuseport = tbl.reuseport;
v6only = tbl.v6only;
})
return new_server {
socket = s;
tls = tls;
ctx = ctx;
max_concurrent = tbl.max_concurrent;
client_timeout = tbl.client_timeout;
}
end
-- Actually wait for and *do* the binding
-- Don't *need* to call this, as if not it will be done lazily
function server_methods:listen(timeout)
return self.socket:listen(timeout)
end
function server_methods:localname()
return self.socket:localname()
end
function server_methods:pause()
self.paused = true
self.pause_cond:signal()
end
function server_methods:close()
self:pause()
cqueues.poll()
cqueues.poll()
self.socket:close()
end
function server_methods:run(on_stream, cq)
cq = assert(cq or cqueues.running())
self.paused = false
repeat
if self.n_connections >= self.max_concurrent then
cqueues.poll(self.connection_done, self.pause_cond)
if self.paused then
break
end
end
local socket, accept_errno = self.socket:accept({nodelay = true;}, 0)
if socket == nil then
if accept_errno == ce.ETIMEDOUT then
-- Yield this thread until a client arrives or server paused
cqueues.poll(self.socket, self.pause_cond)
elseif accept_errno == ce.EMFILE then
-- Wait for another request to finish
if cqueues.poll(self.connection_done, self.pause_cond, hang_timeout) == hang_timeout then
-- If we're stuck waiting, run a garbage collection sweep
-- This can prevent a hang
collectgarbage()
end
else
return nil, ce.strerror(accept_errno), accept_errno
end
else
self.n_connections = self.n_connections + 1
cq:wrap(function()
local ok, err
local conn, is_h2, errno = wrap_socket(self, socket)
if not conn then
err = is_h2
socket:close()
if errno == ce.ECONNRESET or err == 'read: Connection reset by peer' then
ok = true
end
else
ok, err = handle_client(conn, on_stream)