Add dynamic backend routing
diff --git a/conf.d/managed_endpoints.conf b/conf.d/managed_endpoints.conf
index cf917d0..ea06ac6 100644
--- a/conf.d/managed_endpoints.conf
+++ b/conf.d/managed_endpoints.conf
@@ -25,6 +25,7 @@
listen 8080 default_server;
server_tokens off;
+ ignore_invalid_headers off;
#turn off the uninitialized_variable_warn ,as it writes to error_log , hence io
uninitialized_variable_warn off;
@@ -70,7 +71,7 @@
local routing = require "routing"
routing.processCall()
local cors = require "cors"
- ngx.var.cors, ngx.var.cors_methods = cors.processCall(ngx.var["tenant"], ngx.var["gatewayPath"])
+ ngx.var.cors, ngx.var.cors_methods = cors.processCall(ngx.var["tenant"], ngx.var["gatewayPath"])
}
proxy_pass $upstream;
diff --git a/scripts/lua/management/apis.lua b/scripts/lua/management/apis.lua
index 231461d..8f0ff99 100644
--- a/scripts/lua/management/apis.lua
+++ b/scripts/lua/management/apis.lua
@@ -231,22 +231,22 @@
-- @param security security object
function checkOptionalPolicies(policies, security)
if policies then
- for k, v in pairs(policies) do
- local validTypes = {reqMapping = true, rateLimit = true}
+ for _, v in pairs(policies) do
+ local validTypes = {"reqMapping", "rateLimit", "backendRouting"}
if (v.type == nil or v.value == nil) then
- return false, { statusCode = 400, message = "Missing field in policy object. Need \"type\" and \"scope\"." }
- elseif validTypes[v.type] == nil then
- return false, { statusCode = 400, message = "Invalid type in policy object. Valid: \"reqMapping\", \"rateLimit\"" }
+ return false, { statusCode = 400, message = "Missing field in policy object. Need \"type\" and \"value\"." }
+ elseif utils.tableContains(validTypes, v.type) == false then
+ return false, { statusCode = 400, message = "Invalid type in policy object. Valid types: " .. cjson.encode(validTypes) }
end
end
end
if security then
- for k, sec in ipairs(security) do
- local validScopes = {tenant=true, api=true, resource=true}
+ for _, sec in ipairs(security) do
+ local validScopes = {"tenant", "api", "resource"}
if (sec.type == nil or sec.scope == nil) then
return false, { statusCode = 400, message = "Missing field in security object. Need \"type\" and \"scope\"." }
- elseif validScopes[sec.scope] == nil then
- return false, { statusCode = 400, message = "Invalid scope in security object. Valid: \"tenant\", \"api\", \"resource\"." }
+ elseif utils.tableContains(validScopes, sec.scope) == false == nil then
+ return false, { statusCode = 400, message = "Invalid scope in security object. Valid scopes:" .. cjson.encode(validScopes) }
end
end
end
diff --git a/scripts/lua/policies/backendRouting.lua b/scripts/lua/policies/backendRouting.lua
new file mode 100644
index 0000000..5c5b4f3
--- /dev/null
+++ b/scripts/lua/policies/backendRouting.lua
@@ -0,0 +1,89 @@
+-- Copyright (c) 2016 IBM. All rights reserved.
+--
+-- Permission is hereby granted, free of charge, to any person obtaining a
+-- copy of this software and associated documentation files (the "Software"),
+-- to deal in the Software without restriction, including without limitation
+-- the rights to use, copy, modify, merge, publish, distribute, sublicense,
+-- and/or sell copies of the Software, and to permit persons to whom the
+-- Software is furnished to do so, subject to the following conditions:
+--
+-- The above copyright notice and this permission notice shall be included in
+-- all copies or substantial portions of the Software.
+--
+-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+-- FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+-- DEALINGS IN THE SOFTWARE.
+
+--- @module backendRouting
+-- Used to set the backend Url either statically or dynamically
+
+local url = require "url"
+local utils = require "lib/utils"
+local request = require "lib/request"
+local logger = require "lib/logger"
+
+local _M = {}
+
+--- Set upstream based on the backendUrl
+function _M.setRoute(backendUrl)
+ local u = url.parse(backendUrl)
+ if u.scheme == nil then
+ u = url.parse(utils.concatStrings({'http://', backendUrl}))
+ end
+ ngx.var.backendUrl = backendUrl
+ ngx.req.set_uri(getUriPath(u.path))
+ setUpstream(u)
+end
+
+--- Set dynamic route based on the based on the header that is passed in
+function _M.setDynamicRoute(obj)
+ local whitelist = obj.whitelist
+ for k in pairs(whitelist) do
+ whitelist[k] = whitelist[k]:lower()
+ end
+ local header = obj.header ~= nil and obj.header or 'X-Cf-Forwarded-Url'
+ local dynamicBackend = ngx.req.get_headers()[header];
+ if dynamicBackend ~= nil and dynamicBackend ~= '' then
+ local u = url.parse(dynamicBackend)
+ if u.scheme == nil or u.scheme == '' then
+ u = url.parse(utils.concatStrings({'http://', dynamicBackend}))
+ end
+ if utils.tableContains(whitelist, u.host) then
+ ngx.req.set_uri(getUriPath(u.path))
+ setUpstream(u)
+ else
+ request.err(403, 'Dynamic backend host not part of whitelist.')
+ end
+ else
+ logger.info('Header for dynamic routing not found. Defaulting to backendUrl.')
+ end
+end
+
+function getUriPath(backendPath)
+ local gatewayPath = ngx.unescape_uri(ngx.var.gatewayPath)
+ gatewayPath = gatewayPath:gsub('-', '%%-')
+ local _, j = ngx.var.request_uri:find(gatewayPath)
+ local incomingPath = ((j and ngx.var.request_uri:sub(j + 1)) or nil)
+ -- Check for backendUrl path
+ if backendPath == nil or backendPath == '' or backendPath == '/' then
+ incomingPath = (incomingPath and incomingPath ~= '') and incomingPath or '/'
+ incomingPath = string.sub(incomingPath, 1, 1) == '/' and incomingPath or utils.concatStrings({'/', incomingPath})
+ return incomingPath
+ else
+ return utils.concatStrings({backendPath, incomingPath})
+ end
+end
+
+function setUpstream(u)
+ local upstream = utils.concatStrings({u.scheme, '://', u.host})
+ if u.port ~= nil and u.port ~= '' then
+ upstream = utils.concatStrings({upstream, ':', u.port})
+ end
+ ngx.var.upstream = upstream
+end
+
+return _M
\ No newline at end of file
diff --git a/scripts/lua/policies/mapping.lua b/scripts/lua/policies/mapping.lua
index 97f7c71..643070f 100644
--- a/scripts/lua/policies/mapping.lua
+++ b/scripts/lua/policies/mapping.lua
@@ -28,10 +28,10 @@
local _M = {}
-local body = nil
-local query = nil
-local headers = nil
-local path = nil
+local body
+local query
+local headers
+local path
--- Implementation for the mapping policy.
-- @param map The mapping object that contains details about request tranformations
diff --git a/scripts/lua/routing.lua b/scripts/lua/routing.lua
index c1e2720..af4da88 100644
--- a/scripts/lua/routing.lua
+++ b/scripts/lua/routing.lua
@@ -25,11 +25,11 @@
local utils = require "lib/utils"
local request = require "lib/request"
local redis = require "lib/redis"
-local url = require "url"
-- load policies
local security = require "policies/security"
local mapping = require "policies/mapping"
local rateLimit = require "policies/rateLimit"
+local backendRouting = require "policies/backendRouting"
local REDIS_HOST = os.getenv("REDIS_HOST")
local REDIS_PORT = os.getenv("REDIS_PORT")
@@ -44,14 +44,13 @@
local resourceKeys = redis.getAllResourceKeys(red, ngx.var.tenant)
local redisKey = _M.findRedisKey(resourceKeys, ngx.var.tenant, ngx.var.gatewayPath)
if redisKey == nil then
- return request.err(404, 'Not found.')
+ request.err(404, 'Not found.')
end
local obj = cjson.decode(redis.getResource(red, redisKey, "resources"))
- local found = false
for verb, opFields in pairs(obj.operations) do
if string.upper(verb) == ngx.req.get_method() then
-- Check if auth is required
- local key = nil
+ local key
if (opFields.security) then
for k, sec in ipairs(opFields.security) do
local result = utils.concatStrings({key, security.process(sec)})
@@ -60,36 +59,20 @@
end
end
end
- -- Parse backend url
- local u = url.parse(opFields.backendUrl)
- -- add http:// if no protocol is specified
- if u.scheme == nil then
- u = url.parse(utils.concatStrings({'http://', opFields.backendUrl}))
- end
- ngx.req.set_uri(getUriPath(u.path))
- ngx.var.backendUrl = opFields.backendUrl
- -- Set upstream
- local upstream = utils.concatStrings({u.scheme, '://', u.host})
- -- add port if it's in the backendURL
- if u.port ~= nil and u.port ~= '' then
- upstream = utils.concatStrings({upstream, ':', u.port})
- end
- ngx.var.upstream = upstream
-- Set backend method
if opFields.backendMethod ~= nil then
setVerb(opFields.backendMethod)
end
+ -- Set backend upstream and uri
+ backendRouting.setRoute(opFields.backendUrl)
-- Parse policies
if opFields.policies ~= nil then
parsePolicies(opFields.policies, key)
end
- found = true
- break
+ return
end
end
- if found == false then
- request.err(404, 'Whoops. Verb not supported.')
- end
+ request.err(404, 'Whoops. Verb not supported.')
end
--- Find the correct redis key based on the path that's passed in
@@ -168,6 +151,8 @@
mapping.processMap(v.value)
elseif v.type == 'rateLimit' then
rateLimit.limit(v.value, apiKey)
+ elseif v.type == 'backendRouting' then
+ backendRouting.setDynamicRoute(v.value)
end
end
end
@@ -177,26 +162,11 @@
function setVerb(v)
local allowedVerbs = {'GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'}
local verb = string.upper(v)
- if(utils.tableContains(allowedVerbs, verb)) then
+ if utils.tableContains(allowedVerbs, verb) then
ngx.req.set_method(ngx[utils.concatStrings({"HTTP_", verb})])
else
ngx.req.set_method(ngx.HTTP_GET)
end
end
-function getUriPath(backendPath)
- local gatewayPath = ngx.unescape_uri(ngx.var.gatewayPath)
- gatewayPath = gatewayPath:gsub('-', '%%-')
- local _, j = ngx.var.uri:find(gatewayPath)
- local incomingPath = ((j and ngx.var.uri:sub(j + 1)) or nil)
- -- Check for backendUrl path
- if backendPath == nil or backendPath == '' or backendPath == '/' then
- incomingPath = (incomingPath and incomingPath ~= '') and incomingPath or '/'
- incomingPath = string.sub(incomingPath, 1, 1) == '/' and incomingPath or utils.concatStrings({'/', incomingPath})
- return incomingPath
- else
- return utils.concatStrings({backendPath, incomingPath})
- end
-end
-
return _M