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.
- checksums.yaml +4 -4
- data/README.md +61 -20
- data/lib/castle.rb +5 -4
- data/lib/castle/api.rb +16 -7
- data/lib/castle/api/connection.rb +24 -0
- data/lib/castle/api/request.rb +16 -11
- data/lib/castle/api/session.rb +20 -0
- data/lib/castle/client.rb +29 -8
- data/lib/castle/configuration.rb +45 -20
- data/lib/castle/context/default.rb +1 -1
- data/lib/castle/extractors/headers.rb +10 -10
- data/lib/castle/extractors/ip.rb +37 -17
- data/lib/castle/{header_filter.rb → headers_filter.rb} +7 -7
- data/lib/castle/headers_formatter.rb +22 -0
- data/lib/castle/version.rb +1 -1
- data/spec/lib/castle/api/connection_spec.rb +59 -0
- data/spec/lib/castle/api/request_spec.rb +75 -37
- data/spec/lib/castle/api/session_spec.rb +86 -0
- data/spec/lib/castle/api_spec.rb +4 -4
- data/spec/lib/castle/client_spec.rb +2 -2
- data/spec/lib/castle/commands/impersonate_spec.rb +2 -2
- data/spec/lib/castle/configuration_spec.rb +17 -16
- data/spec/lib/castle/context/default_spec.rb +3 -2
- data/spec/lib/castle/extractors/client_id_spec.rb +1 -1
- data/spec/lib/castle/extractors/headers_spec.rb +11 -12
- data/spec/lib/castle/extractors/ip_spec.rb +39 -6
- data/spec/lib/castle/{header_filter_spec.rb → headers_filter_spec.rb} +6 -6
- data/spec/lib/castle/{header_formatter_spec.rb → headers_formatter_spec.rb} +2 -2
- data/spec/spec_helper.rb +1 -2
- metadata +18 -15
- data/lib/castle/api/request/build.rb +0 -27
- data/lib/castle/header_formatter.rb +0 -12
- data/spec/lib/castle/api/request/build_spec.rb +0 -46
@@ -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
|
8
|
-
|
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
|
11
|
-
|
10
|
+
# Headers that will always be scrubbed, even if allowlisted.
|
11
|
+
ALWAYS_DENYLISTED = %w[Cookie Authorization].freeze
|
12
12
|
|
13
|
-
private_constant :
|
13
|
+
private_constant :ALWAYS_ALLOWLISTED, :ALWAYS_DENYLISTED
|
14
14
|
|
15
15
|
# @param headers [Hash]
|
16
16
|
def initialize(headers)
|
17
17
|
@headers = headers
|
18
|
-
@
|
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
|
37
|
-
return value if
|
38
|
-
return true if Castle.config.
|
39
|
-
return value if @
|
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
|
data/lib/castle/extractors/ip.rb
CHANGED
@@ -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
|
9
|
-
#
|
10
|
-
|
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 :
|
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
|
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
|
-
|
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
|
-
|
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.
|
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]+/)
|
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
|
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
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
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 =
|
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
|
data/lib/castle/version.rb
CHANGED
@@ -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
|
-
|
15
|
-
|
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
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
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
|
-
|
29
|
-
|
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
|
-
|
32
|
-
|
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.
|
37
|
-
|
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
|
-
|
41
|
-
|
42
|
-
|
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
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
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
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
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
|