def __init__(self, address: str, or_port: Union[int, str], dir_port: Union[int, str], fingerprint: str, nickname: str, orport_v6: Tuple[str, int]) -> None: identifier = '%s (%s)' % (fingerprint, nickname) if nickname else fingerprint if not connection.is_valid_ipv4_address(address): raise ValueError('%s has an invalid IPv4 address: %s' % (identifier, address)) elif not connection.is_valid_port(or_port): raise ValueError('%s has an invalid ORPort: %s' % (identifier, or_port)) elif not connection.is_valid_port(dir_port): raise ValueError('%s has an invalid DirPort: %s' % (identifier, dir_port)) elif not tor_tools.is_valid_fingerprint(fingerprint): raise ValueError('%s has an invalid fingerprint: %s' % (identifier, fingerprint)) elif nickname and not tor_tools.is_valid_nickname(nickname): raise ValueError('%s has an invalid nickname: %s' % (fingerprint, nickname)) if orport_v6: if not isinstance(orport_v6, tuple) or len(orport_v6) != 2: raise ValueError('%s orport_v6 should be a two value tuple: %s' % (identifier, str(orport_v6))) elif not connection.is_valid_ipv6_address(orport_v6[0]): raise ValueError('%s has an invalid IPv6 address: %s' % (identifier, orport_v6[0])) elif not connection.is_valid_port(orport_v6[1]): raise ValueError('%s has an invalid IPv6 port: %s' % (identifier, orport_v6[1])) self.address = address self.or_port = int(or_port) self.dir_port = int(dir_port) self.fingerprint = fingerprint self.nickname = nickname self.orport_v6 = (orport_v6[0], int(orport_v6[1])) if orport_v6 else None
def exits_can_exit_to(self, host, port): ''' Return exits that can MOST LIKELY exit to the given host:port. **host** can be a hostname, but be warned that we will resolve it locally and use the first (arbitrary/unknown order) result when checking exit policies, which is different than what other parts of the code may do (leaving it up to the exit to resolve the name). An exit can only MOST LIKELY not just because of the above DNS disconnect, but also because fundamentally our Tor client is most likely using microdescriptors which do not have full information about exit policies. ''' c = self._controller if not is_valid_ipv4_address(host) and not is_valid_ipv6_address(host): # It certainly isn't perfect trying to guess if an exit can connect # to an ipv4/6 address based on the DNS result we got locally. But # it's the best we can do. # # Also, only use the first ipv4/6 we get even if there is more than # one. host = resolve(host)[0] assert is_valid_ipv4_address(host) or is_valid_ipv6_address(host) exits = [] for exit in self.exits: # If we have the exit policy already, easy if exit.exit_policy: policy = exit.exit_policy else: # Otherwise ask Tor for the microdescriptor and assume the exit # won't work if the desc isn't available try: fp = exit.fingerprint policy = c.get_microdescriptor(fp).exit_policy except DescriptorUnavailable as e: log.debug(e) continue # There's a weird KeyError we sometimes hit when checking # policy.can_exit_to()... so catch that and log about it. Maybe # someday it can be fixed? try: if policy is not None and policy.can_exit_to(port=port): exits.append(exit) except KeyError as e: log.exception('Got that KeyError in stem again...: %s', e) continue return exits
def from_cache(): """ Provides fallback directory information cached with Stem. Unlike :func:`~stem.descriptor.remote.FallbackDirectory.from_remote` this doesn't have any system requirements, and is faster too. Only drawback is that these fallback directories are only as up to date as the Stem release we're using. :returns: **dict** of **str** fingerprints to their :class:`~stem.descriptor.remote.FallbackDirectory` """ conf = stem.util.conf.Config() conf.load(CACHE_PATH) results = {} for fingerprint in set([key.split('.')[0] for key in conf.keys()]): if fingerprint in ('tor_commit', 'stem_commit'): continue attr = {} for attr_name in ('address', 'or_port', 'dir_port', 'orport6_address', 'orport6_port'): key = '%s.%s' % (fingerprint, attr_name) attr[attr_name] = conf.get(key) if not attr[attr_name] and not attr_name.startswith('orport6_'): raise IOError("'%s' is missing from %s" % (key, CACHE_PATH)) if not connection.is_valid_ipv4_address(attr['address']): raise IOError("'%s.address' was an invalid IPv4 address (%s)" % (fingerprint, attr['address'])) elif not connection.is_valid_port(attr['or_port']): raise IOError("'%s.or_port' was an invalid port (%s)" % (fingerprint, attr['or_port'])) elif not connection.is_valid_port(attr['dir_port']): raise IOError("'%s.dir_port' was an invalid port (%s)" % (fingerprint, attr['dir_port'])) elif attr['orport6_address'] and not connection.is_valid_ipv6_address(attr['orport6_address']): raise IOError("'%s.orport6_address' was an invalid IPv6 address (%s)" % (fingerprint, attr['orport6_address'])) elif attr['orport6_port'] and not connection.is_valid_port(attr['orport6_port']): raise IOError("'%s.orport6_port' was an invalid port (%s)" % (fingerprint, attr['orport6_port'])) if attr['orport6_address'] and attr['orport6_port']: orport_v6 = (attr['orport6_address'], int(attr['orport6_port'])) else: orport_v6 = None results[fingerprint] = FallbackDirectory( address = attr['address'], or_port = int(attr['or_port']), dir_port = int(attr['dir_port']), fingerprint = fingerprint, orport_v6 = orport_v6, ) return results
def _parse(self): if self.type not in ('server', 'client'): raise stem.ProtocolError("Transport type should either be 'server' or 'client': %s" % self) if not connection.is_valid_ipv4_address(self.address) and \ not connection.is_valid_ipv6_address(self.address): raise stem.ProtocolError("Transport address isn't a valid IPv4 or IPv6 address: %s" % self) if not connection.is_valid_port(self.port): raise stem.ProtocolError('Transport port is invalid: %s' % self) self.port = int(self.port)
def can_exit_to(self, host, port): ''' Returns if this relay can MOST LIKELY exit to the given host:port. **host** can be a hostname, but be warned that we will resolve it locally and use the first (arbitrary/unknown order) result when checking exit policies, which is different than what other parts of the code may do (leaving it up to the exit to resolve the name). ''' if not self.exit_policy: return False assert isinstance(host, str) assert isinstance(port, int) if not is_valid_ipv4_address(host) and not is_valid_ipv6_address(host): # It certainly isn't perfect trying to guess if an exit can connect # to an ipv4/6 address based on the DNS result we got locally. But # it's the best we can do. # # Also, only use the first ipv4/6 we get even if there is more than # one. host = resolve(host)[0] assert is_valid_ipv4_address(host) or is_valid_ipv6_address(host) return self.exit_policy.can_exit_to(host, port)
def _parse(self): if self.type not in ('server', 'client'): raise stem.ProtocolError( "Transport type should either be 'server' or 'client': %s" % self) if not connection.is_valid_ipv4_address(self.address) and \ not connection.is_valid_ipv6_address(self.address): raise stem.ProtocolError( "Transport address isn't a valid IPv4 or IPv6 address: %s" % self) if not connection.is_valid_port(self.port): raise stem.ProtocolError('Transport port is invalid: %s' % self) self.port = int(self.port)
def from_remote(timeout=60): """ Reads and parses tor's latest fallback directories `from gitweb.torproject.org <https://gitweb.torproject.org/tor.git/plain/src/or/fallback_dirs.inc>`_. Note that while convenient, this reliance on GitWeb means you should alway call with a fallback, such as... :: try: fallback_directories = stem.descriptor.remote.from_remote() except IOError: fallback_directories = stem.descriptor.remote.from_cache() :param int timeout: seconds to wait before timing out the request :returns: **dict** of **str** fingerprints to their :class:`~stem.descriptor.remote.FallbackDirectory` :raises: **IOError** if unable to retrieve the fallback directories """ try: fallback_dir_page = str_tools._to_unicode( urllib.urlopen(GITWEB_FALLBACK_DIR_URL, timeout=timeout).read()) except: exc = sys.exc_info()[1] raise IOError( "Unable to download tor's fallback directories from %s: %s" % (GITWEB_FALLBACK_DIR_URL, exc)) # Example of an entry... # # "5.175.233.86:80 orport=443 id=5525D0429BFE5DC4F1B0E9DE47A4CFA169661E33" # " ipv6=[2a03:b0c0:0:1010::a4:b001]:9001" # " weight=43680", results, attr = {}, {} for line in fallback_dir_page.splitlines(): if line.startswith('"'): addr_line_match = re.match( '"([\d\.]+):(\d+) orport=(\d+) id=([\dA-F]{40}).*', line) ipv6_line_match = re.match('" ipv6=\[([\da-f:]+)\]:(\d+)"', line) if addr_line_match: address, dir_port, or_port, fingerprint = addr_line_match.groups( ) if not connection.is_valid_ipv4_address(address): raise IOError('%s has an invalid IPv4 address: %s' % (fingerprint, address)) elif not connection.is_valid_port(or_port): raise IOError('%s has an invalid or_port: %s' % (fingerprint, or_port)) elif not connection.is_valid_port(dir_port): raise IOError('%s has an invalid dir_port: %s' % (fingerprint, dir_port)) elif not tor_tools.is_valid_fingerprint(fingerprint): raise IOError('%s has an invalid fingerprint: %s' % (fingerprint, fingerprint)) attr = { 'address': address, 'or_port': int(or_port), 'dir_port': int(dir_port), 'fingerprint': fingerprint, } elif ipv6_line_match: address, port = ipv6_line_match.groups() if not connection.is_valid_ipv6_address(address): raise IOError('%s has an invalid IPv6 address: %s' % (fingerprint, address)) elif not connection.is_valid_port(port): raise IOError( '%s has an invalid ORPort for its IPv6 endpoint: %s' % (fingerprint, port)) attr['orport_v6'] = (address, int(port)) elif line.startswith('" weight=') and 'fingerprint' in attr: results[attr.get('fingerprint')] = FallbackDirectory( address=attr.get('address'), or_port=attr.get('or_port'), dir_port=attr.get('dir_port'), fingerprint=attr.get('fingerprint'), orport_v6=attr.get('orport_v6'), ) attr = {} return results
def _parse_v2(fallback_dir_page): # Example of an entry... # # "5.9.110.236:9030 orport=9001 id=0756B7CD4DFC8182BE23143FAC0642F515182CEB" # " ipv6=[2a01:4f8:162:51e2::2]:9001" # /* nickname=rueckgrat */ # /* extrainfo=1 */ results, attr = {}, {} for line in fallback_dir_page.splitlines(): addr_line_match = re.match( '"([\d\.]+):(\d+) orport=(\d+) id=([\dA-F]{40}).*', line) nickname_match = re.match('/\* nickname=(\S+) \*/', line) has_extrainfo_match = re.match('/\* extrainfo=([0-1]) \*/', line) ipv6_line_match = re.match('" ipv6=\[([\da-f:]+)\]:(\d+)"', line) if addr_line_match: address, dir_port, or_port, fingerprint = addr_line_match.groups( ) if not connection.is_valid_ipv4_address(address): raise IOError('%s has an invalid IPv4 address: %s' % (fingerprint, address)) elif not connection.is_valid_port(or_port): raise IOError('%s has an invalid or_port: %s' % (fingerprint, or_port)) elif not connection.is_valid_port(dir_port): raise IOError('%s has an invalid dir_port: %s' % (fingerprint, dir_port)) elif not tor_tools.is_valid_fingerprint(fingerprint): raise IOError('%s has an invalid fingerprint: %s' % (fingerprint, fingerprint)) attr = { 'address': address, 'or_port': int(or_port), 'dir_port': int(dir_port), 'fingerprint': fingerprint, } elif ipv6_line_match: address, port = ipv6_line_match.groups() if not connection.is_valid_ipv6_address(address): raise IOError('%s has an invalid IPv6 address: %s' % (fingerprint, address)) elif not connection.is_valid_port(port): raise IOError( '%s has an invalid ORPort for its IPv6 endpoint: %s' % (fingerprint, port)) attr['orport_v6'] = (address, int(port)) elif nickname_match: nickname = nickname_match.group(1) if not tor_tools.is_valid_nickname(nickname): raise IOError('%s has an invalid nickname: %s' % (fingerprint, nickname)) attr['nickname'] = nickname elif has_extrainfo_match: attr['has_extrainfo'] = has_extrainfo_match.group(1) == '1' results[attr.get('fingerprint')] = FallbackDirectory( address=attr.get('address'), or_port=attr.get('or_port'), dir_port=attr.get('dir_port'), fingerprint=attr.get('fingerprint'), nickname=attr.get('nickname'), has_extrainfo=attr.get('has_extrainfo', False), orport_v6=attr.get('orport_v6'), ) attr = {} return results
def _parse_v1(fallback_dir_page): # Example of an entry... # # "5.175.233.86:80 orport=443 id=5525D0429BFE5DC4F1B0E9DE47A4CFA169661E33" # " ipv6=[2a03:b0c0:0:1010::a4:b001]:9001" # " weight=43680", # TODO: this method can be removed once gitweb provides a v2 formatted document results, attr = {}, {} for line in fallback_dir_page.splitlines(): if line.startswith('"'): addr_line_match = re.match( '"([\d\.]+):(\d+) orport=(\d+) id=([\dA-F]{40}).*', line) ipv6_line_match = re.match('" ipv6=\[([\da-f:]+)\]:(\d+)"', line) if addr_line_match: address, dir_port, or_port, fingerprint = addr_line_match.groups( ) if not connection.is_valid_ipv4_address(address): raise IOError('%s has an invalid IPv4 address: %s' % (fingerprint, address)) elif not connection.is_valid_port(or_port): raise IOError('%s has an invalid or_port: %s' % (fingerprint, or_port)) elif not connection.is_valid_port(dir_port): raise IOError('%s has an invalid dir_port: %s' % (fingerprint, dir_port)) elif not tor_tools.is_valid_fingerprint(fingerprint): raise IOError('%s has an invalid fingerprint: %s' % (fingerprint, fingerprint)) attr = { 'address': address, 'or_port': int(or_port), 'dir_port': int(dir_port), 'fingerprint': fingerprint, } elif ipv6_line_match: address, port = ipv6_line_match.groups() if not connection.is_valid_ipv6_address(address): raise IOError('%s has an invalid IPv6 address: %s' % (fingerprint, address)) elif not connection.is_valid_port(port): raise IOError( '%s has an invalid ORPort for its IPv6 endpoint: %s' % (fingerprint, port)) attr['orport_v6'] = (address, int(port)) elif line.startswith('" weight=') and 'fingerprint' in attr: results[attr.get('fingerprint')] = FallbackDirectory( address=attr.get('address'), or_port=attr.get('or_port'), dir_port=attr.get('dir_port'), fingerprint=attr.get('fingerprint'), orport_v6=attr.get('orport_v6'), ) attr = {} return results
def from_remote(timeout = 60): """ Reads and parses tor's latest fallback directories `from gitweb.torproject.org <https://gitweb.torproject.org/tor.git/plain/src/or/fallback_dirs.inc>`_. Note that while convenient, this reliance on GitWeb means you should alway call with a fallback, such as... :: try: fallback_directories = stem.descriptor.remote.from_remote() except IOError: fallback_directories = stem.descriptor.remote.from_cache() :param int timeout: seconds to wait before timing out the request :returns: **dict** of **str** fingerprints to their :class:`~stem.descriptor.remote.FallbackDirectory` :raises: **IOError** if unable to retrieve the fallback directories """ try: fallback_dir_page = str_tools._to_unicode(urllib.urlopen(GITWEB_FALLBACK_DIR_URL, timeout = timeout).read()) except: exc = sys.exc_info()[1] raise IOError("Unable to download tor's fallback directories from %s: %s" % (GITWEB_FALLBACK_DIR_URL, exc)) # Example of an entry... # # "5.175.233.86:80 orport=443 id=5525D0429BFE5DC4F1B0E9DE47A4CFA169661E33" # " ipv6=[2a03:b0c0:0:1010::a4:b001]:9001" # " weight=43680", results, attr = {}, {} for line in fallback_dir_page.splitlines(): if line.startswith('"'): addr_line_match = re.match('"([\d\.]+):(\d+) orport=(\d+) id=([\dA-F]{40}).*', line) ipv6_line_match = re.match('" ipv6=\[([\da-f:]+)\]:(\d+)"', line) if addr_line_match: address, dir_port, or_port, fingerprint = addr_line_match.groups() if not connection.is_valid_ipv4_address(address): raise IOError('%s has an invalid IPv4 address: %s' % (fingerprint, address)) elif not connection.is_valid_port(or_port): raise IOError('%s has an invalid or_port: %s' % (fingerprint, or_port)) elif not connection.is_valid_port(dir_port): raise IOError('%s has an invalid dir_port: %s' % (fingerprint, dir_port)) elif not tor_tools.is_valid_fingerprint(fingerprint): raise IOError('%s has an invalid fingerprint: %s' % (fingerprint, fingerprint)) attr = { 'address': address, 'or_port': int(or_port), 'dir_port': int(dir_port), 'fingerprint': fingerprint, } elif ipv6_line_match: address, port = ipv6_line_match.groups() if not connection.is_valid_ipv6_address(address): raise IOError('%s has an invalid IPv6 address: %s' % (fingerprint, address)) elif not connection.is_valid_port(port): raise IOError('%s has an invalid ORPort for its IPv6 endpoint: %s' % (fingerprint, port)) attr['orport_v6'] = (address, int(port)) elif line.startswith('" weight=') and 'fingerprint' in attr: results[attr.get('fingerprint')] = FallbackDirectory( address = attr.get('address'), or_port = attr.get('or_port'), dir_port = attr.get('dir_port'), fingerprint = attr.get('fingerprint'), orport_v6 = attr.get('orport_v6'), ) attr = {} return results