[tor-commits] [bridgedb/master] Add newly refactored networkstatus parsers and unittests for them.

isis at torproject.org isis at torproject.org
Sun Jan 12 06:06:32 UTC 2014


commit a6d52fe28997d1c81c8f8533277925424b9fc157
Author: Isis Lovecruft <isis at torproject.org>
Date:   Fri Nov 15 15:30:38 2013 +0000

    Add newly refactored networkstatus parsers and unittests for them.
---
 lib/bridgedb/parse/networkstatus.py           |  236 +++++++++++++++++++++++++
 lib/bridgedb/test/test_parse_networkstatus.py |   92 ++++++++++
 2 files changed, 328 insertions(+)

diff --git a/lib/bridgedb/parse/networkstatus.py b/lib/bridgedb/parse/networkstatus.py
new file mode 100644
index 0000000..d38257d
--- /dev/null
+++ b/lib/bridgedb/parse/networkstatus.py
@@ -0,0 +1,236 @@
+# -*- coding: utf-8 -*-
+#
+# This file is part of BridgeDB, a Tor bridge distribution system.
+#
+# :authors: Isis Lovecruft 0xA3ADB67A2CDB8B35 <isis at torproject.org>
+#           please also see AUTHORS file
+# :copyright: (c) 2013 Isis Lovecruft
+#             (c) 2007-2013, The Tor Project, Inc.
+#             (c) 2007-2013, all entities within the AUTHORS file
+# :license: 3-clause BSD, see included LICENSE for information
+
+"""Parsers for ``@type bridge-network-status 1.0`` descriptors.
+
+.. _descriptors: https://metrics.torproject.org/formats.html#descriptortypes
+
+**Module Overview:**
+
+..
+  parse
+   \_networkstatus     
+      |_ parseRLine - Parse an 'r'-line from a networkstatus document
+      |_ parseALine - Parse an 'a'-line from a networkstatus document
+      \_ parseSLine - Parse an 's'-line from a networkstatus document
+"""
+
+import binascii
+import logging
+import string
+import time
+
+from bridgedb.parse import addr
+from bridgedb.parse import padBase64
+
+
+class NetworkstatusParsingError(Exception):
+    """Unable to parse networkstatus document line."""
+
+class InvalidNetworkstatusRouterIdentity(ValueError):
+    """The ID field of a networkstatus document 'r'-line is invalid."""
+
+class InvalidNetworkstatusDescriptorDigest(ValueError):
+    """Descriptor digest of a networkstatus document 'r'-line is invalid."""
+
+
+def isValidRouterNickname(nickname):
+    """Determine if a router's given nickname meets the specification.
+
+    :param string nickname: An OR's nickname.
+    """
+    
+
+def parseRLine(line):
+    """Parse an 'r'-line from a networkstatus document.
+
+    From torspec.git/dir-spec.txt, commit 36761c7d553d L1499-1512:
+      |
+      |"r" SP nickname SP identity SP digest SP publication SP IP SP ORPort
+      |    SP DirPort NL
+      |
+      |    [At start, exactly once.]
+      |
+      |    "Nickname" is the OR's nickname.  "Identity" is a hash of its
+      |    identity key, encoded in base64, with trailing equals sign(s)
+      |    removed.  "Digest" is a hash of its most recent descriptor as
+      |    signed (that is, not including the signature), encoded in base64.
+      |    "Publication" is the
+      |    publication time of its most recent descriptor, in the form
+      |    YYYY-MM-DD HH:MM:SS, in UTC.  "IP" is its current IP address;
+      |    ORPort is its current OR port, "DirPort" is its current directory
+      |    port, or "0" for "none".
+      |
+
+    :param string line: An 'r'-line from an bridge-network-status descriptor.
+
+    """
+    (nickname, ID, descDigest, timestamp,
+     ORaddr, ORport, dirport) = (None for x in xrange(7))
+
+    if not line.startswith('r '):
+        raise NetworkstatusParsingError(
+            "Networkstatus parser received non 'r'-line: %r" % line)
+
+    line = line[2:] # Chop of the 'r '
+
+    fields = line.split()
+    if len(fields) != 8:
+        raise NetworkstatusParsingError(
+            "Wrong number of fields in networkstatus 'r'-line: %r" % line)
+
+    try:
+        nickname, ID = fields[:2]
+
+        if ID.endswith('='):
+            raise InvalidNetworkstatusRouterIdentity(
+                "Skipping networkstatus parsing for router with nickname %r: ",
+                "Unpadded, base64-encoded networkstatus router identity ",
+                "string ends with '=': %r" % (nickname, ID))
+        try:
+            ID = padBase64(ID) # Add the trailing equals sign back in
+        except (AttributeError, ValueError) as error:
+            raise InvalidNetworkstatusRouterIdentity(error.message)
+
+        ID = binascii.a2b_base64(ID) 
+        if not ID:
+            raise InvalidNetworkstatusRouterIdentity(
+                "Skipping networkstatus parsing for router with nickname %r: ",
+                "Base64-encoding for networkstatus router identity string is ",
+                "invalid! Line: %r" % (nickname, line))
+
+    except IndexError as error:
+        logging.error(error.message)
+    except InvalidNetworkstatusRouterIdentity as error:
+        logging.error(error.message)
+        ID = None
+
+    try:
+        descDigest = binascii.a2b_base64(fields[2])
+    except (AttributeError, ValueError) as error:
+        raise InvalidNetworkstatusDescriptorDigest(error.message)
+
+
+        timestamp = time.mktime(time.strptime(" ".join(fields[3:5]),
+                                              "%Y-%m-%d %H:%M:%S"))
+        ORaddr = fields[5]
+        ORport = fields[6]
+        dirport = fields[7]
+
+    finally:
+        return (nickname, ID, descDigest, timestamp, ORaddr, ORport, dirport)
+
+def parseALine(line, fingerprint=None):
+    """Parse an 'a'-line of a bridge networkstatus document.
+
+    From torspec.git/dir-spec.txt, commit 36761c7d553d L1499-1512:
+      |
+      | "a" SP address ":" port NL
+      |
+      |    [Any number.]
+      |
+      |    Present only if the OR has at least one IPv6 address.
+      |
+      |    Address and portlist are as for "or-address" as specified in
+      |    2.1.
+      |
+      |    (Only included when the vote or consensus is generated with
+      |    consensus-method 14 or later.)
+
+    :param string line: An 'a'-line from an bridge-network-status descriptor.
+    :raises: :exc:`NetworkstatusParsingError`
+    :rtype: tuple
+    :returns: A 2-tuple of a string respresenting the IP address and a
+        :class:`bridgedb.parse.addr.PortList`.
+    """
+    ip = None
+    address = None
+    portlist = None
+
+    if not line.startswith('a '):
+        logging.error("Networkstatus parser received non 'a'-line for %r:"
+                      % (fingerprint or 'Unknown'))
+        logging.error("\t%r" % line)
+        return address, portlist
+
+    line = line[2:] # Chop off the 'a '
+
+    try:
+        ip, portlist = line.rsplit(':', 1)
+    except (IndexError, ValueError, addr.InvalidPort) as error:
+        logging.exception(error)
+        raise NetworkstatusParsingError(
+            "Parsing networkstatus 'a'-line for %r failed! Line: %r"
+            %(fingerprint, line))
+    else:
+        ip = ip.strip('[]') 
+        address = addr.isIPAddress(ip)
+        if not ip:
+            raise NetworkstatusParsingError(
+                "Got invalid IP Address in networkstatus 'a'-line for %r: %r"
+                % (fingerprint, ip))
+        
+        portlist = addr.PortList(portlist)
+
+    logging.debug("Parsed networkstatus ORAddress line for %r:" % fingerprint)
+    logging.debug("\tAddress: %s  \tPorts: %s" % (address, portlist))
+
+    return address, portlist
+
+def parseSLine(line):
+    """Parse an 's'-line from a bridge networkstatus document.
+
+    The 's'-line contains all flags assigned to a bridge. The flags which may
+    be assigned to a bridge are as follows:
+
+    From torspec.git/dir-spec.txt, commit 36761c7d553d L1526-1554:
+      |
+      | "s" SP Flags NL
+      |
+      |    [Exactly once.]
+      |
+      |    A series of space-separated status flags, in lexical order (as ASCII
+      |    byte strings).  Currently documented flags are:
+      |
+      |      "BadDirectory" if the router is believed to be useless as a
+      |         directory cache (because its directory port isn't working,
+      |         its bandwidth is always throttled, or for some similar
+      |         reason).
+      |      "Fast" if the router is suitable for high-bandwidth circuits.
+      |      "Guard" if the router is suitable for use as an entry guard.
+      |      "HSDir" if the router is considered a v2 hidden service directory.
+      |      "Named" if the router's identity-nickname mapping is canonical,
+      |         and this authority binds names.
+      |      "Stable" if the router is suitable for long-lived circuits.
+      |      "Running" if the router is currently usable.
+      |      "Valid" if the router has been 'validated'.
+      |      "V2Dir" if the router implements the v2 directory protocol.
+
+    :param string line: An 's'-line from an bridge-network-status descriptor.
+    :rtype: tuple
+    :returns: A 2-tuple of booleans, the first is True if the bridge has the
+        "Running" flag, and the second is True if it has the "Stable" flag.
+    """
+    fast, running, stable, guard, valid = False
+    
+    line = line[2:]
+
+    flags = [x.capitalize() for x in line.split()]
+    fast    = 'Fast' in flags
+    running = 'Running' in flags
+    stable  = 'Stable' in flags
+    guard   = 'Guard' in flags
+    valid   = 'Valid' in flags
+    
+    logging.debug("Parsed Flags: %s" % flags)
+
+    # Right now, we only care about 'Running' and 'Stable'
+    return running, stable
diff --git a/lib/bridgedb/test/test_parse_networkstatus.py b/lib/bridgedb/test/test_parse_networkstatus.py
new file mode 100644
index 0000000..830f96c
--- /dev/null
+++ b/lib/bridgedb/test/test_parse_networkstatus.py
@@ -0,0 +1,92 @@
+# -*- coding: utf-8 -*-
+#
+# This file is part of BridgeDB, a Tor bridge distribution system.
+#
+# :authors: Isis Lovecruft 0xA3ADB67A2CDB8B35 <isis at torproject.org>
+#           please also see AUTHORS file
+# :copyright: (c) 2013, Isis Lovecruft
+#             (c) 2007-2013, The Tor Project, Inc.
+#             (c) 2007-2013, all entities within the AUTHORS file
+# :license: 3-Clause BSD, see LICENSE for licensing information
+
+"""Unittests for the :mod:`bridgedb.parse.networkstatus` module.
+
+These tests are meant to ensure that the :mod:`bridgedb.parse.networkstatus`
+module is functioning correctly.
+"""
+
+from __future__ import print_function
+
+from twisted.trial import unittest
+from bridgedb.parse import networkstatus
+
+import sure
+from sure import this, these, those, the, it
+
+
+class ParseNetworkStatusRLineTests(unittest.TestCase):
+    """Tests for :func:`bridgedb.parse.networkstatus.parseRLine`."""
+
+    pre   = 'r '
+    nick  = 'Testing'
+    ident = 'bXw2N1K9AAKR5undPaTgNUySNxI'
+    desc  = 'Z6cisoPT9s6hEd4JkHFAlIWAwXQ='
+    ts    = '2013-10-31 15:15:15'
+    ip    = '221.251.0.42'
+    port  = '9001'
+    dirp  = '0'
+
+    def test_missingPrefix(self):
+        line = ' '.join([self.nick, self.ident, self.desc,
+                         self.ts, self.ip, self.port, self.dirp])
+        self.assertRaises(networkstatus.NetworkstatusParsingError,
+                          networkstatus.parseRLine, line)
+
+    def test_wrongNumberOfFields(self):
+        line = ' '.join([self.pre, self.nick, self.ident, self.ts, self.ip])
+        self.assertRaises(networkstatus.NetworkstatusParsingError,
+                          networkstatus.parseRLine, line)
+
+    def test_wrongFieldOrder(self):
+        line = ' '.join([self.pre, self.nick, self.desc, self.ident,
+                         self.ts, self.ip, self.port, self.dirp])
+        fields = networkstatus.parseRLine(line)
+        nick, others = fields[0], fields[1:]
+
+        this(nick).should.be.ok
+        this(nick).should.be.a(str)
+        this(nick).should.equal(self.nick)
+
+        the(others).should.be.a(tuple)
+        the(others).should.have.length_of(6)
+        for other in others:
+            the(other).should.be(None)
+
+    def test_invalidTimestampMissingDate(self):
+        line = ' '.join([self.pre, self.nick, self.ident, self.desc,
+                         '15:15:15', self.ip, self.port, self.dirp])
+        self.assertRaises(networkstatus.NetworkstatusParsingError,
+                          networkstatus.parseRLine, line)
+
+    def test_invalidBase64(self):
+        line = ' '.join([self.pre, self.nick, '%$>#@,<', self.desc,
+                         self.ts, self.ip, self.port, self.dirp])
+        nick, ident, desc, ts, ip, port, dirp = networkstatus.parseRLine(line)
+
+        the(nick).should.be.ok
+        the(nick).should.be.a(str)
+        the(nick).should.equal(self.nick)
+
+        the(ident).should.be(None)
+        the(desc).should.be(None)
+
+    def test_invalidTimestamp(self):
+        line = ' '.join([self.pre, self.nick, self.ident, self.desc,
+                         '123456789 987654321', self.ip, self.port, self.dirp])
+        fields = networkstatus.parseRLine(line)
+        
+    def test_invalidIPAddress(self):
+        line = ' '.join([self.pre, self.nick, self.ident, self.desc,
+                         self.ts, '0.0.0.0', self.port, self.dirp])
+        fields = networkstatus.parseRLine(line)
+        





More information about the tor-commits mailing list