rs
  1# frozen_string_literal: true
  2
  3module Net
  4  module Hippie
  5    # Connection abstraction layer that supports both Ruby and Rust backends.
  6    #
  7    # The Connection class provides a unified interface for HTTP connections,
  8    # automatically selecting between the Ruby implementation and the optional
  9    # high-performance Rust backend based on availability and configuration.
 10    #
 11    # Backend selection logic:
 12    # 1. If NET_HIPPIE_RUST=true and Rust extension available -> RustConnection
 13    # 2. Otherwise -> RubyConnection (classic net/http implementation)
 14    #
 15    # @since 0.1.0
 16    # @since 2.0.0 Added Rust backend support
 17    #
 18    # == Backend Switching
 19    #
 20    #   # Enable Rust backend (requires compilation)
 21    #   ENV['NET_HIPPIE_RUST'] = 'true'
 22    #   connection = Net::Hippie::Connection.new('https', 'api.example.com', 443)
 23    #   # Uses RustConnection if available, falls back to RubyConnection
 24    #
 25    # @see RubyConnection The Ruby/net-http implementation
 26    # @see RustConnection The optional Rust implementation  
 27    class Connection
 28      # Creates a new connection with automatic backend selection.
 29      #
 30      # @param scheme [String] URL scheme ('http' or 'https')
 31      # @param host [String] Target hostname
 32      # @param port [Integer] Target port number
 33      # @param options [Hash] Connection configuration options
 34      # @option options [Integer] :read_timeout Socket read timeout in seconds
 35      # @option options [Integer] :open_timeout Socket connection timeout in seconds
 36      # @option options [Integer] :verify_mode SSL verification mode
 37      # @option options [String] :certificate Client certificate for mutual TLS
 38      # @option options [String] :key Private key for client certificate
 39      # @option options [String] :passphrase Passphrase for encrypted private key
 40      # @option options [Logger] :logger Logger for connection debugging
 41      #
 42      # @since 0.1.0
 43      # @since 2.0.0 Added automatic backend selection
 44      def initialize(scheme, host, port, options = {})
 45        @scheme = scheme
 46        @host = host
 47        @port = port
 48        @options = options
 49
 50        if RustBackend.enabled?
 51          require_relative 'rust_connection'
 52          @backend = RustConnection.new(scheme, host, port, options)
 53        else
 54          @backend = create_ruby_backend(scheme, host, port, options)
 55        end
 56      end
 57
 58      # Executes an HTTP request using the selected backend.
 59      #
 60      # @param request [Net::HTTPRequest] The HTTP request to execute
 61      # @return [Net::HTTPResponse] The HTTP response
 62      # @raise [Net::ReadTimeout, Net::OpenTimeout] When request times out
 63      # @since 0.1.0
 64      def run(request)
 65        @backend.run(request)
 66      end
 67
 68      # Builds a complete URL from a path, handling absolute and relative URLs.
 69      #
 70      # @param path [String] URL path (absolute or relative)
 71      # @return [String] Complete URL
 72      # @since 0.1.0
 73      def build_url_for(path)
 74        @backend.build_url_for(path)
 75      end
 76
 77      private
 78
 79      # Creates the Ruby backend implementation.
 80      #
 81      # @param scheme [String] URL scheme
 82      # @param host [String] Target hostname  
 83      # @param port [Integer] Target port
 84      # @param options [Hash] Connection options
 85      # @return [RubyConnection] Ruby backend instance
 86      # @since 2.0.0
 87      def create_ruby_backend(scheme, host, port, options)
 88        # This is the original Ruby implementation wrapped in an object
 89        # that matches the same interface as RustConnection
 90        RubyConnection.new(scheme, host, port, options)
 91      end
 92    end
 93
 94    # Ruby implementation of HTTP connections using net/http.
 95    #
 96    # This class provides the traditional net/http-based HTTP client functionality
 97    # that has been the backbone of Net::Hippie since its inception. It supports
 98    # all standard HTTP features including SSL/TLS, client certificates, and
 99    # comprehensive timeout configuration.
100    #
101    # @since 2.0.0 Extracted from Connection class
102    # @see Connection The main connection interface
103    class RubyConnection
104      # Creates a new Ruby HTTP connection using net/http.
105      #
106      # @param scheme [String] URL scheme ('http' or 'https')
107      # @param host [String] Target hostname
108      # @param port [Integer] Target port number
109      # @param options [Hash] Connection configuration options
110      # @option options [Integer] :read_timeout Socket read timeout (default: 10)
111      # @option options [Integer] :open_timeout Socket connection timeout (default: 10)
112      # @option options [Integer] :verify_mode SSL verification mode
113      # @option options [String] :certificate Client certificate for mutual TLS
114      # @option options [String] :key Private key for client certificate
115      # @option options [String] :passphrase Passphrase for encrypted private key
116      # @option options [Logger] :logger Logger for connection debugging
117      #
118      # @since 2.0.0
119      def initialize(scheme, host, port, options = {})
120        @scheme = scheme
121        @host = host
122        @port = port
123        
124        http = Net::HTTP.new(host, port)
125        http.read_timeout = options.fetch(:read_timeout, 10)
126        http.open_timeout = options.fetch(:open_timeout, 10)
127        http.use_ssl = scheme == 'https'
128        http.verify_mode = options.fetch(:verify_mode, Net::Hippie.verify_mode)
129        http.set_debug_output(options[:logger]) if options[:logger]
130        apply_client_tls_to(http, options)
131        @http = http
132      end
133
134      # Executes an HTTP request using net/http.
135      #
136      # @param request [Net::HTTPRequest] The HTTP request to execute
137      # @return [Net::HTTPResponse] The HTTP response
138      # @raise [Net::ReadTimeout] When read timeout expires
139      # @raise [Net::OpenTimeout] When connection timeout expires
140      # @since 2.0.0
141      def run(request)
142        @http.request(request)
143      end
144
145      # Builds a complete URL from a path.
146      #
147      # @param path [String] URL path (absolute URLs returned as-is)
148      # @return [String] Complete URL with scheme, host, and path
149      # @since 2.0.0
150      #
151      # @example Relative path
152      #   connection.build_url_for('/api/users') 
153      #   # => "https://api.example.com/api/users"
154      #
155      # @example Absolute URL
156      #   connection.build_url_for('https://other.com/path')
157      #   # => "https://other.com/path"
158      def build_url_for(path)
159        return path if path.start_with?('http')
160
161        "#{@http.use_ssl? ? 'https' : 'http'}://#{@http.address}#{path}"
162      end
163
164      private
165
166      # Applies client TLS certificate configuration to the HTTP connection.
167      #
168      # @param http [Net::HTTP] The HTTP connection object
169      # @param options [Hash] TLS configuration options
170      # @option options [String] :certificate Client certificate in PEM format
171      # @option options [String] :key Private key in PEM format
172      # @option options [String] :passphrase Optional passphrase for encrypted key
173      # @since 2.0.0
174      def apply_client_tls_to(http, options)
175        return if options[:certificate].nil? || options[:key].nil?
176
177        http.cert = OpenSSL::X509::Certificate.new(options[:certificate])
178        http.key = private_key(options[:key], options[:passphrase])
179      end
180
181      # Creates a private key object from PEM data.
182      #
183      # @param key [String] Private key in PEM format
184      # @param passphrase [String, nil] Optional passphrase for encrypted keys
185      # @param type [Class] OpenSSL key class (default: RSA)
186      # @return [OpenSSL::PKey] Private key object
187      # @since 2.0.0
188      def private_key(key, passphrase, type = OpenSSL::PKey::RSA)
189        passphrase ? type.new(key, passphrase) : type.new(key)
190      end
191    end
192  end
193end