Skip to content

Commit

Permalink
feat(plugin/redirect): plugin to redirect requests to a new location (#…
Browse files Browse the repository at this point in the history
…13900)

* feat(plugin/redirect): plugin to redirect requests to a new location

* feat(plugin/redirect): remove the "merge" strategy
  • Loading branch information
mheap authored Nov 22, 2024
1 parent 7bb20ea commit fa9bed9
Show file tree
Hide file tree
Showing 11 changed files with 426 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .github/labeler.yml
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,10 @@ plugins/standard-webhooks:
- changed-files:
- any-glob-to-any-file: kong/plugins/standard-webhooks/**/*

plugins/redirect:
- changed-files:
- any-glob-to-any-file: kong/plugins/redirect/**/*

schema-change-noteworthy:
- changed-files:
- any-glob-to-any-file: [
Expand Down
4 changes: 4 additions & 0 deletions changelog/unreleased/kong/plugins-redirect.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
message: |
"**redirect**: Add a new plugin to redirect requests to another location
type: "feature"
scope: "Plugin"
3 changes: 3 additions & 0 deletions kong-3.9.0-0.rockspec
Original file line number Diff line number Diff line change
Expand Up @@ -664,6 +664,9 @@ build = {
["kong.plugins.standard-webhooks.internal"] = "kong/plugins/standard-webhooks/internal.lua",
["kong.plugins.standard-webhooks.schema"] = "kong/plugins/standard-webhooks/schema.lua",

["kong.plugins.redirect.handler"] = "kong/plugins/redirect/handler.lua",
["kong.plugins.redirect.schema"] = "kong/plugins/redirect/schema.lua",

["kong.vaults.env"] = "kong/vaults/env/init.lua",
["kong.vaults.env.schema"] = "kong/vaults/env/schema.lua",

Expand Down
1 change: 1 addition & 0 deletions kong/constants.lua
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ local plugins = {
"ai-request-transformer",
"ai-response-transformer",
"standard-webhooks",
"redirect"
}

local plugin_map = {}
Expand Down
32 changes: 32 additions & 0 deletions kong/plugins/redirect/handler.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
local kong = kong
local kong_meta = require "kong.meta"
local ada = require "resty.ada"

local RedirectHandler = {}

-- Priority 779 so that it runs after all rate limiting/validation plugins
-- and all transformation plugins, but before any AI plugins which call upstream
RedirectHandler.PRIORITY = 779
RedirectHandler.VERSION = kong_meta.version

function RedirectHandler:access(conf)
-- Use the 'location' as-is as the default
-- This is equivalent to conf.incoming_path == 'ignore'
local location = conf.location

if conf.keep_incoming_path then
-- Parse the URL in 'conf.location' and the incoming request
local location_url = ada.parse(location)

-- Overwrite the path in 'location' with the path from the incoming request
location = location_url:set_pathname(kong.request.get_path()):set_search(kong.request.get_raw_query()):get_href()
end

local headers = {
["Location"] = location
}

return kong.response.exit(conf.status_code, "redirecting", headers)
end

return RedirectHandler
39 changes: 39 additions & 0 deletions kong/plugins/redirect/schema.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
local typedefs = require "kong.db.schema.typedefs"

return {
name = "redirect",
fields = {
{
protocols = typedefs.protocols_http
},
{
config = {
type = "record",
fields = {
{
status_code = {
description = "The response code to send. Must be an integer between 100 and 599.",
type = "integer",
required = true,
default = 301,
between = { 100, 599 }
}
},
{
location = typedefs.url {
description = "The URL to redirect to",
required = true
}
},
{
keep_incoming_path = {
description = "Use the incoming request's path and query string in the redirect URL",
type = "boolean",
default = false
}
}
}
}
}
}
}
1 change: 1 addition & 0 deletions spec/01-unit/12-plugins_order_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ describe("Plugins", function()
"response-ratelimiting",
"request-transformer",
"response-transformer",
"redirect",
"ai-request-transformer",
"ai-prompt-template",
"ai-prompt-decorator",
Expand Down
99 changes: 99 additions & 0 deletions spec/03-plugins/45-redirect/01-schema_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
local PLUGIN_NAME = "redirect"
local null = ngx.null

-- helper function to validate data against a schema
local validate
do
local validate_entity = require("spec.helpers").validate_plugin_config_schema
local plugin_schema = require("kong.plugins." .. PLUGIN_NAME .. ".schema")

function validate(data)
return validate_entity(data, plugin_schema)
end
end

describe("Plugin: redirect (schema)", function()
it("should accept a valid status_code", function()
local ok, err = validate({
status_code = 404,
location = "https://example.com"
})
assert.is_nil(err)
assert.is_truthy(ok)
end)

it("should accept a valid location", function()
local ok, err = validate({
location = "https://example.com"
})
assert.is_nil(err)
assert.is_truthy(ok)
end)



describe("errors", function()
it("status_code should only accept integers", function()
local ok, err = validate({
status_code = "abcd",
location = "https://example.com"
})
assert.falsy(ok)
assert.same("expected an integer", err.config.status_code)
end)

it("status_code is not nullable", function()
local ok, err = validate({
status_code = null,
location = "https://example.com"
})
assert.falsy(ok)
assert.same("required field missing", err.config.status_code)
end)

it("status_code < 100", function()
local ok, err = validate({
status_code = 99,
location = "https://example.com"
})
assert.falsy(ok)
assert.same("value should be between 100 and 599", err.config.status_code)
end)

it("status_code > 599", function()
local ok, err = validate({
status_code = 600,
location = "https://example.com"
})
assert.falsy(ok)
assert.same("value should be between 100 and 599", err.config.status_code)
end)

it("location is required", function()
local ok, err = validate({
status_code = 301
})
assert.falsy(ok)
assert.same("required field missing", err.config.location)
end)

it("location must be a url", function()
local ok, err = validate({
status_code = 301,
location = "definitely_not_a_url"
})
assert.falsy(ok)
assert.same("missing host in url", err.config.location)
end)

it("incoming_path must be a boolean", function()
local ok, err = validate({
status_code = 301,
location = "https://example.com",
keep_incoming_path = "invalid"
})
assert.falsy(ok)
assert.same("expected a boolean", err.config.keep_incoming_path)
end)
end)
end)
148 changes: 148 additions & 0 deletions spec/03-plugins/45-redirect/02-access_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
local helpers = require "spec.helpers"

for _, strategy in helpers.each_strategy() do
describe("Plugin: redirect (access) [#" .. strategy .. "]", function()
local proxy_client
local admin_client

lazy_setup(function()
local bp = helpers.get_db_utils(strategy, { "routes", "services", "plugins" })

-- Default status code
local route1 = bp.routes:insert({
hosts = { "api1.redirect.test" }
})

bp.plugins:insert {
name = "redirect",
route = {
id = route1.id
},
config = {
location = "https://example.com"
}
}

-- Custom status code
local route2 = bp.routes:insert({
hosts = { "api2.redirect.test" }
})

bp.plugins:insert {
name = "redirect",
route = {
id = route2.id
},
config = {
status_code = 302,
location = "https://example.com"
}
}

-- config.keep_incoming_path = false
local route3 = bp.routes:insert({
hosts = { "api3.redirect.test" }
})

bp.plugins:insert {
name = "redirect",
route = {
id = route3.id
},
config = {
location = "https://example.com/path?foo=bar"
}
}

-- config.keep_incoming_path = true
local route4 = bp.routes:insert({
hosts = { "api4.redirect.test" }
})

bp.plugins:insert {
name = "redirect",
route = {
id = route4.id
},
config = {
location = "https://example.com/some_path?foo=bar",
keep_incoming_path = true
}
}

assert(helpers.start_kong({
database = strategy,
nginx_conf = "spec/fixtures/custom_nginx.template",
headers_upstream = "off"
}))
end)

lazy_teardown(function()
helpers.stop_kong()
end)

before_each(function()
proxy_client = helpers.proxy_client()
admin_client = helpers.admin_client()
end)

after_each(function()
if proxy_client then
proxy_client:close()
end
if admin_client then
admin_client:close()
end
end)

describe("status code", function()
it("default status code", function()
local res = assert(proxy_client:send {
method = "GET",
path = "/status/200",
headers = {
["Host"] = "api1.redirect.test"
}
})
assert.res_status(301, res)
end)

it("custom status code", function()
local res = assert(proxy_client:send {
method = "GET",
path = "/status/200",
headers = {
["Host"] = "api2.redirect.test"
}
})
assert.res_status(302, res)
end)
end)

describe("location header", function()
it("supports path and query params in location", function()
local res = assert(proxy_client:send {
method = "GET",
path = "/status/200",
headers = {
["Host"] = "api3.redirect.test"
}
})
local header = assert.response(res).has.header("location")
assert.equals("https://example.com/path?foo=bar", header)
end)

it("keeps the existing redirect URL", function()
local res = assert(proxy_client:send {
method = "GET",
path = "/status/200?keep=this",
headers = {
["Host"] = "api4.redirect.test"
}
})
local header = assert.response(res).has.header("location")
assert.equals("https://example.com/status/200?keep=this", header)
end)
end)
end)
end
Loading

1 comment on commit fa9bed9

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bazel Build

Docker image available kong/kong:fa9bed9f386487c152f3a0b2466a0cdca3f9af84
Artifacts available https://github.com/Kong/kong/actions/runs/11973329487

Please sign in to comment.