mirror of
				https://github.com/stripe/stripe-ruby.git
				synced 2025-11-03 00:03:35 -05:00 
			
		
		
		
	
		
			
				
	
	
		
			199 lines
		
	
	
		
			7.0 KiB
		
	
	
	
		
			Ruby
		
	
	
	
	
	
			
		
		
	
	
			199 lines
		
	
	
		
			7.0 KiB
		
	
	
	
		
			Ruby
		
	
	
	
	
	
# frozen_string_literal: true
 | 
						|
 | 
						|
module Stripe
 | 
						|
  # Manages connections across multiple hosts which is useful because the
 | 
						|
  # library may connect to multiple hosts during a typical session (main API,
 | 
						|
  # Connect, Uploads). Ruby doesn't provide an easy way to make this happen
 | 
						|
  # easily, so this class is designed to track what we're connected to and
 | 
						|
  # manage the lifecycle of those connections.
 | 
						|
  #
 | 
						|
  # Note that this class in itself is *not* thread safe. We expect it to be
 | 
						|
  # instantiated once per thread.
 | 
						|
  class ConnectionManager
 | 
						|
    # Timestamp (in seconds procured from the system's monotonic clock)
 | 
						|
    # indicating when the connection manager last made a request. This is used
 | 
						|
    # by `StripeClient` to determine whether a connection manager should be
 | 
						|
    # garbage collected or not.
 | 
						|
    attr_reader :last_used
 | 
						|
    attr_reader :config
 | 
						|
 | 
						|
    def initialize(config = Stripe.config)
 | 
						|
      @config = config
 | 
						|
      @active_connections = {}
 | 
						|
      @last_used = Util.monotonic_time
 | 
						|
 | 
						|
      # A connection manager may be accessed across threads as one thread makes
 | 
						|
      # requests on it while another is trying to clear it (either because it's
 | 
						|
      # trying to garbage collect the manager or trying to clear it because a
 | 
						|
      # configuration setting has changed). That's probably thread-safe already
 | 
						|
      # because of Ruby's GIL, but just in case the library's running on JRuby
 | 
						|
      # or the like, use a mutex to synchronize access in this connection
 | 
						|
      # manager.
 | 
						|
      @mutex = Mutex.new
 | 
						|
    end
 | 
						|
 | 
						|
    # Finishes any active connections by closing their TCP connection and
 | 
						|
    # clears them from internal tracking.
 | 
						|
    def clear
 | 
						|
      @mutex.synchronize do
 | 
						|
        @active_connections.each do |_, connection|
 | 
						|
          connection.finish
 | 
						|
        end
 | 
						|
        @active_connections = {}
 | 
						|
      end
 | 
						|
    end
 | 
						|
 | 
						|
    # Gets a connection for a given URI. This is for internal use only as it's
 | 
						|
    # subject to change (we've moved between HTTP client schemes in the past
 | 
						|
    # and may do it again).
 | 
						|
    #
 | 
						|
    # `uri` is expected to be a string.
 | 
						|
    def connection_for(uri)
 | 
						|
      @mutex.synchronize do
 | 
						|
        u = URI.parse(uri)
 | 
						|
        connection = @active_connections[[u.host, u.port]]
 | 
						|
 | 
						|
        if connection.nil?
 | 
						|
          connection = create_connection(u)
 | 
						|
          connection.start
 | 
						|
 | 
						|
          @active_connections[[u.host, u.port]] = connection
 | 
						|
        end
 | 
						|
 | 
						|
        connection
 | 
						|
      end
 | 
						|
    end
 | 
						|
 | 
						|
    # Executes an HTTP request to the given URI with the given method. Also
 | 
						|
    # allows a request body, headers, and query string to be specified.
 | 
						|
    def execute_request(method, uri, body: nil, headers: nil, query: nil,
 | 
						|
                        &block)
 | 
						|
      # Perform some basic argument validation because it's easy to get
 | 
						|
      # confused between strings and hashes for things like body and query
 | 
						|
      # parameters.
 | 
						|
      raise ArgumentError, "method should be a symbol" \
 | 
						|
        unless method.is_a?(Symbol)
 | 
						|
      raise ArgumentError, "uri should be a string" \
 | 
						|
        unless uri.is_a?(String)
 | 
						|
      raise ArgumentError, "body should be a string" \
 | 
						|
        if body && !body.is_a?(String)
 | 
						|
      raise ArgumentError, "headers should be a hash" \
 | 
						|
        if headers && !headers.is_a?(Hash)
 | 
						|
      raise ArgumentError, "query should be a string" \
 | 
						|
        if query && !query.is_a?(String)
 | 
						|
 | 
						|
      @last_used = Util.monotonic_time
 | 
						|
 | 
						|
      connection = connection_for(uri)
 | 
						|
 | 
						|
      u = URI.parse(uri)
 | 
						|
      path = if query
 | 
						|
               u.path + "?" + query
 | 
						|
             else
 | 
						|
               u.path
 | 
						|
             end
 | 
						|
 | 
						|
      method_name = method.to_s.upcase
 | 
						|
      has_response_body = method_name != "HEAD"
 | 
						|
      request = Net::HTTPGenericRequest.new(
 | 
						|
        method_name,
 | 
						|
        (body ? true : false),
 | 
						|
        has_response_body,
 | 
						|
        path,
 | 
						|
        headers
 | 
						|
      )
 | 
						|
 | 
						|
      Util.log_debug("ConnectionManager starting request",
 | 
						|
                     method_name: method_name,
 | 
						|
                     path: path,
 | 
						|
                     process_id: Process.pid,
 | 
						|
                     thread_object_id: Thread.current.object_id,
 | 
						|
                     connection_manager_object_id: object_id,
 | 
						|
                     connection_object_id: connection.object_id,
 | 
						|
                     log_timestamp: Util.monotonic_time)
 | 
						|
 | 
						|
      resp = @mutex.synchronize do
 | 
						|
        # The block parameter is special here. If a block is provided, the block
 | 
						|
        # is invoked with the Net::HTTPResponse. However, the body will not have
 | 
						|
        # been read yet in the block, and can be streamed by calling
 | 
						|
        # HTTPResponse#read_body.
 | 
						|
        connection.request(request, body, &block)
 | 
						|
      end
 | 
						|
 | 
						|
      Util.log_debug("ConnectionManager request complete",
 | 
						|
                     method_name: method_name,
 | 
						|
                     path: path,
 | 
						|
                     process_id: Process.pid,
 | 
						|
                     thread_object_id: Thread.current.object_id,
 | 
						|
                     connection_manager_object_id: object_id,
 | 
						|
                     connection_object_id: connection.object_id,
 | 
						|
                     response_object_id: resp.object_id,
 | 
						|
                     log_timestamp: Util.monotonic_time)
 | 
						|
 | 
						|
      resp
 | 
						|
    end
 | 
						|
 | 
						|
    #
 | 
						|
    # private
 | 
						|
    #
 | 
						|
 | 
						|
    # `uri` should be a parsed `URI` object.
 | 
						|
    private def create_connection(uri)
 | 
						|
      # These all come back as `nil` if no proxy is configured.
 | 
						|
      proxy_host, proxy_port, proxy_user, proxy_pass = proxy_parts
 | 
						|
 | 
						|
      connection = Net::HTTP.new(uri.host, uri.port,
 | 
						|
                                 proxy_host, proxy_port,
 | 
						|
                                 proxy_user, proxy_pass)
 | 
						|
 | 
						|
      # Time in seconds within which Net::HTTP will try to reuse an already
 | 
						|
      # open connection when issuing a new operation. Outside this window, Ruby
 | 
						|
      # will transparently close and re-open the connection without trying to
 | 
						|
      # reuse it.
 | 
						|
      #
 | 
						|
      # Ruby's default of 2 seconds is almost certainly too short. Here I've
 | 
						|
      # reused Go's default for `DefaultTransport`.
 | 
						|
      connection.keep_alive_timeout = 30
 | 
						|
 | 
						|
      connection.open_timeout = config.open_timeout
 | 
						|
      connection.read_timeout = config.read_timeout
 | 
						|
      connection.write_timeout = config.write_timeout if connection.respond_to?(:write_timeout=)
 | 
						|
 | 
						|
      connection.use_ssl = uri.scheme == "https"
 | 
						|
 | 
						|
      if config.verify_ssl_certs
 | 
						|
        connection.verify_mode = OpenSSL::SSL::VERIFY_PEER
 | 
						|
        connection.cert_store = config.ca_store
 | 
						|
      else
 | 
						|
        connection.verify_mode = OpenSSL::SSL::VERIFY_NONE
 | 
						|
        warn_ssl_verify_none
 | 
						|
      end
 | 
						|
 | 
						|
      connection
 | 
						|
    end
 | 
						|
 | 
						|
    # `Net::HTTP` somewhat awkwardly requires each component of a proxy URI
 | 
						|
    # (host, port, etc.) rather than the URI itself. This method simply parses
 | 
						|
    # out those pieces to make passing them into a new connection a little less
 | 
						|
    # ugly.
 | 
						|
    private def proxy_parts
 | 
						|
      if config.proxy.nil?
 | 
						|
        [nil, nil, nil, nil]
 | 
						|
      else
 | 
						|
        u = URI.parse(config.proxy)
 | 
						|
        [u.host, u.port, u.user, u.password]
 | 
						|
      end
 | 
						|
    end
 | 
						|
 | 
						|
    private def warn_ssl_verify_none
 | 
						|
      return if @verify_ssl_warned
 | 
						|
 | 
						|
      @verify_ssl_warned = true
 | 
						|
      warn("WARNING: Running without SSL cert verification. " \
 | 
						|
           "You should never do this in production. " \
 | 
						|
           "Execute `Stripe.verify_ssl_certs = true` to enable " \
 | 
						|
           "verification.")
 | 
						|
    end
 | 
						|
  end
 | 
						|
end
 |