]> git.ipfire.org Git - thirdparty/knot-resolver.git/commitdiff
modules/http: allow passing server options to http configuration
authorMarek Vavruša <mvavrusa@cloudflare.com>
Tue, 6 Mar 2018 22:29:45 +0000 (14:29 -0800)
committerMarek Vavruša <mvavrusa@cloudflare.com>
Tue, 19 Jun 2018 20:16:18 +0000 (13:16 -0700)
This allows HTTP server to start with reuseport, reuseaddr or v6only.
The reuseport allows running HTTP module on all forks, not just the main one.

modules/http/README.rst
modules/http/http.lua
modules/http/http.test.lua

index 806e07f197fb66cab11667470c5d3c8335d9c070..5a896bd3aae59b7aa7663879a321cf5f469d7010 100644 (file)
@@ -26,8 +26,8 @@ for starters?
        -- Load HTTP module with defaults
        modules = {
                http = {
-                       host = 'localhost',
-                       port = 8053,
+                       host = 'localhost', -- Default: 'localhost'
+                       port = 8053,        -- Default: 8053
                        geoip = 'GeoLite2-City.mmdb' -- Optional, see
                        -- e.g. https://dev.maxmind.com/geoip/geoip2/geolite2/
                        -- and install mmdblua library
@@ -50,8 +50,6 @@ Major drawback is that current browsers won't do HTTP/2 over insecure connection
 .. code-block:: lua
 
        http = {
-               host = 'localhost',
-               port = 8053,
                cert = false,
        }
 
@@ -60,8 +58,6 @@ If you want to provide your own certificate and key, you're welcome to do so:
 .. code-block:: lua
 
        http = {
-               host = 'localhost',
-               port = 8053,
                cert = 'mycert.crt',
                key  = 'mykey.key',
        }
@@ -286,10 +282,18 @@ Services exposed in the previous part share the same external interface. This me
 
 .. code-block:: lua
 
-       http.interface('127.0.0.1', 8080, {
-               ['/conf'] = {'application/json', function (h, stream) print('configuration API') end},
-               ['/private'] = {'text/html', static_page},
-       })
+       http.add_interface {
+               endpoints = {
+                       ['/conf'] = {
+                               'application/json', function (h, stream)
+                                       return 'configuration API\n'
+                               end
+                       },
+               },
+               -- Same options as the config() method
+               host = 'localhost',
+               port = '8054',
+       }
 
 This way you can have different internal-facing and external-facing services at the same time.
 
index ef9864aa109e6d7dd2a8cf4fcab2797735becfd5..11b8c9cae09a21bcaaac1dc55d5433b0b75e5e79 100644 (file)
@@ -1,9 +1,6 @@
 -- Load dependent modules
 if not stats then modules.load('stats') end
 
--- This is leader-only module
-if worker.id > 0 then return {} end
-
 -- This is a module that does the heavy lifting to provide an HTTP/2 enabled
 -- server that supports TLS by default and provides endpoint for other modules
 -- in order to enable them to export restful APIs and websocket streams.
@@ -275,44 +272,53 @@ local function updatecert(crtfile, keyfile)
 end
 
 -- @function Listen on given HTTP(s) host
-function M.interface(host, port, endpoints, crtfile, keyfile)
+function M.add_interface(conf)
        local crt, key, ephemeral
-       if crtfile ~= false then
+       if conf.crtfile ~= false then
                -- Check if the cert file exists
-               if not crtfile then
-                       crtfile = 'self.crt'
-                       keyfile = 'self.key'
+               if not conf.crtfile then
+                       conf.crtfile = 'self.crt'
+                       conf.keyfile = 'self.key'
                        ephemeral = true
-               elseif not keyfile then
+               elseif not conf.keyfile then
                        error('certificate provided, but missing key')
                end
                -- Read or create self-signed x509 certificate
-               local f = io.open(crtfile, 'r')
+               local f = io.open(conf.crtfile, 'r')
                if f then
                        crt = assert(x509.new(f:read('*all')))
                        f:close()
                        -- Continue reading key file
                        if crt then
-                               f = io.open(keyfile, 'r')
+                               f = io.open(conf.keyfile, 'r')
                                key = assert(pkey.new(f:read('*all')))
                                f:close()
                        end
                elseif ephemeral then
-                       crt, key = updatecert(crtfile, keyfile)
+                       crt, key = updatecert(conf.crtfile, conf.keyfile)
                end
                -- Check loaded certificate
                if not crt or not key then
-                       panic('failed to load certificate "%s"', crtfile)
+                       panic('failed to load certificate "%s"', conf.crtfile)
                end
        end
        -- Compose server handler
-       local routes = route(endpoints)
+       local routes = route(conf.endpoints or M.endpoints)
+       -- Enable SO_REUSEPORT by default (unless explicitly turned off)
+       local reuseport = (conf.reuseport ~= nil) and conf.reuseport or true
+       if not reuseport and worker.id > 0 then
+               warn('[http] the "reuseport" option is disabled and multiple forks are used, ' ..
+                        'port binding will fail on some instances')
+       end
        -- Create TLS context and start listening
        local s, err = http_server.listen {
                cq = worker.bg_worker.cq,
-               host = host,
-               port = port,
-               client_timeout = 5,
+               host = conf.host or 'localhost',
+               port = conf.port or 8053,
+               v6only = conf.v6only,
+               reuseaddr = conf.reuseaddr,
+               reuseport = reuseport,
+               client_timeout = conf.client_timeout or 5,
                ctx = crt and tlscontext(crt, key),
                onstream = routes,
        }
@@ -321,7 +327,7 @@ function M.interface(host, port, endpoints, crtfile, keyfile)
                err = select(2, s:listen())
        end
        if err then
-               panic('failed to listen on %s@%d: %s', host, port, err)
+               panic('failed to listen on %s@%d: %s', conf.host, conf.port, err)
        end
        table.insert(M.servers, s)
        -- Create certificate renewal timer if ephemeral
@@ -330,12 +336,23 @@ function M.interface(host, port, endpoints, crtfile, keyfile)
                expiry = math.max(0, expiry - (os.time() - 3 * 24 * 3600))
                event.after(expiry, function ()
                        log('[http] refreshed ephemeral certificate')
-                       crt, key = updatecert(crtfile, keyfile)
+                       crt, key = updatecert(conf.crtfile, conf.keyfile)
                        s.ctx = tlscontext(crt, key)
                end)
        end
 end
 
+-- @function Listen on given HTTP(s) host (backwards compatible interface)
+function M.interface(host, port, endpoints, crtfile, keyfile)
+       return M.add_interface {
+               host = host,
+               port = port,
+               endpoints = endpoints,
+               crtfile = crtfile,
+               keyfile = keyfile,
+       }
+end
+
 -- @function Init module
 function M.init()
        worker.coroutine(prometheus.init)
@@ -355,8 +372,6 @@ function M.config(conf)
        if conf == true then conf = {} end
        assert(type(conf) == 'table', 'config { host = "...", port = 443, cert = "...", key = "..." }')
        -- Configure web interface for resolver
-       if not conf.port then conf.port = 8053 end
-       if not conf.host then conf.host = 'localhost' end
        if conf.geoip then
                if has_mmdb then
                        M.geoip = mmdb.open(conf.geoip)
@@ -364,12 +379,7 @@ function M.config(conf)
                        error('[http] mmdblua library not found, please remove GeoIP configuration')
                end
        end
-       -- Add endpoints to default endpoints
-       local endpoints = conf.endpoints or {}
-       for k, v in pairs(M.endpoints) do
-               endpoints[k] = v
-       end
-       M.interface(conf.host, conf.port, endpoints, conf.cert, conf.key)
+       M.add_interface(conf)
 end
 
 return M
index a4f2a0a9eccdb45d7e2a0a997f25c4810517d021..e22024b9be7e068378d6c125d6c35e4dd6b0a521 100644 (file)
@@ -1,17 +1,21 @@
 -- check prerequisites
-local supports_http = pcall(require, 'http') and pcall(require, 'http.request')
-if not supports_http then
+local has_http = pcall(require, 'http') and pcall(require, 'http.request')
+if not has_http then
        pass('skipping http module test because its not installed')
        done()
 else
        local request = require('http.request')
+       local endpoints = require('http').endpoints
+
+       -- custom endpoints
+       endpoints['/test'] = {'text/custom', function () return 'hello' end}
 
        -- setup resolver
        modules = {
                http = {
                        port = 0, -- Select random port
                        cert = false,
-                       endpoints = { ['/test'] = {'text/custom', function () return 'hello' end} },
+                       endpoints = endpoints,
                }
        }