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