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 1 commit
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
73 changes: 73 additions & 0 deletions kong/plugins/redirect/handler.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
local kong = kong
local kong_meta = require "kong.meta"
local socket_url = require "socket.url"

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.incoming_path ~= "ignore" then
-- Parse the URL in 'conf.location' and the incoming request
local request_path = kong.request.get_path_with_query()
local request_path_url = socket_url.parse(request_path)
local location_path_url = socket_url.parse(location)

-- The path + query are different depending on the 'incoming_path' configuration
local path = ""
local query = ""

-- If incoming_path == 'keep', use the incoming request query
if conf.incoming_path == "keep" then
query = request_path_url.query
path = request_path_url.path;
end

-- If it's 'merge', merge the incoming request path+query with the location path+query
if conf.incoming_path == "merge" then
-- Build the path
path = location_path_url.path .. "/" .. request_path_url.path

-- Build a table containing all 'location' and 'request' query parameters
-- Overwrite the 'location' query parameters with the 'request' query parameters
local request_path_kv = {}
for k, v in string.gmatch(request_path_url.query, "([^&=]+)=([^&=]+)") do
request_path_kv[k] = v
end
for k, v in string.gmatch(location_path_url.query, "([^&=]+)=([^&=]+)") do
request_path_kv[k] = v
end

-- Rebuild the query string from the new table
for k, v in pairs(request_path_kv) do
query = query .. k .. "=" .. v .. "&"
end

-- Trim last &
query = query:sub(1, -2)
end

-- Build the URL with the information
location = socket_url.build({
scheme = location_path_url.scheme,
host = location_path_url.host,
port = location_path_url.port,
path = path,
query = query
})
end

local headers = {
["Location"] = location
}
return kong.response.exit(conf.status_code, "redirecting", headers)
end

return RedirectHandler
44 changes: 44 additions & 0 deletions kong/plugins/redirect/schema.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
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 }
}
},
{
-- This is intentionally flexible and does not require a http/https prefix in order to support
-- redirecting to uris such as someapp://path
location = {
description = "The URL to redirect to",
type = "string",
required = true
}
},
{
incoming_path = {
description =
"What to do with the incoming path. 'ignore' will use the path from the 'location' field, 'keep' will keep the incoming path, 'merge' will merge the incoming path with the location path, choosing the location query parameters over the incoming one.",
type = "string",
default = "ignore",
one_of = { "ignore", "keep", "merge" }
}
}
}
}
}
}
}
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
118 changes: 118 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,118 @@
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)

it("incoming_path can be 'ignore'", function()
local ok, err = validate({
status_code = 301,
location = "https://example.com",
incoming_path = "ignore"
})
assert.is_nil(err)
assert.is_truthy(ok)
end)

it("incoming_path can be 'keep'", function()
local ok, err = validate({
status_code = 301,
location = "https://example.com",
incoming_path = "keep"
})
assert.is_nil(err)
assert.is_truthy(ok)
end)

it("incoming_path can be 'merge'", function()
local ok, err = validate({
status_code = 301,
location = "https://example.com",
incoming_path = "merge"
})
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("incoming_path must be a one_of value", function()
local ok, err = validate({
status_code = 301,
location = "https://example.com",
incoming_path = "invalid"
})
assert.falsy(ok)
assert.same("expected one of: ignore, keep, merge", err.config.incoming_path)
end)
end)
end)
Loading
Loading