Class: Radfish::HttpClient
- Inherits:
-
Object
- Object
- Radfish::HttpClient
- Includes:
- Debuggable
- Defined in:
- lib/radfish/http_client.rb
Overview
Shared HTTP client for all BMC connections
Constant Summary collapse
- REDIRECT_STATUSES =
Redirects we follow, and the methods we follow them for. GET/HEAD only: 307/308 preserve the method, but 301/302/303 do not, so re-sending a POST body after one would be wrong. Callers doing anything else get the 3xx back.
[301, 302, 303, 307, 308].freeze
- REDIRECT_METHODS =
[:get, :head].freeze
- IDEMPOTENT_METHODS =
Methods we retry by default. A repeated GET or DELETE is harmless, but a repeated POST is not: on Redfish it can mean a second session, a second reset, a second job. Callers that know a POST is safe to repeat can widen this per client with retry_methods:.
[:get, :head, :put, :delete].freeze
- RETRY_STATUSES =
[408, 429, 500, 502, 503, 504].freeze
- LOG_FILTERS =
Secrets stripped from the debug log. Redfish session payloads use "Password" where other calls use "password", and the session token comes back as a header, so match either case and both header shapes.
[ # Faraday logs a header as `Authorization: "Basic dXNlcjpwdw=="` and a # headers hash as `"Authorization"=>"Basic ..."`, so the quote sits # between the colon and the scheme. Match both shapes, and any scheme, # keeping the scheme itself visible because it is useful and not secret. [/(Authorization"?\s*(?:=>|:)\s*"?)((?:Basic|Bearer|Digest)\s+)?[^"\n]+/i, '\1\2[FILTERED]'], [/(Password"=>"?)([^,"]+)/i, '\1[FILTERED]'], [/("password"\s*:\s*")([^"]+)/i, '\1[FILTERED]'], [/((?:X-Auth-Token)"?\s*(?:=>|:)\s*"?)([^",\n]+)/i, '\1[FILTERED]'] ].freeze
Instance Attribute Summary collapse
-
#host ⇒ Object
readonly
Returns the value of attribute host.
-
#log_device ⇒ Object
Returns the value of attribute log_device.
-
#max_redirects ⇒ Object
Returns the value of attribute max_redirects.
-
#password ⇒ Object
Returns the value of attribute password.
-
#port ⇒ Object
readonly
Returns the value of attribute port.
-
#retry_count ⇒ Object
Returns the value of attribute retry_count.
-
#retry_delay ⇒ Object
Returns the value of attribute retry_delay.
-
#retry_methods ⇒ Object
Returns the value of attribute retry_methods.
-
#use_ssl ⇒ Object
readonly
Returns the value of attribute use_ssl.
-
#username ⇒ Object
Returns the value of attribute username.
-
#verbosity ⇒ Object
Returns the value of attribute verbosity.
-
#verify_ssl ⇒ Object
readonly
Returns the value of attribute verify_ssl.
Class Method Summary collapse
-
.scrub(text) ⇒ Object
The Faraday logger applies LOG_FILTERS, but our own debug lines bypass it, so anything we print that can carry a header or a body goes through here.
Instance Method Summary collapse
- #base_url ⇒ Object
- #delete(path, headers: {}, **options) ⇒ Object
- #get(path, headers: {}, **options) ⇒ Object
-
#initialize(host:, port: 443, use_ssl: true, verify_ssl: false, username: nil, password: nil, verbosity: 0, retry_count: 3, retry_delay: 1, retry_methods: IDEMPOTENT_METHODS, max_redirects: 3, log_device: STDOUT, **options) ⇒ HttpClient
constructor
A new instance of HttpClient.
- #patch(path, body: nil, headers: {}, **options) ⇒ Object
- #post(path, body: nil, headers: {}, **options) ⇒ Object
- #put(path, body: nil, headers: {}, **options) ⇒ Object
- #redirect?(method, response) ⇒ Boolean
-
#request(method, path, body: nil, headers: {}, auth: true, timeout: nil, max_redirects: nil, **options) ⇒ Object
max_redirects overrides the client default for one call; pass 0 to get the 3xx response itself rather than what it points at.
-
#safe_redirect_path(location) ⇒ Object
The path (with query) to request for
location, or nil when we must not follow it.
Methods included from Debuggable
Constructor Details
#initialize(host:, port: 443, use_ssl: true, verify_ssl: false, username: nil, password: nil, verbosity: 0, retry_count: 3, retry_delay: 1, retry_methods: IDEMPOTENT_METHODS, max_redirects: 3, log_device: STDOUT, **options) ⇒ HttpClient
Returns a new instance of HttpClient.
45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
# File 'lib/radfish/http_client.rb', line 45 def initialize(host:, port: 443, use_ssl: true, verify_ssl: false, username: nil, password: nil, verbosity: 0, retry_count: 3, retry_delay: 1, retry_methods: IDEMPOTENT_METHODS, max_redirects: 3, log_device: STDOUT, **) @host = host @port = port @use_ssl = use_ssl @verify_ssl = verify_ssl @username = username @password = password @verbosity = verbosity @retry_count = retry_count @retry_delay = retry_delay @retry_methods = retry_methods @max_redirects = max_redirects @log_device = log_device @options = end |
Instance Attribute Details
#host ⇒ Object (readonly)
Returns the value of attribute host.
41 42 43 |
# File 'lib/radfish/http_client.rb', line 41 def host @host end |
#log_device ⇒ Object
Returns the value of attribute log_device.
42 43 44 |
# File 'lib/radfish/http_client.rb', line 42 def log_device @log_device end |
#max_redirects ⇒ Object
Returns the value of attribute max_redirects.
42 43 44 |
# File 'lib/radfish/http_client.rb', line 42 def max_redirects @max_redirects end |
#password ⇒ Object
Returns the value of attribute password.
42 43 44 |
# File 'lib/radfish/http_client.rb', line 42 def password @password end |
#port ⇒ Object (readonly)
Returns the value of attribute port.
41 42 43 |
# File 'lib/radfish/http_client.rb', line 41 def port @port end |
#retry_count ⇒ Object
Returns the value of attribute retry_count.
42 43 44 |
# File 'lib/radfish/http_client.rb', line 42 def retry_count @retry_count end |
#retry_delay ⇒ Object
Returns the value of attribute retry_delay.
42 43 44 |
# File 'lib/radfish/http_client.rb', line 42 def retry_delay @retry_delay end |
#retry_methods ⇒ Object
Returns the value of attribute retry_methods.
42 43 44 |
# File 'lib/radfish/http_client.rb', line 42 def retry_methods @retry_methods end |
#use_ssl ⇒ Object (readonly)
Returns the value of attribute use_ssl.
41 42 43 |
# File 'lib/radfish/http_client.rb', line 41 def use_ssl @use_ssl end |
#username ⇒ Object
Returns the value of attribute username.
42 43 44 |
# File 'lib/radfish/http_client.rb', line 42 def username @username end |
#verbosity ⇒ Object
Returns the value of attribute verbosity.
42 43 44 |
# File 'lib/radfish/http_client.rb', line 42 def verbosity @verbosity end |
#verify_ssl ⇒ Object (readonly)
Returns the value of attribute verify_ssl.
41 42 43 |
# File 'lib/radfish/http_client.rb', line 41 def verify_ssl @verify_ssl end |
Class Method Details
.scrub(text) ⇒ Object
The Faraday logger applies LOG_FILTERS, but our own debug lines bypass it, so anything we print that can carry a header or a body goes through here.
67 68 69 |
# File 'lib/radfish/http_client.rb', line 67 def self.scrub(text) LOG_FILTERS.reduce(text.to_s) { |t, (pattern, replacement)| t.gsub(pattern, replacement) } end |
Instance Method Details
#base_url ⇒ Object
71 72 73 74 |
# File 'lib/radfish/http_client.rb', line 71 def base_url protocol = use_ssl ? 'https' : 'http' "#{protocol}://#{host}:#{port}" end |
#delete(path, headers: {}, **options) ⇒ Object
92 93 94 |
# File 'lib/radfish/http_client.rb', line 92 def delete(path, headers: {}, **) request(:delete, path, headers: headers, **) end |
#get(path, headers: {}, **options) ⇒ Object
76 77 78 |
# File 'lib/radfish/http_client.rb', line 76 def get(path, headers: {}, **) request(:get, path, headers: headers, **) end |
#patch(path, body: nil, headers: {}, **options) ⇒ Object
88 89 90 |
# File 'lib/radfish/http_client.rb', line 88 def patch(path, body: nil, headers: {}, **) request(:patch, path, body: body, headers: headers, **) end |
#post(path, body: nil, headers: {}, **options) ⇒ Object
80 81 82 |
# File 'lib/radfish/http_client.rb', line 80 def post(path, body: nil, headers: {}, **) request(:post, path, body: body, headers: headers, **) end |
#put(path, body: nil, headers: {}, **options) ⇒ Object
84 85 86 |
# File 'lib/radfish/http_client.rb', line 84 def put(path, body: nil, headers: {}, **) request(:put, path, body: body, headers: headers, **) end |
#redirect?(method, response) ⇒ Boolean
189 190 191 |
# File 'lib/radfish/http_client.rb', line 189 def redirect?(method, response) REDIRECT_METHODS.include?(method) && REDIRECT_STATUSES.include?(response.status) end |
#request(method, path, body: nil, headers: {}, auth: true, timeout: nil, max_redirects: nil, **options) ⇒ Object
max_redirects overrides the client default for one call; pass 0 to get the 3xx response itself rather than what it points at.
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 |
# File 'lib/radfish/http_client.rb', line 98 def request(method, path, body: nil, headers: {}, auth: true, timeout: nil, max_redirects: nil, **) debug "Starting HTTP #{method.upcase} request to #{path}", 2, :yellow redirects_left = max_redirects.nil? ? @max_redirects.to_i : max_redirects.to_i # Add host header if specified (needed for SSH tunnels to iDRAC) if @options[:host_header] headers = headers.merge('Host' => @options[:host_header]) debug "Added Host header: #{@options[:host_header]}", 2, :cyan end debug "Creating connection...", 2, :yellow conn = connection(auth: auth) debug "Connection created, sending #{method} request...", 2, :yellow response = conn.send(method) do |req| debug "Setting request URL: #{path}", 3, :cyan req.url path debug "Merging headers: #{self.class.scrub(headers)}", 3, :cyan req.headers.merge!(headers) req.body = body if body # Override timeout if specified if timeout debug "Setting timeout: #{timeout}s", 3, :cyan req..timeout = timeout req..open_timeout = [timeout / 2, 5].min end # Apply any additional options .each do |key, value| req.[key] = value if req..respond_to?(:"#{key}=") end debug "Request configured, about to send...", 2, :yellow end debug "Request completed with status: #{response.status}", 2, :green if redirects_left > 0 && redirect?(method, response) target = safe_redirect_path(response['location']) if target.nil? debug "Not following redirect to #{response['location'].inspect} - it leaves #{base_url}", 1, :yellow else debug "Following redirect to #{target}, #{redirects_left - 1} left", 2, :yellow return request(method, target, body: body, headers: headers, auth: auth, timeout: timeout, max_redirects: redirects_left - 1, **) end end response rescue Faraday::ConnectionFailed => e debug "Connection failed: #{e.}", 1, :red raise Radfish::ConnectionError, "Failed to connect to #{host}: #{e.}" rescue Faraday::TimeoutError => e debug "Request timed out: #{e.}", 1, :red raise Radfish::TimeoutError, "Request to #{host} timed out: #{e.}" rescue Faraday::SSLError => e debug "SSL error: #{e.}", 1, :red # Test if this might be HTTP instead of HTTPS begin debug "Testing if endpoint supports HTTP instead of HTTPS...", 2, :yellow test_http_socket = TCPSocket.new(host, port) test_http_socket.write("GET /redfish/v1 HTTP/1.1\r\nHost: #{@host_header || host}\r\nConnection: close\r\n\r\n") response = test_http_socket.read(1024) # Read first 1KB test_http_socket.close if response && response.include?("HTTP/") && !response.include?("301") && !response.include?("302") debug "HTTP response received - this endpoint might support HTTP instead of HTTPS", 1, :yellow debug "HTTP response preview: #{response[0..200]}", 2 else debug "No valid HTTP response - endpoint requires HTTPS but SSL failed", 2, :red end rescue => http_test_error debug "HTTP test failed: #{http_test_error.}", 2 end raise Radfish::ConnectionError, "SSL error connecting to #{host}: #{e.}" rescue Faraday::Error => e # HttpClient is the seam: callers speak Radfish errors, not Faraday ones. debug "HTTP request failed: #{e.class} - #{e.}", 1, :red raise Radfish::Error, "Request to #{host} failed: #{e.}" rescue => e debug "HTTP request failed: #{e.class} - #{e.}", 1, :red debug "Exception backtrace: #{e.backtrace.first(5).join("\n")}", 1, :red # Don't re-raise as generic error - let the original exception propagate raise e end |
#safe_redirect_path(location) ⇒ Object
The path (with query) to request for location, or nil when we must not
follow it. Every request we make carries credentials -- Basic auth here, an
X-Auth-Token on the adapters' authenticated_request -- and Faraday honours an
absolute URL by replacing the host, so a BMC answering with
"Location: https://elsewhere/" would be handed those credentials. Accept a
relative path, or an absolute URL for this same endpoint, and return only the
path so the request stays on this connection.
Note: with an SSH tunnel (host_header set) an absolute redirect to the BMC's own name is refused, since that name is not the host we connect to. Relative redirects are unaffected.
204 205 206 207 208 209 210 211 212 213 214 215 |
# File 'lib/radfish/http_client.rb', line 204 def safe_redirect_path(location) location = location.to_s return nil if location.empty? uri = URI.parse(location) return nil if uri.path.to_s.empty? return nil unless uri.host.nil? || same_endpoint?(uri) uri.query ? "#{uri.path}?#{uri.query}" : uri.path rescue URI::InvalidURIError nil end |