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

Introduce URL rewriting policy [THREESCALE-296] #529

Merged
merged 4 commits into from
Dec 12, 2017
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
## Added

- Definition of JSON schemas for policy configurations [PR #522](https://github.com/3scale/apicast/pull/522)
- URL rewriting policy [PR #529](https://github.com/3scale/apicast/pull/529)

## Fixed

Expand Down
75 changes: 75 additions & 0 deletions gateway/src/apicast/policy/url_rewriting/policy.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
--- URL rewriting policy
-- This policy allows to modify the path of a request.

local ipairs = ipairs
local sub = ngx.re.sub
local gsub = ngx.re.gsub

local policy = require('apicast.policy')
local _M = policy.new('URL rewriting policy')

local new = _M.new

local substitute_functions = { sub = sub, gsub = gsub }

-- func needs to be ngx.re.sub or ngx.re.gsub.
-- This method simply calls one of those 2. They have the same interface.
local function substitute(func, subject, regex, replace, options)
local new_uri, num_changes, err = func(subject, regex, replace, options)

if not new_uri then
ngx.log(ngx.WARN, 'There was an error applying the regex: ', err)
end

return new_uri, num_changes > 0
end

-- Returns true when the URL was rewritten and false otherwise
local function apply_rewrite_command(command)
local func = substitute_functions[command.op]

if not func then
ngx.log(ngx.WARN, "Unknown URL rewrite operation: ", command.op)
end

local new_uri, changed = substitute(
func, ngx.var.uri, command.regex, command.replace, command.options)

if changed then
ngx.req.set_uri(new_uri)
end

return changed
end

--- Initialize a URL rewriting policy
-- @tparam[opt] table config Contains the rewrite commands.
-- The rewrite commands are based on the 'ngx.re.sub' and 'ngx.re.gsub'
-- functions provided by OpenResty. Please check
-- https://github.com/openresty/lua-nginx-module for more details.
-- Each rewrite command is a table with the following fields:
--
-- - op: can be 'sub' or 'gsub'.
-- - regex: regular expression to be matched.
-- - replace: string that will replace whatever is matched by the regex.
-- - options[opt]: options to control how the regex match will be done.
-- Accepted options are the ones in 'ngx.re.sub' and 'ngx.re.gsub'.
-- - break[opt]: defaults to false. When set to true, if the command rewrote
Copy link
Contributor

Choose a reason for hiding this comment

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

This is not recognized by the ldoc and looks kind of ugly in the generated doc. Not sure how better describe it though.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed :)

-- the URL, it will be the last command applied.
function _M.new(config)
local self = new()
self.config = config or {}
return self
end

function _M:rewrite()
for _, command in ipairs(self.config) do
local rewritten = apply_rewrite_command(command)

if rewritten and command['break'] then
break
end
end
end

return _M
32 changes: 32 additions & 0 deletions gateway/src/apicast/policy/url_rewriting/schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "URL rewriting policy configuration",
"type": "object",
"properties": {
"commands": {
"type": "array",
"items": {
"type": "object",
"properties": {
"op": {
"type": "string",
"enum": ["sub", "gsub"]
},
"regex": {
"type": "string"
},
"replace": {
"type": "string"
},
"options": {
"type": "string"
},
"break": {
Copy link
Contributor

Choose a reason for hiding this comment

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

We are considering also other mutually exclusive values like: last, redirect, persistent right?

IMO might be nicer to model it in a way they are truly mutually exclusive by being a value of one key: "type": "break".

But since we are not releasing this yet, It could wait until we have some more options.

"type": "boolean"
}
},
"required": ["op", "regex", "replace"]
}
}
}
}
236 changes: 236 additions & 0 deletions t/apicast-policy-url-rewriting.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
use lib 't';
use TestAPIcastBlackbox 'no_plan';

repeat_each(2);
run_tests();

__DATA__

=== TEST 1: sub operation
--- backend
location /transactions/authrep.xml {
content_by_lua_block {
local expected = "service_token=token-value&service_id=42&usage%5Bhits%5D=2&user_key=value"
local args = ngx.var.args
if args == expected then
ngx.exit(200)
else
ngx.log(ngx.ERR, expected, ' did not match: ', args)
ngx.exit(403)
end
}
}
--- configuration
{
"services": [
{
"id": 42,
"backend_version": 1,
"backend_authentication_type": "service_token",
"backend_authentication_value": "token-value",
"proxy": {
"api_backend": "http://test:$TEST_NGINX_SERVER_PORT/",
"proxy_rules": [
{ "pattern": "/", "http_method": "GET", "metric_system_name": "hits", "delta": 2 }
],
"policy_chain": [
{ "name": "apicast.policy.apicast" },
{
"name": "apicast.policy.url_rewriting",
"configuration":
[
{ "op": "sub", "regex": "original", "replace": "new" }
]
}
]
}
}
]
}
--- upstream
location ~ /xxx_new_yyy$ {
content_by_lua_block {
ngx.say('yay, api backend');
}
}
--- request
GET /xxx_original_yyy?user_key=value
--- response_body
yay, api backend
--- error_code: 200
--- no_error_log
[error]

=== TEST 2: gsub operation
--- backend
location /transactions/authrep.xml {
content_by_lua_block {
local expected = "service_token=token-value&service_id=42&usage%5Bhits%5D=2&user_key=value"
local args = ngx.var.args
if args == expected then
ngx.exit(200)
else
ngx.log(ngx.ERR, expected, ' did not match: ', args)
ngx.exit(403)
end
}
}
--- configuration
{
"services": [
{
"id": 42,
"backend_version": 1,
"backend_authentication_type": "service_token",
"backend_authentication_value": "token-value",
"proxy": {
"api_backend": "http://test:$TEST_NGINX_SERVER_PORT/",
"proxy_rules": [
{ "pattern": "/", "http_method": "GET", "metric_system_name": "hits", "delta": 2 }
],
"policy_chain": [
{ "name": "apicast.policy.apicast" },
{
"name": "apicast.policy.url_rewriting",
"configuration":
[
{ "op": "gsub", "regex": "original", "replace": "new" }
]
}
]
}
}
]
}
--- upstream
location ~ /aaa_new_bbb_new_ccc$ {
content_by_lua_block {
ngx.say('yay, api backend');
}
}
--- request
GET /aaa_original_bbb_original_ccc?user_key=value
--- response_body
yay, api backend
--- error_code: 200
--- no_error_log
[error]

=== TEST 3: ordered commands
Substitutions are applied in the order specified.
--- backend
location /transactions/authrep.xml {
content_by_lua_block {
local expected = "service_token=token-value&service_id=42&usage%5Bhits%5D=2&user_key=value"
local args = ngx.var.args
if args == expected then
ngx.exit(200)
else
ngx.log(ngx.ERR, expected, ' did not match: ', args)
ngx.exit(403)
end
}
}
--- configuration
{
"services": [
{
"id": 42,
"backend_version": 1,
"backend_authentication_type": "service_token",
"backend_authentication_value": "token-value",
"proxy": {
"api_backend": "http://test:$TEST_NGINX_SERVER_PORT/",
"proxy_rules": [
{ "pattern": "/", "http_method": "GET", "metric_system_name": "hits", "delta": 2 }
],
"policy_chain": [
{ "name": "apicast.policy.apicast" },
{
"name": "apicast.policy.url_rewriting",
"configuration":
[
{ "op": "gsub", "regex": "aaa", "replace": "bbb", "options": "i" },
{ "op": "sub", "regex": "bbb", "replace": "ccc" },
{ "op": "sub", "regex": "ccc", "replace": "ddd" }
]
}
]
}
}
]
}
--- upstream
location ~ /ddd_bbb$ {
content_by_lua_block {
ngx.say('yay, api backend');
}
}
--- request
GET /aaa_aaa?user_key=value
--- response_body
yay, api backend
--- error_code: 200
--- no_error_log
[error]

=== TEST 4: break
We need to test 2 things:
1) A break is only applied when the URL is rewritten.
2) When break is specified in a command, it will be the last one applied.
--- backend
location /transactions/authrep.xml {
content_by_lua_block {
local expected = "service_token=token-value&service_id=42&usage%5Bhits%5D=2&user_key=value"
local args = ngx.var.args
if args == expected then
ngx.exit(200)
else
ngx.log(ngx.ERR, expected, ' did not match: ', args)
ngx.exit(403)
end
}
}
--- configuration
{
"services": [
{
"id": 42,
"backend_version": 1,
"backend_authentication_type": "service_token",
"backend_authentication_value": "token-value",
"proxy": {
"api_backend": "http://test:$TEST_NGINX_SERVER_PORT/",
"proxy_rules": [
{ "pattern": "/", "http_method": "GET", "metric_system_name": "hits", "delta": 2 }
],
"policy_chain": [
{ "name": "apicast.policy.apicast" },
{
"name": "apicast.policy.url_rewriting",
"configuration":
[
{ "op": "sub", "regex": "does_not_match", "replace": "a", "break": true },
{ "op": "sub", "regex": "aaa", "replace": "bbb" },
{ "op": "sub", "regex": "bbb", "replace": "ccc", "break": true },
{ "op": "sub", "regex": "ccc", "replace": "ddd" }
]
}
]
}
}
]
}
--- upstream
location ~ /ccc$ {
content_by_lua_block {
ngx.say('yay, api backend');
}
}
--- request
GET /aaa?user_key=value
--- response_body
yay, api backend
--- error_code: 200
--- no_error_log
[error]