Top Level Namespace
Constant Summary collapse
- TEXT_MIME_TYPES =
[ 'application/json', 'application/javascript', 'application/xml', 'application/vnd.api+json', 'image/svg+xml' ].freeze
Instance Method Summary collapse
- #all_casings(input_string) ⇒ Object
- #base_path ⇒ Object
- #build_environ(event:, context:, headers:, body:) ⇒ Object
- #format_body(body:, headers:, text_mime_types:) ⇒ Object
- #format_grouped_headers(headers:) ⇒ Object
- #format_response(event:, status:, headers:, body:, text_mime_types:) ⇒ Object
- #format_split_headers(headers:) ⇒ Object
- #format_status_description(event:, status:) ⇒ Object
- #handle_request(app:, event:, context:, config: {}) ⇒ Object
- #keepalive_event?(event) ⇒ Boolean
- #parse_body(event) ⇒ Object
- #parse_headers(event) ⇒ Object
- #parse_http_headers(headers) ⇒ Object
- #parse_path_info(event) ⇒ Object
- #parse_query_string(event) ⇒ Object
- #parse_script_name(event, headers) ⇒ Object
- #text_mime_type?(headers:, text_mime_types:) ⇒ Boolean
Instance Method Details
#all_casings(input_string) ⇒ Object
149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 |
# File 'lib/serverless_rack.rb', line 149 def all_casings(input_string) # Permute all casings of a given string. # A pretty algoritm, via @Amber # http://stackoverflow.com/questions/6792803/finding-all-possible-case-permutations-in-python if input_string.empty? yield '' else first = input_string[0] if first.downcase == first.upcase all_casings(input_string[1..-1]) do |sub_casing| yield first + sub_casing end else all_casings(input_string[1..-1]) do |sub_casing| yield first.downcase + sub_casing yield first.upcase + sub_casing end end end end |
#base_path ⇒ Object
18 19 20 |
# File 'lib/serverless_rack.rb', line 18 def base_path "/#{ENV['API_GATEWAY_BASE_PATH']}" unless ENV['API_GATEWAY_BASE_PATH'].to_s.empty? end |
#build_environ(event:, context:, headers:, body:) ⇒ Object
85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 |
# File 'lib/serverless_rack.rb', line 85 def build_environ(event:, context:, headers:, body:) { 'REQUEST_METHOD' => event['httpMethod'], 'SCRIPT_NAME' => parse_script_name(event, headers), 'PATH_INFO' => parse_path_info(event), 'QUERY_STRING' => parse_query_string(event), 'SERVER_NAME' => headers['Host'] || 'lambda', 'SERVER_PORT' => headers['X-Forwarded-Port'] || '80', 'CONTENT_LENGTH' => body.bytesize.to_s, 'CONTENT_TYPE' => headers['Content-Type'] || '', 'SERVER_PROTOCOL' => 'HTTP/1.1', 'REMOTE_ADDR' => (event['requestContext']['identity'] || {})['sourceIp'] || '', 'REMOTE_USER' => (event['requestContext']['authorizer'] || {})['principalId'] || '', 'rack.version' => Rack::VERSION, 'rack.url_scheme' => headers['X-Forwarded-Proto'] || 'http', 'rack.input' => StringIO.new(body), 'rack.errors' => $stderr, 'rack.multithread' => false, 'rack.multiprocess' => false, 'rack.run_once' => false, 'serverless.event' => event, 'serverless.context' => context, 'serverless.authorizer' => event['requestContext']['authorizer'] }.merge(parse_http_headers(headers)) end |
#format_body(body:, headers:, text_mime_types:) ⇒ Object
130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 |
# File 'lib/serverless_rack.rb', line 130 def format_body(body:, headers:, text_mime_types:) response_data = '' body.each { |part| response_data += part } return {} if response_data.empty? if text_mime_type?(headers: headers, text_mime_types: text_mime_types) { 'body' => response_data, 'isBase64Encoded' => false } else { 'body' => Base64.strict_encode64(response_data), 'isBase64Encoded' => true } end end |
#format_grouped_headers(headers:) ⇒ Object
193 194 195 196 197 |
# File 'lib/serverless_rack.rb', line 193 def format_grouped_headers(headers:) { 'multiValueHeaders' => headers.transform_values do |value| value.split("\n") end } end |
#format_response(event:, status:, headers:, body:, text_mime_types:) ⇒ Object
199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 |
# File 'lib/serverless_rack.rb', line 199 def format_response(event:, status:, headers:, body:, text_mime_types:) response = { 'statusCode' => status } if event.include? 'multiValueHeaders' response.merge!(format_grouped_headers(headers: headers)) else response.merge!(format_split_headers(headers: headers)) end response.merge!( format_status_description(event: event, status: status) ) response.merge!( format_body( body: body, headers: headers, text_mime_types: text_mime_types ) ) response end |
#format_split_headers(headers:) ⇒ Object
170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 |
# File 'lib/serverless_rack.rb', line 170 def format_split_headers(headers:) headers = headers.to_hash keys = headers.keys # If there are headers multiple occurrences, e.g. Set-Cookie, create # case-mutated variations in order to pass them through APIGW. # This is a hack that's currently needed. keys.each do |key| values = headers[key].split("\n") next if values.size < 2 headers.delete(key) all_casings(key) do |casing| headers[casing] = values.shift break if values.empty? end end { 'headers' => headers } end |
#format_status_description(event:, status:) ⇒ Object
111 112 113 114 115 116 117 118 |
# File 'lib/serverless_rack.rb', line 111 def format_status_description(event:, status:) return {} unless event['requestContext']['elb'] # If the request comes from ALB we need to add a status description description = Rack::Utils::HTTP_STATUS_CODES[status] { 'statusDescription' => "#{status} #{description}" } end |
#handle_request(app:, event:, context:, config: {}) ⇒ Object
223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 |
# File 'lib/serverless_rack.rb', line 223 def handle_request(app:, event:, context:, config: {}) return {} if keepalive_event?(event) status, headers, body = app.call( build_environ( event: event, context: context, headers: parse_headers(event), body: parse_body(event) ) ) format_response( event: event, status: status, headers: headers, body: body, text_mime_types: TEXT_MIME_TYPES + config['text_mime_types'].to_a ) end |
#keepalive_event?(event) ⇒ Boolean
22 23 24 |
# File 'lib/serverless_rack.rb', line 22 def keepalive_event?(event) ['aws.events', 'serverless-plugin-warmup'].include?(event['source']) end |
#parse_body(event) ⇒ Object
53 54 55 56 57 58 59 |
# File 'lib/serverless_rack.rb', line 53 def parse_body(event) if event['isBase64Encoded'] Base64.decode64(event['body']) else event['body'] || '' end end |
#parse_headers(event) ⇒ Object
61 62 63 64 65 66 67 68 69 70 71 |
# File 'lib/serverless_rack.rb', line 61 def parse_headers(event) if event.include? 'multiValueHeaders' Rack::Utils::HeaderHash.new( (event['multiValueHeaders'] || {}).transform_values do |value| value.join("\n") end ) else Rack::Utils::HeaderHash.new(event['headers'] || {}) end end |
#parse_http_headers(headers) ⇒ Object
73 74 75 76 77 78 79 80 81 82 83 |
# File 'lib/serverless_rack.rb', line 73 def parse_http_headers(headers) headers = headers.map do |key, value| ["HTTP_#{key.upcase.tr('-', '_')}", value] end headers = headers.reject do |key, _value| %w[HTTP_CONTENT_TYPE HTTP_CONTENT_LENGTH].include?(key) end headers.to_h end |
#parse_path_info(event) ⇒ Object
34 35 36 37 38 39 40 41 42 43 |
# File 'lib/serverless_rack.rb', line 34 def parse_path_info(event) # If a user is using a custom domain on API Gateway, they may have a base # path in their URL. This allows us to strip it out via an optional # environment variable. if base_path && event['path'].start_with?(base_path) event['path'][base_path.length..-1] else event['path'] end end |
#parse_query_string(event) ⇒ Object
45 46 47 48 49 50 51 |
# File 'lib/serverless_rack.rb', line 45 def parse_query_string(event) if event.include? 'multiValueQueryStringParameters' Rack::Utils.build_query(event['multiValueQueryStringParameters'] || {}) else Rack::Utils.build_query(event['queryStringParameters'] || {}) end end |
#parse_script_name(event, headers) ⇒ Object
26 27 28 29 30 31 32 |
# File 'lib/serverless_rack.rb', line 26 def parse_script_name(event, headers) if base_path.nil? && (headers['Host'] || '').include?('amazonaws.com') "/#{event['requestContext']['stage']}" else base_path.to_s end end |
#text_mime_type?(headers:, text_mime_types:) ⇒ Boolean
120 121 122 123 124 125 126 127 128 |
# File 'lib/serverless_rack.rb', line 120 def text_mime_type?(headers:, text_mime_types:) mime_type = headers['Content-Type'] || 'text/plain' return false if headers['Content-Encoding'] return true if mime_type.start_with?('text/') return true if text_mime_types.include?(mime_type) false end |