[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]
[tor-commits] [stem/master] Add a descriptor type_annotation method
commit 1fa75186657a6b5f1f839314ed666ccf618abc7a
Author: Damian Johnson <atagar@xxxxxxxxxxxxxx>
Date: Sun Nov 11 17:11:38 2018 -0800
Add a descriptor type_annotation method
Interesting feature request from irl...
https://trac.torproject.org/projects/tor/ticket/28397
We can't knowledgeably provide a version number (those come from CollecTor,
for instance to specify which bridge scrubbing specification metrics used). But
we can certainly provide a valid annotation.
---
docs/change_log.rst | 1 +
stem/descriptor/__init__.py | 59 ++++++++++++++++++----
stem/descriptor/extrainfo_descriptor.py | 4 ++
stem/descriptor/hidden_service_descriptor.py | 2 +
stem/descriptor/microdescriptor.py | 2 +
stem/descriptor/networkstatus.py | 17 +++++++
stem/descriptor/server_descriptor.py | 4 ++
stem/descriptor/tordnsel.py | 2 +
test/unit/descriptor/extrainfo_descriptor.py | 4 ++
test/unit/descriptor/hidden_service_descriptor.py | 1 +
test/unit/descriptor/microdescriptor.py | 2 +
.../descriptor/networkstatus/bridge_document.py | 1 +
test/unit/descriptor/networkstatus/document_v2.py | 1 +
test/unit/descriptor/networkstatus/document_v3.py | 4 ++
.../descriptor/networkstatus/key_certificate.py | 1 +
test/unit/descriptor/server_descriptor.py | 2 +
test/unit/descriptor/tordnsel.py | 2 +
17 files changed, 99 insertions(+), 10 deletions(-)
diff --git a/docs/change_log.rst b/docs/change_log.rst
index 01f3f2a4..f8db0b52 100644
--- a/docs/change_log.rst
+++ b/docs/change_log.rst
@@ -51,6 +51,7 @@ The following are only available within Stem's `git repository
* **Descriptors**
+ * Added :func:`~stem.descriptor.Descriptor.type_annotation` method (:trac:`28397`)
* DescriptorDownloader crashed if **use_mirrors** is set (:trac:`28393`)
* Don't download from Serge, a bridge authority that frequently timeout
diff --git a/stem/descriptor/__init__.py b/stem/descriptor/__init__.py
index a9860140..13c69f11 100644
--- a/stem/descriptor/__init__.py
+++ b/stem/descriptor/__init__.py
@@ -125,6 +125,24 @@ DocumentHandler = stem.util.enum.UppercaseEnum(
)
+class TypeAnnotation(collections.namedtuple('TypeAnnotation', ['name', 'major_version', 'minor_version'])):
+ """
+ `Tor metrics type annotation
+ <https://metrics.torproject.org/collector.html#relay-descriptors>`_. The
+ string representation is the header annotation, for example "@type
+ server-descriptor 1.0".
+
+ .. versionadded:: 1.8.0
+
+ :var str name: name of the descriptor type
+ :var int major_version: major version number
+ :var int minor_version: minor version number
+ """
+
+ def __str__(self):
+ return '@type %s %s.%s' % (self.name, self.major_version, self.minor_version)
+
+
class SigningKey(collections.namedtuple('SigningKey', ['private', 'public', 'public_digest'])):
"""
Key used by relays to sign their server and extrainfo descriptors.
@@ -333,30 +351,30 @@ def _parse_metrics_file(descriptor_type, major_version, minor_version, descripto
# Parses descriptor files from metrics, yielding individual descriptors. This
# throws a TypeError if the descriptor_type or version isn't recognized.
- if descriptor_type == 'server-descriptor' and major_version == 1:
+ if descriptor_type == stem.descriptor.server_descriptor.RelayDescriptor.TYPE_ANNOTATION_NAME and major_version == 1:
for desc in stem.descriptor.server_descriptor._parse_file(descriptor_file, is_bridge = False, validate = validate, **kwargs):
yield desc
- elif descriptor_type == 'bridge-server-descriptor' and major_version == 1:
+ elif descriptor_type == stem.descriptor.server_descriptor.BridgeDescriptor.TYPE_ANNOTATION_NAME and major_version == 1:
for desc in stem.descriptor.server_descriptor._parse_file(descriptor_file, is_bridge = True, validate = validate, **kwargs):
yield desc
- elif descriptor_type == 'extra-info' and major_version == 1:
+ elif descriptor_type == stem.descriptor.extrainfo_descriptor.RelayExtraInfoDescriptor.TYPE_ANNOTATION_NAME and major_version == 1:
for desc in stem.descriptor.extrainfo_descriptor._parse_file(descriptor_file, is_bridge = False, validate = validate, **kwargs):
yield desc
- elif descriptor_type == 'microdescriptor' and major_version == 1:
+ elif descriptor_type == stem.descriptor.microdescriptor.Microdescriptor.TYPE_ANNOTATION_NAME and major_version == 1:
for desc in stem.descriptor.microdescriptor._parse_file(descriptor_file, validate = validate, **kwargs):
yield desc
- elif descriptor_type == 'bridge-extra-info' and major_version == 1:
+ elif descriptor_type == stem.descriptor.extrainfo_descriptor.BridgeExtraInfoDescriptor.TYPE_ANNOTATION_NAME and major_version == 1:
# version 1.1 introduced a 'transport' field...
# https://trac.torproject.org/6257
for desc in stem.descriptor.extrainfo_descriptor._parse_file(descriptor_file, is_bridge = True, validate = validate, **kwargs):
yield desc
- elif descriptor_type == 'network-status-2' and major_version == 1:
+ elif descriptor_type == stem.descriptor.networkstatus.NetworkStatusDocumentV2.TYPE_ANNOTATION_NAME and major_version == 1:
document_type = stem.descriptor.networkstatus.NetworkStatusDocumentV2
for desc in stem.descriptor.networkstatus._parse_file(descriptor_file, document_type, validate = validate, document_handler = document_handler, **kwargs):
yield desc
- elif descriptor_type == 'dir-key-certificate-3' and major_version == 1:
+ elif descriptor_type == stem.descriptor.networkstatus.KeyCertificate.TYPE_ANNOTATION_NAME and major_version == 1:
for desc in stem.descriptor.networkstatus._parse_file_key_certs(descriptor_file, validate = validate, **kwargs):
yield desc
elif descriptor_type in ('network-status-consensus-3', 'network-status-vote-3') and major_version == 1:
@@ -369,17 +387,17 @@ def _parse_metrics_file(descriptor_type, major_version, minor_version, descripto
for desc in stem.descriptor.networkstatus._parse_file(descriptor_file, document_type, is_microdescriptor = True, validate = validate, document_handler = document_handler, **kwargs):
yield desc
- elif descriptor_type == 'bridge-network-status' and major_version == 1:
+ elif descriptor_type == stem.descriptor.networkstatus.BridgeNetworkStatusDocument.TYPE_ANNOTATION_NAME and major_version == 1:
document_type = stem.descriptor.networkstatus.BridgeNetworkStatusDocument
for desc in stem.descriptor.networkstatus._parse_file(descriptor_file, document_type, validate = validate, document_handler = document_handler, **kwargs):
yield desc
- elif descriptor_type == 'tordnsel' and major_version == 1:
+ elif descriptor_type == stem.descriptor.tordnsel.TorDNSEL.TYPE_ANNOTATION_NAME and major_version == 1:
document_type = stem.descriptor.tordnsel.TorDNSEL
for desc in stem.descriptor.tordnsel._parse_file(descriptor_file, validate = validate, **kwargs):
yield desc
- elif descriptor_type == 'hidden-service-descriptor' and major_version == 1:
+ elif descriptor_type == stem.descriptor.hidden_service_descriptor.HiddenServiceDescriptor.TYPE_ANNOTATION_NAME and major_version == 1:
document_type = stem.descriptor.hidden_service_descriptor.HiddenServiceDescriptor
for desc in stem.descriptor.hidden_service_descriptor._parse_file(descriptor_file, validate = validate, **kwargs):
@@ -617,6 +635,7 @@ class Descriptor(object):
ATTRIBUTES = {} # mapping of 'attribute' => (default_value, parsing_function)
PARSER_FOR_LINE = {} # line keyword to its associated parsing function
+ TYPE_ANNOTATION_NAME = None
def __init__(self, contents, lazy_load = False):
self._path = None
@@ -675,6 +694,26 @@ class Descriptor(object):
return cls(cls.content(attr, exclude, sign), validate = validate)
+ def type_annotation(self):
+ """
+ Provides the `Tor metrics annotation
+ <https://metrics.torproject.org/collector.html#relay-descriptors>`_ of this
+ descriptor type. For example, "@type server-descriptor 1.0" for server
+ descriptors.
+
+ Please note that the version number component is specific to CollecTor,
+ and for the moment hardcode as 1.0. This may change in the future.
+
+ .. versionadded:: 1.8.0
+
+ :returns: :class:`~stem.descriptor.TypeAnnotation` with our type information
+ """
+
+ if self.TYPE_ANNOTATION_NAME is not None:
+ return TypeAnnotation(self.TYPE_ANNOTATION_NAME, 1, 0)
+ else:
+ raise NotImplementedError('%s does not have a @type annotation' % type(self).__name__)
+
def get_path(self):
"""
Provides the absolute path that we loaded this descriptor from.
diff --git a/stem/descriptor/extrainfo_descriptor.py b/stem/descriptor/extrainfo_descriptor.py
index 485b3063..8d8894d1 100644
--- a/stem/descriptor/extrainfo_descriptor.py
+++ b/stem/descriptor/extrainfo_descriptor.py
@@ -903,6 +903,8 @@ class RelayExtraInfoDescriptor(ExtraInfoDescriptor):
Added the ed25519_certificate and ed25519_signature attributes.
"""
+ TYPE_ANNOTATION_NAME = 'extra-info'
+
ATTRIBUTES = dict(ExtraInfoDescriptor.ATTRIBUTES, **{
'ed25519_certificate': (None, _parse_identity_ed25519_line),
'ed25519_signature': (None, _parse_router_sig_ed25519_line),
@@ -963,6 +965,8 @@ class BridgeExtraInfoDescriptor(ExtraInfoDescriptor):
Added the ed25519_certificate_hash and router_digest_sha256 attributes.
"""
+ TYPE_ANNOTATION_NAME = 'bridge-extra-info'
+
ATTRIBUTES = dict(ExtraInfoDescriptor.ATTRIBUTES, **{
'ed25519_certificate_hash': (None, _parse_master_key_ed25519_line),
'router_digest_sha256': (None, _parse_router_digest_sha256_line),
diff --git a/stem/descriptor/hidden_service_descriptor.py b/stem/descriptor/hidden_service_descriptor.py
index 83618dd8..a7cc0e3d 100644
--- a/stem/descriptor/hidden_service_descriptor.py
+++ b/stem/descriptor/hidden_service_descriptor.py
@@ -213,6 +213,8 @@ class HiddenServiceDescriptor(Descriptor):
Added the **skip_crypto_validation** constructor argument.
"""
+ TYPE_ANNOTATION_NAME = 'hidden-service-descriptor'
+
ATTRIBUTES = {
'descriptor_id': (None, _parse_rendezvous_service_descriptor_line),
'version': (None, _parse_version_line),
diff --git a/stem/descriptor/microdescriptor.py b/stem/descriptor/microdescriptor.py
index 731e8453..74a01071 100644
--- a/stem/descriptor/microdescriptor.py
+++ b/stem/descriptor/microdescriptor.py
@@ -233,6 +233,8 @@ class Microdescriptor(Descriptor):
Added the protocols attribute.
"""
+ TYPE_ANNOTATION_NAME = 'microdescriptor'
+
ATTRIBUTES = {
'onion_key': (None, _parse_onion_key_line),
'ntor_onion_key': (None, _parse_ntor_onion_key_line),
diff --git a/stem/descriptor/networkstatus.py b/stem/descriptor/networkstatus.py
index 57098e81..63483c94 100644
--- a/stem/descriptor/networkstatus.py
+++ b/stem/descriptor/networkstatus.py
@@ -65,6 +65,7 @@ import stem.version
from stem.descriptor import (
PGP_BLOCK_END,
Descriptor,
+ TypeAnnotation,
DocumentHandler,
_descriptor_content,
_descriptor_components,
@@ -442,6 +443,8 @@ class NetworkStatusDocumentV2(NetworkStatusDocument):
a default value, others are left as **None** if undefined
"""
+ TYPE_ANNOTATION_NAME = 'network-status-2'
+
ATTRIBUTES = {
'version': (None, _parse_network_status_version_line),
'hostname': (None, _parse_dir_source_line),
@@ -1088,6 +1091,16 @@ class NetworkStatusDocumentV3(NetworkStatusDocument):
self.routers = dict((desc.fingerprint, desc) for desc in router_iter)
self._footer(document_file, validate)
+ def type_annotation(self):
+ if not self.is_microdescriptor:
+ return TypeAnnotation('network-status-consensus-3' if not self.is_vote else 'network-status-vote-3', 1, 0)
+ else:
+ # Directory authorities do not issue a 'microdescriptor consensus' vote,
+ # so unlike the above there isn't a 'network-status-microdesc-vote-3'
+ # counterpart here.
+
+ return TypeAnnotation('network-status-microdesc-consensus-3', 1, 0)
+
def validate_signatures(self, key_certs):
"""
Validates we're properly signed by the signing certificates.
@@ -1613,6 +1626,8 @@ class KeyCertificate(Descriptor):
**\*** mandatory attribute
"""
+ TYPE_ANNOTATION_NAME = 'dir-key-certificate-3'
+
ATTRIBUTES = {
'version': (None, _parse_dir_key_certificate_version_line),
'address': (None, _parse_dir_address_line),
@@ -1766,6 +1781,8 @@ class BridgeNetworkStatusDocument(NetworkStatusDocument):
:var datetime published: time when the document was published
"""
+ TYPE_ANNOTATION_NAME = 'bridge-network-status'
+
def __init__(self, raw_content, validate = False):
super(BridgeNetworkStatusDocument, self).__init__(raw_content)
diff --git a/stem/descriptor/server_descriptor.py b/stem/descriptor/server_descriptor.py
index d212459e..0d12f875 100644
--- a/stem/descriptor/server_descriptor.py
+++ b/stem/descriptor/server_descriptor.py
@@ -793,6 +793,8 @@ class RelayDescriptor(ServerDescriptor):
Added the **skip_crypto_validation** constructor argument.
"""
+ TYPE_ANNOTATION_NAME = 'server-descriptor'
+
ATTRIBUTES = dict(ServerDescriptor.ATTRIBUTES, **{
'certificate': (None, _parse_identity_ed25519_line),
'ed25519_certificate': (None, _parse_identity_ed25519_line),
@@ -997,6 +999,8 @@ class BridgeDescriptor(ServerDescriptor):
descriptors).
"""
+ TYPE_ANNOTATION_NAME = 'bridge-server-descriptor'
+
ATTRIBUTES = dict(ServerDescriptor.ATTRIBUTES, **{
'ed25519_certificate_hash': (None, _parse_master_key_ed25519_for_hash_line),
'router_digest_sha256': (None, _parse_router_digest_sha256_line),
diff --git a/stem/descriptor/tordnsel.py b/stem/descriptor/tordnsel.py
index b573b79c..c4aba296 100644
--- a/stem/descriptor/tordnsel.py
+++ b/stem/descriptor/tordnsel.py
@@ -60,6 +60,8 @@ class TorDNSEL(Descriptor):
a default value, others are left as **None** if undefined
"""
+ TYPE_ANNOTATION_NAME = 'tordnsel'
+
def __init__(self, raw_contents, validate):
super(TorDNSEL, self).__init__(raw_contents)
raw_contents = stem.util.str_tools._to_unicode(raw_contents)
diff --git a/test/unit/descriptor/extrainfo_descriptor.py b/test/unit/descriptor/extrainfo_descriptor.py
index d649040a..f3252d4e 100644
--- a/test/unit/descriptor/extrainfo_descriptor.py
+++ b/test/unit/descriptor/extrainfo_descriptor.py
@@ -72,6 +72,8 @@ k0d2aofcVbHr4fPQOSST0LXDrhFl5Fqo5um296zpJGvRUeO6S44U/EfJAGShtqWw
dir_write_values_start = [0, 0, 0, 227328, 349184, 382976, 738304]
self.assertEqual(dir_write_values_start, desc.dir_write_history_values[:7])
+ self.assertEqual('@type extra-info 1.0', str(desc.type_annotation()))
+
def test_metrics_bridge_descriptor(self):
"""
Parses and checks our results against an extrainfo bridge descriptor from
@@ -133,6 +135,8 @@ k0d2aofcVbHr4fPQOSST0LXDrhFl5Fqo5um296zpJGvRUeO6S44U/EfJAGShtqWw
self.assertEqual({}, desc.dir_v2_responses_unknown)
self.assertEqual({}, desc.dir_v2_responses_unknown)
+ self.assertEqual('@type bridge-extra-info 1.0', str(desc.type_annotation()))
+
@test.require.cryptography
def test_descriptor_signing(self):
RelayExtraInfoDescriptor.create(sign = True)
diff --git a/test/unit/descriptor/hidden_service_descriptor.py b/test/unit/descriptor/hidden_service_descriptor.py
index 23067c62..437366a2 100644
--- a/test/unit/descriptor/hidden_service_descriptor.py
+++ b/test/unit/descriptor/hidden_service_descriptor.py
@@ -424,6 +424,7 @@ class TestHiddenServiceDescriptor(unittest.TestCase):
self.assertEqual([], desc.introduction_points_auth)
self.assertEqual(b'', desc.introduction_points_content)
self.assertEqual([], desc.introduction_points())
+ self.assertEqual('@type hidden-service-descriptor 1.0', str(desc.type_annotation()))
def test_unrecognized_line(self):
"""
diff --git a/test/unit/descriptor/microdescriptor.py b/test/unit/descriptor/microdescriptor.py
index 5f245619..bb4b91a2 100644
--- a/test/unit/descriptor/microdescriptor.py
+++ b/test/unit/descriptor/microdescriptor.py
@@ -74,6 +74,8 @@ class TestMicrodescriptor(unittest.TestCase):
self.assertEqual({b'@last-listed': b'2013-02-24 00:18:36'}, router.get_annotations())
self.assertEqual([b'@last-listed 2013-02-24 00:18:36'], router.get_annotation_lines())
+ self.assertEqual('@type microdescriptor 1.0', str(router.type_annotation()))
+
def test_minimal_microdescriptor(self):
"""
Basic sanity check that we can parse a microdescriptor with minimal
diff --git a/test/unit/descriptor/networkstatus/bridge_document.py b/test/unit/descriptor/networkstatus/bridge_document.py
index d027c94d..97e3e178 100644
--- a/test/unit/descriptor/networkstatus/bridge_document.py
+++ b/test/unit/descriptor/networkstatus/bridge_document.py
@@ -51,6 +51,7 @@ class TestBridgeNetworkStatusDocument(unittest.TestCase):
self.assertEqual(datetime.datetime(2012, 6, 1, 4, 7, 4), document.published)
self.assertEqual({}, document.routers)
self.assertEqual([], document.get_unrecognized_lines())
+ self.assertEqual('@type bridge-network-status 1.0', str(document.type_annotation()))
def test_document(self):
"""
diff --git a/test/unit/descriptor/networkstatus/document_v2.py b/test/unit/descriptor/networkstatus/document_v2.py
index a02ea7a5..7dcc235b 100644
--- a/test/unit/descriptor/networkstatus/document_v2.py
+++ b/test/unit/descriptor/networkstatus/document_v2.py
@@ -56,6 +56,7 @@ TpQQk3nNQF8z6UIvdlvP+DnJV4izWVkQEZgUZgIVM0E=
self.assertEqual([], document.get_unrecognized_lines())
self.assertEqual(3, len(document.routers))
+ self.assertEqual('@type network-status-2 1.0', str(document.type_annotation()))
router1 = document.routers['719BE45DE224B607C53707D0E2143E2D423E74CF']
self.assertEqual('moria2', router1.nickname)
diff --git a/test/unit/descriptor/networkstatus/document_v3.py b/test/unit/descriptor/networkstatus/document_v3.py
index 18d9088a..e4258dab 100644
--- a/test/unit/descriptor/networkstatus/document_v3.py
+++ b/test/unit/descriptor/networkstatus/document_v3.py
@@ -121,6 +121,7 @@ ci356fosgLiM1sVqCUkNdA==
self.assertEqual([], document.consensus_methods)
self.assertEqual(None, document.published)
self.assertEqual([], document.get_unrecognized_lines())
+ self.assertEqual('@type network-status-consensus-3 1.0', str(document.type_annotation()))
router = document.routers['348225F83C854796B2DD6364E65CB189B33BD696']
self.assertEqual('test002r', router.nickname)
@@ -254,6 +255,7 @@ DnN5aFtYKiTc19qIC7Nmo+afPdDEf0MlJvEOP5EWl3w=
self.assertEqual('178.218.213.229', router.address)
self.assertEqual(80, router.or_port)
self.assertEqual(None, router.dir_port)
+ self.assertEqual('@type network-status-vote-3 1.0', str(document.type_annotation()))
authority = document.directory_authorities[0]
self.assertEqual(1, len(document.directory_authorities))
@@ -1143,6 +1145,8 @@ DnN5aFtYKiTc19qIC7Nmo+afPdDEf0MlJvEOP5EWl3w=
self.assertTrue(entry1 in document.routers.values())
self.assertTrue(entry2 in document.routers.values())
+ self.assertEqual('@type network-status-microdesc-consensus-3 1.0', str(document.type_annotation()))
+
# try with an invalid RouterStatusEntry
entry3 = RouterStatusEntryMicroV3(RouterStatusEntryMicroV3.content({'r': 'ugabuga'}), False)
diff --git a/test/unit/descriptor/networkstatus/key_certificate.py b/test/unit/descriptor/networkstatus/key_certificate.py
index 0da8da11..985610fb 100644
--- a/test/unit/descriptor/networkstatus/key_certificate.py
+++ b/test/unit/descriptor/networkstatus/key_certificate.py
@@ -89,6 +89,7 @@ PPc3r7zKlL/jEGHwz+C7kE88HIvkVnKLLn//40b6HxitHSOCkZ1vtp8YyXae6xnU
self.assertEqual(expected_signing_key, cert.signing_key)
self.assertEqual(expected_crosscert, cert.crosscert)
self.assertEqual(expected_key_cert, cert.certification)
+ self.assertEqual('@type dir-key-certificate-3 1.0', str(cert.type_annotation()))
self.assertEqual([], cert.get_unrecognized_lines())
def test_metrics_certificate(self):
diff --git a/test/unit/descriptor/server_descriptor.py b/test/unit/descriptor/server_descriptor.py
index 6e6d27d4..ec1af553 100644
--- a/test/unit/descriptor/server_descriptor.py
+++ b/test/unit/descriptor/server_descriptor.py
@@ -155,6 +155,7 @@ Qlx9HNCqCY877ztFRC624ja2ql6A2hBcuoYMbkHjcQ4=
self.assertEqual([], desc.get_unrecognized_lines())
self.assertEqual('2C7B27BEAB04B4E2459D89CA6D5CD1CC5F95A689', desc.digest())
+ self.assertEqual('@type server-descriptor 1.0', str(desc.type_annotation()))
self.assertEqual(['2'], desc.hidden_service_dir) # obsolete field
def test_metrics_descriptor_multiple(self):
@@ -436,6 +437,7 @@ Qlx9HNCqCY877ztFRC624ja2ql6A2hBcuoYMbkHjcQ4=
self.assertFalse(hasattr(desc, 'ed25519_certificate'))
self.assertEqual('lgIuiAJCoXPRwWoHgG4ZAoKtmrv47aPr4AsbmESj8AA', desc.ed25519_certificate_hash)
self.assertEqual('OB/fqLD8lYmjti09R+xXH/D4S2qlizxdZqtudnsunxE', desc.router_digest_sha256)
+ self.assertEqual('@type bridge-server-descriptor 1.0', str(desc.type_annotation()))
self.assertEqual([], desc.get_unrecognized_lines())
def test_cr_in_contact_line(self):
diff --git a/test/unit/descriptor/tordnsel.py b/test/unit/descriptor/tordnsel.py
index f6cf07ff..fbc17442 100644
--- a/test/unit/descriptor/tordnsel.py
+++ b/test/unit/descriptor/tordnsel.py
@@ -86,3 +86,5 @@ class TestTorDNSELDescriptor(unittest.TestCase):
self.assertTrue(is_valid_fingerprint(desc.fingerprint))
self.assertEqual('030B22437D99B2DB2908B747B6962EAD13AB4038', desc.fingerprint)
self.assertEqual(0, len(desc.exit_addresses))
+
+ self.assertEqual('@type tordnsel 1.0', str(desc.type_annotation()))
_______________________________________________
tor-commits mailing list
tor-commits@xxxxxxxxxxxxxxxxxxxx
https://lists.torproject.org/cgi-bin/mailman/listinfo/tor-commits