Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 2 commits into from
Nov 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -648,6 +648,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"
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
"redirect"
"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
Loading