castle-rb 6.0.1 → 7.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +8 -8
- data/lib/castle.rb +7 -11
- data/lib/castle/api.rb +7 -12
- data/lib/castle/api/approve_device.rb +1 -6
- data/lib/castle/api/authenticate.rb +10 -7
- data/lib/castle/api/end_impersonation.rb +3 -8
- data/lib/castle/api/filter.rb +37 -0
- data/lib/castle/api/get_device.rb +1 -6
- data/lib/castle/api/get_devices_for_user.rb +1 -6
- data/lib/castle/api/log.rb +37 -0
- data/lib/castle/api/report_device.rb +1 -6
- data/lib/castle/api/risk.rb +37 -0
- data/lib/castle/api/start_impersonation.rb +3 -8
- data/lib/castle/api/track.rb +1 -6
- data/lib/castle/client.rb +36 -16
- data/lib/castle/commands/approve_device.rb +1 -5
- data/lib/castle/commands/end_impersonation.rb +1 -1
- data/lib/castle/commands/filter.rb +23 -0
- data/lib/castle/commands/get_device.rb +1 -5
- data/lib/castle/commands/get_devices_for_user.rb +1 -5
- data/lib/castle/commands/{identify.rb → log.rb} +4 -3
- data/lib/castle/commands/report_device.rb +1 -5
- data/lib/castle/commands/risk.rb +23 -0
- data/lib/castle/commands/start_impersonation.rb +1 -1
- data/lib/castle/configuration.rb +18 -8
- data/lib/castle/core/get_connection.rb +3 -1
- data/lib/castle/core/process_response.rb +5 -2
- data/lib/castle/core/process_webhook.rb +10 -5
- data/lib/castle/core/send_request.rb +8 -16
- data/lib/castle/errors.rb +37 -13
- data/lib/castle/failover/prepare_response.rb +2 -7
- data/lib/castle/failover/strategy.rb +3 -0
- data/lib/castle/headers/extract.rb +4 -4
- data/lib/castle/headers/filter.rb +9 -6
- data/lib/castle/ips/extract.rb +4 -2
- data/lib/castle/logger.rb +3 -3
- data/lib/castle/payload/prepare.rb +3 -4
- data/lib/castle/secure_mode.rb +3 -2
- data/lib/castle/support/hanami.rb +2 -6
- data/lib/castle/support/rails.rb +1 -3
- data/lib/castle/utils/clean_invalid_chars.rb +1 -3
- data/lib/castle/verdict.rb +2 -0
- data/lib/castle/version.rb +1 -1
- data/lib/castle/webhooks/verify.rb +9 -7
- data/spec/integration/rails/rails_spec.rb +9 -7
- data/spec/integration/rails/support/home_controller.rb +26 -24
- data/spec/lib/castle/api/approve_device_spec.rb +3 -3
- data/spec/lib/castle/api/authenticate_spec.rb +20 -24
- data/spec/lib/castle/api/end_impersonation_spec.rb +11 -5
- data/spec/lib/castle/api/filter_spec.rb +5 -0
- data/spec/lib/castle/api/get_device_spec.rb +3 -3
- data/spec/lib/castle/api/get_devices_for_user_spec.rb +3 -3
- data/spec/lib/castle/api/log_spec.rb +5 -0
- data/spec/lib/castle/api/report_device_spec.rb +3 -3
- data/spec/lib/castle/api/risk_spec.rb +5 -0
- data/spec/lib/castle/api/start_impersonation_spec.rb +11 -5
- data/spec/lib/castle/api/track_spec.rb +11 -7
- data/spec/lib/castle/api_spec.rb +4 -20
- data/spec/lib/castle/client_id/extract_spec.rb +4 -13
- data/spec/lib/castle/client_spec.rb +81 -84
- data/spec/lib/castle/commands/authenticate_spec.rb +8 -15
- data/spec/lib/castle/commands/end_impersonation_spec.rb +6 -9
- data/spec/lib/castle/commands/{identify_spec.rb → filter_spec.rb} +41 -19
- data/spec/lib/castle/commands/log_spec.rb +100 -0
- data/spec/lib/castle/commands/risk_spec.rb +100 -0
- data/spec/lib/castle/commands/start_impersonation_spec.rb +6 -9
- data/spec/lib/castle/commands/track_spec.rb +9 -18
- data/spec/lib/castle/configuration_spec.rb +2 -6
- data/spec/lib/castle/context/get_default_spec.rb +8 -8
- data/spec/lib/castle/context/prepare_spec.rb +6 -7
- data/spec/lib/castle/core/get_connection_spec.rb +6 -22
- data/spec/lib/castle/core/process_response_spec.rb +1 -8
- data/spec/lib/castle/core/send_request_spec.rb +4 -29
- data/spec/lib/castle/headers/extract_spec.rb +1 -3
- data/spec/lib/castle/headers/filter_spec.rb +12 -11
- data/spec/lib/castle/ips/extract_spec.rb +4 -13
- data/spec/lib/castle/logger_spec.rb +2 -6
- data/spec/lib/castle/payload/prepare_spec.rb +5 -4
- data/spec/lib/castle/session_spec.rb +13 -36
- data/spec/lib/castle/singleton_configuration_spec.rb +2 -6
- data/spec/lib/castle/utils/clean_invalid_chars_spec.rb +2 -2
- data/spec/lib/castle/utils/merge_spec.rb +3 -1
- data/spec/lib/castle/validators/present_spec.rb +5 -6
- data/spec/lib/castle/webhooks/verify_spec.rb +8 -24
- data/spec/lib/castle_spec.rb +4 -10
- data/spec/spec_helper.rb +1 -3
- data/spec/support/shared_examples/action_request.rb +152 -0
- data/spec/support/shared_examples/configuration.rb +14 -42
- metadata +23 -18
- data/lib/castle/api/identify.rb +0 -26
- data/lib/castle/api/review.rb +0 -24
- data/lib/castle/commands/review.rb +0 -17
- data/lib/castle/events.rb +0 -49
- data/spec/lib/castle/api/identify_spec.rb +0 -68
- data/spec/lib/castle/api/review_spec.rb +0 -19
- data/spec/lib/castle/commands/review_spec.rb +0 -24
- data/spec/lib/castle/events_spec.rb +0 -5
@@ -9,11 +9,7 @@ module Castle
|
|
9
9
|
# @return [Castle::Command]
|
10
10
|
def build(options = {})
|
11
11
|
Castle::Validators::Present.call(options, %i[device_token])
|
12
|
-
Castle::Command.new(
|
13
|
-
"devices/#{options[:device_token]}/approve",
|
14
|
-
nil,
|
15
|
-
:put
|
16
|
-
)
|
12
|
+
Castle::Command.new("devices/#{options[:device_token]}/approve", nil, :put)
|
17
13
|
end
|
18
14
|
end
|
19
15
|
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Castle
|
4
|
+
module Commands
|
5
|
+
# Generates the payload for the filter request
|
6
|
+
class Filter
|
7
|
+
class << self
|
8
|
+
# @param options [Hash]
|
9
|
+
# @return [Castle::Command]
|
10
|
+
def build(options = {})
|
11
|
+
Castle::Validators::Present.call(options, %i[event])
|
12
|
+
context = Castle::Context::Sanitize.call(options[:context])
|
13
|
+
|
14
|
+
Castle::Command.new(
|
15
|
+
'filter',
|
16
|
+
options.merge(context: context, sent_at: Castle::Utils::GetTimestamp.call),
|
17
|
+
:post
|
18
|
+
)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -9,11 +9,7 @@ module Castle
|
|
9
9
|
# @return [Castle::Command]
|
10
10
|
def build(options = {})
|
11
11
|
Castle::Validators::Present.call(options, %i[device_token])
|
12
|
-
Castle::Command.new(
|
13
|
-
"devices/#{options[:device_token]}",
|
14
|
-
nil,
|
15
|
-
:get
|
16
|
-
)
|
12
|
+
Castle::Command.new("devices/#{options[:device_token]}", nil, :get)
|
17
13
|
end
|
18
14
|
end
|
19
15
|
end
|
@@ -9,11 +9,7 @@ module Castle
|
|
9
9
|
# @return [Castle::Command]
|
10
10
|
def build(options = {})
|
11
11
|
Castle::Validators::Present.call(options, %i[user_id])
|
12
|
-
Castle::Command.new(
|
13
|
-
"users/#{options[:user_id]}/devices",
|
14
|
-
nil,
|
15
|
-
:get
|
16
|
-
)
|
12
|
+
Castle::Command.new("users/#{options[:user_id]}/devices", nil, :get)
|
17
13
|
end
|
18
14
|
end
|
19
15
|
end
|
@@ -2,16 +2,17 @@
|
|
2
2
|
|
3
3
|
module Castle
|
4
4
|
module Commands
|
5
|
-
|
5
|
+
# Generates the payload for the log request
|
6
|
+
class Log
|
6
7
|
class << self
|
7
8
|
# @param options [Hash]
|
8
9
|
# @return [Castle::Command]
|
9
10
|
def build(options = {})
|
10
|
-
Castle::Validators::
|
11
|
+
Castle::Validators::Present.call(options, %i[event])
|
11
12
|
context = Castle::Context::Sanitize.call(options[:context])
|
12
13
|
|
13
14
|
Castle::Command.new(
|
14
|
-
'
|
15
|
+
'log',
|
15
16
|
options.merge(context: context, sent_at: Castle::Utils::GetTimestamp.call),
|
16
17
|
:post
|
17
18
|
)
|
@@ -9,11 +9,7 @@ module Castle
|
|
9
9
|
# @return [Castle::Command]
|
10
10
|
def build(options = {})
|
11
11
|
Castle::Validators::Present.call(options, %i[device_token])
|
12
|
-
Castle::Command.new(
|
13
|
-
"devices/#{options[:device_token]}/report",
|
14
|
-
nil,
|
15
|
-
:put
|
16
|
-
)
|
12
|
+
Castle::Command.new("devices/#{options[:device_token]}/report", nil, :put)
|
17
13
|
end
|
18
14
|
end
|
19
15
|
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Castle
|
4
|
+
module Commands
|
5
|
+
# Generates the payload for the risk request
|
6
|
+
class Risk
|
7
|
+
class << self
|
8
|
+
# @param options [Hash]
|
9
|
+
# @return [Castle::Command]
|
10
|
+
def build(options = {})
|
11
|
+
Castle::Validators::Present.call(options, %i[event])
|
12
|
+
context = Castle::Context::Sanitize.call(options[:context])
|
13
|
+
|
14
|
+
Castle::Command.new(
|
15
|
+
'risk',
|
16
|
+
options.merge(context: context, sent_at: Castle::Utils::GetTimestamp.call),
|
17
|
+
:post
|
18
|
+
)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/lib/castle/configuration.rb
CHANGED
@@ -8,15 +8,18 @@ module Castle
|
|
8
8
|
# API endpoint
|
9
9
|
BASE_URL = 'https://api.castle.io/v1'
|
10
10
|
REQUEST_TIMEOUT = 1000 # in milliseconds
|
11
|
+
|
11
12
|
# regexp of trusted proxies which is always appended to the trusted proxy list
|
12
|
-
TRUSTED_PROXIES = [
|
13
|
+
TRUSTED_PROXIES = [
|
14
|
+
/
|
13
15
|
\A127\.0\.0\.1\Z|
|
14
16
|
\A(10|172\.(1[6-9]|2[0-9]|30|31)|192\.168)\.|
|
15
17
|
\A::1\Z|\Afd[0-9a-f]{2}:.+|
|
16
18
|
\Alocalhost\Z|
|
17
19
|
\Aunix\Z|
|
18
20
|
\Aunix:
|
19
|
-
/ix
|
21
|
+
/ix
|
22
|
+
].freeze
|
20
23
|
|
21
24
|
# @note this value is not assigned as we don't recommend using a allowlist. If you need to use
|
22
25
|
# one, this constant is provided as a good default.
|
@@ -47,8 +50,14 @@ module Castle
|
|
47
50
|
].freeze
|
48
51
|
|
49
52
|
attr_accessor :request_timeout, :trust_proxy_chain, :logger
|
50
|
-
attr_reader :api_secret,
|
51
|
-
:
|
53
|
+
attr_reader :api_secret,
|
54
|
+
:allowlisted,
|
55
|
+
:denylisted,
|
56
|
+
:failover_strategy,
|
57
|
+
:ip_headers,
|
58
|
+
:trusted_proxies,
|
59
|
+
:trusted_proxy_depth,
|
60
|
+
:base_url
|
52
61
|
|
53
62
|
def initialize
|
54
63
|
@header_format = Castle::Headers::Format
|
@@ -96,7 +105,9 @@ module Castle
|
|
96
105
|
# sets trusted proxies
|
97
106
|
# @param value [Array<String,Regexp>]
|
98
107
|
def trusted_proxies=(value)
|
99
|
-
|
108
|
+
unless value.is_a?(Array)
|
109
|
+
raise Castle::ConfigurationError, 'trusted proxies must be an Array'
|
110
|
+
end
|
100
111
|
|
101
112
|
@trusted_proxies = value
|
102
113
|
end
|
@@ -111,9 +122,8 @@ module Castle
|
|
111
122
|
end
|
112
123
|
|
113
124
|
def failover_strategy=(value)
|
114
|
-
@failover_strategy =
|
115
|
-
strategy == value.to_sym
|
116
|
-
end
|
125
|
+
@failover_strategy =
|
126
|
+
Castle::Failover::STRATEGIES.detect { |strategy| strategy == value.to_sym }
|
117
127
|
raise Castle::ConfigurationError, 'unrecognized failover strategy' if @failover_strategy.nil?
|
118
128
|
end
|
119
129
|
|
@@ -8,7 +8,9 @@ module Castle
|
|
8
8
|
|
9
9
|
class << self
|
10
10
|
# @param config [Castle::Configuration, Castle::SingletonConfiguration]
|
11
|
-
|
11
|
+
# @return [Net::HTTP]
|
12
|
+
def call(config = nil)
|
13
|
+
config ||= Castle.config
|
12
14
|
http = Net::HTTP.new(config.base_url.host, config.base_url.port)
|
13
15
|
http.read_timeout = config.request_timeout / 1000.0
|
14
16
|
|
@@ -14,10 +14,13 @@ module Castle
|
|
14
14
|
}.freeze
|
15
15
|
|
16
16
|
class << self
|
17
|
-
|
17
|
+
# @param response [Response]
|
18
|
+
# @param config [Castle::Configuration, Castle::SingletonConfiguration, nil]
|
19
|
+
# @return [Hash]
|
20
|
+
def call(response, config = nil)
|
18
21
|
verify!(response)
|
19
22
|
|
20
|
-
Castle::Logger.call('response:', response.body.to_s)
|
23
|
+
Castle::Logger.call('response:', response.body.to_s, config)
|
21
24
|
|
22
25
|
return {} if response.body.nil? || response.body.empty?
|
23
26
|
|
@@ -7,12 +7,17 @@ module Castle
|
|
7
7
|
class << self
|
8
8
|
# Checks if webhook is valid
|
9
9
|
# @param webhook [Request]
|
10
|
-
|
11
|
-
|
12
|
-
|
10
|
+
# @param config [Castle::Configuration, Castle::SingletonConfiguration, nil]
|
11
|
+
# @return [String]
|
12
|
+
def call(webhook, config = nil)
|
13
|
+
webhook
|
14
|
+
.body
|
15
|
+
.read
|
16
|
+
.tap do |result|
|
17
|
+
raise Castle::ApiError, 'Invalid webhook from Castle API' if result.blank?
|
13
18
|
|
14
|
-
|
15
|
-
|
19
|
+
Castle::Logger.call('webhook:', result.to_s, config)
|
20
|
+
end
|
16
21
|
end
|
17
22
|
end
|
18
23
|
end
|
@@ -5,9 +5,7 @@ module Castle
|
|
5
5
|
# this class is responsible for making requests to api
|
6
6
|
module SendRequest
|
7
7
|
# Default headers that we add to passed ones
|
8
|
-
DEFAULT_HEADERS = {
|
9
|
-
'Content-Type' => 'application/json'
|
10
|
-
}.freeze
|
8
|
+
DEFAULT_HEADERS = { 'Content-Type' => 'application/json' }.freeze
|
11
9
|
|
12
10
|
private_constant :DEFAULT_HEADERS
|
13
11
|
|
@@ -15,31 +13,25 @@ module Castle
|
|
15
13
|
# @param command [String]
|
16
14
|
# @param headers [Hash]
|
17
15
|
# @param http [Net::HTTP]
|
18
|
-
# @param config [Castle::Configuration, Castle::SingletonConfiguration]
|
19
|
-
def call(command, headers, http = nil, config =
|
16
|
+
# @param config [Castle::Configuration, Castle::SingletonConfiguration, nil]
|
17
|
+
def call(command, headers, http = nil, config = nil)
|
20
18
|
(http || Castle::Core::GetConnection.call).request(
|
21
|
-
build(
|
22
|
-
command,
|
23
|
-
headers.merge(DEFAULT_HEADERS),
|
24
|
-
config
|
25
|
-
)
|
19
|
+
build(command, headers.merge(DEFAULT_HEADERS), config)
|
26
20
|
)
|
27
21
|
end
|
28
22
|
|
29
23
|
# @param command [String]
|
30
24
|
# @param headers [Hash]
|
31
|
-
# @param config [Castle::Configuration, Castle::SingletonConfiguration]
|
32
|
-
def build(command, headers, config
|
25
|
+
# @param config [Castle::Configuration, Castle::SingletonConfiguration, nil]
|
26
|
+
def build(command, headers, config)
|
33
27
|
url = "#{config.base_url.path}/#{command.path}"
|
34
28
|
request_obj = Net::HTTP.const_get(command.method.to_s.capitalize).new(url, headers)
|
35
29
|
|
36
30
|
unless command.method == :get
|
37
|
-
request_obj.body = ::Castle::Utils::CleanInvalidChars.call(
|
38
|
-
command.data
|
39
|
-
).to_json
|
31
|
+
request_obj.body = ::Castle::Utils::CleanInvalidChars.call(command.data).to_json
|
40
32
|
end
|
41
33
|
|
42
|
-
Castle::Logger.call("#{url}:", request_obj.body)
|
34
|
+
Castle::Logger.call("#{url}:", request_obj.body, config)
|
43
35
|
|
44
36
|
request_obj.basic_auth('', config.api_secret)
|
45
37
|
request_obj
|
data/lib/castle/errors.rb
CHANGED
@@ -2,7 +2,9 @@
|
|
2
2
|
|
3
3
|
module Castle
|
4
4
|
# general error
|
5
|
-
class Error < RuntimeError
|
5
|
+
class Error < RuntimeError
|
6
|
+
end
|
7
|
+
|
6
8
|
# Raised when anything is wrong with the request (any unhappy path)
|
7
9
|
# This error indicates that either we would wait too long for a response or something
|
8
10
|
# else happened somewhere in the middle and we weren't able to get the results
|
@@ -14,30 +16,52 @@ module Castle
|
|
14
16
|
@reason = reason
|
15
17
|
end
|
16
18
|
end
|
19
|
+
|
17
20
|
# security error
|
18
|
-
class SecurityError < Castle::Error
|
21
|
+
class SecurityError < Castle::Error
|
22
|
+
end
|
23
|
+
|
19
24
|
# wrong configuration error
|
20
|
-
class ConfigurationError < Castle::Error
|
25
|
+
class ConfigurationError < Castle::Error
|
26
|
+
end
|
27
|
+
|
21
28
|
# error returned by api
|
22
|
-
class ApiError < Castle::Error
|
29
|
+
class ApiError < Castle::Error
|
30
|
+
end
|
31
|
+
|
23
32
|
# webhook signature verification error
|
24
|
-
class WebhookVerificationError < Castle::Error
|
33
|
+
class WebhookVerificationError < Castle::Error
|
34
|
+
end
|
25
35
|
|
26
36
|
# api error bad request 400
|
27
|
-
class BadRequestError < Castle::ApiError
|
37
|
+
class BadRequestError < Castle::ApiError
|
38
|
+
end
|
39
|
+
|
28
40
|
# api error forbidden 403
|
29
|
-
class ForbiddenError < Castle::ApiError
|
41
|
+
class ForbiddenError < Castle::ApiError
|
42
|
+
end
|
43
|
+
|
30
44
|
# api error not found 404
|
31
|
-
class NotFoundError < Castle::ApiError
|
45
|
+
class NotFoundError < Castle::ApiError
|
46
|
+
end
|
47
|
+
|
32
48
|
# api error user unauthorized 419
|
33
|
-
class UserUnauthorizedError < Castle::ApiError
|
49
|
+
class UserUnauthorizedError < Castle::ApiError
|
50
|
+
end
|
51
|
+
|
34
52
|
# api error invalid param 422
|
35
|
-
class InvalidParametersError < Castle::ApiError
|
53
|
+
class InvalidParametersError < Castle::ApiError
|
54
|
+
end
|
55
|
+
|
36
56
|
# api error unauthorized 401
|
37
|
-
class UnauthorizedError < Castle::ApiError
|
57
|
+
class UnauthorizedError < Castle::ApiError
|
58
|
+
end
|
59
|
+
|
38
60
|
# all internal server errors
|
39
|
-
class InternalServerError < Castle::ApiError
|
61
|
+
class InternalServerError < Castle::ApiError
|
62
|
+
end
|
40
63
|
|
41
64
|
# impersonation command failed
|
42
|
-
class ImpersonationFailed < Castle::ApiError
|
65
|
+
class ImpersonationFailed < Castle::ApiError
|
66
|
+
end
|
43
67
|
end
|
@@ -4,19 +4,14 @@ module Castle
|
|
4
4
|
module Failover
|
5
5
|
# generate failover authentication response
|
6
6
|
class PrepareResponse
|
7
|
-
def initialize(user_id, reason:, strategy:
|
7
|
+
def initialize(user_id, reason:, strategy:)
|
8
8
|
@strategy = strategy
|
9
9
|
@reason = reason
|
10
10
|
@user_id = user_id
|
11
11
|
end
|
12
12
|
|
13
13
|
def call
|
14
|
-
{
|
15
|
-
action: @strategy.to_s,
|
16
|
-
user_id: @user_id,
|
17
|
-
failover: true,
|
18
|
-
failover_reason: @reason
|
19
|
-
}
|
14
|
+
{ action: @strategy.to_s, user_id: @user_id, failover: true, failover_reason: @reason }
|
20
15
|
end
|
21
16
|
end
|
22
17
|
end
|
@@ -13,11 +13,11 @@ module Castle
|
|
13
13
|
private_constant :ALWAYS_ALLOWLISTED, :ALWAYS_DENYLISTED
|
14
14
|
|
15
15
|
# @param headers [Hash]
|
16
|
-
# @param config [Castle::Configuration, Castle::SingletonConfiguration]
|
17
|
-
def initialize(headers, config =
|
16
|
+
# @param config [Castle::Configuration, Castle::SingletonConfiguration, nil]
|
17
|
+
def initialize(headers, config = nil)
|
18
18
|
@headers = headers
|
19
|
-
@config = config
|
20
|
-
@no_allowlist = config.allowlisted.empty?
|
19
|
+
@config = config || Castle.config
|
20
|
+
@no_allowlist = @config.allowlisted.empty?
|
21
21
|
end
|
22
22
|
|
23
23
|
# Serialize HTTP headers
|
@@ -12,7 +12,8 @@ module Castle
|
|
12
12
|
HTTP(?:_|-).*|
|
13
13
|
CONTENT(?:_|-)LENGTH|
|
14
14
|
REMOTE(?:_|-)ADDR
|
15
|
-
$/xi
|
15
|
+
$/xi
|
16
|
+
.freeze
|
16
17
|
|
17
18
|
private_constant :VALUABLE_HEADERS
|
18
19
|
|
@@ -25,12 +26,14 @@ module Castle
|
|
25
26
|
# Serialize HTTP headers
|
26
27
|
# @return [Hash]
|
27
28
|
def call
|
28
|
-
@request_env
|
29
|
-
|
29
|
+
@request_env
|
30
|
+
.keys
|
31
|
+
.each_with_object({}) do |header_name, acc|
|
32
|
+
next unless header_name.match(VALUABLE_HEADERS)
|
30
33
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
+
formatted_name = @header_format.call(header_name)
|
35
|
+
acc[formatted_name] = @request_env[header_name]
|
36
|
+
end
|
34
37
|
end
|
35
38
|
end
|
36
39
|
end
|