castle-rb 2.2.0 → 2.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
checksums.yaml
CHANGED
@@ -1,7 +1,4 @@
|
|
1
1
|
---
|
2
|
-
SHA1:
|
3
|
-
metadata.gz: cf802329a5b427cc2b979f02931c572601397506
|
4
|
-
data.tar.gz: 58746daf7163fe23e054261b1b52a44e918b7074
|
5
2
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5119758641db337ccc160ed9709f68c91a9e65d80748d31b719f71af40f25d7fe512e0026ec1252fce167511a5edac3a07c35b436981347d02007fe02c1de774
|
4
|
+
data.tar.gz: 6464b7e94ed647fadf2f5ee6928db94c29f77be387f7d98330c7868597a3dfed036565458fb1f354d8df0d8cc18e1117f8f40d877e13641e5441462a74452184
|
data/README.md
CHANGED
@@ -28,7 +28,7 @@ A Castle client instance will be made available as `castle` in your Rails, Sinat
|
|
28
28
|
|
29
29
|
## Exceptions
|
30
30
|
|
31
|
-
`Castle::Error` will be thrown if the Castle API returns a 400 or a 500 level HTTP response. You can also choose to catch a more [finegrained error](https://github.com/castle/castle-ruby/blob/master/lib/castle
|
31
|
+
`Castle::Error` will be thrown if the Castle API returns a 400 or a 500 level HTTP response. You can also choose to catch a more [finegrained error](https://github.com/castle/castle-ruby/blob/master/lib/castle/errors.rb).
|
32
32
|
|
33
33
|
```ruby
|
34
34
|
begin
|
data/lib/castle-rb.rb
CHANGED
@@ -1,20 +1,3 @@
|
|
1
|
-
|
2
|
-
require 'net/http'
|
3
|
-
|
4
|
-
require 'castle-rb/version'
|
5
|
-
|
6
|
-
require 'castle-rb/configuration'
|
7
|
-
require 'castle-rb/client'
|
8
|
-
require 'castle-rb/errors'
|
9
|
-
require 'castle-rb/api'
|
10
|
-
require 'castle-rb/support/cookie_store'
|
11
|
-
require 'castle-rb/support/rails' if defined?(Rails::Railtie)
|
12
|
-
|
13
|
-
if defined?(Sinatra::Base)
|
14
|
-
if defined?(Padrino)
|
15
|
-
require 'castle-rb/support/padrino'
|
16
|
-
else
|
17
|
-
require 'castle-rb/support/sinatra'
|
18
|
-
end
|
19
|
-
end
|
1
|
+
# frozen_string_literal: true
|
20
2
|
|
3
|
+
require 'castle'
|
data/lib/castle.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'openssl'
|
4
|
+
require 'net/http'
|
5
|
+
|
6
|
+
require 'castle/version'
|
7
|
+
|
8
|
+
require 'castle/configuration'
|
9
|
+
require 'castle/client'
|
10
|
+
require 'castle/errors'
|
11
|
+
require 'castle/system'
|
12
|
+
require 'castle/extractors/client_id'
|
13
|
+
require 'castle/extractors/headers'
|
14
|
+
require 'castle/extractors/ip'
|
15
|
+
require 'castle/headers'
|
16
|
+
require 'castle/response'
|
17
|
+
require 'castle/request'
|
18
|
+
require 'castle/api'
|
19
|
+
require 'castle/cookie_store'
|
20
|
+
|
21
|
+
# main sdk module
|
22
|
+
module Castle
|
23
|
+
class << self
|
24
|
+
def configure(config_hash = nil)
|
25
|
+
(config_hash || {}).each do |config_name, config_value|
|
26
|
+
config.send("#{config_name}=", config_value)
|
27
|
+
end
|
28
|
+
|
29
|
+
yield(config) if block_given?
|
30
|
+
end
|
31
|
+
|
32
|
+
def config
|
33
|
+
@configuration ||= Castle::Configuration.new
|
34
|
+
end
|
35
|
+
|
36
|
+
def api_secret=(api_secret)
|
37
|
+
config.api_secret = api_secret
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
require 'castle/support'
|
data/lib/castle/api.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Castle
|
4
|
+
# this class is responsible for making requests to api
|
5
|
+
class API
|
6
|
+
def initialize(cookie_id, ip, headers)
|
7
|
+
@config = Castle.config
|
8
|
+
@config_api_endpoint = @config.api_endpoint
|
9
|
+
@http = prepare_http
|
10
|
+
@headers = Castle::Headers.new.prepare(cookie_id, ip, headers)
|
11
|
+
end
|
12
|
+
|
13
|
+
def request_query(endpoint)
|
14
|
+
request = Castle::Request.new(@headers).build_query(endpoint)
|
15
|
+
perform_request(request)
|
16
|
+
end
|
17
|
+
|
18
|
+
def request(endpoint, args, method = :post)
|
19
|
+
request = Castle::Request.new(@headers).build(endpoint, args, method)
|
20
|
+
perform_request(request)
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def prepare_http
|
26
|
+
http = Net::HTTP.new(
|
27
|
+
@config_api_endpoint.host,
|
28
|
+
@config_api_endpoint.port
|
29
|
+
)
|
30
|
+
http.read_timeout = @config.request_timeout
|
31
|
+
prepare_http_for_ssl(http) if @config_api_endpoint.scheme == 'https'
|
32
|
+
http
|
33
|
+
end
|
34
|
+
|
35
|
+
def prepare_http_for_ssl(http)
|
36
|
+
http.use_ssl = true
|
37
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
38
|
+
end
|
39
|
+
|
40
|
+
def perform_request(req)
|
41
|
+
Castle::Response.new(@http.request(req)).parse
|
42
|
+
rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, EOFError,
|
43
|
+
Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError,
|
44
|
+
Net::ProtocolError
|
45
|
+
raise Castle::RequestError, 'Castle API connection error'
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Castle
|
4
|
+
class Client
|
5
|
+
attr_accessor :api
|
6
|
+
|
7
|
+
def initialize(request, response)
|
8
|
+
@do_not_track = false
|
9
|
+
cookie_id = Extractors::ClientId.new(request).call(response, '__cid')
|
10
|
+
ip = Extractors::IP.new(request).call
|
11
|
+
headers = Extractors::Headers.new(request).call
|
12
|
+
@api = API.new(cookie_id, ip, headers)
|
13
|
+
end
|
14
|
+
|
15
|
+
def fetch_review(id)
|
16
|
+
@api.request_query("reviews/#{id}")
|
17
|
+
end
|
18
|
+
|
19
|
+
def identify(args)
|
20
|
+
@api.request('identify', args) unless do_not_track?
|
21
|
+
end
|
22
|
+
|
23
|
+
def authenticate(args)
|
24
|
+
@api.request('authenticate', args)
|
25
|
+
end
|
26
|
+
|
27
|
+
def track(args)
|
28
|
+
@api.request('track', args) unless do_not_track?
|
29
|
+
end
|
30
|
+
|
31
|
+
def do_not_track!
|
32
|
+
@do_not_track = true
|
33
|
+
end
|
34
|
+
|
35
|
+
def track!
|
36
|
+
@do_not_track = false
|
37
|
+
end
|
38
|
+
|
39
|
+
def do_not_track?
|
40
|
+
@do_not_track
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Castle
|
4
|
+
# manages configuration variables
|
5
|
+
class Configuration
|
6
|
+
SUPPORTED = %i[source_header request_timeout api_secret api_endpoint].freeze
|
7
|
+
REQUEST_TIMEOUT = 30.0
|
8
|
+
API_ENDPOINT = 'https://api.castle.io/v1'
|
9
|
+
|
10
|
+
attr_accessor :request_timeout, :source_header
|
11
|
+
attr_reader :api_secret, :api_endpoint
|
12
|
+
|
13
|
+
def initialize
|
14
|
+
@request_timeout = REQUEST_TIMEOUT
|
15
|
+
self.api_endpoint = API_ENDPOINT
|
16
|
+
self.api_secret = ''
|
17
|
+
end
|
18
|
+
|
19
|
+
def api_endpoint=(value)
|
20
|
+
@api_endpoint = URI(
|
21
|
+
ENV.fetch('CASTLE_API_ENDPOINT', value)
|
22
|
+
)
|
23
|
+
end
|
24
|
+
|
25
|
+
def api_secret=(value)
|
26
|
+
@api_secret = ENV.fetch('CASTLE_API_SECRET', value).to_s
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def respond_to_missing?(method_name, _include_private)
|
32
|
+
/^(\w+)=$/ =~ method_name
|
33
|
+
end
|
34
|
+
|
35
|
+
def method_missing(_m, *_args)
|
36
|
+
raise Castle::ConfigurationError, 'there is no such a config'
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -1,6 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Castle
|
2
4
|
module CookieStore
|
3
5
|
class Base
|
6
|
+
EXPIRATION_TIME = 20 * 365 * 24 * 60 * 60 # cookie expiration time 20y
|
7
|
+
|
4
8
|
def initialize(request, response)
|
5
9
|
@request = request
|
6
10
|
@response = response
|
@@ -13,9 +17,10 @@ module Castle
|
|
13
17
|
def []=(key, value)
|
14
18
|
@request.cookies[key] = value
|
15
19
|
if value
|
16
|
-
@response.set_cookie(key,
|
17
|
-
|
18
|
-
|
20
|
+
@response.set_cookie(key,
|
21
|
+
value: value,
|
22
|
+
expires: Time.now + EXPIRATION_TIME,
|
23
|
+
path: '/')
|
19
24
|
else
|
20
25
|
@response.delete_cookie(key)
|
21
26
|
end
|
@@ -35,7 +40,7 @@ module Castle
|
|
35
40
|
if value
|
36
41
|
@cookies[key] = {
|
37
42
|
value: value,
|
38
|
-
expires: Time.now +
|
43
|
+
expires: Time.now + EXPIRATION_TIME,
|
39
44
|
path: '/'
|
40
45
|
}
|
41
46
|
else
|
@@ -43,6 +48,5 @@ module Castle
|
|
43
48
|
end
|
44
49
|
end
|
45
50
|
end
|
46
|
-
|
47
51
|
end
|
48
52
|
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Castle
|
4
|
+
# general error
|
5
|
+
class Error < RuntimeError; end
|
6
|
+
# request error
|
7
|
+
class RequestError < Castle::Error; end
|
8
|
+
# security error
|
9
|
+
class SecurityError < Castle::Error; end
|
10
|
+
# wrong configuration error
|
11
|
+
class ConfigurationError < Castle::Error; end
|
12
|
+
# error returned by api
|
13
|
+
class ApiError < Castle::Error; end
|
14
|
+
|
15
|
+
# api error bad request 400
|
16
|
+
class BadRequestError < Castle::ApiError; end
|
17
|
+
# api error forbidden 403
|
18
|
+
class ForbiddenError < Castle::ApiError; end
|
19
|
+
# api error not found 404
|
20
|
+
class NotFoundError < Castle::ApiError; end
|
21
|
+
# api error user unauthorized 419
|
22
|
+
class UserUnauthorizedError < Castle::ApiError; end
|
23
|
+
# api error invalid param 422
|
24
|
+
class InvalidParametersError < Castle::ApiError; end
|
25
|
+
# api error unauthorized 401
|
26
|
+
class UnauthorizedError < Castle::ApiError; end
|
27
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Castle
|
4
|
+
module Extractors
|
5
|
+
# used for extraction of cookies and headers from the request
|
6
|
+
class ClientId
|
7
|
+
def initialize(request)
|
8
|
+
@request = request
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(response, name)
|
12
|
+
extract_cookie(response)[name] ||
|
13
|
+
@request.env.fetch('HTTP_X_CASTLE_CLIENT_ID', '')
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
# Extract the cookie set by the Castle JavaScript
|
19
|
+
def extract_cookie(response)
|
20
|
+
if response.class.name == 'ActionDispatch::Cookies::CookieJar'
|
21
|
+
Castle::CookieStore::Rack.new(response)
|
22
|
+
else
|
23
|
+
Castle::CookieStore::Base.new(@request, response)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Castle
|
4
|
+
module Extractors
|
5
|
+
# used for extraction of cookies and headers from the request
|
6
|
+
class Headers
|
7
|
+
def initialize(request)
|
8
|
+
@request = request
|
9
|
+
@request_env = @request.env
|
10
|
+
@disabled_headers = ['Cookie']
|
11
|
+
end
|
12
|
+
|
13
|
+
# Serialize HTTP headers
|
14
|
+
def call
|
15
|
+
headers = http_headers.each_with_object({}) do |header, acc|
|
16
|
+
name = format_header_name(header)
|
17
|
+
unless @disabled_headers.include?(name)
|
18
|
+
acc[name] = @request_env[header]
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
JSON.generate(headers)
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def format_header_name(header)
|
28
|
+
header.gsub(/^HTTP_/, '').split('_').map(&:capitalize).join('-')
|
29
|
+
end
|
30
|
+
|
31
|
+
def http_headers
|
32
|
+
@request_env.keys.grep(/^HTTP_/)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Castle
|
4
|
+
# setups headers for requests
|
5
|
+
class Headers
|
6
|
+
def initialize
|
7
|
+
@config = Castle.config
|
8
|
+
@headers = {
|
9
|
+
'Content-Type' => 'application/json',
|
10
|
+
'User-Agent' => "Castle/v1 RubyBindings/#{Castle::VERSION}"
|
11
|
+
}
|
12
|
+
end
|
13
|
+
|
14
|
+
def prepare(client_id, ip, castle_headers)
|
15
|
+
@headers.merge!(
|
16
|
+
'X-Castle-Client-Id' => client_id,
|
17
|
+
'X-Castle-Ip' => ip,
|
18
|
+
'X-Castle-Headers' => castle_headers,
|
19
|
+
'X-Castle-Client-User-Agent' => JSON.generate(client_user_agent),
|
20
|
+
'X-Castle-Source' => @config.source_header
|
21
|
+
)
|
22
|
+
@headers.delete_if { |_k, header_value| header_value.nil? }
|
23
|
+
@headers
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def client_user_agent
|
29
|
+
{
|
30
|
+
bindings_version: Castle::VERSION,
|
31
|
+
lang: 'ruby',
|
32
|
+
lang_version: Castle::System.ruby_version,
|
33
|
+
platform: Castle::System.platform,
|
34
|
+
publisher: 'castle',
|
35
|
+
uname: Castle::System.uname
|
36
|
+
}
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,34 @@
|
|
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_query(endpoint)
|
12
|
+
request = Net::HTTP::Get.new(
|
13
|
+
"#{@config.api_endpoint.path}/#{endpoint}", @headers
|
14
|
+
)
|
15
|
+
add_basic_auth(request)
|
16
|
+
request
|
17
|
+
end
|
18
|
+
|
19
|
+
def build(endpoint, args, method)
|
20
|
+
request = Net::HTTP.const_get(method.to_s.capitalize).new(
|
21
|
+
"#{@config.api_endpoint.path}/#{endpoint}", @headers
|
22
|
+
)
|
23
|
+
request.body = args.to_json
|
24
|
+
add_basic_auth(request)
|
25
|
+
request
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def add_basic_auth(request)
|
31
|
+
request.basic_auth('', @config.api_secret)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|