-
Notifications
You must be signed in to change notification settings - Fork 4.8k
/
Copy pathaccess.lua
560 lines (479 loc) · 16.5 KB
/
access.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
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
local multipart = require "multipart"
local cjson = require("cjson.safe").new()
local pl_template = require "pl.template"
local sandbox = require "kong.tools.sandbox"
local cycle_aware_deep_copy = require("kong.tools.table").cycle_aware_deep_copy
local table_insert = table.insert
local get_uri_args = kong.request.get_query
local set_uri_args = kong.service.request.set_query
local clear_header = kong.service.request.clear_header
local get_header = kong.request.get_header
local set_header = kong.service.request.set_header
local get_headers = kong.request.get_headers
local set_headers = kong.service.request.set_headers
local set_method = kong.service.request.set_method
local set_path = kong.service.request.set_path
local get_raw_body = kong.request.get_raw_body
local set_raw_body = kong.service.request.set_raw_body
local encode_args = ngx.encode_args
local ngx_decode_args = ngx.decode_args
local type = type
local str_find = string.find
local pairs = pairs
local error = error
local rawset = rawset
local lua_enabled = sandbox.configuration.enabled
local sandbox_enabled = sandbox.configuration.sandbox_enabled
local _M = {}
local template_cache = setmetatable( {}, { __mode = "k" })
local DEBUG = ngx.DEBUG
local CONTENT_LENGTH = "content-length"
local CONTENT_TYPE = "content-type"
local HOST = "host"
local JSON, MULTI, ENCODED = "json", "multi_part", "form_encoded"
local EMPTY = require("kong.tools.table").EMPTY
local compile_opts = {
escape = "\xff", -- disable '#' as a valid template escape
}
cjson.decode_array_with_array_mt(true)
local function parse_json(body)
if body then
return cjson.decode(body)
end
end
local function decode_args(body)
if body then
return ngx_decode_args(body)
end
return {}
end
local function get_content_type(content_type)
if content_type == nil then
return
end
if str_find(content_type:lower(), "application/json", nil, true) then
return JSON
elseif str_find(content_type:lower(), "multipart/form-data", nil, true) then
return MULTI
elseif str_find(content_type:lower(), "application/x-www-form-urlencoded", nil, true) then
return ENCODED
end
end
local function param_value(source_template, config_array, template_env)
if not source_template or source_template == "" then
return nil
end
if not lua_enabled then
-- Detect expressions in the source template
local expr = str_find(source_template, "%$%(.*%)")
if expr then
return nil, "loading of untrusted Lua code disabled because " ..
"'untrusted_lua' config option is set to 'off'"
end
-- Lua is disabled, no need to render the template
return source_template
end
-- find compiled templates for this plugin-configuration array
local compiled_templates = template_cache[config_array]
if not compiled_templates then
compiled_templates = {}
-- store it by `config_array` which is part of the plugin `conf` table
-- it will be GC'ed at the same time as `conf` and hence invalidate the
-- compiled templates here as well as the cache-table has weak-keys
template_cache[config_array] = compiled_templates
end
-- Find or compile the specific template
local compiled_template = compiled_templates[source_template]
if not compiled_template then
compiled_template = assert(pl_template.compile(source_template, compile_opts))
compiled_templates[source_template] = compiled_template
end
return compiled_template:render(template_env)
end
local function iter(config_array, template_env)
return function(config_array, i, previous_name, previous_value)
i = i + 1
local current_pair = config_array[i]
if current_pair == nil then -- n + 1
return nil
end
local current_name, current_value = current_pair:match("^([^:]+):*(.-)$")
if current_value == "" then
return i, current_name
end
local res, err = param_value(current_value, config_array, template_env)
if err then
return error("[request-transformer] failed to render the template " ..
current_value .. ", error:" .. err)
end
kong.log.debug("[request-transformer] template `", current_value,
"` rendered to `", res, "`")
return i, current_name, res
end, config_array, 0
end
local function append_value(current_value, value)
local current_value_type = type(current_value)
if current_value_type == "string" then
return { current_value, value }
elseif current_value_type == "table" then
table_insert(current_value, value)
return current_value
else
return { value }
end
end
local function rename(tbl, old_name, new_name)
if old_name == new_name then
return
end
local value = tbl[old_name]
if value then
tbl[old_name] = nil
tbl[new_name] = value
return true
end
end
local function transform_headers(conf, template_env)
local headers = get_headers()
local headers_to_remove = {}
headers.host = nil
-- Remove header(s)
for _, name, value in iter(conf.remove.headers, template_env) do
name = name:lower()
if headers[name] then
headers[name] = nil
headers_to_remove[name] = true
end
end
-- Rename headers(s)
for _, old_name, new_name in iter(conf.rename.headers, template_env) do
local lower_old_name, lower_new_name = old_name:lower(), new_name:lower()
-- headers by default are case-insensitive
-- but if we have a case change, we need to handle it as a special case
local need_remove
if lower_old_name == lower_new_name then
need_remove = rename(headers, old_name, new_name)
else
need_remove = rename(headers, lower_old_name, lower_new_name)
end
if need_remove then
headers_to_remove[old_name] = true
end
end
-- Replace header(s)
for _, name, value in iter(conf.replace.headers, template_env) do
name = name:lower()
if headers[name] or name == HOST then
headers[name] = value
end
end
-- Add header(s)
for _, name, value in iter(conf.add.headers, template_env) do
if not headers[name] and name:lower() ~= HOST then
headers[name] = value
end
end
-- Append header(s)
for _, name, value in iter(conf.append.headers, template_env) do
local name_lc = name:lower()
if name_lc ~= HOST and name ~= name_lc and headers[name] ~= nil then
-- keep original content, use configd case
-- note: the __index method of table returned by ngx.req.get_header
-- is overwritten to check for lower case as well, see documentation
-- for ngx.req.get_header to get more information
-- effectively, it does this: headers[name] = headers[name] or headers[name_lc]
headers[name] = headers[name]
headers[name_lc] = nil
end
headers[name] = append_value(headers[name], value)
end
for name, _ in pairs(headers_to_remove) do
clear_header(name)
end
set_headers(headers)
end
local function transform_querystrings(conf, template_env)
if not (#conf.remove.querystring > 0 or #conf.rename.querystring > 0 or
#conf.replace.querystring > 0 or #conf.add.querystring > 0 or
#conf.append.querystring > 0) then
return
end
local querystring = cycle_aware_deep_copy(template_env.query_params)
-- Remove querystring(s)
for _, name, value in iter(conf.remove.querystring, template_env) do
querystring[name] = nil
end
-- Rename querystring(s)
for _, old_name, new_name in iter(conf.rename.querystring, template_env) do
rename(querystring, old_name, new_name)
end
for _, name, value in iter(conf.replace.querystring, template_env) do
if querystring[name] then
querystring[name] = value
end
end
-- Add querystring(s)
for _, name, value in iter(conf.add.querystring, template_env) do
if not querystring[name] then
querystring[name] = value
end
end
-- Append querystring(s)
for _, name, value in iter(conf.append.querystring, template_env) do
querystring[name] = append_value(querystring[name], value)
end
set_uri_args(querystring)
end
local function transform_json_body(conf, body, content_length, template_env)
local removed, renamed, replaced, added, appended = false, false, false, false, false
local content_length = (body and #body) or 0
local parameters = parse_json(body)
if parameters == nil then
if content_length > 0 then
return false, nil
end
parameters = {}
end
if content_length > 0 and #conf.remove.body > 0 then
for _, name, value in iter(conf.remove.body, template_env) do
parameters[name] = nil
removed = true
end
end
if content_length > 0 and #conf.rename.body > 0 then
for _, old_name, new_name in iter(conf.rename.body, template_env) do
renamed = rename(parameters, old_name, new_name) or renamed
end
end
if content_length > 0 and #conf.replace.body > 0 then
for _, name, value in iter(conf.replace.body, template_env) do
if parameters[name] then
parameters[name] = value
replaced = true
end
end
end
if #conf.add.body > 0 then
for _, name, value in iter(conf.add.body, template_env) do
if not parameters[name] then
parameters[name] = value
added = true
end
end
end
if #conf.append.body > 0 then
for _, name, value in iter(conf.append.body, template_env) do
local old_value = parameters[name]
parameters[name] = append_value(old_value, value)
appended = true
end
end
if removed or renamed or replaced or added or appended then
return true, assert(cjson.encode(parameters))
end
end
local function transform_url_encoded_body(conf, body, content_length, template_env)
local renamed, removed, replaced, added, appended = false, false, false, false, false
local parameters = decode_args(body)
if content_length > 0 and #conf.remove.body > 0 then
for _, name, value in iter(conf.remove.body, template_env) do
parameters[name] = nil
removed = true
end
end
if content_length > 0 and #conf.rename.body > 0 then
for _, old_name, new_name in iter(conf.rename.body, template_env) do
renamed = rename(parameters, old_name, new_name) or renamed
end
end
if content_length > 0 and #conf.replace.body > 0 then
for _, name, value in iter(conf.replace.body, template_env) do
if parameters[name] then
parameters[name] = value
replaced = true
end
end
end
if #conf.add.body > 0 then
for _, name, value in iter(conf.add.body, template_env) do
if parameters[name] == nil then
parameters[name] = value
added = true
end
end
end
if #conf.append.body > 0 then
for _, name, value in iter(conf.append.body, template_env) do
local old_value = parameters[name]
parameters[name] = append_value(old_value, value)
appended = true
end
end
if removed or renamed or replaced or added or appended then
return true, encode_args(parameters)
end
end
local function transform_multipart_body(conf, body, content_length, content_type_value, template_env)
local removed, renamed, replaced, added, appended = false, false, false, false, false
local parameters = multipart(body and body or "", content_type_value)
if content_length > 0 and #conf.rename.body > 0 then
for _, old_name, new_name in iter(conf.rename.body, template_env) do
local para = parameters:get(old_name)
if para and old_name ~= new_name then
local value = para.value
parameters:set_simple(new_name, value)
parameters:delete(old_name)
renamed = true
end
end
end
if content_length > 0 and #conf.remove.body > 0 then
for _, name, value in iter(conf.remove.body, template_env) do
parameters:delete(name)
removed = true
end
end
if content_length > 0 and #conf.replace.body > 0 then
for _, name, value in iter(conf.replace.body, template_env) do
if parameters:get(name) then
parameters:delete(name)
parameters:set_simple(name, value)
replaced = true
end
end
end
if #conf.add.body > 0 then
for _, name, value in iter(conf.add.body, template_env) do
if not parameters:get(name) then
parameters:set_simple(name, value)
added = true
end
end
end
if removed or renamed or replaced or added or appended then
return true, parameters:tostring()
end
end
local function transform_body(conf, template_env)
local content_type_value = get_header(CONTENT_TYPE)
local content_type = get_content_type(content_type_value)
if content_type == nil or #conf.rename.body < 1 and
#conf.remove.body < 1 and #conf.replace.body < 1 and
#conf.add.body < 1 and #conf.append.body < 1 then
return
end
-- Call req_read_body to read the request body first
local body, err = get_raw_body()
if err then
kong.log.warn(err)
end
local is_body_transformed = false
local content_length = (body and #body) or 0
if content_type == ENCODED then
is_body_transformed, body = transform_url_encoded_body(conf, body, content_length, template_env)
elseif content_type == MULTI then
is_body_transformed, body = transform_multipart_body(conf, body, content_length, content_type_value, template_env)
elseif content_type == JSON then
is_body_transformed, body = transform_json_body(conf, body, content_length, template_env)
end
if is_body_transformed then
set_raw_body(body)
set_header(CONTENT_LENGTH, #body)
end
end
local function transform_method(conf)
if conf.http_method then
set_method(conf.http_method:upper())
if conf.http_method == "GET" or conf.http_method == "HEAD" or conf.http_method == "TRACE" then
local content_type_value = get_header(CONTENT_TYPE)
local content_type = get_content_type(content_type_value)
if content_type == ENCODED then
-- Also put the body into querystring
local body = get_raw_body()
local parameters = decode_args(body)
-- Append to querystring
if type(parameters) == "table" and next(parameters) then
local querystring = get_uri_args()
for name, value in pairs(parameters) do
if querystring[name] then
if type(querystring[name]) == "table" then
append_value(querystring[name], value)
else
querystring[name] = { querystring[name], value }
end
else
querystring[name] = value
end
end
set_uri_args(querystring)
end
end
end
end
end
local function transform_uri(conf, template_env)
if conf.replace.uri then
local res, err = param_value(conf.replace.uri, conf.replace, template_env)
if err then
error("[request-transformer] failed to render the template " ..
tostring(conf.replace.uri) .. ", error:" .. err)
end
kong.log.debug(DEBUG, "[request-transformer] template `", conf.replace.uri,
"` rendered to `", res, "`")
if res then
set_path(res)
end
end
end
function _M.execute(conf)
-- meta table for the sandbox, exposing lazily loaded values
local __meta_environment = {
__index = function(self, key)
local lazy_loaders = {
headers = function(self)
return get_headers() or EMPTY
end,
query_params = function(self)
return get_uri_args() or EMPTY
end,
uri_captures = function(self)
return (ngx.ctx.router_matches or EMPTY).uri_captures or EMPTY
end,
shared = function(self)
return ((kong or EMPTY).ctx or EMPTY).shared or EMPTY
end,
}
local loader = lazy_loaders[key]
if not loader then
if lua_enabled and not sandbox_enabled then
return _G[key]
end
return
end
-- set the result on the table to not load again
local value = loader()
rawset(self, key, value)
return value
end,
__newindex = function(self)
error("This environment is read-only.")
end,
}
local template_env = {}
if lua_enabled and sandbox_enabled then
-- load the sandbox environment to be used to render the template
template_env = cycle_aware_deep_copy(sandbox.configuration.environment)
-- here we can optionally add functions to expose to the sandbox, eg:
-- tostring = tostring,
-- because headers may contain array elements such as duplicated headers
-- type is a useful function in these cases. See issue #25.
template_env.type = type
end
setmetatable(template_env, __meta_environment)
transform_uri(conf, template_env)
transform_method(conf)
transform_headers(conf, template_env)
transform_body(conf, template_env)
transform_querystrings(conf, template_env)
end
return _M