Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ gem "rake", "~> 13.0"
gem "rubocop", "~> 1.7"
gem "rubocop-minitest", require: false
gem "rubocop-rake", require: false
gem "webmock"
25 changes: 21 additions & 4 deletions lib/sparoid.rb
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ def generate_messages(ip)
ips = ips.grep_v(Resolv::IPv6)
ips << Resolv::IPv6.create(native_ipv6)
end
raise PublicIPError, "Sparoid could not resolve a public IP to knock from" if ips.empty?

ips.map { |i| message(i) }
end
end
Expand Down Expand Up @@ -253,16 +255,31 @@ class Error < StandardError; end

class ResolvError < Error; end

# Instance of SPAroid that only resolved public_ips once
class PublicIPError < Error; end

# Instance of SPAroid that resolves its public IP once and reuses it
Comment thread
oskgu360 marked this conversation as resolved.
class Instance
include Sparoid

def public_ips(*args)
Comment thread
walro marked this conversation as resolved.
@public_ips ||= super
def initialize
@resolve_mutex = Mutex.new
Comment thread
oskgu360 marked this conversation as resolved.
@public_ips = []
end

def cached_public_ips
public_ips
return @public_ips if @public_ips.any?

@resolve_mutex.synchronize do
return @public_ips if @public_ips.any?

ips = public_ips
if ips.empty?
warn "Sparoid: Failed to retrieve public IPs"
else
@public_ips = ips
end
ips
end
end
end
end
33 changes: 33 additions & 0 deletions test/sparoid_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ def test_that_it_has_a_version_number
end

def test_it_resolves_public_ip
stub_icanhazip(ipv4_body: "203.0.113.7\n", ipv6_body: "2001:db8::1\n")
addresses = Sparoid.send(:public_ips)
assert(addresses.any? { |ip| ip.is_a?(Resolv::IPv4) || ip.is_a?(Resolv::IPv6) })
end
Expand Down Expand Up @@ -228,6 +229,38 @@ def test_keygen_outputs_keys
assert_match(/^hmac-key = [0-9a-f]{64}$/, output.lines[1].chomp)
end

def stub_icanhazip(ipv4_body:, ipv6_body: "")
stub_request(:get, "http://ipv6.icanhazip.com/").to_return(status: 200, body: ipv6_body)
stub_request(:get, "http://ipv4.icanhazip.com/").to_return(status: 200, body: ipv4_body)
end

def test_instance_retries_and_recovers_instead_of_pinning_empty
instance = Sparoid::Instance.new

stub_icanhazip(ipv4_body: "", ipv6_body: "")
_out, err = capture_io { assert_empty instance.cached_public_ips }
assert_match(/Failed to retrieve public IPs/, err)
assert_empty instance.instance_variable_get(:@public_ips), "empty result must not be memoized"

stub_icanhazip(ipv4_body: "203.0.113.7\n")
assert_equal ["203.0.113.7"], instance.cached_public_ips.map(&:to_s)
end

def test_auth_raises_when_no_public_ip_resolved
key = "0000000000000000000000000000000000000000000000000000000000000000"
hmac_key = "0000000000000000000000000000000000000000000000000000000000000000"
s = Sparoid::Instance.new

s.stub(:cached_public_ips, []) do
s.stub(:public_ipv6_by_udp, nil) do
err = assert_raises(Sparoid::PublicIPError) do
s.auth(key, hmac_key, "127.0.0.1", 1337)
end
assert_match(/could not resolve a public IP/, err.message)
end
end
end

def test_fdpass_closes_socket_when_connect_fails_synchronously
addr = Addrinfo.tcp("127.0.0.1", 0)
closed = false
Expand Down
1 change: 1 addition & 0 deletions test/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@

require "minitest/stub_const"
require "minitest/autorun"
require "webmock/minitest"