Skip to content
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

Merged
merged 10 commits into from
Dec 28, 2016
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions kong-0.9.5-0.rockspec
Original file line number Diff line number Diff line change
Expand Up @@ -264,5 +264,9 @@ build = {
["kong.plugins.bot-detection.rules"] = "kong/plugins/bot-detection/rules.lua",
["kong.plugins.bot-detection.cache"] = "kong/plugins/bot-detection/cache.lua",
["kong.plugins.bot-detection.hooks"] = "kong/plugins/bot-detection/hooks.lua",

["kong.plugins.aws-lambda.handler"] = "kong/plugins/aws-lambda/handler.lua",
["kong.plugins.aws-lambda.schema"] = "kong/plugins/aws-lambda/schema.lua",
["kong.plugins.aws-lambda.v4"] = "kong/plugins/aws-lambda/v4.lua",
}
}
3 changes: 2 additions & 1 deletion kong/constants.lua
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ local plugins = {
"file-log", "http-log", "key-auth", "hmac-auth", "basic-auth", "ip-restriction",
"galileo", "request-transformer", "response-transformer",
"request-size-limiting", "rate-limiting", "response-ratelimiting", "syslog",
"loggly", "datadog", "runscope", "ldap-auth", "statsd", "bot-detection"
"loggly", "datadog", "runscope", "ldap-auth", "statsd", "bot-detection",
"aws-lambda"
}

local plugin_map = {}
Expand Down
113 changes: 113 additions & 0 deletions kong/plugins/aws-lambda/handler.lua
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
Copy link
Member

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

17 changes: 17 additions & 0 deletions kong/plugins/aws-lambda/schema.lua
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"}},
Copy link
Member

Choose a reason for hiding this comment

The 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

Copy link
Member Author

Choose a reason for hiding this comment

The 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"}}
}
}
228 changes: 228 additions & 0 deletions kong/plugins/aws-lambda/v4.lua
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

from their looks the above two functions do the same thing; sha256

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean update and final ?

Copy link
Member

Choose a reason for hiding this comment

The 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this code style breaks with current style; local hex_encode do. imo drop the outer scope

Copy link
Member Author

Choose a reason for hiding this comment

The 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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this required? sorting? seems expensive

Copy link
Member Author

Choose a reason for hiding this comment

The 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()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

using lower here, while just above we're using upper on a first glance not efficient

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see the upper.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

line 144

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's line 140 now, where it is commented as -- convert to standard header title case

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
Copy link
Member

Choose a reason for hiding this comment

The 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

Loading