[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]
[tor-commits] [stem/master] Parse exit list entry.
commit 4631228400e0cca43d8c7ba514c40cbcbf2bda34
Author: Arlo Breault <arlolra@xxxxxxxxx>
Date: Mon Aug 19 22:49:40 2013 -0700
Parse exit list entry.
Published by DNSEL or TorBEL to indicate what ip address exit relay X
had at timestamp Y.
See #8255
---
stem/descriptor/__init__.py | 9 ++-
stem/descriptor/tordnsel.py | 120 ++++++++++++++++++++++++++++++++++++++
test/settings.cfg | 1 +
test/unit/descriptor/tordnsel.py | 80 +++++++++++++++++++++++++
4 files changed, 209 insertions(+), 1 deletion(-)
diff --git a/stem/descriptor/__init__.py b/stem/descriptor/__init__.py
index cd1f42a..b4fc54a 100644
--- a/stem/descriptor/__init__.py
+++ b/stem/descriptor/__init__.py
@@ -46,6 +46,7 @@ __all__ = [
"microdescriptor",
"networkstatus",
"router_status_entry",
+ "tordnsel",
"parse_file",
"Descriptor",
]
@@ -115,7 +116,7 @@ def parse_file(descriptor_file, descriptor_type = None, validate = True, documen
bridge-extra-info 1.1 :class:`~stem.descriptor.extrainfo_descriptor.BridgeExtraInfoDescriptor`
torperf 1.0 **unsupported**
bridge-pool-assignment 1.0 **unsupported**
- tordnsel 1.0 **unsupported**
+ tordnsel 1.0 :class:`~stem.descriptor.tordnsel.TorDNSEL`
========================================= =====
If you're using **python 3** then beware that the open() function defaults to
@@ -255,6 +256,11 @@ def _parse_metrics_file(descriptor_type, major_version, minor_version, descripto
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:
+ document_type = stem.descriptor.tordnsel.TorDNSEL
+
+ for desc in stem.descriptor.tordnsel._parse_file(descriptor_file, validate = validate, **kwargs):
+ yield desc
else:
raise TypeError("Unrecognized metrics descriptor format. type: '%s', version: '%i.%i'" % (descriptor_type, major_version, minor_version))
@@ -542,3 +548,4 @@ import stem.descriptor.server_descriptor
import stem.descriptor.extrainfo_descriptor
import stem.descriptor.networkstatus
import stem.descriptor.microdescriptor
+import stem.descriptor.tordnsel
diff --git a/stem/descriptor/tordnsel.py b/stem/descriptor/tordnsel.py
new file mode 100644
index 0000000..ddeef2e
--- /dev/null
+++ b/stem/descriptor/tordnsel.py
@@ -0,0 +1,120 @@
+# Copyright 2012-2013, Damian Johnson
+# See LICENSE for licensing information
+
+"""
+Parsing for TorDNSEL files.
+"""
+
+import datetime
+
+import stem.util.connection
+import stem.util.str_tools
+import stem.util.tor_tools
+
+from stem.descriptor import (
+ Descriptor,
+ _read_until_keywords,
+ _get_descriptor_components,
+)
+
+
+def _parse_file(tordnsel_file, validate = True, **kwargs):
+ """
+ Iterates over a tordnsel file.
+
+ :returns: iterator for :class:`~stem.descriptor.tordnsel.TorDNSEL`
+ instances in the file
+
+ :raises:
+ * **ValueError** if the contents is malformed and validate is **True**
+ * **IOError** if the file can't be read
+ """
+
+ # skip content prior to the first ExitNode
+ _read_until_keywords("ExitNode", tordnsel_file, skip = True)
+
+ while True:
+ contents = _read_until_keywords("ExitAddress", tordnsel_file)
+ contents += _read_until_keywords("ExitNode", tordnsel_file)
+ if contents:
+ yield TorDNSEL(bytes.join(b"", contents), validate, **kwargs)
+ else:
+ break # done parsing file
+
+
+class TorDNSEL(Descriptor):
+ """
+ TorDNSEL descriptor (`exitlist specification
+ <https://www.torproject.org/tordnsel/exitlist-spec.txt>`_)
+
+ :var str fingerprint: **\*** authority's fingerprint
+ :var datetime published: **\*** time in UTC when this descriptor was made
+ :var datetime last_status: **\*** time in UTC when the relay was seen in a v2 network status
+ :var list exit_addresses: **\*** list of (str address, datetime date) tuples consisting of the found IPv4 exit address and the time
+
+ **\*** attribute is either required when we're parsed with validation or has
+ a default value, others are left as **None** if undefined
+ """
+
+ def __init__(self, raw_contents, validate):
+ super(TorDNSEL, self).__init__(raw_contents)
+ raw_contents = stem.util.str_tools._to_unicode(raw_contents)
+ entries = _get_descriptor_components(raw_contents, validate)
+
+ self.fingerprint = None
+ self.published = None
+ self.last_status = None
+ self.exit_addresses = []
+
+ self._parse(entries, validate)
+
+ def _parse(self, entries, validate):
+
+ for keyword, values in entries.items():
+ value, block_content = values[0]
+
+ if validate and block_content:
+ raise ValueError("Unexpected block content: %s" % block_content)
+
+ if keyword == "ExitNode":
+ if validate and not stem.util.tor_tools.is_valid_fingerprint(value):
+ raise ValueError("Tor relay fingerprints consist of forty hex digits: %s" % value)
+ self.fingerprint = value
+
+ elif keyword == "Published":
+ try:
+ self.published = datetime.datetime.strptime(value, "%Y-%m-%d %H:%M:%S")
+ except ValueError:
+ if validate:
+ raise ValueError("Published time wasn't parsable: %s" % value)
+
+ elif keyword == "LastStatus":
+ try:
+ self.last_status = datetime.datetime.strptime(value, "%Y-%m-%d %H:%M:%S")
+ except ValueError:
+ if validate:
+ raise ValueError("LastStatus time wasn't parsable: %s" % value)
+
+ elif keyword == "ExitAddress":
+ for value, block_content in values:
+
+ if validate and block_content:
+ raise ValueError("Unexpected block content: %s" % block_content)
+
+ address, date = value.split(" ", 1)
+
+ if validate and not stem.util.connection.is_valid_ipv4_address(address):
+ raise ValueError("ExitAddress isn't a valid IPv4 address: %s" % address)
+ try:
+ date = datetime.datetime.strptime(date, "%Y-%m-%d %H:%M:%S")
+ except ValueError:
+ if validate:
+ raise ValueError("ExitAddress found time wasn't parsable: %s" % value)
+ else:
+ continue
+
+ self.exit_addresses.append((address, date))
+
+ else:
+ if validate:
+ raise ValueError("Saw a keyword that wasn't expected.")
diff --git a/test/settings.cfg b/test/settings.cfg
index 80aaf9a..b97c57f 100644
--- a/test/settings.cfg
+++ b/test/settings.cfg
@@ -161,6 +161,7 @@ test.unit_tests
|test.unit.descriptor.extrainfo_descriptor.TestExtraInfoDescriptor
|test.unit.descriptor.microdescriptor.TestMicrodescriptor
|test.unit.descriptor.router_status_entry.TestRouterStatusEntry
+|test.unit.descriptor.tordnsel.TestTorDNSELDescriptor
|test.unit.descriptor.networkstatus.directory_authority.TestDirectoryAuthority
|test.unit.descriptor.networkstatus.key_certificate.TestKeyCertificate
|test.unit.descriptor.networkstatus.document_v2.TestNetworkStatusDocument
diff --git a/test/unit/descriptor/tordnsel.py b/test/unit/descriptor/tordnsel.py
new file mode 100644
index 0000000..4861b38
--- /dev/null
+++ b/test/unit/descriptor/tordnsel.py
@@ -0,0 +1,80 @@
+"""
+Unit tests for stem.descriptor.tordnsel.
+"""
+
+import io
+import unittest
+import datetime
+
+from stem.util.tor_tools import is_valid_fingerprint
+from stem.descriptor.tordnsel import TorDNSEL, _parse_file
+
+
+class TestTorDNSELDescriptor(unittest.TestCase):
+ def test_parse_file(self):
+ """
+ Try parsing a document via the _parse_file() function.
+ """
+ desc_text = """
+@type tordnsel 1.0
+Downloaded 2013-08-19 04:02:03
+ExitNode 003A71137D959748C8157C4A76ECA639CEF5E33E
+Published 2013-08-19 02:13:53
+LastStatus 2013-08-19 03:02:47
+ExitAddress 66.223.170.168 2013-08-19 03:18:51
+ExitNode 00FF300624FECA7F40515C8D854EE925332580D6
+Published 2013-08-18 07:02:14
+LastStatus 2013-08-18 09:02:58
+ExitAddress 82.252.181.153 2013-08-18 08:03:01
+ExitAddress 82.252.181.154 2013-08-18 08:03:02
+ExitAddress 82.252.181.155 2013-08-18 08:03:03
+ExitNode 030B22437D99B2DB2908B747B6962EAD13AB4039
+Published 2013-08-18 12:44:20
+LastStatus 2013-08-18 13:02:57
+ExitAddress 46.10.211.205 2013-08-18 13:18:48
+"""
+
+ # parse file and assert values
+ descriptors = list(_parse_file(io.BytesIO(desc_text)))
+ self.assertEqual(3, len(descriptors))
+ self.assertTrue(isinstance(descriptors[0], TorDNSEL))
+ desc = descriptors[1]
+ self.assertTrue(is_valid_fingerprint(desc.fingerprint))
+ self.assertEqual("00FF300624FECA7F40515C8D854EE925332580D6", desc.fingerprint)
+ self.assertEqual(datetime.datetime(2013, 8, 18, 7, 2, 14), desc.published)
+ self.assertEqual(datetime.datetime(2013, 8, 18, 9, 2, 58), desc.last_status)
+ self.assertEqual(3, len(desc.exit_addresses))
+ exit = desc.exit_addresses[0]
+ self.assertEqual("82.252.181.153", exit[0])
+ self.assertEqual(datetime.datetime(2013, 8, 18, 8, 3, 1), exit[1])
+
+ # block content raises value error
+ extra = "ExtraContent goes here\n"
+ descriptors = _parse_file(io.BytesIO(desc_text + extra))
+ self.assertRaises(ValueError, list, descriptors)
+
+ # malformed fingerprint raises value errors
+ extra = "ExitNode 030B22437D99B2DB2908B747B6"
+ self.assertRaises(ValueError, list, _parse_file(io.BytesIO(desc_text + extra)))
+
+ # malformed date raises value errors
+ extra = """
+ExitNode 030B22437D99B2DB2908B747B6962EAD13AB4038
+Published Today!
+LastStatus 2013-08-18 13:02:57
+ExitAddress 46.10.211.205 2013-08-18 13:18:48
+"""
+ self.assertRaises(ValueError, list, _parse_file(io.BytesIO(desc_text + extra)))
+
+ # skip exit address if malformed date and validate is False
+ extra = """
+@type tordnsel 1.0
+ExitNode 030B22437D99B2DB2908B747B6962EAD13AB4038
+Published Today!
+LastStatus 2013-08-18 13:02:57
+ExitAddress 46.10.211.205 2013-08-18 Never
+"""
+ desc = _parse_file(io.BytesIO(extra), validate=False).next()
+ self.assertTrue(is_valid_fingerprint(desc.fingerprint))
+ self.assertEqual("030B22437D99B2DB2908B747B6962EAD13AB4038", desc.fingerprint)
+ self.assertEqual(0, len(desc.exit_addresses))
_______________________________________________
tor-commits mailing list
tor-commits@xxxxxxxxxxxxxxxxxxxx
https://lists.torproject.org/cgi-bin/mailman/listinfo/tor-commits