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