Class: Collavre::LinkPreviewFetcher::PinnedHttpClient

Inherits:
Object
  • Object
show all
Defined in:
app/services/collavre/link_preview_fetcher.rb

Overview

Performs a single (non-redirect-following) GET against a pre-resolved IP while keeping the original hostname for the Host header and TLS SNI/cert verification. Net::HTTP#ipaddr= pins the socket to ip, so no second DNS lookup happens between validation and connect. Enforces the caller's open/ read timeouts and byte cap.

Instance Method Summary collapse

Instance Method Details

#get(uri, ip:, headers:, open_timeout:, read_timeout:, max_bytes:) ⇒ Object



276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
# File 'app/services/collavre/link_preview_fetcher.rb', line 276

def get(uri, ip:, headers:, open_timeout:, read_timeout:, max_bytes:)
  # Pass an explicit nil proxy: Net::HTTP.new defaults p_addr to :ENV, so a
  # set http_proxy/HTTP_PROXY would route through the proxy, which resolves
  # the hostname itself and defeats `http.ipaddr = ip` pinning — reopening
  # the DNS-rebinding/SSRF gap. nil forces a direct, pinned connection.
  http = Net::HTTP.new(uri.hostname, uri.port, nil)
  http.use_ssl = uri.scheme == "https"
  http.ipaddr = ip
  http.open_timeout = open_timeout
  http.read_timeout = read_timeout

  request = Net::HTTP::Get.new(uri, headers)
  response_struct = nil

  http.start do |conn|
    conn.request(request) do |response|
      body = read_capped_body(response, max_bytes)
      response_struct = Response.new(
        code: response.code.to_i,
        content_type: response.content_type,
        body: body,
        location: response["location"]
      )
    end
  end

  response_struct
ensure
  http&.finish if http&.started?
end