castle-rb 3.5.0 → 3.5.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
[![Build Status](https://travis-ci.org/castle/castle-ruby.svg?branch=master)](https://travis-ci.org/castle/castle-ruby)
|
4
4
|
[![Coverage Status](https://coveralls.io/repos/github/castle/castle-ruby/badge.svg?branch=coveralls)](https://coveralls.io/github/castle/castle-ruby?branch=coveralls)
|
5
5
|
[![Gem Version](https://badge.fury.io/rb/castle-rb.svg)](https://badge.fury.io/rb/castle-rb)
|
6
|
-
[![Dependency Status](https://gemnasium.com/badges/github.com/castle/castle-ruby.svg)](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
|