The way Ruby’s net/http library exposes customization of SNI (Sever Name Indication) is quite counterintuitive at the first sight. It is also pretty much not documented. I discovered it through this closed feature request and the relevant source code.
In this article I’m going to share two small code snippets on how to do that:
(1) with net/http directly (2) with Faraday gem. Let’s assume our domain is example.com
and we want to force HTTPS requests to it to forced-endpoint.com
server.
Directly with net/http
require 'net/http'
uri = URI('https://example.com')
Net::HTTP.start(uri.host, uri.port, use_ssl: true, ipaddr: 'forced-endpoint.com') do |http|
request = Net::HTTP::Get.new(uri)
response = http.request(request)
p response.code
p response.each_header.to_h.to_s
end
Using Faraday gem
require 'faraday'
conn = Faraday.new('https://example.com') do |f|
f.adapter :net_http do |http|
http.ipaddr = 'forced-endpoint.com'
end
end
response = conn.get('/')
p response.status
Both code snippet will result in TCP connection to forced-endpoint.com
, where TLS handshake is done
using example.com
as SNI while HTTP Host header is set to example.com
.
In both cases the key is setting ipaddr
attribute. One can also set it to a specific IP address.
That is where the counterintuitiveness of this (at the first sight) configuration comes from:
you have to mention address for TCP/socket layer in order to force net/http to use provided host as SNI.
What would seemingly be more intuitive is to explicitly set SNI, for example that is how Go does it
using TLSClientConfig.ServerName
attribute. However if you think about it a bit more, Ruby’s approach is arguably better!
The author of the related patch explains it in this comment as to why
he chose to expose overriding of ipaddr
for underlying TCP connection instead of the SNI.
The tldr; is that when you override SNI, most of the times you also want to override HTTP Host
header
so that they are the same - that is also how curl
with its --resolve
argument works.
comments powered by Disqus