castle-rb 4.0.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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