diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index befbde2..1dd7975 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,18 +1,22 @@ name: Ruby -on: [push,pull_request] +on: + push: + branches: + - main + pull_request: jobs: build: strategy: fail-fast: false matrix: - ruby: [ 2.7, '3.0', 3.1, ruby-head ] + ruby: [ 2.7, 3.1, 3.2, 3.3, ruby-head ] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} - - run: bundle install + bundler-cache: true - run: bundle exec rake diff --git a/lib/sparoid.rb b/lib/sparoid.rb index 0b1c1dc..a8d2271 100644 --- a/lib/sparoid.rb +++ b/lib/sparoid.rb @@ -149,9 +149,21 @@ def write_cache end end - def public_ip - Resolv::DNS.open(nameserver: ["208.67.222.222", "208.67.220.220"]) do |dns| - dns.getresource("myip.opendns.com", Resolv::DNS::Resource::IN::A).address + def public_ip(host = "checkip.amazonaws.com", port = 80) # rubocop:disable Metrics/MethodLength + Socket.tcp(host, port, connect_timeout: 3) do |sock| + sock.sync = true + sock.print "GET / HTTP/1.1\r\nHost: #{host}\r\nConnection: close\r\n\r\n" + status = sock.readline(chomp: true) + raise(ResolvError, "#{host}:#{port} response: #{status}") unless status.start_with? "HTTP/1.1 200 " + + content_length = 0 + until (header = sock.readline(chomp: true)).empty? + if (m = header.match(/^Content-Length: (\d+)/)) + content_length = m[1].to_i + end + end + ip = sock.read(content_length).chomp + Resolv::IPv4.create ip end end @@ -172,9 +184,7 @@ class ResolvError < Error; end class Instance include Sparoid - private - - def public_ip + def public_ip(*args) @public_ip ||= super end diff --git a/test/sparoid_test.rb b/test/sparoid_test.rb index 72f282b..08d257e 100644 --- a/test/sparoid_test.rb +++ b/test/sparoid_test.rb @@ -61,26 +61,41 @@ def test_it_sends_message_with_empty_cache_file end end - def test_it_resolves_public_ip_only_once_per_instance - dns = Minitest::Mock.new - dns.expect :getresource, Resolv::IPv4.create("1.1.1.1"), ["myip.opendns.com", Resolv::DNS::Resource::IN::A] - Resolv::DNS.stub(:open, ->(_, &blk) { blk.call dns }) do - s = Sparoid::Instance.new - 2.times do - s.send(:public_ip) - end + def test_it_resolves_public_ip_only_once_per_instance # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + server = TCPServer.new "127.0.0.1", 0 + host = server.addr[3] + port = server.addr[1] + Thread.new do + client = server.accept + client_ip = client.addr[3] + assert_equal "GET / HTTP/1.1", client.readline(chomp: true) + assert_match "Host: ", client.readline(chomp: true) + assert_equal "Connection: close", client.readline(chomp: true) + + client.print "HTTP/1.1 200 OK\r\n" + client.print "Content-Length: #{client_ip.bytesize}\r\n" + client.print "\r\n" + client.print client_ip + client.close + server.close + end + + s = Sparoid::Instance.new + 2.times do + ip = s.public_ip host, port + assert_equal Resolv::IPv4.create("127.0.0.1"), ip end - dns.verify end def test_it_raises_resolve_error_on_dns_socket_error key = "0000000000000000000000000000000000000000000000000000000000000000" hmac_key = "0000000000000000000000000000000000000000000000000000000000000000" + open_for_ip = Resolv::IPv4.create("1.1.1.1") error = ->(*_) { raise SocketError, "getaddrinfo: Name or service not known" } Addrinfo.stub(:getaddrinfo, error) do assert_raises(Sparoid::ResolvError) do - Sparoid::Instance.new.auth(key, hmac_key, "127.0.0.1", 1337) + Sparoid::Instance.new.auth(key, hmac_key, "127.0.0.1", 1337, open_for_ip: open_for_ip) end end end