-- -*- 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