[tor-commits] [bridgedb/master] Rewrite Bridge class.

isis at torproject.org isis at torproject.org
Sat Mar 21 02:02:57 UTC 2015


commit 170d391e5b456fc1f66b2d01d2aaa2892ea85a78
Author: Isis Lovecruft <isis at torproject.org>
Date:   Tue Nov 4 22:12:24 2014 +0000

    Rewrite Bridge class.
    
    The bridge class is now capable of storing additional information which
    we might find useful about a bridge. Additionally, there are now methods
    for updating a Bridge instance's attribute via descriptors parsed with
    Stem.
---
 lib/bridgedb/bridges.py |  301 +++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 301 insertions(+)

diff --git a/lib/bridgedb/bridges.py b/lib/bridgedb/bridges.py
index 65d4d02..1ab88f1 100644
--- a/lib/bridgedb/bridges.py
+++ b/lib/bridgedb/bridges.py
@@ -31,6 +31,17 @@ class InvalidPluggableTransportIP(MalformedBridgeInfo):
     """Raised when a :class:`PluggableTransport` has an invalid address."""
 
 
+class ServerDescriptorDigestMismatch(MalformedBridgeInfo):
+    """Raised when the digest in an ``@type bridge-networkstatus`` document
+    doesn't match the hash digest of the ``@type bridge-server-descriptor``'s
+    contents.
+    """
+
+class ServerDescriptorWithoutNetworkstatus(MalformedBridgeInfo):
+    """Raised when we find a ``@type bridge-server-descriptor`` which was not
+    mentioned in the latest ``@type bridge-networkstatus`` document.
+    """
+
 class PluggableTransport(object):
     """A single instance of a Pluggable Transport (PT) offered by a
     :class:`Bridge`.
@@ -288,3 +299,293 @@ class PluggableTransport(object):
         self.port = kitchenSink[1]
         self.arguments = self._parseArgumentsIntoDict(kitchenSink[2])
         self._runChecks()
+
+
+class Bridge(object):
+    """A single bridge, and all the information we have for it.
+
+    :type fingerprint: str or None
+    :ivar fingerprint:
+
+    :type nickname: str or None
+    :ivar nickname:
+
+    :ivar orPort: int or None
+    :ivar orPort:
+
+    :ivar socksPort: int
+    :ivar socksPort:
+
+    :type dirPort: int
+    :ivar dirPort:
+
+    :type orAddresses: list
+    :ivar orAddresses:
+
+    :type transports: list
+    :ivar transports:
+
+    :type flags: :class:`~bridgedb.bridges.Flags`
+    :ivar flags:
+
+    :type hibernating: bool
+    :ivar hibernating:
+
+    :type contact: str or None
+    :ivar contact: The contact information for the this bridge's operator.
+
+    :type platform: str or None
+    :ivar platform: The ``platform`` line taken from the
+        ``@type bridge-server-descriptor``, e.g.
+        ``'Tor 0.2.5.4-alpha on Linux'``.
+
+    :type family: set or None
+    :ivar family: The fingerprints of other bridges related to this one.
+    """
+    #: (bool) If ``True``, check that the signature of the bridge's
+    #: ``@type bridge-server-descriptor`` is valid and that the signature was
+    #: created with the ``signing-key`` contained in that descriptor.
+    _checkServerDescriptorSignature = True
+
+    def __init__(self):
+        """Create a new ``Bridge``."""
+        self.fingerprint = None
+        self.nickname = None
+        self.address = None
+        self.orPort = None
+        self.socksPort = 0  # Bridges should always have ``SOCKSPort`` and
+        self.dirPort = 0    # ``DirPort`` set to ``0``
+        self.orAddresses = []
+        self.transports = []
+        self.flags = BridgeFlags()
+        self.hibernating = False
+
+        self.bandwidth = None
+        self.bandwidthAverage = None
+        self.bandwidthBurst = None
+        self.bandwidthObserverd = None
+
+        self.contact = None
+        self.family = None
+        self.platform = None
+        self.software = None
+        self.os = None
+        self.uptime = None
+
+        self.onionKey = None
+        self.ntorOnionKey = None
+        self.signingKey = None
+
+        self.descriptors = {'networkstatus': None,
+                            'server': None,
+                            'extrainfo': None}
+
+        #: The hash digest of this bridge's ``@type bridge-server-descriptor``,
+        #: as signed (but not including the signature). This is found in the
+        #: 'r'-line of this bridge's ``@type bride-networkstatus`` document,
+        #: however it is stored here re-encoded from base64 into hexadecimal,
+        #: and converted to uppercase.
+        self.descriptorDigest = None
+        self.extrainfoDigest = None
+
+    def _checkServerDescriptor(self, descriptor):
+        # If we're parsing the server-descriptor, require a networkstatus
+        # document:
+        if not self.descriptors['networkstatus']:
+            raise ServerDescriptorWithoutNetworkstatus(
+                ("We received a server-descriptor for bridge '%s' which has "
+                 "no corresponding networkstatus document.") %
+                descriptor.fingerprint)
+
+        ns = self.descriptors['networkstatus']
+
+        # We must have the digest of the server-descriptor from the
+        # networkstatus document:
+        if not self.descriptorDigest:
+            raise MissingServerDescriptorDigest(
+                ("The server-descriptor digest was missing from networkstatus "
+                 "document for bridge '%s'.") % descriptor.fingerprint)
+
+        digested = descriptor.digest()
+        # The digested server-descriptor must match the digest reported by the
+        # BridgeAuthority in the bridge's networkstatus document:
+        if not self.descriptorDigest == digested:
+            raise ServerDescriptorDigestMismatch(
+                ("The server-descriptor digest for bridge '%s' doesn't match "
+                 "the digest reported by the BridgeAuthority in the "
+                 "networkstatus document: \n"
+                 "Digest reported in networkstatus: %s\n"
+                 "Actual descriptor digest:         %s\n") %
+                (descriptor.fingerprint, self.descriptorDigest, digested))
+
+    def _updateORAddresses(self, orAddresses):
+        for (address, port, ipVersion) in orAddresses:
+            if not ipVersion:  # `False` means IPv4; `True` means IPv6.
+                # See https://bugs.torproject.org/9380#comment:27
+                warnings.warn(FutureWarning(
+                    ("Got IPv4 address in 'a'/'or-address' line! "
+                     "Desriptor format may have changed!")))
+            self.orAddresses.append(tuple([address, port]))
+
+    def assertOK(self):
+        """Perform some additional validation on this bridge's info.
+
+        We require that:
+
+          1. This bridge's :data:`fingerprint` is valid, accoring to
+             :func:`~bridgedb.parse.fingerprint.isValidFingerprint`.
+
+          2. This bridge's :data:`address` and any IP addresses contained in
+             :data:`orAddresses` are valid, according to
+             :func:`~bridgedb.parse.addr.isValidIP`.
+
+          3. The :data:`orPort` and any ports in :data:`orAddresses` are
+             between ``1`` and ``65535`` (inclusive).
+
+        :raises MalformedBridgeInfo: if something was found to be malformed or
+            invalid.
+        """
+        malformed = []
+
+        if not isValidFingerprint(self.fingerprint):
+            malformed.append("Invalid fingerprint: '%s'" % self.fingerprint)
+        if not isValidIP(self.address):
+            malformed.append("Invalid ORPort address: '%s'" % self.address)
+        if not (1 <= self.orPort <= 65535):
+            malformed.append("Invalid ORPort port: '%d'" % self.orPort)
+        for (address, port) in self.orAddresses:
+            if not isValidIP(address):
+                malformed.append("Invalid ORAddress address: '%s'" % address)
+            if not (1 <= port <= 65535):
+                malformed.append("Invalid ORAddress port: '%d'" % port)
+
+        if malformed:
+            raise MalformedBridgeInfo('\n'.join(malformed))
+
+    def getDescriptorLastPublished(self):
+        """Get the timestamp for when this bridge's last known server
+        descriptor was published.
+
+        :rtype: :type:`datetime.datetime` or ``None``
+        :returns: A datetime object representing the timestamp of when the
+            last known ``@type bridge-server-descriptor`` was published, or
+            ``None`` if we have never seen a server descriptor for this
+            bridge.
+        """
+        return getattr(self.descriptors['server'], 'published', None)
+
+    def getExtrainfoLastPublished(self):
+        """Get the timestamp for when this bridge's last known extrainfo
+        descriptor was published.
+
+        :rtype: :type:`datetime.datetime` or ``None``
+        :returns: A datetime object representing the timestamp of when the
+            last known ``@type bridge-extrainfo`` descriptor was published, or
+            ``None`` if we have never seen an extrainfo descriptor for this
+            bridge.
+        """
+        return getattr(self.descriptors['extrainfo'], 'published', None)
+
+    def getNetworkstatusLastPublished(self):
+        """Get the timestamp for when this bridge's last known networkstatus
+        descriptor was published.
+
+        :rtype: :type:`datetime.datetime` or ``None``
+        :returns: A datetime object representing the timestamp of when the
+            last known ``@type networkstatus-bridge`` document was published,
+            or ``None`` if we have never seen a networkstatus document for
+            this bridge.
+        """
+        return getattr(self.descriptors['networkstatus'], 'published', None)
+
+    def updateFromNetworkstatus(self, descriptor):
+        """Update this bridge's attributes from a parsed networkstatus
+        descriptor.
+
+        :type ns: :api:`stem.descriptors.router_status_entry.RouterStatusEntry`
+        :param ns:
+        """
+        self.descriptors['networkstatus'] = descriptor
+
+        # These fields are *only* found in the networkstatus document:
+        self.descriptorDigest = descriptor.digest
+        self.flags.update(descriptor.flags)
+        self.bandwidth = descriptor.bandwith
+
+        # These fields are also found in the server-descriptor. We will prefer
+        # to use the information taken later from the server-descriptor
+        # because it is signed by the bridge. However, for now, we harvest all
+        # the info we can:
+        self.fingerprint = descriptor.fingerprint
+        self.nickname = descriptor.nickname
+        self.address = descriptor.address
+        self.orPort = descriptor.or_port
+
+        self._updateORAddresses(descriptor.or_addresses)
+
+    def updateFromServerDescriptor(self, descriptor):
+        """Update this bridge's info from an ``@type bridge-server-descriptor``.
+
+        .. info::
+            If :func:`~bridgedb.parse.descriptor.parseServerDescriptorFile` is
+            called with ``validate=True``, then Stem will handle checking that
+            the ``signing-key`` hashes to the ``fingerprint``. Stem will also
+            check that the ``router-signature`` on the descriptor is valid,
+            was created with the ``signing-key``, and is a signature of the
+            correct digest of the descriptor document (it recalculates the
+            digest for the descriptor to ensure that the signed one and the
+            actual digest match).
+
+        :type descriptor:
+            :api:`stem.descriptor.server_descriptor.RelayDescriptor`
+        :param descriptor:
+        """
+        self.descriptors['server'] = descriptor
+
+        try:
+            self._checkServerDescriptor(descriptor)
+        except ValueError as error:
+            logging.warn(error)
+            # XXX should we throw away this descriptor?
+
+        # Replace the values which we harvested from the networkstatus
+        # descriptor, because that one isn't signed with the bridge's identity
+        # key.
+        self.fingerprint = descriptor.fingerprint
+        self.address = descriptor.address
+        self.nickname = descriptor.nickname
+        self.orPort = descriptor.or_port
+        self._updateORAddresses(descriptor.or_addresses)
+        self.hibernating = descriptor.hibernating
+
+        self.onionKey = descriptor.onion_key
+        self.ntorOnionKey = descriptor.ntor_onion_key
+        self.signingKey = descriptor.signing_key
+
+        self.bandwidthAverage = descriptor.average_bandwidth
+        self.bandwidthBurst = descriptor.burst_bandwidth
+        self.bandwidthObserved = descriptor.bandwidth_observed
+
+        self.contact = descriptor.contact
+        self.family = descriptor.family
+        self.platform = descriptor.platform
+        self.software = descriptor.tor_version
+        self.os = descriptor.operating_system
+        self.uptime = descriptor.uptime
+
+        self.extrainfoDigest = descriptor.extrainfoDigest
+
+    def updateFromExtraInfoDescriptor(self, descriptor):
+        """Update this bridge's information from an extrainfo descriptor.
+
+        .. todo:: The ``transport`` attribute of Stem's
+            ``BridgeExtraInfoDescriptor`` class is a dictionary that uses the
+            Pluggable Transport's eype as the keys. Meaning that if a bridge
+            were to offer four instances of ``obfs3``, only one of them would
+            get to us through Stem. This might pose a problem someday.
+
+        :type descriptor:
+            :api:`stem.descriptor.extrainfo_descriptor.BridgeExtraInfoDescriptor`
+        :param descriptor: DOCDOC
+        """
+        self.descriptors['extrainfo'] = descriptor





More information about the tor-commits mailing list