castle-rb 4.0.0 → 5.0.0

Sign up to get free protection for your applications and to get access to all the features.
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)