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
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
|