diff --git a/CHANGELOG.md b/CHANGELOG.md index f02748246..ec9823bf4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/gateway/src/apicast/policy/url_rewriting/policy.lua b/gateway/src/apicast/policy/url_rewriting/policy.lua new file mode 100644 index 000000000..66bab37a7 --- /dev/null +++ b/gateway/src/apicast/policy/url_rewriting/policy.lua @@ -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 +-- 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 diff --git a/gateway/src/apicast/policy/url_rewriting/schema.json b/gateway/src/apicast/policy/url_rewriting/schema.json new file mode 100644 index 000000000..07b9637f7 --- /dev/null +++ b/gateway/src/apicast/policy/url_rewriting/schema.json @@ -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": { + "type": "boolean" + } + }, + "required": ["op", "regex", "replace"] + } + } + } +} diff --git a/t/apicast-policy-url-rewriting.t b/t/apicast-policy-url-rewriting.t new file mode 100644 index 000000000..68c1ffb61 --- /dev/null +++ b/t/apicast-policy-url-rewriting.t @@ -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]