mirror of
https://github.com/discourse/discourse.git
synced 2026-08-06 09:05:23 +08:00
Ruby 3.4 shipped support for happy eyeballs in `Socket.tcp` and `TCPSocket`. However, our `FinalDestination::HTTP` wrapper was performing a DNS lookup and passing IP addresses one at a time when opening the socket. That meant that we didn't benefit from the new Ruby feature in most Discourse features. This commit factors the strategy. Now, `FinalDestination::HTTP` encodes the DNS result and passes it to the underlying implementation as a fake hostname string. A patch to `Addrinfo` detects this fake hostname and returns the given IPs instead of performing its own lookup. For this Addrinfo patch to work, we also had to patch `TCPSocket` so that it uses the ruby-based `Socket.tcp` rather than its native C socket-opening code. The result is that we now get the benefit of the native Ruby 'Happy Eyeballs' support for concurrent ipv4 and ipv6 connections. All this patching of low-level ruby classes is not ideal, but there is no native way to control name resolution in `Net::HTTP` or its dependencies.
67 lines
1.8 KiB
Ruby
Vendored
67 lines
1.8 KiB
Ruby
Vendored
# frozen_string_literal: true
|
|
|
|
klass = defined?(WebPush) ? WebPush : Webpush
|
|
|
|
RSpec.describe klass do
|
|
before do
|
|
FinalDestination::SSRFDetector.allow_ip_lookups_in_test!
|
|
WebMock.enable!(except: [:final_destination])
|
|
end
|
|
|
|
after do
|
|
WebMock.enable!
|
|
FinalDestination::SSRFDetector.disallow_ip_lookups_in_test!
|
|
end
|
|
|
|
it "should filter endpoint hostname through our SSRF detector" do
|
|
klass::Request.any_instance.expects(:encrypt_payload)
|
|
klass::Request.any_instance.expects(:headers)
|
|
|
|
stub_ip_lookup("example.com", %W[0.0.0.0])
|
|
|
|
expect do
|
|
klass.payload_send(
|
|
endpoint: "http://example.com",
|
|
message: "test",
|
|
p256dh: "somep256dh",
|
|
auth: "someauth",
|
|
vapid: {
|
|
subject: "someurl",
|
|
public_key: "somepublickey",
|
|
private_key: "someprivatekey",
|
|
},
|
|
)
|
|
end.to raise_error(FinalDestination::SSRFDetector::DisallowedIpError)
|
|
end
|
|
|
|
it "should send the right request if endpoint hostname resolves to a public ip address" do
|
|
klass::Request.any_instance.expects(:encrypt_payload)
|
|
klass::Request.any_instance.expects(:headers)
|
|
|
|
stub_ip_lookup("example.com", %W[52.125.123.12])
|
|
|
|
success = Class.new(StandardError)
|
|
TCPSocket
|
|
.stubs(:open)
|
|
.with do |addr|
|
|
FinalDestination::Connector.token?(addr) &&
|
|
FinalDestination::Connector.addresses(addr) == %w[52.125.123.12]
|
|
end
|
|
.once
|
|
.raises(success)
|
|
|
|
expect do
|
|
klass.payload_send(
|
|
endpoint: "http://example.com",
|
|
message: "test",
|
|
p256dh: "somep256dh",
|
|
auth: "someauth",
|
|
vapid: {
|
|
subject: "someurl",
|
|
public_key: "somepublickey",
|
|
private_key: "someprivatekey",
|
|
},
|
|
)
|
|
end.to raise_error(success)
|
|
end
|
|
end
|