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
  • Loading branch information
mheap committed Nov 20, 2024
1 parent 05f3136 commit 90527ee
Show file tree
Hide file tree
Showing 11 changed files with 547 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 @@ -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"
}

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

0 comments on commit 90527ee

Please sign in to comment.