diff --git a/kong/db/schema/entities/routes.lua b/kong/db/schema/entities/routes.lua index 8afdd727498b..fcc182c5c4b4 100644 --- a/kong/db/schema/entities/routes.lua +++ b/kong/db/schema/entities/routes.lua @@ -104,7 +104,7 @@ return { }, }, }, { tags = typedefs.tags }, - { service = { type = "foreign", reference = "services", required = true }, }, + { service = { type = "foreign", reference = "services" }, }, }, entity_checks = { diff --git a/kong/router.lua b/kong/router.lua index be2cc5c18060..b2e44799ec8c 100644 --- a/kong/router.lua +++ b/kong/router.lua @@ -13,6 +13,7 @@ local upper = string.upper local lower = string.lower local find = string.find local sub = string.sub +local tonumber = tonumber local ipairs = ipairs local pairs = pairs local error = error @@ -67,6 +68,9 @@ local MATCH_RULES = { } +local EMPTY_T = {} + + local match_route local reduce @@ -333,7 +337,9 @@ local function marshall_route(r) route_t.upstream_url_t.scheme = protocol end - local host = service.host + local s = service or EMPTY_T + + local host = s.host if host then route_t.upstream_url_t.host = host route_t.upstream_url_t.type = hostname_type(host) @@ -342,7 +348,7 @@ local function marshall_route(r) route_t.upstream_url_t.type = hostname_type("") end - local port = service.port + local port = s.port if port then route_t.upstream_url_t.port = port @@ -356,7 +362,7 @@ local function marshall_route(r) end if route_t.type == "http" then - route_t.upstream_url_t.path = service.path or "/" + route_t.upstream_url_t.path = s.path or "/" end return route_t @@ -1129,14 +1135,13 @@ function _M.new(routes) local upstream_url_t = matched_route.upstream_url_t local matches = ctx.matches - -- Path construction if matched_route.type == "http" then -- if we do not have a path-match, then the postfix is simply the -- incoming path, without the initial slash local request_postfix = matches.uri_postfix or sub(req_uri, 2, -1) - local upstream_base = upstream_url_t.path + local upstream_base = upstream_url_t.path or "/" if matched_route.strip_uri then -- we drop the matched part, replacing it with the upstream path @@ -1229,11 +1234,24 @@ function _M.new(routes) -- debug HTTP request header logic if ngx.var.http_kong_debug then - ngx.header["Kong-Route-Id"] = match_t.route.id - ngx.header["Kong-Service-Id"] = match_t.service.id + if match_t.route then + if match_t.route.id then + ngx.header["Kong-Route-Id"] = match_t.route.id + end - if match_t.service.name then - ngx.header["Kong-Service-Name"] = match_t.service.name + if match_t.route.name then + ngx.header["Kong-Route-Name"] = match_t.route.name + end + end + + if match_t.service then + if match_t.service.id then + ngx.header["Kong-Service-Id"] = match_t.service.id + end + + if match_t.service.name then + ngx.header["Kong-Service-Name"] = match_t.service.name + end end end diff --git a/kong/runloop/handler.lua b/kong/runloop/handler.lua index 979895a92f5d..b1db3268b3e4 100644 --- a/kong/runloop/handler.lua +++ b/kong/runloop/handler.lua @@ -25,18 +25,22 @@ local ipairs = ipairs local tostring = tostring local tonumber = tonumber local sub = string.sub +local find = string.find local lower = string.lower local fmt = string.format local sort = table.sort local ngx = ngx local log = ngx.log local ngx_now = ngx.now +local re_match = ngx.re.match +local re_find = ngx.re.find local update_time = ngx.update_time local subsystem = ngx.config.subsystem local unpack = unpack local ERR = ngx.ERR +local WARN = ngx.WARN local DEBUG = ngx.DEBUG @@ -251,39 +255,13 @@ local function balancer_setup_stage1(ctx, scheme, host_type, host, port, -- hash_cookie = nil, -- if Upstream sets hash_on_cookie } - -- TODO: this is probably not optimal do - local retries = service.retries - if retries then - balancer_data.retries = retries + local s = service or EMPTY_T - else - balancer_data.retries = 5 - end - - local connect_timeout = service.connect_timeout - if connect_timeout then - balancer_data.connect_timeout = connect_timeout - - else - balancer_data.connect_timeout = 60000 - end - - local send_timeout = service.write_timeout - if send_timeout then - balancer_data.send_timeout = send_timeout - - else - balancer_data.send_timeout = 60000 - end - - local read_timeout = service.read_timeout - if read_timeout then - balancer_data.read_timeout = read_timeout - - else - balancer_data.read_timeout = 60000 - end + balancer_data.retries = s.retries or 5 + balancer_data.connect_timeout = s.connect_timeout or 60000 + balancer_data.send_timeout = s.write_timeout or 60000 + balancer_data.read_timeout = s.read_timeout or 60000 end ctx.service = service @@ -321,7 +299,7 @@ end -- in the table below the `before` and `after` is to indicate when they run: -- before or after the plugins return { - build_router = build_router, + build_router = build_router, -- exported for unit-testing purposes only _set_check_router_rebuild = _set_check_router_rebuild, @@ -656,16 +634,29 @@ return { ctx.KONG_PREREAD_START = get_now() - local api = match_t.api or EMPTY_T - local route = match_t.route or EMPTY_T - local service = match_t.service or EMPTY_T + local route = match_t.route + local service = match_t.service local upstream_url_t = match_t.upstream_url_t + if not service then + ----------------------------------------------------------------------- + -- Serviceless stream route + ----------------------------------------------------------------------- + local service_scheme = ssl_termination_ctx and "tls" or "tcp" + local service_host = var.server_addr + + match_t.upstream_scheme = service_scheme + upstream_url_t.scheme = service_scheme -- for completeness + upstream_url_t.type = utils.hostname_type(service_host) + upstream_url_t.host = service_host + upstream_url_t.port = tonumber(var.server_port, 10) + end + balancer_setup_stage1(ctx, match_t.upstream_scheme, upstream_url_t.type, upstream_url_t.host, upstream_url_t.port, - service, route, api) + service, route) end, after = function(ctx) local ok, err, errcode = balancer_setup_stage2(ctx) @@ -706,9 +697,13 @@ return { return kong.response.exit(404, { message = "no Route matched with those values" }) end - local route = match_t.route or EMPTY_T - local service = match_t.service or EMPTY_T - local upstream_url_t = match_t.upstream_url_t + local scheme = var.scheme + local host = var.host + local port = tonumber(var.server_port, 10) + + local route = match_t.route + local service = match_t.service + local upstream_url_t = match_t.upstream_url_t local realip_remote_addr = var.realip_remote_addr local forwarded_proto @@ -726,25 +721,125 @@ return { local trusted_ip = kong.ip.is_trusted(realip_remote_addr) if trusted_ip then - forwarded_proto = var.http_x_forwarded_proto or var.scheme - forwarded_host = var.http_x_forwarded_host or var.host - forwarded_port = var.http_x_forwarded_port or var.server_port + forwarded_proto = var.http_x_forwarded_proto or scheme + forwarded_host = var.http_x_forwarded_host or host + forwarded_port = var.http_x_forwarded_port or port else - forwarded_proto = var.scheme - forwarded_host = var.host - forwarded_port = var.server_port + forwarded_proto = scheme + forwarded_host = host + forwarded_port = port end local protocols = route.protocols - if (protocols and - protocols.https and not protocols.http and forwarded_proto ~= "https") + if (protocols and protocols.https and not protocols.http and + forwarded_proto ~= "https") then ngx.header["connection"] = "Upgrade" ngx.header["upgrade"] = "TLS/1.2, HTTP/1.1" return kong.response.exit(426, { message = "Please use HTTPS protocol" }) end + if not service then + ----------------------------------------------------------------------- + -- Serviceless HTTP / HTTPS / HTTP2 route + ----------------------------------------------------------------------- + local service_scheme + local service_host + local service_port + + -- 1. try to find information from a request-line + local request_line = var.request + if request_line then + local matches, err = re_match(request_line, [[\w+ (https?)://([^/?#\s]+)]], "ajos") + if err then + log(WARN, "pcre runtime error when matching a request-line: ", err) + + elseif matches then + local uri_scheme = lower(matches[1]) + if uri_scheme == "https" or uri_scheme == "http" then + service_scheme = uri_scheme + service_host = lower(matches[2]) + end + --[[ TODO: check if these make sense here? + elseif uri_scheme == "wss" then + service_scheme = "https" + service_host = lower(matches[2]) + elseif uri_scheme == "ws" then + service_scheme = "http" + service_host = lower(matches[2]) + end + --]] + end + end + + -- 2. try to find information from a host header + if not service_host then + local http_host = var.http_host + if http_host then + service_scheme = scheme + service_host = lower(http_host) + end + end + + -- 3. split host to host and port + if service_host then + -- remove possible userinfo + local pos = find(service_host, "@", 1, true) + if pos then + service_host = sub(service_host, pos + 1) + end + + pos = find(service_host, ":", 2, true) + if pos then + service_port = sub(service_host, pos + 1) + service_host = sub(service_host, 1, pos - 1) + + local found, _, err = re_find(service_port, [[[1-9]{1}\d{0,4}$]], "adjo") + if err then + log(WARN, "pcre runtime error when matching a port number: ", err) + + elseif found then + service_port = tonumber(service_port, 10) + if not service_port or service_port > 65535 then + service_scheme = nil + service_host = nil + service_port = nil + end + + else + service_scheme = nil + service_host = nil + service_port = nil + end + end + end + + -- 4. use known defaults + if service_host and not service_port then + if service_scheme == "http" then + service_port = 80 + elseif service_scheme == "https" then + service_port = 443 + else + service_port = port + end + end + + -- 5. fall-back to server address + if not service_host then + service_scheme = scheme + service_host = var.server_addr + service_port = port + end + + match_t.upstream_scheme = service_scheme + upstream_url_t.scheme = service_scheme -- for completeness + upstream_url_t.type = utils.hostname_type(service_host) + upstream_url_t.host = service_host + upstream_url_t.port = service_port + end + balancer_setup_stage1(ctx, match_t.upstream_scheme, upstream_url_t.type, upstream_url_t.host, @@ -888,9 +983,9 @@ return { local ok, err = cookie:set(hash_cookie) if not ok then - log(ngx.WARN, "failed to set the cookie for hash-based load balancing: ", err, - " (key=", hash_cookie.key, - ", path=", hash_cookie.path, ")") + log(WARN, "failed to set the cookie for hash-based load balancing: ", err, + " (key=", hash_cookie.key, + ", path=", hash_cookie.path, ")") end end, after = function(ctx) diff --git a/spec/01-unit/01-db/01-schema/06-routes_spec.lua b/spec/01-unit/01-db/01-schema/06-routes_spec.lua index 8a30c4e4121f..9db6afeeee89 100644 --- a/spec/01-unit/01-db/01-schema/06-routes_spec.lua +++ b/spec/01-unit/01-db/01-schema/06-routes_spec.lua @@ -35,12 +35,20 @@ describe("routes schema", function() assert.falsy(route.strip_path) end) - it("fails when service is null", function() + it("it does not fail when service is null", function() local route = { service = ngx.null, paths = {"/"} } route = Routes:process_auto_fields(route, "insert") local ok, errs = Routes:validate_insert(route) - assert.falsy(ok) - assert.truthy(errs["service"]) + assert.truthy(ok) + assert.is_nil(errs) + end) + + it("it does not fail when service is missing", function() + local route = { paths = {"/"} } + route = Routes:process_auto_fields(route, "insert") + local ok, errs = Routes:validate_insert(route) + assert.truthy(ok) + assert.is_nil(errs) end) it("fails when service.id is null", function() diff --git a/spec/01-unit/08-router_spec.lua b/spec/01-unit/08-router_spec.lua index 395c78b3eed7..17bb47a204ae 100644 --- a/spec/01-unit/08-router_spec.lua +++ b/spec/01-unit/08-router_spec.lua @@ -12,7 +12,7 @@ local service = { local use_case = { --- host + -- 1. host { service = service, route = { @@ -24,7 +24,7 @@ local use_case = { }, }, }, - -- method + -- 2. method { service = service, route = { @@ -33,7 +33,7 @@ local use_case = { }, } }, - -- uri + -- 3. uri { service = service, route = { @@ -42,7 +42,7 @@ local use_case = { }, } }, - -- host + uri + -- 4. host + uri { service = service, route = { @@ -57,7 +57,7 @@ local use_case = { }, }, }, - -- host + method + -- 5. host + method { service = service, route = { @@ -74,7 +74,7 @@ local use_case = { }, }, }, - -- uri + method + -- 6. uri + method { service = service, route = { @@ -88,7 +88,7 @@ local use_case = { }, } }, - -- host + uri + method + -- 7. host + uri + method { service = service, route = { @@ -108,6 +108,14 @@ local use_case = { }, }, }, + -- 8. serviceless-route + { + route = { + paths = { + "/serviceless" + }, + } + }, } describe("Router", function() @@ -240,6 +248,15 @@ describe("Router", function() assert.same(match_t.matches.uri_captures, nil) end) + it("[serviceless]", function() + local match_t = router.select("GET", "/serviceless") + assert.truthy(match_t) + assert.is_nil(match_t.service) + assert.is_nil(match_t.matches.uri_captures) + assert.same(use_case[8].route, match_t.route) + assert.same(match_t.matches.uri, use_case[8].route.paths[1]) + end) + describe("[uri prefix]", function() it("matches when given [uri] is in request URI prefix", function() -- uri prefix diff --git a/spec/02-integration/02-cmd/02-start_stop_spec.lua b/spec/02-integration/02-cmd/02-start_stop_spec.lua index ddc45c2bb80e..10bb792612e5 100644 --- a/spec/02-integration/02-cmd/02-start_stop_spec.lua +++ b/spec/02-integration/02-cmd/02-start_stop_spec.lua @@ -61,11 +61,14 @@ describe("kong start/stop #" .. strategy, function() if strategy == "cassandra" then it("start resolves cassandra contact points", function() assert(helpers.kong_exec("start", { + prefix = helpers.test_conf.prefix, database = strategy, cassandra_contact_points = "localhost", cassandra_keyspace = helpers.test_conf.cassandra_keyspace, })) - assert(helpers.kong_exec("stop")) + assert(helpers.kong_exec("stop", { + prefix = helpers.test_conf.prefix, + })) end) end @@ -356,7 +359,7 @@ describe("kong start/stop #" .. strategy, function() if strategy == "cassandra" then it("errors when cassandra contact points cannot be resolved", function() - local ok, stderr = helpers.kong_exec("start --prefix " .. helpers.test_conf.prefix, { + local ok, stderr = helpers.start_kong({ database = strategy, cassandra_contact_points = "invalid.inexistent.host", cassandra_keyspace = helpers.test_conf.cassandra_keyspace, @@ -367,6 +370,7 @@ describe("kong start/stop #" .. strategy, function() "(cassandra_contact_points = 'invalid.inexistent.host')", stderr, nil, true) finally(function() + helpers.stop_kong() helpers.kill_all() pcall(helpers.dir.rmtree) end) diff --git a/spec/02-integration/03-db/02-db_core_entities_spec.lua b/spec/02-integration/03-db/02-db_core_entities_spec.lua index d902ebd666ee..6190be1fc317 100644 --- a/spec/02-integration/03-db/02-db_core_entities_spec.lua +++ b/spec/02-integration/03-db/02-db_core_entities_spec.lua @@ -346,12 +346,10 @@ for _, strategy in helpers.each_strategy() do name = "schema violation", strategy = strategy, message = unindent([[ - 2 schema violations - (must set one of 'methods', 'hosts', 'paths' when 'protocols' is 'http' or 'https'; - service: required field missing) + schema violation + (must set one of 'methods', 'hosts', 'paths' when 'protocols' is 'http' or 'https') ]], true, true), fields = { - service = "required field missing", ["@entity"] = { "must set one of 'methods', 'hosts', 'paths' when 'protocols' is 'http' or 'https'", } @@ -529,6 +527,42 @@ for _, strategy in helpers.each_strategy() do }, route) end) + it("creates a Route without a service", function() + local route, err, err_t = db.routes:insert({ + protocols = { "http" }, + hosts = { "example.com" }, + paths = { "/example" }, + regex_priority = 3, + strip_path = true, + }, { nulls = true }) + assert.is_nil(err_t) + assert.is_nil(err) + + assert.is_table(route) + assert.is_number(route.created_at) + assert.is_number(route.updated_at) + assert.is_true(utils.is_valid_uuid(route.id)) + + assert.same({ + id = route.id, + created_at = route.created_at, + updated_at = route.updated_at, + protocols = { "http" }, + name = ngx.null, + methods = ngx.null, + hosts = { "example.com" }, + paths = { "/example" }, + snis = ngx.null, + sources = ngx.null, + destinations = ngx.null, + tags = ngx.null, + regex_priority = 3, + strip_path = true, + preserve_host = false, + service = ngx.null, + }, route) + end) + it("created_at/updated_at defaults and formats are respected", function() local now = ngx.time() @@ -1447,6 +1481,20 @@ for _, strategy in helpers.each_strategy() do assert.same(new_route.service, { id = service2.id }) end) + it(":update() detaches a Route from an existing Service", function() + local service1 = bp.services:insert({ host = "service1.com" }) + local route = bp.routes:insert({ service = service1, methods = { "GET" } }) + local new_route, err, err_t = db.routes:update({ id = route.id }, { + service = ngx.null + }) + assert.is_nil(err_t) + assert.is_nil(err) + route.service = nil + route.updated_at = nil + new_route.updated_at = nil + assert.same(route, new_route) + end) + it(":update() cannot attach a Route to a non-existing Service", function() local service = { id = utils.uuid() diff --git a/spec/02-integration/03-db/07-tags_spec.lua b/spec/02-integration/03-db/07-tags_spec.lua index 3eb15fbe8774..d4154c8dafe5 100644 --- a/spec/02-integration/03-db/07-tags_spec.lua +++ b/spec/02-integration/03-db/07-tags_spec.lua @@ -16,7 +16,7 @@ for _, strategy in helpers.each_strategy() do -- Note by default the page size is 100, we should keep this number -- less than 100/(tags_per_entity) - -- otherwise the 'limits maximum queries in single request' tests + -- otherwise the 'limits maximum queries in single request' tests -- for Cassandra might fail local test_entity_count = 10 @@ -87,9 +87,9 @@ for _, strategy in helpers.each_strategy() do -- due to the different sql in postgres stragey -- we need to test these two methods seperately local scenarios = { - { "update", { id = service1.id }, "service1", }, + { "update", { id = service1.id }, "service1", }, { "update_by_name", "service2", "service2"}, - { "upsert", { id = service3.id }, "service3" }, + { "upsert", { id = service3.id }, "service3" }, { "upsert_by_name", "service4", "service4"}, } for _, scenario in pairs(scenarios) do @@ -140,7 +140,7 @@ for _, strategy in helpers.each_strategy() do -- due to the different sql in postgres stragey -- we need to test these two methods seperately local scenarios = { - { "delete", { id = service5.id }, "service5" }, + { "delete", { id = service5.id }, "service5" }, { "delete_by_name", "service6", "service6" }, } for i, scenario in pairs(scenarios) do @@ -179,7 +179,7 @@ for _, strategy in helpers.each_strategy() do -- note this is different from test "update row in tags table with" -- as this test actually creats new records local scenarios = { - { "upsert", { id = require("kong.tools.utils").uuid() }, { "service-upsert-1" } }, + { "upsert", { id = require("kong.tools.utils").uuid() }, { "service-upsert-1" } }, { "upsert_by_name", "service-upsert-2", { "service-upsert-2" } }, } for _, scenario in pairs(scenarios) do @@ -232,7 +232,7 @@ for _, strategy in helpers.each_strategy() do { { { "team_paging_1", "team_paging_2" }, "or" }, total_entities_count/single_tag_count*2, - }, + }, { { { "paging", "team_paging_1" }, "and" }, total_entities_count/single_tag_count, @@ -297,7 +297,7 @@ for _, strategy in helpers.each_strategy() do it("and exits early if PAGING_MAX_QUERY_ROUNDS exceeded", function() stub(ngx, "log") - local rows, err, err_t, offset = db.services:page(2, nil, + local rows, err, err_t, offset = db.services:page(2, nil, { tags = { "paging", "tag_notexist" }, tags_cond = 'and' }) assert(is_valid_page(rows, err, err_t)) assert.is_not_nil(offset) @@ -325,7 +325,7 @@ for _, strategy in helpers.each_strategy() do assert.stub(ngx.log).was_not_called() end) - it("and returns as normal if page size is large enough", function() + it("#flaky and returns as normal if page size is large enough", function() stub(ngx, "log") local rows, err, err_t, offset = db.services:page(enough_page_size, nil, @@ -459,4 +459,4 @@ for _, strategy in helpers.each_strategy() do end end) end) -end \ No newline at end of file +end diff --git a/spec/02-integration/04-admin_api/09-routes_routes_spec.lua b/spec/02-integration/04-admin_api/09-routes_routes_spec.lua index 02172bc3d193..74173878cb40 100644 --- a/spec/02-integration/04-admin_api/09-routes_routes_spec.lua +++ b/spec/02-integration/04-admin_api/09-routes_routes_spec.lua @@ -78,6 +78,34 @@ for _, strategy in helpers.each_strategy() do end end) + it_content_types("creates a route without service", function(content_type) + return function() + if content_type == "multipart/form-data" then + -- the client doesn't play well with this + return + end + + local res = client:post("/routes", { + body = { + protocols = { "http" }, + hosts = { "my.route.com" }, + }, + headers = { ["Content-Type"] = content_type } + }) + local body = assert.res_status(201, res) + local json = cjson.decode(body) + assert.same({ "my.route.com" }, json.hosts) + assert.is_number(json.created_at) + assert.is_number(json.regex_priority) + assert.is_string(json.id) + assert.equals(cjson.null, json.name) + assert.equals(cjson.null, json.paths) + assert.equals(cjson.null, json.service) + assert.False(json.preserve_host) + assert.True(json.strip_path) + end + end) + it_content_types("creates a complex route", function(content_type) return function() if content_type == "multipart/form-data" then @@ -138,12 +166,10 @@ for _, strategy in helpers.each_strategy() do code = Errors.codes.SCHEMA_VIOLATION, name = "schema violation", message = unindent([[ - 2 schema violations - (must set one of 'methods', 'hosts', 'paths' when 'protocols' is 'http' or 'https'; - service: required field missing) + schema violation + (must set one of 'methods', 'hosts', 'paths' when 'protocols' is 'http' or 'https') ]], true, true), fields = { - service = "required field missing", ["@entity"] = { "must set one of 'methods', 'hosts', 'paths' when 'protocols' is 'http' or 'https'" } @@ -162,12 +188,10 @@ for _, strategy in helpers.each_strategy() do assert.same({ code = Errors.codes.SCHEMA_VIOLATION, name = "schema violation", - message = "2 schema violations " .. - "(protocols.1: expected one of: http, https, tcp, tls; " .. - "service: required field missing)", + message = "schema violation " .. + "(protocols.1: expected one of: http, https, tcp, tls)", fields = { protocols = { "expected one of: http, https, tcp, tls" }, - service = "required field missing", } }, cjson.decode(body)) end @@ -410,6 +434,35 @@ for _, strategy in helpers.each_strategy() do end end) + it_content_types("creates without service if not found", function(content_type) + return function() + if content_type == "multipart/form-data" then + -- the client doesn't play well with this + return + end + + local id = utils.uuid() + local res = client:put("/routes/" .. id, { + headers = { + ["Content-Type"] = content_type + }, + body = { + paths = { "/updated-paths" }, + }, + }) + local body = assert.res_status(200, res) + local json = cjson.decode(body) + assert.same({ "/updated-paths" }, json.paths) + assert.same(cjson.null, json.hosts) + assert.same(cjson.null, json.methods) + assert.same(cjson.null, json.service) + assert.equal(id, json.id) + + local in_db = assert(db.routes:select({ id = id }, { nulls = true })) + assert.same(json, in_db) + end + end) + it_content_types("creates if not found by name", function(content_type) return function() if content_type == "multipart/form-data" then @@ -533,12 +586,10 @@ for _, strategy in helpers.each_strategy() do code = Errors.codes.SCHEMA_VIOLATION, name = "schema violation", message = unindent([[ - 2 schema violations - (must set one of 'methods', 'hosts', 'paths' when 'protocols' is 'http' or 'https'; - service: required field missing) + schema violation + (must set one of 'methods', 'hosts', 'paths' when 'protocols' is 'http' or 'https') ]], true, true), fields = { - service = "required field missing", ["@entity"] = { "must set one of 'methods', 'hosts', 'paths' when 'protocols' is 'http' or 'https'" } @@ -557,12 +608,10 @@ for _, strategy in helpers.each_strategy() do assert.same({ code = Errors.codes.SCHEMA_VIOLATION, name = "schema violation", - message = "2 schema violations " .. - "(protocols.1: expected one of: http, https, tcp, tls; " .. - "service: required field missing)", + message = "schema violation " .. + "(protocols.1: expected one of: http, https, tcp, tls)", fields = { protocols = { "expected one of: http, https, tcp, tls" }, - service = "required field missing", } }, cjson.decode(body)) @@ -762,6 +811,43 @@ for _, strategy in helpers.each_strategy() do assert.equal(route.id, json.id) end) + it_content_types("removes service association", function(content_type) + return function() + if content_type == "multipart/form-data" then + -- the client doesn't play well with this + return + end + + local route = bp.routes:insert({ + name = "my-patch-route", + paths = { "/my-route" }, + }) + local res = client:patch("/routes/my-patch-route", { + headers = { + ["Content-Type"] = content_type + }, + body = { + methods = cjson.null, + hosts = cjson.null, + service = cjson.null, + paths = { "/updated-paths" }, + }, + }) + local body = assert.res_status(200, res) + local json = cjson.decode(body) + assert.same({ "/updated-paths" }, json.paths) + assert.same(cjson.null, json.hosts) + assert.same(cjson.null, json.methods) + assert.same(cjson.null, json.service) + assert.equal(route.id, json.id) + + local in_db = assert(db.routes:select({ id = route.id }, { nulls = true })) + assert.same(json, in_db) + + db.routes:delete({ id = route.id }) + end + end) + describe("errors", function() it_content_types("returns 404 if not found", function(content_type) return function() diff --git a/spec/02-integration/05-proxy/18-serviceless_proxying_spec.lua b/spec/02-integration/05-proxy/18-serviceless_proxying_spec.lua new file mode 100644 index 000000000000..7b19cd88cb3a --- /dev/null +++ b/spec/02-integration/05-proxy/18-serviceless_proxying_spec.lua @@ -0,0 +1,386 @@ +local http_request = require "http.request" +local http_tls = require "http.tls" +local openssl_ssl = require "openssl.ssl" +local helpers = require "spec.helpers" +local cjson = require "cjson" +local meta = require "kong.meta" + + +local tonumber = tonumber +local null = ngx.null + + +local HTTP_PROXY_HOST = helpers.get_proxy_ip(false) +local HTTP_PROXY_PORT = helpers.get_proxy_port(false) +local HTTPS_PROXY_HOST = helpers.get_proxy_ip(true) +local HTTPS_PROXY_PORT = helpers.get_proxy_port(true) +local HTTP_PROXY_URI = "http://" .. HTTP_PROXY_HOST .. ":" .. HTTP_PROXY_PORT +--local HTTPS_PROXY_URI = "https://" .. HTTPS_PROXY_HOST .. ":" .. HTTPS_PROXY_PORT +local HTTP_UPSTREAM_URI = helpers.mock_upstream_url .. "/anything" +local HTTPS_UPSTREAM_URI = helpers.mock_upstream_ssl_url .. "/anything" +local HTTP_UPSTREAM_HOST = helpers.mock_upstream_host .. ":" .. helpers.mock_upstream_port +local HTTPS_UPSTREAM_HOST = helpers.mock_upstream_host .. ":" .. helpers.mock_upstream_ssl_port +local STREAM_UPSTREAM_HOST = helpers.mock_upstream_host +local STREAM_UPSTREAM_PORT = helpers.mock_upstream_stream_port +local STREAM_UPSTREAM_SSL_PORT = helpers.mock_upstream_stream_ssl_port + + +for _, strategy in helpers.each_strategy() do + describe("Serviceless Proxying [#" .. strategy .. "]", function() + describe("[http]", function() + lazy_setup(function() + local bp = helpers.get_db_utils(strategy, { + "routes", + }) + + bp.routes:insert { + paths = { "/" }, + service = null, + } + + assert(helpers.start_kong({ + database = strategy, + nginx_conf = "spec/fixtures/custom_nginx.template", + stream_listen = "off", + admin_listen = "off", + })) + end) + + lazy_teardown(function() + helpers.stop_kong(helpers.test_conf.prefix, true) + end) + + it("proxies http to http (request-line)", function() + local request = http_request.new_from_uri(HTTP_UPSTREAM_URI) + request.proxy = HTTP_PROXY_URI + request.version = 1.1 + request.tls = false + local headers, stream = request:go() + + assert.equal(200, tonumber((headers:get(":status")))) + assert.equal(meta._SERVER_TOKENS, (headers:get("via"))) + + local body = assert(stream:get_body_as_string()) + local json = cjson.decode(body) + + assert.equal(HTTP_PROXY_PORT, tonumber(json.headers["x-forwarded-port"])) + assert.equal("http", json.headers["x-forwarded-proto"]) + + stream:shutdown() + end) + + it("proxies https to http (request-line)", function() + local ctx = http_tls.new_client_context() + ctx:setVerify(openssl_ssl.VERIFY_NONE) + + local request = http_request.new_from_uri(HTTP_UPSTREAM_URI) + request.headers:upsert(":path", request:to_uri(false)) + request.host = HTTPS_PROXY_HOST + request.port = HTTPS_PROXY_PORT + request.version = 1.1 + request.tls = true + request.ctx = ctx + local headers, stream = request:go() + + assert.equal(200, tonumber((headers:get(":status")))) + assert.equal(meta._SERVER_TOKENS, (headers:get("via"))) + + local body = assert(stream:get_body_as_string()) + local json = cjson.decode(body) + + assert.equal(HTTP_UPSTREAM_HOST, json.headers["host"]) + assert.equal(HTTPS_PROXY_PORT, tonumber(json.headers["x-forwarded-port"])) + assert.equal("https", json.headers["x-forwarded-proto"]) + + stream:shutdown() + end) + + it("proxies http to http (host-header)", function() + local request = http_request.new_from_uri(HTTP_UPSTREAM_URI) + request.headers:upsert(":path", "/anything") + request.headers:upsert("host", HTTP_UPSTREAM_HOST) + request.host = HTTP_PROXY_HOST + request.port = HTTP_PROXY_PORT + request.version = 1.1 + request.tls = false + local headers, stream = request:go() + + assert.equal(200, tonumber((headers:get(":status")))) + assert.equal(meta._SERVER_TOKENS, (headers:get("via"))) + + local body = assert(stream:get_body_as_string()) + local json = cjson.decode(body) + + assert.equal(HTTP_UPSTREAM_HOST, json.headers["host"]) + assert.equal(HTTP_PROXY_PORT, tonumber(json.headers["x-forwarded-port"])) + assert.equal("http", json.headers["x-forwarded-proto"]) + + stream:shutdown() + end) + + -- TODO: needs https://github.com/chobits/ngx_http_proxy_connect_module + pending("proxies http to https (connect)", function() + local ctx = http_tls.new_client_context() + ctx:setVerify(openssl_ssl.VERIFY_NONE) + + local request = http_request.new_from_uri(HTTPS_UPSTREAM_URI) + request.proxy = HTTP_PROXY_URI + request.version = 1.1 + request.tls = true + request.ctx = ctx + local headers, stream = request:go() + + assert.equal(200, tonumber((headers:get(":status")))) + + local body = assert(stream:get_body_as_string()) + local json = cjson.decode(body) + + assert.equal(HTTPS_UPSTREAM_HOST, json.headers["host"]) + + stream:shutdown() + end) + + -- TODO: needs https://github.com/chobits/ngx_http_proxy_connect_module + pending("proxies https to https (connect)", function() + end) + + -- TODO: transparent needs iptables / pf to work on travis + pending("proxies http to http (transparent)", function() + end) + + -- TODO: transparent needs iptables / pf to work on travis + pending("proxies https to http (transparent)", function() + end) + + -- TODO: transparent needs iptables / pf to work on travis + pending("proxies http to https (transparent)", function() + end) + + -- TODO: transparent needs iptables / pf to work on travis + pending("proxies https to https (transparent)", function() + end) + end) + + describe("[http2]", function() + lazy_setup(function() + local bp = helpers.get_db_utils(strategy, { + "routes", + }) + + bp.routes:insert { + paths = { "/" }, + service = null, + } + + assert(helpers.start_kong({ + proxy_listen = HTTP_PROXY_HOST .. ":" .. HTTP_PROXY_PORT .. " http2, " .. + HTTPS_PROXY_HOST .. ":" .. HTTPS_PROXY_PORT .. " http2 ssl", + database = strategy, + nginx_conf = "spec/fixtures/custom_nginx.template", + stream_listen = "off", + admin_listen = "off", + })) + + end) + + lazy_teardown(function() + helpers.stop_kong(helpers.test_conf.prefix, true) + end) + + -- TODO: nginx doesn't allow absolute uris in http2 path component + pending("proxies http to http (request-line)", function() + local request = http_request.new_from_uri(HTTP_UPSTREAM_URI) + request.proxy = HTTP_PROXY_URI + request.version = 2.0 + request.tls = false + local headers, stream = request:go() + + assert.equal(200, tonumber((headers:get(":status")))) + assert.equal(meta._SERVER_TOKENS, (headers:get("via"))) + + local body = assert(stream:get_body_as_string()) + local json = cjson.decode(body) + + assert.equal(HTTP_UPSTREAM_HOST, json.headers["host"]) + assert.equal(HTTP_PROXY_PORT, tonumber(json.headers["x-forwarded-port"])) + assert.equal("http", json.headers["x-forwarded-proto"]) + + stream:shutdown() + end) + + -- TODO: nginx doesn't allow absolute uris in http2 path component + pending("proxies https to http (request-line)", function() + local ctx = http_tls.new_client_context() + ctx:setVerify(openssl_ssl.VERIFY_NONE) + + local request = http_request.new_from_uri(HTTP_UPSTREAM_URI) + request.headers:upsert(":path", request:to_uri(false)) + request.host = helpers.get_proxy_ip(true) + request.port = helpers.get_proxy_port(true) + request.version = 2.0 + request.tls = true + request.ctx = ctx + local headers, stream = request:go() + + assert.equal(200, tonumber((headers:get(":status")))) + assert.equal(meta._SERVER_TOKENS, (headers:get("via"))) + + local body = assert(stream:get_body_as_string()) + local json = cjson.decode(body) + + assert.equal(HTTP_UPSTREAM_HOST, json.headers["host"]) + assert.equal(HTTPS_PROXY_PORT, tonumber(json.headers["x-forwarded-port"])) + assert.equal("https", json.headers["x-forwarded-proto"]) + + stream:shutdown() + end) + + it("proxies http to http (host-header)", function() + local request = http_request.new_from_uri(HTTP_UPSTREAM_URI) + request.headers:upsert(":path", "/anything") + request.headers:upsert("host", HTTP_UPSTREAM_HOST) + request.host = HTTP_PROXY_HOST + request.port = HTTP_PROXY_PORT + request.version = 2.0 + request.tls = false + local headers, stream = request:go() + + assert.equal(200, tonumber((headers:get(":status")))) + assert.equal(meta._SERVER_TOKENS, (headers:get("via"))) + + local body = assert(stream:get_body_as_string()) + local json = cjson.decode(body) + + assert.equal(HTTP_UPSTREAM_HOST, json.headers["host"]) + assert.equal(HTTP_PROXY_PORT, tonumber(json.headers["x-forwarded-port"])) + assert.equal("http", json.headers["x-forwarded-proto"]) + + stream:shutdown() + end) + + -- TODO: needs https://github.com/chobits/ngx_http_proxy_connect_module + pending("proxies http to https (connect)", function() + local ctx = http_tls.new_client_context() + ctx:setVerify(openssl_ssl.VERIFY_NONE) + + local request = http_request.new_from_uri(HTTPS_UPSTREAM_URI) + request.headers:upsert(":path", "/anything") + request.headers:upsert(":authority", HTTPS_UPSTREAM_HOST) + request.host = HTTP_PROXY_HOST + request.port = HTTP_PROXY_PORT + request.version = 2.0 + request.tls = true + request.ctx = ctx + local headers, stream = request:go() + + print(stream) + assert.equal(200, tonumber((headers:get(":status")))) + + local body = assert(stream:get_body_as_string()) + local json = cjson.decode(body) + + assert.equal(HTTPS_UPSTREAM_HOST, json.headers["host"]) + + stream:shutdown() + end) + + -- TODO: needs https://github.com/chobits/ngx_http_proxy_connect_module + pending("proxies https to https (connect)", function() + end) + + -- TODO: transparent needs iptables / pf to work on travis + pending("proxies http to http (transparent)", function() + end) + + -- TODO: transparent needs iptables / pf to work on travis + pending("proxies https to http (transparent)", function() + end) + + -- TODO: transparent needs iptables / pf to work on travis + pending("proxies http to https (transparent)", function() + end) + + -- TODO: transparent needs iptables / pf to work on travis + pending("proxies https to https (transparent)", function() + end) + end) + + describe("[stream]", function() + local MESSAGE = "echo, ping, pong. echo, ping, pong. echo, ping, pong.\n" + + lazy_setup(function() + local bp = helpers.get_db_utils(strategy, { + "routes", + }) + + assert(bp.routes:insert { + destinations = { + { port = 19000, }, + { port = 19443, }, + }, + protocols = { + "tcp", + "tls", + }, + service = null, + }) + + assert(helpers.start_kong({ + stream_listen = HTTP_PROXY_HOST .. ":19000," .. + HTTPS_PROXY_HOST .. ":19443", + database = strategy, + nginx_conf = "spec/fixtures/custom_nginx.template", + proxy_listen = "off", + admin_listen = "off", + origins = "tcp://127.0.0.1:19000=" .. + "tcp://" .. STREAM_UPSTREAM_HOST .. ":" .. STREAM_UPSTREAM_PORT .. "," .. + "tls://127.0.0.1:19443=" .. + "tls://" .. STREAM_UPSTREAM_HOST .. ":" .. STREAM_UPSTREAM_SSL_PORT + })) + end) + + lazy_teardown(function() + helpers.stop_kong(helpers.test_conf.prefix, true) + end) + + it("proxies tcp to tcp (origins)", function() + local tcp = require "socket".tcp() + assert(tcp:connect(HTTP_PROXY_HOST, 19000)) + + -- TODO: we need to get rid of the next line! + assert(tcp:send(MESSAGE)) + + local body = assert(tcp:receive("*a")) + assert.equal(MESSAGE, body) + + tcp:close() + end) + + it("proxies tls to tls (origins)", function() + local tcp = require "socket".tcp() + local ssl = require("ssl") + + assert(tcp:connect(HTTPS_PROXY_HOST, 19443)) + + tcp = ssl.wrap(tcp, { + mode = "client", + verify = "none", + protocol = "any", + }) + + -- TODO: should SNI really be mandatory? + tcp:sni( "this-is-needed.org") + + assert(tcp:dohandshake()) + + -- TODO: we need to get rid of the next line! + assert(tcp:send(MESSAGE)) + + local body = assert(tcp:receive("*a")) + assert.equal(MESSAGE, body) + + tcp:close() + end) + end) + end) +end diff --git a/spec/03-plugins/22-aws-lambda/01-access_spec.lua b/spec/03-plugins/22-aws-lambda/01-access_spec.lua index a419a7d0b1b7..b9b7cbb88304 100644 --- a/spec/03-plugins/22-aws-lambda/01-access_spec.lua +++ b/spec/03-plugins/22-aws-lambda/01-access_spec.lua @@ -102,6 +102,12 @@ for _, strategy in helpers.each_strategy() do service = service12, } + local route14 = bp.routes:insert { + hosts = { "lambda14.com" }, + protocols = { "http", "https" }, + service = ngx.null, + } + bp.plugins:insert { name = "aws-lambda", route = { id = route1.id }, @@ -275,6 +281,18 @@ for _, strategy in helpers.each_strategy() do } } + bp.plugins:insert { + name = "aws-lambda", + route = { id = route14.id }, + config = { + port = 10001, + aws_key = "mock-key", + aws_secret = "mock-secret", + aws_region = "us-east-1", + function_name = "kongLambdaTest", + }, + } + assert(helpers.start_kong{ database = strategy, nginx_conf = "spec/fixtures/custom_nginx.template", @@ -771,6 +789,21 @@ for _, strategy in helpers.each_strategy() do local b = assert.response(res).has.jsonbody() assert.equal("Bad Gateway", b.message) end) + + it("invokes a Lambda function with GET using serviceless route", function() + local res = assert(proxy_client:send { + method = "GET", + path = "/get?key1=some_value1&key2=some_value2&key3=some_value3", + headers = { + ["Host"] = "lambda14.com" + } + }) + assert.res_status(200, res) + local body = assert.response(res).has.jsonbody() + assert.is_string(res.headers["x-amzn-RequestId"]) + assert.equal("some_value1", body.key1) + assert.is_nil(res.headers["X-Amz-Function-Error"]) + end) end) end) end diff --git a/spec/03-plugins/26-request-termination/02-access_spec.lua b/spec/03-plugins/26-request-termination/02-access_spec.lua index 9c752f983531..1c7f0454b6f9 100644 --- a/spec/03-plugins/26-request-termination/02-access_spec.lua +++ b/spec/03-plugins/26-request-termination/02-access_spec.lua @@ -12,7 +12,7 @@ for _, strategy in helpers.each_strategy() do local admin_client lazy_setup(function() - local bp = helpers.get_db_utils(strategy, { + local bp, db = helpers.get_db_utils(strategy, { "routes", "services", "plugins", @@ -42,41 +42,45 @@ for _, strategy in helpers.each_strategy() do hosts = { "api6.request-termination.com" }, }) + local route7 = db.routes:insert({ + hosts = { "api7.request-termination.com" }, + }) + bp.plugins:insert { - name = "request-termination", - route = { id = route1.id }, - config = {}, + name = "request-termination", + route = { id = route1.id }, + config = {}, } bp.plugins:insert { - name = "request-termination", - route = { id = route2.id }, - config = { + name = "request-termination", + route = { id = route2.id }, + config = { status_code = 404, }, } bp.plugins:insert { - name = "request-termination", - route = { id = route3.id }, - config = { + name = "request-termination", + route = { id = route3.id }, + config = { status_code = 406, message = "Invalid", }, } bp.plugins:insert { - name = "request-termination", - route = { id = route4.id }, - config = { + name = "request-termination", + route = { id = route4.id }, + config = { body = "