Skip to content

Commit

Permalink
feat(plugins) AWS Lambda (#1777)
Browse files Browse the repository at this point in the history
  • Loading branch information
subnetmarco authored Dec 28, 2016
1 parent b36ffcb commit ef52403
Show file tree
Hide file tree
Showing 7 changed files with 539 additions and 5 deletions.
4 changes: 4 additions & 0 deletions kong-0.9.7-0.rockspec
Original file line number Diff line number Diff line change
Expand Up @@ -268,5 +268,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
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"}},
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

local function hex_encode(str) -- From prosody's util.hex
return (str:gsub(".", CHAR_TO_HEX))
end

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)
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()
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
Loading

0 comments on commit ef52403

Please sign in to comment.