lamby_updated 5.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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