-
Notifications
You must be signed in to change notification settings - Fork 4.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(plugins) AWS Lambda plugin #1777
Changes from 4 commits
200c54c
9351760
0c7c193
ce647bb
2a52469
5cec020
86fffbe
44b3eb0
c49519c
b1bf789
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
-- Copyright (C) Mashape, Inc. | ||
|
||
local BasePlugin = require "kong.plugins.base_plugin" | ||
local aws_v4 = require "kong.plugins.aws-lambda.v4" | ||
local responses = require "kong.tools.responses" | ||
local utils = require "kong.tools.utils" | ||
local Multipart = require "multipart" | ||
local http = require "resty.http" | ||
local cjson = require "cjson.safe" | ||
|
||
local string_find = string.find | ||
local ngx_req_get_headers = ngx.req.get_headers | ||
local ngx_req_read_body = ngx.req.read_body | ||
local ngx_req_get_post_args = ngx.req.get_post_args | ||
local ngx_req_get_uri_args = ngx.req.get_uri_args | ||
local ngx_req_get_body_data = ngx.req.get_body_data | ||
|
||
local CONTENT_TYPE = "content-type" | ||
|
||
local AWSLambdaHandler = BasePlugin:extend() | ||
|
||
function AWSLambdaHandler:new() | ||
AWSLambdaHandler.super.new(self, "aws-lambda") | ||
end | ||
|
||
local function retrieve_parameters() | ||
ngx_req_read_body() | ||
local body_parameters, err | ||
local content_type = ngx_req_get_headers()[CONTENT_TYPE] | ||
if content_type and string_find(content_type:lower(), "multipart/form-data", nil, true) then | ||
body_parameters = Multipart(ngx_req_get_body_data(), content_type):get_all() | ||
elseif content_type and string_find(content_type:lower(), "application/json", nil, true) then | ||
body_parameters, err = cjson.decode(ngx_req_get_body_data()) | ||
if err then | ||
body_parameters = {} | ||
end | ||
else | ||
body_parameters = ngx_req_get_post_args() | ||
end | ||
|
||
return utils.table_merge(ngx_req_get_uri_args(), body_parameters) | ||
end | ||
|
||
function AWSLambdaHandler:access(conf) | ||
AWSLambdaHandler.super.access(self) | ||
|
||
local bodyJson = cjson.encode(retrieve_parameters()) | ||
|
||
local host = string.format("lambda.%s.amazonaws.com", conf.aws_region) | ||
local path = string.format("/2015-03-31/functions/%s/invocations", | ||
conf.function_name) | ||
|
||
local opts = { | ||
region = conf.aws_region, | ||
service = "lambda", | ||
method = "POST", | ||
headers = { | ||
["X-Amz-Target"] = "invoke", | ||
["X-Amz-Invocation-Type"] = conf.invocation_type, | ||
["X-Amx-Log-Type"] = conf.log_type, | ||
["Content-Type"] = "application/x-amz-json-1.1", | ||
["Content-Length"] = tostring(#bodyJson) | ||
}, | ||
body = bodyJson, | ||
path = path, | ||
access_key = conf.aws_key, | ||
secret_key = conf.aws_secret, | ||
query = conf.qualifier and "Qualifier="..conf.qualifier | ||
} | ||
|
||
local request, err = aws_v4(opts) | ||
if err then | ||
return responses.send_HTTP_INTERNAL_SERVER_ERROR(err) | ||
end | ||
|
||
-- Trigger request | ||
local client = http.new() | ||
client:connect(host, 443) | ||
client:set_timeout(conf.timeout) | ||
local ok, err = client:ssl_handshake() | ||
if not ok then | ||
return responses.send_HTTP_INTERNAL_SERVER_ERROR(err) | ||
end | ||
|
||
local res, err = client:request { | ||
method = "POST", | ||
path = request.url, | ||
body = request.body, | ||
headers = request.headers | ||
} | ||
if not res then | ||
return responses.send_HTTP_INTERNAL_SERVER_ERROR(err) | ||
end | ||
|
||
local status = res.status | ||
local body = res:read_body() | ||
local headers = res.headers | ||
|
||
local ok, err = client:set_keepalive(conf.keepalive) | ||
if not ok then | ||
return responses.send_HTTP_INTERNAL_SERVER_ERROR(err) | ||
end | ||
|
||
-- Send response to client | ||
for k, v in pairs(headers) do | ||
ngx.header[k] = v | ||
end | ||
responses.send(status, body, headers, true) | ||
end | ||
|
||
AWSLambdaHandler.PRIORITY = 750 | ||
|
||
return AWSLambdaHandler | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
return { | ||
fields = { | ||
timeout = {type = "number", default = 60000, required = true }, | ||
keepalive = {type = "number", default = 60000, required = true }, | ||
aws_key = {type = "string", required = true}, | ||
aws_secret = {type = "string", required = true}, | ||
aws_region = {type = "string", required = true, enum = { | ||
"us-east-1", "us-east-2", "ap-northeast-1", "ap-northeast-2", | ||
"ap-southeast-1", "ap-southeast-2", "eu-central-1", "eu-west-1"}}, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do we want those to be literals? requires maintenance when amazon decides to change things There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. for now I would keep it like this - we can always change it in the future |
||
function_name = {type="string", required = true}, | ||
qualifier = {type = "string"}, | ||
invocation_type = {type = "string", required = true, default = "RequestResponse", | ||
enum = {"RequestResponse", "Event", "DryRun"}}, | ||
log_type = {type = "string", required = true, default = "Tail", | ||
enum = {"Tail", "None"}} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,228 @@ | ||
-- Performs AWSv4 Signing | ||
-- http://docs.aws.amazon.com/general/latest/gr/sigv4_signing.html | ||
|
||
local resty_sha256 = require "resty.sha256" | ||
local pl_string = require "pl.stringx" | ||
local crypto = require "crypto" | ||
|
||
local ALGORITHM = "AWS4-HMAC-SHA256" | ||
|
||
local CHAR_TO_HEX = {}; | ||
for i = 0, 255 do | ||
local char = string.char(i) | ||
local hex = string.format("%02x", i) | ||
CHAR_TO_HEX[char] = hex | ||
end | ||
|
||
local function hmac(key, msg) | ||
return crypto.hmac.digest("sha256", msg, key, true) | ||
end | ||
|
||
local function hash(str) | ||
local sha256 = resty_sha256:new() | ||
sha256:update(str) | ||
return sha256:final() | ||
end | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. from their looks the above two functions do the same thing; There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You mean There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nevermind, they are different |
||
|
||
local function hex_encode(str) -- From prosody's util.hex | ||
return (str:gsub(".", CHAR_TO_HEX)) | ||
end | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this code style breaks with current style; There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done |
||
|
||
local function percent_encode(char) | ||
return string.format("%%%02X", string.byte(char)) | ||
end | ||
|
||
local function urldecode(str) | ||
return (str:gsub("%%(%x%x)", function(c) | ||
return string.char(tonumber(c, 16)) | ||
end)) | ||
end | ||
|
||
local function canonicalise_path(path) | ||
local segments = {} | ||
for segment in path:gmatch("/([^/]*)") do | ||
if segment == "" or segment == "." then | ||
segments = segments -- do nothing and avoid lint | ||
elseif segment == ".." then | ||
-- intentionally discards components at top level | ||
segments[#segments] = nil | ||
else | ||
segments[#segments+1] = urldecode(segment):gsub("[^%w%-%._~]", percent_encode) | ||
end | ||
end | ||
local len = #segments | ||
if len == 0 then return "/" end | ||
-- If there was a slash on the end, keep it there. | ||
if path:sub(-1, -1) == "/" then | ||
len = len + 1 | ||
segments[len] = "" | ||
end | ||
segments[0] = "" | ||
segments = table.concat(segments, "/", 0, len) | ||
return segments | ||
end | ||
|
||
local function canonicalise_query_string(query) | ||
local q = {} | ||
for key, val in query:gmatch("([^&=]+)=?([^&]*)") do | ||
key = urldecode(key):gsub("[^%w%-%._~]", percent_encode) | ||
val = urldecode(val):gsub("[^%w%-%._~]", percent_encode) | ||
q[#q+1] = key .. "=" .. val | ||
end | ||
table.sort(q) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is this required? sorting? seems expensive There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. required |
||
return table.concat(q, "&") | ||
end | ||
|
||
local function derive_signing_key(kSecret, date, region, service) | ||
local kDate = hmac("AWS4" .. kSecret, date) | ||
local kRegion = hmac(kDate, region) | ||
local kService = hmac(kRegion, service) | ||
local kSigning = hmac(kService, "aws4_request") | ||
return kSigning | ||
end | ||
|
||
local function prepare_awsv4_request(tbl) | ||
local domain = tbl.domain or "amazonaws.com" | ||
local region = tbl.region | ||
local service = tbl.service | ||
local request_method = tbl.method | ||
local canonicalURI = tbl.canonicalURI | ||
local path = tbl.path | ||
if path and not canonicalURI then | ||
canonicalURI = canonicalise_path(path) | ||
elseif canonicalURI == nil or canonicalURI == "" then | ||
canonicalURI = "/" | ||
end | ||
local canonical_querystring = tbl.canonical_querystring | ||
local query = tbl.query | ||
if query and not canonical_querystring then | ||
canonical_querystring = canonicalise_query_string(query) | ||
end | ||
local req_headers = tbl.headers or {} | ||
local req_payload = tbl.body | ||
local access_key = tbl.access_key | ||
local signing_key = tbl.signing_key | ||
local secret_key | ||
if not signing_key then | ||
secret_key = tbl.secret_key | ||
if secret_key == nil then | ||
return nil, "either 'signing_key' or 'secret_key' must be provided" | ||
end | ||
end | ||
local timestamp = tbl.timestamp or os.time() | ||
local tls = tbl.tls | ||
if tls == nil then tls = true end | ||
local port = tbl.port or (tls and 443 or 80) | ||
local req_date = os.date("!%Y%m%dT%H%M%SZ", timestamp) | ||
local date = os.date("!%Y%m%d", timestamp) | ||
|
||
local host = service .. "." .. region .. "." .. domain | ||
local host_header do -- If the "standard" port is not in use, the port should be added to the Host header | ||
local with_port | ||
if tls then | ||
with_port = port ~= 443 | ||
else | ||
with_port = port ~= 80 | ||
end | ||
if with_port then | ||
host_header = string.format("%s:%d", host, port) | ||
else | ||
host_header = host | ||
end | ||
end | ||
|
||
local headers = { | ||
["X-Amz-Date"] = req_date; | ||
Host = host_header; | ||
} | ||
local add_auth_header = true | ||
for k, v in pairs(req_headers) do | ||
k = k:gsub("%f[^%z-]%w", string.upper) -- convert to standard header title case | ||
if k == "Authorization" then | ||
add_auth_header = false | ||
elseif v == false then -- don't allow a default value for this header | ||
v = nil | ||
end | ||
headers[k] = v | ||
end | ||
|
||
-- Task 1: Create a Canonical Request For Signature Version 4 | ||
-- http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html | ||
local canonical_headers, signed_headers do | ||
-- We structure this code in a way so that we only have to sort once. | ||
canonical_headers, signed_headers = {}, {} | ||
local i = 0 | ||
for name, value in pairs(headers) do | ||
if value then -- ignore headers with 'false', they are used to override defaults | ||
i = i + 1 | ||
local name_lower = name:lower() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't see the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. line 144 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's line 140 now, where it is commented as |
||
signed_headers[i] = name_lower | ||
if canonical_headers[name_lower] ~= nil then | ||
return nil, "header collision" | ||
end | ||
canonical_headers[name_lower] = pl_string.strip(value) | ||
end | ||
end | ||
table.sort(signed_headers) | ||
for j=1, i do | ||
local name = signed_headers[j] | ||
local value = canonical_headers[name] | ||
canonical_headers[j] = name .. ":" .. value .. "\n" | ||
end | ||
signed_headers = table.concat(signed_headers, ";", 1, i) | ||
canonical_headers = table.concat(canonical_headers, nil, 1, i) | ||
end | ||
local canonical_request = | ||
request_method .. '\n' .. | ||
canonicalURI .. '\n' .. | ||
(canonical_querystring or "") .. '\n' .. | ||
canonical_headers .. '\n' .. | ||
signed_headers .. '\n' .. | ||
hex_encode(hash(req_payload or "")) | ||
|
||
local hashed_canonical_request = hex_encode(hash(canonical_request)) | ||
-- Task 2: Create a String to Sign for Signature Version 4 | ||
-- http://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html | ||
local credential_scope = date .. "/" .. region .. "/" .. service .. "/aws4_request" | ||
local string_to_sign = | ||
ALGORITHM .. '\n' .. | ||
req_date .. '\n' .. | ||
credential_scope .. '\n' .. | ||
hashed_canonical_request | ||
|
||
-- Task 3: Calculate the AWS Signature Version 4 | ||
-- http://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html | ||
if signing_key == nil then | ||
signing_key = derive_signing_key(secret_key, date, region, service) | ||
end | ||
local signature = hex_encode(hmac(signing_key, string_to_sign)) | ||
-- Task 4: Add the Signing Information to the Request | ||
-- http://docs.aws.amazon.com/general/latest/gr/sigv4-add-signature-to-request.html | ||
local authorization = ALGORITHM | ||
.. " Credential=" .. access_key .."/" .. credential_scope | ||
.. ", SignedHeaders=" .. signed_headers | ||
.. ", Signature=" .. signature | ||
if add_auth_header then | ||
headers.Authorization = authorization | ||
end | ||
|
||
local target = path or canonicalURI | ||
if query or canonical_querystring then | ||
target = target .. "?" .. (query or canonical_querystring) | ||
end | ||
local scheme = tls and "https" or "http" | ||
local url = scheme .. "://" .. host_header .. target | ||
|
||
return { | ||
url = url, | ||
host = host, | ||
port = port, | ||
tls = tls, | ||
method = request_method, | ||
target = target, | ||
headers = headers, | ||
body = req_payload, | ||
} | ||
end | ||
|
||
return prepare_awsv4_request | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. general; lots of locals to cache; from string and table, and then some |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Merge the
access.lua
file with this one, no use in multiple files here