castle-rb 4.0.0 → 5.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -4,7 +4,7 @@ module Castle
4
4
  module Context
5
5
  class Default
6
6
  def initialize(request, cookies = nil)
7
- @pre_headers = HeaderFilter.new(request).call
7
+ @pre_headers = HeadersFilter.new(request).call
8
8
  @cookies = cookies || request.cookies
9
9
  @request = request
10
10
  end
@@ -4,18 +4,18 @@ module Castle
4
4
  module Extractors
5
5
  # used for extraction of cookies and headers from the request
6
6
  class Headers
7
- # Headers that we will never scrub, even if they land on the configuration blacklist.
8
- ALWAYS_WHITELISTED = %w[User-Agent].freeze
7
+ # Headers that we will never scrub, even if they land on the configuration denylist.
8
+ ALWAYS_ALLOWLISTED = %w[User-Agent].freeze
9
9
 
10
- # Headers that will always be scrubbed, even if whitelisted.
11
- ALWAYS_BLACKLISTED = %w[Cookie Authorization].freeze
10
+ # Headers that will always be scrubbed, even if allowlisted.
11
+ ALWAYS_DENYLISTED = %w[Cookie Authorization].freeze
12
12
 
13
- private_constant :ALWAYS_WHITELISTED, :ALWAYS_BLACKLISTED
13
+ private_constant :ALWAYS_ALLOWLISTED, :ALWAYS_DENYLISTED
14
14
 
15
15
  # @param headers [Hash]
16
16
  def initialize(headers)
17
17
  @headers = headers
18
- @no_whitelist = Castle.config.whitelisted.empty?
18
+ @no_allowlist = Castle.config.allowlisted.empty?
19
19
  end
20
20
 
21
21
  # Serialize HTTP headers
@@ -33,10 +33,10 @@ module Castle
33
33
  # @param value [String]
34
34
  # @return [TrueClass | FalseClass | String]
35
35
  def header_value(name, value)
36
- return true if ALWAYS_BLACKLISTED.include?(name)
37
- return value if ALWAYS_WHITELISTED.include?(name)
38
- return true if Castle.config.blacklisted.include?(name)
39
- return value if @no_whitelist || Castle.config.whitelisted.include?(name)
36
+ return true if ALWAYS_DENYLISTED.include?(name)
37
+ return value if ALWAYS_ALLOWLISTED.include?(name)
38
+ return true if Castle.config.denylisted.include?(name)
39
+ return value if @no_allowlist || Castle.config.allowlisted.include?(name)
40
40
 
41
41
  true
42
42
  end
@@ -5,47 +5,56 @@ module Castle
5
5
  # used for extraction of ip from the request
6
6
  class IP
7
7
  # ordered list of ip headers for ip extraction
8
- DEFAULT = %w[X-Forwarded-For Client-Ip Remote-Addr].freeze
9
- # default header fallback when ip is not found
10
- FALLBACK = 'Remote-Addr'
8
+ DEFAULT = %w[X-Forwarded-For Remote-Addr].freeze
9
+ # list of header which are used with proxy depth setting
10
+ DEPTH_RELATED = %w[X-Forwarded-For].freeze
11
11
 
12
- private_constant :FALLBACK, :DEFAULT
12
+ private_constant :DEFAULT
13
13
 
14
14
  # @param headers [Hash]
15
15
  def initialize(headers)
16
16
  @headers = headers
17
- @ip_headers = Castle.config.ip_headers + DEFAULT
17
+ @ip_headers = Castle.config.ip_headers.empty? ? DEFAULT : Castle.config.ip_headers
18
18
  @proxies = Castle.config.trusted_proxies + Castle::Configuration::TRUSTED_PROXIES
19
+ @trust_proxy_chain = Castle.config.trust_proxy_chain
20
+ @trusted_proxy_depth = Castle.config.trusted_proxy_depth
19
21
  end
20
22
 
21
23
  # Order of headers:
22
24
  # .... list of headers defined by ip_headers
23
25
  # X-Forwarded-For
24
- # Client-Ip is
25
26
  # Remote-Addr
26
27
  # @return [String]
27
28
  def call
29
+ all_ips = []
30
+
28
31
  @ip_headers.each do |ip_header|
29
- ip_value = calculate_ip(ip_header)
32
+ ips = ips_from(ip_header)
33
+ ip_value = remove_proxies(ips)
34
+
30
35
  return ip_value if ip_value
36
+
37
+ all_ips.push(*ips)
31
38
  end
32
39
 
33
- @headers[FALLBACK]
40
+ # fallback to first listed ip
41
+ all_ips.first
34
42
  end
35
43
 
36
44
  private
37
45
 
38
- # @param header [String]
39
- # @return [String]
40
- def calculate_ip(header)
41
- ips = ips_from(header)
42
- remove_proxies(ips).first
43
- end
44
-
45
46
  # @param ips [Array<String>]
46
47
  # @return [Array<String>]
47
48
  def remove_proxies(ips)
48
- ips.reject { |ip| @proxies.any? { |proxy| proxy.match(ip) } }
49
+ return ips.first if @trust_proxy_chain
50
+
51
+ ips.reject { |ip| proxy?(ip) }.last
52
+ end
53
+
54
+ # @param ip [String]
55
+ # @return [Boolean]
56
+ def proxy?(ip)
57
+ @proxies.any? { |proxy| proxy.match(ip) }
49
58
  end
50
59
 
51
60
  # @param header [String]
@@ -55,7 +64,18 @@ module Castle
55
64
 
56
65
  return [] unless value
57
66
 
58
- value.strip.split(/[,\s]+/).reverse
67
+ ips = value.strip.split(/[,\s]+/)
68
+
69
+ limit_proxy_depth(ips, header)
70
+ end
71
+
72
+ # @param ips [Array<String>]
73
+ # @param ip_header [String]
74
+ # @return [Array<String>]
75
+ def limit_proxy_depth(ips, ip_header)
76
+ ips.pop(@trusted_proxy_depth) if DEPTH_RELATED.include?(ip_header)
77
+
78
+ ips
59
79
  end
60
80
  end
61
81
  end
@@ -2,23 +2,23 @@
2
2
 
3
3
  module Castle
4
4
  # used for preparing valuable headers list
5
- class HeaderFilter
5
+ class HeadersFilter
6
6
  # headers filter
7
7
  # HTTP_ - this is how Rack prefixes incoming HTTP headers
8
8
  # CONTENT_LENGTH - for responses without Content-Length or Transfer-Encoding header
9
9
  # REMOTE_ADDR - ip address header returned by web server
10
- VALUABLE_HEADERS = /^(
11
- HTTP_.*|
12
- CONTENT_LENGTH|
13
- REMOTE_ADDR
14
- )$/x.freeze
10
+ VALUABLE_HEADERS = /^
11
+ HTTP(?:_|-).*|
12
+ CONTENT(?:_|-)LENGTH|
13
+ REMOTE(?:_|-)ADDR
14
+ $/xi.freeze
15
15
 
16
16
  private_constant :VALUABLE_HEADERS
17
17
 
18
18
  # @param request [Rack::Request]
19
19
  def initialize(request)
20
20
  @request_env = request.env
21
- @formatter = HeaderFormatter.new
21
+ @formatter = HeadersFormatter
22
22
  end
23
23
 
24
24
  # Serialize HTTP headers
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castle
4
+ # formats header name
5
+ class HeadersFormatter
6
+ class << self
7
+ # @param header [String]
8
+ # @return [String]
9
+ def call(header)
10
+ format(header.to_s.gsub(/^HTTP(?:_|-)/i, ''))
11
+ end
12
+
13
+ private
14
+
15
+ # @param header [String]
16
+ # @return [String]
17
+ def format(header)
18
+ header.split(/_|-/).map(&:capitalize).join('-')
19
+ end
20
+ end
21
+ end
22
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Castle
4
- VERSION = '4.0.0'
4
+ VERSION = '5.0.0'
5
5
  end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe Castle::API::Connection do
4
+ describe '.call' do
5
+ subject(:class_call) { described_class.call }
6
+
7
+ context 'when ssl false' do
8
+ let(:localhost) { 'localhost' }
9
+ let(:port) { 3002 }
10
+ let(:api_url) { '/test' }
11
+
12
+ before do
13
+ Castle.config.url = 'http://localhost:3002'
14
+
15
+ allow(Net::HTTP)
16
+ .to receive(:new)
17
+ .with(localhost, port)
18
+ .and_call_original
19
+ end
20
+
21
+ it do
22
+ class_call
23
+
24
+ expect(Net::HTTP)
25
+ .to have_received(:new)
26
+ .with(localhost, port)
27
+ end
28
+
29
+ it do
30
+ expect(class_call).to be_an_instance_of(Net::HTTP)
31
+ end
32
+ end
33
+
34
+ context 'when ssl true' do
35
+ let(:localhost) { 'localhost' }
36
+ let(:port) { 443 }
37
+
38
+ before do
39
+ Castle.config.url = 'https://localhost'
40
+ end
41
+
42
+ context 'with block' do
43
+ let(:api_url) { '/test' }
44
+ let(:request) { Net::HTTP::Get.new(api_url) }
45
+
46
+ before do
47
+ allow(Net::HTTP)
48
+ .to receive(:new)
49
+ .with(localhost, port)
50
+ .and_call_original
51
+ end
52
+
53
+ it do
54
+ expect(class_call).to be_an_instance_of(Net::HTTP)
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -2,59 +2,97 @@
2
2
 
3
3
  describe Castle::API::Request do
4
4
  describe '#call' do
5
- subject(:call) { described_class.call(command, api_secret, headers) }
6
-
7
- let(:http) { instance_double('Net::HTTP') }
8
- let(:request_build) { instance_double('Castle::API::Request::Build') }
9
5
  let(:command) { Castle::Commands::Track.new({}).build(event: '$login.succeeded') }
10
6
  let(:headers) { {} }
11
7
  let(:api_secret) { 'secret' }
8
+ let(:request_build) { {} }
12
9
  let(:expected_headers) { { 'Content-Type' => 'application/json' } }
10
+ let(:http) { instance_double('Net::HTTP') }
13
11
 
14
- before do
15
- allow(described_class).to receive(:http).and_return(http)
16
- allow(http).to receive(:request).with(request_build)
17
- allow(Castle::API::Request::Build).to receive(:call)
18
- .with(command, expected_headers, api_secret)
19
- .and_return(request_build)
20
- call
21
- end
12
+ context 'without http arg provided' do
13
+ subject(:call) { described_class.call(command, api_secret, headers) }
22
14
 
23
- it do
24
- expect(Castle::API::Request::Build).to have_received(:call)
25
- .with(command, expected_headers, api_secret)
26
- end
15
+ let(:http) { instance_double('Net::HTTP') }
16
+ let(:command) { Castle::Commands::Track.new({}).build(event: '$login.succeeded') }
17
+ let(:headers) { {} }
18
+ let(:api_secret) { 'secret' }
19
+ let(:request_build) { {} }
20
+ let(:expected_headers) { { 'Content-Type' => 'application/json' } }
27
21
 
28
- it { expect(http).to have_received(:request).with(request_build) }
29
- end
22
+ before do
23
+ allow(Castle::API::Connection).to receive(:call).and_return(http)
24
+ allow(http).to receive(:request)
25
+ allow(described_class).to receive(:build).and_return(request_build)
26
+ call
27
+ end
28
+
29
+ it do
30
+ expect(described_class).to have_received(:build).with(command, expected_headers, api_secret)
31
+ end
32
+
33
+ it { expect(http).to have_received(:request).with(request_build) }
34
+ end
30
35
 
31
- describe '#http' do
32
- subject(:http) { described_class.http }
36
+ context 'with http arg provided' do
37
+ subject(:call) { described_class.call(command, api_secret, headers, http) }
33
38
 
34
- context 'when ssl false' do
35
39
  before do
36
- Castle.config.host = 'localhost'
37
- Castle.config.port = 3002
40
+ allow(Castle::API::Connection).to receive(:call)
41
+ allow(http).to receive(:request)
42
+ allow(described_class).to receive(:build).and_return(request_build)
43
+ call
38
44
  end
39
45
 
40
- after do
41
- Castle.config.host = Castle::Configuration::HOST
42
- Castle.config.port = Castle::Configuration::PORT
46
+ it { expect(Castle::API::Connection).not_to have_received(:call) }
47
+
48
+ it do
49
+ expect(described_class).to have_received(:build).with(command, expected_headers, api_secret)
43
50
  end
44
51
 
45
- it { expect(http).to be_instance_of(Net::HTTP) }
46
- it { expect(http.address).to eq(Castle.config.host) }
47
- it { expect(http.port).to eq(Castle.config.port) }
48
- it { expect(http.use_ssl?).to be false }
49
- it { expect(http.verify_mode).to be_nil }
52
+ it { expect(http).to have_received(:request).with(request_build) }
53
+ end
54
+ end
55
+
56
+ describe '#build' do
57
+ subject(:build) { described_class.build(command, headers, api_secret) }
58
+
59
+ let(:headers) { { 'SAMPLE-HEADER' => '1' } }
60
+ let(:api_secret) { 'secret' }
61
+
62
+ context 'when get' do
63
+ let(:command) { Castle::Commands::Review.build(review_id) }
64
+ let(:review_id) { SecureRandom.uuid }
65
+
66
+ it { expect(build.body).to be_nil }
67
+ it { expect(build.method).to eql('GET') }
68
+ it { expect(build.path).to eql("/v1/#{command.path}") }
69
+ it { expect(build.to_hash).to have_key('authorization') }
70
+ it { expect(build.to_hash).to have_key('sample-header') }
71
+ it { expect(build.to_hash['sample-header']).to eql(['1']) }
50
72
  end
51
73
 
52
- context 'when ssl true' do
53
- it { expect(http).to be_instance_of(Net::HTTP) }
54
- it { expect(http.address).to eq(Castle.config.host) }
55
- it { expect(http.port).to eq(Castle.config.port) }
56
- it { expect(http.use_ssl?).to be true }
57
- it { expect(http.verify_mode).to eq(OpenSSL::SSL::VERIFY_PEER) }
74
+ context 'when post' do
75
+ let(:time) { Time.now.utc.iso8601(3) }
76
+ let(:command) do
77
+ Castle::Commands::Track.new({}).build(event: '$login.succeeded', name: "\xC4")
78
+ end
79
+ let(:expected_body) do
80
+ {
81
+ event: '$login.succeeded',
82
+ name: '�',
83
+ context: {},
84
+ sent_at: time
85
+ }
86
+ end
87
+
88
+ before { allow(Castle::Utils::Timestamp).to receive(:call).and_return(time) }
89
+
90
+ it { expect(build.body).to be_eql(expected_body.to_json) }
91
+ it { expect(build.method).to eql('POST') }
92
+ it { expect(build.path).to eql("/v1/#{command.path}") }
93
+ it { expect(build.to_hash).to have_key('authorization') }
94
+ it { expect(build.to_hash).to have_key('sample-header') }
95
+ it { expect(build.to_hash['sample-header']).to eql(['1']) }
58
96
  end
59
97
  end
60
98
  end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe Castle::API::Session do
4
+ describe '.call' do
5
+ context 'when ssl false' do
6
+ let(:localhost) { 'localhost' }
7
+ let(:port) { 3002 }
8
+
9
+ before do
10
+ Castle.config.url = 'http://localhost:3002'
11
+ stub_request(:get, 'localhost:3002/test').to_return(status: 200, body: '{}', headers: {})
12
+ end
13
+
14
+ context 'with block' do
15
+ let(:api_url) { '/test' }
16
+ let(:request) { Net::HTTP::Get.new(api_url) }
17
+
18
+ before do
19
+ allow(Net::HTTP)
20
+ .to receive(:new)
21
+ .with(localhost, port)
22
+ .and_call_original
23
+
24
+ described_class.call do |http|
25
+ http.request(request)
26
+ end
27
+ end
28
+
29
+ it do
30
+ expect(Net::HTTP)
31
+ .to have_received(:new)
32
+ .with(localhost, port)
33
+ end
34
+
35
+ it do
36
+ expect(a_request(:get, 'localhost:3002/test'))
37
+ .to have_been_made.once
38
+ end
39
+ end
40
+
41
+ context 'without block' do
42
+ before { described_class.call }
43
+
44
+ it do
45
+ expect(a_request(:get, 'localhost:3002/test'))
46
+ .not_to have_been_made
47
+ end
48
+ end
49
+ end
50
+
51
+ context 'when ssl true' do
52
+ let(:localhost) { 'localhost' }
53
+ let(:port) { 443 }
54
+
55
+ before do
56
+ Castle.config.url = 'https://localhost'
57
+ stub_request(:get, 'https://localhost/test').to_return(status: 200, body: '{}', headers: {})
58
+ end
59
+
60
+ context 'with block' do
61
+ let(:api_url) { '/test' }
62
+ let(:request) { Net::HTTP::Get.new(api_url) }
63
+
64
+ before do
65
+ allow(Net::HTTP)
66
+ .to receive(:new)
67
+ .with(localhost, port)
68
+ .and_call_original
69
+
70
+ allow(Net::HTTP)
71
+ .to receive(:start)
72
+
73
+ described_class.call do |http|
74
+ http.request(request)
75
+ end
76
+ end
77
+
78
+ it do
79
+ expect(Net::HTTP)
80
+ .to have_received(:new)
81
+ .with(localhost, port)
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end