From e12132599c31fb8b5fe0138812860075312dab72 Mon Sep 17 00:00:00 2001 From: Thijs Schreijer Date: Fri, 4 Nov 2016 13:09:35 +0100 Subject: [PATCH] feat(core) implement internal dns with loadbalancing, replace dnsmasq (#1587) Implements an internal DNS resolver, some new settings and a bugfix - uses same dns resolution in Kong proxy and Kong cli - bugfix: wildcarded hostnames are now properly validated - implements SRV record resolution - implements internal (weighted) roundrobin loadbalancing on the dns records - drops dnsmasq as a dependency - implements the `retries` setting (per api) - implements the `keepalive` setting (global) - now using the `balancer_by_lua` directives - revert back to standard `pgmoon` from the workaround `pgmoon-mashape` clone - exports ipv4, ipv6 and hostname verifying and normalization to the utils module --- kong-0.9.4-0.rockspec | 8 +- kong.conf.default | 23 +- kong/cmd/compile.lua | 2 +- kong/cmd/health.lua | 1 - kong/cmd/quit.lua | 5 - kong/cmd/reload.lua | 4 - kong/cmd/restart.lua | 2 +- kong/cmd/start.lua | 15 +- kong/cmd/stop.lua | 4 - kong/cmd/utils/dnsmasq_signals.lua | 75 ------ kong/cmd/utils/prefix_handler.lua | 3 - kong/conf_loader.lua | 26 ++- kong/core/balancer.lua | 79 +++++++ kong/core/globalpatches.lua | 214 +++++++++++++++--- kong/core/handler.lua | 26 ++- kong/core/resolver.lua | 85 +++---- kong/dao/db/postgres.lua | 2 +- kong/dao/migrations/cassandra.lua | 36 +++ kong/dao/migrations/postgres.lua | 16 +- kong/dao/schemas/apis.lua | 41 ++-- kong/kong.lua | 43 ++++ kong/meta.lua | 1 - kong/singletons.lua | 1 + kong/templates/kong_defaults.lua | 4 +- kong/templates/nginx_kong.lua | 11 +- kong/tools/dns.lua | 37 +++ kong/tools/utils.lua | 206 +++++++++++++++-- spec/01-unit/02-conf_loader_spec.lua | 28 +-- spec/01-unit/04-utils_spec.lua | 122 ++++++++++ spec/01-unit/08-entities_schemas_spec.lua | 38 +++- spec/01-unit/12-resolver_spec.lua | 142 ++++++++---- .../01-cmd/02-start_stop_spec.lua | 48 +--- spec/02-integration/01-cmd/06-check_spec.lua | 1 - .../02-integration/01-cmd/08-restart_spec.lua | 16 +- spec/02-integration/01-cmd/09-health_spec.lua | 11 - .../02-dao/02-migrations_spec.lua | 6 +- .../03-admin_api/02-apis_routes_spec.lua | 9 +- spec/02-integration/04-core/02-hooks_spec.lua | 2 +- spec/02-integration/04-core/03-dns_spec.lua | 81 +++++++ spec/fixtures/custom_nginx.template | 1 - spec/fixtures/invalid.conf | 2 +- spec/fixtures/shm-stub.lua | 4 +- spec/fixtures/to-strip.conf | 11 +- spec/helpers.lua | 19 +- 44 files changed, 1104 insertions(+), 407 deletions(-) delete mode 100644 kong/cmd/utils/dnsmasq_signals.lua create mode 100644 kong/core/balancer.lua create mode 100644 kong/tools/dns.lua create mode 100644 spec/02-integration/04-core/03-dns_spec.lua diff --git a/kong-0.9.4-0.rockspec b/kong-0.9.4-0.rockspec index e6fcc54fae3..fa57a72bac5 100644 --- a/kong-0.9.4-0.rockspec +++ b/kong-0.9.4-0.rockspec @@ -21,14 +21,15 @@ dependencies = { "version == 0.2", "lapis == 1.5.1", "lua-cassandra == dev-0", - "pgmoon-mashape == 2.0.1", + "pgmoon == 1.6.0", "luatz == 0.3", "lua_system_constants == 0.1.1", "lua-resty-iputils == 0.2.1", "luacrypto == 0.3.2", "luasyslog == 1.0.0", "lua_pack == 1.0.4", - "lua-resty-worker-events == 0.3.0" + "dns == 0.2.1", + "lua-resty-worker-events == 0.3.0", } build = { type = "builtin", @@ -64,7 +65,6 @@ build = { ["kong.cmd.utils.serf_signals"] = "kong/cmd/utils/serf_signals.lua", ["kong.cmd.utils.nginx_signals"] = "kong/cmd/utils/nginx_signals.lua", ["kong.cmd.utils.prefix_handler"] = "kong/cmd/utils/prefix_handler.lua", - ["kong.cmd.utils.dnsmasq_signals"] = "kong/cmd/utils/dnsmasq_signals.lua", ["kong.api"] = "kong/api/init.lua", ["kong.api.api_helpers"] = "kong/api/api_helpers.lua", @@ -76,6 +76,7 @@ build = { ["kong.api.routes.cache"] = "kong/api/routes/cache.lua", ["kong.api.routes.cluster"] = "kong/api/routes/cluster.lua", + ["kong.tools.dns"] = "kong/tools/dns.lua", ["kong.tools.utils"] = "kong/tools/utils.lua", ["kong.tools.printable"] = "kong/tools/printable.lua", ["kong.tools.responses"] = "kong/tools/responses.lua", @@ -92,6 +93,7 @@ build = { ["kong.core.events"] = "kong/core/events.lua", ["kong.core.error_handlers"] = "kong/core/error_handlers.lua", ["kong.core.globalpatches"] = "kong/core/globalpatches.lua", + ["kong.core.balancer"] = "kong/core/balancer.lua", ["kong.dao.errors"] = "kong/dao/errors.lua", ["kong.dao.schemas_validation"] = "kong/dao/schemas_validation.lua", diff --git a/kong.conf.default b/kong.conf.default index 3a1fdb89a31..7024a300ea8 100644 --- a/kong.conf.default +++ b/kong.conf.default @@ -109,6 +109,12 @@ # to the SSL key for the `admin_listen_ssl` # address. +#nginx_keepalive = 60 # Sets the maximum number of idle keepalive + # connections to upstream servers that are + # preserved in the cache of each worker + # process. When this number is exceeded, the + # least recently used connections are closed. + #------------------------------------------------------------------------------ # DATASTORE #------------------------------------------------------------------------------ @@ -262,18 +268,13 @@ # DNS RESOLVER #------------------------------------------------------------------------------ -#dnsmasq = on # Toggles if Kong should start/stop dnsmasq, - # which can be used as the Nginx DNS resolver. - # Using dnsmasq allows Nginx to resolve - # domains defined in /etc/hosts. - # dnsmasq must be installed and available in - # your $PATH. - -#dnsmasq_port = 8053 # The port on which dnsmasq should listen to - # for queries. +#dns_resolver = # Comma separated list of name servers, each + # entry in `ipv4[:port]` format to be used by + # Kong. If not specified the nameservers in + # the local `resolv.conf` file will be used. + # Port defaults to 53 if omitted. -#dns_resolver = 8.8.8.8 # Configure a name server to be used by Nginx. - # Only valid when `dnsmasq` is disabled. +#dns_hostsfile = /etc/hosts # The `hosts` file to use. #------------------------------------------------------------------------------ # DEVELOPMENT & MISCELLANEOUS diff --git a/kong/cmd/compile.lua b/kong/cmd/compile.lua index 35fbeb7ad28..ca59bff7e37 100644 --- a/kong/cmd/compile.lua +++ b/kong/cmd/compile.lua @@ -24,7 +24,7 @@ Example usage: } Note: - Third-party services such as Serf and dnsmasq need to be properly configured + Third-party services such as Serf need to be properly configured and started for Kong to be fully compatible while embedded. Options: diff --git a/kong/cmd/health.lua b/kong/cmd/health.lua index 5a2a2319000..cd760808563 100644 --- a/kong/cmd/health.lua +++ b/kong/cmd/health.lua @@ -21,7 +21,6 @@ local function execute(args) local pids = { serf = conf.serf_pid, nginx = conf.nginx_pid, - dnsmasq = conf.dnsmasq and conf.dnsmasq_pid or nil } local count = 0 diff --git a/kong/cmd/quit.lua b/kong/cmd/quit.lua index 2a32b59ea36..92e8cb5f370 100644 --- a/kong/cmd/quit.lua +++ b/kong/cmd/quit.lua @@ -1,4 +1,3 @@ -local dnsmasq_signals = require "kong.cmd.utils.dnsmasq_signals" local nginx_signals = require "kong.cmd.utils.nginx_signals" local serf_signals = require "kong.cmd.utils.serf_signals" local conf_loader = require "kong.conf_loader" @@ -40,10 +39,6 @@ local function execute(args) local dao = assert(DAOFactory.new(conf)) assert(serf_signals.stop(conf, dao)) - if conf.dnsmasq then - assert(dnsmasq_signals.stop(conf)) - end - log("Kong stopped (gracefully)") end diff --git a/kong/cmd/reload.lua b/kong/cmd/reload.lua index ea29e461e5c..394f783b9e6 100644 --- a/kong/cmd/reload.lua +++ b/kong/cmd/reload.lua @@ -1,4 +1,3 @@ -local dnsmasq_signals = require "kong.cmd.utils.dnsmasq_signals" local prefix_handler = require "kong.cmd.utils.prefix_handler" local nginx_signals = require "kong.cmd.utils.nginx_signals" local serf_signals = require "kong.cmd.utils.serf_signals" @@ -22,9 +21,6 @@ local function execute(args) prefix = args.prefix })) assert(prefix_handler.prepare_prefix(conf, args.nginx_conf)) - if conf.dnsmasq then - assert(dnsmasq_signals.start(conf)) - end local dao = assert(DAOFactory.new(conf)) assert(serf_signals.start(conf, dao)) diff --git a/kong/cmd/restart.lua b/kong/cmd/restart.lua index 9fb0118186f..ff88a6ed31a 100644 --- a/kong/cmd/restart.lua +++ b/kong/cmd/restart.lua @@ -16,7 +16,7 @@ end local lapp = [[ Usage: kong restart [OPTIONS] -Restart a Kong node (and other configured services like dnsmasq and Serf) +Restart a Kong node (and other configured services like Serf) in the given prefix directory. This command is equivalent to doing both 'kong stop' and diff --git a/kong/cmd/start.lua b/kong/cmd/start.lua index 863d9f8b294..d0f846789e9 100644 --- a/kong/cmd/start.lua +++ b/kong/cmd/start.lua @@ -1,4 +1,3 @@ -local dnsmasq_signals = require "kong.cmd.utils.dnsmasq_signals" local prefix_handler = require "kong.cmd.utils.prefix_handler" local nginx_signals = require "kong.cmd.utils.nginx_signals" local serf_signals = require "kong.cmd.utils.serf_signals" @@ -20,24 +19,18 @@ local function execute(args) xpcall(function() assert(prefix_handler.prepare_prefix(conf, args.nginx_conf)) assert(dao:run_migrations()) - if conf.dnsmasq then - assert(dnsmasq_signals.start(conf)) - end assert(serf_signals.start(conf, dao)) assert(nginx_signals.start(conf)) log("Kong started") end, function(e) - log.verbose("could not start Kong, stopping services") - pcall(nginx_signals.stop(conf)) - pcall(serf_signals.stop(conf, dao)) - if conf.dnsmasq then - pcall(dnsmasq_signals.stop(conf)) - end err = e -- cannot throw from this function - log.verbose("stopped services") end) if err then + log.verbose("could not start Kong, stopping services") + pcall(nginx_signals.stop(conf)) + pcall(serf_signals.stop(conf, dao)) + log.verbose("stopped services") error(err) -- report to main error handler end end diff --git a/kong/cmd/stop.lua b/kong/cmd/stop.lua index 9710ab95c35..96f0fb589b0 100644 --- a/kong/cmd/stop.lua +++ b/kong/cmd/stop.lua @@ -1,4 +1,3 @@ -local dnsmasq_signals = require "kong.cmd.utils.dnsmasq_signals" local nginx_signals = require "kong.cmd.utils.nginx_signals" local serf_signals = require "kong.cmd.utils.serf_signals" local conf_loader = require "kong.conf_loader" @@ -21,9 +20,6 @@ local function execute(args) local dao = assert(DAOFactory.new(conf)) assert(nginx_signals.stop(conf)) assert(serf_signals.stop(conf, dao)) - if conf.dnsmasq then - assert(dnsmasq_signals.stop(conf)) - end log("Kong stopped") end diff --git a/kong/cmd/utils/dnsmasq_signals.lua b/kong/cmd/utils/dnsmasq_signals.lua deleted file mode 100644 index d323e8d6560..00000000000 --- a/kong/cmd/utils/dnsmasq_signals.lua +++ /dev/null @@ -1,75 +0,0 @@ -local pl_stringx = require "pl.stringx" -local pl_utils = require "pl.utils" -local pl_path = require "pl.path" -local pl_file = require "pl.file" -local kill = require "kong.cmd.utils.kill" -local log = require "kong.cmd.utils.log" -local fmt = string.format - -local _M = {} - -local dnsmasq_bin_name = "dnsmasq" -local dnsmasq_search_paths = { - "/usr/local/sbin", - "/usr/local/bin", - "/usr/sbin", - "/usr/bin", - "/bin", - "" -} - -local function find_dnsmasq_bin() - log.debug("searching for 'dnsmasq' executable") - - local found - for _, path in ipairs(dnsmasq_search_paths) do - local path_to_check = pl_path.join(path, dnsmasq_bin_name) - local cmd = fmt("%s -v", path_to_check) - local ok, _, stdout = pl_utils.executeex(cmd) - log.debug("%s: '%s'", cmd, pl_stringx.splitlines(stdout)[1]) - if ok then - found = path_to_check - break - end - log.debug("'dnsmasq' executable not found at %s", path_to_check) - end - - if not found then - return nil, "could not find 'dnsmasq' executable" - end - - log.debug("found 'dnsmasq' executable at %s", found) - - return found -end - -function _M.start(kong_config) - -- is dnsmasq already running in this prefix? - if kill.is_running(kong_config.dnsmasq_pid) then - log.verbose("dnsmasq already running at %s", kong_config.dnsmasq_pid) - return true - else - log.verbose("dnsmasq not running, deleting %s", kong_config.dnsmasq_pid) - pl_file.delete(kong_config.dnsmasq_pid) - end - - local dnsmasq_bin, err = find_dnsmasq_bin() - if not dnsmasq_bin then return nil, err end - - local cmd = fmt("%s -p %d --pid-file=%s -N -o --listen-address=127.0.0.1", - dnsmasq_bin, kong_config.dnsmasq_port, kong_config.dnsmasq_pid) - - log.debug("starting dnsmasq: %s", cmd) - - local ok, _, _, stderr = pl_utils.executeex(cmd) - if not ok then return nil, stderr end - - return true -end - -function _M.stop(kong_config) - log.verbose("stopping dnsmasq at %s", kong_config.dnsmasq_pid) - return kill.kill(kong_config.dnsmasq_pid, "-15") -- SIGTERM -end - -return _M diff --git a/kong/cmd/utils/prefix_handler.lua b/kong/cmd/utils/prefix_handler.lua index 134bad53b33..0cf5019b170 100644 --- a/kong/cmd/utils/prefix_handler.lua +++ b/kong/cmd/utils/prefix_handler.lua @@ -168,9 +168,6 @@ local function compile_conf(kong_config, conf_template) tostring = tostring } - if kong_config.dnsmasq then - compile_env["dns_resolver"] = "127.0.0.1:"..kong_config.dnsmasq_port - end if kong_config.anonymous_reports and socket.dns.toip(constants.SYSLOG.ADDRESS) then compile_env["syslog_reports"] = fmt("error_log syslog:server=%s:%d error;", constants.SYSLOG.ADDRESS, constants.SYSLOG.PORT) diff --git a/kong/conf_loader.lua b/kong/conf_loader.lua index 21292138eb2..6e27ed4fedf 100644 --- a/kong/conf_loader.lua +++ b/kong/conf_loader.lua @@ -7,16 +7,17 @@ local pl_config = require "pl.config" local pl_file = require "pl.file" local pl_path = require "pl.path" local tablex = require "pl.tablex" +local utils = require "kong.tools.utils" local log = require "kong.cmd.utils.log" +local ipv4_port_pattern = "^(%d+)%.(%d+)%.(%d+)%.(%d+):(%d+)$" + local DEFAULT_PATHS = { "/etc/kong/kong.conf", "/etc/kong.conf" } local PREFIX_PATHS = { - dnsmasq_pid = {"pids", "dnsmasq.pid"} - ; serf_pid = {"pids", "serf.pid"}, serf_log = {"logs", "serf.log"}, serf_event = {"serf", "serf_event.sh"}, @@ -60,6 +61,7 @@ local CONF_INFERENCES = { cluster_listen_rpc = {typ = "string"}, cluster_advertise = {typ = "string"}, nginx_worker_processes = {typ = "string"}, + nginx_keepalive = {typ = "number"}, database = {enum = {"postgres", "cassandra"}}, pg_port = {typ = "number"}, @@ -82,8 +84,7 @@ local CONF_INFERENCES = { cluster_profile = {enum = {"local", "lan", "wan"}}, cluster_ttl_on_failure = {typ = "number"}, - dnsmasq = {typ = "boolean"}, - dnsmasq_port = {typ = "number"}, + dns_resolver = {typ = "array"}, ssl = {typ = "boolean"}, admin_ssl = {typ = "boolean"}, @@ -206,13 +207,16 @@ local function check_and_infer(conf) end end - if conf.dns_resolver and conf.dnsmasq then - errors[#errors+1] = "must disable dnsmasq when a custom DNS resolver is specified" - elseif not conf.dns_resolver and not conf.dnsmasq then - errors[#errors+1] = "must specify a custom DNS resolver when dnsmasq is turned off" + if conf.dns_resolver then + for _, server in ipairs(conf.dns_resolver) do + local dns = utils.normalize_ip(server) + if (not dns) or (dns.type ~= "ipv4") then + errors[#errors+1] = "dns_resolver must be a comma separated list in the form of IPv4 or IPv4:port" + break -- one error is enough + end + end end - local ipv4_port_pattern = "^(%d+)%.(%d+)%.(%d+)%.(%d+):(%d+)$" if not conf.cluster_listen:match(ipv4_port_pattern) then errors[#errors+1] = "cluster_listen must be in the form of IPv4:port" end @@ -409,6 +413,10 @@ local function load(path, custom_conf) log.verbose("prefix in use: %s", conf.prefix) + -- initialize the dns client, so the globally patched tcp.connect method + -- will work from here onwards. + assert(require("kong.tools.dns")(conf)) + return setmetatable(conf, nil) -- remove Map mt end diff --git a/kong/core/balancer.lua b/kong/core/balancer.lua new file mode 100644 index 00000000000..87185847dc4 --- /dev/null +++ b/kong/core/balancer.lua @@ -0,0 +1,79 @@ +local dns_client = require "dns.client" + +local toip = dns_client.toip + +-- looks up a balancer for the target. +-- @param target the table with the target details +-- @return balancer if found, or nil if not found, or nil+error on error +local get_balancer = function(target) + return nil -- TODO: place holder, forces dns use to first fix regression +end + + +local first_try_balancer = function(target) +end + +local retry_balancer = function(target) +end + +local first_try_dns = function(target) + local ip, port = toip(target.upstream.host, target.upstream.port, false) + if not ip then + return nil, port + end + target.ip = ip + target.port = port + return true +end + +local retry_dns = function(target) + local ip, port = toip(target.upstream.host, target.upstream.port, true) + if type(ip) ~= "string" then + return nil, port + end + target.ip = ip + target.port = port + return true +end + + +-- Resolves the target structure in-place (fields `ip` and `port`). +-- +-- If the hostname matches an 'upstream' pool, then it must be balanced in that +-- pool, in this case any port number provided will be ignored, as the pool provides it. +-- +-- @param target the data structure as defined in `core.access.before` where it is created +-- @return true on success, nil+error otherwise +local function execute(target) + if target.type ~= "name" then + -- it's an ip address (v4 or v6), so nothing we can do... + target.ip = target.upstream.host + target.port = target.upstream.port or 80 + return true + end + + -- when tries == 0 it runs before the `balancer` context (in the `access` context), + -- when tries >= 2 then it performs a retry in the `balancer` context + if target.tries == 0 then + local err + -- first try, so try and find a matching balancer/upstream object + target.balancer, err = get_balancer(target) + if err then return nil, err end + + if target.balancer then + return first_try_balancer(target) + else + return first_try_dns(target) + end + else + if target.balancer then + return retry_balancer(target) + else + return retry_dns(target) + end + end +end + +return { + execute = execute, +} \ No newline at end of file diff --git a/kong/core/globalpatches.lua b/kong/core/globalpatches.lua index f8bd1e99444..58b6b26b79e 100644 --- a/kong/core/globalpatches.lua +++ b/kong/core/globalpatches.lua @@ -1,20 +1,23 @@ return function(options) + options = options or {} + local meta = require "kong.meta" + + _G._KONG = { + _NAME = meta._NAME, + _VERSION = meta._VERSION + } if options.cli then ngx.IS_CLI = true + ngx.exit = function() end + end - ngx.exit = function()end - -- force LuaSocket usage to resolve `/etc/hosts` until - -- supported by resty-cli. - -- See https://github.com/Mashape/kong/issues/1523 - for _, namespace in ipairs({"cassandra", "pgmoon-mashape"}) do - local socket = require(namespace .. ".socket") - socket.force_luasocket(ngx.get_phase(), true) - end - do + do -- implement a Lua based shm, for; cli (and hence rbusted) + + if options.cli then -- ngx.shared.DICT proxy -- https://github.com/bsm/fakengx/blob/master/fakengx.lua -- with minor fixes and addtions such as exptime @@ -61,7 +64,9 @@ return function(options) return true, nil, false end function SharedDict:delete(key) - self.data[key] = nil + if self.data[key] ~= nil then + self.data[key] = nil + end return true end function SharedDict:incr(key, value) @@ -120,19 +125,16 @@ return function(options) end }) end + end - if options.rbusted then - - do - -- patch luassert's 'assert' because very often we use the Lua idiom: - -- local res = assert(some_method()) - -- in our tests. - -- luassert's 'assert' would error out in case the assertion fails, and - -- if 'some_method()' returns a third return value because we attempt to - -- perform arithmetic (+1) to the 'level' argument of 'assert'. - -- This error would often supersed the actual error (arg #2) and be painful - -- to debug. + + + do -- patch luassert when running in the Busted test environment + + if options.rbusted then + -- patch luassert's 'assert' to fix the 'third' argument problem + -- see https://github.com/Olivine-Labs/luassert/pull/141 local assert = require "luassert.assert" local assert_mt = getmetatable(assert) if assert_mt then @@ -149,18 +151,58 @@ return function(options) end end - else + end + + + + do -- randomseeding patch, for; cli, rbusted and Kong - do - local meta = require "kong.meta" + if options.rbusted then + + -- we need this version because we cannot hit the ffi, same issue + -- as with the semaphore patch + -- only used for running tests + local randomseed = math.randomseed + local seed - _G._KONG = { - _NAME = meta._NAME, - _VERSION = meta._VERSION - } - end + --- Seeds the random generator, use with care. + -- Once - properly - seeded, this method is replaced with a stub + -- one. This is to enforce best-practises for seeding in ngx_lua, + -- and prevents third-party modules from overriding our correct seed + -- (many modules make a wrong usage of `math.randomseed()` by calling + -- it multiple times or do not use unique seed for Nginx workers). + -- + -- This patched method will create a unique seed per worker process, + -- using a combination of both time and the worker's pid. + -- luacheck: globals math + _G.math.randomseed = function() + if not seed then + -- If we're in runtime nginx, we have multiple workers so we _only_ + -- accept seeding when in the 'init_worker' phase. + -- That is because that phase is the earliest one before the + -- workers have a chance to process business logic, and because + -- if we'd do that in the 'init' phase, the Lua VM is not forked + -- yet and all workers would end-up using the same seed. + if not options.cli and ngx.get_phase() ~= "init_worker" then + ngx.log(ngx.ERR, debug.traceback("math.randomseed() must be called in init_worker")) + end + + seed = ngx.time() + ngx.worker.pid() + ngx.log(ngx.DEBUG, "random seed: ", seed, " for worker nb ", ngx.worker.id(), + " (pid: ", ngx.worker.pid(), ")") + randomseed(seed) + else + ngx.log(ngx.DEBUG, "attempt to seed random number generator, but ", + "already seeded with ", seed) + end - do + return seed + end + + else + + -- this version of the randomseeding patch is required for + -- production, but doesn't work in tests, due to the ffi dependency local util = require "kong.tools.utils" local seed local randomseed = math.randomseed @@ -203,7 +245,7 @@ return function(options) ngx.log(ngx.DEBUG, "random seed: ", seed, " for worker nb ", ngx.worker.id()) - if not options.cli then + if ngx.shared.kong then local ok, err = ngx.shared.kong:safe_set("pid: " .. ngx.worker.pid(), seed) if not ok then ngx.log(ngx.WARN, "could not store PRNG seed in kong shm: ", err) @@ -220,5 +262,113 @@ return function(options) return seed end end + + end + + + + do -- pure lua semaphore patch, for; rbusted + + if options.rbusted then + -- when testing, busted will cleanup the global environment for test + -- insulation. This includes the `ffi` module. Because the dns module + -- is loaded ahead of busted, it (and its dependencies) become part + -- of the global environment that busted does NOT cleanup. + -- The semaphore library depends on the ffi, and hence prevents it + -- from being GCed. The result is that reloading other libraries will + -- generate `attempt to redefine` errors when the ffi stuff is defined + -- (because they weren't GCed). + -- __NOTE__: though it works for now, it remains a bad idea to recycle + -- the ffi module as it is c-based and will result in unpredictable + -- segfaults. At the cost of reduced test insulation the busted option + -- `--no-auto-insulation` could/should be used. + package.loaded["ngx.semaphore"] = { + new = function(n) + return { + resources = n or 0, + waiting = 0, + post = function(self, n) + self.resources = self.resources + (n or 1) + end, + wait = function(self, timeout) -- timeout = seconds + if self.resources > 0 then + self.resources = self.resources - 1 + return true + end + self.waiting = self.waiting + 1 + local expire = ngx.now() + timeout + while expire > ngx.now() do + ngx.sleep(0.001) + if self.resources > 0 then + self.resources = self.resources - 1 + self.waiting = self.waiting - 1 + return true + end + end + self.waiting = self.waiting - 1 + return nil, "timeout" + end, + count = function(self) + if self.resources > 0 then return self.resources end + return -self.waiting + end + } + end + } + end + + end + + + + do -- cosockets connect patch for dns resolution, for; cli, rbusted and Kong + + --- Patch the TCP connect method such that all connections will be resolved + -- first by the internal DNS resolver. + -- STEP 1: load code that should not be using the patched versions + require "resty.dns.resolver" -- will cache TCP and UDP functions + -- STEP 2: forward declaration of locals to hold stuff loaded AFTER patching + local toip + -- STEP 3: store original unpatched versions + local old_tcp = ngx.socket.tcp + -- STEP 4: patch globals + _G.ngx.socket.tcp = function(...) + local sock = old_tcp(...) + local old_connect = sock.connect + + sock.connect = function(s, host, port, sock_opts) + local target_ip, target_port = toip(host, port) + if not target_ip then + return nil, "[toip() name lookup failed]:"..tostring(target_port) + else + -- need to do the extra check here: https://github.com/openresty/lua-nginx-module/issues/860 + if not sock_opts then + return old_connect(s, target_ip, target_port) + else + return old_connect(s, target_ip, target_port, sock_opts) + end + end + end + return sock + end + -- STEP 5: load code that should be using the patched versions, if any (because of dependency chain) + toip = require("dns.client").toip -- this will load utils and penlight modules for example + end -end + + + + do -- patch for LuaSocket tcp sockets, block usage in cli (and hence rbusted). + + if options.cli then + local socket = require "socket" + socket.tcp = function(...) + error("should not be using this") + end + end + + end + + + +end \ No newline at end of file diff --git a/kong/core/handler.lua b/kong/core/handler.lua index 5779af7987a..7a4372723e6 100644 --- a/kong/core/handler.lua +++ b/kong/core/handler.lua @@ -10,9 +10,11 @@ local utils = require "kong.tools.utils" local reports = require "kong.core.reports" local cluster = require "kong.core.cluster" -local resolver = require "kong.core.resolver" +local resolve = require("kong.core.resolver").execute local constants = require "kong.constants" +local responses = require "kong.tools.responses" local certificate = require "kong.core.certificate" +local balancer_execute = require("kong.core.balancer").execute local ngx_now = ngx.now local server_header = _KONG._NAME.."/".._KONG._VERSION @@ -37,7 +39,27 @@ return { access = { before = function() ngx.ctx.KONG_ACCESS_START = get_now() - ngx.ctx.api, ngx.ctx.upstream_url, ngx.var.upstream_host = resolver.execute(ngx.var.request_uri, ngx.req.get_headers()) + local upstream_host, balancer_address, upstream_table + ngx.ctx.api, ngx.ctx.upstream_url, upstream_host, upstream_table = resolve(ngx.var.request_uri, ngx.req.get_headers()) + + balancer_address = { + upstream = upstream_table, -- original parsed upstream url from the resolver + type = utils.hostname_type(upstream_table.host), -- the type of `upstream.host`; ipv4, ipv6 or name + tries = 0, -- retry counter + -- ip = nil, -- final target IP address + port = upstream_table.port, -- final target port + retries = ngx.ctx.api.retries, -- number of retries for the balancer + -- health data, see https://github.com/openresty/lua-resty-core/blob/master/lib/ngx/balancer.md#get_last_failure + -- failures = nil, -- for each failure an entry { name = "...", code = xx } + -- balancer = nil, -- the balancer object, in case of a balancer + } + ngx.ctx.balancer_address = balancer_address + ngx.var.upstream_host = upstream_host + local ok, err = balancer_execute(balancer_address) + if not ok then + ngx.log(ngx.ERR, "failed the initial dns/balancer resolve: ", err) + return responses.send_HTTP_INTERNAL_SERVER_ERROR() + end end, -- Only executed if the `resolver` module found an API and allows nginx to proxy it. after = function() diff --git a/kong/core/resolver.lua b/kong/core/resolver.lua index 72e3b5519dc..4ea33f4c28f 100644 --- a/kong/core/resolver.lua +++ b/kong/core/resolver.lua @@ -1,5 +1,6 @@ local singletons = require "kong.singletons" local url = require "socket.url" +local utils = require "kong.tools.utils" local cache = require "kong.tools.database_cache" local constants = require "kong.constants" local responses = require "kong.tools.responses" @@ -11,6 +12,11 @@ local type = type local _M = {} +local HEADERS_HOST_OVERRIDE = constants.HEADERS.HOST_OVERRIDE +local HEADERS_FORWARDED_HOST = constants.HEADERS.FORWARDED_HOST +local HEADERS_FORWARDED_PREFIX = constants.HEADERS.FORWARDED_PREFIX +local DEFAULT_PORTS = { http = 80, https = 443 } + -- Take a request_host and make it a pattern for wildcard matching. -- Only do so if the request_host actually has a wildcard. local function create_wildcard_pattern(request_host) @@ -24,22 +30,9 @@ local function create_strip_request_path_pattern(request_path) return request_path:gsub("[%(%)%.%%%+%-%*%?%[%]%^%$]", function(c) return "%"..c end) end -local function get_upstream_url(api) - -- Remove trailing slash because ngx.var.request_uri always starts with a slash - return api.upstream_url:match("^(.-)/?$") -end - -local function get_host_from_upstream_url(val) - local parsed_url = url.parse(val) - - if parsed_url.port then return parsed_url.host..":"..parsed_url.port end - if parsed_url.scheme == "https" then return parsed_url.host..":443" end - return parsed_url.host -end - -- Load all APIs in memory. -- Sort the data for faster lookup: dictionary per request_host and an array of wildcard request_host. -function _M.load_apis_in_memory() +local function load_apis_in_memory() local apis, err = singletons.dao.apis:find_all() if err then return nil, err @@ -77,13 +70,13 @@ function _M.load_apis_in_memory() return { by_dns = dns_dic, request_path_arr = request_path_arr, -- all APIs with a request_path - wildcard_dns_arr = dns_wildcard_arr -- all APIs with a wildcard request_host + wildcard_dns_arr = dns_wildcard_arr, -- all APIs with a wildcard request_host } end -function _M.find_api_by_request_host(req_headers, apis_dics) +local function find_api_by_request_host(req_headers, apis_dics) local hosts_list = {} - for _, header_name in ipairs({"Host", constants.HEADERS.HOST_OVERRIDE}) do + for _, header_name in ipairs({"Host", HEADERS_HOST_OVERRIDE}) do local hosts = req_headers[header_name] if hosts then if type(hosts) == "string" then @@ -125,7 +118,7 @@ end -- ^ This would not match. -- @param `uri` The URI for this request. -- @param `request_path_arr` An array of all APIs that have a request_path property. -function _M.find_api_by_request_path(uri, request_path_arr) +local function find_api_by_request_path(uri, request_path_arr) if uri:sub(-1) ~= "/" then uri = uri.."/" end @@ -140,21 +133,6 @@ function _M.find_api_by_request_path(uri, request_path_arr) end end --- Replace `/request_path` with `request_path`, and then prefix with a `/` --- or replace `/request_path/foo` with `/foo`, and then do not prefix with `/`. -function _M.strip_request_path(uri, strip_request_path_pattern, upstream_url_has_path) - local uri = uri:gsub(strip_request_path_pattern, "", 1) - - -- Sometimes uri can be an empty string, and adding a slash "/"..uri will lead to a trailing slash - -- We don't want to add a trailing slash in one specific scenario, when the upstream_url already has - -- a path (so it's not root, like http://hello.com/, but http://hello.com/path) in order to avoid - -- having an unnecessary trailing slash not wanted by the user. Hence the "upstream_url_has_path" check. - if (not upstream_url_has_path) and (uri:sub(1,1) ~= "/") then - return "/"..uri - end - return uri -end - -- Find an API from a request made to nginx. Either from one of the Host or X-Host-Override headers -- matching the API's `request_host`, either from the `uri` matching the API's `request_path`. -- @@ -179,26 +157,21 @@ local function find_api(uri, headers) end -- Find by Host header - api, matched_host, hosts_list = _M.find_api_by_request_host(headers, apis_dics) + api, matched_host, hosts_list = find_api_by_request_host(headers, apis_dics) -- If it was found by Host, return if api then - ngx.req.set_header(constants.HEADERS.FORWARDED_HOST, matched_host) + ngx.req.set_header(HEADERS_FORWARDED_HOST, matched_host) return nil, api, matched_host, hosts_list end -- Otherwise, we look for it by request_path. We have to loop over all APIs and compare the requested URI. - api, strip_request_path_pattern = _M.find_api_by_request_path(uri, apis_dics.request_path_arr) + api, strip_request_path_pattern = find_api_by_request_path(uri, apis_dics.request_path_arr) return nil, api, nil, hosts_list, strip_request_path_pattern end -local function url_has_path(url) - local _, count_slashes = url:gsub("/", "") - return count_slashes > 2 -end - function _M.execute(request_uri, request_headers) - local uri = request_uri:match("^([^%?]+)") -- grab everything before "?" + local uri = request_uri:match("^([^%?]+)") -- grab everything before "?", dropping the query string local err, api, matched_host, hosts_list, strip_request_path_pattern = find_api(uri, request_headers) if err then return responses.send_HTTP_INTERNAL_SERVER_ERROR(err) @@ -210,26 +183,38 @@ function _M.execute(request_uri, request_headers) } end - local upstream_host - local upstream_url = get_upstream_url(api) + -- Remove trailing slash because ngx.var.request_uri always starts with a slash + local upstream_url = api.upstream_url:match("^(.-)/?$") -- If API was retrieved by request_path and the request_path needs to be stripped if strip_request_path_pattern and api.strip_request_path then - uri = _M.strip_request_path(uri, strip_request_path_pattern, url_has_path(upstream_url)) - ngx.req.set_header(constants.HEADERS.FORWARDED_PREFIX, api.request_path) + local _, count_slashes = upstream_url:gsub("/", "") + uri = uri:gsub(strip_request_path_pattern, "", 1) + if (count_slashes <= 2) and (uri:sub(1,1) ~= "/") then + -- upstream_url has no path, only the 2 scheme slashes, and the uri doesn't start with a slash + -- so we need to insert one + uri = "/"..uri + end + + ngx.req.set_header(HEADERS_FORWARDED_PREFIX, api.request_path) end upstream_url = upstream_url..uri + local upstream_table = url.parse(upstream_url) + upstream_table.port = upstream_table.port or DEFAULT_PORTS[upstream_table.scheme] or DEFAULT_PORTS.http + local upstream_host if api.preserve_host then upstream_host = matched_host or ngx.req.get_headers()["host"] end - - if upstream_host == nil then - upstream_host = get_host_from_upstream_url(upstream_url) + if not upstream_host then + upstream_host = utils.format_host(upstream_table) end - return api, upstream_url, upstream_host + return api, upstream_table.scheme.."://kong_upstream"..upstream_table.path, upstream_host, upstream_table end +-- export local for test pruposes +_M.load_apis_in_memory = load_apis_in_memory + return _M diff --git a/kong/dao/db/postgres.lua b/kong/dao/db/postgres.lua index dbd4aa4518b..0eabb02cb8f 100644 --- a/kong/dao/db/postgres.lua +++ b/kong/dao/db/postgres.lua @@ -1,4 +1,4 @@ -local pgmoon = require "pgmoon-mashape" +local pgmoon = require "pgmoon" local Errors = require "kong.dao.errors" local utils = require "kong.tools.utils" local cjson = require "cjson" diff --git a/kong/dao/migrations/cassandra.lua b/kong/dao/migrations/cassandra.lua index b03b6509bbf..2bdfe8b93bc 100644 --- a/kong/dao/migrations/cassandra.lua +++ b/kong/dao/migrations/cassandra.lua @@ -143,5 +143,41 @@ return { down = [[ ALTER TABLE nodes WITH default_time_to_live = 3600; ]] + }, + { + -- This is a 2 step migration; first create the extra column, using a cql + -- statement and following iterate over the entries to insert default values. + + -- Step 1) create extra column + name = "2016-09-05-212515_retries_step_1", + up = [[ + ALTER TABLE apis ADD retries int; + ]], + down = [[ + ALTER TABLE apis DROP retries; + ]] + }, + { + -- Step 2) insert default values + name = "2016-09-05-212515_retries_step_2", + up = function(_, _, dao) + local rows, err = dao.apis:find_all() -- fetch all rows + if err then + return err + end + + for _, row in ipairs(rows) do + if not row.retries then -- only if retries is not set already + -- we do not specify default values explicitly, as they will be + -- taken from the schema automatically by the dao. + local _, err = dao.apis:update(row, { id = row.id }, {full = true}) + if err then + return err + end + end + end + end, + down = nil, } } + diff --git a/kong/dao/migrations/postgres.lua b/kong/dao/migrations/postgres.lua index ff890866726..d7a19aff789 100644 --- a/kong/dao/migrations/postgres.lua +++ b/kong/dao/migrations/postgres.lua @@ -141,5 +141,19 @@ return { DROP TABLE ttls; DROP FUNCTION upsert_ttl(text, uuid, text, text, timestamp); ]] - } + }, + { + name = "2016-09-05-212515_retries", + up = [[ + DO $$ + BEGIN + ALTER TABLE apis ADD COLUMN retries smallint NOT NULL DEFAULT 5; + EXCEPTION WHEN duplicate_column THEN + -- Do nothing, accept existing state + END$$; + ]], + down = [[ + ALTER TABLE apis DROP COLUMN IF EXISTS retries; + ]] + }, } diff --git a/kong/dao/schemas/apis.lua b/kong/dao/schemas/apis.lua index a9721b35637..bd4e0e7e8d1 100644 --- a/kong/dao/schemas/apis.lua +++ b/kong/dao/schemas/apis.lua @@ -28,8 +28,6 @@ local function check_request_host_and_path(api_t) return true end -local dns_pattern = "^[%d%a%-%.%_]+$" - local function check_request_host(request_host, api_t) local valid, err = check_request_host_and_path(api_t) if valid == false then @@ -37,22 +35,15 @@ local function check_request_host(request_host, api_t) end if request_host ~= nil and request_host ~= "" then - local _, count = request_host:gsub("%*", "") - if count == 0 then - -- Validate regular request_host - local match = request_host:match(dns_pattern) - if match == nil then - return false, "Invalid value: "..request_host - end - - -- Reject prefix/trailing dashes and dots in each segment - -- note: punycode allowes prefixed dash, if the characters before the dash are escaped - for _, segment in ipairs(utils.split(request_host, ".")) do - if segment == "" or segment:match("-$") or segment:match("^%.") or segment:match("%.$") then - return false, "Invalid value: "..request_host - end - end - elseif count == 1 then + local temp, count = request_host:gsub("%*", "abc") -- insert valid placeholder for verification + + -- Validate regular request_host + local normalized = utils.normalize_ip(temp) + if (not normalized) or normalized.port then + return false, "Invalid value: "..request_host + end + + if count == 1 then -- Validate wildcard request_host local valid local pos = request_host:find("%*") @@ -65,7 +56,7 @@ local function check_request_host(request_host, api_t) if not valid then return false, "Invalid wildcard placement: "..request_host end - else + elseif count > 1 then return false, "Only one wildcard is allowed: "..request_host end end @@ -152,6 +143,15 @@ local function check_name(name) return true end +-- check that retries is a valid number +local function check_retries(retries) + -- Postgres 'smallint' size, 2 bytes + if (retries < 0) or (math.floor(retries) ~= retries) or (retries > 32767) then + return false, "retries must be an integer, from 0 to 32767" + end + return true +end + return { table = "apis", primary_key = {"id"}, @@ -163,7 +163,8 @@ return { request_path = {type = "string", unique = true, func = check_request_path}, strip_request_path = {type = "boolean", default = false}, upstream_url = {type = "url", required = true, func = validate_upstream_url_protocol}, - preserve_host = {type = "boolean", default = false} + preserve_host = {type = "boolean", default = false}, + retries = {type = "number", default = 5, func = check_retries}, }, marshall_event = function(self, t) return { id = t.id } diff --git a/kong/kong.lua b/kong/kong.lua index 6524a797d2c..aed08346267 100644 --- a/kong/kong.lua +++ b/kong/kong.lua @@ -26,16 +26,23 @@ require("kong.core.globalpatches")() +local dns = require "kong.tools.dns" local core = require "kong.core.handler" local Serf = require "kong.serf" local utils = require "kong.tools.utils" local Events = require "kong.core.events" +local responses = require "kong.tools.responses" local constants = require "kong.constants" local singletons = require "kong.singletons" local DAOFactory = require "kong.dao.factory" +local ngx_balancer = require "ngx.balancer" local plugins_iterator = require "kong.core.plugins_iterator" +local balancer_execute = require("kong.core.balancer").execute local ipairs = ipairs +local get_last_failure = ngx_balancer.get_last_failure +local set_current_peer = ngx_balancer.set_current_peer +local set_more_tries = ngx_balancer.set_more_tries local function attach_hooks(events, hooks) for k, v in pairs(hooks) do @@ -126,6 +133,7 @@ function Kong.init() assert(dao:run_migrations()) -- migrating in case embedded in custom nginx -- populate singletons + singletons.dns = dns(config) singletons.loaded_plugins = assert(load_plugins(config, dao, events)) singletons.serf = Serf.new(config, dao) singletons.dao = dao @@ -184,6 +192,41 @@ function Kong.ssl_certificate() end end +function Kong.balancer() + local addr = ngx.ctx.balancer_address + addr.tries = addr.tries + 1 + if addr.tries > 1 then + -- only call balancer on retry, first one is done in `core.access.before` which runs + -- in the ACCESS context and hence has less limitations than this BALANCER context + -- where the retries are executed + + -- record failure data + addr.failures = addr.failures or {} + local state, code = get_last_failure() + addr.failures[addr.tries-1] = { name = state, code = code } + + local ok, err = balancer_execute(addr) + if not ok then + ngx.log(ngx.ERR, "failed to retry the balancer/resolver: ", err) + return responses.send_HTTP_INTERNAL_SERVER_ERROR() + end + else + -- first try, so set the max number of retries + local retries = addr.retries + if retries > 0 then + set_more_tries(retries) + end + end + + -- set the targets as resolved + local ok, err = set_current_peer(addr.ip, addr.port) + if not ok then + ngx.log(ngx.ERR, "failed to set the current peer (address:'", + tostring(addr.ip),"' port:",tostring(addr.port),"): ", tostring(err)) + return responses.send_HTTP_INTERNAL_SERVER_ERROR() + end +end + function Kong.access() core.access.before() diff --git a/kong/meta.lua b/kong/meta.lua index 56128d0db50..971c6373f9b 100644 --- a/kong/meta.lua +++ b/kong/meta.lua @@ -21,6 +21,5 @@ return { nginx = {"1.9.15.1", "1.11.2.1"}, serf = {"0.7.0", "0.8.0"}, --resty = {}, -- not version dependent for now - --dnsmasq = {} -- not version dependent for now } } diff --git a/kong/singletons.lua b/kong/singletons.lua index 0d9e7b3a2fc..faee14a42dd 100644 --- a/kong/singletons.lua +++ b/kong/singletons.lua @@ -2,6 +2,7 @@ local _M = { configuration = nil, events = nil, dao = nil, + dns = nil, serf = nil, loaded_plugins = nil } diff --git a/kong/templates/kong_defaults.lua b/kong/templates/kong_defaults.lua index 885b3f3626b..55c5f1c2eb9 100644 --- a/kong/templates/kong_defaults.lua +++ b/kong/templates/kong_defaults.lua @@ -11,6 +11,7 @@ admin_listen_ssl = 0.0.0.0:8444 nginx_worker_processes = auto nginx_optimizations = on nginx_daemon = on +nginx_keepalive = 60 mem_cache_size = 128m ssl = on ssl_cert = NONE @@ -49,9 +50,8 @@ cluster_encrypt_key = NONE cluster_profile = wan cluster_ttl_on_failure = 3600 -dnsmasq = on -dnsmasq_port = 8053 dns_resolver = NONE +dns_hostsfile = /etc/hosts lua_code_cache = on lua_ssl_trusted_certificate = NONE diff --git a/kong/templates/nginx_kong.lua b/kong/templates/nginx_kong.lua index eb2c1a63d1b..e0feb28b3f1 100644 --- a/kong/templates/nginx_kong.lua +++ b/kong/templates/nginx_kong.lua @@ -1,5 +1,4 @@ return [[ -resolver ${{DNS_RESOLVER}} ipv6=off; charset UTF-8; error_log logs/error.log ${{LOG_LEVEL}}; @@ -55,6 +54,16 @@ init_worker_by_lua_block { kong.init_worker() } +proxy_next_upstream_tries 999; + +upstream kong_upstream { + server 0.0.0.1; + balancer_by_lua_block { + kong.balancer() + } + keepalive ${{NGINX_KEEPALIVE}}; +} + server { server_name kong; listen ${{PROXY_LISTEN}}; diff --git a/kong/tools/dns.lua b/kong/tools/dns.lua new file mode 100644 index 00000000000..01e3e121ad1 --- /dev/null +++ b/kong/tools/dns.lua @@ -0,0 +1,37 @@ +local dns_client + +--- Load and setup the DNS client according to the provided configuration. +-- @param conf (table) Kong configuration +-- @return the initialized `dns.client` module, or an error +local setup_client = function(conf) + if not dns_client then + dns_client = require "dns.client" + end + + conf = conf or {} + local hosts = conf.dns_hostsfile -- filename + local servers = {} + + -- servers must be reformatted as name/port sub-arrays + if conf.dns_resolver then + for i, server in ipairs(conf.dns_resolver) do + local ip, port = server:match("^([^:]+)%:*(%d*)$") + servers[i] = { ip, tonumber(port) or 53 } -- inserting port if omitted + end + end + + local opts = { + hosts = hosts, + resolv_conf = nil, -- defaults to system resolv.conf + nameservers = servers, -- provided list or taken from resolv.conf + retrans = nil, -- taken from system resolv.conf; attempts + timeout = nil, -- taken from system resolv.conf; timeout + bad_ttl = nil, -- ttl in seconds for bad dns responses (empty/error) + } + + assert(dns_client.init(opts)) + + return dns_client +end + +return setup_client diff --git a/kong/tools/utils.lua b/kong/tools/utils.lua index abb18d8f7a6..bf162e9e0c4 100644 --- a/kong/tools/utils.lua +++ b/kong/tools/utils.lua @@ -10,21 +10,25 @@ local ffi = require "ffi" local uuid = require "resty.jit-uuid" +local pl_stringx = require "pl.stringx" local C = ffi.C local ffi_new = ffi.new local ffi_str = ffi.string -local fmt = string.format local type = type local pairs = pairs local ipairs = ipairs -local re_find = ngx.re.find local tostring = tostring local sort = table.sort local concat = table.concat local insert = table.insert +local lower = string.lower +local fmt = string.format local find = string.find local gsub = string.gsub +local split = pl_stringx.split +local strip = pl_stringx.strip +local re_find = ngx.re.find ffi.cdef[[ typedef unsigned char u_char; @@ -42,6 +46,17 @@ const char *ERR_reason_error_string(unsigned long e); local _M = {} +--- splits a string. +-- just a placeholder to the penlight `pl.stringx.split` function +-- @function split +_M.split = split + +--- strips whitespace from a string. +-- just a placeholder to the penlight `pl.stringx.strip` function +-- @function strip +_M.strip = strip + + --- Retrieves the hostname of the local machine -- @return string The hostname function _M.get_hostname() @@ -144,13 +159,6 @@ end -- return str == "00000000-0000-0000-0000-000000000000" or uuid.is_valid(str) --end -do - local pl_stringx = require "pl.stringx" - - _M.split = pl_stringx.split - _M.strip = pl_stringx.strip -end - do local url = require "socket.url" @@ -304,10 +312,13 @@ function _M.deep_copy(orig) return copy end +--- Copies a table into a new table. +-- neither sub tables nor metatables will be copied. +-- @param orig The table to copy +-- @return Returns a copy of the input table function _M.shallow_copy(orig) - local orig_type = type(orig) local copy - if orig_type == "table" then + if type(orig) == "table" then copy = {} for orig_key, orig_value in pairs(orig) do copy[orig_key] = orig_value @@ -370,9 +381,6 @@ function _M.load_module_if_exists(module_name) end end -local find = string.find -local tostring = tostring - -- Numbers taken from table 3-7 in www.unicode.org/versions/Unicode6.2.0/UnicodeStandard-6.2.pdf -- find-based solution inspired by http://notebook.kulchenko.com/programming/fixing-malformed-utf8-in-lua function _M.validate_utf8(val) @@ -396,4 +404,174 @@ function _M.validate_utf8(val) return true end +--- checks the hostname type; ipv4, ipv6, or name. +-- Type is determined by exclusion, not by validation. So if it returns 'ipv6' then +-- it can only be an ipv6, but it is not necessarily a valid ipv6 address. +-- @param name the string to check (this may contain a portnumber) +-- @return string either; 'ipv4', 'ipv6', or 'name' +-- @usage hostname_type("123.123.123.123") --> "ipv4" +-- hostname_type("::1") --> "ipv6" +-- hostname_type("some::thing") --> "ipv6", but invalid... +_M.hostname_type = function(name) + local remainder, colons = gsub(name, ":", "") + if colons > 1 then return "ipv6" end + if remainder:match("^[%d%.]+$") then return "ipv4" end + return "name" +end + +--- parses, validates and normalizes an ipv4 address. +-- @param address the string containing the address (formats; ipv4, ipv4:port) +-- @return normalized address (string) + port (number or nil), or alternatively nil+error +_M.normalize_ipv4 = function(address) + local a,b,c,d,port + if address:find(":") then + -- has port number + a,b,c,d,port = address:match("^(%d%d?%d?)%.(%d%d?%d?)%.(%d%d?%d?)%.(%d%d?%d?):(%d+)$") + else + -- without port number + a,b,c,d,port = address:match("^(%d%d?%d?)%.(%d%d?%d?)%.(%d%d?%d?)%.(%d%d?%d?)$") + end + if not a then + return nil, "invalid ipv4 address: "..address + end + a,b,c,d = tonumber(a), tonumber(b), tonumber(c), tonumber(d) + if (a<0) or (a>255) or (b<0) or (b>255) or (c<0) or (c>255) or (d<0) or (d>255) then + return nil, "invalid ipv4 address: "..address + end + if port then port = tonumber(port) end + + return fmt("%d.%d.%d.%d",a,b,c,d), port +end + +--- parses, validates and normalizes an ipv6 address. +-- @param address the string containing the address (formats; ipv6, [ipv6], [ipv6]:port) +-- @return normalized expanded address (string) + port (number or nil), or alternatively nil+error +_M.normalize_ipv6 = function(address) + local check, port = address:match("^(%b[])(.-)$") + if port == "" then port = nil end + if check then + check = check:sub(2, -2) -- drop the brackets + -- we have ipv6 in brackets, now get port if we got something left + if port then + port = port:match("^:(%d-)$") + if not port then + return nil, "invalid ipv6 address" + end + end + else + -- no brackets, so full address only; no brackets, no port + check = address + port = nil + end + -- check ipv6 format and normalize + if check:sub(1,1) == ":" then check = "0"..check end + if check:sub(-1,-1) == ":" then check = check.."0" end + if check:find("::") then + -- expand double colon + local _, count = gsub(check, ":", "") + local ins = ":"..string.rep("0:", 8 - count) + check = gsub(check, "::", ins, 1) -- replace only 1 occurence! + end + local a,b,c,d,e,f,g,h = check:match("^(%x%x?%x?%x?):(%x%x?%x?%x?):(%x%x?%x?%x?):(%x%x?%x?%x?):(%x%x?%x?%x?):(%x%x?%x?%x?):(%x%x?%x?%x?):(%x%x?%x?%x?)$") + if not a then + -- not a valid IPv6 address + return nil, "invalid ipv6 address: "..address + end + local zeros = "0000" + if port then + port = tonumber(port) + end + return lower(fmt("%s:%s:%s:%s:%s:%s:%s:%s", + zeros:sub(1, 4 - #a)..a, + zeros:sub(1, 4 - #b)..b, + zeros:sub(1, 4 - #c)..c, + zeros:sub(1, 4 - #d)..d, + zeros:sub(1, 4 - #e)..e, + zeros:sub(1, 4 - #f)..f, + zeros:sub(1, 4 - #g)..g, + zeros:sub(1, 4 - #h)..h)), port +end + +--- parses and validates a hostname. +-- @param address the string containing the hostname (formats; name, name:port) +-- @return hostname (string) + port (number or nil), or alternatively nil+error +_M.check_hostname = function(address) + local name = address + local port = address:match(":(%d+)$") + if port then + name = name:sub(1, -(#port+2)) + port = tonumber(port) + end + local match = name:match("^[%d%a%-%.%_]+$") + if match == nil then + return nil, "invalid hostname: "..address + end + + -- Reject prefix/trailing dashes and dots in each segment + -- note: punycode allowes prefixed dash, if the characters before the dash are escaped + for _, segment in ipairs(split(name, ".")) do + if segment == "" or segment:match("-$") or segment:match("^%.") or segment:match("%.$") then + return nil, "invalid hostname: "..address + end + end + return name, port +end + +local verify_types = { + ipv4 = _M.normalize_ipv4, + ipv6 = _M.normalize_ipv6, + name = _M.check_hostname, +} +--- verifies and normalizes ip adresses and hostnames. Supports ipv4, ipv4:port, ipv6, [ipv6]:port, name, name:port. +-- Returned ipv4 addresses will have no leading zero's, ipv6 will be fully expanded without brackets. +-- Note: a name will not be normalized! +-- @param address string containing the address +-- @return table with the following fields: `host` (string; normalized address, or name), `type` (string; 'ipv4', 'ipv6', 'name'), and `port` (number or nil), or alternatively nil+error on invalid input +_M.normalize_ip = function(address) + local atype = _M.hostname_type(address) + local addr, port = verify_types[atype](address) + if not addr then return nil, port end + return { + type = atype, + host = addr, + port = port + } +end + +--- Formats an ip address or hostname with an (optional) port for use in urls. +-- Supports ipv4, ipv6 and names. +-- +-- Explictly accepts 'nil+error' as input, to pass through any errors from the normalizing and name checking functions. +-- @param p1 address to format, either string with name/ip, table returned from `normalize_ip`, or from the `socket.url` library. +-- @param p2 port (optional) if p1 is a table, then this port will be inserted if no port-field is in the table +-- @return formatted address or nil+error +-- @usage +-- local addr, err = format_ip(normalize_ip("001.002.003.004:123")) --> "1.2.3.4:123" +-- local addr, err = format_ip(normalize_ip("::1")) --> "[0000:0000:0000:0000:0000:0000:0000:0001]" +-- local addr, err = format_ip("::1", 80)) --> "[::1]:80" +-- local addr, err = format_ip(check_hostname("//bad..name\\")) --> nil, "invalid hostname: ..." +_M.format_host = function(p1, p2) + local t = type(p1) + if t == "nil" then + return p1, p2 -- just pass through any errors passed in + end + local host, port, typ + if t == "table" then + port = p1.port or p2 + host = p1.host + typ = p1.type or _M.hostname_type(host) + elseif t == "string" then + port = p2 + host = p1 + typ = _M.hostname_type(host) + else + return nil, "cannot format type '"..t.."'" + end + if (typ == "ipv6") and (not find(host, "%[")) then + return "["..host.."]" .. (port and ":"..port or "") + else + return host .. (port and ":"..port or "") + end +end + return _M diff --git a/spec/01-unit/02-conf_loader_spec.lua b/spec/01-unit/02-conf_loader_spec.lua index 139c9b284bc..c8714140b91 100644 --- a/spec/01-unit/02-conf_loader_spec.lua +++ b/spec/01-unit/02-conf_loader_spec.lua @@ -86,7 +86,6 @@ describe("Configuration loader", function() end) it("attaches prefix paths", function() local conf = assert(conf_loader()) - assert.equal("/usr/local/kong/pids/dnsmasq.pid", conf.dnsmasq_pid) assert.equal("/usr/local/kong/pids/serf.pid", conf.serf_pid) assert.equal("/usr/local/kong/logs/serf.log", conf.serf_log) assert.equal("/usr/local/kong/serf/serf_event.sh", conf.serf_event) @@ -112,7 +111,7 @@ describe("Configuration loader", function() end) it("overcomes penlight's list_delim option", function() local conf = assert(conf_loader("spec/fixtures/to-strip.conf")) - assert.False(conf.dnsmasq) + assert.False(conf.pg_ssl) assert.True(conf.plugins.foobar) assert.True(conf.plugins["hello-world"]) end) @@ -120,7 +119,6 @@ describe("Configuration loader", function() describe("inferences", function() it("infer booleans (on/off/true/false strings)", function() local conf = assert(conf_loader()) - assert.True(conf.dnsmasq) assert.equal("on", conf.nginx_daemon) assert.equal("on", conf.lua_code_cache) assert.True(conf.anonymous_reports) @@ -236,30 +234,26 @@ describe("Configuration loader", function() assert.is_nil(conf) assert.equal("proxy_listen_ssl must be of form 'address:port'", err) end) - it("errors when both a resolver and dnsmasq are enabled", function() + it("errors when dns_resolver is not a list in ipv4[:port] format", function() local conf, err = conf_loader(nil, { - dnsmasq = true, - dns_resolver = "8.8.8.8:53" + dns_resolver = "[::1]:53" + }) + assert.equal("dns_resolver must be a comma separated list in the form of IPv4 or IPv4:port", err) + assert.is_nil(conf) + + local conf, err = conf_loader(nil, { + dns_resolver = "1.2.3.4:53;4.3.2.1" -- ; as separator }) - assert.equal("must disable dnsmasq when a custom DNS resolver is specified", err) + assert.equal("dns_resolver must be a comma separated list in the form of IPv4 or IPv4:port", err) assert.is_nil(conf) conf, err = conf_loader(nil, { - dnsmasq = false, - dns_resolver = "8.8.8.8:53" + dns_resolver = "8.8.8.8,1.2.3.4:53" }) assert.is_nil(err) assert.is_table(conf) - end) - it("requires a dns_resolver when dnsmasq is disabled", function() - local conf, err = conf_loader(nil, { - dnsmasq = false - }) - assert.equal("must specify a custom DNS resolver when dnsmasq is turned off", err) - assert.is_nil(conf) conf, err = conf_loader(nil, { - dnsmasq = false, dns_resolver = "8.8.8.8:53" }) assert.is_nil(err) diff --git a/spec/01-unit/04-utils_spec.lua b/spec/01-unit/04-utils_spec.lua index a804d1fff0b..ecb71262880 100644 --- a/spec/01-unit/04-utils_spec.lua +++ b/spec/01-unit/04-utils_spec.lua @@ -331,4 +331,126 @@ describe("Utils", function() end) end) end) + + describe("hostnames and ip addresses", function() + describe("hostname_type", function() + -- no check on "name" type as anything not ipv4 and not ipv6 will be labelled as 'name' anyway + it("checks valid IPv4 address types", function() + assert.are.same("ipv4", utils.hostname_type("123.123.123.123")) + assert.are.same("ipv4", utils.hostname_type("1.2.3.4")) + end) + it("checks valid IPv6 address types", function() + assert.are.same("ipv6", utils.hostname_type("::1")) + assert.are.same("ipv6", utils.hostname_type("2345::6789")) + assert.are.same("ipv6", utils.hostname_type("0001:0001:0001:0001:0001:0001:0001:0001")) + end) + end) + describe("parsing", function() + it("normalizes IPv4 address types", function() + assert.are.same({"123.123.123.123"}, {utils.normalize_ipv4("123.123.123.123")}) + assert.are.same({"123.123.123.123", 80}, {utils.normalize_ipv4("123.123.123.123:80")}) + assert.are.same({"1.1.1.1"}, {utils.normalize_ipv4("1.1.1.1")}) + assert.are.same({"1.1.1.1", 80}, {utils.normalize_ipv4("001.001.001.001:00080")}) + end) + it("fails normalizing bad IPv4 address types", function() + assert.is_nil(utils.normalize_ipv4("123.123:80")) + assert.is_nil(utils.normalize_ipv4("123.123.123.999")) + assert.is_nil(utils.normalize_ipv4("123.123.123.123:80a")) + assert.is_nil(utils.normalize_ipv4("123.123.123.123.123:80")) + assert.is_nil(utils.normalize_ipv4("localhost:80")) + assert.is_nil(utils.normalize_ipv4("[::1]:80")) + end) + it("normalizes IPv6 address types", function() + assert.are.same({"0000:0000:0000:0000:0000:0000:0000:0001"}, {utils.normalize_ipv6("::1")}) + assert.are.same({"0000:0000:0000:0000:0000:0000:0000:0001"}, {utils.normalize_ipv6("[::1]")}) + assert.are.same({"0000:0000:0000:0000:0000:0000:0000:0001", 80}, {utils.normalize_ipv6("[::1]:80")}) + assert.are.same({"0000:0000:0000:0000:0000:0000:0000:0001", 80}, {utils.normalize_ipv6("[0000:0000:0000:0000:0000:0000:0000:0001]:80")}) + end) + it("fails normalizing bad IPv6 address types", function() + assert.is_nil(utils.normalize_ipv6("123.123.123.123")) + assert.is_nil(utils.normalize_ipv6("localhost:80")) + assert.is_nil(utils.normalize_ipv6("::x")) + assert.is_nil(utils.normalize_ipv6("[::x]:80")) + assert.is_nil(utils.normalize_ipv6("[::1]:80a")) + assert.is_nil(utils.normalize_ipv6("1")) + end) + it("validates hostnames", function() + local valids = {"hello.com", "hello.fr", "test.hello.com", "1991.io", "hello.COM", + "HELLO.com", "123helloWORLD.com", "mockbin.123", "mockbin-api.com", + "hello.abcd", "mockbin_api.com", "localhost", + -- punycode examples from RFC3492; https://tools.ietf.org/html/rfc3492#page-14 + -- specifically the japanese ones as they mix ascii with escaped characters + "3B-ww4c5e180e575a65lsy2b", "-with-SUPER-MONKEYS-pc58ag80a8qai00g7n9n", + "Hello-Another-Way--fc4qua05auwb3674vfr0b", "2-u9tlzr9756bt3uc0v", + "MajiKoi5-783gue6qz075azm5e", "de-jg4avhby1noc0d", "d9juau41awczczp", + } + local invalids = {"/mockbin", ".mockbin", "mockbin.", "mock;bin", + "mockbin.com/org", + "mockbin-.org", "mockbin.org-", + "hello..mockbin.com", "hello-.mockbin.com"} + for _, name in ipairs(valids) do + assert.are.same(name, (utils.check_hostname(name))) + end + for _, name in ipairs(valids) do + assert.are.same({ [1] = name, [2] = 80}, { utils.check_hostname(name..":80")}) + end + for _, name in ipairs(valids) do + assert.is_nil((utils.check_hostname(name..":xx"))) + end + for _, name in ipairs(invalids) do + assert.is_nil((utils.check_hostname(name))) + assert.is_nil((utils.check_hostname(name..":80"))) + end + end) + it("validates addresses", function() + assert.are.same({host = "1.2.3.4", type = "ipv4", port = 80}, utils.normalize_ip("1.2.3.4:80")) + assert.are.same({host = "1.2.3.4", type = "ipv4", port = nil}, utils.normalize_ip("1.2.3.4")) + assert.are.same({host = "0000:0000:0000:0000:0000:0000:0000:0001", type = "ipv6", port = 80}, utils.normalize_ip("[::1]:80")) + assert.are.same({host = "0000:0000:0000:0000:0000:0000:0000:0001", type = "ipv6", port = nil}, utils.normalize_ip("::1")) + assert.are.same({host = "localhost", type = "name", port = 80}, utils.normalize_ip("localhost:80")) + assert.are.same({host = "mashape.com", type = "name", port = nil}, utils.normalize_ip("mashape.com")) + + assert.is_nil((utils.normalize_ip("1.2.3.4:8x0"))) + assert.is_nil((utils.normalize_ip("1.2.3.400"))) + assert.is_nil((utils.normalize_ip("[::1]:8x0"))) + assert.is_nil((utils.normalize_ip(":x:1"))) + assert.is_nil((utils.normalize_ip("localhost:8x0"))) + assert.is_nil((utils.normalize_ip("mashape..com"))) + end) + end) + describe("formatting", function() + it("correctly formats addresses", function() + assert.are.equal("1.2.3.4", utils.format_host("1.2.3.4")) + assert.are.equal("1.2.3.4:80", utils.format_host("1.2.3.4", 80)) + assert.are.equal("[::1]", utils.format_host("::1")) + assert.are.equal("[::1]:80", utils.format_host("::1", 80)) + assert.are.equal("localhost", utils.format_host("localhost")) + assert.are.equal("mashape.com:80", utils.format_host("mashape.com", 80)) + -- passthrough (string) + assert.are.equal("1.2.3.4", utils.format_host(utils.normalize_ipv4("1.2.3.4"))) + assert.are.equal("1.2.3.4:80", utils.format_host(utils.normalize_ipv4("1.2.3.4:80"))) + assert.are.equal("[0000:0000:0000:0000:0000:0000:0000:0001]", utils.format_host(utils.normalize_ipv6("::1"))) + assert.are.equal("[0000:0000:0000:0000:0000:0000:0000:0001]:80", utils.format_host(utils.normalize_ipv6("[::1]:80"))) + assert.are.equal("localhost", utils.format_host(utils.check_hostname("localhost"))) + assert.are.equal("mashape.com:80", utils.format_host(utils.check_hostname("mashape.com:80"))) + -- passthrough general (table) + assert.are.equal("1.2.3.4", utils.format_host(utils.normalize_ip("1.2.3.4"))) + assert.are.equal("1.2.3.4:80", utils.format_host(utils.normalize_ip("1.2.3.4:80"))) + assert.are.equal("[0000:0000:0000:0000:0000:0000:0000:0001]", utils.format_host(utils.normalize_ip("::1"))) + assert.are.equal("[0000:0000:0000:0000:0000:0000:0000:0001]:80", utils.format_host(utils.normalize_ip("[::1]:80"))) + assert.are.equal("localhost", utils.format_host(utils.normalize_ip("localhost"))) + assert.are.equal("mashape.com:80", utils.format_host(utils.normalize_ip("mashape.com:80"))) + -- passthrough errors + local one, two = utils.format_host(utils.normalize_ipv4("1.2.3.4.5")) + assert.are.equal("nilstring", type(one)..type(two)) + local one, two = utils.format_host(utils.normalize_ipv6("not ipv6 ...")) + assert.are.equal("nilstring", type(one)..type(two)) + local one, two = utils.format_host(utils.check_hostname("//bad..name\\:123")) + assert.are.equal("nilstring", type(one)..type(two)) + local one, two = utils.format_host(utils.normalize_ip("m a s h a p e.com:80")) + assert.are.equal("nilstring", type(one)..type(two)) + end) + end) + end) + end) diff --git a/spec/01-unit/08-entities_schemas_spec.lua b/spec/01-unit/08-entities_schemas_spec.lua index 3ca89cd63bf..6411dea2c66 100644 --- a/spec/01-unit/08-entities_schemas_spec.lua +++ b/spec/01-unit/08-entities_schemas_spec.lua @@ -134,7 +134,7 @@ describe("Entities Schemas", function() assert.False(valid) assert.equal("Only one wildcard is allowed: *.mockbin.*", errors.request_host) end) - it("should refuse invalid wildcard request_host", function() + it("should refuse invalid wildcard request_host placement", function() local invalids = {"*mockbin.com", "www.mockbin*", "mock*bin.com"} for _, v in ipairs(invalids) do @@ -144,6 +144,21 @@ describe("Entities Schemas", function() assert.False(valid) end end) + it("should refuse invalid wildcard request_host", function() + local invalids = {"/mockbin", ".mockbin", "mockbin.", "mock;bin", + "mockbin.com/org", + "mockbin-.org", "mockbin.org-", + "hello..mockbin.com", "hello-.mockbin.com"} + + for _, v in ipairs(invalids) do + v = "*."..v + local t = {request_host = v, upstream_url = "http://mockbin.com", name = "mockbin"} + local valid, errors = validate_entity(t, api_schema) + assert.equal("Invalid value: "..v, (errors and errors.request_host or "")) + assert.falsy(errors.request_path) + assert.False(valid) + end + end) end) describe("request_path", function() @@ -242,6 +257,27 @@ describe("Entities Schemas", function() end end) end) + + describe("retries", function() + it("accepts valid values", function() + local valids = {0, 5, 100, 32767} + for _, v in ipairs(valids) do + local t = {request_host = "mydomain.com", upstream_url = "http://mockbin.com", name = "mockbin", retries = v} + local valid, errors = validate_entity(t, api_schema) + assert.falsy(errors) + assert.True(valid) + end + end) + it("rejects invalid values", function() + local valids = { -5, 32768} + for _, v in ipairs(valids) do + local t = {request_host = "mydomain.com", upstream_url = "http://mockbin.com", name = "mockbin", retries = v} + local valid, errors = validate_entity(t, api_schema) + assert.False(valid) + assert.equal("retries must be an integer, from 0 to 32767", errors.retries) + end + end) + end) it("should validate without a request_path", function() local valid, errors = validate_entity({ diff --git a/spec/01-unit/12-resolver_spec.lua b/spec/01-unit/12-resolver_spec.lua index 2a0af17eebe..4b60f270481 100644 --- a/spec/01-unit/12-resolver_spec.lua +++ b/spec/01-unit/12-resolver_spec.lua @@ -82,37 +82,23 @@ describe("Resolver", function() end) end) - describe("strip_request_path()", function() - local apis_dics - setup(function() - apis_dics = resolver.load_apis_in_memory() - end) - - it("should strip the api's request_path from the requested URI", function() - assert.equal("/status/200", resolver.strip_request_path("/mockbin/status/200", apis_dics.request_path_arr[7].strip_request_path_pattern)) - assert.equal("/status/200", resolver.strip_request_path("/mockbin-with-dashes/status/200", apis_dics.request_path_arr[6].strip_request_path_pattern)) - assert.equal("/", resolver.strip_request_path("/mockbin", apis_dics.request_path_arr[7].strip_request_path_pattern)) - assert.equal("/", resolver.strip_request_path("/mockbin/", apis_dics.request_path_arr[7].strip_request_path_pattern)) - end) - it("should only strip the first pattern", function() - assert.equal("/mockbin/status/200/mockbin", resolver.strip_request_path("/mockbin/mockbin/status/200/mockbin", apis_dics.request_path_arr[7].strip_request_path_pattern)) - end) - it("should not add final slash", function() - assert.equal("hello", resolver.strip_request_path("hello", apis_dics.request_path_arr[3].strip_request_path_pattern, true)) - assert.equal("/hello", resolver.strip_request_path("hello", apis_dics.request_path_arr[3].strip_request_path_pattern, false)) - end) - end) - -- Note: ngx.var.request_uri always adds a trailing slash even with a request without any -- `curl kong:8000` will result in ngx.var.request_uri being '/' describe("execute()", function() local DEFAULT_REQUEST_URI = "/" it("should find an API by the request's simple Host header", function() - local api, upstream_url, upstream_host = resolver.execute(DEFAULT_REQUEST_URI, {["Host"] = "mockbin.com"}) + local api, upstream_url, upstream_host, upstream_table = resolver.execute(DEFAULT_REQUEST_URI, {["Host"] = "mockbin.com"}) assert.same(APIS_FIXTURES[1], api) - assert.equal("http://mockbin.com/", upstream_url) - assert.equal("mockbin.com", upstream_host) + assert.same({ + authority = 'mockbin.com', + host = 'mockbin.com', + path = '/', + port = 80, + scheme = 'http', + }, upstream_table) + assert.equal("http://kong_upstream/", upstream_url) + assert.equal("mockbin.com:80", upstream_host) api = resolver.execute(DEFAULT_REQUEST_URI, {["Host"] = "mockbin-auth.com"}) assert.same(APIS_FIXTURES[2], api) @@ -121,10 +107,17 @@ describe("Resolver", function() assert.same(APIS_FIXTURES[1], api) end) it("should find an API by the request's wildcard Host header", function() - local api, upstream_url, upstream_host = resolver.execute(DEFAULT_REQUEST_URI, {["Host"] = "foobar.wildcard.com"}) + local api, upstream_url, upstream_host, upstream_table = resolver.execute(DEFAULT_REQUEST_URI, {["Host"] = "foobar.wildcard.com"}) assert.same(APIS_FIXTURES[3], api) - assert.equal("http://mockbin.com/", upstream_url) - assert.equal("mockbin.com", upstream_host) + assert.same({ + authority = 'mockbin.com', + host = 'mockbin.com', + path = '/', + port = 80, + scheme = 'http', + }, upstream_table) + assert.equal("http://kong_upstream/", upstream_url) + assert.equal("mockbin.com:80", upstream_host) api = resolver.execute(DEFAULT_REQUEST_URI, {["Host"] = "something.wildcard.com"}) assert.same(APIS_FIXTURES[3], api) @@ -136,10 +129,17 @@ describe("Resolver", function() assert.same(APIS_FIXTURES[4], api) end) it("should find an API by the request's URI (path component)", function() - local api, upstream_url, upstream_host = resolver.execute("/mockbin", {}) + local api, upstream_url, upstream_host, upstream_table = resolver.execute("/mockbin", {}) assert.same(APIS_FIXTURES[5], api) - assert.equal("http://mockbin.com/mockbin", upstream_url) - assert.equal("mockbin.com", upstream_host) + assert.same({ + authority = 'mockbin.com', + host = 'mockbin.com', + path = '/mockbin', + port = 80, + scheme = 'http', + }, upstream_table) + assert.equal("http://kong_upstream/mockbin", upstream_url) + assert.equal("mockbin.com:80", upstream_host) api = resolver.execute("/mockbin-with-dashes", {}) assert.same(APIS_FIXTURES[6], api) @@ -158,7 +158,7 @@ describe("Resolver", function() end) -- non existant request_path - local api, upstream_url, upstream_host = resolver.execute("/inexistant-mockbin", {}) + local api, upstream_url, upstream_host, _ = resolver.execute("/inexistant-mockbin", {}) assert.falsy(api) assert.falsy(upstream_url) assert.falsy(upstream_host) @@ -221,45 +221,73 @@ describe("Resolver", function() assert.same(APIS_FIXTURES[9], api) -- strip when contains pattern characters - local api, upstream_url, upstream_host = resolver.execute("/strip-me/hello/world", {}) + local api, upstream_url, upstream_host, upstream_table = resolver.execute("/strip-me/hello/world", {}) assert.same(APIS_FIXTURES[10], api) - assert.equal("http://mockbin.com/hello/world", upstream_url) - assert.equal("mockbin.com", upstream_host) + assert.same({ + authority = 'mockbin.com', + host = 'mockbin.com', + path = '/hello/world', + port = 80, + scheme = 'http', + }, upstream_table) + assert.equal("http://kong_upstream/hello/world", upstream_url) + assert.equal("mockbin.com:80", upstream_host) -- only strip first match of request_uri api, upstream_url = resolver.execute("/strip-me/strip-me/hello/world", {}) assert.same(APIS_FIXTURES[10], api) - assert.equal("http://mockbin.com/strip-me/hello/world", upstream_url) + assert.equal("http://kong_upstream/strip-me/hello/world", upstream_url) end) it("should preserve_host", function() - local api, upstream_url, upstream_host = resolver.execute(DEFAULT_REQUEST_URI, {["Host"] = "preserve-host.com"}) + local api, upstream_url, upstream_host, upstream_table = resolver.execute(DEFAULT_REQUEST_URI, {["Host"] = "preserve-host.com"}) assert.same(APIS_FIXTURES[11], api) - assert.equal("http://mockbin.com/", upstream_url) + assert.same({ + authority = 'mockbin.com', + host = 'mockbin.com', + path = '/', + port = 80, + scheme = 'http', + }, upstream_table) + assert.equal("http://kong_upstream/", upstream_url) assert.equal("preserve-host.com", upstream_host) - api, upstream_url, upstream_host = resolver.execute(DEFAULT_REQUEST_URI, { + api, upstream_url, upstream_host, upstream_table = resolver.execute(DEFAULT_REQUEST_URI, { ["Host"] = {"inexistant.com", "preserve-host.com"}, ["X-Host-Override"] = "hello.com" }) assert.same(APIS_FIXTURES[11], api) - assert.equal("http://mockbin.com/", upstream_url) + assert.same({ + authority = 'mockbin.com', + host = 'mockbin.com', + path = '/', + port = 80, + scheme = 'http', + }, upstream_table) + assert.equal("http://kong_upstream/", upstream_url) assert.equal("preserve-host.com", upstream_host) -- No host given to this request, we extract if from the configured upstream_url api, upstream_url, upstream_host = resolver.execute("/preserve-host", {}) assert.same(APIS_FIXTURES[11], api) - assert.equal("http://mockbin.com/preserve-host", upstream_url) - assert.equal("mockbin.com", upstream_host) + assert.same({ + authority = 'mockbin.com', + host = 'mockbin.com', + path = '/', + port = 80, + scheme = 'http', + }, upstream_table) + assert.equal("http://kong_upstream/preserve-host", upstream_url) + assert.equal("mockbin.com:80", upstream_host) end) it("should not decode percent-encoded values in URI", function() -- they should be forwarded as-is local api, upstream_url = resolver.execute("/mockbin/path%2Fwith%2Fencoded/values", {}) assert.same(APIS_FIXTURES[5], api) - assert.equal("http://mockbin.com/mockbin/path%2Fwith%2Fencoded/values", upstream_url) + assert.equal("http://kong_upstream/mockbin/path%2Fwith%2Fencoded/values", upstream_url) api, upstream_url = resolver.execute("/strip-me/path%2Fwith%2Fencoded/values", {}) assert.same(APIS_FIXTURES[10], api) - assert.equal("http://mockbin.com/path%2Fwith%2Fencoded/values", upstream_url) + assert.equal("http://kong_upstream/path%2Fwith%2Fencoded/values", upstream_url) end) it("should not recognized request_path if percent-encoded", function() local responses = require "kong.tools.responses" @@ -275,27 +303,41 @@ describe("Resolver", function() ngx.status = nil end) it("should have or not have a trailing slash depending on the request URI", function() - local api, upstream_url = resolver.execute("/strip/", {}) + local api, upstream_url, _, upstream_table = resolver.execute("/strip/", {}) assert.same(APIS_FIXTURES[9], api) - assert.equal("http://mockbin.com/some/path/", upstream_url) + assert.same({ + authority = 'mockbin.com', + host = 'mockbin.com', + path = '/some/path/', + port = 80, + scheme = 'http', + }, upstream_table) + assert.equal("http://kong_upstream/some/path/", upstream_url) api, upstream_url = resolver.execute("/strip", {}) assert.same(APIS_FIXTURES[9], api) - assert.equal("http://mockbin.com/some/path", upstream_url) + assert.equal("http://kong_upstream/some/path", upstream_url) api, upstream_url = resolver.execute("/mockbin-with-dashes", {}) assert.same(APIS_FIXTURES[6], api) - assert.equal("http://mockbin.com/some/path/mockbin-with-dashes", upstream_url) + assert.equal("http://kong_upstream/some/path/mockbin-with-dashes", upstream_url) api, upstream_url = resolver.execute("/mockbin-with-dashes/", {}) assert.same(APIS_FIXTURES[6], api) - assert.equal("http://mockbin.com/some/path/mockbin-with-dashes/", upstream_url) + assert.equal("http://kong_upstream/some/path/mockbin-with-dashes/", upstream_url) end) it("should strip the querystring out of the URI", function() -- it will be re-inserted by core.handler just before proxying, once all plugins have been run and eventually modified it - local api, upstream_url = resolver.execute("/?hello=world&foo=bar", {["Host"] = "mockbin.com"}) + local api, upstream_url, _, upstream_table = resolver.execute("/?hello=world&foo=bar", {["Host"] = "mockbin.com"}) assert.same(APIS_FIXTURES[1], api) - assert.equal("http://mockbin.com/", upstream_url) + assert.same({ + authority = 'mockbin.com', + host = 'mockbin.com', + path = '/', + port = 80, + scheme = 'http', + }, upstream_table) + assert.equal("http://kong_upstream/", upstream_url) end) end) end) diff --git a/spec/02-integration/01-cmd/02-start_stop_spec.lua b/spec/02-integration/01-cmd/02-start_stop_spec.lua index 5ead1ad8c8e..57662b6dc62 100644 --- a/spec/02-integration/01-cmd/02-start_stop_spec.lua +++ b/spec/02-integration/01-cmd/02-start_stop_spec.lua @@ -102,16 +102,13 @@ describe("kong start/stop", function() end) describe("/etc/hosts resolving in CLI", function() - -- dnsmasq is not yet started at this point and resty-cli does not include - -- it in its resolver directive. As such and until supported by resty-cli, - -- we must force the use of LuaSocket in our CLI to resolve localhost. - it("resolves cassandra hostname", function() + it("resolves #cassandra hostname", function() assert(helpers.kong_exec("start --vv --conf "..helpers.test_conf_path, { cassandra_contact_points = "localhost", database = "cassandra" })) end) - it("resolves postgres hostname", function() + it("resolves #postgres hostname", function() assert(helpers.kong_exec("start --conf "..helpers.test_conf_path, { pg_host = "localhost", database = "postgres" @@ -141,41 +138,6 @@ describe("kong start/stop", function() end) end) - describe("dnsmasq", function() - it("starts dnsmasq daemon", function() - assert(helpers.kong_exec("start --conf "..helpers.test_conf_path, { - dnsmasq = true, - dns_resolver = "" - })) - - local cmd = string.format("kill -0 `cat %s` >/dev/null 2>&1", - helpers.test_conf.dnsmasq_pid) - local _, code = helpers.utils.executeex(cmd) - assert.equal(0, code) - end) - it("recovers from expired dnsmasq.pid file", function() - assert(helpers.execute("touch "..helpers.test_conf.dnsmasq_pid)) -- dumb pid - assert(helpers.kong_exec("start --conf "..helpers.test_conf_path, { - dnsmasq = true, - dns_resolver = "" - })) - - local cmd = string.format("kill -0 `cat %s` >/dev/null 2>&1", - helpers.test_conf.dnsmasq_pid) - local _, code = helpers.utils.executeex(cmd) - assert.equal(0, code) - end) - it("dumps PID in prefix", function() - assert(helpers.kong_exec("start --conf "..helpers.test_conf_path, { - dnsmasq = true, - dns_resolver = "" - })) - assert.truthy(helpers.path.exists(helpers.test_conf.dnsmasq_pid)) - assert(helpers.kong_exec("stop --prefix "..helpers.test_conf.prefix)) - assert.False(helpers.path.exists(helpers.test_conf.dnsmasq_pid)) - end) - end) - describe("errors", function() it("start inexistent Kong conf file", function() local ok, stderr = helpers.kong_exec "start --conf foobar.conf" @@ -215,14 +177,10 @@ describe("kong start/stop", function() thread:join() end) - local ok, err = helpers.kong_exec("start --conf "..helpers.test_conf_path, { - dnsmasq = true, - dns_resolver = "" - }) + local ok, err = helpers.kong_exec("start --conf "..helpers.test_conf_path) assert.False(ok) assert.matches("Address already in use", err, nil, true) - assert.falsy(kill.is_running(helpers.test_conf.dnsmasq_pid)) assert.falsy(kill.is_running(helpers.test_conf.serf_pid)) end) it("should not stop Kong if already running in prefix", function() diff --git a/spec/02-integration/01-cmd/06-check_spec.lua b/spec/02-integration/01-cmd/06-check_spec.lua index 5263de66677..3569730eb21 100644 --- a/spec/02-integration/01-cmd/06-check_spec.lua +++ b/spec/02-integration/01-cmd/06-check_spec.lua @@ -8,7 +8,6 @@ describe("kong check", function() it("reports invalid conf", function() local _, stderr = helpers.kong_exec("check spec/fixtures/invalid.conf") assert.matches("[error] cassandra_repl_strategy has", stderr, nil, true) - assert.matches("[error] must disable dnsmasq when a custom DNS resolver is specified", stderr, nil, true) end) it("doesn't like invalid files", function() local _, stderr = helpers.kong_exec("check inexistent.conf") diff --git a/spec/02-integration/01-cmd/08-restart_spec.lua b/spec/02-integration/01-cmd/08-restart_spec.lua index bcab478b15f..efbcf108200 100644 --- a/spec/02-integration/01-cmd/08-restart_spec.lua +++ b/spec/02-integration/01-cmd/08-restart_spec.lua @@ -19,27 +19,18 @@ describe("kong restart", function() assert(helpers.kong_exec("restart --conf "..helpers.test_conf_path)) end) it("restarts if already running from --conf", function() - local env = { - dnsmasq = true, - dns_resolver = "" - } - - assert(helpers.kong_exec("start --conf "..helpers.test_conf_path, env)) + assert(helpers.kong_exec("start --conf "..helpers.test_conf_path, {})) ngx.sleep(1) local serf_pid = assert(helpers.file.read(helpers.test_conf.serf_pid)) local nginx_pid = assert(helpers.file.read(helpers.test_conf.nginx_pid)) - local dnsmasq_pid = assert(helpers.file.read(helpers.test_conf.dnsmasq_pid)) - assert(helpers.kong_exec("restart --conf "..helpers.test_conf_path, env)) + assert(helpers.kong_exec("restart --conf "..helpers.test_conf_path, {})) ngx.sleep(1) assert.is_not.equal(assert(helpers.file.read(helpers.test_conf.nginx_pid)), nginx_pid) assert.is_not.equal(assert(helpers.file.read(helpers.test_conf.serf_pid)), serf_pid) - assert.is_not.equal(assert(helpers.file.read(helpers.test_conf.dnsmasq_pid)), dnsmasq_pid) end) it("restarts if already running from --prefix", function() local env = { - dnsmasq = true, - dns_resolver = "", pg_database = helpers.test_conf.pg_database } @@ -47,13 +38,11 @@ describe("kong restart", function() ngx.sleep(1) local serf_pid = assert(helpers.file.read(helpers.test_conf.serf_pid)) local nginx_pid = assert(helpers.file.read(helpers.test_conf.nginx_pid)) - local dnsmasq_pid = assert(helpers.file.read(helpers.test_conf.dnsmasq_pid)) assert(helpers.kong_exec("restart --prefix "..helpers.test_conf.prefix, env)) ngx.sleep(1) assert.is_not.equal(assert(helpers.file.read(helpers.test_conf.nginx_pid)), nginx_pid) assert.is_not.equal(assert(helpers.file.read(helpers.test_conf.serf_pid)), serf_pid) - assert.is_not.equal(assert(helpers.file.read(helpers.test_conf.dnsmasq_pid)), dnsmasq_pid) end) it("accepts a custom nginx template", function() local env = { @@ -82,7 +71,6 @@ describe("kong restart", function() database = helpers.test_conf.database, pg_database = helpers.test_conf.pg_database, cassandra_keyspace = helpers.test_conf.cassandra_keyspace, - dnsmasq = true, dns_resolver = "" } diff --git a/spec/02-integration/01-cmd/09-health_spec.lua b/spec/02-integration/01-cmd/09-health_spec.lua index e3bca99dc5a..e712199abb0 100644 --- a/spec/02-integration/01-cmd/09-health_spec.lua +++ b/spec/02-integration/01-cmd/09-health_spec.lua @@ -21,7 +21,6 @@ describe("kong health", function() local _, _, stdout = assert(helpers.kong_exec("health --prefix "..helpers.test_conf.prefix)) assert.matches("serf%.-running", stdout) assert.matches("nginx%.-running", stdout) - assert.not_matches("dnsmasq.*running", stdout) assert.matches("Kong is healthy at "..helpers.test_conf.prefix, stdout, nil, true) end) it("fails when Kong is not running", function() @@ -37,16 +36,6 @@ describe("kong health", function() assert.False(ok) assert.matches("some services are not running", stderr, nil, true) end) - it("checks dnsmasq if enabled", function() - assert(helpers.kong_exec("start --conf "..helpers.test_conf_path)) - - local ok, stderr = helpers.kong_exec("health --prefix "..helpers.test_conf.prefix, { - dnsmasq = true, - dns_resolver = "" - }) - assert.False(ok) - assert.matches("some services are not running", stderr, nil, true) - end) describe("errors", function() it("errors on inexisting prefix", function() diff --git a/spec/02-integration/02-dao/02-migrations_spec.lua b/spec/02-integration/02-dao/02-migrations_spec.lua index c48734a7871..2520f7e12e6 100644 --- a/spec/02-integration/02-dao/02-migrations_spec.lua +++ b/spec/02-integration/02-dao/02-migrations_spec.lua @@ -7,8 +7,10 @@ helpers.for_each_dao(function(kong_config) describe("Model migrations with DB: #"..kong_config.database, function() local factory setup(function() - local f = assert(Factory.new(kong_config)) - f:drop_schema() + -- some `setup` functions also use `factory` and they run before the `before_each` chain + -- hence we need to set it here, and again in `before_each`. + factory = assert(Factory.new(kong_config)) + factory:drop_schema() end) before_each(function() factory = assert(Factory.new(kong_config)) diff --git a/spec/02-integration/03-admin_api/02-apis_routes_spec.lua b/spec/02-integration/03-admin_api/02-apis_routes_spec.lua index 5fc81f6ca24..fa778af53e3 100644 --- a/spec/02-integration/03-admin_api/02-apis_routes_spec.lua +++ b/spec/02-integration/03-admin_api/02-apis_routes_spec.lua @@ -46,6 +46,7 @@ describe("Admin API", function() assert.is_nil(json.request_path) assert.False(json.preserve_host) assert.False(json.strip_request_path) + assert.equals(5, json.retries) end end) describe("errors", function() @@ -134,7 +135,8 @@ describe("Admin API", function() name = "my-api", request_host = "my.api.com", upstream_url = "http://my-api.com", - created_at = 1461276890000 + created_at = 1461276890000, + retries = 0, }, headers = {["Content-Type"] = content_type} }) @@ -148,6 +150,7 @@ describe("Admin API", function() assert.is_nil(json.request_path) assert.False(json.preserve_host) assert.False(json.strip_request_path) + assert.equals(0, json.retries) end end) it_content_types("replaces if exists", function(content_type) @@ -173,7 +176,8 @@ describe("Admin API", function() name = "my-new-api", request_host = "my-new-api.com", upstream_url = "http://my-api.com", - created_at = json.created_at + created_at = json.created_at, + retries = 99, }, headers = {["Content-Type"] = content_type} }) @@ -184,6 +188,7 @@ describe("Admin API", function() assert.equal(json.upstream_url, updated_json.upstream_url) assert.equal(json.id, updated_json.id) assert.equal(json.created_at, updated_json.created_at) + assert.equal(99, updated_json.retries) end end) describe("errors", function() diff --git a/spec/02-integration/04-core/02-hooks_spec.lua b/spec/02-integration/04-core/02-hooks_spec.lua index 6190f71ab65..50c16928577 100644 --- a/spec/02-integration/04-core/02-hooks_spec.lua +++ b/spec/02-integration/04-core/02-hooks_spec.lua @@ -852,7 +852,7 @@ describe("Core Hooks", function() os.execute(string.format("kill `cat %s` >/dev/null 2>&1", PID_FILE)) end - it("should syncronize nodes on members events", function() + it("should synchronize nodes on members events", function() start_serf() -- Tell Kong to join the new Serf diff --git a/spec/02-integration/04-core/03-dns_spec.lua b/spec/02-integration/04-core/03-dns_spec.lua new file mode 100644 index 00000000000..a6dfc7a69ba --- /dev/null +++ b/spec/02-integration/04-core/03-dns_spec.lua @@ -0,0 +1,81 @@ +local helpers = require "spec.helpers" + +local TCP_PORT = 35000 + +-- @param port the port to listen on +-- @param duration the duration for which to listen and accept connections (seconds) +local function bad_tcp_server(port, duration, ...) + local threads = require "llthreads2.ex" + local thread = threads.new({ + function(port, duration) + local socket = require "socket" + local expire = socket.gettime() + duration + local server = assert(socket.tcp()) + local tries = 0 + server:settimeout(0.1) + assert(server:setoption('reuseaddr', true)) + assert(server:bind("*", port)) + assert(server:listen()) + while socket.gettime() < expire do + local client, err = server:accept() + socket.sleep(0.1) + if client then + client:close() -- we're behaving bad, do nothing, just close + tries = tries + 1 + elseif err ~= "timeout" then + return nil, "error accepting tcp connection; "..tostring(err) + end + end + server:close() + return tries + end + }, port, duration) + + return thread:start(...) +end + +describe("Core DNS", function() + describe("retries", function() + + local retries = 3 + local client + + setup(function() + assert(helpers.start_kong()) + client = helpers.proxy_client() + + assert(helpers.dao.apis:insert { + name = "tests-retries", + request_host = "retries.com", + upstream_url = "http://127.0.0.1:"..TCP_PORT, + retries = retries, + }) + end) + + teardown(function() + if client then client:close() end + helpers.stop_kong() + end) + + it("validates the number of retries", function() + -- setup a bad server + local thread = bad_tcp_server(TCP_PORT, 1) + + -- make a request to it + local r = client:send { + method = "GET", + path = "/", + headers = { + host = "retries.com" + } + } + assert.equals(502, r.status) + + -- Getting back the TCP server count of the tries + local ok, tries = thread:join() + assert.True(ok) + assert.equals(retries, tries-1 ) -- the -1 is because the initial one is not a retry. + + end) + end) +end) diff --git a/spec/fixtures/custom_nginx.template b/spec/fixtures/custom_nginx.template index d3c578c281e..9a98e678c52 100644 --- a/spec/fixtures/custom_nginx.template +++ b/spec/fixtures/custom_nginx.template @@ -9,7 +9,6 @@ error_log logs/error.log ${{LOG_LEVEL}}; events {} http { - resolver ${{DNS_RESOLVER}} ipv6=off; charset UTF-8; error_log logs/error.log ${{LOG_LEVEL}}; diff --git a/spec/fixtures/invalid.conf b/spec/fixtures/invalid.conf index 6e1423a79f9..a209fb35223 100644 --- a/spec/fixtures/invalid.conf +++ b/spec/fixtures/invalid.conf @@ -1,3 +1,3 @@ -dnsmasq = on +pg_ssl = on dns_resolver = 8.8.8.8 cassandra_repl_strategy = foo diff --git a/spec/fixtures/shm-stub.lua b/spec/fixtures/shm-stub.lua index 747a6766a40..fe309fb4c5e 100644 --- a/spec/fixtures/shm-stub.lua +++ b/spec/fixtures/shm-stub.lua @@ -45,7 +45,9 @@ function SharedDict:replace(key, value) end function SharedDict:delete(key) - self.data[key] = nil + if self.data[key] ~= nil then + self.data[key] = nil + end end function SharedDict:incr(key, value) diff --git a/spec/fixtures/to-strip.conf b/spec/fixtures/to-strip.conf index 0f1ffc97ba6..5853a7d1a6f 100644 --- a/spec/fixtures/to-strip.conf +++ b/spec/fixtures/to-strip.conf @@ -1,12 +1,9 @@ database = cassandra # strip me log_level = debug # strip this # comment too -dnsmasq = off # Toggles if Kong should start/stop dnsmasq, - # which can be used as the Nginx DNS resolver. - # Using dnsmasq allows Nginx to resolve - # domains defined in /etc/hosts. - # dnsmasq must be installed and available in - # your $PATH. +pg_ssl = off # Toggles client-server TLS connections + # between Kong and PostgreSQL. + dns_resolver = 8.8.8.8 custom_plugins = foobar,hello-world # Comma-separated list of additional plugins @@ -14,4 +11,4 @@ custom_plugins = foobar,hello-world # Comma-separated list of additional plugin # Use this property to load custom plugins # that are not bundled with Kong. # Plugins will be loaded from the - # `kong.plugins.{name}.*` namespace. + # `kong.plugins.{name}.*` namespace. \ No newline at end of file diff --git a/spec/helpers.lua b/spec/helpers.lua index 10aa2158ed3..628887269a7 100644 --- a/spec/helpers.lua +++ b/spec/helpers.lua @@ -34,6 +34,21 @@ local dao = assert(DAOFactory.new(conf)) ----------------- local resty_http_proxy_mt = {} +local pack = function(...) return { n = select("#", ...), ... } end +local unpack = function(t) return unpack(t, 1, t.n) end + +--- Prints all returned parameters. +-- Simple debugging aid. +-- @usage -- modify +-- local a,b = some_func(c,d) +-- -- into +-- local a,b = intercept(some_func(c,d)) +local function intercept(...) + local args = pack(...) + print(require("pl.pretty").write(args)) + return unpack(args) +end + -- Case insensitive lookup function, returns the value and the original key. Or -- if not found nil and the search key -- @usage -- sample usage @@ -822,6 +837,9 @@ return { proxy_ssl_client = proxy_ssl_client, prepare_prefix = prepare_prefix, clean_prefix = clean_prefix, + + -- miscellaneous + intercept = intercept, start_kong = function(env) env = env or {} @@ -851,7 +869,6 @@ return { -- kill kong_tests.conf services for _, pid_path in ipairs {running_conf.nginx_pid, - running_conf.dnsmasq_pid, running_conf.serf_pid} do if pl_path.exists(pid_path) then kill.kill(pid_path, "-TERM")