lamby_updated 5.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,137 @@
1
+ module Lamby
2
+ class Handler
3
+
4
+ class << self
5
+
6
+ def call(app, event, context, options = {})
7
+ Lamby::ColdStartMetrics.instrument! if Lamby.config.cold_start_metrics?
8
+ new(app, event, context, options).call.response
9
+ end
10
+
11
+ end
12
+
13
+ def initialize(app, event, context, options = {})
14
+ @app = app
15
+ @event = event
16
+ @context = context
17
+ @options = options
18
+ end
19
+
20
+ def response
21
+ @response
22
+ end
23
+
24
+ def status
25
+ @status
26
+ end
27
+
28
+ def headers
29
+ @headers
30
+ end
31
+
32
+ def set_cookies
33
+ return @set_cookies if defined?(@set_cookies)
34
+ @set_cookies = if @headers && @headers['Set-Cookie']
35
+ @headers.delete('Set-Cookie').split("\n")
36
+ end
37
+ end
38
+
39
+ def body
40
+ @rbody ||= ''.tap do |rbody|
41
+ @body.each { |part| rbody << part if part }
42
+ @body.close if @body.respond_to? :close
43
+ end
44
+ end
45
+
46
+ def call
47
+ @response ||= call_app
48
+ self
49
+ end
50
+
51
+ def base64_encodeable?(hdrs = @headers)
52
+ hdrs && (
53
+ hdrs['Content-Transfer-Encoding'] == 'binary' ||
54
+ content_encoding_compressed?(hdrs) ||
55
+ hdrs['X-Lamby-Base64'] == '1'
56
+ )
57
+ end
58
+
59
+ def body64
60
+ Base64.strict_encode64(body)
61
+ end
62
+
63
+ private
64
+
65
+ def rack
66
+ return @rack if defined?(@rack)
67
+ @rack = begin
68
+ type = rack_option
69
+ klass = Lamby::Rack.lookup type, @event
70
+ (klass && klass.handle?(@event)) ? klass.new(@event, @context) : false
71
+ end
72
+ end
73
+
74
+ def rack_option
75
+ return if ENV['LAMBY_TEST_DYNAMIC_HANDLER']
76
+ @options[:rack]
77
+ end
78
+
79
+ def rack_response
80
+ { statusCode: status,
81
+ headers: headers,
82
+ body: body }.merge(rack.response(self))
83
+ end
84
+
85
+
86
+ def call_app
87
+ retries ||= 0
88
+ if Debug.on?(@event)
89
+ Debug.call @event, @context, rack.env
90
+ elsif rack?
91
+ @status, @headers, @body = @app.call rack.env
92
+ set_cookies
93
+ rack_response
94
+ elsif lambdakiq?
95
+ Lambdakiq.cmd event: @event, context: @context
96
+ elsif lambda_cable?
97
+ LambdaCable.cmd event: @event, context: @context
98
+ elsif LambdaConsole.handle?(@event)
99
+ LambdaConsole.handle(@event)
100
+ elsif event_bridge?
101
+ Lamby.config.event_bridge_handler.call @event, @context
102
+ else
103
+ [404, {}, StringIO.new('')]
104
+ end
105
+ rescue ActiveRecord::ConnectionNotEstablished => e
106
+ retries += 1
107
+ if retries < 3
108
+ sleep(2 ** retries) # Exponential backoff
109
+ retry
110
+ else
111
+ raise e
112
+ end
113
+ end
114
+
115
+ def content_encoding_compressed?(hdrs)
116
+ content_encoding_header = hdrs['Content-Encoding'] || ''
117
+ content_encoding_header.split(', ').any? { |h| ['br', 'gzip'].include?(h) }
118
+ end
119
+
120
+ def rack?
121
+ rack
122
+ end
123
+
124
+ def event_bridge?
125
+ Lamby.config.event_bridge_handler &&
126
+ @event.key?('source') && @event.key?('detail') && @event.key?('detail-type')
127
+ end
128
+
129
+ def lambdakiq?
130
+ defined?(::Lambdakiq) && ::Lambdakiq.jobs?(@event)
131
+ end
132
+
133
+ def lambda_cable?
134
+ defined?(::LambdaCable) && ::LambdaCable.handle?(@event, @context)
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,18 @@
1
+ require 'logger'
2
+
3
+ if ENV['AWS_EXECUTION_ENV']
4
+ ENV['RAILS_LOG_TO_STDOUT'] = '1'
5
+
6
+ module Lamby
7
+ module Logger
8
+
9
+ def initialize(*args, **kwargs)
10
+ args[0] = STDOUT
11
+ super(*args, **kwargs)
12
+ end
13
+
14
+ end
15
+ end
16
+
17
+ Logger.prepend Lamby::Logger unless ENV['LAMBY_TEST']
18
+ end
@@ -0,0 +1,25 @@
1
+ module Lamby
2
+ # This class is used by the `lamby:proxy_server` Rake task to run a
3
+ # Rack server for local development proxy. Specifically, this class
4
+ # accepts a JSON respresentation of a Lambda context object converted
5
+ # to a Hash as the single arugment.
6
+ #
7
+ class ProxyContext
8
+ def initialize(data)
9
+ @data = data
10
+ end
11
+
12
+ def method_missing(method_name, *args, &block)
13
+ key = method_name.to_s
14
+ if @data.key?(key)
15
+ @data[key]
16
+ else
17
+ super
18
+ end
19
+ end
20
+
21
+ def respond_to_missing?(method_name, include_private = false)
22
+ @data.key?(method_name.to_s) || super
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,36 @@
1
+ module Lamby
2
+ class ProxyServer
3
+
4
+ METHOD_NOT_ALLOWED = <<-HEREDOC.strip
5
+ <h1>Method Not Allowed</h1>
6
+ <p>Please POST to this endpoint with an application/json content type and JSON payload of your Lambda's event and context.<p>
7
+ <p>Example: <code>{ "event": event, "context": context }</code></p>
8
+ HEREDOC
9
+
10
+ def call(env)
11
+ return method_not_allowed unless method_allowed?(env)
12
+ event, context = event_and_context(env)
13
+ lambda_to_rack Lamby.cmd(event: event, context: context)
14
+ end
15
+
16
+ private
17
+
18
+ def event_and_context(env)
19
+ data = env['rack.input'].dup.read
20
+ json = JSON.parse(data)
21
+ [ json['event'], Lamby::ProxyContext.new(json['context']) ]
22
+ end
23
+
24
+ def method_allowed?(env)
25
+ env['REQUEST_METHOD'] == 'POST' && env['CONTENT_TYPE'] == 'application/json'
26
+ end
27
+
28
+ def method_not_allowed
29
+ [405, {"Content-Type" => "text/html"}, [ METHOD_NOT_ALLOWED.dup ]]
30
+ end
31
+
32
+ def lambda_to_rack(response)
33
+ [ 200, {"Content-Type" => "application/json"}, [ response.to_json ] ]
34
+ end
35
+ end
36
+ end
data/lib/lamby/rack.rb ADDED
@@ -0,0 +1,115 @@
1
+ module Lamby
2
+ class Rack
3
+ LAMBDA_EVENT = 'lambda.event'.freeze
4
+ LAMBDA_CONTEXT = 'lambda.context'.freeze
5
+ HTTP_X_REQUESTID = 'HTTP_X_REQUEST_ID'.freeze
6
+ HTTP_X_REQUEST_START = 'HTTP_X_REQUEST_START'.freeze
7
+ HTTP_COOKIE = 'HTTP_COOKIE'.freeze
8
+
9
+ class << self
10
+
11
+ def lookup(type, event)
12
+ types[type] || types.values.detect { |t| t.handle?(event) }
13
+ end
14
+
15
+ # Order is important. REST is hardest to isolated with handle? method.
16
+ def types
17
+ { alb: RackAlb,
18
+ http: RackHttp,
19
+ rest: RackRest,
20
+ api: RackRest }
21
+ end
22
+
23
+ end
24
+
25
+ attr_reader :event, :context
26
+
27
+ def initialize(event, context)
28
+ @event = event
29
+ @context = context
30
+ end
31
+
32
+ def env
33
+ @env ||= env_base.merge!(env_headers)
34
+ end
35
+
36
+ def response(_handler)
37
+ {}
38
+ end
39
+
40
+ def multi_value?
41
+ false
42
+ end
43
+
44
+ private
45
+
46
+ def env_base
47
+ raise NotImplementedError
48
+ end
49
+
50
+ def env_headers
51
+ headers.transform_keys do |key|
52
+ "HTTP_#{key.to_s.upcase.tr '-', '_'}"
53
+ end.tap do |hdrs|
54
+ hdrs[HTTP_X_REQUESTID] = request_id
55
+ hdrs[HTTP_X_REQUEST_START] = "t=#{request_start}" if request_start
56
+ end
57
+ end
58
+
59
+ def content_type
60
+ headers.delete('Content-Type') || headers.delete('content-type') || headers.delete('CONTENT_TYPE')
61
+ end
62
+
63
+ def content_length
64
+ bytesize = body.bytesize.to_s if body
65
+ headers.delete('Content-Length') || headers.delete('content-length') || headers.delete('CONTENT_LENGTH') || bytesize
66
+ end
67
+
68
+ def body
69
+ @body ||= if event['body'] && base64_encoded?
70
+ Base64.decode64 event['body']
71
+ else
72
+ event['body']
73
+ end
74
+ end
75
+
76
+ def headers
77
+ @headers ||= event['headers'] || {}
78
+ end
79
+
80
+ def query_string
81
+ @query_string ||= if event.key?('rawQueryString')
82
+ event['rawQueryString']
83
+ elsif event.key?('multiValueQueryStringParameters')
84
+ query = event['multiValueQueryStringParameters'] || {}
85
+ query.map do |key, value|
86
+ value.map{ |v| "#{key}=#{v}" }.join('&')
87
+ end.flatten.join('&')
88
+ else
89
+ build_query_string
90
+ end
91
+ end
92
+
93
+ def build_query_string
94
+ return if event['queryStringParameters'].nil?
95
+ ::Rack::Utils.build_nested_query(
96
+ event.fetch('queryStringParameters')
97
+ ).gsub('[', '%5B')
98
+ .gsub(']', '%5D')
99
+ end
100
+
101
+ def base64_encoded?
102
+ event['isBase64Encoded']
103
+ end
104
+
105
+ def request_id
106
+ context.aws_request_id
107
+ end
108
+
109
+ def request_start
110
+ event.dig('requestContext', 'timeEpoch') ||
111
+ event.dig('requestContext', 'requestTimeEpoch')
112
+ end
113
+
114
+ end
115
+ end
@@ -0,0 +1,82 @@
1
+ module Lamby
2
+ class RackAlb < Lamby::Rack
3
+
4
+ class << self
5
+
6
+ def handle?(event)
7
+ event.key?('httpMethod') &&
8
+ event.dig('requestContext', 'elb')
9
+ end
10
+
11
+ end
12
+
13
+ def alb?
14
+ true
15
+ end
16
+
17
+ def multi_value?
18
+ event.key? 'multiValueHeaders'
19
+ end
20
+
21
+ def response(handler)
22
+ hhdrs = handler.headers
23
+ if multi_value?
24
+ multivalue_headers = hhdrs.transform_values { |v| Array[v].compact.flatten }
25
+ multivalue_headers['Set-Cookie'] = handler.set_cookies if handler.set_cookies
26
+ end
27
+ status_description = "#{handler.status} #{::Rack::Utils::HTTP_STATUS_CODES[handler.status]}"
28
+ base64_encode = handler.base64_encodeable?(hhdrs)
29
+ body = Base64.strict_encode64(handler.body) if base64_encode
30
+ { multiValueHeaders: multivalue_headers,
31
+ statusDescription: status_description,
32
+ isBase64Encoded: base64_encode,
33
+ body: body }.compact
34
+ end
35
+
36
+ private
37
+
38
+ def env_base
39
+ { ::Rack::REQUEST_METHOD => event['httpMethod'],
40
+ ::Rack::SCRIPT_NAME => '',
41
+ ::Rack::PATH_INFO => event['path'] || '',
42
+ ::Rack::QUERY_STRING => query_string,
43
+ ::Rack::SERVER_NAME => headers['host'],
44
+ ::Rack::SERVER_PORT => headers['x-forwarded-port'],
45
+ ::Rack::SERVER_PROTOCOL => 'HTTP/1.1',
46
+ ::Rack::RACK_VERSION => ::Rack::VERSION,
47
+ ::Rack::RACK_URL_SCHEME => headers['x-forwarded-proto'],
48
+ ::Rack::RACK_INPUT => StringIO.new(body || ''),
49
+ ::Rack::RACK_ERRORS => $stderr,
50
+ ::Rack::RACK_MULTITHREAD => false,
51
+ ::Rack::RACK_MULTIPROCESS => false,
52
+ ::Rack::RACK_RUNONCE => false,
53
+ LAMBDA_EVENT => event,
54
+ LAMBDA_CONTEXT => context
55
+ }.tap do |env|
56
+ ct = content_type
57
+ cl = content_length
58
+ env['CONTENT_TYPE'] = ct if ct
59
+ env['CONTENT_LENGTH'] = cl if cl
60
+ end
61
+ end
62
+
63
+ def headers
64
+ @headers ||= multi_value? ? headers_multi : super
65
+ end
66
+
67
+ def headers_multi
68
+ Hash[(event['multiValueHeaders'] || {}).map do |k,v|
69
+ if v.is_a?(Array)
70
+ if k == 'x-forwarded-for'
71
+ [k, v.join(', ')]
72
+ else
73
+ [k, v.first]
74
+ end
75
+ else
76
+ [k,v]
77
+ end
78
+ end]
79
+ end
80
+
81
+ end
82
+ end
@@ -0,0 +1,107 @@
1
+ module Lamby
2
+ class RackHttp < Lamby::Rack
3
+
4
+ class << self
5
+
6
+ def handle?(event)
7
+ event.key?('version') &&
8
+ ( event.dig('requestContext', 'http') ||
9
+ event.dig('requestContext', 'httpMethod') )
10
+ end
11
+
12
+ end
13
+
14
+ def response(handler)
15
+ if handler.base64_encodeable?
16
+ { isBase64Encoded: true, body: handler.body64 }
17
+ else
18
+ super
19
+ end.tap do |r|
20
+ if cookies = handler.set_cookies
21
+ if payload_version_one?
22
+ r[:multiValueHeaders] ||= {}
23
+ r[:multiValueHeaders]['Set-Cookie'] = cookies
24
+ else
25
+ r[:cookies] = cookies
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def env_base
34
+ { ::Rack::REQUEST_METHOD => request_method,
35
+ ::Rack::SCRIPT_NAME => '',
36
+ ::Rack::PATH_INFO => path_info,
37
+ ::Rack::QUERY_STRING => query_string,
38
+ ::Rack::SERVER_NAME => server_name,
39
+ ::Rack::SERVER_PORT => server_port,
40
+ ::Rack::SERVER_PROTOCOL => server_protocol,
41
+ ::Rack::RACK_VERSION => ::Rack::VERSION,
42
+ ::Rack::RACK_URL_SCHEME => 'https',
43
+ ::Rack::RACK_INPUT => StringIO.new(body || ''),
44
+ ::Rack::RACK_ERRORS => $stderr,
45
+ ::Rack::RACK_MULTITHREAD => false,
46
+ ::Rack::RACK_MULTIPROCESS => false,
47
+ ::Rack::RACK_RUNONCE => false,
48
+ LAMBDA_EVENT => event,
49
+ LAMBDA_CONTEXT => context
50
+ }.tap do |env|
51
+ ct = content_type
52
+ cl = content_length
53
+ env['CONTENT_TYPE'] = ct if ct
54
+ env['CONTENT_LENGTH'] = cl if cl
55
+ end
56
+ end
57
+
58
+ def env_headers
59
+ super.tap do |hdrs|
60
+ if cookies.any?
61
+ hdrs[HTTP_COOKIE] = cookies.join('; ')
62
+ end
63
+ end
64
+ end
65
+
66
+ def request_method
67
+ event.dig('requestContext', 'http', 'method') || event['httpMethod']
68
+ end
69
+
70
+ def cookies
71
+ event['cookies'] || []
72
+ end
73
+
74
+ # Using custom domain names with v1.0 yields a good `path` parameter sans
75
+ # stage. However, v2.0 and others do not. So we are just going to remove stage
76
+ # no matter waht from other places for both.
77
+ #
78
+ def path_info
79
+ stage = event.dig('requestContext', 'stage')
80
+ spath = event.dig('requestContext', 'http', 'path') || event.dig('requestContext', 'path')
81
+ spath = event['rawPath'] if spath != event['rawPath'] && !payload_version_one?
82
+ spath.sub /\A\/#{stage}/, ''
83
+ end
84
+
85
+ def server_name
86
+ headers['x-forwarded-host'] ||
87
+ headers['X-Forwarded-Host'] ||
88
+ headers['host'] ||
89
+ headers['Host']
90
+ end
91
+
92
+ def server_port
93
+ headers['x-forwarded-port'] || headers['X-Forwarded-Port']
94
+ end
95
+
96
+ def server_protocol
97
+ event.dig('requestContext', 'http', 'protocol') ||
98
+ event.dig('requestContext', 'protocol') ||
99
+ 'HTTP/1.1'
100
+ end
101
+
102
+ def payload_version_one?
103
+ event['version'] == '1.0'
104
+ end
105
+
106
+ end
107
+ end
@@ -0,0 +1,53 @@
1
+ module Lamby
2
+ class RackRest < Lamby::Rack
3
+
4
+ class << self
5
+
6
+ def handle?(event)
7
+ event.key?('httpMethod')
8
+ end
9
+
10
+ end
11
+
12
+ def response(handler)
13
+ if handler.base64_encodeable?
14
+ { isBase64Encoded: true, body: handler.body64 }
15
+ else
16
+ super
17
+ end.tap do |r|
18
+ if cookies = handler.set_cookies
19
+ r[:multiValueHeaders] ||= {}
20
+ r[:multiValueHeaders]['Set-Cookie'] = cookies
21
+ end
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def env_base
28
+ { ::Rack::REQUEST_METHOD => event['httpMethod'],
29
+ ::Rack::SCRIPT_NAME => '',
30
+ ::Rack::PATH_INFO => event['path'] || '',
31
+ ::Rack::QUERY_STRING => query_string,
32
+ ::Rack::SERVER_NAME => headers['Host'],
33
+ ::Rack::SERVER_PORT => headers['X-Forwarded-Port'],
34
+ ::Rack::SERVER_PROTOCOL => event.dig('requestContext', 'protocol') || 'HTTP/1.1',
35
+ ::Rack::RACK_VERSION => ::Rack::VERSION,
36
+ ::Rack::RACK_URL_SCHEME => 'https',
37
+ ::Rack::RACK_INPUT => StringIO.new(body || ''),
38
+ ::Rack::RACK_ERRORS => $stderr,
39
+ ::Rack::RACK_MULTITHREAD => false,
40
+ ::Rack::RACK_MULTIPROCESS => false,
41
+ ::Rack::RACK_RUNONCE => false,
42
+ LAMBDA_EVENT => event,
43
+ LAMBDA_CONTEXT => context
44
+ }.tap do |env|
45
+ ct = content_type
46
+ cl = content_length
47
+ env['CONTENT_TYPE'] = ct if ct
48
+ env['CONTENT_LENGTH'] = cl if cl
49
+ end
50
+ end
51
+
52
+ end
53
+ end
@@ -0,0 +1,9 @@
1
+ module Lamby
2
+ class Railtie < ::Rails::Railtie
3
+ config.lamby = Lamby::Config.config
4
+
5
+ rake_tasks do
6
+ load 'lamby/tasks.rake'
7
+ end
8
+ end
9
+ end