castle-rb 4.2.1 → 7.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 +4 -4
 - data/README.md +160 -45
 - data/lib/castle.rb +49 -28
 - data/lib/castle/api.rb +21 -14
 - data/lib/castle/api/approve_device.rb +20 -0
 - data/lib/castle/api/authenticate.rb +37 -0
 - data/lib/castle/api/end_impersonation.rb +24 -0
 - data/lib/castle/api/filter.rb +37 -0
 - data/lib/castle/api/get_device.rb +20 -0
 - data/lib/castle/api/get_devices_for_user.rb +20 -0
 - data/lib/castle/api/log.rb +37 -0
 - data/lib/castle/api/report_device.rb +20 -0
 - data/lib/castle/api/risk.rb +37 -0
 - data/lib/castle/api/start_impersonation.rb +24 -0
 - data/lib/castle/api/track.rb +21 -0
 - data/lib/castle/client.rb +78 -51
 - data/lib/castle/{extractors/client_id.rb → client_id/extract.rb} +2 -2
 - data/lib/castle/commands/approve_device.rb +17 -0
 - data/lib/castle/commands/authenticate.rb +13 -13
 - data/lib/castle/commands/end_impersonation.rb +25 -0
 - data/lib/castle/commands/filter.rb +23 -0
 - data/lib/castle/commands/get_device.rb +17 -0
 - data/lib/castle/commands/get_devices_for_user.rb +17 -0
 - data/lib/castle/commands/log.rb +23 -0
 - data/lib/castle/commands/report_device.rb +17 -0
 - data/lib/castle/commands/risk.rb +23 -0
 - data/lib/castle/commands/start_impersonation.rb +25 -0
 - data/lib/castle/commands/track.rb +12 -13
 - data/lib/castle/configuration.rb +57 -32
 - data/lib/castle/context/{default.rb → get_default.rb} +5 -6
 - data/lib/castle/context/{merger.rb → merge.rb} +3 -3
 - data/lib/castle/context/prepare.rb +18 -0
 - data/lib/castle/context/{sanitizer.rb → sanitize.rb} +1 -1
 - data/lib/castle/core/get_connection.rb +27 -0
 - data/lib/castle/{api/response.rb → core/process_response.rb} +8 -3
 - data/lib/castle/core/process_webhook.rb +25 -0
 - data/lib/castle/core/send_request.rb +42 -0
 - data/lib/castle/errors.rb +38 -12
 - data/lib/castle/failover/prepare_response.rb +18 -0
 - data/lib/castle/failover/strategy.rb +23 -0
 - data/lib/castle/headers/extract.rb +47 -0
 - data/lib/castle/headers/filter.rb +40 -0
 - data/lib/castle/headers/format.rb +24 -0
 - data/lib/castle/{extractors/ip.rb → ips/extract.rb} +31 -9
 - data/lib/castle/logger.rb +19 -0
 - data/lib/castle/payload/prepare.rb +26 -0
 - data/lib/castle/secure_mode.rb +7 -2
 - data/lib/castle/session.rb +18 -0
 - data/lib/castle/singleton_configuration.rb +9 -0
 - data/lib/castle/support/hanami.rb +2 -6
 - data/lib/castle/support/rails.rb +1 -3
 - data/lib/castle/utils/clean_invalid_chars.rb +22 -0
 - data/lib/castle/utils/clone.rb +15 -0
 - data/lib/castle/utils/deep_symbolize_keys.rb +45 -0
 - data/lib/castle/utils/get_timestamp.rb +15 -0
 - data/lib/castle/utils/{merger.rb → merge.rb} +3 -3
 - data/lib/castle/utils/secure_compare.rb +22 -0
 - data/lib/castle/validators/not_supported.rb +1 -0
 - data/lib/castle/validators/present.rb +1 -0
 - data/lib/castle/verdict.rb +15 -0
 - data/lib/castle/version.rb +1 -1
 - data/lib/castle/webhooks/verify.rb +45 -0
 - data/spec/integration/rails/rails_spec.rb +42 -14
 - data/spec/integration/rails/support/application.rb +3 -1
 - data/spec/integration/rails/support/home_controller.rb +50 -6
 - data/spec/lib/castle/api/approve_device_spec.rb +21 -0
 - data/spec/lib/castle/api/authenticate_spec.rb +136 -0
 - data/spec/lib/castle/api/end_impersonation_spec.rb +65 -0
 - data/spec/lib/castle/api/filter_spec.rb +5 -0
 - data/spec/lib/castle/api/get_device_spec.rb +19 -0
 - data/spec/lib/castle/api/get_devices_for_user_spec.rb +19 -0
 - data/spec/lib/castle/api/log_spec.rb +5 -0
 - data/spec/lib/castle/api/report_device_spec.rb +21 -0
 - data/spec/lib/castle/api/risk_spec.rb +5 -0
 - data/spec/lib/castle/api/start_impersonation_spec.rb +65 -0
 - data/spec/lib/castle/api/track_spec.rb +72 -0
 - data/spec/lib/castle/api_spec.rb +14 -15
 - data/spec/lib/castle/{extractors/client_id_spec.rb → client_id/extract_spec.rb} +6 -15
 - data/spec/lib/castle/client_spec.rb +108 -93
 - data/spec/lib/castle/commands/approve_device_spec.rb +24 -0
 - data/spec/lib/castle/commands/authenticate_spec.rb +15 -31
 - data/spec/lib/castle/commands/end_impersonation_spec.rb +79 -0
 - data/spec/lib/castle/commands/filter_spec.rb +99 -0
 - data/spec/lib/castle/commands/get_device_spec.rb +24 -0
 - data/spec/lib/castle/commands/{review_spec.rb → get_devices_for_user_spec.rb} +7 -7
 - data/spec/lib/castle/commands/log_spec.rb +100 -0
 - data/spec/lib/castle/commands/report_device_spec.rb +24 -0
 - data/spec/lib/castle/commands/risk_spec.rb +100 -0
 - data/spec/lib/castle/commands/start_impersonation_spec.rb +79 -0
 - data/spec/lib/castle/commands/track_spec.rb +14 -34
 - data/spec/lib/castle/configuration_spec.rb +8 -141
 - data/spec/lib/castle/context/{default_spec.rb → get_default_spec.rb} +9 -10
 - data/spec/lib/castle/context/{merger_spec.rb → merge_spec.rb} +1 -1
 - data/spec/lib/castle/context/prepare_spec.rb +43 -0
 - data/spec/lib/castle/context/{sanitizer_spec.rb → sanitize_spec.rb} +1 -1
 - data/spec/lib/castle/core/get_connection_spec.rb +43 -0
 - data/spec/lib/castle/{api/response_spec.rb → core/process_response_spec.rb} +49 -1
 - data/spec/lib/castle/core/process_webhook_spec.rb +46 -0
 - data/spec/lib/castle/core/send_request_spec.rb +77 -0
 - data/spec/lib/castle/failover/strategy_spec.rb +12 -0
 - data/spec/lib/castle/{extractors/headers_spec.rb → headers/extract_spec.rb} +18 -20
 - data/spec/lib/castle/headers/filter_spec.rb +39 -0
 - data/spec/lib/castle/headers/format_spec.rb +25 -0
 - data/spec/lib/castle/{extractors/ip_spec.rb → ips/extract_spec.rb} +27 -8
 - data/spec/lib/castle/logger_spec.rb +38 -0
 - data/spec/lib/castle/payload/prepare_spec.rb +55 -0
 - data/spec/lib/castle/session_spec.rb +65 -0
 - data/spec/lib/castle/singleton_configuration_spec.rb +14 -0
 - data/spec/lib/castle/utils/clean_invalid_chars_spec.rb +69 -0
 - data/spec/lib/castle/utils/{cloner_spec.rb → clone_spec.rb} +3 -3
 - data/spec/lib/castle/utils/deep_symbolize_keys_spec.rb +50 -0
 - data/spec/lib/castle/utils/{timestamp_spec.rb → get_timestamp_spec.rb} +1 -1
 - data/spec/lib/castle/utils/merge_spec.rb +15 -0
 - data/spec/lib/castle/validators/present_spec.rb +5 -6
 - data/spec/lib/castle/verdict_spec.rb +9 -0
 - data/spec/lib/castle/webhooks/verify_spec.rb +53 -0
 - data/spec/lib/castle_spec.rb +4 -10
 - data/spec/spec_helper.rb +3 -3
 - data/spec/support/shared_examples/action_request.rb +152 -0
 - data/spec/support/shared_examples/configuration.rb +101 -0
 - metadata +146 -64
 - data/lib/castle/api/request.rb +0 -42
 - data/lib/castle/api/session.rb +0 -39
 - data/lib/castle/commands/identify.rb +0 -23
 - data/lib/castle/commands/impersonate.rb +0 -26
 - data/lib/castle/commands/review.rb +0 -14
 - data/lib/castle/events.rb +0 -49
 - data/lib/castle/extractors/headers.rb +0 -45
 - data/lib/castle/failover_auth_response.rb +0 -21
 - data/lib/castle/headers_filter.rb +0 -35
 - data/lib/castle/headers_formatter.rb +0 -22
 - data/lib/castle/review.rb +0 -11
 - data/lib/castle/utils.rb +0 -55
 - data/lib/castle/utils/cloner.rb +0 -11
 - data/lib/castle/utils/timestamp.rb +0 -12
 - data/spec/lib/castle/api/request_spec.rb +0 -72
 - data/spec/lib/castle/commands/identify_spec.rb +0 -88
 - data/spec/lib/castle/commands/impersonate_spec.rb +0 -107
 - data/spec/lib/castle/events_spec.rb +0 -5
 - data/spec/lib/castle/headers_filter_spec.rb +0 -37
 - data/spec/lib/castle/headers_formatter_spec.rb +0 -25
 - data/spec/lib/castle/review_spec.rb +0 -19
 - data/spec/lib/castle/utils/merger_spec.rb +0 -13
 - data/spec/lib/castle/utils_spec.rb +0 -156
 
| 
         @@ -0,0 +1,19 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Castle
         
     | 
| 
      
 4 
     | 
    
         
            +
              # module for logger handling
         
     | 
| 
      
 5 
     | 
    
         
            +
              module Logger
         
     | 
| 
      
 6 
     | 
    
         
            +
                class << self
         
     | 
| 
      
 7 
     | 
    
         
            +
                  # @param message [String]
         
     | 
| 
      
 8 
     | 
    
         
            +
                  # @param data [String]
         
     | 
| 
      
 9 
     | 
    
         
            +
                  # @param config [Castle::Configuration, Castle::SingletonConfiguration, nil]
         
     | 
| 
      
 10 
     | 
    
         
            +
                  def call(message, data = nil, config = nil)
         
     | 
| 
      
 11 
     | 
    
         
            +
                    logger = (config || Castle.config).logger
         
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
                    return unless logger
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
                    logger.info("[CASTLE] #{message} #{data}".strip)
         
     | 
| 
      
 16 
     | 
    
         
            +
                  end
         
     | 
| 
      
 17 
     | 
    
         
            +
                end
         
     | 
| 
      
 18 
     | 
    
         
            +
              end
         
     | 
| 
      
 19 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,26 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Castle
         
     | 
| 
      
 4 
     | 
    
         
            +
              module Payload
         
     | 
| 
      
 5 
     | 
    
         
            +
                # prepares payload based on the request
         
     | 
| 
      
 6 
     | 
    
         
            +
                module Prepare
         
     | 
| 
      
 7 
     | 
    
         
            +
                  class << self
         
     | 
| 
      
 8 
     | 
    
         
            +
                    # @param payload_options [Hash]
         
     | 
| 
      
 9 
     | 
    
         
            +
                    # @param request [Request]
         
     | 
| 
      
 10 
     | 
    
         
            +
                    # @param options [Hash] required for context preparation
         
     | 
| 
      
 11 
     | 
    
         
            +
                    # @return [Hash]
         
     | 
| 
      
 12 
     | 
    
         
            +
                    def call(payload_options, request, options = {})
         
     | 
| 
      
 13 
     | 
    
         
            +
                      context = Castle::Context::Prepare.call(request, payload_options.merge(options))
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
                      payload =
         
     | 
| 
      
 16 
     | 
    
         
            +
                        Castle::Utils::DeepSymbolizeKeys.call(payload_options || {}).merge(context: context)
         
     | 
| 
      
 17 
     | 
    
         
            +
                      payload[:timestamp] ||= Castle::Utils::GetTimestamp.call
         
     | 
| 
      
 18 
     | 
    
         
            +
             
     | 
| 
      
 19 
     | 
    
         
            +
                      warn '[DEPRECATION] use user_traits instead of traits key' if payload.key?(:traits)
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
                      payload
         
     | 
| 
      
 22 
     | 
    
         
            +
                    end
         
     | 
| 
      
 23 
     | 
    
         
            +
                  end
         
     | 
| 
      
 24 
     | 
    
         
            +
                end
         
     | 
| 
      
 25 
     | 
    
         
            +
              end
         
     | 
| 
      
 26 
     | 
    
         
            +
            end
         
     | 
    
        data/lib/castle/secure_mode.rb
    CHANGED
    
    | 
         @@ -4,8 +4,13 @@ require 'openssl' 
     | 
|
| 
       4 
4 
     | 
    
         | 
| 
       5 
5 
     | 
    
         
             
            module Castle
         
     | 
| 
       6 
6 
     | 
    
         
             
              module SecureMode
         
     | 
| 
       7 
     | 
    
         
            -
                 
     | 
| 
       8 
     | 
    
         
            -
                   
     | 
| 
      
 7 
     | 
    
         
            +
                class << self
         
     | 
| 
      
 8 
     | 
    
         
            +
                  # @param user_id [String]
         
     | 
| 
      
 9 
     | 
    
         
            +
                  # @param config [Castle::Configuration, Castle::SingletonConfiguration, nil]
         
     | 
| 
      
 10 
     | 
    
         
            +
                  def signature(user_id, config = nil)
         
     | 
| 
      
 11 
     | 
    
         
            +
                    config ||= Castle.config
         
     | 
| 
      
 12 
     | 
    
         
            +
                    OpenSSL::HMAC.hexdigest('sha256', config.api_secret, user_id.to_s)
         
     | 
| 
      
 13 
     | 
    
         
            +
                  end
         
     | 
| 
       9 
14 
     | 
    
         
             
                end
         
     | 
| 
       10 
15 
     | 
    
         
             
              end
         
     | 
| 
       11 
16 
     | 
    
         
             
            end
         
     | 
| 
         @@ -0,0 +1,18 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Castle
         
     | 
| 
      
 4 
     | 
    
         
            +
              # this module uses the Connection object
         
     | 
| 
      
 5 
     | 
    
         
            +
              # and provides start method for persistent connection usage
         
     | 
| 
      
 6 
     | 
    
         
            +
              # when there is a need of sending multiple requests at once
         
     | 
| 
      
 7 
     | 
    
         
            +
              module Session
         
     | 
| 
      
 8 
     | 
    
         
            +
                HTTPS_SCHEME = 'https'
         
     | 
| 
      
 9 
     | 
    
         
            +
             
     | 
| 
      
 10 
     | 
    
         
            +
                class << self
         
     | 
| 
      
 11 
     | 
    
         
            +
                  def call(&block)
         
     | 
| 
      
 12 
     | 
    
         
            +
                    return unless block_given?
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
                    Castle::Core::GetConnection.call.start(&block)
         
     | 
| 
      
 15 
     | 
    
         
            +
                  end
         
     | 
| 
      
 16 
     | 
    
         
            +
                end
         
     | 
| 
      
 17 
     | 
    
         
            +
              end
         
     | 
| 
      
 18 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -4,16 +4,12 @@ module Castle 
     | 
|
| 
       4 
4 
     | 
    
         
             
              module Hanami
         
     | 
| 
       5 
5 
     | 
    
         
             
                module Action
         
     | 
| 
       6 
6 
     | 
    
         
             
                  def castle
         
     | 
| 
       7 
     | 
    
         
            -
                    @castle ||= ::Castle::Client.from_request(request, cookies: (cookies if defined? 
     | 
| 
      
 7 
     | 
    
         
            +
                    @castle ||= ::Castle::Client.from_request(request, cookies: (cookies if defined?(cookies)))
         
     | 
| 
       8 
8 
     | 
    
         
             
                  end
         
     | 
| 
       9 
9 
     | 
    
         
             
                end
         
     | 
| 
       10 
10 
     | 
    
         | 
| 
       11 
11 
     | 
    
         
             
                def self.included(base)
         
     | 
| 
       12 
     | 
    
         
            -
                  base.configure  
     | 
| 
       13 
     | 
    
         
            -
                    controller.prepare do
         
     | 
| 
       14 
     | 
    
         
            -
                      include Castle::Hanami::Action
         
     | 
| 
       15 
     | 
    
         
            -
                    end
         
     | 
| 
       16 
     | 
    
         
            -
                  end
         
     | 
| 
      
 12 
     | 
    
         
            +
                  base.configure { controller.prepare { include Castle::Hanami::Action } }
         
     | 
| 
       17 
13 
     | 
    
         
             
                end
         
     | 
| 
       18 
14 
     | 
    
         
             
              end
         
     | 
| 
       19 
15 
     | 
    
         
             
            end
         
     | 
    
        data/lib/castle/support/rails.rb
    CHANGED
    
    
| 
         @@ -0,0 +1,22 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Castle
         
     | 
| 
      
 4 
     | 
    
         
            +
              module Utils
         
     | 
| 
      
 5 
     | 
    
         
            +
                module CleanInvalidChars
         
     | 
| 
      
 6 
     | 
    
         
            +
                  class << self
         
     | 
| 
      
 7 
     | 
    
         
            +
                    def call(arg)
         
     | 
| 
      
 8 
     | 
    
         
            +
                      case arg
         
     | 
| 
      
 9 
     | 
    
         
            +
                      when ::String
         
     | 
| 
      
 10 
     | 
    
         
            +
                        arg.encode('UTF-8', invalid: :replace, undef: :replace)
         
     | 
| 
      
 11 
     | 
    
         
            +
                      when ::Hash
         
     | 
| 
      
 12 
     | 
    
         
            +
                        arg.transform_values { |v| Castle::Utils::CleanInvalidChars.call(v) }
         
     | 
| 
      
 13 
     | 
    
         
            +
                      when ::Array
         
     | 
| 
      
 14 
     | 
    
         
            +
                        arg.map { |el| Castle::Utils::CleanInvalidChars.call(el) }
         
     | 
| 
      
 15 
     | 
    
         
            +
                      else
         
     | 
| 
      
 16 
     | 
    
         
            +
                        arg
         
     | 
| 
      
 17 
     | 
    
         
            +
                      end
         
     | 
| 
      
 18 
     | 
    
         
            +
                    end
         
     | 
| 
      
 19 
     | 
    
         
            +
                  end
         
     | 
| 
      
 20 
     | 
    
         
            +
                end
         
     | 
| 
      
 21 
     | 
    
         
            +
              end
         
     | 
| 
      
 22 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,45 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Castle
         
     | 
| 
      
 4 
     | 
    
         
            +
              module Utils
         
     | 
| 
      
 5 
     | 
    
         
            +
                module DeepSymbolizeKeys
         
     | 
| 
      
 6 
     | 
    
         
            +
                  class << self
         
     | 
| 
      
 7 
     | 
    
         
            +
                    # Returns a new hash with all keys converted to symbols, as long as
         
     | 
| 
      
 8 
     | 
    
         
            +
                    # they respond to +to_sym+. This includes the keys from the root hash
         
     | 
| 
      
 9 
     | 
    
         
            +
                    # and from all nested hashes and arrays.
         
     | 
| 
      
 10 
     | 
    
         
            +
                    #
         
     | 
| 
      
 11 
     | 
    
         
            +
                    #   hash = { 'person' => { 'name' => 'Rob', 'age' => '28' } }
         
     | 
| 
      
 12 
     | 
    
         
            +
                    #
         
     | 
| 
      
 13 
     | 
    
         
            +
                    #   Castle::Hash.deep_symbolize_keys(hash)
         
     | 
| 
      
 14 
     | 
    
         
            +
                    #   # => {:person=>{:name=>"Rob", :age=>"28"}}
         
     | 
| 
      
 15 
     | 
    
         
            +
                    def call(object, &block)
         
     | 
| 
      
 16 
     | 
    
         
            +
                      case object
         
     | 
| 
      
 17 
     | 
    
         
            +
                      when Hash
         
     | 
| 
      
 18 
     | 
    
         
            +
                        object.each_with_object({}) do |(key, value), result|
         
     | 
| 
      
 19 
     | 
    
         
            +
                          result[key.to_sym] = Castle::Utils::DeepSymbolizeKeys.call(value, &block)
         
     | 
| 
      
 20 
     | 
    
         
            +
                        end
         
     | 
| 
      
 21 
     | 
    
         
            +
                      when Array
         
     | 
| 
      
 22 
     | 
    
         
            +
                        object.map { |e| Castle::Utils::DeepSymbolizeKeys.call(e, &block) }
         
     | 
| 
      
 23 
     | 
    
         
            +
                      else
         
     | 
| 
      
 24 
     | 
    
         
            +
                        object
         
     | 
| 
      
 25 
     | 
    
         
            +
                      end
         
     | 
| 
      
 26 
     | 
    
         
            +
                    end
         
     | 
| 
      
 27 
     | 
    
         
            +
             
     | 
| 
      
 28 
     | 
    
         
            +
                    def call!(object, &block)
         
     | 
| 
      
 29 
     | 
    
         
            +
                      case object
         
     | 
| 
      
 30 
     | 
    
         
            +
                      when Hash
         
     | 
| 
      
 31 
     | 
    
         
            +
                        object.each_key do |key|
         
     | 
| 
      
 32 
     | 
    
         
            +
                          value = object.delete(key)
         
     | 
| 
      
 33 
     | 
    
         
            +
                          object[key.to_sym] = Castle::Utils::DeepSymbolizeKeys.call!(value, &block)
         
     | 
| 
      
 34 
     | 
    
         
            +
                        end
         
     | 
| 
      
 35 
     | 
    
         
            +
                        object
         
     | 
| 
      
 36 
     | 
    
         
            +
                      when Array
         
     | 
| 
      
 37 
     | 
    
         
            +
                        object.map! { |e| Castle::Utils::DeepSymbolizeKeys.call!(e, &block) }
         
     | 
| 
      
 38 
     | 
    
         
            +
                      else
         
     | 
| 
      
 39 
     | 
    
         
            +
                        object
         
     | 
| 
      
 40 
     | 
    
         
            +
                      end
         
     | 
| 
      
 41 
     | 
    
         
            +
                    end
         
     | 
| 
      
 42 
     | 
    
         
            +
                  end
         
     | 
| 
      
 43 
     | 
    
         
            +
                end
         
     | 
| 
      
 44 
     | 
    
         
            +
              end
         
     | 
| 
      
 45 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,15 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Castle
         
     | 
| 
      
 4 
     | 
    
         
            +
              module Utils
         
     | 
| 
      
 5 
     | 
    
         
            +
                # Generates a timestamp
         
     | 
| 
      
 6 
     | 
    
         
            +
                class GetTimestamp
         
     | 
| 
      
 7 
     | 
    
         
            +
                  class << self
         
     | 
| 
      
 8 
     | 
    
         
            +
                    # Returns current time as ISO8601 formatted string
         
     | 
| 
      
 9 
     | 
    
         
            +
                    def call
         
     | 
| 
      
 10 
     | 
    
         
            +
                      Time.now.utc.iso8601(3)
         
     | 
| 
      
 11 
     | 
    
         
            +
                    end
         
     | 
| 
      
 12 
     | 
    
         
            +
                  end
         
     | 
| 
      
 13 
     | 
    
         
            +
                end
         
     | 
| 
      
 14 
     | 
    
         
            +
              end
         
     | 
| 
      
 15 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -2,10 +2,10 @@ 
     | 
|
| 
       2 
2 
     | 
    
         | 
| 
       3 
3 
     | 
    
         
             
            module Castle
         
     | 
| 
       4 
4 
     | 
    
         
             
              module Utils
         
     | 
| 
       5 
     | 
    
         
            -
                class  
     | 
| 
      
 5 
     | 
    
         
            +
                class Merge
         
     | 
| 
       6 
6 
     | 
    
         
             
                  def self.call(base, extra)
         
     | 
| 
       7 
     | 
    
         
            -
                    base_s = Castle::Utils. 
     | 
| 
       8 
     | 
    
         
            -
                    extra_s = Castle::Utils. 
     | 
| 
      
 7 
     | 
    
         
            +
                    base_s = Castle::Utils::DeepSymbolizeKeys.call(base)
         
     | 
| 
      
 8 
     | 
    
         
            +
                    extra_s = Castle::Utils::DeepSymbolizeKeys.call(extra)
         
     | 
| 
       9 
9 
     | 
    
         | 
| 
       10 
10 
     | 
    
         
             
                    extra_s.each do |name, value|
         
     | 
| 
       11 
11 
     | 
    
         
             
                      if value.nil?
         
     | 
| 
         @@ -0,0 +1,22 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Castle
         
     | 
| 
      
 4 
     | 
    
         
            +
              module Utils
         
     | 
| 
      
 5 
     | 
    
         
            +
                # Code borrowed from ActiveSupport
         
     | 
| 
      
 6 
     | 
    
         
            +
                class SecureCompare
         
     | 
| 
      
 7 
     | 
    
         
            +
                  class << self
         
     | 
| 
      
 8 
     | 
    
         
            +
                    # @param str_a [String] first string to be compared
         
     | 
| 
      
 9 
     | 
    
         
            +
                    # @param str_b [String] second string to be compared
         
     | 
| 
      
 10 
     | 
    
         
            +
                    def call(str_a, str_b)
         
     | 
| 
      
 11 
     | 
    
         
            +
                      return false unless str_a.bytesize == str_b.bytesize
         
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
                      l = str_a.unpack "C#{str_a.bytesize}"
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
                      res = 0
         
     | 
| 
      
 16 
     | 
    
         
            +
                      str_b.each_byte { |byte| res |= byte ^ l.shift }
         
     | 
| 
      
 17 
     | 
    
         
            +
                      res.zero?
         
     | 
| 
      
 18 
     | 
    
         
            +
                    end
         
     | 
| 
      
 19 
     | 
    
         
            +
                  end
         
     | 
| 
      
 20 
     | 
    
         
            +
                end
         
     | 
| 
      
 21 
     | 
    
         
            +
              end
         
     | 
| 
      
 22 
     | 
    
         
            +
            end
         
     | 
    
        data/lib/castle/version.rb
    CHANGED
    
    
| 
         @@ -0,0 +1,45 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Castle
         
     | 
| 
      
 4 
     | 
    
         
            +
              module Webhooks
         
     | 
| 
      
 5 
     | 
    
         
            +
                # Verify a webhook
         
     | 
| 
      
 6 
     | 
    
         
            +
                class Verify
         
     | 
| 
      
 7 
     | 
    
         
            +
                  class << self
         
     | 
| 
      
 8 
     | 
    
         
            +
                    # Checks if webhook is valid
         
     | 
| 
      
 9 
     | 
    
         
            +
                    # @param webhook [Request]
         
     | 
| 
      
 10 
     | 
    
         
            +
                    # @param config [Castle::Configuration, Castle::SingletonConfiguration, nil]
         
     | 
| 
      
 11 
     | 
    
         
            +
                    def call(webhook, config = nil)
         
     | 
| 
      
 12 
     | 
    
         
            +
                      config ||= Castle.config
         
     | 
| 
      
 13 
     | 
    
         
            +
                      expected_signature = compute_signature(webhook, config)
         
     | 
| 
      
 14 
     | 
    
         
            +
                      signature = webhook.env['HTTP_X_CASTLE_SIGNATURE']
         
     | 
| 
      
 15 
     | 
    
         
            +
                      verify_signature(signature, expected_signature)
         
     | 
| 
      
 16 
     | 
    
         
            +
                    end
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
                    private
         
     | 
| 
      
 19 
     | 
    
         
            +
             
     | 
| 
      
 20 
     | 
    
         
            +
                    # Computes a webhook signature using provided user_id
         
     | 
| 
      
 21 
     | 
    
         
            +
                    # @param webhook [Request]
         
     | 
| 
      
 22 
     | 
    
         
            +
                    # @param config [Castle::Configuration, Castle::SingletonConfiguration]
         
     | 
| 
      
 23 
     | 
    
         
            +
                    # @return [String]
         
     | 
| 
      
 24 
     | 
    
         
            +
                    def compute_signature(webhook, config)
         
     | 
| 
      
 25 
     | 
    
         
            +
                      Base64.encode64(
         
     | 
| 
      
 26 
     | 
    
         
            +
                        OpenSSL::HMAC.digest(
         
     | 
| 
      
 27 
     | 
    
         
            +
                          OpenSSL::Digest.new('sha256'),
         
     | 
| 
      
 28 
     | 
    
         
            +
                          config.api_secret,
         
     | 
| 
      
 29 
     | 
    
         
            +
                          Castle::Core::ProcessWebhook.call(webhook, config)
         
     | 
| 
      
 30 
     | 
    
         
            +
                        )
         
     | 
| 
      
 31 
     | 
    
         
            +
                      ).strip
         
     | 
| 
      
 32 
     | 
    
         
            +
                    end
         
     | 
| 
      
 33 
     | 
    
         
            +
             
     | 
| 
      
 34 
     | 
    
         
            +
                    # Check if the signatures are matching
         
     | 
| 
      
 35 
     | 
    
         
            +
                    # @param signature [String] first signature to be compared
         
     | 
| 
      
 36 
     | 
    
         
            +
                    # @param expected_signature [String] second signature to be compared
         
     | 
| 
      
 37 
     | 
    
         
            +
                    def verify_signature(signature, expected_signature)
         
     | 
| 
      
 38 
     | 
    
         
            +
                      return if Castle::Utils::SecureCompare.call(signature, expected_signature)
         
     | 
| 
      
 39 
     | 
    
         
            +
             
     | 
| 
      
 40 
     | 
    
         
            +
                      raise Castle::WebhookVerificationError, 'Signature not matching the expected signature'
         
     | 
| 
      
 41 
     | 
    
         
            +
                    end
         
     | 
| 
      
 42 
     | 
    
         
            +
                  end
         
     | 
| 
      
 43 
     | 
    
         
            +
                end
         
     | 
| 
      
 44 
     | 
    
         
            +
              end
         
     | 
| 
      
 45 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -4,21 +4,25 @@ require 'spec_helper' 
     | 
|
| 
       4 
4 
     | 
    
         
             
            require_relative 'support/all'
         
     | 
| 
       5 
5 
     | 
    
         | 
| 
       6 
6 
     | 
    
         
             
            RSpec.describe HomeController, type: :request do
         
     | 
| 
       7 
     | 
    
         
            -
               
     | 
| 
      
 7 
     | 
    
         
            +
              context 'with index pages' do
         
     | 
| 
       8 
8 
     | 
    
         
             
                let(:request) do
         
     | 
| 
       9 
9 
     | 
    
         
             
                  {
         
     | 
| 
       10 
10 
     | 
    
         
             
                    'event' => '$login.succeeded',
         
     | 
| 
       11 
11 
     | 
    
         
             
                    'user_id' => '123',
         
     | 
| 
       12 
     | 
    
         
            -
                    'properties' => { 
     | 
| 
       13 
     | 
    
         
            -
             
     | 
| 
      
 12 
     | 
    
         
            +
                    'properties' => {
         
     | 
| 
      
 13 
     | 
    
         
            +
                      'key' => 'value'
         
     | 
| 
      
 14 
     | 
    
         
            +
                    },
         
     | 
| 
      
 15 
     | 
    
         
            +
                    'user_traits' => {
         
     | 
| 
      
 16 
     | 
    
         
            +
                      'key' => 'value'
         
     | 
| 
      
 17 
     | 
    
         
            +
                    },
         
     | 
| 
       14 
18 
     | 
    
         
             
                    'timestamp' => now.utc.iso8601(3),
         
     | 
| 
       15 
19 
     | 
    
         
             
                    'sent_at' => now.utc.iso8601(3),
         
     | 
| 
       16 
20 
     | 
    
         
             
                    'context' => {
         
     | 
| 
       17 
21 
     | 
    
         
             
                      'client_id' => '',
         
     | 
| 
       18 
22 
     | 
    
         
             
                      'active' => true,
         
     | 
| 
       19 
     | 
    
         
            -
                      'origin' => 'web',
         
     | 
| 
       20 
23 
     | 
    
         
             
                      'headers' => {
         
     | 
| 
       21 
     | 
    
         
            -
                        'Accept' => 
     | 
| 
      
 24 
     | 
    
         
            +
                        'Accept' =>
         
     | 
| 
      
 25 
     | 
    
         
            +
                          'text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5',
         
     | 
| 
       22 
26 
     | 
    
         
             
                        'Authorization' => true,
         
     | 
| 
       23 
27 
     | 
    
         
             
                        'Content-Length' => '0',
         
     | 
| 
       24 
28 
     | 
    
         
             
                        'Cookie' => true,
         
     | 
| 
         @@ -36,26 +40,50 @@ RSpec.describe HomeController, type: :request do 
     | 
|
| 
       36 
40 
     | 
    
         
             
                end
         
     | 
| 
       37 
41 
     | 
    
         
             
                let(:now) { Time.now }
         
     | 
| 
       38 
42 
     | 
    
         
             
                let(:headers) do
         
     | 
| 
       39 
     | 
    
         
            -
                  {
         
     | 
| 
       40 
     | 
    
         
            -
                    'HTTP_AUTHORIZATION' => 'Basic 123',
         
     | 
| 
       41 
     | 
    
         
            -
                    'HTTP_X_FORWARDED_FOR' => '5.5.5.5, 1.2.3.4'
         
     | 
| 
       42 
     | 
    
         
            -
                  }
         
     | 
| 
      
 43 
     | 
    
         
            +
                  { 'HTTP_AUTHORIZATION' => 'Basic 123', 'HTTP_X_FORWARDED_FOR' => '5.5.5.5, 1.2.3.4' }
         
     | 
| 
       43 
44 
     | 
    
         
             
                end
         
     | 
| 
       44 
45 
     | 
    
         | 
| 
       45 
46 
     | 
    
         
             
                before do
         
     | 
| 
       46 
47 
     | 
    
         
             
                  Timecop.freeze(now)
         
     | 
| 
       47 
48 
     | 
    
         
             
                  stub_request(:post, 'https://api.castle.io/v1/track')
         
     | 
| 
       48 
     | 
    
         
            -
                  get '/', headers: headers
         
     | 
| 
       49 
49 
     | 
    
         
             
                end
         
     | 
| 
       50 
50 
     | 
    
         | 
| 
       51 
51 
     | 
    
         
             
                after { Timecop.return }
         
     | 
| 
       52 
52 
     | 
    
         | 
| 
       53 
     | 
    
         
            -
                 
     | 
| 
       54 
     | 
    
         
            -
                   
     | 
| 
       55 
     | 
    
         
            -
             
     | 
| 
      
 53 
     | 
    
         
            +
                describe '#index1' do
         
     | 
| 
      
 54 
     | 
    
         
            +
                  before { get '/index1', headers: headers }
         
     | 
| 
      
 55 
     | 
    
         
            +
             
     | 
| 
      
 56 
     | 
    
         
            +
                  it do
         
     | 
| 
      
 57 
     | 
    
         
            +
                    assert_requested :post, 'https://api.castle.io/v1/track', times: 1 do |req|
         
     | 
| 
      
 58 
     | 
    
         
            +
                      JSON.parse(req.body) == request
         
     | 
| 
      
 59 
     | 
    
         
            +
                    end
         
     | 
| 
       56 
60 
     | 
    
         
             
                  end
         
     | 
| 
      
 61 
     | 
    
         
            +
             
     | 
| 
      
 62 
     | 
    
         
            +
                  it { expect(response).to be_successful }
         
     | 
| 
       57 
63 
     | 
    
         
             
                end
         
     | 
| 
       58 
64 
     | 
    
         | 
| 
       59 
     | 
    
         
            -
                 
     | 
| 
      
 65 
     | 
    
         
            +
                describe '#index2' do
         
     | 
| 
      
 66 
     | 
    
         
            +
                  before { get '/index2', headers: headers }
         
     | 
| 
      
 67 
     | 
    
         
            +
             
     | 
| 
      
 68 
     | 
    
         
            +
                  it do
         
     | 
| 
      
 69 
     | 
    
         
            +
                    assert_requested :post, 'https://api.castle.io/v1/track', times: 1 do |req|
         
     | 
| 
      
 70 
     | 
    
         
            +
                      JSON.parse(req.body) == request
         
     | 
| 
      
 71 
     | 
    
         
            +
                    end
         
     | 
| 
      
 72 
     | 
    
         
            +
                  end
         
     | 
| 
      
 73 
     | 
    
         
            +
             
     | 
| 
      
 74 
     | 
    
         
            +
                  it { expect(response).to be_successful }
         
     | 
| 
      
 75 
     | 
    
         
            +
                end
         
     | 
| 
      
 76 
     | 
    
         
            +
             
     | 
| 
      
 77 
     | 
    
         
            +
                describe '#index3' do
         
     | 
| 
      
 78 
     | 
    
         
            +
                  before { get '/index3', headers: headers }
         
     | 
| 
      
 79 
     | 
    
         
            +
             
     | 
| 
      
 80 
     | 
    
         
            +
                  it do
         
     | 
| 
      
 81 
     | 
    
         
            +
                    assert_requested :post, 'https://api.castle.io/v1/track', times: 1 do |req|
         
     | 
| 
      
 82 
     | 
    
         
            +
                      JSON.parse(req.body) == request
         
     | 
| 
      
 83 
     | 
    
         
            +
                    end
         
     | 
| 
      
 84 
     | 
    
         
            +
                  end
         
     | 
| 
      
 85 
     | 
    
         
            +
             
     | 
| 
      
 86 
     | 
    
         
            +
                  it { expect(response).to be_successful }
         
     | 
| 
      
 87 
     | 
    
         
            +
                end
         
     | 
| 
       60 
88 
     | 
    
         
             
              end
         
     | 
| 
       61 
89 
     | 
    
         
             
            end
         
     |