diff --git a/setup.py b/setup.py index e3f97bac..e873a251 100644 --- a/setup.py +++ b/setup.py @@ -16,6 +16,7 @@ EXTRAS_REQUIRE = { 'prctl': ['python_prctl'], # Named threads 'qrcode': ['qrcode'], 'sound;platform_system=="Windows"': ['winsound'], + 'tor': ['stem'], 'docs': [ 'sphinx', # fab build_docs 'graphviz', # fab build_docs @@ -129,6 +130,9 @@ if __name__ == "__main__": ext_modules=[bitmsghash], zip_safe=False, entry_points={ + 'bitmessage.nodes.validator': [ + 'onion = pybitmessage.plugins.validator_onion [tor]' + ], 'bitmessage.gui.menu': [ 'address.qrcode = pybitmessage.plugins.menu_qrcode [qrcode]' ], diff --git a/src/plugins/test_validator_onion.py b/src/plugins/test_validator_onion.py new file mode 100644 index 00000000..b275ea83 --- /dev/null +++ b/src/plugins/test_validator_onion.py @@ -0,0 +1,31 @@ +import unittest + +from pybitmessage.state import Peer + +try: + from validator_onion import OnionValidator + validator_onion = True +except ImportError: + validator_onion = False + + +@unittest.skipIf(not validator_onion, 'OnionValidator is not available') +class TestOnionValidator(unittest.TestCase): + """Test case for OnionValidator""" + @classmethod + def setUpClass(cls): + cls.check = OnionValidator() + + def test_valid(self): + """Ensure validator returns True for valid nodes""" + # not onion node + self.assertTrue(self.check(Peer('5.45.99.75', 8444))) + # default onion node + self.assertTrue(self.check(Peer('quzwelsuziwqgpt2.onion', 8444))) + + def test_invalid(self): + """Ensure validator returns False for invalid hostnames""" + # is not base32 + self.assertFalse(self.check(Peer('test.onion', 8444))) + # no descriptor + self.assertFalse(self.check(Peer('aaaaaaaaaaaaaaaa.onion', 8444))) diff --git a/src/plugins/validator_onion.py b/src/plugins/validator_onion.py new file mode 100644 index 00000000..32c73174 --- /dev/null +++ b/src/plugins/validator_onion.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- + +import base64 + +import stem +from stem.control import Controller + + +class OnionValidator(object): + """Validation plugin for onion nodes""" + def __init__(self): + try: # TODO: deal with authentication + self.controller = Controller.from_port() + self.controller.authenticate() + except (stem.SocketError, stem.connection.AuthenticationFailure): + raise ValueError # do not load this if controller is not available + + def _validate_onion(self, addr): + """Check the .onion address validity""" + try: + base64.b32decode(addr, True) + except TypeError: + return False + + if not self.controller: + return True + + try: + self.controller.get_hidden_service_descriptor(addr) + except stem.DescriptorUnavailable: + return False + + return True + + def __call__(self, node): + """Filter check for .onion addresses validation""" + addr = node.host + try: + addr, dom = addr.split('.') + except ValueError: + return True + return self._validate_onion(addr) if dom == 'onion' else True + + +connect_plugin = OnionValidator()