castle-rb 3.6.2 → 4.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 +41 -5
- data/lib/castle.rb +3 -1
- data/lib/castle/client.rb +20 -15
- data/lib/castle/configuration.rb +30 -3
- data/lib/castle/context/default.rb +36 -18
- data/lib/castle/context/sanitizer.rb +1 -0
- data/lib/castle/events.rb +49 -0
- data/lib/castle/extractors/client_id.rb +7 -3
- data/lib/castle/extractors/headers.rb +24 -30
- data/lib/castle/extractors/ip.rb +49 -5
- data/lib/castle/header_filter.rb +35 -0
- data/lib/castle/header_formatter.rb +3 -0
- data/lib/castle/validators/not_supported.rb +1 -0
- data/lib/castle/validators/present.rb +1 -0
- data/lib/castle/version.rb +1 -1
- data/spec/integration/rails/rails_spec.rb +61 -0
- data/spec/integration/rails/support/all.rb +6 -0
- data/spec/integration/rails/support/application.rb +15 -0
- data/spec/integration/rails/support/home_controller.rb +21 -0
- data/spec/lib/castle/api/request/build_spec.rb +4 -2
- data/spec/lib/castle/api/request_spec.rb +1 -0
- data/spec/lib/castle/client_spec.rb +5 -3
- data/spec/lib/castle/commands/authenticate_spec.rb +1 -0
- data/spec/lib/castle/commands/identify_spec.rb +1 -0
- data/spec/lib/castle/commands/impersonate_spec.rb +1 -0
- data/spec/lib/castle/commands/track_spec.rb +1 -0
- data/spec/lib/castle/configuration_spec.rb +18 -2
- data/spec/lib/castle/context/default_spec.rb +10 -11
- data/spec/lib/castle/events_spec.rb +5 -0
- data/spec/lib/castle/extractors/client_id_spec.rb +2 -1
- data/spec/lib/castle/extractors/headers_spec.rb +66 -49
- data/spec/lib/castle/extractors/ip_spec.rb +56 -12
- data/spec/lib/castle/header_filter_spec.rb +38 -0
- data/spec/lib/castle/header_formatter_spec.rb +1 -1
- data/spec/lib/castle/utils/cloner_spec.rb +1 -0
- data/spec/lib/castle/utils/timestamp_spec.rb +3 -4
- data/spec/lib/castle/utils_spec.rb +1 -1
- data/spec/lib/castle/validators/not_supported_spec.rb +1 -3
- metadata +31 -3
data/lib/castle/extractors/ip.rb
CHANGED
@@ -4,14 +4,58 @@ module Castle
|
|
4
4
|
module Extractors
|
5
5
|
# used for extraction of ip from the request
|
6
6
|
class IP
|
7
|
-
|
8
|
-
|
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'
|
11
|
+
|
12
|
+
private_constant :FALLBACK, :DEFAULT
|
13
|
+
|
14
|
+
# @param headers [Hash]
|
15
|
+
def initialize(headers)
|
16
|
+
@headers = headers
|
17
|
+
@ip_headers = Castle.config.ip_headers + DEFAULT
|
18
|
+
@proxies = Castle.config.trusted_proxies + Castle::Configuration::TRUSTED_PROXIES
|
9
19
|
end
|
10
20
|
|
21
|
+
# Order of headers:
|
22
|
+
# .... list of headers defined by ip_headers
|
23
|
+
# X-Forwarded-For
|
24
|
+
# Client-Ip is
|
25
|
+
# Remote-Addr
|
26
|
+
# @return [String]
|
11
27
|
def call
|
12
|
-
|
13
|
-
|
14
|
-
|
28
|
+
@ip_headers.each do |ip_header|
|
29
|
+
ip_value = calculate_ip(ip_header)
|
30
|
+
return ip_value if ip_value
|
31
|
+
end
|
32
|
+
|
33
|
+
@headers[FALLBACK]
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
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
|
+
# @param ips [Array<String>]
|
46
|
+
# @return [Array<String>]
|
47
|
+
def remove_proxies(ips)
|
48
|
+
ips.reject { |ip| @proxies.any? { |proxy| proxy.match(ip) } }
|
49
|
+
end
|
50
|
+
|
51
|
+
# @param header [String]
|
52
|
+
# @return [Array<String>]
|
53
|
+
def ips_from(header)
|
54
|
+
value = @headers[header]
|
55
|
+
|
56
|
+
return [] unless value
|
57
|
+
|
58
|
+
value.strip.split(/[,\s]+/).reverse
|
15
59
|
end
|
16
60
|
end
|
17
61
|
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Castle
|
4
|
+
# used for preparing valuable headers list
|
5
|
+
class HeaderFilter
|
6
|
+
# headers filter
|
7
|
+
# HTTP_ - this is how Rack prefixes incoming HTTP headers
|
8
|
+
# CONTENT_LENGTH - for responses without Content-Length or Transfer-Encoding header
|
9
|
+
# REMOTE_ADDR - ip address header returned by web server
|
10
|
+
VALUABLE_HEADERS = /^(
|
11
|
+
HTTP_.*|
|
12
|
+
CONTENT_LENGTH|
|
13
|
+
REMOTE_ADDR
|
14
|
+
)$/x.freeze
|
15
|
+
|
16
|
+
private_constant :VALUABLE_HEADERS
|
17
|
+
|
18
|
+
# @param request [Rack::Request]
|
19
|
+
def initialize(request)
|
20
|
+
@request_env = request.env
|
21
|
+
@formatter = HeaderFormatter.new
|
22
|
+
end
|
23
|
+
|
24
|
+
# Serialize HTTP headers
|
25
|
+
# @return [Hash]
|
26
|
+
def call
|
27
|
+
@request_env.keys.each_with_object({}) do |header_name, acc|
|
28
|
+
next unless header_name.match(VALUABLE_HEADERS)
|
29
|
+
|
30
|
+
formatted_name = @formatter.call(header_name)
|
31
|
+
acc[formatted_name] = @request_env[header_name]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
data/lib/castle/version.rb
CHANGED
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
require_relative 'support/all'
|
5
|
+
|
6
|
+
RSpec.describe HomeController, type: :request do
|
7
|
+
describe '#index' do
|
8
|
+
let(:request) do
|
9
|
+
{
|
10
|
+
'event' => '$login.succeeded',
|
11
|
+
'user_id' => '123',
|
12
|
+
'properties' => { 'key' => 'value' },
|
13
|
+
'user_traits' => { 'key' => 'value' },
|
14
|
+
'timestamp' => now.utc.iso8601(3),
|
15
|
+
'sent_at' => now.utc.iso8601(3),
|
16
|
+
'context' => {
|
17
|
+
'client_id' => '',
|
18
|
+
'active' => true,
|
19
|
+
'origin' => 'web',
|
20
|
+
'headers' => {
|
21
|
+
'Accept' => 'text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5',
|
22
|
+
'Authorization' => true,
|
23
|
+
'Content-Length' => '0',
|
24
|
+
'Cookie' => true,
|
25
|
+
'Host' => 'www.example.com',
|
26
|
+
'X-Forwarded-For' => '5.5.5.5, 1.2.3.4',
|
27
|
+
'Remote-Addr' => '127.0.0.1'
|
28
|
+
},
|
29
|
+
'ip' => '1.2.3.4',
|
30
|
+
'library' => {
|
31
|
+
'name' => 'castle-rb',
|
32
|
+
'version' => Castle::VERSION
|
33
|
+
}
|
34
|
+
}
|
35
|
+
}
|
36
|
+
end
|
37
|
+
let(:now) { Time.now }
|
38
|
+
let(:headers) do
|
39
|
+
{
|
40
|
+
'HTTP_AUTHORIZATION' => 'Basic 123',
|
41
|
+
'HTTP_X_FORWARDED_FOR' => '5.5.5.5, 1.2.3.4'
|
42
|
+
}
|
43
|
+
end
|
44
|
+
|
45
|
+
before do
|
46
|
+
Timecop.freeze(now)
|
47
|
+
stub_request(:post, 'https://api.castle.io/v1/track')
|
48
|
+
get '/', headers: headers
|
49
|
+
end
|
50
|
+
|
51
|
+
after { Timecop.return }
|
52
|
+
|
53
|
+
it do
|
54
|
+
assert_requested :post, 'https://api.castle.io/v1/track', times: 1 do |req|
|
55
|
+
JSON.parse(req.body) == request
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
it { expect(response).to be_successful }
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'action_controller/railtie'
|
4
|
+
|
5
|
+
class TestApp < Rails::Application
|
6
|
+
secrets.secret_token = 'secret_token'
|
7
|
+
secrets.secret_key_base = 'secret_key_base'
|
8
|
+
|
9
|
+
config.logger = Logger.new($stdout)
|
10
|
+
Rails.logger = config.logger
|
11
|
+
|
12
|
+
routes.draw do
|
13
|
+
get '/' => 'home#index'
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class HomeController < ActionController::Base
|
4
|
+
def index
|
5
|
+
request_context = ::Castle::Client.to_context(request)
|
6
|
+
track_options = ::Castle::Client.to_options(
|
7
|
+
event: '$login.succeeded',
|
8
|
+
user_id: '123',
|
9
|
+
properties: {
|
10
|
+
key: 'value'
|
11
|
+
},
|
12
|
+
user_traits: {
|
13
|
+
key: 'value'
|
14
|
+
}
|
15
|
+
)
|
16
|
+
client = ::Castle::Client.new(request_context)
|
17
|
+
client.track(track_options)
|
18
|
+
|
19
|
+
render inline: 'hello'
|
20
|
+
end
|
21
|
+
end
|
@@ -21,11 +21,13 @@ describe Castle::API::Request::Build do
|
|
21
21
|
|
22
22
|
context 'when post' do
|
23
23
|
let(:time) { Time.now.utc.iso8601(3) }
|
24
|
-
let(:command)
|
24
|
+
let(:command) do
|
25
|
+
Castle::Commands::Track.new({}).build(event: '$login.succeeded', name: "\xC4")
|
26
|
+
end
|
25
27
|
let(:expected_body) do
|
26
28
|
{
|
27
29
|
event: '$login.succeeded',
|
28
|
-
name:
|
30
|
+
name: '�',
|
29
31
|
context: {},
|
30
32
|
sent_at: time
|
31
33
|
}
|
@@ -244,11 +244,13 @@ describe Castle::Client do
|
|
244
244
|
it { expect(request_response[:action]).to be_eql('allow') }
|
245
245
|
it { expect(request_response[:user_id]).to be_eql('1234') }
|
246
246
|
it { expect(request_response[:failover]).to be true }
|
247
|
-
it { expect(request_response[:failover_reason]).to be_eql('Castle set to do not track.') }
|
247
|
+
it { expect(request_response[:failover_reason]).to be_eql('Castle is set to do not track.') }
|
248
248
|
end
|
249
249
|
|
250
250
|
context 'when request with fail' do
|
251
|
-
before
|
251
|
+
before do
|
252
|
+
allow(Castle::API).to receive(:request).and_raise(Castle::RequestError.new(Timeout::Error))
|
253
|
+
end
|
252
254
|
|
253
255
|
context 'with request error and throw strategy' do
|
254
256
|
before { allow(Castle.config).to receive(:failover_strategy).and_return(:throw) }
|
@@ -274,7 +276,7 @@ describe Castle::Client do
|
|
274
276
|
it { expect { request_response }.to raise_error(Castle::InternalServerError) }
|
275
277
|
end
|
276
278
|
|
277
|
-
|
279
|
+
describe 'not throw on eg deny strategy' do
|
278
280
|
it { assert_not_requested :post, 'https://:secret@api.castle.io/v1/authenticate' }
|
279
281
|
it { expect(request_response[:action]).to be_eql('allow') }
|
280
282
|
it { expect(request_response[:user_id]).to be_eql('1234') }
|
@@ -31,14 +31,25 @@ describe Castle::Configuration do
|
|
31
31
|
|
32
32
|
describe 'api_secret' do
|
33
33
|
context 'with env' do
|
34
|
+
let(:secret_key_env) { 'secret_key_env' }
|
35
|
+
let(:secret_key) { 'secret_key' }
|
36
|
+
|
34
37
|
before do
|
35
38
|
allow(ENV).to receive(:fetch).with(
|
36
39
|
'CASTLE_API_SECRET', ''
|
37
|
-
).and_return(
|
40
|
+
).and_return(secret_key_env)
|
38
41
|
end
|
39
42
|
|
40
43
|
it do
|
41
|
-
expect(config.api_secret).to be_eql(
|
44
|
+
expect(config.api_secret).to be_eql(secret_key_env)
|
45
|
+
end
|
46
|
+
|
47
|
+
context 'when key is overwritten' do
|
48
|
+
before { config.api_secret = secret_key }
|
49
|
+
|
50
|
+
it do
|
51
|
+
expect(config.api_secret).to be_eql(secret_key)
|
52
|
+
end
|
42
53
|
end
|
43
54
|
end
|
44
55
|
|
@@ -48,6 +59,7 @@ describe Castle::Configuration do
|
|
48
59
|
before do
|
49
60
|
config.api_secret = value
|
50
61
|
end
|
62
|
+
|
51
63
|
it do
|
52
64
|
expect(config.api_secret).to be_eql(value)
|
53
65
|
end
|
@@ -69,6 +81,7 @@ describe Castle::Configuration do
|
|
69
81
|
before do
|
70
82
|
config.request_timeout = value
|
71
83
|
end
|
84
|
+
|
72
85
|
it do
|
73
86
|
expect(config.request_timeout).to be_eql(value)
|
74
87
|
end
|
@@ -84,6 +97,7 @@ describe Castle::Configuration do
|
|
84
97
|
before do
|
85
98
|
config.whitelisted = ['header']
|
86
99
|
end
|
100
|
+
|
87
101
|
it do
|
88
102
|
expect(config.whitelisted).to be_eql(['Header'])
|
89
103
|
end
|
@@ -99,6 +113,7 @@ describe Castle::Configuration do
|
|
99
113
|
before do
|
100
114
|
config.blacklisted = ['header']
|
101
115
|
end
|
116
|
+
|
102
117
|
it do
|
103
118
|
expect(config.blacklisted).to be_eql(['Header'])
|
104
119
|
end
|
@@ -114,6 +129,7 @@ describe Castle::Configuration do
|
|
114
129
|
before do
|
115
130
|
config.failover_strategy = :deny
|
116
131
|
end
|
132
|
+
|
117
133
|
it do
|
118
134
|
expect(config.failover_strategy).to be_eql(:deny)
|
119
135
|
end
|
@@ -16,24 +16,23 @@ describe Castle::Context::Default do
|
|
16
16
|
let(:request) { Rack::Request.new(env) }
|
17
17
|
let(:default_context) { subject.call }
|
18
18
|
let(:version) { '2.2.0' }
|
19
|
-
|
20
|
-
|
21
|
-
stub_const('Castle::VERSION', version)
|
22
|
-
end
|
23
|
-
|
24
|
-
it { expect(default_context[:active]).to be_eql(true) }
|
25
|
-
it { expect(default_context[:origin]).to be_eql('web') }
|
26
|
-
|
27
|
-
it do
|
28
|
-
expect(default_context[:headers]).to be_eql(
|
19
|
+
let(:result_headers) do
|
20
|
+
{
|
29
21
|
'X-Forwarded-For' => '1.2.3.4',
|
30
22
|
'Accept-Language' => 'en',
|
31
23
|
'User-Agent' => 'test',
|
32
24
|
'Content-Length' => '0',
|
33
25
|
'Cookie' => true
|
34
|
-
|
26
|
+
}
|
35
27
|
end
|
36
28
|
|
29
|
+
before do
|
30
|
+
stub_const('Castle::VERSION', version)
|
31
|
+
end
|
32
|
+
|
33
|
+
it { expect(default_context[:active]).to be_eql(true) }
|
34
|
+
it { expect(default_context[:origin]).to be_eql('web') }
|
35
|
+
it { expect(default_context[:headers]).to be_eql(result_headers) }
|
37
36
|
it { expect(default_context[:ip]).to be_eql(ip) }
|
38
37
|
it { expect(default_context[:library][:name]).to be_eql('castle-rb') }
|
39
38
|
it { expect(default_context[:library][:version]).to be_eql(version) }
|
@@ -1,8 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
describe Castle::Extractors::ClientId do
|
4
|
-
subject(:extractor) { described_class.new(
|
4
|
+
subject(:extractor) { described_class.new(formatted_headers, cookies) }
|
5
5
|
|
6
|
+
let(:formatted_headers) { Castle::HeaderFilter.new(request).call }
|
6
7
|
let(:client_id_cookie) { 'abcd' }
|
7
8
|
let(:client_id_header) { 'abcde' }
|
8
9
|
let(:cookies) { request.cookies }
|