jets 1.1.5 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -0
- data/Gemfile.lock +10 -6
- data/README/testing.md +5 -1
- data/jets.gemspec +1 -0
- data/lib/jets.rb +5 -1
- data/lib/jets/application.rb +39 -19
- data/lib/jets/aws_services.rb +16 -10
- data/lib/jets/aws_services/stack_status.rb +7 -0
- data/lib/jets/booter.rb +6 -2
- data/lib/jets/builders/code_builder.rb +14 -0
- data/lib/jets/builders/handler_generator.rb +15 -0
- data/lib/jets/builders/shim_vars/app.rb +4 -3
- data/lib/jets/builders/shim_vars/shared.rb +8 -4
- data/lib/jets/builders/templates/shim.js +7 -3
- data/lib/jets/camelizer.rb +2 -1
- data/lib/jets/cfn/builders.rb +0 -1
- data/lib/jets/cfn/builders/api_deployment_builder.rb +27 -0
- data/lib/jets/cfn/builders/api_gateway_builder.rb +22 -2
- data/lib/jets/cfn/ship.rb +38 -6
- data/lib/jets/commands/call.rb +0 -1
- data/lib/jets/commands/call/guesser.rb +0 -3
- data/lib/jets/commands/clean/log.rb +18 -0
- data/lib/jets/commands/console.rb +1 -1
- data/lib/jets/commands/import/sequence.rb +2 -3
- data/lib/jets/commands/runner.rb +1 -1
- data/lib/jets/commands/sequence.rb +0 -1
- data/lib/jets/commands/templates/skeleton/config/application.rb.tt +11 -0
- data/lib/jets/commands/url.rb +32 -7
- data/lib/jets/controller/base.rb +21 -5
- data/lib/jets/controller/layout.rb +0 -3
- data/lib/jets/controller/middleware/local/api_gateway.rb +2 -5
- data/lib/jets/controller/middleware/local/mimic_aws_call.rb +2 -2
- data/lib/jets/controller/params.rb +42 -10
- data/lib/jets/controller/rack/adapter.rb +5 -2
- data/lib/jets/controller/rack/env.rb +17 -8
- data/lib/jets/controller/renderers/rack_renderer.rb +1 -1
- data/lib/jets/controller/rendering.rb +4 -1
- data/lib/jets/core.rb +8 -16
- data/lib/jets/internal/app/functions/jets/base_path.rb +153 -0
- data/lib/jets/klass.rb +38 -5
- data/lib/jets/lambda/dsl.rb +0 -2
- data/lib/jets/mega/request.rb +44 -13
- data/lib/jets/mega/request/source.rb +21 -0
- data/lib/jets/middleware/configurator.rb +1 -1
- data/lib/jets/middleware/default_stack.rb +2 -2
- data/lib/jets/resource.rb +1 -0
- data/lib/jets/resource/api_gateway.rb +5 -3
- data/lib/jets/resource/api_gateway/base_path.rb +5 -0
- data/lib/jets/resource/api_gateway/base_path/function.rb +42 -0
- data/lib/jets/resource/api_gateway/base_path/mapping.rb +44 -0
- data/lib/jets/resource/api_gateway/base_path/role.rb +76 -0
- data/lib/jets/resource/api_gateway/cors.rb +1 -1
- data/lib/jets/resource/api_gateway/deployment.rb +9 -5
- data/lib/jets/resource/api_gateway/domain_name.rb +56 -0
- data/lib/jets/resource/api_gateway/method.rb +3 -4
- data/lib/jets/resource/api_gateway/resource.rb +4 -3
- data/lib/jets/resource/api_gateway/rest_api.rb +42 -14
- data/lib/jets/resource/api_gateway/rest_api/change_detection.rb +42 -0
- data/lib/jets/resource/api_gateway/rest_api/logical_id.rb +59 -0
- data/lib/jets/resource/api_gateway/rest_api/routes.rb +127 -0
- data/lib/jets/resource/child_stack/api_deployment.rb +5 -1
- data/lib/jets/resource/function.rb +3 -20
- data/lib/jets/resource/function/environment.rb +23 -0
- data/lib/jets/resource/iam/application_role.rb +1 -1
- data/lib/jets/resource/route53.rb +3 -0
- data/lib/jets/resource/route53/record_set.rb +70 -0
- data/lib/jets/router.rb +2 -0
- data/lib/jets/ruby_server.rb +6 -3
- data/lib/jets/stack.rb +1 -3
- data/lib/jets/stack/main/dsl.rb +1 -1
- data/lib/jets/stack/main/extensions/lambda.rb +4 -2
- data/lib/jets/turbine.rb +0 -3
- data/lib/jets/version.rb +1 -1
- data/vendor/jets-gems/lib/jets/gems.rb +1 -0
- data/vendor/jets-gems/lib/jets/gems/agree.rb +41 -0
- data/vendor/jets-gems/lib/jets/gems/check.rb +15 -2
- metadata +30 -2
data/lib/jets/camelizer.rb
CHANGED
data/lib/jets/cfn/builders.rb
CHANGED
@@ -16,6 +16,33 @@ class Jets::Cfn::Builders
|
|
16
16
|
add_resource(deployment)
|
17
17
|
add_parameters(deployment.parameters)
|
18
18
|
add_outputs(deployment.outputs)
|
19
|
+
|
20
|
+
add_base_path_mapping
|
21
|
+
end
|
22
|
+
|
23
|
+
# Because Jets generates a new timestamped logical id for the API Deployment
|
24
|
+
# resource it also creates a new root base path mapping and fails. Additionally,
|
25
|
+
# the base path mapping depends on the API Deploy for the stage name.
|
26
|
+
#
|
27
|
+
# We resolve this by using a custom resource that does an in-place update.
|
28
|
+
#
|
29
|
+
# Note, also tried to change the domain name of to something like demo-dev-[random].mydomain.com
|
30
|
+
# but that does not work because the domain name has to match the route53 record exactly.
|
31
|
+
#
|
32
|
+
def add_base_path_mapping
|
33
|
+
return unless Jets.custom_domain?
|
34
|
+
|
35
|
+
function = Jets::Resource::ApiGateway::BasePath::Function.new
|
36
|
+
add_resource(function)
|
37
|
+
add_outputs(function.outputs)
|
38
|
+
|
39
|
+
mapping = Jets::Resource::ApiGateway::BasePath::Mapping.new
|
40
|
+
add_resource(mapping)
|
41
|
+
add_outputs(mapping.outputs)
|
42
|
+
|
43
|
+
iam_role = Jets::Resource::ApiGateway::BasePath::Role.new
|
44
|
+
add_resource(iam_role)
|
45
|
+
add_outputs(iam_role.outputs)
|
19
46
|
end
|
20
47
|
|
21
48
|
# template_path is an interface method
|
@@ -13,6 +13,7 @@ class Jets::Cfn::Builders
|
|
13
13
|
return unless @options[:templates] || @options[:stack_type] != :minimal
|
14
14
|
|
15
15
|
add_gateway_rest_api
|
16
|
+
add_custom_domain
|
16
17
|
add_gateway_routes
|
17
18
|
end
|
18
19
|
|
@@ -33,7 +34,26 @@ class Jets::Cfn::Builders
|
|
33
34
|
add_outputs(rest_api.outputs)
|
34
35
|
|
35
36
|
deployment = Jets::Resource::ApiGateway::Deployment.new
|
36
|
-
|
37
|
+
outputs = deployment.outputs(true)
|
38
|
+
add_output("RestApiUrl", Value: outputs["RestApiUrl"])
|
39
|
+
end
|
40
|
+
|
41
|
+
def add_custom_domain
|
42
|
+
return unless Jets.custom_domain?
|
43
|
+
add_domain_name
|
44
|
+
add_route53_dns
|
45
|
+
end
|
46
|
+
|
47
|
+
def add_domain_name
|
48
|
+
domain_name = Jets::Resource::ApiGateway::DomainName.new
|
49
|
+
add_resource(domain_name)
|
50
|
+
add_outputs(domain_name.outputs)
|
51
|
+
end
|
52
|
+
|
53
|
+
def add_route53_dns
|
54
|
+
dns = Jets::Resource::Route53::RecordSet.new
|
55
|
+
add_resource(dns)
|
56
|
+
add_outputs(dns.outputs)
|
37
57
|
end
|
38
58
|
|
39
59
|
# Adds route related Resources and Outputs
|
@@ -51,7 +71,7 @@ class Jets::Cfn::Builders
|
|
51
71
|
homepage = path == ''
|
52
72
|
next if homepage # handled by RootResourceId output already
|
53
73
|
|
54
|
-
resource = Jets::Resource::ApiGateway::Resource.new(path)
|
74
|
+
resource = Jets::Resource::ApiGateway::Resource.new(path, internal: true)
|
55
75
|
add_resource(resource)
|
56
76
|
add_outputs(resource.outputs)
|
57
77
|
end
|
data/lib/jets/cfn/ship.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
class Jets::Cfn
|
2
2
|
class Ship
|
3
|
+
extend Memoist
|
3
4
|
include Jets::AwsServices
|
4
5
|
|
5
6
|
def initialize(options)
|
@@ -32,7 +33,9 @@ class Jets::Cfn
|
|
32
33
|
|
33
34
|
wait_for_stack
|
34
35
|
prewarm
|
36
|
+
clean_deploy_logs
|
35
37
|
show_api_endpoint
|
38
|
+
show_custom_domain
|
36
39
|
end
|
37
40
|
|
38
41
|
def save_stack
|
@@ -89,15 +92,34 @@ class Jets::Cfn
|
|
89
92
|
end
|
90
93
|
end
|
91
94
|
|
92
|
-
def
|
93
|
-
|
94
|
-
|
95
|
-
resp, status = stack_status
|
96
|
-
return if status.include?("ROLLBACK")
|
95
|
+
def clean_deploy_logs
|
96
|
+
Jets::Commands::Clean::Log.new.clean_deploys
|
97
|
+
end
|
97
98
|
|
99
|
+
def endpoint_unavailable?
|
100
|
+
return true unless @options[:stack_type] == :full # s3 bucket is available
|
101
|
+
return true if Jets::Router.routes.empty?
|
102
|
+
_, status = stack_status
|
103
|
+
return true if status.include?("ROLLBACK")
|
104
|
+
return true unless api_gateway
|
105
|
+
end
|
106
|
+
|
107
|
+
# Do not memoize this because on first stack run it will be nil
|
108
|
+
# It only gets called one more time so just let it get called.
|
109
|
+
def api_gateway
|
98
110
|
resp = cfn.describe_stack_resources(stack_name: @parent_stack_name)
|
99
111
|
resources = resp.stack_resources
|
100
|
-
|
112
|
+
resources.find { |resource| resource.logical_resource_id == "ApiGateway" }
|
113
|
+
end
|
114
|
+
memoize :api_gateway
|
115
|
+
|
116
|
+
def endpoint_available?
|
117
|
+
!endpoint_unavailable?
|
118
|
+
end
|
119
|
+
|
120
|
+
def show_api_endpoint
|
121
|
+
return unless endpoint_available?
|
122
|
+
|
101
123
|
stack_id = api_gateway["physical_resource_id"]
|
102
124
|
|
103
125
|
resp = cfn.describe_stacks(stack_name: stack_id)
|
@@ -107,6 +129,16 @@ class Jets::Cfn
|
|
107
129
|
puts "API Gateway Endpoint: #{endpoint}"
|
108
130
|
end
|
109
131
|
|
132
|
+
def show_custom_domain
|
133
|
+
return unless endpoint_available? && Jets.custom_domain?
|
134
|
+
|
135
|
+
domain_name = Jets::Resource::ApiGateway::DomainName.new
|
136
|
+
# Looks funny but its right.
|
137
|
+
# domain_name is a method on the Jets::Resource::ApiGateway::Domain instance
|
138
|
+
url = "https://#{domain_name.domain_name}"
|
139
|
+
puts "Custom Domain: #{url}"
|
140
|
+
end
|
141
|
+
|
110
142
|
# All CloudFormation states listed here:
|
111
143
|
# http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-describing-stacks.html
|
112
144
|
def stack_status
|
data/lib/jets/commands/call.rb
CHANGED
@@ -25,6 +25,24 @@ class Jets::Commands::Clean
|
|
25
25
|
say "Removed CloudWatch logs for #{prefix_guess}"
|
26
26
|
end
|
27
27
|
|
28
|
+
def clean_deploys
|
29
|
+
groups = deploy_log_groups.sort_by do |g|
|
30
|
+
g.log_group_name
|
31
|
+
end
|
32
|
+
# Keep the last 2 recent log groups so we can see the deleted logic
|
33
|
+
groups = groups[0..-3]
|
34
|
+
groups.each do |g|
|
35
|
+
logs.delete_log_group(log_group_name: g.log_group_name) unless @options[:noop]
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def deploy_log_groups
|
40
|
+
log_groups.select do |g|
|
41
|
+
!keep_log_group?(g.log_group_name) &&
|
42
|
+
g.log_group_name.include?('jets-base-path')
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
28
46
|
private
|
29
47
|
def prefix_guess
|
30
48
|
Jets::Naming.parent_stack_name
|
data/lib/jets/commands/runner.rb
CHANGED
@@ -47,4 +47,15 @@ Jets.application.configure do
|
|
47
47
|
# config.assets.cache_control = nil # IE: "public, max-age=3600" # override max_age for more fine-grain control.
|
48
48
|
# config.assets.base_url = nil # IE: https://cloudfront.com/my/base/path, defaults to the s3 bucket url
|
49
49
|
# IE: https://s3-us-west-2.amazonaws.com/demo-dev-s3bucket-1inlzkvujq8zb
|
50
|
+
|
51
|
+
# config.api.endpoint_type = 'PRIVATE' # Default is 'EDGE' https://amzn.to/2r0Iu2L
|
52
|
+
# config.api.authorization_type = "AWS_IAM" # default is 'NONE' https://amzn.to/2qZ7zLh
|
53
|
+
|
54
|
+
|
55
|
+
# config.domain.hosted_zone_name = "example.com"
|
56
|
+
# us-west-2 REGIONAL endpoint
|
57
|
+
# config.domain.cert_arn = "arn:aws:acm:us-west-2:112233445566:certificate/8d8919ce-a710-4050-976b-b33da991e123"
|
58
|
+
# us-east-1 EDGE endpoint
|
59
|
+
# config.domain.cert_arn = "arn:aws:acm:us-east-1:112233445566:certificate/d68472ba-04f8-45ba-b9db-14f839d57123"
|
60
|
+
# config.domain.endpoint_type = "EDGE"
|
50
61
|
end
|
data/lib/jets/commands/url.rb
CHANGED
@@ -16,14 +16,16 @@ module Jets::Commands
|
|
16
16
|
stack = cfn.describe_stacks(stack_name: stack_name).stacks.first
|
17
17
|
|
18
18
|
api_gateway_stack_arn = lookup(stack[:outputs], "ApiGateway")
|
19
|
-
if api_gateway_stack_arn
|
20
|
-
|
19
|
+
if api_gateway_stack_arn && endpoint_available?
|
20
|
+
api_gateway_endpoint = get_gateway_endpoint(api_gateway_stack_arn)
|
21
|
+
STDOUT.puts "API Gateway Endpoint: #{api_gateway_endpoint}"
|
22
|
+
show_custom_domain
|
21
23
|
else
|
22
24
|
puts "API Gateway not found. This jets app does have an API Gateway associated with it. Please double check your config/routes.rb if you were expecting to see a url for the app. Also check that #{stack_name.colorize(:green)} is a jets app."
|
23
25
|
end
|
24
26
|
end
|
25
27
|
|
26
|
-
def
|
28
|
+
def get_gateway_endpoint(api_gateway_stack_arn)
|
27
29
|
stack = cfn.describe_stacks(stack_name: api_gateway_stack_arn).stacks.first
|
28
30
|
rest_api = lookup(stack[:outputs], "RestApi")
|
29
31
|
region_id = lookup(stack[:outputs], "Region")
|
@@ -34,10 +36,33 @@ module Jets::Commands
|
|
34
36
|
"https://#{rest_api}.execute-api.#{region_id}.amazonaws.com/#{stage_name}"
|
35
37
|
end
|
36
38
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
39
|
+
def show_custom_domain
|
40
|
+
return unless endpoint_available? && Jets.custom_domain?
|
41
|
+
|
42
|
+
domain_name = Jets::Resource::ApiGateway::DomainName.new
|
43
|
+
# Looks funny but its right.
|
44
|
+
# domain_name is a method on the Jets::Resource::ApiGateway::Domain instance
|
45
|
+
url = "https://#{domain_name.domain_name}"
|
46
|
+
puts "Custom Domain: #{url}"
|
47
|
+
end
|
48
|
+
|
49
|
+
def endpoint_unavailable?
|
50
|
+
return false if Jets::Router.routes.empty?
|
51
|
+
resp, status = stack_status
|
52
|
+
return false if status.include?("ROLLBACK")
|
53
|
+
end
|
54
|
+
|
55
|
+
def endpoint_available?
|
56
|
+
!endpoint_unavailable?
|
57
|
+
end
|
58
|
+
|
59
|
+
# All CloudFormation states listed here:
|
60
|
+
# http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-describing-stacks.html
|
61
|
+
def stack_status
|
62
|
+
resp = cfn.describe_stacks(stack_name: @parent_stack_name)
|
63
|
+
status = resp.stacks[0].stack_status
|
64
|
+
[resp, status]
|
41
65
|
end
|
66
|
+
|
42
67
|
end
|
43
68
|
end
|
data/lib/jets/controller/base.rb
CHANGED
@@ -1,5 +1,3 @@
|
|
1
|
-
require "active_support/core_ext/hash"
|
2
|
-
require "active_support/core_ext/object"
|
3
1
|
require "json"
|
4
2
|
require "rack/utils" # Rack::Utils.parse_nested_query
|
5
3
|
|
@@ -37,9 +35,7 @@ class Jets::Controller
|
|
37
35
|
|
38
36
|
def dispatch!
|
39
37
|
t1 = Time.now
|
40
|
-
|
41
|
-
Jets.logger.info " Event: #{@event.inspect}"
|
42
|
-
Jets.logger.info " Parameters: #{params(raw: true).to_h.inspect}"
|
38
|
+
log_info_start
|
43
39
|
|
44
40
|
run_before_actions
|
45
41
|
send(@meth)
|
@@ -53,6 +49,26 @@ class Jets::Controller
|
|
53
49
|
triplet # status, headers, body
|
54
50
|
end
|
55
51
|
|
52
|
+
def log_info_start
|
53
|
+
display_event = @event.dup
|
54
|
+
display_event['body'] = '[BASE64_ENCODED]' if @event['isBase64Encoded']
|
55
|
+
# Interesting, JSON.dump makes logging look like JSON.pretty_generate in
|
56
|
+
# CloudWatch but not locally. This is what we want.
|
57
|
+
ip = request.ip
|
58
|
+
Jets.logger.info "Started #{@event['httpMethod']} \"#{@event['path']}\" for #{ip} at #{Time.now}"
|
59
|
+
Jets.logger.info "Processing #{self.class.name}##{@meth}"
|
60
|
+
Jets.logger.info " Event: #{json_dump(display_event)}"
|
61
|
+
Jets.logger.info " Parameters: #{JSON.dump(params(raw: true).to_h)}"
|
62
|
+
end
|
63
|
+
|
64
|
+
# Handles binary data safely
|
65
|
+
def json_dump(data)
|
66
|
+
JSON.dump(data)
|
67
|
+
rescue Encoding::UndefinedConversionError
|
68
|
+
data['body'] = '[BINARY]'
|
69
|
+
JSON.dump(data)
|
70
|
+
end
|
71
|
+
|
56
72
|
def self.process(event, context={}, meth)
|
57
73
|
controller = new(event, context, meth)
|
58
74
|
# Using send because process! is private method in Jets::RackController so
|
@@ -63,10 +63,10 @@ class Jets::Controller::Middleware::Local
|
|
63
63
|
h[key] = v
|
64
64
|
h
|
65
65
|
end
|
66
|
-
# Content type is not prepended with HTTP_
|
66
|
+
# Content type is not prepended with HTTP_
|
67
67
|
headers["Content-Type"] = @env["CONTENT_TYPE"] if @env["CONTENT_TYPE"]
|
68
68
|
|
69
|
-
# Adjust the casing so it matches the Lambda AWS Proxy
|
69
|
+
# Adjust the casing so it matches the Lambda AWS Proxy structure
|
70
70
|
CASING_MAP.each do |nice_casing, bad_casing|
|
71
71
|
if headers.key?(nice_casing)
|
72
72
|
headers[bad_casing] = headers.delete(nice_casing)
|
@@ -83,9 +83,6 @@ class Jets::Controller::Middleware::Local
|
|
83
83
|
# To get the post body:
|
84
84
|
# rack.input: #<StringIO:0x007f8ccf8db9a0>
|
85
85
|
def get_body
|
86
|
-
# @env["rack.input"] is always provided by rack and we should make
|
87
|
-
# the test data always have rack.input to mimic rack defaulting to
|
88
|
-
# StringIO.new to help make testing easier
|
89
86
|
input = @env["rack.input"] || StringIO.new
|
90
87
|
body = input.read
|
91
88
|
input.rewind # IMPORTANT or else it screws up other middlewares that use the body
|
@@ -27,12 +27,12 @@ class Jets::Controller::Middleware::Local
|
|
27
27
|
end
|
28
28
|
|
29
29
|
def event
|
30
|
-
ApiGateway.new(@route, @env).event
|
30
|
+
@env['adapter.event'] || ApiGateway.new(@route, @env).event
|
31
31
|
end
|
32
32
|
memoize :event
|
33
33
|
|
34
34
|
def context
|
35
|
-
{}
|
35
|
+
@env['adapter.context'] || {"fake" => "context in mimic_aws_call.rb"}
|
36
36
|
end
|
37
37
|
end
|
38
38
|
end
|
@@ -1,8 +1,11 @@
|
|
1
1
|
require "action_controller/metal/strong_parameters"
|
2
|
+
require "action_dispatch"
|
2
3
|
require "rack"
|
3
4
|
|
4
5
|
class Jets::Controller
|
5
6
|
module Params
|
7
|
+
extend Memoist
|
8
|
+
|
6
9
|
# Merge all the parameters together for convenience. Users still have
|
7
10
|
# access via events.
|
8
11
|
#
|
@@ -10,46 +13,75 @@ class Jets::Controller
|
|
10
13
|
# 1. path parameters have highest precdence
|
11
14
|
# 2. query string parameters
|
12
15
|
# 3. body parameters
|
13
|
-
def params(raw: false, path_parameters: true)
|
14
|
-
query_string_params = event["queryStringParameters"] || {}
|
16
|
+
def params(raw: false, path_parameters: true, body_parameters: true)
|
15
17
|
path_params = event["pathParameters"] || {}
|
16
|
-
|
17
|
-
|
18
|
+
|
19
|
+
params = {}
|
20
|
+
params = params.deep_merge(body_params) if body_parameters
|
21
|
+
params = params.deep_merge(query_parameters) # always
|
18
22
|
params = params.deep_merge(path_params) if path_parameters
|
19
23
|
|
20
24
|
if raw
|
21
25
|
params
|
22
26
|
else
|
27
|
+
params = ActionDispatch::Request::Utils.normalize_encode_params(params) # for file uploads
|
23
28
|
ActionController::Parameters.new(params)
|
24
29
|
end
|
25
30
|
end
|
26
31
|
|
27
|
-
|
32
|
+
def query_parameters
|
33
|
+
event["queryStringParameters"] || {}
|
34
|
+
end
|
35
|
+
|
28
36
|
def body_params
|
29
|
-
body = event["body"]
|
37
|
+
body = event['isBase64Encoded'] ? base64_decode(event["body"]) : event["body"]
|
30
38
|
return {} if body.nil?
|
31
39
|
|
32
|
-
# Try json parsing
|
33
40
|
parsed_json = parse_json(body)
|
34
41
|
return parsed_json if parsed_json
|
35
42
|
|
36
|
-
|
37
|
-
# For content-type application/x-www-form-urlencoded CGI.parse the body
|
38
43
|
headers = event["headers"] || {}
|
39
|
-
headers = headers.transform_keys { |key| key.downcase }
|
40
44
|
# API Gateway seems to use either: content-type or Content-Type
|
45
|
+
headers = headers.transform_keys { |key| key.downcase }
|
41
46
|
content_type = headers["content-type"]
|
47
|
+
|
42
48
|
if content_type.to_s.include?("application/x-www-form-urlencoded")
|
43
49
|
return ::Rack::Utils.parse_nested_query(body)
|
50
|
+
elsif content_type.to_s.include?("multipart/form-data")
|
51
|
+
return parse_multipart(body)
|
44
52
|
end
|
45
53
|
|
46
54
|
{} # fallback to empty Hash
|
47
55
|
end
|
56
|
+
memoize :body_params
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def parse_multipart(body)
|
61
|
+
boundary = ::Rack::Multipart::Parser.parse_boundary(headers["content-type"])
|
62
|
+
options = multipart_options(body, boundary)
|
63
|
+
env = ::Rack::MockRequest.env_for("/", options)
|
64
|
+
::Rack::Multipart.parse_multipart(env) # params Hash
|
65
|
+
end
|
66
|
+
|
67
|
+
def multipart_options(data, boundary = "AaB03x")
|
68
|
+
type = %(multipart/form-data; boundary=#{boundary})
|
69
|
+
length = data.bytesize
|
70
|
+
|
71
|
+
{ "CONTENT_TYPE" => type,
|
72
|
+
"CONTENT_LENGTH" => length.to_s,
|
73
|
+
:input => StringIO.new(data) }
|
74
|
+
end
|
48
75
|
|
49
76
|
def parse_json(text)
|
50
77
|
JSON.parse(text)
|
51
78
|
rescue JSON::ParserError
|
52
79
|
nil
|
53
80
|
end
|
81
|
+
|
82
|
+
def base64_decode(body)
|
83
|
+
return nil if body.nil?
|
84
|
+
Base64.decode64(body)
|
85
|
+
end
|
54
86
|
end
|
55
87
|
end
|