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

Instance Method Details

#all_casings(input_string) ⇒ Object



148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
# File 'lib/serverless_rack.rb', line 148

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

#build_environ(event:, context:, headers:, body:) ⇒ Object



84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/serverless_rack.rb', line 84

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



129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
# File 'lib/serverless_rack.rb', line 129

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



192
193
194
195
196
# File 'lib/serverless_rack.rb', line 192

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



198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
# File 'lib/serverless_rack.rb', line 198

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



169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
# File 'lib/serverless_rack.rb', line 169

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



110
111
112
113
114
115
116
117
# File 'lib/serverless_rack.rb', line 110

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



222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
# File 'lib/serverless_rack.rb', line 222

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

Returns:

  • (Boolean)


18
19
20
# File 'lib/serverless_rack.rb', line 18

def keepalive_event?(event)
  ['aws.events', 'serverless-plugin-warmup'].include?(event['source'])
end

#parse_body(event) ⇒ Object



52
53
54
55
56
57
58
# File 'lib/serverless_rack.rb', line 52

def parse_body(event)
  if event['isBase64Encoded']
    Base64.decode64(event['body'])
  else
    event['body'] || ''
  end
end

#parse_headers(event) ⇒ Object



60
61
62
63
64
65
66
67
68
69
70
# File 'lib/serverless_rack.rb', line 60

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



72
73
74
75
76
77
78
79
80
81
82
# File 'lib/serverless_rack.rb', line 72

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



32
33
34
35
36
37
38
39
40
41
42
# File 'lib/serverless_rack.rb', line 32

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 ENV['API_GATEWAY_BASE_PATH']
    base_path = "/#{ENV['API_GATEWAY_BASE_PATH']}"
    return event['path'][base_path.length..-1] if event['path'].start_with?(base_path)
  end

  event['path']
end

#parse_query_string(event) ⇒ Object



44
45
46
47
48
49
50
# File 'lib/serverless_rack.rb', line 44

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



22
23
24
25
26
27
28
29
30
# File 'lib/serverless_rack.rb', line 22

def parse_script_name(event, headers)
  if ENV['API_GATEWAY_BASE_PATH']
    "/#{ENV['API_GATEWAY_BASE_PATH']}"
  elsif (headers['Host'] || '').include?('amazonaws.com')
    "/#{event['requestContext']['stage']}"
  else
    ''
  end
end

#text_mime_type?(headers:, text_mime_types:) ⇒ Boolean

Returns:

  • (Boolean)


119
120
121
122
123
124
125
126
127
# File 'lib/serverless_rack.rb', line 119

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