mirror of
https://github.com/bigchaindb/bigchaindb.git
synced 2024-10-13 13:34:05 +00:00

- All files moved to k8s/nginx-3scale with directory structure consistent with k8s/nginx-http(s) - Top level LICENCES.md updated - Renaming entry point script to nginx_openresty_entrypoint.bash
417 lines
12 KiB
Lua
417 lines
12 KiB
Lua
-- -*- mode: lua; -*-
|
|
-- Generated on: 2017-04-10 14:41:18 +0000 --
|
|
-- Version:
|
|
-- Error Messages per service
|
|
|
|
-- Ref: https://github.com/openresty/lua-nginx-module
|
|
-- Ref: https://ipdbtestnet-admin.3scale.net/p/admin/api_docs
|
|
-- Ref: http://nginx.org/en/docs/debugging_log.html
|
|
|
|
local custom_config = false
|
|
|
|
local _M = {
|
|
['services'] = {
|
|
['SERVICE_ID'] = {
|
|
error_auth_failed = 'Authentication failed',
|
|
error_auth_missing = 'Authentication parameters missing',
|
|
auth_failed_headers = 'text/plain; charset=us-ascii',
|
|
auth_missing_headers = 'text/plain; charset=us-ascii',
|
|
error_no_match = 'No Mapping Rule matched',
|
|
no_match_headers = 'text/plain; charset=us-ascii',
|
|
no_match_status = 404,
|
|
auth_failed_status = 403,
|
|
auth_missing_status = 403,
|
|
secret_token = 'THREESCALE_RESPONSE_SECRET_TOKEN',
|
|
get_credentials = function(service, params)
|
|
return (
|
|
(params.app_id and params.app_key)
|
|
) or error_no_credentials(service)
|
|
end,
|
|
extract_usage = function (service, request)
|
|
local method, url = unpack(string.split(request," "))
|
|
local path, querystring = unpack(string.split(url, "?"))
|
|
local usage_t = {}
|
|
local matched_rules = {}
|
|
|
|
local args = get_auth_params(nil, method)
|
|
|
|
for i,r in ipairs(service.rules) do
|
|
check_rule({path=path, method=method, args=args}, r, usage_t, matched_rules)
|
|
end
|
|
|
|
-- if there was no match, usage is set to nil and it will respond a 404, this behavior can be changed
|
|
return usage_t, table.concat(matched_rules, ", ")
|
|
end,
|
|
rules = {
|
|
{
|
|
method = 'POST',
|
|
pattern = '/api/{version}/transactions$',
|
|
parameters = { 'version' },
|
|
querystring_params = function(args)
|
|
return true
|
|
end,
|
|
system_name = 'hits',
|
|
delta = 1
|
|
},
|
|
{
|
|
method = 'POST',
|
|
pattern = '/api/{version}/transactions$',
|
|
parameters = { 'version' },
|
|
querystring_params = function(args)
|
|
return true
|
|
end,
|
|
system_name = 'request_body_size',
|
|
delta = 1
|
|
},
|
|
{
|
|
method = 'POST',
|
|
pattern = '/api/{version}/transactions$',
|
|
parameters = { 'version' },
|
|
querystring_params = function(args)
|
|
return true
|
|
end,
|
|
system_name = 'response_body_size',
|
|
delta = 1
|
|
},
|
|
{
|
|
method = 'POST',
|
|
pattern = '/api/{version}/transactions$',
|
|
parameters = { 'version' },
|
|
querystring_params = function(args)
|
|
return true
|
|
end,
|
|
system_name = 'post_transactions',
|
|
delta = 1
|
|
},
|
|
{
|
|
method = 'POST',
|
|
pattern = '/api/{version}/transactions$',
|
|
parameters = { 'version' },
|
|
querystring_params = function(args)
|
|
return true
|
|
end,
|
|
system_name = 'total_body_size',
|
|
delta = 1
|
|
},
|
|
}
|
|
},
|
|
}
|
|
}
|
|
|
|
-- Error Codes
|
|
function error_no_credentials(service)
|
|
ngx.status = service.auth_missing_status
|
|
ngx.header.content_type = service.auth_missing_headers
|
|
ngx.print(service.error_auth_missing)
|
|
ngx.exit(ngx.HTTP_OK)
|
|
end
|
|
|
|
function error_authorization_failed(service)
|
|
ngx.status = service.auth_failed_status
|
|
ngx.header.content_type = service.auth_failed_headers
|
|
ngx.print(service.error_auth_failed)
|
|
ngx.exit(ngx.HTTP_OK)
|
|
end
|
|
|
|
function error_no_match(service)
|
|
ngx.status = service.no_match_status
|
|
ngx.header.content_type = service.no_match_headers
|
|
ngx.print(service.error_no_match)
|
|
ngx.exit(ngx.HTTP_OK)
|
|
end
|
|
-- End Error Codes
|
|
|
|
-- Aux function to split a string
|
|
|
|
function string:split(delimiter)
|
|
local result = { }
|
|
local from = 1
|
|
local delim_from, delim_to = string.find( self, delimiter, from )
|
|
if delim_from == nil then return {self} end
|
|
while delim_from do
|
|
table.insert( result, string.sub( self, from , delim_from-1 ) )
|
|
from = delim_to + 1
|
|
delim_from, delim_to = string.find( self, delimiter, from )
|
|
end
|
|
table.insert( result, string.sub( self, from ) )
|
|
return result
|
|
end
|
|
|
|
function first_values(a)
|
|
r = {}
|
|
for k,v in pairs(a) do
|
|
if type(v) == "table" then
|
|
r[k] = v[1]
|
|
else
|
|
r[k] = v
|
|
end
|
|
end
|
|
return r
|
|
end
|
|
|
|
function set_or_inc(t, name, delta)
|
|
return (t[name] or 0) + delta
|
|
end
|
|
|
|
function build_querystring_formatter(fmt)
|
|
return function (query)
|
|
local function kvmap(f, t)
|
|
local res = {}
|
|
for k, v in pairs(t) do
|
|
table.insert(res, f(k, v))
|
|
end
|
|
return res
|
|
end
|
|
|
|
return table.concat(kvmap(function(k,v) return string.format(fmt, k, v) end, query or {}), "&")
|
|
end
|
|
end
|
|
|
|
local build_querystring = build_querystring_formatter("usage[%s]=%s")
|
|
local build_query = build_querystring_formatter("%s=%s")
|
|
|
|
function regexpify(path)
|
|
return path:gsub('?.*', ''):gsub("{.-}", '([\\w_.-]+)'):gsub("%.", "\\.")
|
|
end
|
|
|
|
function check_rule(req, rule, usage_t, matched_rules)
|
|
local param = {}
|
|
local p = regexpify(rule.pattern)
|
|
local m = ngx.re.match(req.path,
|
|
string.format("^%s",p))
|
|
if m and req.method == rule.method then
|
|
local args = req.args
|
|
if rule.querystring_params(args) then -- may return an empty table
|
|
-- when no querystringparams
|
|
-- in the rule. it's fine
|
|
for i,p in ipairs(rule.parameters) do
|
|
param[p] = m[i]
|
|
end
|
|
|
|
table.insert(matched_rules, rule.pattern)
|
|
usage_t[rule.system_name] = set_or_inc(usage_t, rule.system_name, rule.delta)
|
|
end
|
|
end
|
|
end
|
|
|
|
--[[
|
|
Authorization logic
|
|
NOTE: We do not use any of the authorization logic defined in the template.
|
|
We use custom authentication and authorization logic defined in the
|
|
custom_app_id_authorize() function.
|
|
]]--
|
|
|
|
function get_auth_params(where, method)
|
|
local params = {}
|
|
if where == "headers" then
|
|
params = ngx.req.get_headers()
|
|
elseif method == "GET" then
|
|
params = ngx.req.get_uri_args()
|
|
else
|
|
ngx.req.read_body()
|
|
params = ngx.req.get_post_args()
|
|
end
|
|
return first_values(params)
|
|
end
|
|
|
|
function get_debug_value()
|
|
local h = ngx.req.get_headers()
|
|
if h["X-3scale-debug"] == 'SERVICE_TOKEN' then
|
|
return true
|
|
else
|
|
return false
|
|
end
|
|
end
|
|
|
|
function _M.authorize(auth_strat, params, service)
|
|
if auth_strat == 'oauth' then
|
|
oauth(params, service)
|
|
else
|
|
authrep(params, service)
|
|
end
|
|
end
|
|
|
|
function oauth(params, service)
|
|
ngx.var.cached_key = ngx.var.cached_key .. ":" .. ngx.var.usage
|
|
local access_tokens = ngx.shared.api_keys
|
|
local is_known = access_tokens:get(ngx.var.cached_key)
|
|
|
|
if is_known ~= 200 then
|
|
local res = ngx.location.capture("/threescale_oauth_authrep", { share_all_vars = true })
|
|
|
|
-- IN HERE YOU DEFINE THE ERROR IF CREDENTIALS ARE PASSED, BUT THEY ARE NOT VALID
|
|
if res.status ~= 200 then
|
|
access_tokens:delete(ngx.var.cached_key)
|
|
ngx.status = res.status
|
|
ngx.header.content_type = "application/json"
|
|
ngx.var.cached_key = nil
|
|
error_authorization_failed(service)
|
|
else
|
|
access_tokens:set(ngx.var.cached_key,200)
|
|
end
|
|
|
|
ngx.var.cached_key = nil
|
|
end
|
|
end
|
|
|
|
function authrep(params, service)
|
|
ngx.var.cached_key = ngx.var.cached_key .. ":" .. ngx.var.usage
|
|
local api_keys = ngx.shared.api_keys
|
|
local is_known = api_keys:get(ngx.var.cached_key)
|
|
|
|
if is_known ~= 200 then
|
|
local res = ngx.location.capture("/threescale_authrep", { share_all_vars = true })
|
|
|
|
-- IN HERE YOU DEFINE THE ERROR IF CREDENTIALS ARE PASSED, BUT THEY ARE NOT VALID
|
|
if res.status ~= 200 then
|
|
-- remove the key, if it's not 200 let's go the slow route, to 3scale's backend
|
|
api_keys:delete(ngx.var.cached_key)
|
|
ngx.status = res.status
|
|
ngx.header.content_type = "application/json"
|
|
ngx.var.cached_key = nil
|
|
error_authorization_failed(service)
|
|
else
|
|
api_keys:set(ngx.var.cached_key,200)
|
|
end
|
|
ngx.var.cached_key = nil
|
|
end
|
|
end
|
|
|
|
function _M.access()
|
|
local params = {}
|
|
local host = ngx.req.get_headers()["Host"]
|
|
local auth_strat = ""
|
|
local service = {}
|
|
local usage = {}
|
|
local matched_patterns = ''
|
|
|
|
if ngx.status == 403 then
|
|
ngx.say("Throttling due to too many requests")
|
|
ngx.exit(403)
|
|
end
|
|
|
|
if ngx.var.service_id == 'SERVICE_ID' then
|
|
local parameters = get_auth_params("headers", string.split(ngx.var.request, " ")[1] )
|
|
service = _M.services['SERVICE_ID'] --
|
|
ngx.var.secret_token = service.secret_token
|
|
params.app_id = parameters["app_id"]
|
|
params.app_key = parameters["app_key"] -- or "" -- Uncoment the first part if you want to allow not passing app_key
|
|
service.get_credentials(service, params)
|
|
ngx.var.cached_key = "SERVICE_ID" .. ":" .. params.app_id ..":".. params.app_key
|
|
auth_strat = "2"
|
|
ngx.var.service_id = "SERVICE_ID"
|
|
ngx.var.proxy_pass = "http://backend_SERVICE_ID"
|
|
usage, matched_patterns = service:extract_usage(ngx.var.request)
|
|
end
|
|
|
|
usage['post_transactions'] = 0
|
|
usage['request_body_size'] = 0
|
|
usage['total_body_size'] = 0
|
|
usage['response_body_size'] = 0
|
|
ngx.var.credentials = build_query(params)
|
|
ngx.var.usage = build_querystring(usage)
|
|
|
|
-- WHAT TO DO IF NO USAGE CAN BE DERIVED FROM THE REQUEST.
|
|
if ngx.var.usage == '' then
|
|
ngx.header["X-3scale-matched-rules"] = ''
|
|
error_no_match(service)
|
|
end
|
|
|
|
if get_debug_value() then
|
|
ngx.header["X-3scale-matched-rules"] = matched_patterns
|
|
ngx.header["X-3scale-credentials"] = ngx.var.credentials
|
|
ngx.header["X-3scale-usage"] = ngx.var.usage
|
|
ngx.header["X-3scale-hostname"] = ngx.var.hostname
|
|
end
|
|
_M.custom_app_id_authorize(params, service)
|
|
end
|
|
|
|
function _M.custom_app_id_authorize(params, service)
|
|
ngx.var.cached_key = ngx.var.cached_key .. ":" .. ngx.var.usage
|
|
local api_keys = ngx.shared.api_keys
|
|
local res = ngx.location.capture("/threescale_auth", { share_all_vars = true })
|
|
if res.status ~= 200 then
|
|
ngx.status = res.status
|
|
ngx.header.content_type = "application/json"
|
|
ngx.var.cached_key = nil
|
|
error_authorization_failed(service)
|
|
end
|
|
ngx.var.cached_key = nil
|
|
end
|
|
|
|
function _M.post_action_content()
|
|
local report_data = {}
|
|
|
|
-- increment POST count
|
|
report_data['post_transactions'] = 1
|
|
|
|
-- NOTE: When we are querying for the length of the request here, we already
|
|
-- have the complete request data with us and hence can just use the len()
|
|
-- function to get the size of the payload in bytes.
|
|
-- However, we might not have a complete response from the backend at this
|
|
-- stage (esp. if it's a large response size). So, we decipher the payload
|
|
-- size by peeking into the content length header of the response.
|
|
-- Otherwise, nginx will have to buffer every response and then calculate
|
|
-- response payload size.
|
|
|
|
-- req data size
|
|
local req_data = ngx.req.get_body_data()
|
|
if req_data then
|
|
report_data['request_body_size'] = req_data:len()
|
|
else
|
|
report_data['request_body_size'] = 0
|
|
end
|
|
|
|
-- res data size
|
|
local all_headers = cjson.decode(ngx.var.resp_headers)
|
|
local variable_header = "content-length" --<-- case sensitive
|
|
if all_headers[variable_header] then
|
|
report_data['response_body_size'] = all_headers[variable_header]
|
|
else
|
|
report_data['response_body_size'] = 0
|
|
end
|
|
|
|
-- total data size
|
|
report_data['total_body_size'] = report_data['request_body_size'] + report_data['response_body_size']
|
|
|
|
-- get the app_id
|
|
local app_id = ""
|
|
local credentials = ngx.var.credentials:split("&")
|
|
for i in pairs(credentials) do
|
|
if credentials[i]:match('app_id') then
|
|
local temp = credentials[i]:split("=")
|
|
app_id = temp[2]
|
|
end
|
|
end
|
|
|
|
-- form the payload to report to 3scale
|
|
local report = {}
|
|
report['service_id'] = ngx.var.service_id
|
|
report['service_token'] = ngx.var.service_token
|
|
report['transactions[0][app_id]'] = app_id
|
|
report['transactions[0][usage][post_transactions]'] = report_data['post_transactions']
|
|
report['transactions[0][usage][request_body_size]'] = report_data['request_body_size']
|
|
report['transactions[0][usage][response_body_size]'] = report_data['response_body_size']
|
|
report['transactions[0][usage][total_body_size]'] = report_data['total_body_size']
|
|
local res1 = ngx.location.capture("/threescale_report", {method = ngx.HTTP_POST, body = ngx.encode_args(report), share_all_vars = true })
|
|
--ngx.log(0, ngx.encode_args(report))
|
|
ngx.log(0, "Status: "..res1.status)
|
|
ngx.log(0, "Body: "..res1.body)
|
|
--if res1.status ~= 200 then
|
|
-- local api_keys = ngx.shared.api_keys
|
|
-- api_keys:delete(cached_key)
|
|
--end
|
|
ngx.exit(ngx.HTTP_OK)
|
|
end
|
|
|
|
if custom_config then
|
|
local ok, c = pcall(function() return require(custom_config) end)
|
|
if ok and type(c) == 'table' and type(c.setup) == 'function' then
|
|
c.setup(_M)
|
|
end
|
|
end
|
|
|
|
return _M
|
|
|
|
-- END OF SCRIPT
|