castle-rb 4.0.0 → 5.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 40234604d033086c79d951cbec5a52902df4ccda433eff1749c20cfeaca114d6
4
- data.tar.gz: e46b4d91acbcf6c370de3a0804d3f9e75ad86bc181b40e2727e92d5d63579953
3
+ metadata.gz: 441cbae1aed67e419bff1683c41183abb10c3890d5a159f3365c0b4bd3855f0b
4
+ data.tar.gz: 6ef7663b21e2de6b1ef833917dd4b62ee1891f017e33f88a1cb57252e5c3bcc4
5
5
  SHA512:
6
- metadata.gz: d70f1b03fb44e3d23afcb03ca83165f5b521a53269f34d2ed63740cc181e110da3692db50d6f701477a24430262d08859a71f7faeae8867c8962242d7421545d
7
- data.tar.gz: b4a32a603c55df97450e59f936677d5b56387b8d4bf145d4355ca7f5ec606c557d00da72a92bb3f2b641d93a3d3f6e21b9f9412fff35f1840c43db66789b3b6c
6
+ metadata.gz: b54200b206ea055878d2e4a6b46f82960cda186c4b57009765c7e4c8cce28c7f00c66ad216840f8fa5412a7ed876224f285a18388620bf7df9201425a0efb055
7
+ data.tar.gz: cf9f8b5019377138fe5e61ced02b6323aa27464dc117c521fe560a6832115416bbd7e92a64a4710b96939adcd49af12feb538f3d24392d9654b14a21a48021a5
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Ruby SDK for Castle
2
2
 
3
- [![Build Status](https://travis-ci.org/castle/castle-ruby.svg?branch=master)](https://travis-ci.org/castle/castle-ruby)
3
+ [![Build Status](https://circleci.com/gh/castle/castle-ruby.svg?style=shield&branch=master)](https://circleci.com/gh/castle/castle-ruby)
4
4
  [![Coverage Status](https://coveralls.io/repos/github/castle/castle-ruby/badge.svg?branch=coveralls)](https://coveralls.io/github/castle/castle-ruby?branch=coveralls)
5
5
  [![Gem Version](https://badge.fury.io/rb/castle-rb.svg)](https://badge.fury.io/rb/castle-rb)
6
6
 
@@ -65,39 +65,61 @@ Castle.configure do |config|
65
65
  # Castle::RequestError is raised when timing out in milliseconds (default: 500 milliseconds)
66
66
  config.request_timeout = 2000
67
67
 
68
- # Whitelisted and Blacklisted headers are case insensitive and allow to use _ and - as a separator, http prefixes are removed
69
- # Whitelisted headers
68
+ # Allowlisted and Denylisted headers are case insensitive and allow to use _ and - as a separator, http prefixes are removed
69
+ # Allowlisted headers
70
70
  # By default, the SDK sends all HTTP headers, except for Cookie and Authorization.
71
- # If you decide to use a whitelist, the SDK will:
71
+ # If you decide to use a allowlist, the SDK will:
72
72
  # - always send the User-Agent header
73
- # - send scrubbed values of non-whitelisted headers
74
- # - send proper values of whitelisted headers.
73
+ # - send scrubbed values of non-allowlisted headers
74
+ # - send proper values of allowlisted headers.
75
75
  # @example
76
- # config.whitelisted = ['X_HEADER']
76
+ # config.allowlisted = ['X_HEADER']
77
77
  # # will send { 'User-Agent' => 'Chrome', 'X_HEADER' => 'proper value', 'Any-Other-Header' => true }
78
78
  #
79
- # We highly suggest using blacklist instead of whitelist, so that Castle can use as many data points
80
- # as possible to secure your users. If you want to use the whitelist, this is the minimal
79
+ # We highly suggest using denylist instead of allowlist, so that Castle can use as many data points
80
+ # as possible to secure your users. If you want to use the allowlist, this is the minimal
81
81
  # amount of headers we recommend:
82
- config.whitelisted = Castle::Configuration::DEFAULT_WHITELIST
82
+ config.allowlisted = Castle::Configuration::DEFAULT_ALLOWLIST
83
83
 
84
- # Blacklisted headers take precedence over whitelisted elements
85
- # We always blacklist Cookie and Authentication headers. If you use any other headers that
86
- # might contain sensitive information, you should blacklist them.
87
- config.blacklisted = ['HTTP-X-header']
84
+ # Denylisted headers take precedence over allowlisted elements
85
+ # We always denylist Cookie and Authentication headers. If you use any other headers that
86
+ # might contain sensitive information, you should denylist them.
87
+ config.denylisted = ['HTTP-X-header']
88
88
 
89
89
  # Castle needs the original IP of the client, not the IP of your proxy or load balancer.
90
- # we try to fetch proper ip based on X-Forwarded-For, X-Client-Id or Remote-Addr headers in that order
91
- # but sometimes proper ip may be stored in different header or order could be different.
92
- # SDK can extract ip automatically for you, but you must configure which ip_headers you would like to use
90
+ # The SDK will only trust the proxy chain as defined in the configuration.
91
+ # We try to fetch the client IP based on X-Forwarded-For or Remote-Addr headers in that order,
92
+ # but sometimes the client IP may be stored in a different header or order.
93
+ # The SDK can be configured to look for the client IP address in headers that you specify.
94
+
95
+ # Sometimes, Cloud providers do not use consistent IP addresses to proxy requests.
96
+ # In this case, the client IP is usually preserved in a custom header. Example:
97
+ # Cloudflare preserves the client request in the 'Cf-Connecting-Ip' header.
98
+ # It would be used like so: configuration.ip_headers=['Cf-Connecting-Ip']
93
99
  configuration.ip_headers = []
94
100
 
95
- # Additionally to make X-Forwarded-For or X-Client-Id work better discovering client ip address,
101
+ # If the specified header or X-Forwarded-For default contains a proxy chain with public IP addresses,
102
+ # then one of the following must be set
103
+ # 1. The trusted_proxies value must match the known proxy IP's
104
+ # 2. The trusted_proxy_depth value must be set to the number of known trusted proxies in the chain (see below)
105
+
106
+ # Additionally to make X-Forwarded-For and other headers work better discovering client ip address,
96
107
  # and not the address of a reverse proxy server, you can define trusted proxies
97
108
  # which will help to fetch proper ip from those headers
109
+
110
+ # In order to extract the client IP of the X-Forwarded-For header
111
+ # and not the address of a reverse proxy server, you must define all trusted public proxies
112
+ # you can achieve this by listing all the proxies ip defined by string or regular expressions
113
+ # in trusted_proxies setting
98
114
  configuration.trusted_proxies = []
99
- # *Note: proxies list can be provided as an array of regular expressions
100
- # *Note: default always marked as trusty list is here: Castle::Configuration::TRUSTED_PROXIES
115
+ # or by providing number of trusted proxies used in the chain
116
+ configuration.trusted_proxy_depth = 0
117
+
118
+ # If there is no possibility to define options above and there is no other header which can have client ip
119
+ # then you may set trust_proxy_chain = true to trust all of the proxy IP's in X-Forwarded-For
120
+ configuration.trust_proxy_chain = false
121
+
122
+ # *Note: default list of proxies which is always marked as trusted: Castle::Configuration::TRUSTED_PROXIES
101
123
  end
102
124
  ```
103
125
 
@@ -175,6 +197,25 @@ track_options = ::Castle::Client.to_options({
175
197
  CastleTrackingWorker.perform_async(request_context, track_options)
176
198
  ```
177
199
 
200
+ ## Connection reuse
201
+
202
+ If you want to reuse the connection to send multiple events:
203
+
204
+ ```ruby
205
+ Castle::API::Session.call do |http|
206
+ castle.track(
207
+ event: ::Castle::Events::LOGOUT_SUCCEEDED,
208
+ user_id: user2.id
209
+ http: http
210
+ )
211
+ castle.track(
212
+ event: ::Castle::Events::LOGIN_SUCCEEDED,
213
+ user_id: user1.id
214
+ http: http
215
+ )
216
+ end
217
+ ```
218
+
178
219
  ## Events
179
220
 
180
221
  List of Recognized Events can be found [here](https://github.com/castle/castle-ruby/tree/master/lib/castle/events.rb) or in the [docs](https://docs.castle.io/api_reference/#list-of-recognized-events)
@@ -29,15 +29,16 @@
29
29
  castle/configuration
30
30
  castle/failover_auth_response
31
31
  castle/client
32
- castle/header_filter
33
- castle/header_formatter
32
+ castle/headers_filter
33
+ castle/headers_formatter
34
34
  castle/secure_mode
35
35
  castle/extractors/client_id
36
36
  castle/extractors/headers
37
37
  castle/extractors/ip
38
+ castle/api/connection
38
39
  castle/api/response
39
40
  castle/api/request
40
- castle/api/request/build
41
+ castle/api/session
41
42
  castle/review
42
43
  castle/api
43
44
  ].each(&method(:require))
@@ -54,7 +55,7 @@ module Castle
54
55
  end
55
56
 
56
57
  def config
57
- @config ||= Castle::Configuration.new
58
+ Configuration.instance
58
59
  end
59
60
 
60
61
  def api_secret=(api_secret)
@@ -17,16 +17,18 @@ module Castle
17
17
  private_constant :HANDLED_ERRORS
18
18
 
19
19
  class << self
20
- def request(command, headers = {})
20
+ # @param command [String]
21
+ # @param headers [Hash]
22
+ # @param http [Net::HTTP]
23
+ def request(command, headers = {}, http = nil)
21
24
  raise Castle::ConfigurationError, 'configuration is not valid' unless Castle.config.valid?
22
25
 
23
26
  begin
24
- Castle::API::Response.call(
25
- Castle::API::Request.call(
26
- command,
27
- Castle.config.api_secret,
28
- headers
29
- )
27
+ Castle::API::Request.call(
28
+ command,
29
+ Castle.config.api_secret,
30
+ headers,
31
+ http
30
32
  )
31
33
  rescue *HANDLED_ERRORS => e
32
34
  # @note We need to initialize the error, as the original error is a cause for this
@@ -35,6 +37,13 @@ module Castle
35
37
  raise Castle::RequestError.new(e) # rubocop:disable Style/RaiseArgs
36
38
  end
37
39
  end
40
+
41
+ # @param command [String]
42
+ # @param headers [Hash]
43
+ # @param http [Net::HTTP]
44
+ def call(command, headers = {}, http = nil)
45
+ Castle::API::Response.call(request(command, headers, http))
46
+ end
38
47
  end
39
48
  end
40
49
  end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castle
4
+ module API
5
+ # this module returns a new configured Net::HTTP object
6
+ module Connection
7
+ HTTPS_SCHEME = 'https'
8
+
9
+ class << self
10
+ def call
11
+ http = Net::HTTP.new(Castle.config.url.host, Castle.config.url.port)
12
+ http.read_timeout = Castle.config.request_timeout / 1000.0
13
+
14
+ if Castle.config.url.scheme == HTTPS_SCHEME
15
+ http.use_ssl = true
16
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
17
+ end
18
+
19
+ http
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Castle
4
- # this class is responsible for making requests to api
5
4
  module API
5
+ # this class is responsible for making requests to api
6
6
  module Request
7
7
  # Default headers that we add to passed ones
8
8
  DEFAULT_HEADERS = {
@@ -12,9 +12,9 @@ module Castle
12
12
  private_constant :DEFAULT_HEADERS
13
13
 
14
14
  class << self
15
- def call(command, api_secret, headers)
16
- http.request(
17
- Castle::API::Request::Build.call(
15
+ def call(command, api_secret, headers, http = nil)
16
+ (http || Castle::API::Connection.call).request(
17
+ build(
18
18
  command,
19
19
  headers.merge(DEFAULT_HEADERS),
20
20
  api_secret
@@ -22,14 +22,19 @@ module Castle
22
22
  )
23
23
  end
24
24
 
25
- def http
26
- http = Net::HTTP.new(Castle.config.host, Castle.config.port)
27
- http.read_timeout = Castle.config.request_timeout / 1000.0
28
- if Castle.config.port == 443
29
- http.use_ssl = true
30
- http.verify_mode = OpenSSL::SSL::VERIFY_PEER
25
+ def build(command, headers, api_secret)
26
+ request_obj = Net::HTTP.const_get(
27
+ command.method.to_s.capitalize
28
+ ).new("#{Castle.config.url.path}/#{command.path}", headers)
29
+
30
+ unless command.method == :get
31
+ request_obj.body = ::Castle::Utils.replace_invalid_characters(
32
+ command.data
33
+ ).to_json
31
34
  end
32
- http
35
+
36
+ request_obj.basic_auth('', api_secret)
37
+ request_obj
33
38
  end
34
39
  end
35
40
  end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castle
4
+ module API
5
+ # this module uses the Connection object
6
+ # and provides start method for persistent connection usage
7
+ # when there is a need of sending multiple requests at once
8
+ module Session
9
+ HTTPS_SCHEME = 'https'
10
+
11
+ class << self
12
+ def call(&block)
13
+ return unless block_given?
14
+
15
+ Connection.call.start(&block)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -42,9 +42,11 @@ module Castle
42
42
  return generate_do_not_track_response(options[:user_id]) unless tracked?
43
43
 
44
44
  add_timestamp_if_necessary(options)
45
- command = Castle::Commands::Authenticate.new(@context).build(options)
45
+
46
46
  begin
47
- Castle::API.request(command).merge(failover: false, failover_reason: nil)
47
+ Castle::API
48
+ .call(authenticate_command(options), {}, options[:http])
49
+ .merge(failover: false, failover_reason: nil)
48
50
  rescue Castle::RequestError, Castle::InternalServerError => e
49
51
  self.class.failover_response_or_raise(
50
52
  FailoverAuthResponse.new(options[:user_id], reason: e.to_s), e
@@ -59,8 +61,7 @@ module Castle
59
61
 
60
62
  add_timestamp_if_necessary(options)
61
63
 
62
- command = Castle::Commands::Identify.new(@context).build(options)
63
- Castle::API.request(command)
64
+ Castle::API.call(identify_command(options), {}, options[:http])
64
65
  end
65
66
 
66
67
  def track(options = {})
@@ -70,15 +71,15 @@ module Castle
70
71
 
71
72
  add_timestamp_if_necessary(options)
72
73
 
73
- command = Castle::Commands::Track.new(@context).build(options)
74
- Castle::API.request(command)
74
+ Castle::API.call(track_command(options), {}, options[:http])
75
75
  end
76
76
 
77
77
  def impersonate(options = {})
78
78
  options = Castle::Utils.deep_symbolize_keys(options || {})
79
+
79
80
  add_timestamp_if_necessary(options)
80
- command = Castle::Commands::Impersonate.new(@context).build(options)
81
- Castle::API.request(command).tap do |response|
81
+
82
+ Castle::API.call(impersonate_command(options), {}, options[:http]).tap do |response|
82
83
  raise Castle::ImpersonationFailed unless response[:success]
83
84
  end
84
85
  end
@@ -104,6 +105,26 @@ module Castle
104
105
  ).generate
105
106
  end
106
107
 
108
+ # @param options [Hash]
109
+ def authenticate_command(options)
110
+ Castle::Commands::Authenticate.new(@context).build(options)
111
+ end
112
+
113
+ # @param options [Hash]
114
+ def identify_command(options)
115
+ Castle::Commands::Identify.new(@context).build(options)
116
+ end
117
+
118
+ # @param options [Hash]
119
+ def impersonate_command(options)
120
+ Castle::Commands::Impersonate.new(@context).build(options)
121
+ end
122
+
123
+ # @param options [Hash]
124
+ def track_command(options)
125
+ Castle::Commands::Track.new(@context).build(options)
126
+ end
127
+
107
128
  def add_timestamp_if_necessary(options)
108
129
  options[:timestamp] ||= @timestamp if @timestamp
109
130
  end
@@ -1,11 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'singleton'
4
+ require 'uri'
5
+
3
6
  module Castle
4
7
  # manages configuration variables
5
8
  class Configuration
6
- HOST = 'api.castle.io'
7
- PORT = 443
8
- URL_PREFIX = 'v1'
9
+ include Singleton
10
+
11
+ # API endpoint
12
+ URL = 'https://api.castle.io/v1'
9
13
  FAILOVER_STRATEGY = :allow
10
14
  REQUEST_TIMEOUT = 500 # in milliseconds
11
15
  FAILOVER_STRATEGIES = %i[allow deny challenge throw].freeze
@@ -19,9 +23,9 @@ module Castle
19
23
  \Aunix:
20
24
  /ix].freeze
21
25
 
22
- # @note this value is not assigned as we don't recommend using a whitelist. If you need to use
26
+ # @note this value is not assigned as we don't recommend using a allowlist. If you need to use
23
27
  # one, this constant is provided as a good default.
24
- DEFAULT_WHITELIST = %w[
28
+ DEFAULT_ALLOWLIST = %w[
25
29
  Accept
26
30
  Accept-Charset
27
31
  Accept-Datetime
@@ -31,42 +35,58 @@ module Castle
31
35
  Connection
32
36
  Content-Length
33
37
  Content-Type
38
+ Dnt
34
39
  Host
35
40
  Origin
36
41
  Pragma
37
42
  Referer
38
- TE
43
+ Sec-Fetch-Dest
44
+ Sec-Fetch-Mode
45
+ Sec-Fetch-Site
46
+ Sec-Fetch-User
47
+ Te
39
48
  Upgrade-Insecure-Requests
49
+ User-Agent
40
50
  X-Castle-Client-Id
51
+ X-Requested-With
41
52
  ].freeze
42
53
 
43
- attr_accessor :host, :port, :request_timeout, :url_prefix
44
- attr_reader :api_secret, :whitelisted, :blacklisted, :failover_strategy, :ip_headers, :trusted_proxies
54
+ attr_accessor :request_timeout, :trust_proxy_chain
55
+ attr_reader :api_secret, :allowlisted, :denylisted, :failover_strategy, :ip_headers,
56
+ :trusted_proxies, :trusted_proxy_depth, :url
45
57
 
46
58
  def initialize
47
- @formatter = Castle::HeaderFormatter.new
59
+ @formatter = Castle::HeadersFormatter
48
60
  @request_timeout = REQUEST_TIMEOUT
61
+ reset
62
+ end
63
+
64
+ def reset
49
65
  self.failover_strategy = FAILOVER_STRATEGY
50
- self.host = HOST
51
- self.port = PORT
52
- self.url_prefix = URL_PREFIX
53
- self.whitelisted = [].freeze
54
- self.blacklisted = [].freeze
66
+ self.url = URL
67
+ self.allowlisted = [].freeze
68
+ self.denylisted = [].freeze
55
69
  self.api_secret = ENV.fetch('CASTLE_API_SECRET', '')
56
70
  self.ip_headers = [].freeze
57
71
  self.trusted_proxies = [].freeze
72
+ self.trust_proxy_chain = false
73
+ self.trusted_proxy_depth = nil
74
+ end
75
+
76
+ def url=(value)
77
+ @url = URI(value)
58
78
  end
59
79
 
60
80
  def api_secret=(value)
61
81
  @api_secret = value.to_s
62
82
  end
63
83
 
64
- def whitelisted=(value)
65
- @whitelisted = (value ? value.map { |header| @formatter.call(header) } : []).freeze
84
+ def allowlisted=(value)
85
+ @allowlisted = (value ? value.map { |header| @formatter.call(header) } : []).freeze
66
86
  end
67
87
 
68
- def blacklisted=(value)
69
- @blacklisted = (value ? value.map { |header| @formatter.call(header) } : []).freeze
88
+ def denylisted=(value)
89
+ @denylisted = (value ? value.map { |header| @formatter.call(header) } : []).freeze
70
90
  end
71
91
 
72
92
  # sets ip headers
@@ -78,15 +98,20 @@ module Castle
78
98
  end
79
99
 
80
100
  # sets trusted proxies
81
- # @param value [Array<String|Regexp>]
101
+ # @param value [Array<String,Regexp>]
82
102
  def trusted_proxies=(value)
83
103
  raise Castle::ConfigurationError, 'trusted proxies must be an Array' unless value.is_a?(Array)
84
104
 
85
105
  @trusted_proxies = value
86
106
  end
87
107
 
108
+ # @param value [String,Number,NilClass]
109
+ def trusted_proxy_depth=(value)
110
+ @trusted_proxy_depth = value.to_i
111
+ end
112
+
88
113
  def valid?
89
- !api_secret.to_s.empty? && !host.to_s.empty? && !port.to_s.empty?
114
+ !api_secret.to_s.empty? && !url.host.to_s.empty? && !url.port.to_s.empty?
90
115
  end
91
116
 
92
117
  def failover_strategy=(value)