diff --git a/noipy/__init__.py b/noipy/__init__.py index b53e976..d60a092 100644 --- a/noipy/__init__.py +++ b/noipy/__init__.py @@ -9,7 +9,7 @@ """ __title__ = "noipy" -__version_info__ = ('1', '5', '3') +__version_info__ = ('1', '5', '4') __version__ = ".".join(__version_info__) __author__ = "Pablo O Vieira" __email__ = "noipy@pv8.io" diff --git a/noipy/utils.py b/noipy/utils.py index 355f7ce..3e3d6e8 100644 --- a/noipy/utils.py +++ b/noipy/utils.py @@ -5,12 +5,23 @@ # Copyright (c) 2013 Pablo O Vieira (povieira) # See README.rst and LICENSE for details. +try: + from StringIO import StringIO +except ImportError: + from io import StringIO + import socket +import dns.resolver import requests HTTPBIN_URL = "https://httpbin.org/ip" +IP4ONLY_URL = "http://ip4only.me/api" +IP6ONLY_URL = "http://ip6only.me/api" + +COMMON_DNS = "8.8.8.8" + try: input = raw_input except NameError: @@ -21,20 +32,59 @@ def read_input(message): return input(message) -def get_ip(): - """Return machine's origin IP address. - """ +def _try_request_get_and_store(url, callback): try: - r = requests.get(HTTPBIN_URL) - return r.json()['origin'] if r.status_code == 200 else None + r = requests.get(url) + if r.status_code == 200: + callback(r) except requests.exceptions.ConnectionError: + pass + + +def get_ip(): + """Return machine's origin IP address(es). + """ + + lst = [] + for url in (IP4ONLY_URL, IP6ONLY_URL): + _try_request_get_and_store( + url, + lambda r: lst.append(r.text.split(',')[1]) + ) + if not lst: + _try_request_get_and_store( + HTTPBIN_URL, + lambda r: lst.append(r.json()['origin']) + ) + if not lst: return None + return ','.join(lst) + + +def _safe_resolve(dnsname, dnstype): + resolver = dns.resolver.Resolver(StringIO("nameserver %s" % COMMON_DNS)) + + try: + resolve = resolver.resolve + except AttributeError: + resolve = resolver.query + + try: + return list(resolve(dnsname, dnstype)) + except dns.exception.DNSException: + return [] def get_dns_ip(dnsname): - """Return machine's current IP address in DNS. + """Return machine's current IP address(es) in DNS. """ - try: - return socket.gethostbyname(dnsname) - except socket.error: + + lst = [a.address for a in _safe_resolve(dnsname, 'A') + _safe_resolve(dnsname, 'AAAA')] + if not lst: + try: + lst.append(socket.gethostbyname(dnsname)) + except socket.error: + pass + if not lst: return None + return ','.join(lst) diff --git a/requirements-dev.txt b/requirements-dev.txt index 7c84001..e7989be 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -3,3 +3,5 @@ flake8==3.8.4 tox==3.23.0 pytest==4.6.9 +coverage +IPy diff --git a/requirements.txt b/requirements.txt index 9d84d35..bf6fae6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ +dnspython requests==2.25.1 diff --git a/setup.py b/setup.py index 681f62a..e4b2ef8 100644 --- a/setup.py +++ b/setup.py @@ -13,6 +13,7 @@ install_requires = [ "requests>=2.0", + "dnspython", ] if sys.version_info[:2] < (2, 7): diff --git a/test/test_noipy.py b/test/test_noipy.py index 44eeab0..dc78d95 100644 --- a/test/test_noipy.py +++ b/test/test_noipy.py @@ -5,9 +5,10 @@ # Copyright (c) 2013 Pablo O Vieira (povieira) # See README.rst and LICENSE for details. +from IPy import IP + import getpass import os -import re import shutil import unittest @@ -32,9 +33,44 @@ def tearDown(self): shutil.rmtree(self.test_dir) def test_get_ip(self): + ips = utils.get_ip() + + for ip in ips.split(','): + try: + IP(ip) + except Exception as ex: + self.assertIsNone(ex, "get_ip() failed.") + + # monkey patch for testing (forcing HTTP 404) + utils.IP6ONLY_URL = "http://ip6only.me/bad" ip = utils.get_ip() + self.assertIsNotNone(ip) + self.assertNotIn(',', ip) - self.assertTrue(re.match(VALID_IP_REGEX, ip), "get_ip() failed.") + # monkey patch for testing (forcing ConnectionError) + utils.IP6ONLY_URL = "http://example.nothing" + ip = utils.get_ip() + self.assertIsNotNone(ip) + self.assertNotIn(',', ip) + + # monkey patch for testing (forcing HTTP 404) + utils.IP4ONLY_URL = "http://ip4only.me/bad" + ip = utils.get_ip() + self.assertIsNotNone(ip) + self.assertNotIn(',', ip) + + # monkey patch for testing (forcing ConnectionError) + utils.IP4ONLY_URL = "http://example.nothing" + + ip = utils.get_ip() + self.assertIsNotNone(ip) + self.assertNotIn(',', ip) + + # monkey patch for testing (forcing HTTP 404) + utils.HTTPBIN_URL = "https://httpbin.org/bad" + + ip = utils.get_ip() + self.assertTrue(ip is None, "get_ip() should return None. IP=%s" % ip) # monkey patch for testing (forcing ConnectionError) utils.HTTPBIN_URL = "http://example.nothing" @@ -47,7 +83,13 @@ def test_get_dns_ip(self): self.assertEqual(ip, "127.0.0.1", "get_dns_ip() failed.") - ip = utils.get_dns_ip("http://example.nothing") + ip = utils.get_dns_ip("ip4only.me") + self.assertIsNotNone(ip, "get_dns_ip() should resolve IPv4") + + ip = utils.get_dns_ip("ip6only.me") + self.assertIsNotNone(ip, "get_dns_ip() should resolve IPv6") + + ip = utils.get_dns_ip("example.nothing") self.assertTrue(ip is None, "get_dns_ip() should return None. IP=%s" % ip) @@ -56,7 +98,7 @@ class PluginsTest(unittest.TestCase): def setUp(self): self.parser = main.create_parser() - self.test_ip = "10.1.2.3" + self.test_ip = "10.1.2.3,2004::1:2:3:4" def tearDown(self): pass @@ -168,7 +210,7 @@ class AuthInfoTest(unittest.TestCase): def setUp(self): self.parser = main.create_parser() - self.test_ip = "10.1.2.3" + self.test_ip = "10.1.2.3,2004::1:2:3:4" self.test_dir = os.path.join(os.path.expanduser("~"), "noipy_test") def tearDown(self): diff --git a/tox.ini b/tox.ini index 609ac50..a55168d 100644 --- a/tox.ini +++ b/tox.ini @@ -6,6 +6,7 @@ passenv = CI TRAVIS_BUILD_ID TRAVIS TRAVIS_BRANCH TRAVIS_JOB_NUMBER TRAVIS_PULL_ deps = -rrequirements-dev.txt py{26,27,35,36,37},pypy,pypy3: coverage + py{26,27,35,36,37},pypy,pypy3: IPy commands = python --version