1 | from __future__ import annotations |
---|
2 | |
---|
3 | import base64 |
---|
4 | import os |
---|
5 | import stat |
---|
6 | import sys |
---|
7 | import time |
---|
8 | from textwrap import dedent |
---|
9 | import configparser |
---|
10 | |
---|
11 | from hypothesis import ( |
---|
12 | given, |
---|
13 | ) |
---|
14 | from hypothesis.strategies import ( |
---|
15 | integers, |
---|
16 | sets, |
---|
17 | ) |
---|
18 | |
---|
19 | from unittest import skipIf |
---|
20 | |
---|
21 | from twisted.python.filepath import ( |
---|
22 | FilePath, |
---|
23 | ) |
---|
24 | from twisted.python.runtime import platform |
---|
25 | from twisted.trial import unittest |
---|
26 | from twisted.internet import defer |
---|
27 | |
---|
28 | import foolscap.logging.log |
---|
29 | |
---|
30 | from twisted.application import service |
---|
31 | from allmydata.node import ( |
---|
32 | PortAssignmentRequired, |
---|
33 | PrivacyError, |
---|
34 | tub_listen_on, |
---|
35 | create_tub_options, |
---|
36 | create_main_tub, |
---|
37 | create_node_dir, |
---|
38 | create_default_connection_handlers, |
---|
39 | create_connection_handlers, |
---|
40 | config_from_string, |
---|
41 | read_config, |
---|
42 | MissingConfigEntry, |
---|
43 | _tub_portlocation, |
---|
44 | formatTimeTahoeStyle, |
---|
45 | UnescapedHashError, |
---|
46 | ) |
---|
47 | from allmydata.introducer.server import create_introducer |
---|
48 | from allmydata import client |
---|
49 | |
---|
50 | from allmydata.util import fileutil, iputil |
---|
51 | from allmydata.util.namespace import Namespace |
---|
52 | from allmydata.util.configutil import ( |
---|
53 | ValidConfiguration, |
---|
54 | UnknownConfigError, |
---|
55 | ) |
---|
56 | |
---|
57 | from allmydata.util.i2p_provider import create as create_i2p_provider |
---|
58 | from allmydata.util.tor_provider import create as create_tor_provider |
---|
59 | import allmydata.test.common_util as testutil |
---|
60 | |
---|
61 | from .common import ( |
---|
62 | ConstantAddresses, |
---|
63 | SameProcessStreamEndpointAssigner, |
---|
64 | UseNode, |
---|
65 | superuser, |
---|
66 | ) |
---|
67 | |
---|
68 | def port_numbers(): |
---|
69 | return integers(min_value=1, max_value=2 ** 16 - 1) |
---|
70 | |
---|
71 | class LoggingMultiService(service.MultiService): |
---|
72 | def log(self, msg, **kw): |
---|
73 | pass |
---|
74 | |
---|
75 | |
---|
76 | # see https://tahoe-lafs.org/trac/tahoe-lafs/ticket/2946 |
---|
77 | def testing_tub(reactor, config_data=''): |
---|
78 | """ |
---|
79 | Creates a 'main' Tub for testing purposes, from config data |
---|
80 | """ |
---|
81 | basedir = 'dummy_basedir' |
---|
82 | config = config_from_string(basedir, 'DEFAULT_PORTNUMFILE_BLANK', config_data) |
---|
83 | fileutil.make_dirs(os.path.join(basedir, 'private')) |
---|
84 | |
---|
85 | i2p_provider = create_i2p_provider(reactor, config) |
---|
86 | tor_provider = create_tor_provider(reactor, config) |
---|
87 | handlers = create_connection_handlers(config, i2p_provider, tor_provider) |
---|
88 | default_connection_handlers, foolscap_connection_handlers = handlers |
---|
89 | tub_options = create_tub_options(config) |
---|
90 | |
---|
91 | main_tub = create_main_tub( |
---|
92 | config, tub_options, default_connection_handlers, |
---|
93 | foolscap_connection_handlers, i2p_provider, tor_provider, |
---|
94 | cert_filename='DEFAULT_CERTFILE_BLANK' |
---|
95 | ) |
---|
96 | return main_tub |
---|
97 | |
---|
98 | |
---|
99 | class TestCase(testutil.SignalMixin, unittest.TestCase): |
---|
100 | |
---|
101 | def setUp(self): |
---|
102 | testutil.SignalMixin.setUp(self) |
---|
103 | self.parent = LoggingMultiService() |
---|
104 | # We can use a made-up port number because these tests never actually |
---|
105 | # try to bind the port. We'll use a low-numbered one that's likely to |
---|
106 | # conflict with another service to prove it. |
---|
107 | self._available_port = 22 |
---|
108 | self.port_assigner = SameProcessStreamEndpointAssigner() |
---|
109 | self.port_assigner.setUp() |
---|
110 | self.addCleanup(self.port_assigner.tearDown) |
---|
111 | |
---|
112 | def _test_location( |
---|
113 | self, |
---|
114 | expected_addresses, |
---|
115 | tub_port=None, |
---|
116 | tub_location=None, |
---|
117 | local_addresses=None, |
---|
118 | ): |
---|
119 | """ |
---|
120 | Verify that a Tub configured with the given *tub.port* and *tub.location* |
---|
121 | values generates fURLs with the given addresses in its location hints. |
---|
122 | |
---|
123 | :param [str] expected_addresses: The addresses which must appear in |
---|
124 | the generated fURL for the test to pass. All addresses must |
---|
125 | appear. |
---|
126 | |
---|
127 | :param tub_port: If not ``None`` then a value for the *tub.port* |
---|
128 | configuration item. |
---|
129 | |
---|
130 | :param tub_location: If not ``None`` then a value for the *tub.port* |
---|
131 | configuration item. |
---|
132 | |
---|
133 | :param local_addresses: If not ``None`` then a list of addresses to |
---|
134 | supply to the system under test as local addresses. |
---|
135 | """ |
---|
136 | from twisted.internet import reactor |
---|
137 | |
---|
138 | basedir = self.mktemp() |
---|
139 | create_node_dir(basedir, "testing") |
---|
140 | if tub_port is None: |
---|
141 | # Always configure a usable tub.port address instead of relying on |
---|
142 | # the automatic port assignment. The automatic port assignment is |
---|
143 | # prone to collisions and spurious test failures. |
---|
144 | _, tub_port = self.port_assigner.assign(reactor) |
---|
145 | |
---|
146 | config_data = "[node]\n" |
---|
147 | config_data += "tub.port = {}\n".format(tub_port) |
---|
148 | |
---|
149 | # If they wanted a certain location, go for it. This probably won't |
---|
150 | # agree with the tub.port value we set but that only matters if |
---|
151 | # anything tries to use this to establish a connection ... which |
---|
152 | # nothing in this test suite will. |
---|
153 | if tub_location is not None: |
---|
154 | config_data += "tub.location = {}\n".format(tub_location) |
---|
155 | |
---|
156 | if local_addresses is not None: |
---|
157 | self.patch(iputil, 'get_local_addresses_sync', |
---|
158 | lambda: local_addresses) |
---|
159 | |
---|
160 | tub = testing_tub(reactor, config_data) |
---|
161 | |
---|
162 | class Foo(object): |
---|
163 | pass |
---|
164 | |
---|
165 | furl = tub.registerReference(Foo()) |
---|
166 | for address in expected_addresses: |
---|
167 | self.assertIn(address, furl) |
---|
168 | |
---|
169 | def test_location1(self): |
---|
170 | return self._test_location(expected_addresses=["192.0.2.0:1234"], |
---|
171 | tub_location="192.0.2.0:1234") |
---|
172 | |
---|
173 | def test_location2(self): |
---|
174 | return self._test_location(expected_addresses=["192.0.2.0:1234", "example.org:8091"], |
---|
175 | tub_location="192.0.2.0:1234,example.org:8091") |
---|
176 | |
---|
177 | def test_location_not_set(self): |
---|
178 | """Checks the autogenerated furl when tub.location is not set.""" |
---|
179 | return self._test_location( |
---|
180 | expected_addresses=[ |
---|
181 | "127.0.0.1:{}".format(self._available_port), |
---|
182 | "192.0.2.0:{}".format(self._available_port), |
---|
183 | ], |
---|
184 | tub_port=self._available_port, |
---|
185 | local_addresses=["127.0.0.1", "192.0.2.0"], |
---|
186 | ) |
---|
187 | |
---|
188 | def test_location_auto_and_explicit(self): |
---|
189 | """Checks the autogenerated furl when tub.location contains 'AUTO'.""" |
---|
190 | return self._test_location( |
---|
191 | expected_addresses=[ |
---|
192 | "127.0.0.1:{}".format(self._available_port), |
---|
193 | "192.0.2.0:{}".format(self._available_port), |
---|
194 | "example.com:4321", |
---|
195 | ], |
---|
196 | tub_port=self._available_port, |
---|
197 | tub_location="AUTO,example.com:{}".format(self._available_port), |
---|
198 | local_addresses=["127.0.0.1", "192.0.2.0", "example.com:4321"], |
---|
199 | ) |
---|
200 | |
---|
201 | def test_tahoe_cfg_utf8(self): |
---|
202 | basedir = "test_node/test_tahoe_cfg_utf8" |
---|
203 | fileutil.make_dirs(basedir) |
---|
204 | f = open(os.path.join(basedir, 'tahoe.cfg'), 'wb') |
---|
205 | f.write(u"\uFEFF[node]\n".encode('utf-8')) |
---|
206 | f.write(u"nickname = \u2621\n".encode('utf-8')) |
---|
207 | f.close() |
---|
208 | |
---|
209 | config = read_config(basedir, "") |
---|
210 | self.failUnlessEqual(config.get_config("node", "nickname"), |
---|
211 | u"\u2621") |
---|
212 | |
---|
213 | def test_tahoe_cfg_hash_in_name(self): |
---|
214 | basedir = "test_node/test_cfg_hash_in_name" |
---|
215 | nickname = "Hash#Bang!" # a clever nickname containing a hash |
---|
216 | fileutil.make_dirs(basedir) |
---|
217 | f = open(os.path.join(basedir, 'tahoe.cfg'), 'wt') |
---|
218 | f.write("[node]\n") |
---|
219 | f.write("nickname = %s\n" % (nickname,)) |
---|
220 | f.close() |
---|
221 | |
---|
222 | config = read_config(basedir, "") |
---|
223 | self.failUnless(config.nickname == nickname) |
---|
224 | |
---|
225 | def test_hash_in_furl(self): |
---|
226 | """ |
---|
227 | Hashes in furl options are not allowed, resulting in exception. |
---|
228 | """ |
---|
229 | basedir = self.mktemp() |
---|
230 | fileutil.make_dirs(basedir) |
---|
231 | with open(os.path.join(basedir, 'tahoe.cfg'), 'wt') as f: |
---|
232 | f.write("[node]\n") |
---|
233 | f.write("log_gatherer.furl = lalal#onohash\n") |
---|
234 | |
---|
235 | config = read_config(basedir, "") |
---|
236 | with self.assertRaises(UnescapedHashError): |
---|
237 | config.get_config("node", "log_gatherer.furl") |
---|
238 | |
---|
239 | def test_missing_config_item(self): |
---|
240 | """ |
---|
241 | If a config item is missing: |
---|
242 | |
---|
243 | 1. Given a default, return default. |
---|
244 | 2. Otherwise, raise MissingConfigEntry. |
---|
245 | """ |
---|
246 | basedir = self.mktemp() |
---|
247 | fileutil.make_dirs(basedir) |
---|
248 | with open(os.path.join(basedir, 'tahoe.cfg'), 'wt') as f: |
---|
249 | f.write("[node]\n") |
---|
250 | config = read_config(basedir, "") |
---|
251 | |
---|
252 | self.assertEquals(config.get_config("node", "log_gatherer.furl", "def"), "def") |
---|
253 | with self.assertRaises(MissingConfigEntry): |
---|
254 | config.get_config("node", "log_gatherer.furl") |
---|
255 | |
---|
256 | def test_missing_config_section(self): |
---|
257 | """ |
---|
258 | Enumerating a missing section returns empty dict |
---|
259 | """ |
---|
260 | basedir = self.mktemp() |
---|
261 | fileutil.make_dirs(basedir) |
---|
262 | with open(os.path.join(basedir, 'tahoe.cfg'), 'w'): |
---|
263 | pass |
---|
264 | config = read_config(basedir, "") |
---|
265 | self.assertEquals( |
---|
266 | config.enumerate_section("not-a-section"), |
---|
267 | {} |
---|
268 | ) |
---|
269 | |
---|
270 | def test_config_required(self): |
---|
271 | """ |
---|
272 | Asking for missing (but required) configuration is an error |
---|
273 | """ |
---|
274 | basedir = u"test_node/test_config_required" |
---|
275 | config = read_config(basedir, "portnum") |
---|
276 | |
---|
277 | with self.assertRaises(Exception): |
---|
278 | config.get_config_from_file("it_does_not_exist", required=True) |
---|
279 | |
---|
280 | def test_config_items(self): |
---|
281 | """ |
---|
282 | All items in a config section can be retrieved. |
---|
283 | """ |
---|
284 | basedir = u"test_node/test_config_items" |
---|
285 | create_node_dir(basedir, "testing") |
---|
286 | |
---|
287 | with open(os.path.join(basedir, 'tahoe.cfg'), 'wt') as f: |
---|
288 | f.write(dedent( |
---|
289 | """ |
---|
290 | [node] |
---|
291 | nickname = foo |
---|
292 | timeout.disconnect = 12 |
---|
293 | """ |
---|
294 | )) |
---|
295 | config = read_config(basedir, "portnum") |
---|
296 | self.assertEqual( |
---|
297 | config.items("node"), |
---|
298 | [("nickname", "foo"), |
---|
299 | ("timeout.disconnect", "12"), |
---|
300 | ], |
---|
301 | ) |
---|
302 | self.assertEqual( |
---|
303 | config.items("node", [("unnecessary", "default")]), |
---|
304 | [("nickname", "foo"), |
---|
305 | ("timeout.disconnect", "12"), |
---|
306 | ], |
---|
307 | ) |
---|
308 | |
---|
309 | |
---|
310 | def test_config_items_missing_section(self): |
---|
311 | """ |
---|
312 | If a default is given for a missing section, the default is used. |
---|
313 | |
---|
314 | Lacking both default and section, an error is raised. |
---|
315 | """ |
---|
316 | basedir = self.mktemp() |
---|
317 | create_node_dir(basedir, "testing") |
---|
318 | |
---|
319 | with open(os.path.join(basedir, 'tahoe.cfg'), 'wt') as f: |
---|
320 | f.write("") |
---|
321 | |
---|
322 | config = read_config(basedir, "portnum") |
---|
323 | with self.assertRaises(configparser.NoSectionError): |
---|
324 | config.items("nosuch") |
---|
325 | default = [("hello", "world")] |
---|
326 | self.assertEqual(config.items("nosuch", default), default) |
---|
327 | |
---|
328 | @skipIf(platform.isWindows(), "We don't know how to set permissions on Windows.") |
---|
329 | @skipIf(superuser, "cannot test as superuser with all permissions") |
---|
330 | def test_private_config_unreadable(self): |
---|
331 | """ |
---|
332 | Asking for inaccessible private config is an error |
---|
333 | """ |
---|
334 | basedir = u"test_node/test_private_config_unreadable" |
---|
335 | create_node_dir(basedir, "testing") |
---|
336 | config = read_config(basedir, "portnum") |
---|
337 | config.get_or_create_private_config("foo", "contents") |
---|
338 | fname = os.path.join(basedir, "private", "foo") |
---|
339 | os.chmod(fname, 0) |
---|
340 | |
---|
341 | with self.assertRaises(Exception): |
---|
342 | config.get_or_create_private_config("foo") |
---|
343 | |
---|
344 | @skipIf(platform.isWindows(), "We don't know how to set permissions on Windows.") |
---|
345 | @skipIf(superuser, "cannot test as superuser with all permissions") |
---|
346 | def test_private_config_unreadable_preexisting(self): |
---|
347 | """ |
---|
348 | error if reading private config data fails |
---|
349 | """ |
---|
350 | basedir = u"test_node/test_private_config_unreadable_preexisting" |
---|
351 | create_node_dir(basedir, "testing") |
---|
352 | config = read_config(basedir, "portnum") |
---|
353 | fname = os.path.join(basedir, "private", "foo") |
---|
354 | with open(fname, "w") as f: |
---|
355 | f.write("stuff") |
---|
356 | os.chmod(fname, 0) |
---|
357 | |
---|
358 | with self.assertRaises(Exception): |
---|
359 | config.get_private_config("foo") |
---|
360 | |
---|
361 | def test_private_config_missing(self): |
---|
362 | """ |
---|
363 | a missing config with no default is an error |
---|
364 | """ |
---|
365 | basedir = u"test_node/test_private_config_missing" |
---|
366 | create_node_dir(basedir, "testing") |
---|
367 | config = read_config(basedir, "portnum") |
---|
368 | |
---|
369 | with self.assertRaises(MissingConfigEntry): |
---|
370 | config.get_or_create_private_config("foo") |
---|
371 | |
---|
372 | def test_private_config(self): |
---|
373 | basedir = u"test_node/test_private_config" |
---|
374 | privdir = os.path.join(basedir, "private") |
---|
375 | fileutil.make_dirs(privdir) |
---|
376 | f = open(os.path.join(privdir, 'already'), 'wt') |
---|
377 | f.write("secret") |
---|
378 | f.close() |
---|
379 | |
---|
380 | basedir = fileutil.abspath_expanduser_unicode(basedir) |
---|
381 | config = config_from_string(basedir, "", "") |
---|
382 | |
---|
383 | self.assertEqual(config.get_private_config("already"), "secret") |
---|
384 | self.assertEqual(config.get_private_config("not", "default"), "default") |
---|
385 | self.assertRaises(MissingConfigEntry, config.get_private_config, "not") |
---|
386 | value = config.get_or_create_private_config("new", "start") |
---|
387 | self.assertEqual(value, "start") |
---|
388 | self.assertEqual(config.get_private_config("new"), "start") |
---|
389 | counter = [] |
---|
390 | def make_newer(): |
---|
391 | counter.append("called") |
---|
392 | return "newer" |
---|
393 | value = config.get_or_create_private_config("newer", make_newer) |
---|
394 | self.assertEqual(len(counter), 1) |
---|
395 | self.assertEqual(value, "newer") |
---|
396 | self.assertEqual(config.get_private_config("newer"), "newer") |
---|
397 | |
---|
398 | value = config.get_or_create_private_config("newer", make_newer) |
---|
399 | self.assertEqual(len(counter), 1) # don't call unless necessary |
---|
400 | self.assertEqual(value, "newer") |
---|
401 | |
---|
402 | @skipIf(superuser, "cannot test as superuser with all permissions") |
---|
403 | def test_write_config_unwritable_file(self): |
---|
404 | """ |
---|
405 | Existing behavior merely logs any errors upon writing |
---|
406 | configuration files; this bad behavior should probably be |
---|
407 | fixed to do something better (like fail entirely). See #2905 |
---|
408 | """ |
---|
409 | basedir = "test_node/configdir" |
---|
410 | fileutil.make_dirs(basedir) |
---|
411 | config = config_from_string(basedir, "", "") |
---|
412 | with open(os.path.join(basedir, "bad"), "w") as f: |
---|
413 | f.write("bad") |
---|
414 | os.chmod(os.path.join(basedir, "bad"), 0o000) |
---|
415 | |
---|
416 | config.write_config_file("bad", "some value") |
---|
417 | |
---|
418 | errs = self.flushLoggedErrors(IOError) |
---|
419 | self.assertEqual(1, len(errs)) |
---|
420 | |
---|
421 | def test_timestamp(self): |
---|
422 | # this modified logger doesn't seem to get used during the tests, |
---|
423 | # probably because we don't modify the LogObserver that trial |
---|
424 | # installs (only the one that twistd installs). So manually exercise |
---|
425 | # it a little bit. |
---|
426 | t = formatTimeTahoeStyle("ignored", time.time()) |
---|
427 | self.failUnless("Z" in t) |
---|
428 | t2 = formatTimeTahoeStyle("ignored", int(time.time())) |
---|
429 | self.failUnless("Z" in t2) |
---|
430 | |
---|
431 | def test_secrets_dir(self): |
---|
432 | basedir = "test_node/test_secrets_dir" |
---|
433 | create_node_dir(basedir, "testing") |
---|
434 | self.failUnless(os.path.exists(os.path.join(basedir, "private"))) |
---|
435 | |
---|
436 | def test_secrets_dir_protected(self): |
---|
437 | if "win32" in sys.platform.lower() or "cygwin" in sys.platform.lower(): |
---|
438 | # We don't know how to test that unprivileged users can't read this |
---|
439 | # thing. (Also we don't know exactly how to set the permissions so |
---|
440 | # that unprivileged users can't read this thing.) |
---|
441 | raise unittest.SkipTest("We don't know how to set permissions on Windows.") |
---|
442 | basedir = "test_node/test_secrets_dir_protected" |
---|
443 | create_node_dir(basedir, "nothing to see here") |
---|
444 | |
---|
445 | # make sure private dir was created with correct modes |
---|
446 | privdir = os.path.join(basedir, "private") |
---|
447 | st = os.stat(privdir) |
---|
448 | bits = stat.S_IMODE(st[stat.ST_MODE]) |
---|
449 | self.failUnless(bits & 0o001 == 0, bits) |
---|
450 | |
---|
451 | @defer.inlineCallbacks |
---|
452 | def test_logdir_is_str(self): |
---|
453 | from twisted.internet import reactor |
---|
454 | |
---|
455 | basedir = FilePath(self.mktemp()) |
---|
456 | fixture = UseNode(None, None, basedir, "pb://introducer/furl", {}, reactor=reactor) |
---|
457 | fixture.setUp() |
---|
458 | self.addCleanup(fixture.cleanUp) |
---|
459 | |
---|
460 | ns = Namespace() |
---|
461 | ns.called = False |
---|
462 | def call_setLogDir(logdir): |
---|
463 | ns.called = True |
---|
464 | self.failUnless(isinstance(logdir, str), logdir) |
---|
465 | self.patch(foolscap.logging.log, 'setLogDir', call_setLogDir) |
---|
466 | |
---|
467 | yield fixture.create_node() |
---|
468 | self.failUnless(ns.called) |
---|
469 | |
---|
470 | def test_set_config_unescaped_furl_hash(self): |
---|
471 | """ |
---|
472 | ``_Config.set_config`` raises ``UnescapedHashError`` if the item being set |
---|
473 | is a furl and the value includes ``"#"`` and does not set the value. |
---|
474 | """ |
---|
475 | basedir = self.mktemp() |
---|
476 | new_config = config_from_string(basedir, "", "") |
---|
477 | with self.assertRaises(UnescapedHashError): |
---|
478 | new_config.set_config("foo", "bar.furl", "value#1") |
---|
479 | with self.assertRaises(MissingConfigEntry): |
---|
480 | new_config.get_config("foo", "bar.furl") |
---|
481 | |
---|
482 | def test_set_config_new_section(self): |
---|
483 | """ |
---|
484 | ``_Config.set_config`` can be called with the name of a section that does |
---|
485 | not already exist to create that section and set an item in it. |
---|
486 | """ |
---|
487 | basedir = self.mktemp() |
---|
488 | new_config = config_from_string(basedir, "", "", ValidConfiguration.everything()) |
---|
489 | new_config.set_config("foo", "bar", "value1") |
---|
490 | self.assertEqual( |
---|
491 | new_config.get_config("foo", "bar"), |
---|
492 | "value1" |
---|
493 | ) |
---|
494 | |
---|
495 | def test_set_config_replace(self): |
---|
496 | """ |
---|
497 | ``_Config.set_config`` can be called with a section and item that already |
---|
498 | exists to change an existing value to a new one. |
---|
499 | """ |
---|
500 | basedir = self.mktemp() |
---|
501 | new_config = config_from_string(basedir, "", "", ValidConfiguration.everything()) |
---|
502 | new_config.set_config("foo", "bar", "value1") |
---|
503 | new_config.set_config("foo", "bar", "value2") |
---|
504 | self.assertEqual( |
---|
505 | new_config.get_config("foo", "bar"), |
---|
506 | "value2" |
---|
507 | ) |
---|
508 | |
---|
509 | def test_set_config_write(self): |
---|
510 | """ |
---|
511 | ``_Config.set_config`` persists the configuration change so it can be |
---|
512 | re-loaded later. |
---|
513 | """ |
---|
514 | # Let our nonsense config through |
---|
515 | valid_config = ValidConfiguration.everything() |
---|
516 | basedir = FilePath(self.mktemp()) |
---|
517 | basedir.makedirs() |
---|
518 | cfg = basedir.child(b"tahoe.cfg") |
---|
519 | cfg.setContent(b"") |
---|
520 | new_config = read_config(basedir.path, "", [], valid_config) |
---|
521 | new_config.set_config("foo", "bar", "value1") |
---|
522 | loaded_config = read_config(basedir.path, "", [], valid_config) |
---|
523 | self.assertEqual( |
---|
524 | loaded_config.get_config("foo", "bar"), |
---|
525 | "value1", |
---|
526 | ) |
---|
527 | |
---|
528 | def test_set_config_rejects_invalid_config(self): |
---|
529 | """ |
---|
530 | ``_Config.set_config`` raises ``UnknownConfigError`` if the section or |
---|
531 | item is not recognized by the validation object and does not set the |
---|
532 | value. |
---|
533 | """ |
---|
534 | # Make everything invalid. |
---|
535 | valid_config = ValidConfiguration.nothing() |
---|
536 | new_config = config_from_string(self.mktemp(), "", "", valid_config) |
---|
537 | with self.assertRaises(UnknownConfigError): |
---|
538 | new_config.set_config("foo", "bar", "baz") |
---|
539 | with self.assertRaises(MissingConfigEntry): |
---|
540 | new_config.get_config("foo", "bar") |
---|
541 | |
---|
542 | |
---|
543 | def _stub_get_local_addresses_sync(): |
---|
544 | """ |
---|
545 | A function like ``allmydata.util.iputil.get_local_addresses_sync``. |
---|
546 | """ |
---|
547 | return ["LOCAL"] |
---|
548 | |
---|
549 | |
---|
550 | def _stub_allocate_tcp_port(): |
---|
551 | """ |
---|
552 | A function like ``allmydata.util.iputil.allocate_tcp_port``. |
---|
553 | """ |
---|
554 | return 999 |
---|
555 | |
---|
556 | def _stub_none(): |
---|
557 | """ |
---|
558 | A function like ``_stub_allocate_tcp`` or ``_stub_get_local_addresses_sync`` |
---|
559 | but that return an empty list since ``allmydata.node._tub_portlocation`` requires a |
---|
560 | callable for paramter 1 and 2 counting from 0. |
---|
561 | """ |
---|
562 | return [] |
---|
563 | |
---|
564 | |
---|
565 | class TestMissingPorts(unittest.TestCase): |
---|
566 | """ |
---|
567 | Test certain ``_tub_portlocation`` error cases for ports setup. |
---|
568 | """ |
---|
569 | def setUp(self): |
---|
570 | self.basedir = self.mktemp() |
---|
571 | create_node_dir(self.basedir, "testing") |
---|
572 | |
---|
573 | def test_listen_on_zero(self): |
---|
574 | """ |
---|
575 | ``_tub_portlocation`` raises ``PortAssignmentRequired`` called with a |
---|
576 | listen address including port 0 and no interface. |
---|
577 | """ |
---|
578 | config_data = ( |
---|
579 | "[node]\n" |
---|
580 | "tub.port = tcp:0\n" |
---|
581 | ) |
---|
582 | config = config_from_string(self.basedir, "portnum", config_data) |
---|
583 | with self.assertRaises(PortAssignmentRequired): |
---|
584 | _tub_portlocation(config, _stub_none, _stub_none) |
---|
585 | |
---|
586 | def test_listen_on_zero_with_host(self): |
---|
587 | """ |
---|
588 | ``_tub_portlocation`` raises ``PortAssignmentRequired`` called with a |
---|
589 | listen address including port 0 and an interface. |
---|
590 | """ |
---|
591 | config_data = ( |
---|
592 | "[node]\n" |
---|
593 | "tub.port = tcp:0:interface=127.0.0.1\n" |
---|
594 | ) |
---|
595 | config = config_from_string(self.basedir, "portnum", config_data) |
---|
596 | with self.assertRaises(PortAssignmentRequired): |
---|
597 | _tub_portlocation(config, _stub_none, _stub_none) |
---|
598 | |
---|
599 | def test_parsing_tcp(self): |
---|
600 | """ |
---|
601 | When ``tub.port`` is given and ``tub.location`` is **AUTO** the port |
---|
602 | number from ``tub.port`` is used as the port number for the value |
---|
603 | constructed for ``tub.location``. |
---|
604 | """ |
---|
605 | config_data = ( |
---|
606 | "[node]\n" |
---|
607 | "tub.port = tcp:777\n" |
---|
608 | "tub.location = AUTO\n" |
---|
609 | ) |
---|
610 | config = config_from_string(self.basedir, "portnum", config_data) |
---|
611 | |
---|
612 | tubport, tublocation = _tub_portlocation( |
---|
613 | config, |
---|
614 | _stub_get_local_addresses_sync, |
---|
615 | _stub_allocate_tcp_port, |
---|
616 | ) |
---|
617 | self.assertEqual(tubport, "tcp:777") |
---|
618 | self.assertEqual(tublocation, b"tcp:LOCAL:777") |
---|
619 | |
---|
620 | def test_parsing_defaults(self): |
---|
621 | """ |
---|
622 | parse empty config, check defaults |
---|
623 | """ |
---|
624 | config_data = ( |
---|
625 | "[node]\n" |
---|
626 | ) |
---|
627 | config = config_from_string(self.basedir, "portnum", config_data) |
---|
628 | |
---|
629 | tubport, tublocation = _tub_portlocation( |
---|
630 | config, |
---|
631 | _stub_get_local_addresses_sync, |
---|
632 | _stub_allocate_tcp_port, |
---|
633 | ) |
---|
634 | self.assertEqual(tubport, "tcp:999") |
---|
635 | self.assertEqual(tublocation, b"tcp:LOCAL:999") |
---|
636 | |
---|
637 | def test_parsing_location_complex(self): |
---|
638 | """ |
---|
639 | location with two options (including defaults) |
---|
640 | """ |
---|
641 | config_data = ( |
---|
642 | "[node]\n" |
---|
643 | "tub.location = tcp:HOST:888,AUTO\n" |
---|
644 | ) |
---|
645 | config = config_from_string(self.basedir, "portnum", config_data) |
---|
646 | |
---|
647 | tubport, tublocation = _tub_portlocation( |
---|
648 | config, |
---|
649 | _stub_get_local_addresses_sync, |
---|
650 | _stub_allocate_tcp_port, |
---|
651 | ) |
---|
652 | self.assertEqual(tubport, "tcp:999") |
---|
653 | self.assertEqual(tublocation, b"tcp:HOST:888,tcp:LOCAL:999") |
---|
654 | |
---|
655 | def test_parsing_all_disabled(self): |
---|
656 | """ |
---|
657 | parse config with both port + location disabled |
---|
658 | """ |
---|
659 | config_data = ( |
---|
660 | "[node]\n" |
---|
661 | "tub.port = disabled\n" |
---|
662 | "tub.location = disabled\n" |
---|
663 | ) |
---|
664 | config = config_from_string(self.basedir, "portnum", config_data) |
---|
665 | |
---|
666 | res = _tub_portlocation( |
---|
667 | config, |
---|
668 | _stub_get_local_addresses_sync, |
---|
669 | _stub_allocate_tcp_port, |
---|
670 | ) |
---|
671 | self.assertTrue(res is None) |
---|
672 | |
---|
673 | def test_empty_tub_port(self): |
---|
674 | """ |
---|
675 | port povided, but empty is an error |
---|
676 | """ |
---|
677 | config_data = ( |
---|
678 | "[node]\n" |
---|
679 | "tub.port = \n" |
---|
680 | ) |
---|
681 | config = config_from_string(self.basedir, "portnum", config_data) |
---|
682 | |
---|
683 | with self.assertRaises(ValueError) as ctx: |
---|
684 | _tub_portlocation( |
---|
685 | config, |
---|
686 | _stub_get_local_addresses_sync, |
---|
687 | _stub_allocate_tcp_port, |
---|
688 | ) |
---|
689 | self.assertIn( |
---|
690 | "tub.port must not be empty", |
---|
691 | str(ctx.exception) |
---|
692 | ) |
---|
693 | |
---|
694 | def test_empty_tub_location(self): |
---|
695 | """ |
---|
696 | location povided, but empty is an error |
---|
697 | """ |
---|
698 | config_data = ( |
---|
699 | "[node]\n" |
---|
700 | "tub.location = \n" |
---|
701 | ) |
---|
702 | config = config_from_string(self.basedir, "portnum", config_data) |
---|
703 | |
---|
704 | with self.assertRaises(ValueError) as ctx: |
---|
705 | _tub_portlocation( |
---|
706 | config, |
---|
707 | _stub_get_local_addresses_sync, |
---|
708 | _stub_allocate_tcp_port, |
---|
709 | ) |
---|
710 | self.assertIn( |
---|
711 | "tub.location must not be empty", |
---|
712 | str(ctx.exception) |
---|
713 | ) |
---|
714 | |
---|
715 | def test_disabled_port_not_tub(self): |
---|
716 | """ |
---|
717 | error to disable port but not location |
---|
718 | """ |
---|
719 | config_data = ( |
---|
720 | "[node]\n" |
---|
721 | "tub.port = disabled\n" |
---|
722 | "tub.location = not_disabled\n" |
---|
723 | ) |
---|
724 | config = config_from_string(self.basedir, "portnum", config_data) |
---|
725 | |
---|
726 | with self.assertRaises(ValueError) as ctx: |
---|
727 | _tub_portlocation( |
---|
728 | config, |
---|
729 | _stub_get_local_addresses_sync, |
---|
730 | _stub_allocate_tcp_port, |
---|
731 | ) |
---|
732 | self.assertIn( |
---|
733 | "tub.port is disabled, but not tub.location", |
---|
734 | str(ctx.exception) |
---|
735 | ) |
---|
736 | |
---|
737 | def test_disabled_tub_not_port(self): |
---|
738 | """ |
---|
739 | error to disable location but not port |
---|
740 | """ |
---|
741 | config_data = ( |
---|
742 | "[node]\n" |
---|
743 | "tub.port = not_disabled\n" |
---|
744 | "tub.location = disabled\n" |
---|
745 | ) |
---|
746 | config = config_from_string(self.basedir, "portnum", config_data) |
---|
747 | |
---|
748 | with self.assertRaises(ValueError) as ctx: |
---|
749 | _tub_portlocation( |
---|
750 | config, |
---|
751 | _stub_get_local_addresses_sync, |
---|
752 | _stub_allocate_tcp_port, |
---|
753 | ) |
---|
754 | self.assertIn( |
---|
755 | "tub.location is disabled, but not tub.port", |
---|
756 | str(ctx.exception) |
---|
757 | ) |
---|
758 | |
---|
759 | def test_tub_location_tcp(self): |
---|
760 | """ |
---|
761 | If ``reveal-IP-address`` is set to false and ``tub.location`` includes a |
---|
762 | **tcp** hint then ``_tub_portlocation`` raises `PrivacyError`` because |
---|
763 | TCP leaks IP addresses. |
---|
764 | """ |
---|
765 | config = config_from_string( |
---|
766 | "fake.port", |
---|
767 | "no-basedir", |
---|
768 | "[node]\nreveal-IP-address = false\ntub.location=tcp:hostname:1234\n", |
---|
769 | ) |
---|
770 | with self.assertRaises(PrivacyError) as ctx: |
---|
771 | _tub_portlocation( |
---|
772 | config, |
---|
773 | _stub_get_local_addresses_sync, |
---|
774 | _stub_allocate_tcp_port, |
---|
775 | ) |
---|
776 | self.assertEqual( |
---|
777 | str(ctx.exception), |
---|
778 | "tub.location includes tcp: hint", |
---|
779 | ) |
---|
780 | |
---|
781 | def test_tub_location_legacy_tcp(self): |
---|
782 | """ |
---|
783 | If ``reveal-IP-address`` is set to false and ``tub.location`` includes a |
---|
784 | "legacy" hint with no explicit type (which means it is a **tcp** hint) |
---|
785 | then the behavior is the same as for an explicit **tcp** hint. |
---|
786 | """ |
---|
787 | config = config_from_string( |
---|
788 | "fake.port", |
---|
789 | "no-basedir", |
---|
790 | "[node]\nreveal-IP-address = false\ntub.location=hostname:1234\n", |
---|
791 | ) |
---|
792 | |
---|
793 | with self.assertRaises(PrivacyError) as ctx: |
---|
794 | _tub_portlocation( |
---|
795 | config, |
---|
796 | _stub_get_local_addresses_sync, |
---|
797 | _stub_allocate_tcp_port, |
---|
798 | ) |
---|
799 | |
---|
800 | self.assertEqual( |
---|
801 | str(ctx.exception), |
---|
802 | "tub.location includes tcp: hint", |
---|
803 | ) |
---|
804 | |
---|
805 | |
---|
806 | BASE_CONFIG = """ |
---|
807 | [tor] |
---|
808 | enabled = false |
---|
809 | [i2p] |
---|
810 | enabled = false |
---|
811 | """ |
---|
812 | |
---|
813 | NOLISTEN = """ |
---|
814 | [node] |
---|
815 | tub.port = disabled |
---|
816 | tub.location = disabled |
---|
817 | """ |
---|
818 | |
---|
819 | DISABLE_STORAGE = """ |
---|
820 | [storage] |
---|
821 | enabled = false |
---|
822 | """ |
---|
823 | |
---|
824 | ENABLE_STORAGE = """ |
---|
825 | [storage] |
---|
826 | enabled = true |
---|
827 | """ |
---|
828 | |
---|
829 | ENABLE_HELPER = """ |
---|
830 | [helper] |
---|
831 | enabled = true |
---|
832 | """ |
---|
833 | |
---|
834 | class FakeTub(object): |
---|
835 | def __init__(self): |
---|
836 | self.tubID = base64.b32encode(b"foo") |
---|
837 | self.listening_ports = [] |
---|
838 | def setOption(self, name, value): pass |
---|
839 | def removeAllConnectionHintHandlers(self): pass |
---|
840 | def addConnectionHintHandler(self, hint_type, handler): pass |
---|
841 | def listenOn(self, what): |
---|
842 | self.listening_ports.append(what) |
---|
843 | def setLocation(self, location): pass |
---|
844 | def setServiceParent(self, parent): pass |
---|
845 | |
---|
846 | class Listeners(unittest.TestCase): |
---|
847 | |
---|
848 | # Randomly allocate a couple distinct port numbers to try out. The test |
---|
849 | # never actually binds these port numbers so we don't care if they're "in |
---|
850 | # use" on the system or not. We just want a couple distinct values we can |
---|
851 | # check expected results against. |
---|
852 | @given(ports=sets(elements=port_numbers(), min_size=2, max_size=2)) |
---|
853 | def test_multiple_ports(self, ports): |
---|
854 | """ |
---|
855 | When there are multiple listen addresses suggested by the ``tub.port`` and |
---|
856 | ``tub.location`` configuration, the node's *main* port listens on all |
---|
857 | of them. |
---|
858 | """ |
---|
859 | port1, port2 = iter(ports) |
---|
860 | port = ("tcp:%d:interface=127.0.0.1,tcp:%d:interface=127.0.0.1" % |
---|
861 | (port1, port2)) |
---|
862 | location = "tcp:localhost:%d,tcp:localhost:%d" % (port1, port2) |
---|
863 | t = FakeTub() |
---|
864 | tub_listen_on(None, None, t, port, location) |
---|
865 | self.assertEqual(t.listening_ports, |
---|
866 | ["tcp:%d:interface=127.0.0.1" % port1, |
---|
867 | "tcp:%d:interface=127.0.0.1" % port2]) |
---|
868 | |
---|
869 | def test_tor_i2p_listeners(self): |
---|
870 | """ |
---|
871 | When configured to listen on an "i2p" or "tor" address, ``tub_listen_on`` |
---|
872 | tells the Tub to listen on endpoints supplied by the given Tor and I2P |
---|
873 | providers. |
---|
874 | """ |
---|
875 | t = FakeTub() |
---|
876 | |
---|
877 | i2p_listener = object() |
---|
878 | i2p_provider = ConstantAddresses(i2p_listener) |
---|
879 | tor_listener = object() |
---|
880 | tor_provider = ConstantAddresses(tor_listener) |
---|
881 | |
---|
882 | tub_listen_on( |
---|
883 | i2p_provider, |
---|
884 | tor_provider, |
---|
885 | t, |
---|
886 | "listen:i2p,listen:tor", |
---|
887 | "tcp:example.org:1234", |
---|
888 | ) |
---|
889 | self.assertEqual( |
---|
890 | t.listening_ports, |
---|
891 | [i2p_listener, tor_listener], |
---|
892 | ) |
---|
893 | |
---|
894 | |
---|
895 | class ClientNotListening(unittest.TestCase): |
---|
896 | |
---|
897 | @defer.inlineCallbacks |
---|
898 | def test_disabled(self): |
---|
899 | basedir = "test_node/test_disabled" |
---|
900 | create_node_dir(basedir, "testing") |
---|
901 | f = open(os.path.join(basedir, 'tahoe.cfg'), 'wt') |
---|
902 | f.write(BASE_CONFIG) |
---|
903 | f.write(NOLISTEN) |
---|
904 | f.write(DISABLE_STORAGE) |
---|
905 | f.close() |
---|
906 | n = yield client.create_client(basedir) |
---|
907 | self.assertEqual(n.tub.getListeners(), []) |
---|
908 | |
---|
909 | @defer.inlineCallbacks |
---|
910 | def test_disabled_but_storage(self): |
---|
911 | basedir = "test_node/test_disabled_but_storage" |
---|
912 | create_node_dir(basedir, "testing") |
---|
913 | f = open(os.path.join(basedir, 'tahoe.cfg'), 'wt') |
---|
914 | f.write(BASE_CONFIG) |
---|
915 | f.write(NOLISTEN) |
---|
916 | f.write(ENABLE_STORAGE) |
---|
917 | f.close() |
---|
918 | with self.assertRaises(ValueError) as ctx: |
---|
919 | yield client.create_client(basedir) |
---|
920 | self.assertIn( |
---|
921 | "storage is enabled, but tub is not listening", |
---|
922 | str(ctx.exception), |
---|
923 | ) |
---|
924 | |
---|
925 | @defer.inlineCallbacks |
---|
926 | def test_disabled_but_helper(self): |
---|
927 | basedir = "test_node/test_disabled_but_helper" |
---|
928 | create_node_dir(basedir, "testing") |
---|
929 | f = open(os.path.join(basedir, 'tahoe.cfg'), 'wt') |
---|
930 | f.write(BASE_CONFIG) |
---|
931 | f.write(NOLISTEN) |
---|
932 | f.write(DISABLE_STORAGE) |
---|
933 | f.write(ENABLE_HELPER) |
---|
934 | f.close() |
---|
935 | with self.assertRaises(ValueError) as ctx: |
---|
936 | yield client.create_client(basedir) |
---|
937 | self.assertIn( |
---|
938 | "helper is enabled, but tub is not listening", |
---|
939 | str(ctx.exception), |
---|
940 | ) |
---|
941 | |
---|
942 | class IntroducerNotListening(unittest.TestCase): |
---|
943 | |
---|
944 | @defer.inlineCallbacks |
---|
945 | def test_port_none_introducer(self): |
---|
946 | basedir = "test_node/test_port_none_introducer" |
---|
947 | create_node_dir(basedir, "testing") |
---|
948 | with open(os.path.join(basedir, 'tahoe.cfg'), 'wt') as f: |
---|
949 | f.write("[node]\n") |
---|
950 | f.write("tub.port = disabled\n") |
---|
951 | f.write("tub.location = disabled\n") |
---|
952 | with self.assertRaises(ValueError) as ctx: |
---|
953 | yield create_introducer(basedir) |
---|
954 | self.assertIn( |
---|
955 | "we are Introducer, but tub is not listening", |
---|
956 | str(ctx.exception), |
---|
957 | ) |
---|
958 | |
---|
959 | class Configuration(unittest.TestCase): |
---|
960 | |
---|
961 | def setUp(self): |
---|
962 | self.basedir = self.mktemp() |
---|
963 | fileutil.make_dirs(self.basedir) |
---|
964 | |
---|
965 | def test_read_invalid_config(self): |
---|
966 | with open(os.path.join(self.basedir, 'tahoe.cfg'), 'w') as f: |
---|
967 | f.write( |
---|
968 | '[invalid section]\n' |
---|
969 | 'foo = bar\n' |
---|
970 | ) |
---|
971 | with self.assertRaises(UnknownConfigError) as ctx: |
---|
972 | read_config( |
---|
973 | self.basedir, |
---|
974 | "client.port", |
---|
975 | ) |
---|
976 | |
---|
977 | self.assertIn( |
---|
978 | "invalid section", |
---|
979 | str(ctx.exception), |
---|
980 | ) |
---|
981 | |
---|
982 | @defer.inlineCallbacks |
---|
983 | def test_create_client_invalid_config(self): |
---|
984 | with open(os.path.join(self.basedir, 'tahoe.cfg'), 'w') as f: |
---|
985 | f.write( |
---|
986 | '[invalid section]\n' |
---|
987 | 'foo = bar\n' |
---|
988 | ) |
---|
989 | with self.assertRaises(UnknownConfigError) as ctx: |
---|
990 | yield client.create_client(self.basedir) |
---|
991 | |
---|
992 | self.assertIn( |
---|
993 | "invalid section", |
---|
994 | str(ctx.exception), |
---|
995 | ) |
---|
996 | |
---|
997 | |
---|
998 | |
---|
999 | class CreateDefaultConnectionHandlersTests(unittest.TestCase): |
---|
1000 | """ |
---|
1001 | Tests for create_default_connection_handlers(). |
---|
1002 | """ |
---|
1003 | |
---|
1004 | def test_tcp_disabled(self): |
---|
1005 | """ |
---|
1006 | If tcp is set to disabled, no TCP handler is set. |
---|
1007 | """ |
---|
1008 | config = config_from_string("", "", dedent(""" |
---|
1009 | [connections] |
---|
1010 | tcp = disabled |
---|
1011 | """)) |
---|
1012 | default_handlers = create_default_connection_handlers( |
---|
1013 | config, |
---|
1014 | {}, |
---|
1015 | ) |
---|
1016 | self.assertIs(default_handlers["tcp"], None) |
---|