import base64 import os import stat import sys import time import mock from hypothesis import ( given, settings, ) from hypothesis.strategies import ( integers, sets, ) from twisted.trial import unittest from twisted.internet import defer from twisted.python import log from foolscap.api import flushEventualQueue import foolscap.logging.log from twisted.application import service from allmydata.node import Node, formatTimeTahoeStyle, MissingConfigEntry, read_config, config_from_string from allmydata.introducer.server import create_introducer from allmydata.client import create_client from allmydata.util import fileutil, iputil from allmydata.util.namespace import Namespace import allmydata.test.common_util as testutil def port_numbers(): return integers(min_value=1, max_value=2 ** 16 - 1) class LoggingMultiService(service.MultiService): def log(self, msg, **kw): pass class TestNode(Node): CERTFILE='DEFAULT_CERTFILE_BLANK' def __init__(self, basedir): config = read_config(basedir, 'DEFAULT_PORTNUMFILE_BLANK') Node.__init__(self, config, basedir) class TestCase(testutil.SignalMixin, unittest.TestCase): def setUp(self): testutil.SignalMixin.setUp(self) self.parent = LoggingMultiService() self.parent.startService() # We can use a made-up port number because these tests never actually # try to bind the port. We'll use a low-numbered one that's likely to # conflict with another service to prove it. self._available_port = 22 def tearDown(self): log.msg("%s.tearDown" % self.__class__.__name__) testutil.SignalMixin.tearDown(self) d = defer.succeed(None) d.addCallback(lambda res: self.parent.stopService()) d.addCallback(flushEventualQueue) return d def _test_location(self, basedir, expected_addresses, tub_port=None, tub_location=None, local_addresses=None): fileutil.make_dirs(basedir) f = open(os.path.join(basedir, 'tahoe.cfg'), 'wt') f.write("[node]\n") if tub_port: f.write("tub.port = %d\n" % (tub_port,)) if tub_location is not None: f.write("tub.location = %s\n" % (tub_location,)) f.close() if local_addresses: self.patch(iputil, 'get_local_addresses_sync', lambda: local_addresses) n = TestNode(basedir) furl = n.tub.registerReference(n) for address in expected_addresses: self.assertIn(address, furl) def test_location1(self): return self._test_location(basedir="test_node/test_location1", expected_addresses=["192.0.2.0:1234"], tub_location="192.0.2.0:1234") def test_location2(self): return self._test_location(basedir="test_node/test_location2", expected_addresses=["192.0.2.0:1234", "example.org:8091"], tub_location="192.0.2.0:1234,example.org:8091") def test_location_not_set(self): """Checks the autogenerated furl when tub.location is not set.""" return self._test_location( basedir="test_node/test_location3", expected_addresses=[ "127.0.0.1:{}".format(self._available_port), "192.0.2.0:{}".format(self._available_port), ], tub_port=self._available_port, local_addresses=["127.0.0.1", "192.0.2.0"], ) def test_location_auto_and_explicit(self): """Checks the autogenerated furl when tub.location contains 'AUTO'.""" return self._test_location( basedir="test_node/test_location4", expected_addresses=[ "127.0.0.1:{}".format(self._available_port), "192.0.2.0:{}".format(self._available_port), "example.com:4321", ], tub_port=self._available_port, tub_location="AUTO,example.com:{}".format(self._available_port), local_addresses=["127.0.0.1", "192.0.2.0", "example.com:4321"], ) def test_tahoe_cfg_utf8(self): basedir = "test_node/test_tahoe_cfg_utf8" fileutil.make_dirs(basedir) f = open(os.path.join(basedir, 'tahoe.cfg'), 'wt') f.write(u"\uFEFF[node]\n".encode('utf-8')) f.write(u"nickname = \u2621\n".encode('utf-8')) f.close() n = TestNode(basedir) n.setServiceParent(self.parent) self.failUnlessEqual(n.get_config("node", "nickname").decode('utf-8'), u"\u2621") def test_tahoe_cfg_hash_in_name(self): basedir = "test_node/test_cfg_hash_in_name" nickname = "Hash#Bang!" # a clever nickname containing a hash fileutil.make_dirs(basedir) f = open(os.path.join(basedir, 'tahoe.cfg'), 'wt') f.write("[node]\n") f.write("nickname = %s\n" % (nickname,)) f.close() n = TestNode(basedir) self.failUnless(n.nickname == nickname) def test_private_config(self): basedir = "test_node/test_private_config" privdir = os.path.join(basedir, "private") fileutil.make_dirs(privdir) f = open(os.path.join(privdir, 'already'), 'wt') f.write("secret") f.close() n = TestNode(basedir) self.failUnlessEqual(n.get_private_config("already"), "secret") self.failUnlessEqual(n.get_private_config("not", "default"), "default") self.failUnlessRaises(MissingConfigEntry, n.get_private_config, "not") value = n.get_or_create_private_config("new", "start") self.failUnlessEqual(value, "start") self.failUnlessEqual(n.get_private_config("new"), "start") counter = [] def make_newer(): counter.append("called") return "newer" value = n.get_or_create_private_config("newer", make_newer) self.failUnlessEqual(len(counter), 1) self.failUnlessEqual(value, "newer") self.failUnlessEqual(n.get_private_config("newer"), "newer") value = n.get_or_create_private_config("newer", make_newer) self.failUnlessEqual(len(counter), 1) # don't call unless necessary self.failUnlessEqual(value, "newer") def test_timestamp(self): # this modified logger doesn't seem to get used during the tests, # probably because we don't modify the LogObserver that trial # installs (only the one that twistd installs). So manually exercise # it a little bit. t = formatTimeTahoeStyle("ignored", time.time()) self.failUnless("Z" in t) t2 = formatTimeTahoeStyle("ignored", int(time.time())) self.failUnless("Z" in t2) def test_secrets_dir(self): basedir = "test_node/test_secrets_dir" fileutil.make_dirs(basedir) n = TestNode(basedir) self.failUnless(isinstance(n, TestNode)) self.failUnless(os.path.exists(os.path.join(basedir, "private"))) def test_secrets_dir_protected(self): if "win32" in sys.platform.lower() or "cygwin" in sys.platform.lower(): # We don't know how to test that unprivileged users can't read this # thing. (Also we don't know exactly how to set the permissions so # that unprivileged users can't read this thing.) raise unittest.SkipTest("We don't know how to set permissions on Windows.") basedir = "test_node/test_secrets_dir_protected" fileutil.make_dirs(basedir) n = TestNode(basedir) self.failUnless(isinstance(n, TestNode)) privdir = os.path.join(basedir, "private") st = os.stat(privdir) bits = stat.S_IMODE(st[stat.ST_MODE]) self.failUnless(bits & 0001 == 0, bits) def test_logdir_is_str(self): basedir = "test_node/test_logdir_is_str" fileutil.make_dirs(basedir) ns = Namespace() ns.called = False def call_setLogDir(logdir): ns.called = True self.failUnless(isinstance(logdir, str), logdir) self.patch(foolscap.logging.log, 'setLogDir', call_setLogDir) TestNode(basedir) self.failUnless(ns.called) class EmptyNode(Node): def __init__(self): config = config_from_string("", "no portfile") Node.__init__(self, config, 'no basedir') EXPECTED = { # top-level key is tub.port category "missing": { # 2nd-level key is tub.location category "missing": "alloc/auto", "empty": "ERR2", "disabled": "ERR4", "hintstring": "alloc/file", }, "empty": { "missing": "ERR1", "empty": "ERR1", "disabled": "ERR1", "hintstring": "ERR1", }, "disabled": { "missing": "ERR3", "empty": "ERR2", "disabled": "no-listen", "hintstring": "ERR3", }, "endpoint": { "missing": "auto", "empty": "ERR2", "disabled": "ERR4", "hintstring": "manual", }, } class PortLocation(unittest.TestCase): def test_all(self): for tp in EXPECTED.keys(): for tl in EXPECTED[tp].keys(): exp = EXPECTED[tp][tl] self._try(tp, tl, exp) def _try(self, tp, tl, exp): log.msg("PortLocation._try:", tp, tl, exp) cfg_tubport = {"missing": None, "empty": "", "disabled": "disabled", "endpoint": "tcp:777", }[tp] cfg_location = {"missing": None, "empty": "", "disabled": "disabled", "hintstring": "tcp:HOST:888,AUTO", }[tl] n = EmptyNode() basedir = os.path.join("test_node/portlocation/%s/%s" % (tp, tl)) fileutil.make_dirs(basedir) config = n.config = read_config(basedir, "node.port") n._reveal_ip = True if exp in ("ERR1", "ERR2", "ERR3", "ERR4"): e = self.assertRaises(ValueError, n.get_tub_portlocation, cfg_tubport, cfg_location) if exp == "ERR1": self.assertEqual("tub.port must not be empty", str(e)) elif exp == "ERR2": self.assertEqual("tub.location must not be empty", str(e)) elif exp == "ERR3": self.assertEqual("tub.port is disabled, but not tub.location", str(e)) elif exp == "ERR4": self.assertEqual("tub.location is disabled, but not tub.port", str(e)) else: self.assert_(False) elif exp == "no-listen": res = n.get_tub_portlocation(cfg_tubport, cfg_location) self.assertEqual(res, None) elif exp in ("alloc/auto", "alloc/file", "auto", "manual"): with mock.patch("allmydata.util.iputil.get_local_addresses_sync", return_value=["LOCAL"]): with mock.patch("allmydata.util.iputil.allocate_tcp_port", return_value=999): port, location = n.get_tub_portlocation(cfg_tubport, cfg_location) try: with open(config.portnum_fname, "r") as f: saved_port = f.read().strip() except EnvironmentError: saved_port = None if exp == "alloc/auto": self.assertEqual(port, "tcp:999") self.assertEqual(location, "tcp:LOCAL:999") self.assertEqual(saved_port, "tcp:999") elif exp == "alloc/file": self.assertEqual(port, "tcp:999") self.assertEqual(location, "tcp:HOST:888,tcp:LOCAL:999") self.assertEqual(saved_port, "tcp:999") elif exp == "auto": self.assertEqual(port, "tcp:777") self.assertEqual(location, "tcp:LOCAL:777") self.assertEqual(saved_port, None) elif exp == "manual": self.assertEqual(port, "tcp:777") self.assertEqual(location, "tcp:HOST:888,tcp:LOCAL:777") self.assertEqual(saved_port, None) else: self.assert_(False) else: self.assert_(False) BASE_CONFIG = """ [client] introducer.furl = empty [tor] enabled = false [i2p] enabled = false [node] """ NOLISTEN = """ [node] tub.port = disabled tub.location = disabled """ DISABLE_STORAGE = """ [storage] enabled = false """ ENABLE_STORAGE = """ [storage] enabled = true """ ENABLE_HELPER = """ [helper] enabled = true """ class FakeTub: def __init__(self): self.tubID = base64.b32encode("foo") self.listening_ports = [] def setOption(self, name, value): pass def removeAllConnectionHintHandlers(self): pass def addConnectionHintHandler(self, hint_type, handler): pass def listenOn(self, what): self.listening_ports.append(what) def setLocation(self, location): pass def setServiceParent(self, parent): pass class Listeners(unittest.TestCase): # Randomly allocate a couple distinct port numbers to try out. The test # never actually binds these port numbers so we don't care if they're "in # use" on the system or not. We just want a couple distinct values we can # check expected results against. @given(ports=sets(elements=port_numbers(), min_size=2, max_size=2)) # A few examples should satisfy us here. The logic for dealing with these # numbers is hardly complex. On the flip side, all of the setup work we # do to get a node we can test against is pretty expensive. @settings(max_examples=10) def test_multiple_ports(self, ports): """ When there are multiple listen addresses suggested by the ``tub.port`` and ``tub.location`` configuration, the node's *main* port listens on all of them. """ n = EmptyNode() n.basedir = self.mktemp() n.config_fname = os.path.join(n.basedir, "tahoe.cfg") os.mkdir(n.basedir) os.mkdir(os.path.join(n.basedir, "private")) port1, port2 = iter(ports) port = ("tcp:%d:interface=127.0.0.1,tcp:%d:interface=127.0.0.1" % (port1, port2)) location = "tcp:localhost:%d,tcp:localhost:%d" % (port1, port2) with open(n.config_fname, "w") as f: f.write(BASE_CONFIG) f.write("tub.port = %s\n" % port) f.write("tub.location = %s\n" % location) # we're doing a lot of calling-into-setup-methods here, it might be # better to just create a real Node instance, I'm not sure. n.config = read_config(n.basedir, "client.port") n.check_privacy() n.services = [] n.create_i2p_provider() n.create_tor_provider() n.init_connections() n.set_tub_options() t = FakeTub() with mock.patch("allmydata.node.Tub", return_value=t): n.create_main_tub() self.assertEqual(t.listening_ports, ["tcp:%d:interface=127.0.0.1" % port1, "tcp:%d:interface=127.0.0.1" % port2]) def test_tor_i2p_listeners(self): n = EmptyNode() n.basedir = self.mktemp() n.config_fname = os.path.join(n.basedir, "tahoe.cfg") os.mkdir(n.basedir) os.mkdir(os.path.join(n.basedir, "private")) with open(n.config_fname, "w") as f: f.write(BASE_CONFIG) f.write("tub.port = listen:i2p,listen:tor\n") f.write("tub.location = tcp:example.org:1234\n") # we're doing a lot of calling-into-setup-methods here, it might be # better to just create a real Node instance, I'm not sure. n.config = read_config(n.basedir, "client.port") n.check_privacy() n.services = [] i2p_ep = object() tor_ep = object() n._i2p_provider = mock.Mock() n._i2p_provider.get_listener = mock.Mock(return_value=i2p_ep) n._tor_provider = mock.Mock() n._tor_provider.get_listener = mock.Mock(return_value=tor_ep) n.init_connections() n.set_tub_options() t = FakeTub() with mock.patch("allmydata.node.Tub", return_value=t): n.create_main_tub() self.assertEqual(n._i2p_provider.get_listener.mock_calls, [mock.call()]) self.assertEqual(n._tor_provider.get_listener.mock_calls, [mock.call()]) self.assertEqual(t.listening_ports, [i2p_ep, tor_ep]) class ClientNotListening(unittest.TestCase): def test_disabled(self): basedir = "test_node/test_disabled" fileutil.make_dirs(basedir) f = open(os.path.join(basedir, 'tahoe.cfg'), 'wt') f.write(BASE_CONFIG) f.write(NOLISTEN) f.write(DISABLE_STORAGE) f.close() n = create_client(basedir) self.assertEqual(n.tub.getListeners(), []) def test_disabled_but_storage(self): basedir = "test_node/test_disabled_but_storage" fileutil.make_dirs(basedir) f = open(os.path.join(basedir, 'tahoe.cfg'), 'wt') f.write(BASE_CONFIG) f.write(NOLISTEN) f.write(ENABLE_STORAGE) f.close() e = self.assertRaises(ValueError, create_client, basedir) self.assertIn("storage is enabled, but tub is not listening", str(e)) def test_disabled_but_helper(self): basedir = "test_node/test_disabled_but_helper" fileutil.make_dirs(basedir) f = open(os.path.join(basedir, 'tahoe.cfg'), 'wt') f.write(BASE_CONFIG) f.write(NOLISTEN) f.write(DISABLE_STORAGE) f.write(ENABLE_HELPER) f.close() e = self.assertRaises(ValueError, create_client, basedir) self.assertIn("helper is enabled, but tub is not listening", str(e)) class IntroducerNotListening(unittest.TestCase): def test_port_none_introducer(self): basedir = "test_node/test_port_none_introducer" fileutil.make_dirs(basedir) f = open(os.path.join(basedir, 'tahoe.cfg'), 'wt') f.write("[node]\n") f.write("tub.port = disabled\n") f.write("tub.location = disabled\n") f.close() e = self.assertRaises(ValueError, create_introducer, basedir) self.assertIn("we are Introducer, but tub is not listening", str(e))