Пример #1
0
    def withoutBlockInCountry(self, data):
        """Determine which countries the bridges for this **request** should
        not be blocked in.

        If :data:`addClientCountryCode` is ``True``, the the client's own
        geolocated country code will be added to the to the
        :data:`notBlockedIn` list.

        :param dict data: The decoded data from the JSON API request.
        """
        countryCodes = data.get("unblocked", list())

        for countryCode in countryCodes:
            try:
                country = UNBLOCKED_PATTERN.match(countryCode).group()
            except (TypeError, AttributeError):
                pass
            else:
                if country:
                    self.notBlockedIn.append(country.lower())
                    logging.info("Moat request for bridges not blocked in: %r"
                                 % country)

        if self.addClientCountryCode:
            # Look up the country code of the input IP, and request bridges
            # not blocked in that country.
            if addr.isIPAddress(self.client):
                country = geo.getCountryCode(ipaddr.IPAddress(self.client))
                if country:
                    self.notBlockedIn.append(country.lower())
                    logging.info(
                        ("Moat client's bridges also shouldn't be blocked "
                         "in their GeoIP country code: %s") % country)
Пример #2
0
    def parseData(self):
        """Parse all data received so far into our
        :class:`<bridgedb.proxy.ProxySet> exitlist`.
        """
        unparseable = []

        data = ''.join(self.data).split('\n')

        for line in data:
            line.strip()
            if not line: continue
            # If it reached an errorpage, then we grabbed raw HTML that starts
            # with an HTML tag:
            if line.startswith('<'): break
            if line.startswith('#'): continue
            ip = isIPAddress(line)
            if ip:
                logging.info("Discovered Tor exit relay: %s" % ip)
                self.exitlist.add(ip)
            else:
                logging.debug("Got exitlist line that wasn't an IP: %s" % line)
                unparseable.append(line)

        if unparseable:
            logging.warn(("There were unparseable lines in the downloaded "
                          "list of Tor exit relays: %r") % unparseable)
Пример #3
0
def loadProxiesFromFile(filename, proxySet=None, removeStale=False):
    """Load proxy IP addresses from a list of files.

    :param str filename: A filename whose path can be either absolute or
        relative to the current working directory. The file should contain the
        IP addresses of various open proxies, one per line, for example::

            11.11.11.11
            22.22.22.22
            123.45.67.89

    :type proxySet: None or :class:`~bridgedb.proxy.ProxySet`.
    :param proxySet: If given, load the addresses read from the files into
        this ``ProxySet``.
    :param bool removeStale: If ``True``, remove proxies from the **proxySet**
        which were not listed in any of the **files**.
        (default: ``False``)
    :returns: A list of all the proxies listed in the **files* (regardless of
        whether they were added or removed).
    """
    logging.info("Reloading proxy lists...")

    addresses = []

    # We have to check the instance because, if the ProxySet was newly
    # created, it will likely be empty, causing it to evaluate to False:
    if isinstance(proxySet, ProxySet):
        oldProxySet = proxySet.copy()

    try:
        with open(filename, 'r') as proxyFile:
            for line in proxyFile.readlines():
                line = line.strip()
                if isinstance(proxySet, ProxySet):
                    # ProxySet.add() will validate the IP address
                    if proxySet.add(line, tag=filename):
                        addresses.append(line)
                else:
                    ip = isIPAddress(line)
                    if ip:
                        addresses.append(ip)
    except Exception as error:
        logging.warn("Error while reading a proxy list file: %s" % str(error))

    if isinstance(proxySet, ProxySet):
        stale = list(oldProxySet.difference(addresses))

        if removeStale:
            for ip in stale:
                if proxySet.getTag(ip) == filename:
                    logging.info("Removing stale IP %s from proxy list." % ip)
                    proxySet.remove(ip)
                else:
                    logging.info("Tag %s didn't match %s"
                                 % (proxySet.getTag(ip), filename))

    return addresses
Пример #4
0
def loadProxiesFromFile(filename, proxySet=None, removeStale=False):
    """Load proxy IP addresses from a list of files.

    :param str filename: A filename whose path can be either absolute or
        relative to the current working directory. The file should contain the
        IP addresses of various open proxies, one per line, for example::

            11.11.11.11
            22.22.22.22
            123.45.67.89

    :type proxySet: None or :class:`~bridgedb.proxy.ProxySet`.
    :param proxySet: If given, load the addresses read from the files into
        this ``ProxySet``.
    :param bool removeStale: If ``True``, remove proxies from the **proxySet**
        which were not listed in any of the **files**.
        (default: ``False``)
    :returns: A list of all the proxies listed in the **files* (regardless of
        whether they were added or removed).
    """
    logging.info("Reloading proxy lists...")

    addresses = []

    # We have to check the instance because, if the ProxySet was newly
    # created, it will likely be empty, causing it to evaluate to False:
    if isinstance(proxySet, ProxySet):
        oldProxySet = proxySet.copy()

    try:
        with open(filename, 'r') as proxyFile:
            for line in proxyFile.readlines():
                line = line.strip()
                if isinstance(proxySet, ProxySet):
                    # ProxySet.add() will validate the IP address
                    if proxySet.add(line, tag=filename):
                        addresses.append(line)
                else:
                    ip = isIPAddress(line)
                    if ip:
                        addresses.append(ip)
    except Exception as error:
        logging.warn("Error while reading a proxy list file: %s" % str(error))

    if isinstance(proxySet, ProxySet):
        stale = list(oldProxySet.difference(addresses))

        if removeStale:
            for ip in stale:
                if proxySet.getTag(ip) == filename:
                    logging.info("Removing stale IP %s from proxy list." % ip)
                    proxySet.remove(ip)
                else:
                    logging.info("Tag %s didn't match %s" %
                                 (proxySet.getTag(ip), filename))

    return addresses
Пример #5
0
 def test_returnUncompressedIP(self):
     """Test returning a :class:`ipaddr.IPAddress`."""
     testAddress = '86.59.30.40'
     result = addr.isIPAddress(testAddress, compressed=False)
     log.msg("addr.isIPAddress(%r, compressed=False) => %r" %
             (testAddress, result))
     self.assertTrue(
         isinstance(result, ipaddr.IPv4Address),
         "Expected %r result from isIPAddress(%r, compressed=False): %r %r"
         % (ipaddr.IPv4Address, testAddress, result, type(result)))
Пример #6
0
 def test_returnUncompressedIP(self):
     """Test returning a :class:`ipaddr.IPAddress`."""
     testAddress = '86.59.30.40'
     result = addr.isIPAddress(testAddress, compressed=False)
     log.msg("addr.isIPAddress(%r, compressed=False) => %r"
             % (testAddress, result))
     self.assertTrue(
         isinstance(result, ipaddr.IPv4Address),
         "Expected %r result from isIPAddress(%r, compressed=False): %r %r"
         % (ipaddr.IPv4Address, testAddress, result, type(result)))
Пример #7
0
    def withIPversion(self):
        """Determine if the request **parameters** were for bridges with IPv6
        addresses or not.

        .. note:: If the client's forwarded IP address was IPv6, then we assume
            the client wanted IPv6 bridges.
        """
        if addr.isIPAddress(self.client):
            if self.client.version == 6:
                logging.info("Moat request for bridges with IPv6 addresses.")
                self.withIPv6()
Пример #8
0
    def withIPversion(self):
        """Determine if the request **parameters** were for bridges with IPv6
        addresses or not.

        .. note:: If the client's forwarded IP address was IPv6, then we assume
            the client wanted IPv6 bridges.
        """
        if addr.isIPAddress(self.client):
            if self.client.version == 6:
                logging.info("Moat request for bridges with IPv6 addresses.")
                self.withIPv6()
Пример #9
0
    def __contains__(self, ip):
        """x.__contains__(y) <==> y in x.

        :type ip: basestring or int
        :param ip: The IP address to check.
        :rtype: boolean
        :returns: True if ``ip`` is in this set; False otherwise.
        """
        ipset = [isIPAddress(ip),]
        if ipset and len(self._proxies.intersection(ipset)) == len(ipset):
            return True
        return False
Пример #10
0
    def __contains__(self, ip):
        """x.__contains__(y) <==> y in x.

        :type ip: basestring or int
        :param ip: The IP address to check.
        :rtype: boolean
        :returns: True if ``ip`` is in this set; False otherwise.
        """
        ipset = [isIPAddress(ip),]
        if ipset and len(self._proxies.intersection(ipset)) == len(ipset):
            return True
        return False
Пример #11
0
    def test_isIPAddress_randomIP6(self):
        """Test :func:`addr.isIPAddress` with a random IPv6 address.

        This test asserts that the returned IP address is not None (because
        the IP being tested is random, it *could* randomly be an invalid IP
        address and thus :func:`~bridgdb.addr.isIPAddress` would return
        ``False``).
        """
        randomAddress = ipaddr.IPv6Address(random.getrandbits(128))
        result = addr.isIPAddress(randomAddress)
        log.msg("Got addr.isIPAddress() result for random IPv6 address %r: %s"
                % (randomAddress, result))
        self.assertTrue(result is not None)
Пример #12
0
    def runTestForAddr(self, testAddress):
        """Test :func:`addr.isIPAddress` with the specified ``testAddress``.

        :param str testAddress: A string which specifies either an IPv4 or
                                IPv6 address to test.
        """
        result = addr.isIPAddress(testAddress)
        log.msg("addr.isIPAddress(%r) => %s" % (testAddress, result))
        self.assertTrue(result is not None,
                        "Got a None for testAddress: %r" % testAddress)
        self.assertFalse(isinstance(result, basestring),
                        "Expected %r result from isIPAddress(%r): %r %r"
                        % (bool, testAddress, result, type(result)))
Пример #13
0
    def test_isIPAddress_randomIP6(self):
        """Test :func:`addr.isIPAddress` with a random IPv6 address.

        This test asserts that the returned IP address is not None (because
        the IP being tested is random, it *could* randomly be an invalid IP
        address and thus :func:`~bridgdb.addr.isIPAddress` would return
        ``False``).
        """
        randomAddress = ipaddr.IPv6Address(random.getrandbits(128))
        result = addr.isIPAddress(randomAddress)
        log.msg("Got addr.isIPAddress() result for random IPv6 address %r: %s"
                % (randomAddress, result))
        self.assertTrue(result is not None)
Пример #14
0
    def runTestForAddr(self, testAddress):
        """Test :func:`addr.isIPAddress` with the specified ``testAddress``.

        :param str testAddress: A string which specifies either an IPv4 or
                                IPv6 address to test.
        """
        result = addr.isIPAddress(testAddress)
        log.msg("addr.isIPAddress(%r) => %s" % (testAddress, result))
        self.assertTrue(result is not None,
                        "Got a None for testAddress: %r" % testAddress)
        self.assertFalse(isinstance(result, basestring),
                        "Expected %r result from isIPAddress(%r): %r %r"
                        % (bool, testAddress, result, type(result)))
Пример #15
0
    def getClientIP(self, request):
        """Get the client's IP address from the :header:`X-Forwarded-For`
        header, or from the :api:`request <twisted.web.server.Request>`.

        :rtype: None or str
        :returns: The client's IP address, if it was obtainable.
        """
        ip = None
        if self.useForwardedHeader:
            h = request.getHeader("X-Forwarded-For")
            if h:
                ip = h.split(",")[-1].strip()
                if not isIPAddress(ip):
                    logging.warn("Got weird X-Forwarded-For value %r" % h)
                    ip = None
        else:
            ip = request.getClientIP()
        return ip
Пример #16
0
    def withoutBlockInCountry(self, request):
        """Determine which countries the bridges for this **request** should
        not be blocked in.

        .. note:: Currently, a **request** for unblocked bridges is recognized
            if it contains an HTTP GET parameter ``unblocked=`` whose value is
            a comma-separater list of two-letter country codes.  Any
            two-letter country code found in the
            :api:`request <twisted.web.http.Request>` ``unblocked=`` HTTP GET
            parameter will be added to the :data:`notBlockedIn` list.

        If :data:`addClientCountryCode` is ``True``, the the client's own
        geolocated country code will be added to the to the
        :data`notBlockedIn` list.

        :type request: :api:`twisted.web.http.Request`
        :param request: A ``Request`` object containing the HTTP method, full
            URI, and any URL/POST arguments and headers present.
        """
        countryCodes = request.args.get("unblocked", list())

        for countryCode in countryCodes:
            try:
                country = UNBLOCKED_PATTERN.match(countryCode).group()
            except (TypeError, AttributeError):
                pass
            else:
                if country:
                    self.notBlockedIn.append(country.lower())
                    logging.info(
                        "HTTPS request for bridges not blocked in: %r" %
                        country)

        if self.addClientCountryCode:
            # Look up the country code of the input IP, and request bridges
            # not blocked in that country.
            if addr.isIPAddress(self.client):
                country = geo.getCountryCode(ipaddr.IPAddress(self.client))
                if country:
                    self.notBlockedIn.append(country.lower())
                    logging.info(
                        ("HTTPS client's bridges also shouldn't be blocked "
                         "in their GeoIP country code: %s") % country)
Пример #17
0
    def withoutBlockInCountry(self, request):
        """Determine which countries the bridges for this **request** should
        not be blocked in.

        .. note:: Currently, a **request** for unblocked bridges is recognized
            if it contains an HTTP GET parameter ``unblocked=`` whose value is
            a comma-separater list of two-letter country codes.  Any
            two-letter country code found in the
            :api:`request <twisted.web.http.Request>` ``unblocked=`` HTTP GET
            parameter will be added to the :data:`notBlockedIn` list.

        If :data:`addClientCountryCode` is ``True``, the the client's own
        geolocated country code will be added to the to the
        :data`notBlockedIn` list.

        :type request: :api:`twisted.web.http.Request`
        :param request: A ``Request`` object containing the HTTP method, full
            URI, and any URL/POST arguments and headers present.
        """
        countryCodes = request.args.get("unblocked", list())

        for countryCode in countryCodes:
            try:
                country = UNBLOCKED_PATTERN.match(countryCode).group()
            except (TypeError, AttributeError):
                pass
            else:
                if country:
                    self.notBlockedIn.append(country.lower())
                    logging.info("HTTPS request for bridges not blocked in: %r"
                                 % country)

        if self.addClientCountryCode:
            # Look up the country code of the input IP, and request bridges
            # not blocked in that country.
            if addr.isIPAddress(self.client):
                country = geo.getCountryCode(ipaddr.IPAddress(self.client))
                if country:
                    self.notBlockedIn.append(country.lower())
                    logging.info(
                        ("HTTPS client's bridges also shouldn't be blocked "
                         "in their GeoIP country code: %s") % country)
Пример #18
0
    def __add__(self, ip=None, tag=None):
        """Add an **ip** to this set, with an optional **tag**.

        This has no effect if the **ip** is already present.  The **ip** is
        only added if it passes the checks in
        :func:`~bridgedb.parse.addr.isIPAddress`.

        :type ip: basestring or int
        :param ip: The IP address to add.
        :param tag: An optional value to link to **ip**. If not given, it will
            be a timestamp (seconds since epoch, as a float) for when **ip**
            was first added to this set.
        :rtype: bool
        :returns: ``True`` if **ip** is in this set; ``False`` otherwise.
        """
        ip = isIPAddress(ip)
        if ip:
            if self._proxies.isdisjoint(set(ip)):
                logging.debug("Adding %s to proxy list..." % ip)
                self._proxies.add(ip)
                self._proxydict[ip] = tag if tag else time.time()
                return True
        return False
Пример #19
0
    def __add__(self, ip=None, tag=None):
        """Add an **ip** to this set, with an optional **tag**.

        This has no effect if the **ip** is already present.  The **ip** is
        only added if it passes the checks in
        :func:`~bridgedb.parse.addr.isIPAddress`.

        :type ip: basestring or int
        :param ip: The IP address to add.
        :param tag: An optional value to link to **ip**. If not given, it will
            be a timestamp (seconds since epoch, as a float) for when **ip**
            was first added to this set.
        :rtype: bool
        :returns: ``True`` if **ip** is in this set; ``False`` otherwise.
        """
        ip = isIPAddress(ip)
        if ip:
            if self._proxies.isdisjoint(set(ip)):
                logging.debug("Adding %s to proxy list..." % ip)
                self._proxies.add(ip)
                self._proxydict[ip] = tag if tag else time.time()
                return True
        return False
Пример #20
0
def getClientIP(request, useForwardedHeader=False, skipLoopback=False):
    """Get the client's IP address from the ``'X-Forwarded-For:'``
    header, or from the :api:`request <twisted.web.server.Request>`.

    :type request: :api:`twisted.web.http.Request`
    :param request: A ``Request`` for a :api:`twisted.web.resource.Resource`.
    :param bool useForwardedHeader: If ``True``, attempt to get the client's
        IP address from the ``'X-Forwarded-For:'`` header.
    :param bool skipLoopback: If ``True``, and ``useForwardedHeader`` is
        also ``True``, then skip any loopback addresses (127.0.0.1/8) when
        parsing the X-Forwarded-For header.
    :rtype: ``None`` or :any:`str`
    :returns: The client's IP address, if it was obtainable.
    """
    ip = None

    if useForwardedHeader:
        header = request.getHeader("X-Forwarded-For")
        if header:
            index = -1
            ip = header.split(",")[index].strip()
            if skipLoopback:
                logging.info(("Parsing X-Forwarded-For again, ignoring "
                              "loopback addresses..."))
                while isLoopback(ip):
                    index -= 1
                    ip = header.split(",")[index].strip()
            if not skipLoopback and isLoopback(ip):
               logging.warn("Accepting loopback address: %s" % ip)
            else:
                if not isIPAddress(ip):
                    logging.warn("Got weird X-Forwarded-For value %r" % header)
                    ip = None
    else:
        ip = request.getClientIP()

    return ip
Пример #21
0
def getClientIP(request, useForwardedHeader=False, skipLoopback=False):
    """Get the client's IP address from the ``'X-Forwarded-For:'``
    header, or from the :api:`request <twisted.web.server.Request>`.

    :type request: :api:`twisted.web.http.Request`
    :param request: A ``Request`` for a :api:`twisted.web.resource.Resource`.
    :param bool useForwardedHeader: If ``True``, attempt to get the client's
        IP address from the ``'X-Forwarded-For:'`` header.
    :param bool skipLoopback: If ``True``, and ``useForwardedHeader`` is
        also ``True``, then skip any loopback addresses (127.0.0.1/8) when
        parsing the X-Forwarded-For header.
    :rtype: ``None`` or :any:`str`
    :returns: The client's IP address, if it was obtainable.
    """
    ip = None

    if useForwardedHeader:
        header = request.getHeader("X-Forwarded-For")
        if header:
            index = -1
            ip = header.split(",")[index].strip()
            if skipLoopback:
                logging.info(("Parsing X-Forwarded-For again, ignoring "
                              "loopback addresses..."))
                while isLoopback(ip):
                    index -= 1
                    ip = header.split(",")[index].strip()
            if not skipLoopback and isLoopback(ip):
                logging.warn("Accepting loopback address: %s" % ip)
            else:
                if not isIPAddress(ip):
                    logging.warn("Got weird X-Forwarded-For value %r" % header)
                    ip = None
    else:
        ip = request.getClientIP()

    return ip
Пример #22
0
def getClientIP(request, useForwardedHeader=False):
    """Get the client's IP address from the ``'X-Forwarded-For:'``
    header, or from the :api:`request <twisted.web.server.Request>`.

    :type request: :api:`twisted.web.http.Request`
    :param request: A ``Request`` for a :api:`twisted.web.resource.Resource`.
    :param bool useForwardedHeader: If ``True``, attempt to get the client's
        IP address from the ``'X-Forwarded-For:'`` header.
    :rtype: ``None`` or :any:`str`
    :returns: The client's IP address, if it was obtainable.
    """
    ip = None

    if useForwardedHeader:
        header = request.getHeader("X-Forwarded-For")
        if header:
            ip = header.split(",")[-1].strip()
            if not isIPAddress(ip):
                logging.warn("Got weird X-Forwarded-For value %r" % header)
                ip = None
    else:
        ip = request.getClientIP()

    return ip
Пример #23
0
 def wrapper():
     ip = None
     while not isIPAddress(ip):
         ip = func()
     return ip
Пример #24
0
    def getBridgeRequestAnswer(self, request):
        """Respond to a client HTTP request for bridges.

        :type request: :api:`twisted.web.http.Request`
        :param request: A ``Request`` object containing the HTTP method, full
            URI, and any URL/POST arguments and headers present.
        :rtype: str
        :returns: A plaintext or HTML response to serve.
        """
        # XXX why are we getting the interval if our distributor might be
        # using bridgedb.schedule.Unscheduled?
        interval = self.schedule.intervalStart(time.time())
        bridges = ( )
        ip = None
        countryCode = None

        # XXX this code is duplicated in CaptchaProtectedResource
        if self.useForwardedHeader:
            h = request.getHeader("X-Forwarded-For")
            if h:
                ip = h.split(",")[-1].strip()
                if not isIPAddress(ip):
                    logging.warn("Got weird forwarded-for value %r",h)
                    ip = None
        else:
            ip = request.getClientIP()

        # Look up the country code of the input IP
        if isIPAddress(ip):
            countryCode = bridgedb.geo.getCountryCode(IPAddress(ip))
        else:
            logging.warn("Invalid IP detected; skipping country lookup.")
            countryCode = None

        # XXX separate function again
        format = request.args.get("format", None)
        if format and len(format): format = format[0] # choose the first arg

        # do want any options?
        transport = ipv6 = unblocked = False

        ipv6 = request.args.get("ipv6", False)
        if ipv6: ipv6 = True # if anything after ?ipv6=

        # XXX oh dear hell. why not check for the '?transport=' arg before
        # regex'ing? And why not compile the regex once, somewhere outside
        # this function and class?
        try:
            # validate method name
            transport = re.match('[_a-zA-Z][_a-zA-Z0-9]*',
                    request.args.get("transport")[0]).group()
        except (TypeError, IndexError, AttributeError):
            transport = None

        try:
            unblocked = re.match('[a-zA-Z]{2,4}',
                    request.args.get("unblocked")[0]).group()
        except (TypeError, IndexError, AttributeError):
            unblocked = False

        logging.info("Replying to web request from %s. Parameters were %r"
                     % (ip, request.args))

        rules = []
        bridgeLines = None

        if ip:
            if ipv6:
                rules.append(filterBridgesByIP6)
                addressClass = IPv6Address
            else:
                rules.append(filterBridgesByIP4)
                addressClass = IPv4Address

            if transport:
                #XXX: A cleaner solution would differentiate between
                # addresses by protocol rather than have separate lists
                # Tor to be a transport, and selecting between them.
                rules = [filterBridgesByTransport(transport, addressClass)]

            if unblocked:
                rules.append(filterBridgesByNotBlockedIn(unblocked,
                    addressClass, transport))

            bridges = self.distributor.getBridgesForIP(ip, interval,
                                                       self.nBridgesToGive,
                                                       countryCode,
                                                       bridgeFilterRules=rules)
            bridgeLines = "".join("%s\n" % b.getConfigLine(
                includeFingerprint=self.includeFingerprints,
                addressClass=addressClass,
                transport=transport,
                request=bridgedb.Dist.uniformMap(ip)
                ) for b in bridges)

        answer = self.renderAnswer(request, bridgeLines, format)
        return answer
Пример #25
0
 def wrapper():
     ip = None
     while not isIPAddress(ip):
         ip = func()
     return ip
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.
    :type fingerprint: string or None
    :param fingerprint: A string which identifies which OR the descriptor
        we're parsing came from (since the 'a'-line doesn't tell us, this can
        help make the log messages clearer).
    :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
    portlist = None

    if line.startswith('a '):
        line = line[2:]  # Chop off the 'a '
    else:
        logging.warn("Networkstatus parser received non 'a'-line for %r:"\
                     "  %r" % (fingerprint or 'Unknown', line))

    try:
        ip, portlist = line.rsplit(':', 1)
    except ValueError as error:
        logging.error("Bad separator in networkstatus 'a'-line: %r" % line)
        return (None, None)

    if ip.startswith('[') and ip.endswith(']'):
        ip = ip.strip('[]')

    try:
        if not addr.isIPAddress(ip):
            raise NetworkstatusParsingError(
                "Got invalid IP Address in networkstatus 'a'-line for %r: %r" %
                (fingerprint or 'Unknown', line))

        if addr.isIPv4(ip):
            warnings.warn(FutureWarning(
                "Got IPv4 address in networkstatus 'a'-line! "\
                "Networkstatus document format may have changed!"))
    except NetworkstatusParsingError as error:
        logging.error(error)
        ip, portlist = None, None

    try:
        portlist = addr.PortList(portlist)
        if not portlist:
            raise NetworkstatusParsingError(
                "Got invalid portlist in 'a'-line for %r!\n  Line: %r" %
                (fingerprint or 'Unknown', line))
    except (addr.InvalidPort, NetworkstatusParsingError) as error:
        logging.error(error)
        portlist = None
    else:
        logging.debug("Parsed networkstatus ORAddress line for %r:"\
                      "\n  Address: %s  \tPorts: %s"
                      % (fingerprint or 'Unknown', ip, portlist))
    finally:
        return (ip, portlist)
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.
    :type fingerprint: string or None
    :param fingerprint: A string which identifies which OR the descriptor
        we're parsing came from (since the 'a'-line doesn't tell us, this can
        help make the log messages clearer).
    :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
    portlist = None

    if line.startswith('a '):
        line = line[2:] # Chop off the 'a '
    else:
        logging.warn("Networkstatus parser received non 'a'-line for %r:"\
                     "  %r" % (fingerprint or 'Unknown', line))

    try:
        ip, portlist = line.rsplit(':', 1)
    except ValueError as error:
        logging.error("Bad separator in networkstatus 'a'-line: %r" % line)
        return (None, None)

    if ip.startswith('[') and ip.endswith(']'):
        ip = ip.strip('[]')

    try:
        if not addr.isIPAddress(ip):
            raise NetworkstatusParsingError(
                "Got invalid IP Address in networkstatus 'a'-line for %r: %r"
                % (fingerprint or 'Unknown', line))

        if addr.isIPv4(ip):
            warnings.warn(FutureWarning(
                "Got IPv4 address in networkstatus 'a'-line! "\
                "Networkstatus document format may have changed!"))
    except NetworkstatusParsingError as error:
        logging.error(error)
        ip, portlist = None, None

    try:
        portlist = addr.PortList(portlist)
        if not portlist:
            raise NetworkstatusParsingError(
                "Got invalid portlist in 'a'-line for %r!\n  Line: %r"
                % (fingerprint or 'Unknown', line))
    except (addr.InvalidPort, NetworkstatusParsingError) as error:
        logging.error(error)
        portlist = None
    else:
        logging.debug("Parsed networkstatus ORAddress line for %r:"\
                      "\n  Address: %s  \tPorts: %s"
                      % (fingerprint or 'Unknown', ip, portlist))
    finally:
        return (ip, portlist)