summaryrefslogtreecommitdiff
path: root/lib/net/hippie/connection.rb
blob: 6dfae870e828b91eaf23dde4aa094e7053b2cebc (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
# frozen_string_literal: true

module Net
  module Hippie
    # Connection abstraction layer that supports both Ruby and Rust backends.
    #
    # The Connection class provides a unified interface for HTTP connections,
    # automatically selecting between the Ruby implementation and the optional
    # high-performance Rust backend based on availability and configuration.
    #
    # Backend selection logic:
    # 1. If NET_HIPPIE_RUST=true and Rust extension available -> RustConnection
    # 2. Otherwise -> RubyConnection (classic net/http implementation)
    #
    # @since 0.1.0
    # @since 2.0.0 Added Rust backend support
    #
    # == Backend Switching
    #
    #   # Enable Rust backend (requires compilation)
    #   ENV['NET_HIPPIE_RUST'] = 'true'
    #   connection = Net::Hippie::Connection.new('https', 'api.example.com', 443)
    #   # Uses RustConnection if available, falls back to RubyConnection
    #
    # @see RubyConnection The Ruby/net-http implementation
    # @see RustConnection The optional Rust implementation  
    class Connection
      # Creates a new connection with automatic backend selection.
      #
      # @param scheme [String] URL scheme ('http' or 'https')
      # @param host [String] Target hostname
      # @param port [Integer] Target port number
      # @param options [Hash] Connection configuration options
      # @option options [Integer] :read_timeout Socket read timeout in seconds
      # @option options [Integer] :open_timeout Socket connection timeout in seconds
      # @option options [Integer] :verify_mode SSL verification mode
      # @option options [String] :certificate Client certificate for mutual TLS
      # @option options [String] :key Private key for client certificate
      # @option options [String] :passphrase Passphrase for encrypted private key
      # @option options [Logger] :logger Logger for connection debugging
      #
      # @since 0.1.0
      # @since 2.0.0 Added automatic backend selection
      def initialize(scheme, host, port, options = {})
        @scheme = scheme
        @host = host
        @port = port
        @options = options

        if RustBackend.enabled?
          require_relative 'rust_connection'
          @backend = RustConnection.new(scheme, host, port, options)
        else
          @backend = create_ruby_backend(scheme, host, port, options)
        end
      end

      # Executes an HTTP request using the selected backend.
      #
      # @param request [Net::HTTPRequest] The HTTP request to execute
      # @return [Net::HTTPResponse] The HTTP response
      # @raise [Net::ReadTimeout, Net::OpenTimeout] When request times out
      # @since 0.1.0
      def run(request)
        @backend.run(request)
      end

      # Builds a complete URL from a path, handling absolute and relative URLs.
      #
      # @param path [String] URL path (absolute or relative)
      # @return [String] Complete URL
      # @since 0.1.0
      def build_url_for(path)
        @backend.build_url_for(path)
      end

      private

      # Creates the Ruby backend implementation.
      #
      # @param scheme [String] URL scheme
      # @param host [String] Target hostname  
      # @param port [Integer] Target port
      # @param options [Hash] Connection options
      # @return [RubyConnection] Ruby backend instance
      # @since 2.0.0
      def create_ruby_backend(scheme, host, port, options)
        # This is the original Ruby implementation wrapped in an object
        # that matches the same interface as RustConnection
        RubyConnection.new(scheme, host, port, options)
      end
    end

    # Ruby implementation of HTTP connections using net/http.
    #
    # This class provides the traditional net/http-based HTTP client functionality
    # that has been the backbone of Net::Hippie since its inception. It supports
    # all standard HTTP features including SSL/TLS, client certificates, and
    # comprehensive timeout configuration.
    #
    # @since 2.0.0 Extracted from Connection class
    # @see Connection The main connection interface
    class RubyConnection
      # Creates a new Ruby HTTP connection using net/http.
      #
      # @param scheme [String] URL scheme ('http' or 'https')
      # @param host [String] Target hostname
      # @param port [Integer] Target port number
      # @param options [Hash] Connection configuration options
      # @option options [Integer] :read_timeout Socket read timeout (default: 10)
      # @option options [Integer] :open_timeout Socket connection timeout (default: 10)
      # @option options [Integer] :verify_mode SSL verification mode
      # @option options [String] :certificate Client certificate for mutual TLS
      # @option options [String] :key Private key for client certificate
      # @option options [String] :passphrase Passphrase for encrypted private key
      # @option options [Logger] :logger Logger for connection debugging
      #
      # @since 2.0.0
      def initialize(scheme, host, port, options = {})
        @scheme = scheme
        @host = host
        @port = port
        
        http = Net::HTTP.new(host, port)
        http.read_timeout = options.fetch(:read_timeout, 10)
        http.open_timeout = options.fetch(:open_timeout, 10)
        http.use_ssl = scheme == 'https'
        http.verify_mode = options.fetch(:verify_mode, Net::Hippie.verify_mode)
        http.set_debug_output(options[:logger]) if options[:logger]
        apply_client_tls_to(http, options)
        @http = http
      end

      # Executes an HTTP request using net/http.
      #
      # @param request [Net::HTTPRequest] The HTTP request to execute
      # @return [Net::HTTPResponse] The HTTP response
      # @raise [Net::ReadTimeout] When read timeout expires
      # @raise [Net::OpenTimeout] When connection timeout expires
      # @since 2.0.0
      def run(request)
        @http.request(request)
      end

      # Builds a complete URL from a path.
      #
      # @param path [String] URL path (absolute URLs returned as-is)
      # @return [String] Complete URL with scheme, host, and path
      # @since 2.0.0
      #
      # @example Relative path
      #   connection.build_url_for('/api/users') 
      #   # => "https://api.example.com/api/users"
      #
      # @example Absolute URL
      #   connection.build_url_for('https://other.com/path')
      #   # => "https://other.com/path"
      def build_url_for(path)
        return path if path.start_with?('http')

        "#{@http.use_ssl? ? 'https' : 'http'}://#{@http.address}#{path}"
      end

      private

      # Applies client TLS certificate configuration to the HTTP connection.
      #
      # @param http [Net::HTTP] The HTTP connection object
      # @param options [Hash] TLS configuration options
      # @option options [String] :certificate Client certificate in PEM format
      # @option options [String] :key Private key in PEM format
      # @option options [String] :passphrase Optional passphrase for encrypted key
      # @since 2.0.0
      def apply_client_tls_to(http, options)
        return if options[:certificate].nil? || options[:key].nil?

        http.cert = OpenSSL::X509::Certificate.new(options[:certificate])
        http.key = private_key(options[:key], options[:passphrase])
      end

      # Creates a private key object from PEM data.
      #
      # @param key [String] Private key in PEM format
      # @param passphrase [String, nil] Optional passphrase for encrypted keys
      # @param type [Class] OpenSSL key class (default: RSA)
      # @return [OpenSSL::PKey] Private key object
      # @since 2.0.0
      def private_key(key, passphrase, type = OpenSSL::PKey::RSA)
        passphrase ? type.new(key, passphrase) : type.new(key)
      end
    end
  end
end