-
Notifications
You must be signed in to change notification settings - Fork 168
/
provider.lua
500 lines (401 loc) · 14.8 KB
/
provider.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
-- provider_key: <%= provider_key %> --
-- -*- mode: lua; -*-
-- Generated on: <%= Time.now %> --
-- Version:
-- Error Messages per service
local cjson = require 'cjson'
local custom_config = os.getenv('APICAST_CUSTOM_CONFIG')
local configuration = require 'configuration'
local inspect = require 'inspect'
local oauth = require 'oauth'
local util = require 'util'
local type = type
local pairs = pairs
local ipairs = ipairs
local insert = table.insert
local lower = string.lower
local concat = table.concat
local tostring = tostring
local format = string.format
local gsub = string.gsub
local unpack = unpack
local split = util.string_split
local resty_resolver = require 'resty.resolver'
local dns_resolver = require 'resty.resolver.dns'
local response_codes = util.env_enabled('APICAST_RESPONSE_CODES')
local request_logs = util.env_enabled('APICAST_REQUEST_LOGS')
local _M = {
-- FIXME: this is really bad idea, this file is shared across all requests,
-- so that means sharing something in this module would be sharing it acros all requests
-- and in multi-tenant environment that would mean leaking information
configuration = {}
}
function _M.configure(contents)
local config, err = configuration.parse(contents)
if err then
ngx.log(ngx.WARN, 'not configured: ', err)
return nil, err
end
_M.contents = configuration.encode(contents)
if config then
_M.configured = true
_M.configuration = config
_M.services = config.services or {} -- for compatibility reasons
return config
else
_M.configured = false
_M.services = false
end
end
function _M.init(config)
math.randomseed(ngx.now())
-- First calls to math.random after a randomseed tend to be similar; discard them
for _=1,3 do math.random() end
return _M.configure(config)
end
-- Error Codes
local function error_no_credentials(service)
ngx.log(ngx.INFO, 'no credentials provided for service ' .. tostring(service.id))
ngx.var.cached_key = nil
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
local function error_authorization_failed(service)
ngx.log(ngx.INFO, 'authorization failed for service ' .. tostring(service.id))
ngx.var.cached_key = nil
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
local function error_no_match(service)
ngx.log(ngx.INFO, 'no rules matched for service ' .. tostring(service.id))
ngx.var.cached_key = nil
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
local function error_service_not_found(host)
ngx.status = 404
ngx.print('')
ngx.log(ngx.WARN, 'could not find service for host: ', host)
ngx.exit(ngx.status)
end
-- End Error Codes
-- Aux function to split a string
local function first_values(a)
local r = {}
for k,v in pairs(a) do
if type(v) == "table" then
r[lower(k)] = v[1] -- TODO: use metatable to convert all access to lowercase
else
r[lower(k)] = v
end
end
return r
end
local function build_querystring_formatter(fmt)
return function (query)
local function kvmap(f, t)
local res = {}
for k, v in pairs(t) do
insert(res, f(k, v))
end
return res
end
return concat(kvmap(function(k,v) return 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")
--[[
Authorization logic
]]--
local 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
local function get_debug_value()
return ngx.var.http_x_3scale_debug == _M.configuration.debug_header
end
local function find_service_strict(host)
for _,service in ipairs(_M.services or {}) do
if type(host) == 'number' and service.id == host then
return service
end
for _,_host in ipairs(service.hosts or {}) do
if _host == host then
return service
end
end
end
ngx.log(ngx.ERR, 'service not found for host ' .. host)
end
local function find_service_cascade(host)
local request = ngx.var.request
for _,service in ipairs(_M.services or {}) do
for _,_host in ipairs(service.hosts or {}) do
if _host == host then
local name = service.system_name or service.id
ngx.log(ngx.DEBUG, 'service ' .. name .. ' matched host ' .. _host)
local usage, matched_patterns = service:extract_usage(request)
if next(usage) and matched_patterns ~= '' then
ngx.log(ngx.DEBUG, 'service ' .. name .. ' matched patterns ' .. matched_patterns)
return service
end
end
end
end
return find_service_strict(host)
end
if util.env_enabled('APICAST_PATH_ROUTING_ENABLED') then
ngx.log(ngx.WARN, 'apicast experimental path routing enabled')
_M.find_service = find_service_cascade
else
_M.find_service = find_service_strict
end
local http = {
get = function(url)
ngx.log(ngx.INFO, '[http] requesting ' .. url)
local backend_upstream = ngx.ctx.backend_upstream
local previous_real_url = ngx.var.real_url
ngx.log(ngx.DEBUG, '[ctx] copying backend_upstream of size: ', #backend_upstream)
local res = ngx.location.capture(assert(url), { share_all_vars = true, ctx = { backend_upstream = backend_upstream } })
local real_url = ngx.var.real_url
if real_url ~= previous_real_url then
ngx.log(ngx.INFO, '[http] ', real_url, ' (',tostring(res.status), ')')
else
ngx.log(ngx.INFO, '[http] status: ', tostring(res.status))
end
ngx.var.real_url = ''
return res
end
}
local function oauth_authrep(service)
ngx.var.cached_key = ngx.var.cached_key .. ":" .. ngx.var.usage
local access_tokens = assert(ngx.shared.api_keys, 'missing shared dictionary: api_keys')
local is_known = access_tokens:get(ngx.var.cached_key)
if is_known ~= 200 then
local res = http.get("/threescale_oauth_authrep")
-- 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"
error_authorization_failed(service)
else
access_tokens:set(ngx.var.cached_key,200)
end
ngx.var.cached_key = nil
end
end
local function authrep(service)
local cached_key = ngx.var.cached_key .. ":" .. ngx.var.usage
local api_keys = ngx.shared.api_keys
local is_known = api_keys and api_keys:get(cached_key)
if is_known == 200 then
ngx.log(ngx.DEBUG, 'apicast cache hit key: ' .. cached_key)
ngx.var.cached_key = cached_key
else
ngx.log(ngx.INFO, 'apicast cache miss key: ' .. cached_key)
local res = http.get("/threescale_authrep")
ngx.log(ngx.DEBUG, '[backend] response status: ' .. tostring(res.status) .. ' body: ' .. tostring(res.body))
if res.status == 200 then
if api_keys then
ngx.log(ngx.INFO, 'apicast cache write key: ' .. tostring(cached_key))
api_keys:set(cached_key, 200)
end
else -- TODO: proper error handling
if api_keys then api_keys:delete(cached_key) end
ngx.status = res.status
ngx.header.content_type = "application/json"
-- error_authorization_failed is an early return, so we have to reset cached_key to nil before -%>
error_authorization_failed(service)
end
-- set this request_to_3scale_backend to nil to avoid doing the out of band authrep -%>
ngx.var.cached_key = nil
end
end
function _M.authorize(backend_version, service)
if backend_version == 'oauth' then
oauth_authrep(service)
else
authrep(service)
end
end
function _M.set_service(host)
host = host or ngx.var.host
local service = _M.find_service(host)
if not service then
error_service_not_found(host)
end
ngx.ctx.service = service
end
function _M.set_upstream()
local service = ngx.ctx.service
-- The default values are only for tests. We need to set at least the scheme.
local scheme, _, _, host, port, path =
unpack(configuration.url(service.api_backend) or { 'http' })
ngx.ctx.dns = dns_resolver:new{ nameservers = resty_resolver.nameservers() }
ngx.ctx.resolver = resty_resolver.new(ngx.ctx.dns)
ngx.var.proxy_pass = scheme .. '://upstream' .. (path or '')
ngx.req.set_header('Host', service.hostname_rewrite or host or ngx.var.host)
ngx.ctx.upstream = ngx.ctx.resolver:get_servers(host, { port = port })
end
function _M.call(host)
host = host or ngx.var.host
if not ngx.ctx.service then
_M.set_service(host)
end
local service = ngx.ctx.service
ngx.var.backend_authentication_type = service.backend_authentication.type
ngx.var.backend_authentication_value = service.backend_authentication.value
ngx.var.backend_host = service.backend.host or ngx.var.backend_host
ngx.var.service_id = tostring(service.id)
ngx.var.version = _M.configuration.version
-- set backend
local scheme, _, _, server, port, path = unpack(configuration.url(service.backend.endpoint or ngx.var.backend_endpoint))
if not port then
if scheme == 'http' then
port = 80
elseif scheme == 'https' then
port = 443
end
end
ngx.ctx.dns = ngx.ctx.dns or dns_resolver:new{ nameservers = resty_resolver.nameservers() }
ngx.ctx.resolver = ngx.ctx.resolver or resty_resolver.new(ngx.ctx.dns)
local backend_upstream = ngx.ctx.resolver:get_servers(server, { port = port or nil })
ngx.log(ngx.DEBUG, '[resolver] resolved backend upstream: ', #backend_upstream)
ngx.ctx.backend_upstream = backend_upstream
ngx.var.backend_endpoint = scheme .. '://backend_upstream' .. (path or '')
if service.backend_version == 'oauth' then
local f, params = oauth.call()
if f then
ngx.log(ngx.DEBUG, 'apicast oauth flow')
return function() return f(params) end
end
end
return function()
-- call access phase
return _M.access(service)
end
end
function _M.access(service)
local scheme, _, _, host, port, path = unpack(configuration.url(service.api_backend) or {})
local backend_version = service.backend_version
local params = {}
local usage
local matched_patterns
if ngx.status == 403 then
ngx.say("Throttling due to too many requests")
ngx.exit(403)
end
local request = ngx.var.request
local credentials = service.credentials
local parameters = get_auth_params(credentials.location, split(request, " ")[1] )
ngx.var.secret_token = service.secret_token
if backend_version == '1' then
params.user_key = parameters[credentials.user_key]
ngx.var.cached_key = concat({service.id, params.user_key}, ':')
elseif backend_version == '2' then
params.app_id = parameters[credentials.app_id]
params.app_key = parameters[credentials.app_key] -- or "" -- Uncoment the first part if you want to allow not passing app_key
ngx.var.cached_key = concat({service.id, params.app_id, params.app_key}, ':')
elseif backend_version == 'oauth' then
ngx.var.access_token = parameters.access_token
params.access_token = parameters.access_token
ngx.var.cached_key = concat({service.id, params.access_token}, ':')
else
error('unknown backend version: ' .. tostring(backend_version))
end
if not service:get_credentials(params) then
return error_no_credentials(service)
end
usage, matched_patterns = service:extract_usage(request)
ngx.log(ngx.INFO, inspect{usage, matched_patterns})
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"] = ''
return 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.authorize(backend_version, service)
-- set upstream
ngx.var.proxy_pass = scheme .. '://upstream' .. (path or '')
ngx.req.set_header('Host', service.hostname_rewrite or host or ngx.var.host)
ngx.ctx.upstream = ngx.ctx.resolver:get_servers(host, { port = port })
end
local function request_logs_encoded_data()
local request_log = {}
if request_logs then
local method, path, headers = ngx.req.get_method(), ngx.var.request_uri, ngx.req.get_headers()
local req = cjson.encode{ method=method, path=path, headers=headers }
local resp = cjson.encode{ body = ngx.var.resp_body, headers = cjson.decode(ngx.var.resp_headers) }
request_log["log[request]"] = req
request_log["log[response]"] = resp
end
if response_codes then
request_log["log[code]"] = ngx.var.status
end
return ngx.escape_uri(ngx.encode_args(request_log))
end
function _M.post_action()
local service_id = tonumber(ngx.var.service_id, 10)
_M.call(service_id) -- initialize resolver and get backend upstream peers
local cached_key = ngx.var.cached_key
local service = ngx.ctx.service
if cached_key and cached_key ~= "null" then
ngx.log(ngx.INFO, '[async] reporting to backend asynchronously')
local auth_uri = service.backend_version == 'oauth' and 'threescale_oauth_authrep' or 'threescale_authrep'
local res = http.get("/".. auth_uri .."?log=" .. request_logs_encoded_data())
if res.status ~= 200 then
local api_keys = ngx.shared.api_keys
if api_keys then
ngx.log(ngx.NOTICE, 'apicast cache delete key: ' .. cached_key .. ' cause status ' .. tostring(res.status))
api_keys:delete(cached_key)
else
ngx.log(ngx.ALERT, 'apicast cache error missing shared memory zone api_keys')
end
end
else
ngx.log(ngx.INFO, '[async] skipping after action, no cached key')
end
ngx.exit(ngx.HTTP_OK)
end
if custom_config then
local path = package.path
local module = gsub(custom_config, '%.lua$', '') -- strip .lua from end of the file
package.path = package.path .. ';' .. ngx.config.prefix() .. '?.lua;'
local ok, c = pcall(function() return require(module) end)
package.path = path
if ok then
if type(c) == 'table' and type(c.setup) == 'function' then
ngx.log(ngx.DEBUG, 'executing custom config ' .. custom_config)
c.setup(_M)
else
ngx.log(ngx.ERR, 'failed to load custom config ' .. tostring(custom_config) .. ' because it does not return table with function setup')
end
else
ngx.log(ngx.ERR, 'failed to load custom config ' .. tostring(custom_config) .. ' with ' .. tostring(c))
end
end
return _M
-- END OF SCRIPT