castle-rb 2.2.0 → 2.3.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 +2 -5
- data/README.md +1 -1
- data/lib/castle-rb.rb +2 -19
- data/lib/castle.rb +42 -0
- data/lib/castle/api.rb +48 -0
- data/lib/castle/client.rb +43 -0
- data/lib/castle/configuration.rb +39 -0
- data/lib/{castle-rb/support → castle}/cookie_store.rb +9 -5
- data/lib/castle/errors.rb +27 -0
- data/lib/castle/extractors/client_id.rb +28 -0
- data/lib/castle/extractors/headers.rb +36 -0
- data/lib/castle/extractors/ip.rb +16 -0
- data/lib/castle/headers.rb +39 -0
- data/lib/castle/request.rb +34 -0
- data/lib/castle/response.rb +43 -0
- data/lib/castle/support.rb +11 -0
- data/lib/{castle-rb → castle}/support/padrino.rb +1 -1
- data/lib/{castle-rb → castle}/support/rails.rb +2 -0
- data/lib/{castle-rb → castle}/support/sinatra.rb +1 -1
- data/lib/castle/system.rb +36 -0
- data/lib/castle/version.rb +5 -0
- data/spec/lib/castle/api_spec.rb +54 -0
- data/spec/lib/castle/client_spec.rb +64 -0
- data/spec/lib/castle/configuration_spec.rb +98 -0
- data/spec/lib/castle/extractors/client_id_spec.rb +39 -0
- data/spec/lib/castle/extractors/headers_spec.rb +25 -0
- data/spec/lib/castle/extractors/ip_spec.rb +20 -0
- data/spec/lib/castle/headers_spec.rb +82 -0
- data/spec/lib/castle/request_spec.rb +37 -0
- data/spec/lib/castle/response_spec.rb +71 -0
- data/spec/lib/castle/system_spec.rb +70 -0
- data/spec/lib/castle/version_spec.rb +9 -0
- data/spec/lib/castle_spec.rb +73 -0
- data/spec/spec_helper.rb +9 -3
- metadata +49 -76
- data/lib/castle-rb/api.rb +0 -94
- data/lib/castle-rb/client.rb +0 -67
- data/lib/castle-rb/configuration.rb +0 -48
- data/lib/castle-rb/errors.rb +0 -15
- data/lib/castle-rb/version.rb +0 -3
- data/spec/api_spec.rb +0 -31
- data/spec/client_spec.rb +0 -55
data/lib/castle-rb/api.rb
DELETED
@@ -1,94 +0,0 @@
|
|
1
|
-
module Castle
|
2
|
-
class API
|
3
|
-
attr_accessor :http, :headers
|
4
|
-
|
5
|
-
def initialize(cookie_id, ip, headers)
|
6
|
-
@http = Net::HTTP.new(Castle.config.api_endpoint.host,
|
7
|
-
Castle.config.api_endpoint.port)
|
8
|
-
|
9
|
-
@http.read_timeout = Castle.config.request_timeout
|
10
|
-
|
11
|
-
if Castle.config.api_endpoint.scheme == 'https'
|
12
|
-
@http.use_ssl = true
|
13
|
-
@http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
14
|
-
end
|
15
|
-
|
16
|
-
@headers = {
|
17
|
-
"Content-Type" => "application/json",
|
18
|
-
"X-Castle-Cookie-Id" => cookie_id,
|
19
|
-
"X-Castle-Ip" => ip,
|
20
|
-
"X-Castle-Headers" => headers,
|
21
|
-
"X-Castle-Client-User-Agent" => JSON.generate(client_user_agent),
|
22
|
-
"X-Castle-Source" => Castle.config.source_header,
|
23
|
-
"User-Agent" => "Castle/v1 RubyBindings/#{Castle::VERSION}"
|
24
|
-
}
|
25
|
-
|
26
|
-
@headers.delete_if { |k, v| v.nil? }
|
27
|
-
end
|
28
|
-
|
29
|
-
def request(endpoint, args, method: :post)
|
30
|
-
http_method = method.to_s.capitalize
|
31
|
-
req = Net::HTTP.const_get(http_method).new(
|
32
|
-
"#{Castle.config.api_endpoint.path}/#{endpoint}", @headers)
|
33
|
-
req.basic_auth("", Castle.config.api_secret)
|
34
|
-
req.body = args.to_json unless http_method == 'Get'
|
35
|
-
|
36
|
-
begin
|
37
|
-
response = @http.request(req)
|
38
|
-
rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, EOFError,
|
39
|
-
Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError,
|
40
|
-
Net::ProtocolError, Net::ReadTimeout => e
|
41
|
-
raise Castle::RequestError, 'Castle API connection error'
|
42
|
-
end
|
43
|
-
|
44
|
-
case response.code.to_i
|
45
|
-
when 200..299
|
46
|
-
# OK
|
47
|
-
when 400
|
48
|
-
raise Castle::BadRequestError, response[:message]
|
49
|
-
when 401
|
50
|
-
raise Castle::UnauthorizedError, response[:message]
|
51
|
-
when 403
|
52
|
-
raise Castle::ForbiddenError, response[:message]
|
53
|
-
when 404
|
54
|
-
raise Castle::NotFoundError, response[:message]
|
55
|
-
when 419
|
56
|
-
raise Castle::UserUnauthorizedError, response[:message]
|
57
|
-
when 422
|
58
|
-
raise Castle::InvalidParametersError, response[:message]
|
59
|
-
else
|
60
|
-
raise Castle::ApiError, response[:message]
|
61
|
-
end
|
62
|
-
|
63
|
-
if response.body.nil? || response.body.empty?
|
64
|
-
{}
|
65
|
-
else
|
66
|
-
begin
|
67
|
-
JSON.parse(response.body, :symbolize_names => true)
|
68
|
-
rescue JSON::ParserError
|
69
|
-
raise Castle::ApiError, 'Invalid response from Castle API'
|
70
|
-
end
|
71
|
-
end
|
72
|
-
end
|
73
|
-
|
74
|
-
def client_user_agent
|
75
|
-
@uname ||= get_uname
|
76
|
-
lang_version = "#{RUBY_VERSION} p#{RUBY_PATCHLEVEL} (#{RUBY_RELEASE_DATE})"
|
77
|
-
|
78
|
-
{
|
79
|
-
:bindings_version => Castle::VERSION,
|
80
|
-
:lang => 'ruby',
|
81
|
-
:lang_version => lang_version,
|
82
|
-
:platform => RUBY_PLATFORM,
|
83
|
-
:publisher => 'castle',
|
84
|
-
:uname => @uname
|
85
|
-
}
|
86
|
-
end
|
87
|
-
|
88
|
-
def get_uname
|
89
|
-
`uname -a 2>/dev/null`.strip if RUBY_PLATFORM =~ /linux|darwin/i
|
90
|
-
rescue Errno::ENOMEM # couldn't create subprocess
|
91
|
-
"uname lookup failed"
|
92
|
-
end
|
93
|
-
end
|
94
|
-
end
|
data/lib/castle-rb/client.rb
DELETED
@@ -1,67 +0,0 @@
|
|
1
|
-
module Castle
|
2
|
-
class Client
|
3
|
-
attr_accessor :do_not_track, :api
|
4
|
-
|
5
|
-
def initialize(request, response)
|
6
|
-
cookie_id = extract_cookie(request, response)['__cid'] || ''
|
7
|
-
ip = request.ip
|
8
|
-
headers = header_string(request)
|
9
|
-
|
10
|
-
@api = API.new(cookie_id, ip, headers)
|
11
|
-
end
|
12
|
-
|
13
|
-
def fetch_review(id)
|
14
|
-
@api.request("reviews/#{id}", nil, method: :get)
|
15
|
-
end
|
16
|
-
|
17
|
-
def identify(args)
|
18
|
-
@api.request('identify', args) unless do_not_track?
|
19
|
-
end
|
20
|
-
|
21
|
-
def authenticate(args)
|
22
|
-
@api.request('authenticate', args)
|
23
|
-
end
|
24
|
-
|
25
|
-
def track(args)
|
26
|
-
@api.request('track', args) unless do_not_track?
|
27
|
-
end
|
28
|
-
|
29
|
-
def do_not_track!
|
30
|
-
@do_not_track = true
|
31
|
-
end
|
32
|
-
|
33
|
-
def track!
|
34
|
-
@do_not_track = false
|
35
|
-
end
|
36
|
-
|
37
|
-
def do_not_track?
|
38
|
-
!!@do_not_track
|
39
|
-
end
|
40
|
-
|
41
|
-
|
42
|
-
private
|
43
|
-
|
44
|
-
# Extract the cookie set by the Castle Javascript
|
45
|
-
def extract_cookie(request, response)
|
46
|
-
if response.class.name == 'ActionDispatch::Cookies::CookieJar'
|
47
|
-
Castle::CookieStore::Rack.new(response)
|
48
|
-
else
|
49
|
-
Castle::CookieStore::Base.new(request, response)
|
50
|
-
end
|
51
|
-
end
|
52
|
-
|
53
|
-
# Serialize HTTP headers
|
54
|
-
def header_string(request)
|
55
|
-
scrub_headers = ['Cookie']
|
56
|
-
|
57
|
-
headers = request.env.keys.grep(/^HTTP_/).map do |header|
|
58
|
-
name = header.gsub(/^HTTP_/, '').split('_').map(&:capitalize).join('-')
|
59
|
-
unless scrub_headers.include?(name)
|
60
|
-
{ name => request.env[header] }
|
61
|
-
end
|
62
|
-
end.compact.inject(:merge)
|
63
|
-
|
64
|
-
JSON.generate(headers || {})
|
65
|
-
end
|
66
|
-
end
|
67
|
-
end
|
@@ -1,48 +0,0 @@
|
|
1
|
-
module Castle
|
2
|
-
class << self
|
3
|
-
def configure(config_hash=nil)
|
4
|
-
if config_hash
|
5
|
-
config_hash.each do |k,v|
|
6
|
-
config.send("#{k}=", v)
|
7
|
-
end
|
8
|
-
end
|
9
|
-
|
10
|
-
yield(config) if block_given?
|
11
|
-
end
|
12
|
-
|
13
|
-
def config
|
14
|
-
@configuration ||= Castle::Configuration.new
|
15
|
-
end
|
16
|
-
|
17
|
-
def api_secret=(api_secret)
|
18
|
-
config.api_secret = api_secret
|
19
|
-
end
|
20
|
-
end
|
21
|
-
|
22
|
-
class Configuration
|
23
|
-
attr_accessor :request_timeout
|
24
|
-
attr_accessor :source_header
|
25
|
-
|
26
|
-
def initialize
|
27
|
-
self.request_timeout = 30.0
|
28
|
-
self.api_endpoint =
|
29
|
-
ENV['CASTLE_API_ENDPOINT'] || 'https://api.castle.io/v1'
|
30
|
-
end
|
31
|
-
|
32
|
-
def api_secret
|
33
|
-
ENV['CASTLE_API_SECRET'] || @_api_secret || ''
|
34
|
-
end
|
35
|
-
|
36
|
-
def api_secret=(value)
|
37
|
-
@_api_secret = value
|
38
|
-
end
|
39
|
-
|
40
|
-
def api_endpoint
|
41
|
-
@_api_endpoint
|
42
|
-
end
|
43
|
-
|
44
|
-
def api_endpoint=(value)
|
45
|
-
@_api_endpoint = URI(value)
|
46
|
-
end
|
47
|
-
end
|
48
|
-
end
|
data/lib/castle-rb/errors.rb
DELETED
@@ -1,15 +0,0 @@
|
|
1
|
-
class Castle::Error < Exception; end
|
2
|
-
|
3
|
-
class Castle::RequestError < Castle::Error; end
|
4
|
-
class Castle::SecurityError < Castle::Error; end
|
5
|
-
class Castle::ConfigurationError < Castle::Error; end
|
6
|
-
|
7
|
-
class Castle::ApiError < Castle::Error; end
|
8
|
-
|
9
|
-
class Castle::BadRequestError < Castle::ApiError; end
|
10
|
-
class Castle::ForbiddenError < Castle::ApiError; end
|
11
|
-
class Castle::NotFoundError < Castle::ApiError; end
|
12
|
-
class Castle::UserUnauthorizedError < Castle::ApiError; end
|
13
|
-
class Castle::InvalidParametersError < Castle::ApiError; end
|
14
|
-
|
15
|
-
class Castle::UnauthorizedError < Castle::ApiError; end
|
data/lib/castle-rb/version.rb
DELETED
data/spec/api_spec.rb
DELETED
@@ -1,31 +0,0 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
|
3
|
-
describe Castle::API do
|
4
|
-
let(:api) { Castle::API.new('abcd', '1.2.3.4', '{}') }
|
5
|
-
|
6
|
-
it 'handles timeout' do
|
7
|
-
stub_request(:any, /api.castle.io/).to_timeout
|
8
|
-
expect {
|
9
|
-
api.request('authenticate', user_id: '1234')
|
10
|
-
}.to raise_error(Castle::RequestError)
|
11
|
-
end
|
12
|
-
|
13
|
-
it 'handles non-OK response code' do
|
14
|
-
stub_request(:any, /api.castle.io/).to_return(status: 400)
|
15
|
-
expect {
|
16
|
-
api.request('authenticate', user_id: '1234')
|
17
|
-
}.to raise_error(Castle::BadRequestError)
|
18
|
-
end
|
19
|
-
|
20
|
-
it 'handles custom API endpoint' do
|
21
|
-
stub_request(:any, /new.herokuapp.com/)
|
22
|
-
|
23
|
-
api_endpoint = 'http://new.herokuapp.com:3000/v2'
|
24
|
-
Castle.config.api_endpoint = api_endpoint
|
25
|
-
uri = URI(api_endpoint)
|
26
|
-
|
27
|
-
api.request('authenticate', user_id: '1234')
|
28
|
-
assert_requested :post,
|
29
|
-
"#{api_endpoint.gsub(/new/, ":secret@new")}/authenticate", times: 1
|
30
|
-
end
|
31
|
-
end
|
data/spec/client_spec.rb
DELETED
@@ -1,55 +0,0 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
|
3
|
-
class Request < Rack::Request
|
4
|
-
def delegate?; false; end
|
5
|
-
end
|
6
|
-
|
7
|
-
describe Castle::Client do
|
8
|
-
let(:ip) {'1.2.3.4' }
|
9
|
-
let(:cookie_id) { 'abcd' }
|
10
|
-
let(:env) do
|
11
|
-
Rack::MockRequest.env_for('/',
|
12
|
-
'HTTP_X_FORWARDED_FOR' => '1.2.3.4',
|
13
|
-
'HTTP_COOKIE' => "__cid=#{cookie_id};other=efgh"
|
14
|
-
)
|
15
|
-
end
|
16
|
-
let(:request) { Request.new(env) }
|
17
|
-
let(:client) { Castle::Client.new(request, nil) }
|
18
|
-
let(:review_id) { '12356789' }
|
19
|
-
|
20
|
-
it 'parses the request' do
|
21
|
-
expect(Castle::API).to receive(:new).with(cookie_id, ip,
|
22
|
-
"{\"X-Forwarded-For\":\"#{ip}\"}").and_call_original
|
23
|
-
|
24
|
-
client = Castle::Client.new(request, nil)
|
25
|
-
client.authenticate({name: '$login.succeeded', user_id: '1234'})
|
26
|
-
end
|
27
|
-
|
28
|
-
it 'identifies' do
|
29
|
-
client.identify( user_id: '1234', traits: { name: 'Jo' })
|
30
|
-
assert_requested :post, 'https://:secret@api.castle.io/v1/identify',
|
31
|
-
times: 1,
|
32
|
-
body: { user_id: '1234', traits: { name: 'Jo' } }
|
33
|
-
end
|
34
|
-
|
35
|
-
it 'authenticates' do
|
36
|
-
client.authenticate(name: '$login.succeeded', user_id: '1234')
|
37
|
-
assert_requested :post, 'https://:secret@api.castle.io/v1/authenticate',
|
38
|
-
times: 1,
|
39
|
-
body: { name: '$login.succeeded', user_id: "1234" }
|
40
|
-
end
|
41
|
-
|
42
|
-
it 'tracks' do
|
43
|
-
client.track(name: '$login.succeeded', user_id: '1234')
|
44
|
-
assert_requested :post, 'https://:secret@api.castle.io/v1/track',
|
45
|
-
times: 1,
|
46
|
-
body: { name: '$login.succeeded', user_id: "1234" }
|
47
|
-
end
|
48
|
-
|
49
|
-
it 'fetches review' do
|
50
|
-
client.fetch_review(review_id)
|
51
|
-
|
52
|
-
assert_requested :get, "https://:secret@api.castle.io/v1/reviews/#{review_id}",
|
53
|
-
times: 1
|
54
|
-
end
|
55
|
-
end
|