def test_router_status_entry(self): """ Tests creation of router status entries. """ desc_without_fingerprint = RelayDescriptor.create() exc_msg = 'Server descriptor lacks a fingerprint. This is an optional field, but required to make a router status entry.' self.assertRaisesRegexp( ValueError, exc_msg, desc_without_fingerprint.make_router_status_entry) desc = RelayDescriptor.create({ 'router': 'caerSidi 71.35.133.197 9001 0 0', 'published': '2012-02-29 04:03:19', 'fingerprint': '4F0C 867D F0EF 6816 0568 C826 838F 482C EA7C FE44', 'or-address': ['71.35.133.197:9001', '[12ab:2e19:3bcf::02:9970]:9001'], 'onion-key': '\n-----BEGIN RSA PUBLIC KEY-----%s-----END RSA PUBLIC KEY-----' % stem.descriptor.CRYPTO_BLOB, 'signing-key': '\n-----BEGIN RSA PUBLIC KEY-----%s-----END RSA PUBLIC KEY-----' % stem.descriptor.CRYPTO_BLOB, }).make_router_status_entry() self.assertEqual( stem.descriptor.router_status_entry.RouterStatusEntryV3, type(desc)) self.assertEqual('caerSidi', desc.nickname) self.assertEqual('4F0C867DF0EF68160568C826838F482CEA7CFE44', desc.fingerprint) self.assertEqual(datetime.datetime(2012, 2, 29, 4, 3, 19), desc.published) self.assertEqual('71.35.133.197', desc.address) self.assertEqual(9001, desc.or_port) self.assertEqual(None, desc.dir_port) self.assertEqual(['Fast', 'Named', 'Running', 'Stable', 'Valid'], desc.flags) self.assertEqual(None, desc.version) self.assertEqual(None, desc.version_line) self.assertEqual([('71.35.133.197', 9001, False), ('12ab:2e19:3bcf::02:9970', 9001, True)], desc.or_addresses) self.assertEqual(None, desc.identifier_type) self.assertEqual(None, desc.identifier) self.assertEqual('4F0069BF91C04581B7C3CA9272E2D3228D4EA571', desc.digest) self.assertEqual(153600, desc.bandwidth) self.assertEqual(None, desc.measured) self.assertEqual(False, desc.is_unmeasured) self.assertEqual([], desc.unrecognized_bandwidth_entries) self.assertEqual(stem.exit_policy.MicroExitPolicy('reject 1-65535'), desc.exit_policy) self.assertEqual([], desc.microdescriptor_hashes)
def test_outdated_relays(self, downloader_mock, stdout_mock): downloader_mock().get_server_descriptors.return_value = [ RelayDescriptor.create({'platform': 'node-Tor 0.2.3.0 on Linux x86_64'}), RelayDescriptor.create({'platform': 'node-Tor 0.1.0 on Linux x86_64'}), RelayDescriptor.create({'opt': 'contact Random Person [email protected]', 'platform': 'node-Tor 0.2.3.0 on Linux x86_64'}), RelayDescriptor.create({'opt': 'contact Sambuddha Basu', 'platform': 'node-Tor 0.1.0 on Linux x86_64'}), ] exec_documentation_example('outdated_relays.py') self.assertCountEqual(OUTDATED_RELAYS_OUTPUT.splitlines(), stdout_mock.getvalue().splitlines())
def test_with_bridge_distribution(self): """ Include a preferred method of bridge distribution. """ desc = RelayDescriptor.create({'bridge-distribution-request': 'email'}) self.assertEqual(BridgeDistribution.EMAIL, desc.bridge_distribution)
def test_ntor_onion_key(self): """ Checks a 'ntor-onion-key' line. """ desc = RelayDescriptor.create({'ntor-onion-key': 'Od2Sj3UXFyDjwESLXk6fhatqW9z/oBL/vAKJ+tbDqUU='}) self.assertEqual('Od2Sj3UXFyDjwESLXk6fhatqW9z/oBL/vAKJ+tbDqUU=', desc.ntor_onion_key)
def test_ipv6_policy(self): """ Checks a 'ipv6-policy' line. """ desc = RelayDescriptor.create({'ipv6-policy': 'accept 22-23,53,80,110'}) self.assertEqual(stem.exit_policy.MicroExitPolicy('accept 22-23,53,80,110'), desc.exit_policy_v6)
def test_unrecognized_line(self): """ Includes unrecognized content in the descriptor. """ desc = RelayDescriptor.create({'pepperjack': 'is oh so tasty!'}) self.assertEqual(['pepperjack is oh so tasty!'], desc.get_unrecognized_lines())
def test_invalid_attributes(self): """ Attempts to make a csv with attributes that don't exist. """ desc = RelayDescriptor.create() self.assertRaises(ValueError, export_csv, desc, ('nickname', 'blarg!'))
def test_with_opt(self): """ Includes an 'opt <keyword> <value>' entry. """ desc = RelayDescriptor.create({'opt': 'contact www.atagar.com/contact/'}) self.assertEqual(b'www.atagar.com/contact/', desc.contact)
def test_minimal_descriptor(self): """ Exports a single minimal tor server descriptor. """ if stem.prereq._is_python_26(): self.skipTest('(header added in python 2.7)') return desc = RelayDescriptor.create({ 'router': 'caerSidi 71.35.133.197 9001 0 0', 'published': '2012-03-01 17:15:27', }) desc_csv = export_csv(desc, included_fields=('nickname', 'address', 'published'), header=False) expected = 'caerSidi,71.35.133.197,2012-03-01 17:15:27\n' self.assertEqual(expected, desc_csv) desc_csv = export_csv(desc, included_fields=('nickname', 'address', 'published'), header=True) expected = 'nickname,address,published\n' + expected self.assertEqual(expected, desc_csv)
def test_protocols(self): """ Checks a 'proto' line. """ desc = RelayDescriptor.create({'proto': 'Cons=1 Desc=1 DirCache=1 HSDir=1 HSIntro=3 HSRend=1 Link=1-4 LinkAuth=1 Microdesc=1 Relay=1-2'}) self.assertEqual({'Cons': [1], 'Desc': [1], 'DirCache': [1], 'HSDir': [1], 'HSIntro': [3], 'HSRend': [1], 'Link': [1, 2, 3, 4], 'LinkAuth': [1], 'Microdesc': [1], 'Relay': [1, 2]}, desc.protocols)
def test_extrainfo_sha256_digest(self): """ Extrainfo descriptor line with both a hex and base64 encoded sha256 digest. """ desc = RelayDescriptor.create({'extra-info-digest': '03272BF7C68484AFBDA508DAE3734D809E4A5BC4 DWMz1AEdqPlcroubwx3lPEoGbT+oX7S2BH653sPIqfI'}) self.assertEqual('03272BF7C68484AFBDA508DAE3734D809E4A5BC4', desc.extra_info_digest) self.assertEqual('DWMz1AEdqPlcroubwx3lPEoGbT+oX7S2BH653sPIqfI', desc.extra_info_sha256_digest)
def test_multiple_descriptor_types(self): """ Attempts to make a csv with multiple descriptor types. """ self.assertRaises( ValueError, export_csv, (RelayDescriptor.create(), BridgeDescriptor.create()))
def test_mirror_mirror_on_the_wall_4(self, get_desc_mock, stdout_mock): get_desc_mock.return_value = iter([RelayDescriptor.create({ 'router': 'caerSidi 71.35.133.197 9001 0 0', 'fingerprint': '2C3C 4662 5698 B6D6 7DF3 2BC1 918A D3EE 1F99 06B1', }, exit_policy = ExitPolicy('accept *:*'), validate = False)]) exec_documentation_example('collector_reading.py') self.assertEqual(' caerSidi (2C3C46625698B6D67DF32BC1918AD3EE1F9906B1)\n', stdout_mock.getvalue())
def test_read_history_empty(self): """ Parses a read-history with an empty value. """ desc = RelayDescriptor.create({'opt read-history': '2005-12-17 01:23:11 (900 s) '}) self.assertEqual(datetime.datetime(2005, 12, 17, 1, 23, 11), desc.read_history_end) self.assertEqual(900, desc.read_history_interval) self.assertEqual([], desc.read_history_values)
def test_platform_for_node_tor(self): """ Parse a platform line belonging to a node-Tor relay. """ desc = RelayDescriptor.create({'platform': 'node-Tor 0.1.0 on Linux x86_64'}) self.assertEqual(b'node-Tor 0.1.0 on Linux x86_64', desc.platform) self.assertEqual(stem.version.Version('0.1.0'), desc.tor_version) self.assertEqual('Linux x86_64', desc.operating_system)
def test_minimal_relay_descriptor(self): """ Basic sanity check that we can parse a relay server descriptor with minimal attributes. """ desc = RelayDescriptor.create({'router': 'caerSidi 71.35.133.197 9001 0 0'}) self.assertEqual('caerSidi', desc.nickname) self.assertEqual('71.35.133.197', desc.address) self.assertEqual(None, desc.fingerprint)
def test_published_leap_year(self): """ Constructs with a published entry for a leap year, and when the date is invalid. """ expect_invalid_attr(self, {'published': '2011-02-29 04:03:19'}, 'published') desc = RelayDescriptor.create({'published': '2012-02-29 04:03:19'}) self.assertEqual(datetime.datetime(2012, 2, 29, 4, 3, 19), desc.published)
def test_mirror_mirror_on_the_wall_4(self, reader_mock, stdout_mock): reader = reader_mock().__enter__() reader.__iter__.return_value = iter([ RelayDescriptor.create( {'router': 'caerSidi 71.35.133.197 9001 0 0'}) ]) exec_documentation_example('past_descriptors.py') self.assertEqual('found relay caerSidi (None)\n', stdout_mock.getvalue())
def test_outdated_relays(self, stdout_mock, downloader_mock): downloader_mock().get_server_descriptors.return_value = [ RelayDescriptor.create( {'platform': 'node-Tor 0.2.3.0 on Linux x86_64'}), RelayDescriptor.create( {'platform': 'node-Tor 0.1.0 on Linux x86_64'}), RelayDescriptor.create({ 'opt': 'contact Random Person [email protected]', 'platform': 'node-Tor 0.2.3.0 on Linux x86_64' }), RelayDescriptor.create({ 'opt': 'contact Sambuddha Basu', 'platform': 'node-Tor 0.1.0 on Linux x86_64' }), ] import outdated_relays self.assertEqual(EXPECTED_OUTDATED_RELAYS, stdout_mock.getvalue())
def test_read_with_parse_file(self, stdout_mock, parse_file_mock): parse_file_mock.return_value = [ RelayDescriptor.create({ 'fingerprint': '4F0C 867D F0EF 6816 0568 C826 838F 482C EA7C FE44' }) ] import read_with_parse_file self.assertEqual('4F0C867DF0EF68160568C826838F482CEA7CFE44\n', stdout_mock.getvalue())
def test_file_output(self): """ Basic test for the export_csv_file() function, checking that it provides the same output as export_csv(). """ desc = RelayDescriptor.create() desc_csv = export_csv(desc) csv_buffer = StringIO() export_csv_file(csv_buffer, desc) self.assertEqual(desc_csv, csv_buffer.getvalue())
def test_router_status_entry(self): """ Tests creation of router status entries. """ desc_without_fingerprint = RelayDescriptor.create() exc_msg = 'Server descriptor lacks a fingerprint. This is an optional field, but required to make a router status entry.' self.assertRaisesRegexp(ValueError, exc_msg, desc_without_fingerprint.make_router_status_entry) desc = RelayDescriptor.create({ 'router': 'caerSidi 71.35.133.197 9001 0 0', 'published': '2012-02-29 04:03:19', 'fingerprint': '4F0C 867D F0EF 6816 0568 C826 838F 482C EA7C FE44', 'or-address': ['71.35.133.197:9001', '[12ab:2e19:3bcf::02:9970]:9001'], 'onion-key': '\n-----BEGIN RSA PUBLIC KEY-----%s-----END RSA PUBLIC KEY-----' % stem.descriptor.CRYPTO_BLOB, 'signing-key': '\n-----BEGIN RSA PUBLIC KEY-----%s-----END RSA PUBLIC KEY-----' % stem.descriptor.CRYPTO_BLOB, }).make_router_status_entry() self.assertEqual(stem.descriptor.router_status_entry.RouterStatusEntryV3, type(desc)) self.assertEqual('caerSidi', desc.nickname) self.assertEqual('4F0C867DF0EF68160568C826838F482CEA7CFE44', desc.fingerprint) self.assertEqual(datetime.datetime(2012, 2, 29, 4, 3, 19), desc.published) self.assertEqual('71.35.133.197', desc.address) self.assertEqual(9001, desc.or_port) self.assertEqual(None, desc.dir_port) self.assertEqual(['Fast', 'Named', 'Running', 'Stable', 'Valid'], desc.flags) self.assertEqual(None, desc.version) self.assertEqual(None, desc.version_line) self.assertEqual([('71.35.133.197', 9001, False), ('12ab:2e19:3bcf::02:9970', 9001, True)], desc.or_addresses) self.assertEqual(None, desc.identifier_type) self.assertEqual(None, desc.identifier) self.assertEqual('4F0069BF91C04581B7C3CA9272E2D3228D4EA571', desc.digest) self.assertEqual(153600, desc.bandwidth) self.assertEqual(None, desc.measured) self.assertEqual(False, desc.is_unmeasured) self.assertEqual([], desc.unrecognized_bandwidth_entries) self.assertEqual(stem.exit_policy.MicroExitPolicy('reject 1-65535'), desc.exit_policy) self.assertEqual([], desc.microdescriptor_hashes)
def test_mirror_mirror_on_the_wall_5(self, downloader_mock, stdout_mock): def tutorial_example(): from stem.descriptor.remote import DescriptorDownloader from stem.util import str_tools # provides a mapping of observed bandwidth to the relay nicknames def get_bw_to_relay(): bw_to_relay = {} downloader = DescriptorDownloader() try: for desc in downloader.get_server_descriptors().run(): if desc.exit_policy.is_exiting_allowed(): bw_to_relay.setdefault(desc.observed_bandwidth, []).append(desc.nickname) except Exception as exc: print('Unable to retrieve the server descriptors: %s' % exc) return bw_to_relay # prints the top fifteen relays bw_to_relay = get_bw_to_relay() count = 1 for bw_value in sorted(bw_to_relay.keys(), reverse=True): for nickname in bw_to_relay[bw_value]: print('%i. %s (%s/s)' % (count, nickname, str_tools.size_label(bw_value, 2))) count += 1 if count > 15: return exit_descriptor = RelayDescriptor.content({ 'router': 'speedyexit 149.255.97.109 9001 0 0' }).replace(b'reject *:*', b'accept *:*') exit_descriptor = RelayDescriptor(exit_descriptor) downloader_mock().get_server_descriptors().run.return_value = [ exit_descriptor, RelayDescriptor.create(), # non-exit exit_descriptor, exit_descriptor, ] tutorial_example() self.assertEqual(MIRROR_MIRROR_OUTPUT, stdout_mock.getvalue())
def test_multiple_descriptors(self): """ Exports multiple descriptors, making sure that we get them back in the same order. """ nicknames = ('relay1', 'relay3', 'relay2', 'caerSidi', 'zeus') descriptors = [] for nickname in nicknames: router_line = '%s 71.35.133.197 9001 0 0' % nickname descriptors.append(RelayDescriptor.create({'router': router_line})) expected = '\n'.join(nicknames) + '\n' self.assertEqual(expected, export_csv(descriptors, included_fields = ('nickname',), header = False))
def test_saving_and_loading_descriptors(self): server_desc = RelayDescriptor.create( {'router': 'caerSidi 71.35.133.197 9001 0 0'}) with patch('stem.descriptor.remote.get_server_descriptors', _download_of(server_desc)): try: import saving_and_loading_descriptors with open('/tmp/descriptor_dump') as descriptor_file: self.assertTrue(descriptor_file.read().startswith( 'router caerSidi 71.35.133.197')) finally: if os.path.exists('/tmp/descriptor_dump'): os.remove('/tmp/descriptor_dump')
def test_excludes_private_attr(self): """ Checks that the default attributes for our csv output doesn't include private fields. """ if stem.prereq._is_python_26(): self.skipTest('(header added in python 2.7)') return desc = RelayDescriptor.create() desc_csv = export_csv(desc) self.assertTrue(',signature' in desc_csv) self.assertFalse(',_digest' in desc_csv) self.assertFalse(',_annotation_lines' in desc_csv)
def test_collector_reading(self, stdout_mock, server_desc_mock): server_desc_mock.return_value = [ RelayDescriptor.create( { 'router': 'caerSidi 71.35.133.197 9001 0 0', 'fingerprint': '4F0C 867D F0EF 6816 0568 C826 838F 482C EA7C FE44', }, exit_policy=ExitPolicy('accept *:*')), ] import collector_reading self.assertEqual(EXPECTED_COLLECTOR_READING, stdout_mock.getvalue())
def test_check_digests(self): import check_digests as module fingerprint = 'A7569A83B5706AB1B1A9CB52EFF7D2D32E4553EB' extrainfo_desc = RelayExtraInfoDescriptor.create() server_desc = RelayDescriptor.create( {'extra-info-digest': extrainfo_desc.digest()}, sign=True) encoded_digest = base64.b64encode( binascii.unhexlify(server_desc.digest())).rstrip(b'=') consensus_desc = RouterStatusEntryV3.create({ 'r': 'caerSidi p1aag7VwarGxqctS7/fS0y5FU+s %s 2012-08-06 11:19:31 71.35.150.29 9001 0' % encoded_digest.decode('utf-8'), }) bad_consensus_desc = RouterStatusEntryV3.create({ 'r': 'caerSidi p1aag7VwarGxqctS7/fS0y5FU+s oQZFLYe9e4A7bOkWKR7TaNxb0JE 2012-08-06 11:19:31 71.35.150.29 9001 0', }) with patch('stem.descriptor.remote.get_server_descriptors', _download_of(server_desc)): with patch('stem.descriptor.remote.get_extrainfo_descriptors', _download_of(extrainfo_desc)): # correctly signed descriptors with patch('stem.descriptor.remote.get_consensus', _download_of(consensus_desc)): with patch('sys.stdout', new_callable=io.StringIO) as stdout_mock: module.validate_relay(fingerprint) self.assertEqual(EXPECTED_CHECK_DIGESTS_OK, stdout_mock.getvalue()) # incorrect server descriptor digest with patch('stem.descriptor.remote.get_consensus', _download_of(bad_consensus_desc)): with patch('sys.stdout', new_callable=io.StringIO) as stdout_mock: module.validate_relay(fingerprint) self.assertEqual( EXPECTED_CHECK_DIGESTS_BAD % server_desc.digest(), stdout_mock.getvalue())
def test_tor_descriptors(self, stdout_mock, downloader_mock): exit_descriptor = RelayDescriptor.content({ 'router': 'speedyexit 149.255.97.109 9001 0 0' }).replace(b'reject *:*', b'accept *:*') exit_descriptor = RelayDescriptor(exit_descriptor) downloader_mock().get_server_descriptors().run.return_value = [ exit_descriptor, RelayDescriptor.create(), # non-exit exit_descriptor, exit_descriptor, ] import tor_descriptors self.assertEqual(EXPECTED_TOR_DESCRIPTORS, stdout_mock.getvalue())
def test_mirror_mirror_on_the_wall_5(self, downloader_mock, stdout_mock): def tutorial_example(): from stem.descriptor.remote import DescriptorDownloader from stem.util import str_tools # provides a mapping of observed bandwidth to the relay nicknames def get_bw_to_relay(): bw_to_relay = {} downloader = DescriptorDownloader() try: for desc in downloader.get_server_descriptors().run(): if desc.exit_policy.is_exiting_allowed(): bw_to_relay.setdefault(desc.observed_bandwidth, []).append(desc.nickname) except Exception as exc: print('Unable to retrieve the server descriptors: %s' % exc) return bw_to_relay # prints the top fifteen relays bw_to_relay = get_bw_to_relay() count = 1 for bw_value in sorted(bw_to_relay.keys(), reverse = True): for nickname in bw_to_relay[bw_value]: print('%i. %s (%s/s)' % (count, nickname, str_tools.size_label(bw_value, 2))) count += 1 if count > 15: return exit_descriptor = RelayDescriptor.content({'router': 'speedyexit 149.255.97.109 9001 0 0'}).replace(b'reject *:*', b'accept *:*') exit_descriptor = RelayDescriptor(exit_descriptor) downloader_mock().get_server_descriptors().run.return_value = [ exit_descriptor, RelayDescriptor.create(), # non-exit exit_descriptor, exit_descriptor, ] tutorial_example() self.assertEqual(MIRROR_MIRROR_OUTPUT, stdout_mock.getvalue())
def test_multiple_descriptors(self): """ Exports multiple descriptors, making sure that we get them back in the same order. """ nicknames = ('relay1', 'relay3', 'relay2', 'caerSidi', 'zeus') descriptors = [] for nickname in nicknames: router_line = '%s 71.35.133.197 9001 0 0' % nickname descriptors.append(RelayDescriptor.create({'router': router_line})) expected = '\n'.join(nicknames) + '\n' self.assertEqual( expected, export_csv(descriptors, included_fields=('nickname', ), header=False))
def test_minimal_descriptor(self): """ Exports a single minimal tor server descriptor. """ if stem.prereq._is_python_26(): self.skipTest('(header added in python 2.7)') return desc = RelayDescriptor.create({ 'router': 'caerSidi 71.35.133.197 9001 0 0', 'published': '2012-03-01 17:15:27', }) desc_csv = export_csv(desc, included_fields = ('nickname', 'address', 'published'), header = False) expected = 'caerSidi,71.35.133.197,2012-03-01 17:15:27\n' self.assertEqual(expected, desc_csv) desc_csv = export_csv(desc, included_fields = ('nickname', 'address', 'published'), header = True) expected = 'nickname,address,published\n' + expected self.assertEqual(expected, desc_csv)
def test_read_and_write_history(self): """ Parses a read-history and write-history entry. This is now a deprecated field for relay server descriptors but is still found in archives and extra-info descriptors. """ for field in ('read-history', 'write-history'): value = '2005-12-16 18:00:48 (900 s) 81,8848,8927,8927,83,8848' desc = RelayDescriptor.create({'opt %s' % field: value}) if field == 'read-history': attr = (desc.read_history_end, desc.read_history_interval, desc.read_history_values) else: attr = (desc.write_history_end, desc.write_history_interval, desc.write_history_values) expected_end = datetime.datetime(2005, 12, 16, 18, 0, 48) expected_values = [81, 8848, 8927, 8927, 83, 8848] self.assertEqual(expected_end, attr[0]) self.assertEqual(900, attr[1]) self.assertEqual(expected_values, attr[2])
def test_download_descriptor(self): import download_descriptor with patch('sys.stdout', new_callable=io.StringIO) as stdout_mock: with patch('stem.descriptor.remote.get_server_descriptors', _download_of([])): download_descriptor.main(['--help']) self.assertTrue(stdout_mock.getvalue().startswith( "Downloads a descriptor through Tor's ORPort")) with patch('sys.stdout', new_callable=io.StringIO) as stdout_mock: download_descriptor.main(['--type', 'kaboom']) self.assertEqual(EXPECTED_DOWNLOAD_DESCRIPTOR_UNKNOWN_TYPE, stdout_mock.getvalue()) server_desc = RelayDescriptor.create( {'router': 'caerSidi 71.35.133.197 9001 0 0'}) with patch('sys.stdout', new_callable=io.StringIO) as stdout_mock: with patch('stem.descriptor.remote.get_server_descriptors', _download_of(server_desc)): download_descriptor.main(['--dirport', '1.2.3.4:443']) self.assertTrue(stdout_mock.getvalue().startswith( EXPECTED_DOWNLOAD_DESCRIPTOR_PREFIX))
def test_descriptor_signing(self): RelayDescriptor.create(sign = True) self.assertRaisesRegexp(NotImplementedError, 'Signing of BridgeDescriptor not implemented', BridgeDescriptor.create, sign = True)
def test_mirror_mirror_on_the_wall_4(self, reader_mock, stdout_mock): reader = reader_mock().__enter__() reader.__iter__.return_value = iter([RelayDescriptor.create({'router': 'caerSidi 71.35.133.197 9001 0 0'})]) exec_documentation_example('past_descriptors.py') self.assertEqual('found relay caerSidi (None)\n', stdout_mock.getvalue())
def test_multiple_descriptor_types(self): """ Attempts to make a csv with multiple descriptor types. """ self.assertRaises(ValueError, export_csv, (RelayDescriptor.create(), BridgeDescriptor.create()))
from stem.descriptor.server_descriptor import RelayDescriptor # prints 'caerSidi (71.35.133.197:9001)' desc = RelayDescriptor.create() print("%s (%s:%s)" % (desc.nickname, desc.address, desc.or_port)) # prints 'demo (127.0.0.1:80)' desc = RelayDescriptor.create({'router': 'demo 127.0.0.1 80 0 0'}) print("%s (%s:%s)" % (desc.nickname, desc.address, desc.or_port))