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.
Files changed (42) hide show
  1. checksums.yaml +2 -5
  2. data/README.md +1 -1
  3. data/lib/castle-rb.rb +2 -19
  4. data/lib/castle.rb +42 -0
  5. data/lib/castle/api.rb +48 -0
  6. data/lib/castle/client.rb +43 -0
  7. data/lib/castle/configuration.rb +39 -0
  8. data/lib/{castle-rb/support → castle}/cookie_store.rb +9 -5
  9. data/lib/castle/errors.rb +27 -0
  10. data/lib/castle/extractors/client_id.rb +28 -0
  11. data/lib/castle/extractors/headers.rb +36 -0
  12. data/lib/castle/extractors/ip.rb +16 -0
  13. data/lib/castle/headers.rb +39 -0
  14. data/lib/castle/request.rb +34 -0
  15. data/lib/castle/response.rb +43 -0
  16. data/lib/castle/support.rb +11 -0
  17. data/lib/{castle-rb → castle}/support/padrino.rb +1 -1
  18. data/lib/{castle-rb → castle}/support/rails.rb +2 -0
  19. data/lib/{castle-rb → castle}/support/sinatra.rb +1 -1
  20. data/lib/castle/system.rb +36 -0
  21. data/lib/castle/version.rb +5 -0
  22. data/spec/lib/castle/api_spec.rb +54 -0
  23. data/spec/lib/castle/client_spec.rb +64 -0
  24. data/spec/lib/castle/configuration_spec.rb +98 -0
  25. data/spec/lib/castle/extractors/client_id_spec.rb +39 -0
  26. data/spec/lib/castle/extractors/headers_spec.rb +25 -0
  27. data/spec/lib/castle/extractors/ip_spec.rb +20 -0
  28. data/spec/lib/castle/headers_spec.rb +82 -0
  29. data/spec/lib/castle/request_spec.rb +37 -0
  30. data/spec/lib/castle/response_spec.rb +71 -0
  31. data/spec/lib/castle/system_spec.rb +70 -0
  32. data/spec/lib/castle/version_spec.rb +9 -0
  33. data/spec/lib/castle_spec.rb +73 -0
  34. data/spec/spec_helper.rb +9 -3
  35. metadata +49 -76
  36. data/lib/castle-rb/api.rb +0 -94
  37. data/lib/castle-rb/client.rb +0 -67
  38. data/lib/castle-rb/configuration.rb +0 -48
  39. data/lib/castle-rb/errors.rb +0 -15
  40. data/lib/castle-rb/version.rb +0 -3
  41. data/spec/api_spec.rb +0 -31
  42. 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: 16c66f7e9c363791316aa12aac26ab42f77770929ce8fca7732b2fdf26e3cf4c8efb304bd8b6683f34c9d31ff74e4b01e88ff4efa44caf7e11d4854c89c35760
7
- data.tar.gz: 1287a6ac045069c0ef795c4764c8e1235c04dcf0a7b8a696d3e88c499f815cd3f112f1bad848580006c55d7ffdb80ee5d25e3477616f8e7ede2c98dbda5f1ef2
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-rb/errors.rb).
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
- require 'openssl'
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, value: value,
17
- expires: Time.now + (20 * 365 * 24 * 60 * 60),
18
- path: '/')
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 + (20 * 365 * 24 * 60 * 60),
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,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castle
4
+ module Extractors
5
+ # used for extraction of ip from the request
6
+ class IP
7
+ def initialize(request)
8
+ @request = request
9
+ end
10
+
11
+ def call
12
+ @request.ip
13
+ end
14
+ end
15
+ end
16
+ 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