castle-rb 3.5.0 → 3.5.1
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 +0 -1
- data/lib/castle.rb +38 -33
- data/lib/castle/api.rb +29 -35
- data/lib/castle/api/request.rb +37 -0
- data/lib/castle/api/request/build.rb +27 -0
- data/lib/castle/api/response.rb +40 -0
- data/lib/castle/client.rb +5 -6
- data/lib/castle/configuration.rb +8 -5
- data/lib/castle/errors.rb +11 -2
- data/lib/castle/review.rb +3 -3
- data/lib/castle/version.rb +1 -1
- data/spec/lib/castle/api/request/build_spec.rb +44 -0
- data/spec/lib/castle/api/request_spec.rb +59 -0
- data/spec/lib/castle/api/response_spec.rb +58 -0
- data/spec/lib/castle/api_spec.rb +15 -23
- data/spec/lib/castle/client_spec.rb +4 -4
- data/spec/lib/castle/review_spec.rb +1 -5
- metadata +12 -9
- data/lib/castle/request.rb +0 -28
- data/lib/castle/response.rb +0 -45
- data/spec/lib/castle/request_spec.rb +0 -40
- data/spec/lib/castle/response_spec.rb +0 -69
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a710c22d8a912a0918f42fe092f6c3b883a4a03ad7e2b86572b17b6ee901931e
|
4
|
+
data.tar.gz: e7325e9e957881ddcee78729d6274805d29b62c1f468e26fd03d34cf88286ac3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 84f69693b63c3c52b3084348aba0bc5e28e047f6f56a5a756559abf895e8031d8c33df06f385c6383c62bcaa240dfb1b8d3550dc7bb6f9b3872b9dd1bf7851ae
|
7
|
+
data.tar.gz: 24ecbd126fa6bb601acf00c442d8204c6042c87b9c47709af42e639e40409b70feca5c06f1c0fab50d7c9d0eb5ba20390229c6f7b6c74f170ab1918b5dec3b03
|
data/README.md
CHANGED
@@ -3,7 +3,6 @@
|
|
3
3
|
[](https://travis-ci.org/castle/castle-ruby)
|
4
4
|
[](https://coveralls.io/github/castle/castle-ruby?branch=coveralls)
|
5
5
|
[](https://badge.fury.io/rb/castle-rb)
|
6
|
-
[](https://gemnasium.com/github.com/castle/castle-ruby)
|
7
6
|
|
8
7
|
**[Castle](https://castle.io) analyzes device, location, and interaction patterns in your web and mobile apps and lets you stop account takeover attacks in real-time..**
|
9
8
|
|
data/lib/castle.rb
CHANGED
@@ -1,39 +1,44 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
3
|
+
%w[
|
4
|
+
openssl
|
5
|
+
net/http
|
6
|
+
json
|
7
|
+
time
|
8
|
+
].each(&method(:require))
|
7
9
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
10
|
+
%w[
|
11
|
+
castle/version
|
12
|
+
castle/errors
|
13
|
+
castle/command
|
14
|
+
castle/utils
|
15
|
+
castle/utils/merger
|
16
|
+
castle/utils/cloner
|
17
|
+
castle/utils/timestamp
|
18
|
+
castle/validators/present
|
19
|
+
castle/validators/not_supported
|
20
|
+
castle/context/merger
|
21
|
+
castle/context/sanitizer
|
22
|
+
castle/context/default
|
23
|
+
castle/commands/identify
|
24
|
+
castle/commands/authenticate
|
25
|
+
castle/commands/track
|
26
|
+
castle/commands/review
|
27
|
+
castle/commands/impersonate
|
28
|
+
castle/configuration
|
29
|
+
castle/failover_auth_response
|
30
|
+
castle/client
|
31
|
+
castle/header_formatter
|
32
|
+
castle/secure_mode
|
33
|
+
castle/extractors/client_id
|
34
|
+
castle/extractors/headers
|
35
|
+
castle/extractors/ip
|
36
|
+
castle/api/response
|
37
|
+
castle/api/request
|
38
|
+
castle/api/request/build
|
39
|
+
castle/review
|
40
|
+
castle/api
|
41
|
+
].each(&method(:require))
|
37
42
|
|
38
43
|
# main sdk module
|
39
44
|
module Castle
|
data/lib/castle/api.rb
CHANGED
@@ -2,44 +2,38 @@
|
|
2
2
|
|
3
3
|
module Castle
|
4
4
|
# this class is responsible for making requests to api
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
command.method
|
17
|
-
)
|
18
|
-
perform_request(request)
|
19
|
-
end
|
5
|
+
module API
|
6
|
+
# Errors we handle internally
|
7
|
+
HANDLED_ERRORS = [
|
8
|
+
Timeout::Error,
|
9
|
+
Errno::EINVAL,
|
10
|
+
Errno::ECONNRESET,
|
11
|
+
EOFError,
|
12
|
+
Net::HTTPBadResponse,
|
13
|
+
Net::HTTPHeaderSyntaxError,
|
14
|
+
Net::ProtocolError
|
15
|
+
].freeze
|
20
16
|
|
21
|
-
|
17
|
+
private_constant :HANDLED_ERRORS
|
22
18
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
prepare_http_for_ssl(http) if @config.port == 443
|
27
|
-
http
|
28
|
-
end
|
29
|
-
|
30
|
-
def prepare_http_for_ssl(http)
|
31
|
-
http.use_ssl = true
|
32
|
-
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
33
|
-
end
|
19
|
+
class << self
|
20
|
+
def request(command, headers = {})
|
21
|
+
raise Castle::ConfigurationError, 'configuration is not valid' unless Castle.config.valid?
|
34
22
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
23
|
+
begin
|
24
|
+
Castle::API::Response.call(
|
25
|
+
Castle::API::Request.call(
|
26
|
+
command,
|
27
|
+
Castle.config.api_secret,
|
28
|
+
headers
|
29
|
+
)
|
30
|
+
)
|
31
|
+
rescue *HANDLED_ERRORS => error
|
32
|
+
# @note We need to initialize the error, as the original error is a cause for this
|
33
|
+
# custom exception. If we would do it the default Ruby way, the original error
|
34
|
+
# would get converted into a string
|
35
|
+
raise Castle::RequestError.new(error) # rubocop:disable Style/RaiseArgs
|
36
|
+
end
|
43
37
|
end
|
44
38
|
end
|
45
39
|
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Castle
|
4
|
+
# this class is responsible for making requests to api
|
5
|
+
module API
|
6
|
+
module Request
|
7
|
+
# Default headers that we add to passed ones
|
8
|
+
DEFAULT_HEADERS = {
|
9
|
+
'Content-Type' => 'application/json'
|
10
|
+
}.freeze
|
11
|
+
|
12
|
+
private_constant :DEFAULT_HEADERS
|
13
|
+
|
14
|
+
class << self
|
15
|
+
def call(command, api_secret, headers)
|
16
|
+
http.request(
|
17
|
+
Castle::API::Request::Build.call(
|
18
|
+
command,
|
19
|
+
headers.merge(DEFAULT_HEADERS),
|
20
|
+
api_secret
|
21
|
+
)
|
22
|
+
)
|
23
|
+
end
|
24
|
+
|
25
|
+
def http
|
26
|
+
http = Net::HTTP.new(Castle.config.host, Castle.config.port)
|
27
|
+
http.read_timeout = Castle.config.request_timeout / 1000.0
|
28
|
+
if Castle.config.port == 443
|
29
|
+
http.use_ssl = true
|
30
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
31
|
+
end
|
32
|
+
http
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Castle
|
4
|
+
module API
|
5
|
+
# generate api request
|
6
|
+
module Request
|
7
|
+
module Build
|
8
|
+
class << self
|
9
|
+
def call(command, headers, api_secret)
|
10
|
+
request = Net::HTTP.const_get(
|
11
|
+
command.method.to_s.capitalize
|
12
|
+
).new("/#{Castle.config.url_prefix}/#{command.path}", headers)
|
13
|
+
|
14
|
+
unless command.method == :get
|
15
|
+
request.body = ::Castle::Utils.replace_invalid_characters(
|
16
|
+
command.data
|
17
|
+
).to_json
|
18
|
+
end
|
19
|
+
|
20
|
+
request.basic_auth('', api_secret)
|
21
|
+
request
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Castle
|
4
|
+
module API
|
5
|
+
# parses api response
|
6
|
+
module Response
|
7
|
+
RESPONSE_ERRORS = {
|
8
|
+
400 => Castle::BadRequestError,
|
9
|
+
401 => Castle::UnauthorizedError,
|
10
|
+
403 => Castle::ForbiddenError,
|
11
|
+
404 => Castle::NotFoundError,
|
12
|
+
419 => Castle::UserUnauthorizedError,
|
13
|
+
422 => Castle::InvalidParametersError
|
14
|
+
}.freeze
|
15
|
+
|
16
|
+
class << self
|
17
|
+
def call(response)
|
18
|
+
verify!(response)
|
19
|
+
|
20
|
+
return {} if response.body.nil? || response.body.empty?
|
21
|
+
|
22
|
+
begin
|
23
|
+
JSON.parse(response.body, symbolize_names: true)
|
24
|
+
rescue JSON::ParserError
|
25
|
+
raise Castle::ApiError, 'Invalid response from Castle API'
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def verify!(response)
|
30
|
+
return if response.code.to_i.between?(200, 299)
|
31
|
+
|
32
|
+
raise Castle::InternalServerError if response.code.to_i.between?(500, 599)
|
33
|
+
|
34
|
+
error = RESPONSE_ERRORS.fetch(response.code.to_i, Castle::ApiError)
|
35
|
+
raise error, response[:message]
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
data/lib/castle/client.rb
CHANGED
@@ -27,13 +27,12 @@ module Castle
|
|
27
27
|
end
|
28
28
|
end
|
29
29
|
|
30
|
-
attr_accessor :
|
30
|
+
attr_accessor :context
|
31
31
|
|
32
32
|
def initialize(context, options = {})
|
33
33
|
@do_not_track = options.fetch(:do_not_track, false)
|
34
34
|
@timestamp = options[:timestamp]
|
35
35
|
@context = context
|
36
|
-
@api = API.new
|
37
36
|
end
|
38
37
|
|
39
38
|
def authenticate(options = {})
|
@@ -43,7 +42,7 @@ module Castle
|
|
43
42
|
add_timestamp_if_necessary(options)
|
44
43
|
command = Castle::Commands::Authenticate.new(@context).build(options)
|
45
44
|
begin
|
46
|
-
|
45
|
+
Castle::API.request(command).merge(failover: false, failover_reason: nil)
|
47
46
|
rescue Castle::RequestError, Castle::InternalServerError => error
|
48
47
|
self.class.failover_response_or_raise(
|
49
48
|
FailoverAuthResponse.new(options[:user_id], reason: error.to_s), error
|
@@ -64,7 +63,7 @@ module Castle
|
|
64
63
|
add_timestamp_if_necessary(options)
|
65
64
|
|
66
65
|
command = Castle::Commands::Identify.new(@context).build(options)
|
67
|
-
|
66
|
+
Castle::API.request(command)
|
68
67
|
end
|
69
68
|
|
70
69
|
def track(options = {})
|
@@ -74,14 +73,14 @@ module Castle
|
|
74
73
|
add_timestamp_if_necessary(options)
|
75
74
|
|
76
75
|
command = Castle::Commands::Track.new(@context).build(options)
|
77
|
-
|
76
|
+
Castle::API.request(command)
|
78
77
|
end
|
79
78
|
|
80
79
|
def impersonate(options = {})
|
81
80
|
options = Castle::Utils.deep_symbolize_keys(options || {})
|
82
81
|
add_timestamp_if_necessary(options)
|
83
82
|
command = Castle::Commands::Impersonate.new(@context).build(options)
|
84
|
-
|
83
|
+
Castle::API.request(command).tap do |response|
|
85
84
|
raise Castle::ImpersonationFailed unless response[:success]
|
86
85
|
end
|
87
86
|
end
|
data/lib/castle/configuration.rb
CHANGED
@@ -3,6 +3,10 @@
|
|
3
3
|
module Castle
|
4
4
|
# manages configuration variables
|
5
5
|
class Configuration
|
6
|
+
HOST = 'api.castle.io'
|
7
|
+
PORT = 443
|
8
|
+
URL_PREFIX = 'v1'
|
9
|
+
FAILOVER_STRATEGY = :allow
|
6
10
|
REQUEST_TIMEOUT = 500 # in milliseconds
|
7
11
|
FAILOVER_STRATEGIES = %i[allow deny challenge throw].freeze
|
8
12
|
WHITELISTED = [
|
@@ -20,7 +24,6 @@ module Castle
|
|
20
24
|
'X-Forwarded-For',
|
21
25
|
'CF_CONNECTING_IP'
|
22
26
|
].freeze
|
23
|
-
|
24
27
|
BLACKLISTED = ['HTTP_COOKIE'].freeze
|
25
28
|
|
26
29
|
attr_accessor :host, :port, :request_timeout, :url_prefix
|
@@ -29,10 +32,10 @@ module Castle
|
|
29
32
|
def initialize
|
30
33
|
@formatter = Castle::HeaderFormatter.new
|
31
34
|
@request_timeout = REQUEST_TIMEOUT
|
32
|
-
self.failover_strategy =
|
33
|
-
self.host =
|
34
|
-
self.port =
|
35
|
-
self.url_prefix =
|
35
|
+
self.failover_strategy = FAILOVER_STRATEGY
|
36
|
+
self.host = HOST
|
37
|
+
self.port = PORT
|
38
|
+
self.url_prefix = URL_PREFIX
|
36
39
|
self.whitelisted = WHITELISTED
|
37
40
|
self.blacklisted = BLACKLISTED
|
38
41
|
self.api_secret = ''
|
data/lib/castle/errors.rb
CHANGED
@@ -3,8 +3,17 @@
|
|
3
3
|
module Castle
|
4
4
|
# general error
|
5
5
|
class Error < RuntimeError; end
|
6
|
-
# request
|
7
|
-
|
6
|
+
# Raised when anything is wrong with the request (any unhappy path)
|
7
|
+
# This error indicates that either we would wait too long for a response or something
|
8
|
+
# else happened somewhere in the middle and we weren't able to get the results
|
9
|
+
class RequestError < Castle::Error
|
10
|
+
attr_reader :reason
|
11
|
+
|
12
|
+
# @param reason [Exception] the core exception that causes this error
|
13
|
+
def initialize(reason)
|
14
|
+
@reason = reason
|
15
|
+
end
|
16
|
+
end
|
8
17
|
# security error
|
9
18
|
class SecurityError < Castle::Error; end
|
10
19
|
# wrong configuration error
|
data/lib/castle/review.rb
CHANGED
data/lib/castle/version.rb
CHANGED
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
describe Castle::API::Request::Build do
|
4
|
+
subject(:call) { described_class.call(command, headers, api_secret) }
|
5
|
+
|
6
|
+
let(:headers) { { 'SAMPLE-HEADER' => '1' } }
|
7
|
+
let(:api_secret) { 'secret' }
|
8
|
+
|
9
|
+
describe 'call' do
|
10
|
+
context 'when get' do
|
11
|
+
let(:command) { Castle::Commands::Review.build(review_id) }
|
12
|
+
let(:review_id) { SecureRandom.uuid }
|
13
|
+
|
14
|
+
it { expect(call.body).to be_nil }
|
15
|
+
it { expect(call.method).to eql('GET') }
|
16
|
+
it { expect(call.path).to eql("/v1/#{command.path}") }
|
17
|
+
it { expect(call.to_hash).to have_key('authorization') }
|
18
|
+
it { expect(call.to_hash).to have_key('sample-header') }
|
19
|
+
it { expect(call.to_hash['sample-header']).to eql(['1']) }
|
20
|
+
end
|
21
|
+
|
22
|
+
context 'when post' do
|
23
|
+
let(:time) { Time.now.utc.iso8601(3) }
|
24
|
+
let(:command) { Castle::Commands::Track.new({}).build(event: '$login.succeeded', name: "\xC4") }
|
25
|
+
let(:expected_body) do
|
26
|
+
{
|
27
|
+
event: '$login.succeeded',
|
28
|
+
name: "�",
|
29
|
+
context: {},
|
30
|
+
sent_at: time
|
31
|
+
}
|
32
|
+
end
|
33
|
+
|
34
|
+
before { allow(Castle::Utils::Timestamp).to receive(:call).and_return(time) }
|
35
|
+
|
36
|
+
it { expect(call.body).to be_eql(expected_body.to_json) }
|
37
|
+
it { expect(call.method).to eql('POST') }
|
38
|
+
it { expect(call.path).to eql("/v1/#{command.path}") }
|
39
|
+
it { expect(call.to_hash).to have_key('authorization') }
|
40
|
+
it { expect(call.to_hash).to have_key('sample-header') }
|
41
|
+
it { expect(call.to_hash['sample-header']).to eql(['1']) }
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
describe Castle::API::Request do
|
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
|
+
let(:command) { Castle::Commands::Track.new({}).build(event: '$login.succeeded') }
|
10
|
+
let(:headers) { {} }
|
11
|
+
let(:api_secret) { 'secret' }
|
12
|
+
let(:expected_headers) { { 'Content-Type' => 'application/json' } }
|
13
|
+
|
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
|
22
|
+
|
23
|
+
it do
|
24
|
+
expect(Castle::API::Request::Build).to have_received(:call)
|
25
|
+
.with(command, expected_headers, api_secret)
|
26
|
+
end
|
27
|
+
it { expect(http).to have_received(:request).with(request_build) }
|
28
|
+
end
|
29
|
+
|
30
|
+
describe '#http' do
|
31
|
+
subject(:http) { described_class.http }
|
32
|
+
|
33
|
+
context 'when ssl false' do
|
34
|
+
before do
|
35
|
+
Castle.config.host = 'localhost'
|
36
|
+
Castle.config.port = 3002
|
37
|
+
end
|
38
|
+
|
39
|
+
after do
|
40
|
+
Castle.config.host = Castle::Configuration::HOST
|
41
|
+
Castle.config.port = Castle::Configuration::PORT
|
42
|
+
end
|
43
|
+
|
44
|
+
it { expect(http).to be_instance_of(Net::HTTP) }
|
45
|
+
it { expect(http.address).to eq(Castle.config.host) }
|
46
|
+
it { expect(http.port).to eq(Castle.config.port) }
|
47
|
+
it { expect(http.use_ssl?).to be false }
|
48
|
+
it { expect(http.verify_mode).to be_nil }
|
49
|
+
end
|
50
|
+
|
51
|
+
context 'when ssl true' do
|
52
|
+
it { expect(http).to be_instance_of(Net::HTTP) }
|
53
|
+
it { expect(http.address).to eq(Castle.config.host) }
|
54
|
+
it { expect(http.port).to eq(Castle.config.port) }
|
55
|
+
it { expect(http.use_ssl?).to be true }
|
56
|
+
it { expect(http.verify_mode).to eq(OpenSSL::SSL::VERIFY_PEER) }
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
describe Castle::API::Response do
|
4
|
+
describe '#call' do
|
5
|
+
subject(:call) { described_class.call(response) }
|
6
|
+
|
7
|
+
context 'when success' do
|
8
|
+
let(:response) { OpenStruct.new(body: '{"user":1}', code: 200) }
|
9
|
+
|
10
|
+
it { expect(call).to eql(user: 1) }
|
11
|
+
end
|
12
|
+
|
13
|
+
context 'when response empty' do
|
14
|
+
let(:response) { OpenStruct.new(body: '', code: 200) }
|
15
|
+
|
16
|
+
it { expect(call).to eql({}) }
|
17
|
+
end
|
18
|
+
|
19
|
+
context 'when response nil' do
|
20
|
+
let(:response) { OpenStruct.new(code: 200) }
|
21
|
+
|
22
|
+
it { expect(call).to eql({}) }
|
23
|
+
end
|
24
|
+
|
25
|
+
context 'when json is malformed' do
|
26
|
+
let(:response) { OpenStruct.new(body: '{a', code: 200) }
|
27
|
+
|
28
|
+
it { expect { call }.to raise_error(Castle::ApiError) }
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe '#verify!' do
|
33
|
+
subject(:verify!) { described_class.verify!(response) }
|
34
|
+
|
35
|
+
context 'without error when response is 2xx' do
|
36
|
+
let(:response) { OpenStruct.new(code: 200) }
|
37
|
+
|
38
|
+
it { expect { verify! }.not_to raise_error }
|
39
|
+
end
|
40
|
+
|
41
|
+
shared_examples 'response_failed' do |code, error|
|
42
|
+
let(:response) { OpenStruct.new(code: code) }
|
43
|
+
|
44
|
+
it "fail when response is #{code}" do
|
45
|
+
expect { verify! }.to raise_error(error)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
it_behaves_like 'response_failed', '400', Castle::BadRequestError
|
50
|
+
it_behaves_like 'response_failed', '401', Castle::UnauthorizedError
|
51
|
+
it_behaves_like 'response_failed', '403', Castle::ForbiddenError
|
52
|
+
it_behaves_like 'response_failed', '404', Castle::NotFoundError
|
53
|
+
it_behaves_like 'response_failed', '419', Castle::UserUnauthorizedError
|
54
|
+
it_behaves_like 'response_failed', '422', Castle::InvalidParametersError
|
55
|
+
it_behaves_like 'response_failed', '499', Castle::ApiError
|
56
|
+
it_behaves_like 'response_failed', '500', Castle::InternalServerError
|
57
|
+
end
|
58
|
+
end
|
data/spec/lib/castle/api_spec.rb
CHANGED
@@ -1,44 +1,36 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
describe Castle::API do
|
4
|
-
|
5
|
-
|
6
|
-
let(:
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
'X-Castle-Ip' => '1.2.3.4' }
|
11
|
-
end
|
4
|
+
subject(:request) { described_class.request(command) }
|
5
|
+
|
6
|
+
let(:command) { Castle::Commands::Track.new({}).build(event: '$login.succeeded') }
|
7
|
+
|
8
|
+
context 'when request timeouts' do
|
9
|
+
before { stub_request(:any, /api.castle.io/).to_timeout }
|
12
10
|
|
13
|
-
describe 'handles timeout' do
|
14
|
-
before do
|
15
|
-
stub_request(:any, /api.castle.io/).to_timeout
|
16
|
-
end
|
17
11
|
it do
|
18
12
|
expect do
|
19
|
-
|
13
|
+
request
|
20
14
|
end.to raise_error(Castle::RequestError)
|
21
15
|
end
|
22
16
|
end
|
23
17
|
|
24
|
-
|
25
|
-
before
|
26
|
-
|
27
|
-
end
|
18
|
+
context 'when non-OK response code' do
|
19
|
+
before { stub_request(:any, /api.castle.io/).to_return(status: 400) }
|
20
|
+
|
28
21
|
it do
|
29
22
|
expect do
|
30
|
-
|
23
|
+
request
|
31
24
|
end.to raise_error(Castle::BadRequestError)
|
32
25
|
end
|
33
26
|
end
|
34
27
|
|
35
|
-
|
36
|
-
before
|
37
|
-
|
38
|
-
end
|
28
|
+
context 'when no api_secret' do
|
29
|
+
before { allow(Castle.config).to receive(:api_secret).and_return('') }
|
30
|
+
|
39
31
|
it do
|
40
32
|
expect do
|
41
|
-
|
33
|
+
request
|
42
34
|
end.to raise_error(Castle::ConfigurationError)
|
43
35
|
end
|
44
36
|
end
|
@@ -50,12 +50,12 @@ describe Castle::Client do
|
|
50
50
|
|
51
51
|
describe 'parses the request' do
|
52
52
|
before do
|
53
|
-
allow(Castle::API).to receive(:
|
53
|
+
allow(Castle::API).to receive(:request).and_call_original
|
54
54
|
end
|
55
55
|
|
56
56
|
it do
|
57
57
|
client.authenticate(event: '$login.succeeded', user_id: '1234')
|
58
|
-
expect(Castle::API).to have_received(:
|
58
|
+
expect(Castle::API).to have_received(:request)
|
59
59
|
end
|
60
60
|
end
|
61
61
|
|
@@ -244,7 +244,7 @@ describe Castle::Client do
|
|
244
244
|
end
|
245
245
|
|
246
246
|
context 'when request with fail' do
|
247
|
-
before { allow(
|
247
|
+
before { allow(Castle::API).to receive(:request).and_raise(Castle::RequestError.new(Timeout::Error)) }
|
248
248
|
|
249
249
|
context 'with request error and throw strategy' do
|
250
250
|
before { allow(Castle.config).to receive(:failover_strategy).and_return(:throw) }
|
@@ -262,7 +262,7 @@ describe Castle::Client do
|
|
262
262
|
end
|
263
263
|
|
264
264
|
context 'when request is internal server error' do
|
265
|
-
before { allow(
|
265
|
+
before { allow(Castle::API).to receive(:request).and_raise(Castle::InternalServerError) }
|
266
266
|
|
267
267
|
describe 'throw strategy' do
|
268
268
|
before { allow(Castle.config).to receive(:failover_strategy).and_return(:throw) }
|
@@ -14,10 +14,6 @@ describe Castle::Review do
|
|
14
14
|
|
15
15
|
before { retrieve }
|
16
16
|
|
17
|
-
it
|
18
|
-
assert_requested :get,
|
19
|
-
"https://api.castle.io/v1/reviews/#{review_id}",
|
20
|
-
times: 1
|
21
|
-
end
|
17
|
+
it { assert_requested :get, "https://api.castle.io/v1/reviews/#{review_id}", times: 1 }
|
22
18
|
end
|
23
19
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: castle-rb
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.5.
|
4
|
+
version: 3.5.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Johan Brissmyr
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-
|
11
|
+
date: 2018-10-27 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: Castle protects your users from account compromise
|
14
14
|
email: johan@castle.io
|
@@ -20,6 +20,9 @@ files:
|
|
20
20
|
- lib/castle-rb.rb
|
21
21
|
- lib/castle.rb
|
22
22
|
- lib/castle/api.rb
|
23
|
+
- lib/castle/api/request.rb
|
24
|
+
- lib/castle/api/request/build.rb
|
25
|
+
- lib/castle/api/response.rb
|
23
26
|
- lib/castle/client.rb
|
24
27
|
- lib/castle/command.rb
|
25
28
|
- lib/castle/commands/authenticate.rb
|
@@ -37,8 +40,6 @@ files:
|
|
37
40
|
- lib/castle/extractors/ip.rb
|
38
41
|
- lib/castle/failover_auth_response.rb
|
39
42
|
- lib/castle/header_formatter.rb
|
40
|
-
- lib/castle/request.rb
|
41
|
-
- lib/castle/response.rb
|
42
43
|
- lib/castle/review.rb
|
43
44
|
- lib/castle/secure_mode.rb
|
44
45
|
- lib/castle/support/hanami.rb
|
@@ -52,6 +53,9 @@ files:
|
|
52
53
|
- lib/castle/validators/not_supported.rb
|
53
54
|
- lib/castle/validators/present.rb
|
54
55
|
- lib/castle/version.rb
|
56
|
+
- spec/lib/castle/api/request/build_spec.rb
|
57
|
+
- spec/lib/castle/api/request_spec.rb
|
58
|
+
- spec/lib/castle/api/response_spec.rb
|
55
59
|
- spec/lib/castle/api_spec.rb
|
56
60
|
- spec/lib/castle/client_spec.rb
|
57
61
|
- spec/lib/castle/command_spec.rb
|
@@ -68,8 +72,6 @@ files:
|
|
68
72
|
- spec/lib/castle/extractors/headers_spec.rb
|
69
73
|
- spec/lib/castle/extractors/ip_spec.rb
|
70
74
|
- spec/lib/castle/header_formatter_spec.rb
|
71
|
-
- spec/lib/castle/request_spec.rb
|
72
|
-
- spec/lib/castle/response_spec.rb
|
73
75
|
- spec/lib/castle/review_spec.rb
|
74
76
|
- spec/lib/castle/secure_mode_spec.rb
|
75
77
|
- spec/lib/castle/utils/cloner_spec.rb
|
@@ -101,7 +103,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
101
103
|
version: '0'
|
102
104
|
requirements: []
|
103
105
|
rubyforge_project:
|
104
|
-
rubygems_version: 2.7.
|
106
|
+
rubygems_version: 2.7.6
|
105
107
|
signing_key:
|
106
108
|
specification_version: 4
|
107
109
|
summary: Castle
|
@@ -121,8 +123,9 @@ test_files:
|
|
121
123
|
- spec/lib/castle/utils/timestamp_spec.rb
|
122
124
|
- spec/lib/castle/utils/merger_spec.rb
|
123
125
|
- spec/lib/castle/command_spec.rb
|
124
|
-
- spec/lib/castle/request_spec.rb
|
125
|
-
- spec/lib/castle/response_spec.rb
|
126
|
+
- spec/lib/castle/api/request_spec.rb
|
127
|
+
- spec/lib/castle/api/response_spec.rb
|
128
|
+
- spec/lib/castle/api/request/build_spec.rb
|
126
129
|
- spec/lib/castle/commands/review_spec.rb
|
127
130
|
- spec/lib/castle/commands/authenticate_spec.rb
|
128
131
|
- spec/lib/castle/commands/track_spec.rb
|
data/lib/castle/request.rb
DELETED
@@ -1,28 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Castle
|
4
|
-
# generate api request
|
5
|
-
class Request
|
6
|
-
def initialize(headers = {})
|
7
|
-
@config = Castle.config
|
8
|
-
@headers = headers
|
9
|
-
end
|
10
|
-
|
11
|
-
def build(endpoint, args, method)
|
12
|
-
request = Net::HTTP.const_get(method.to_s.capitalize).new(build_url(endpoint), @headers)
|
13
|
-
request.body = ::Castle::Utils.replace_invalid_characters(args).to_json unless method == :get
|
14
|
-
add_basic_auth(request)
|
15
|
-
request
|
16
|
-
end
|
17
|
-
|
18
|
-
private
|
19
|
-
|
20
|
-
def build_url(endpoint)
|
21
|
-
"/#{@config.url_prefix}/#{endpoint}"
|
22
|
-
end
|
23
|
-
|
24
|
-
def add_basic_auth(request)
|
25
|
-
request.basic_auth('', @config.api_secret)
|
26
|
-
end
|
27
|
-
end
|
28
|
-
end
|
data/lib/castle/response.rb
DELETED
@@ -1,45 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Castle
|
4
|
-
# parses api response
|
5
|
-
class Response
|
6
|
-
RESPONSE_ERRORS = {
|
7
|
-
400 => Castle::BadRequestError,
|
8
|
-
401 => Castle::UnauthorizedError,
|
9
|
-
403 => Castle::ForbiddenError,
|
10
|
-
404 => Castle::NotFoundError,
|
11
|
-
419 => Castle::UserUnauthorizedError,
|
12
|
-
422 => Castle::InvalidParametersError
|
13
|
-
}.freeze
|
14
|
-
|
15
|
-
def initialize(response)
|
16
|
-
@response = response
|
17
|
-
verify_response_code
|
18
|
-
end
|
19
|
-
|
20
|
-
def parse
|
21
|
-
response_body = @response.body
|
22
|
-
|
23
|
-
return {} if response_body.nil? || response_body.empty?
|
24
|
-
|
25
|
-
begin
|
26
|
-
JSON.parse(response_body, symbolize_names: true)
|
27
|
-
rescue JSON::ParserError
|
28
|
-
raise Castle::ApiError, 'Invalid response from Castle API'
|
29
|
-
end
|
30
|
-
end
|
31
|
-
|
32
|
-
private
|
33
|
-
|
34
|
-
def verify_response_code
|
35
|
-
response_code = @response.code.to_i
|
36
|
-
|
37
|
-
return if response_code.between?(200, 299)
|
38
|
-
|
39
|
-
raise Castle::InternalServerError if response_code.between?(500, 599)
|
40
|
-
|
41
|
-
error = RESPONSE_ERRORS.fetch(response_code, Castle::ApiError)
|
42
|
-
raise error, @response[:message]
|
43
|
-
end
|
44
|
-
end
|
45
|
-
end
|
@@ -1,40 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
describe Castle::Request do
|
4
|
-
subject { described_class.new(headers) }
|
5
|
-
|
6
|
-
let(:headers) { { 'SAMPLE-HEADER' => '1' } }
|
7
|
-
let(:api_secret) { Castle.config.api_secret }
|
8
|
-
|
9
|
-
describe 'build' do
|
10
|
-
context 'when get' do
|
11
|
-
let(:path) { 'endpoint' }
|
12
|
-
let(:params) { { user_id: 1 } }
|
13
|
-
let(:request) { subject.build(path, params, :get) }
|
14
|
-
|
15
|
-
it { expect(request.body).to be_nil }
|
16
|
-
it { expect(request.method).to eql('GET') }
|
17
|
-
it { expect(request.path).to eql('/v1/endpoint') }
|
18
|
-
it { expect(request.to_hash['sample-header']).to eql(['1']) }
|
19
|
-
it { expect(request.to_hash['authorization'][0]).to match(/Basic \w/) }
|
20
|
-
end
|
21
|
-
|
22
|
-
context 'when post' do
|
23
|
-
let(:path) { 'endpoint' }
|
24
|
-
let(:params) { { user_id: 1 } }
|
25
|
-
let(:request) { subject.build(path, params, :post) }
|
26
|
-
|
27
|
-
it { expect(request.body).to be_eql('{"user_id":1}') }
|
28
|
-
it { expect(request.method).to eql('POST') }
|
29
|
-
it { expect(request.path).to eql('/v1/endpoint') }
|
30
|
-
it { expect(request.to_hash['sample-header']).to eql(['1']) }
|
31
|
-
it { expect(request.to_hash['authorization'][0]).to match(/Basic \w/) }
|
32
|
-
|
33
|
-
context 'with non-UTF-8 charaters' do
|
34
|
-
let(:params) { { name: "\xC4" } }
|
35
|
-
|
36
|
-
it { expect(request.body).to eq '{"name":"�"}' }
|
37
|
-
end
|
38
|
-
end
|
39
|
-
end
|
40
|
-
end
|
@@ -1,69 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
describe Castle::Response do
|
4
|
-
subject(:castle_response) do
|
5
|
-
described_class.new(response)
|
6
|
-
end
|
7
|
-
|
8
|
-
describe 'initialize' do
|
9
|
-
context 'without error when response is 2xx' do
|
10
|
-
let(:response) { OpenStruct.new(code: 200) }
|
11
|
-
|
12
|
-
it do
|
13
|
-
expect(castle_response).to be_truthy
|
14
|
-
end
|
15
|
-
end
|
16
|
-
|
17
|
-
shared_examples 'response_failed' do |code, error|
|
18
|
-
let(:response) { OpenStruct.new(code: code) }
|
19
|
-
|
20
|
-
it "fail when response is #{code}" do
|
21
|
-
expect do
|
22
|
-
castle_response
|
23
|
-
end.to raise_error(error)
|
24
|
-
end
|
25
|
-
end
|
26
|
-
|
27
|
-
it_behaves_like 'response_failed', 400, Castle::BadRequestError
|
28
|
-
it_behaves_like 'response_failed', 401, Castle::UnauthorizedError
|
29
|
-
it_behaves_like 'response_failed', 403, Castle::ForbiddenError
|
30
|
-
it_behaves_like 'response_failed', 404, Castle::NotFoundError
|
31
|
-
it_behaves_like 'response_failed', 419, Castle::UserUnauthorizedError
|
32
|
-
it_behaves_like 'response_failed', 422, Castle::InvalidParametersError
|
33
|
-
it_behaves_like 'response_failed', 499, Castle::ApiError
|
34
|
-
it_behaves_like 'response_failed', 500, Castle::InternalServerError
|
35
|
-
end
|
36
|
-
|
37
|
-
describe 'parse' do
|
38
|
-
context 'when success' do
|
39
|
-
let(:response) { OpenStruct.new(body: '{"user":1}', code: 200) }
|
40
|
-
|
41
|
-
it do
|
42
|
-
expect(castle_response.parse).to eql(user: 1)
|
43
|
-
end
|
44
|
-
end
|
45
|
-
context 'when response empty' do
|
46
|
-
let(:response) { OpenStruct.new(body: '', code: 200) }
|
47
|
-
|
48
|
-
it do
|
49
|
-
expect(castle_response.parse).to eql({})
|
50
|
-
end
|
51
|
-
end
|
52
|
-
context 'when response nil' do
|
53
|
-
let(:response) { OpenStruct.new(code: 200) }
|
54
|
-
|
55
|
-
it do
|
56
|
-
expect(castle_response.parse).to eql({})
|
57
|
-
end
|
58
|
-
end
|
59
|
-
context 'when json is malformed' do
|
60
|
-
let(:response) { OpenStruct.new(body: '{a', code: 200) }
|
61
|
-
|
62
|
-
it do
|
63
|
-
expect do
|
64
|
-
castle_response.parse
|
65
|
-
end.to raise_error(Castle::ApiError)
|
66
|
-
end
|
67
|
-
end
|
68
|
-
end
|
69
|
-
end
|