stem-1.6.0/ 0000775 0001750 0001750 00000000000 13177674754 013235 5 ustar atagar atagar 0000000 0000000 stem-1.6.0/tox.ini 0000664 0001750 0001750 00000000320 13115423200 014505 0 ustar atagar atagar 0000000 0000000 [tox]
envlist = py26,py27,py31,py32,py33,py34
skipsdist = True
[testenv]
commands =
pip install --allow-all-external -e .
python run_tests.py {posargs:-a}
rm -rf stem.egg-info
deps =
-rrequirements.txt
stem-1.6.0/test/ 0000775 0001750 0001750 00000000000 13177674754 014214 5 ustar atagar atagar 0000000 0000000 stem-1.6.0/test/settings.cfg 0000664 0001750 0001750 00000030662 13177114716 016527 0 ustar atagar atagar 0000000 0000000 # Testing Configuration
#
# The following contains customizable configuration information for stem's
# testing framework.
#
# integ.test_directory
# Path used for our data directory and any temporary test resources. Relative
# paths are expanded in reference to the location of 'run_tests.py'.
#
# If set then the directory's contents are reused for future tests (so we
# have a faster startup and lower load on authorities). If set to an empty
# value then this makes a fresh data directory for each test run.
#
# integ.log
# Path runtime logs are placed. Relative paths are expanded in reference to
# 'run_tests.py'. Logging is disabled if set ot an empty value.
integ.test_directory ./test/data
exclude_paths .*/stem/test/data/.*
integ.log ./test/data/log
# The following are less testing framework attributes that aren't as commonly
# reconfigured.
#
# msg.*
# Rendered text.
#
# target.*
# Attributes of the integration testing targets. This helps determine what is
# ran when the user runs with '--target'.
msg.help
|Usage run_tests.py [OPTION]
|Runs tests for the stem library.
|
| -a, --all runs unit, integ, and style checks (same as '-ui')
| -u, --unit runs unit tests
| -i, --integ runs integration tests
| -t, --target TARGET comma separated list of extra targets for integ tests
| --test TEST_NAME only run tests modules containing the given name
| -l, --log RUNLEVEL includes logging output with test results, runlevels:
| TRACE, DEBUG, INFO, NOTICE, WARN, ERROR
| --tor PATH custom tor binary to run testing against
| -v, --verbose provides additional test output
| -h, --help presents this help
|
| Examples:
| run_tests.py --unit --integ
| Run unit and integration tests.
|
| run_tests.py --integ --target RUN_ALL
| Run integration tests against all tor configurations.
|
| run_tests.py --integ --test test.integ.util
| Only run integration tests for the util modules.
|
| Integration targets:
##################
# CATEGORY: TARGET #
##################
# The '--help' description of the target.
target.description ONLINE => Includes tests that require network activity.
target.description RELATIVE => Uses a relative path for tor's data directory.
target.description CHROOT => Simulates a chroot setup.
target.description RUN_NONE => Configuration without a way for controllers to connect.
target.description RUN_OPEN => Configuration with an open control port (default).
target.description RUN_PASSWORD => Configuration with password authentication.
target.description RUN_COOKIE => Configuration with an authentication cookie.
target.description RUN_MULTIPLE => Configuration with both password and cookie authentication.
target.description RUN_SOCKET => Configuration with a control socket.
target.description RUN_SCOOKIE => Configuration with a control socket and authentication cookie.
target.description RUN_PTRACE => Configuration with an open control port and 'DisableDebuggerAttachment 0'
target.description RUN_ALL => Runs integration tests for all connection configurations.
# Torrc configuration options included with the target. Having this option set
# means that each of these targets will have a dedicated integration test run.
target.torrc RUN_NONE =>
target.torrc RUN_OPEN => PORT
target.torrc RUN_PASSWORD => PORT, PASSWORD
target.torrc RUN_COOKIE => PORT, COOKIE
target.torrc RUN_MULTIPLE => PORT, PASSWORD, COOKIE
target.torrc RUN_SOCKET => SOCKET
target.torrc RUN_SCOOKIE => SOCKET, COOKIE
target.torrc RUN_PTRACE => PORT, PTRACE
# Pycodestyle compliance issues that we're ignoreing...
#
# * E251 no spaces around keyword / parameter equals
#
# This one I dislike a great deal. It makes keyword arguments different
# from assignments which looks... aweful. I'm not sure what Pycodestyle's
# author was on when he wrote this one but it's stupid.
#
# Someone else can change this if they really care.
#
# * E501 line is over 79 characters
#
# We're no longer on TTY terminals. Overly constraining line length makes
# things far less readable, encouraging bad practices like abbreviated
# variable names.
#
# If the code fits on my tiny netbook screen then it's narrow enough.
#
# * E111, E114, and E121 four space indentations
#
# Ahhh, indentation. The holy war that'll never die. Sticking with two
# space indentations since it leads to shorter lines.
#
# * E127 and E131 continuation line over-indented for visual indent
#
# Pycodestyle only works with this one if we have four space indents (its
# detection is based on multiples of four).
#
# * E722 do not use bare except
#
# Iirc they advise against this because it catches KeyboardInterrups and
# interpreter termination. That's a fair concern, but on the other hand we
# don't have strong assurance that socket errors and the like will be caught
# without it. We've been doing this for years without issue but I'd be
# ameanable to a patch if this causes issues for someone.
pycodestyle.ignore E111
pycodestyle.ignore E114
pycodestyle.ignore E121
pycodestyle.ignore E501
pycodestyle.ignore E251
pycodestyle.ignore E127
pycodestyle.ignore E131
pycodestyle.ignore E722
pycodestyle.ignore stem/__init__.py => E402: import stem.util.enum
pycodestyle.ignore stem/descriptor/__init__.py => E402: import stem.descriptor.server_descriptor
pycodestyle.ignore stem/descriptor/__init__.py => E402: import stem.descriptor.extrainfo_descriptor
pycodestyle.ignore stem/descriptor/__init__.py => E402: import stem.descriptor.networkstatus
pycodestyle.ignore stem/descriptor/__init__.py => E402: import stem.descriptor.microdescriptor
pycodestyle.ignore stem/descriptor/__init__.py => E402: import stem.descriptor.tordnsel
pycodestyle.ignore stem/descriptor/__init__.py => E402: import stem.descriptor.hidden_service_descriptor
pycodestyle.ignore test/unit/util/connection.py => W291: _tor tor 15843 10 pipe 0x0 state:
pycodestyle.ignore test/unit/util/connection.py => W291: _tor tor 15843 11 pipe 0x0 state:
# False positives from pyflakes. These are mappings between the path and the
# issue.
pyflakes.ignore run_tests.py => 'unittest' imported but unused
pyflakes.ignore stem/util/__init__.py => undefined name 'long'
pyflakes.ignore stem/util/__init__.py => undefined name 'unicode'
pyflakes.ignore stem/control.py => undefined name 'controller'
pyflakes.ignore stem/manual.py => undefined name 'unichr'
pyflakes.ignore stem/prereq.py => 'int_to_bytes' imported but unused
pyflakes.ignore stem/prereq.py => 'int_from_bytes' imported but unused
pyflakes.ignore stem/prereq.py => 'default_backend' imported but unused
pyflakes.ignore stem/prereq.py => 'load_der_public_key' imported but unused
pyflakes.ignore stem/prereq.py => 'modes' imported but unused
pyflakes.ignore stem/prereq.py => 'Cipher' imported but unused
pyflakes.ignore stem/prereq.py => 'algorithms' imported but unused
pyflakes.ignore stem/prereq.py => 'unittest' imported but unused
pyflakes.ignore stem/prereq.py => 'unittest.mock' imported but unused
pyflakes.ignore stem/prereq.py => 'long_to_bytes' imported but unused
pyflakes.ignore stem/prereq.py => 'encoding' imported but unused
pyflakes.ignore stem/prereq.py => 'signing' imported but unused
pyflakes.ignore stem/prereq.py => 'sqlite3' imported but unused
pyflakes.ignore stem/prereq.py => 'cryptography.utils.int_to_bytes' imported but unused
pyflakes.ignore stem/prereq.py => 'cryptography.utils.int_from_bytes' imported but unused
pyflakes.ignore stem/prereq.py => 'cryptography.hazmat.backends.default_backend' imported but unused
pyflakes.ignore stem/prereq.py => 'cryptography.hazmat.primitives.serialization.load_der_public_key' imported but unused
pyflakes.ignore stem/prereq.py => 'cryptography.hazmat.primitives.ciphers.modes' imported but unused
pyflakes.ignore stem/prereq.py => 'cryptography.hazmat.primitives.ciphers.Cipher' imported but unused
pyflakes.ignore stem/prereq.py => 'cryptography.hazmat.primitives.ciphers.algorithms' imported but unused
pyflakes.ignore stem/prereq.py => 'nacl.encoding' imported but unused
pyflakes.ignore stem/prereq.py => 'nacl.signing' imported but unused
pyflakes.ignore stem/interpreter/__init__.py => undefined name 'raw_input'
pyflakes.ignore stem/util/conf.py => undefined name 'unicode'
pyflakes.ignore stem/util/test_tools.py => 'pyflakes' imported but unused
pyflakes.ignore stem/util/test_tools.py => 'pycodestyle' imported but unused
pyflakes.ignore test/unit/descriptor/reader.py => 'bz2' imported but unused
pyflakes.ignore test/unit/response/events.py => 'from stem import *' used; unable to detect undefined names
pyflakes.ignore test/unit/response/events.py => *may be undefined, or defined from star imports: stem
pyflakes.ignore test/__init__.py => undefined name 'test'
# Test modules we want to run. Modules are roughly ordered by the dependencies
# so the lowest level tests come first. This is because a problem in say,
# controller message parsing, will cause all higher level tests to fail too.
# Hence we want the test that most narrowly exhibits problems to come first.
test.unit_tests
|test.unit.util.enum.TestEnum
|test.unit.util.connection.TestConnection
|test.unit.util.conf.TestConf
|test.unit.util.log.TestLog
|test.unit.util.proc.TestProc
|test.unit.util.str_tools.TestStrTools
|test.unit.util.system.TestSystem
|test.unit.util.term.TestTerminal
|test.unit.util.tor_tools.TestTorTools
|test.unit.util.__init__.TestBaseUtil
|test.unit.installation.TestInstallation
|test.unit.descriptor.export.TestExport
|test.unit.descriptor.reader.TestDescriptorReader
|test.unit.descriptor.remote.TestDescriptorDownloader
|test.unit.descriptor.server_descriptor.TestServerDescriptor
|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
|test.unit.descriptor.networkstatus.document_v3.TestNetworkStatusDocument
|test.unit.descriptor.networkstatus.bridge_document.TestBridgeNetworkStatusDocument
|test.unit.descriptor.hidden_service_descriptor.TestHiddenServiceDescriptor
|test.unit.descriptor.certificate.TestEd25519Certificate
|test.unit.exit_policy.rule.TestExitPolicyRule
|test.unit.exit_policy.policy.TestExitPolicy
|test.unit.version.TestVersion
|test.unit.manual.TestManual
|test.unit.tutorial.TestTutorial
|test.unit.tutorial_examples.TestTutorialExamples
|test.unit.response.add_onion.TestAddOnionResponse
|test.unit.response.control_message.TestControlMessage
|test.unit.response.control_line.TestControlLine
|test.unit.response.events.TestEvents
|test.unit.response.getinfo.TestGetInfoResponse
|test.unit.response.getconf.TestGetConfResponse
|test.unit.response.singleline.TestSingleLineResponse
|test.unit.response.authchallenge.TestAuthChallengeResponse
|test.unit.response.protocolinfo.TestProtocolInfoResponse
|test.unit.response.mapaddress.TestMapAddressResponse
|test.unit.connection.authentication.TestAuthenticate
|test.unit.connection.connect.TestConnect
|test.unit.control.controller.TestControl
|test.unit.interpreter.arguments.TestArgumentParsing
|test.unit.interpreter.autocomplete.TestAutocompletion
|test.unit.interpreter.help.TestHelpResponses
|test.unit.interpreter.commands.TestInterpreterCommands
|test.unit.doctest.TestDocumentation
test.integ_tests
|test.integ.util.conf.TestConf
|test.integ.util.connection.TestConnection
|test.integ.util.proc.TestProc
|test.integ.util.system.TestSystem
|test.integ.interpreter.TestInterpreter
|test.integ.version.TestVersion
|test.integ.manual.TestManual
|test.integ.response.protocolinfo.TestProtocolInfo
|test.integ.socket.control_socket.TestControlSocket
|test.integ.socket.control_message.TestControlMessage
|test.integ.connection.authentication.TestAuthenticate
|test.integ.connection.connect.TestConnect
|test.integ.control.base_controller.TestBaseController
|test.integ.control.controller.TestController
|test.integ.descriptor.remote.TestDescriptorDownloader
|test.integ.descriptor.server_descriptor.TestServerDescriptor
|test.integ.descriptor.extrainfo_descriptor.TestExtraInfoDescriptor
|test.integ.descriptor.microdescriptor.TestMicrodescriptor
|test.integ.descriptor.networkstatus.TestNetworkStatus
|test.integ.installation.TestInstallation
|test.integ.process.TestProcess
stem-1.6.0/test/require.py 0000664 0001750 0001750 00000006030 13124757510 016221 0 ustar atagar atagar 0000000 0000000 # Copyright 2012-2017, Damian Johnson and The Tor Project
# See LICENSE for licensing information
"""
Testing requirements. This provides annotations to skip tests that shouldn't be
run.
::
Test Requirements
|- only_run_once - skip test if it has been ran before
|- needs - skips the test unless a requirement is met
|
|- cryptography - skips test unless the cryptography module is present
|- pynacl - skips test unless the pynacl module is present
|- command - requires a command to be on the path
|- proc - requires the platform to have recognized /proc contents
|
|- controller - skips test unless tor provides a controller endpoint
|- version - skips test unless we meet a tor version requirement
|- ptrace - requires 'DisableDebuggerAttachment' to be set
+- online - skips unless targets allow for online tests
"""
import stem.util.system
import stem.version
import test
import test.runner
RAN_TESTS = []
def only_run_once(func):
"""
Skips the test if it has ran before. If it hasn't then flags it as being ran.
This is useful to prevent lengthy tests that are independent of integ targets
from being run repeatedly with ``RUN_ALL``.
"""
def wrapped(self, *args, **kwargs):
if self.id() not in RAN_TESTS:
RAN_TESTS.append(self.id())
return func(self, *args, **kwargs)
else:
self.skipTest('(already ran)')
return wrapped
def needs(condition, message):
"""
Skips teh test unless the conditional evaluates to 'true'.
"""
def decorator(func):
def wrapped(self, *args, **kwargs):
if condition():
return func(self, *args, **kwargs)
else:
self.skipTest('(%s)' % message)
return wrapped
return decorator
def _can_access_controller():
return test.runner.get_runner().is_accessible()
def _can_ptrace():
# If we're running a tor version where ptrace is disabled and we didn't
# set 'DisableDebuggerAttachment=1' then we can infer that it's disabled.
has_option = test.tor_version() >= stem.version.Requirement.TORRC_DISABLE_DEBUGGER_ATTACHMENT
return not has_option or test.runner.Torrc.PTRACE in test.runner.get_runner().get_options()
def _is_online():
return test.Target.ONLINE in test.runner.get_runner().attribute_targets
def command(cmd):
"""
Skips the test unless a command is available on the path.
"""
return needs(lambda: stem.util.system.is_available(cmd), '%s unavailable' % cmd)
def version(req_version):
"""
Skips the test unless we meet the required version.
:param stem.version.Version req_version: required tor version for the test
"""
return needs(lambda: test.tor_version() >= req_version, 'requires %s' % req_version)
cryptography = needs(stem.prereq.is_crypto_available, 'requires cryptography')
pynacl = needs(stem.prereq._is_pynacl_available, 'requires pynacl module')
proc = needs(stem.util.proc.is_available, 'proc unavailable')
controller = needs(_can_access_controller, 'no connection')
ptrace = needs(_can_ptrace, 'DisableDebuggerAttachment is set')
online = needs(_is_online, 'requires online target')
stem-1.6.0/test/network.py 0000664 0001750 0001750 00000022770 13124757510 016247 0 ustar atagar atagar 0000000 0000000 # Copyright 2012-2017, Damian Johnson and The Tor Project
# See LICENSE for licensing information
"""
Helper functions and classes to support tests which need to connect through
the tor network.
::
ProxyError - Base error for proxy issues.
+- SocksError - Reports problems returned by the SOCKS proxy.
Socks - Communicate through a SOCKS5 proxy with a socket interface
SocksPatch - Force socket-using code to use test.network.Socks
"""
import functools
import socket
import struct
import stem.util.connection
import stem.util.str_tools
from stem import ProtocolError, SocketError
# Store a reference to the original class so we can find it after
# monkey patching.
_socket_socket = socket.socket
SOCKS5_NOAUTH_GREETING = (0x05, 0x01, 0x00)
SOCKS5_NOAUTH_RESPONSE = (0x05, 0x00)
SOCKS5_CONN_BY_IPV4 = (0x05, 0x01, 0x00, 0x01)
SOCKS5_CONN_BY_NAME = (0x05, 0x01, 0x00, 0x03)
error_msgs = {
0x5a: 'SOCKS4A request granted',
0x5b: 'SOCKS4A request rejected or failed',
0x5c: 'SOCKS4A request failed because client is not running identd (or not reachable from the server)',
0x5d: "SOCKS4A request failed because client's identd could not confirm the user ID string in the request",
}
ip_request = """GET /ip HTTP/1.0
Host: ifconfig.me
Accept-Encoding: identity
"""
class ProxyError(Exception):
'Base error for proxy issues.'
class SocksError(ProxyError):
"""
Exception raised for any problems returned by the SOCKS proxy.
:var int code: error code returned by the SOCKS proxy
"""
# Error messages copied from http://en.wikipedia.org/wiki/SOCKS,
# retrieved 2012-12-15 17:09:21.
_ERROR_MESSAGE = {
0x01: 'general failure',
0x02: 'connection not allowed by ruleset',
0x03: 'network unreachable',
0x04: 'host unreachable',
0x05: 'connection refused by destination host',
0x06: 'TTL expired',
0x07: 'command not supported / protocol error',
0x08: 'address type not supported',
}
def __init__(self, code):
self.code = code
def __str__(self):
code = 0x01
if self.code in self._ERROR_MESSAGE:
code = self.code
return '[%s] %s' % (code, self._ERROR_MESSAGE[code])
class Socks(_socket_socket):
"""
A **socket.socket**-like interface through a SOCKS5 proxy connection.
Tor does not support proxy authentication, so neither does this class.
This class supports the context manager protocol. When used this way, the
socket will automatically close when leaving the context. An example:
::
from test.network import Socks
with Socks(('127.0.0.1', 9050)) as socks:
socks.settimeout(2)
socks.connect(('www.torproject.org', 443))
"""
def __init__(self, proxy_addr, family = socket.AF_INET,
type_ = socket.SOCK_STREAM, proto = 0, _sock = None):
"""
Creates a SOCKS5-aware socket which will route connections through the
proxy_addr SOCKS5 proxy. Currently, only IPv4 TCP connections are
supported, so the defaults for family and type_ are your best option.
:param tuple proxy_addr: address of the SOCKS5 proxy, for IPv4 this
contains (host, port)
:param int family: address family of the socket
:param int type_: address type of the socket (see **socket.socket** for
more information about family and type_)
:returns: :class:`~test.network.Socks`
"""
_socket_socket.__init__(self, family, type_, proto, _sock)
self._proxy_addr = proxy_addr
def __enter__(self, *args, **kwargs):
return self
def __exit__(self, exit_type, value, traceback):
self.close()
return False
def _recvall(self, expected_size):
"""
Returns expected number bytes from the socket, or dies trying.
:param int expected_size: number of bytes to return
:returns:
* **str** in Python 2 (bytes is str)
* **bytes** in Python 3
:raises:
* :class:`socket.error` for socket errors
* :class:`test.SocksError` if the received data was more that expected
"""
while True:
response = self.recv(expected_size * 2)
if len(response) == 0:
raise socket.error('socket closed unexpectedly?')
elif len(response) == expected_size:
return response
elif len(response) > expected_size:
raise SocksError(0x01)
def _ints_to_bytes(self, integers):
"""
Returns a byte string converted from integers.
:param list integers: list of ints to convert
:returns:
* **str** in Python 2 (bytes is str)
* **bytes** in Python 3
"""
if bytes is str:
bytes_ = ''.join([chr(x) for x in integers]) # Python 2
else:
bytes_ = bytes(integers) # Python 3
return bytes_
def _bytes_to_ints(self, bytes_):
"""
Returns a tuple of integers converted from a string (Python 2) or
bytes (Python 3).
:param str,bytes bytes_: byte string to convert
:returns: **list** of ints
"""
try:
integers = [ord(x) for x in bytes_] # Python 2
except TypeError:
integers = [x for x in bytes_] # Python 3
return tuple(integers)
def _pack_string(self, string_):
"""
Returns a packed string for sending over a socket.
:param str string_: string to convert
:returns:
* **str** in Python 2 (bytes is str)
* **bytes** in Python 3
"""
try:
return struct.pack('>%ss' % len(string_), string_)
except struct.error:
# Python 3: encode str to bytes
return struct.pack('>%ss' % len(string_), string_.encode())
def connect(self, address):
"""
Establishes a connection to address through the SOCKS5 proxy.
:param tuple address: target address, for IPv4 this contains
(host, port)
:raises: :class:`test.SocksError` for any errors
"""
_socket_socket.connect(self, (self._proxy_addr[0], self._proxy_addr[1]))
# ask for non-authenticated connection
self.sendall(self._ints_to_bytes(SOCKS5_NOAUTH_GREETING))
response = self._bytes_to_ints(self._recvall(2))
if response != SOCKS5_NOAUTH_RESPONSE:
raise SocksError(0x01)
if stem.util.connection.is_valid_ipv4_address(address[0]):
header = self._ints_to_bytes(SOCKS5_CONN_BY_IPV4)
header = header + socket.inet_aton(address[0])
else:
# As a last gasp, try connecting by name
header = self._ints_to_bytes(SOCKS5_CONN_BY_NAME)
header = header + self._ints_to_bytes([len(address[0])])
header = header + self._pack_string(address[0])
header = header + struct.pack('>H', address[1])
self.sendall(header)
response = self._bytes_to_ints(self._recvall(10))
# check the status byte
if response[1] != 0x00:
raise SocksError(response[1])
def connect_ex(self, address):
"""
Not Implemented.
"""
raise NotImplementedError
class SocksPatch(object):
"""
Monkey-patch **socket.socket** to use :class:`~test.network.Socks`, instead.
Classes in the patched context (e.g. urllib.urlopen in the example below)
do not use the SOCKS5 proxy for domain name resolution and such information
may be leaked.
::
import urllib
from test.network import SocksPatch
with SocksPatch(('127.0.0.1', 9050)):
with urllib.urlopen('https://www.torproject.org') as f:
for line in f.readline():
print line
"""
def __init__(self, *args, **kwargs):
self._partial = functools.partial(Socks, *args, **kwargs)
def __enter__(self):
socket.socket = self._partial
return self
def __exit__(self, exit_type, value, traceback):
socket.socket = _socket_socket
def external_ip(host, port):
"""
Returns the externally visible IP address when using a SOCKS4a proxy.
Negotiates the socks connection, connects to ipconfig.me and requests
http://ifconfig.me/ip to find out the externally visible IP.
Supports only SOCKS4a proxies.
:param str host: hostname/IP of the proxy server
:param int port: port on which the proxy server is listening
:returns: externally visible IP address, or None if it isn't able to
:raises: :class:`stem.socket.SocketError`: unable to connect a socket to the socks server
"""
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((host, int(port)))
except Exception as exc:
raise SocketError('Failed to connect to the socks server: ' + str(exc))
try:
negotiate_socks(sock, 'ifconfig.me', 80)
sock.sendall(ip_request)
response = sock.recv(1000)
# everything after the blank line is the 'data' in a HTTP response
# The response data for our request for request should be an IP address + '\n'
return response[response.find('\r\n\r\n'):].strip()
except Exception as exc:
return None
def negotiate_socks(sock, host, port):
"""
Negotiate with a socks4a server. Closes the socket and raises an exception on
failure.
:param socket sock: socket connected to socks4a server
:param str host: hostname/IP to connect to
:param int port: port to connect to
:raises: :class:`stem.ProtocolError` if the socks server doesn't grant our request
:returns: a list with the IP address and the port that the proxy connected to
"""
# SOCKS4a request here - http://en.wikipedia.org/wiki/SOCKS#Protocol
request = b'\x04\x01' + struct.pack('!H', port) + b'\x00\x00\x00\x01' + b'\x00' + stem.util.str_tools._to_bytes(host) + b'\x00'
sock.sendall(request)
response = sock.recv(8)
if len(response) != 8 or response[0:2] != b'\x00\x5a':
sock.close()
raise ProtocolError(error_msgs.get(response[1], 'SOCKS server returned unrecognized error code'))
return [socket.inet_ntoa(response[4:]), struct.unpack('!H', response[2:4])[0]]
stem-1.6.0/test/__init__.py 0000664 0001750 0001750 00000010202 13165210535 016274 0 ustar atagar atagar 0000000 0000000 # Copyright 2011-2017, Damian Johnson and The Tor Project
# See LICENSE for licensing information
"""
Unit and integration tests for the stem library. Helpers include...
::
get_new_capabilities - missing capabilities found while testing
register_new_capability - note that tor feature stem lacks
get_all_combinations - provides all combinations of attributes
tor_version - provides the version of tor we're testing against
"""
import collections
import itertools
import os
import stem.util.enum
import stem.version
__all__ = [
'network',
'output',
'prompt',
'runner',
]
# Integration targets fall into two categories:
#
# * Run Targets (like RUN_COOKIE and RUN_PTRACE) which customize our torrc.
# We do an integration test run for each run target we get.
#
# * Attribute Target (like CHROOT and ONLINE) which indicates
# non-configuration changes to your test runs. These are applied to all
# integration runs that we perform.
Target = stem.util.enum.UppercaseEnum(
'ONLINE',
'RELATIVE',
'CHROOT',
'RUN_NONE',
'RUN_OPEN',
'RUN_PASSWORD',
'RUN_COOKIE',
'RUN_MULTIPLE',
'RUN_SOCKET',
'RUN_SCOOKIE',
'RUN_PTRACE',
'RUN_ALL',
)
AsyncTestArgs = collections.namedtuple('AsyncTestArgs', ['test_dir', 'tor_cmd'])
TOR_VERSION = None
# We make some paths relative to stem's base directory (the one above us)
# rather than the process' cwd. This doesn't end with a slash.
STEM_BASE = os.path.sep.join(__file__.split(os.path.sep)[:-2])
# Store new capabilities (events, descriptor entries, etc.)
NEW_CAPABILITIES = []
NEW_CAPABILITIES_SUPPRESSION_TOKENS = set()
# File extensions of contents that should be ignored.
IGNORED_FILE_TYPES = []
with open(os.path.join(STEM_BASE, '.gitignore')) as ignore_file:
for line in ignore_file:
if line.startswith('*.'):
IGNORED_FILE_TYPES.append(line[2:].strip())
if os.path.exists(os.path.join(STEM_BASE, '.travis.yml')):
IGNORED_FILE_TYPES.append('.travis.yml')
def get_new_capabilities():
"""
Provides a list of capabilities tor supports but stem doesn't, as discovered
while running our tests.
:returns: **list** of (type, message) tuples for the capabilities
"""
return NEW_CAPABILITIES
def register_new_capability(capability_type, msg, suppression_token = None):
"""
Register new capability found during the tests.
:param str capability_type: type of capability this is
:param str msg: description of what we found
:param str suppression_token: skip registration if this token's already been
provided
"""
if suppression_token not in NEW_CAPABILITIES_SUPPRESSION_TOKENS:
NEW_CAPABILITIES.append((capability_type, msg))
if suppression_token:
NEW_CAPABILITIES_SUPPRESSION_TOKENS.add(suppression_token)
def get_all_combinations(attr, include_empty = False):
"""
Provides an iterator for all combinations of a set of attributes. For
instance...
::
>>> list(test.get_all_combinations(['a', 'b', 'c']))
[('a',), ('b',), ('c',), ('a', 'b'), ('a', 'c'), ('b', 'c'), ('a', 'b', 'c')]
:param list attr: attributes to provide combinations for
:param bool include_empty: includes an entry with zero items if True
:returns: iterator for all combinations
"""
# Makes an itertools.product() call for 'i' copies of attr...
#
# * itertools.product(attr) => all one-element combinations
# * itertools.product(attr, attr) => all two-element combinations
# * ... etc
if include_empty:
yield ()
seen = set()
for index in range(1, len(attr) + 1):
product_arg = [attr for _ in range(index)]
for item in itertools.product(*product_arg):
# deduplicate, sort, and only provide if we haven't seen it yet
item = tuple(sorted(set(item)))
if item not in seen:
seen.add(item)
yield item
def tor_version(tor_path = None):
"""
Provides the version of tor we're testing against.
:param str tor_path: location of tor executable to cehck the version of
:returns: :class:`~stem.version.Version` of tor invoked by our integration
tests
"""
global TOR_VERSION
if TOR_VERSION is None or tor_path:
TOR_VERSION = stem.version.get_system_tor_version(tor_path)
return TOR_VERSION
stem-1.6.0/test/runner.py 0000664 0001750 0001750 00000044712 13136412262 016062 0 ustar atagar atagar 0000000 0000000 # Copyright 2011-2017, Damian Johnson and The Tor Project
# See LICENSE for licensing information
"""
Runtime context for the integration tests. This is used both by the test runner
to start and stop tor, and by the integration tests themselves for information
about the tor test instance they're running against.
::
RunnerStopped - Runner doesn't have an active tor instance
TorInaccessable - Tor can't be queried for the information
exercise_controller - basic sanity check that a controller connection can be used
get_runner - Singleton for fetching our runtime context.
Runner - Runtime context for our integration tests.
|- start - prepares and starts a tor instance for our tests to run against
|- stop - stops our tor instance and cleans up any temporary files
|- is_running - checks if our tor test instance is running
|- is_accessible - checks if our tor instance can be connected to
|- get_options - custom torrc options used for our test instance
|- get_test_dir - testing directory path
|- get_torrc_path - path to our tor instance's torrc
|- get_torrc_contents - contents of our tor instance's torrc
|- get_auth_cookie_path - path for our authentication cookie if we have one
|- get_tor_cwd - current working directory of our tor process
|- get_chroot - provides the path of our emulated chroot if we have one
|- get_pid - process id of our tor process
|- get_tor_socket - provides a socket to our test instance
|- get_tor_controller - provides a controller for our test instance
+- get_tor_command - provides the command used to start tor
"""
import logging
import os
import shutil
import stat
import tempfile
import threading
import time
import uuid
import stem.connection
import stem.prereq
import stem.process
import stem.socket
import stem.util.conf
import stem.util.enum
import test
from test.output import println, STATUS, ERROR, SUBSTATUS, NO_NL
CONFIG = stem.util.conf.config_dict('test', {
'integ.test_directory': './test/data',
'integ.log': './test/data/log',
'target.torrc': {},
})
SOCKS_PORT = 1112
BASE_TORRC = """# configuration for stem integration tests
DataDirectory %%s
SocksPort %i
DownloadExtraInfo 1
Log notice stdout
Log notice file %%s/tor_log
""" % SOCKS_PORT
# singleton Runner instance
INTEG_RUNNER = None
# control authentication options and attributes
CONTROL_PASSWORD = 'pw'
CONTROL_PORT = 1111
CONTROL_SOCKET_PATH = os.path.join(tempfile.gettempdir(), str(uuid.uuid4()), 'socket')
Torrc = stem.util.enum.Enum(
('PORT', 'ControlPort %i' % CONTROL_PORT),
('COOKIE', 'CookieAuthentication 1'),
('PASSWORD', 'HashedControlPassword 16:8C423A41EF4A542C6078985270AE28A4E04D056FB63F9F201505DB8E06'),
('SOCKET', 'ControlSocket %s' % CONTROL_SOCKET_PATH),
('PTRACE', 'DisableDebuggerAttachment 0'),
)
class RunnerStopped(Exception):
"Raised when we try to use a Runner that doesn't have an active tor instance"
class TorInaccessable(Exception):
'Raised when information is needed from tor but the instance we have is inaccessible'
def exercise_controller(test_case, controller):
"""
Checks that we can now use the socket by issuing a 'GETINFO config-file'
query. Controller can be either a :class:`stem.socket.ControlSocket` or
:class:`stem.control.BaseController`.
:param unittest.TestCase test_case: test being ran
:param controller: tor controller connection to be authenticated
"""
runner = get_runner()
torrc_path = runner.get_torrc_path()
if isinstance(controller, stem.socket.ControlSocket):
controller.send('GETINFO config-file')
config_file_response = controller.recv()
else:
config_file_response = controller.msg('GETINFO config-file')
test_case.assertEqual('config-file=%s\nOK' % torrc_path, str(config_file_response))
def get_runner():
"""
Singleton for the runtime context of integration tests.
:returns: :class:`test.runner.Runner` with context for our integration tests
"""
global INTEG_RUNNER
if not INTEG_RUNNER:
INTEG_RUNNER = Runner()
return INTEG_RUNNER
class _MockChrootFile(object):
"""
Wrapper around a file object that strips given content from readline()
responses. This is used to simulate a chroot setup by removing the prefix
directory from the paths we report.
"""
def __init__(self, wrapped_file, strip_text):
self.wrapped_file = wrapped_file
self.strip_text = strip_text
def readline(self):
return self.wrapped_file.readline().replace(self.strip_text, '')
class Runner(object):
def __init__(self):
self.attribute_targets = []
self._runner_lock = threading.RLock()
# runtime attributes, set by the start method
self._test_dir = ''
self._tor_cmd = None
self._tor_cwd = ''
self._torrc_contents = ''
self._custom_opts = None
self._tor_process = None
self._chroot_path = None
# set if we monkey patch stem.socket.recv_message()
self._original_recv_message = None
# The first controller to attach takes ownership so tor will promptly
# terminate if the tests do. As such we need to ensure that first
# connection is our runner's.
self._owner_controller = None
def start(self, config_target, attribute_targets, tor_cmd):
"""
Makes temporary testing resources and starts tor, blocking until it
completes.
:param str config_target: **Target** for this test run's torrc settings
:param list attribute_targets: **Targets** for our non-configuration attributes
:param str tor_cmd: command to start tor with
:raises: OSError if unable to run test preparations or start tor
"""
with self._runner_lock:
self.attribute_targets = attribute_targets
# if we're holding on to a tor process (running or not) then clean up after
# it so we can start a fresh instance
if self._tor_process:
self.stop()
println('Setting up a test instance...', STATUS)
# if 'test_directory' is unset then we make a new data directory in /tmp
# and clean it up when we're done
config_test_dir = CONFIG['integ.test_directory']
if config_test_dir:
self._test_dir = stem.util.system.expand_path(config_test_dir, test.STEM_BASE)
else:
self._test_dir = tempfile.mktemp('-stem-integ')
original_cwd, data_dir_path = os.getcwd(), self._test_dir
self._tor_cmd = stem.util.system.expand_path(tor_cmd) if os.path.sep in tor_cmd else tor_cmd
if test.Target.RELATIVE in self.attribute_targets:
tor_cwd = os.path.dirname(self._test_dir)
if not os.path.exists(tor_cwd):
os.makedirs(tor_cwd)
os.chdir(tor_cwd)
data_dir_path = './%s' % os.path.basename(self._test_dir)
config_csv = CONFIG['target.torrc'].get(config_target)
extra_torrc_opts = []
if config_csv:
for opt in config_csv.split(','):
opt = opt.strip()
if opt in Torrc.keys():
extra_torrc_opts.append(Torrc[opt])
else:
raise ValueError("'%s' isn't a test.runner.Torrc enumeration" % opt)
self._custom_opts = extra_torrc_opts
self._torrc_contents = BASE_TORRC % (data_dir_path, data_dir_path)
if extra_torrc_opts:
self._torrc_contents += '\n'.join(extra_torrc_opts) + '\n'
try:
self._tor_cwd = os.getcwd()
self._run_setup()
self._start_tor(self._tor_cmd)
# strip the testing directory from recv_message responses if we're
# simulating a chroot setup
if test.Target.CHROOT in self.attribute_targets and not self._original_recv_message:
# TODO: when we have a function for telling stem the chroot we'll
# need to set that too
self._original_recv_message = stem.socket.recv_message
self._chroot_path = data_dir_path
def _chroot_recv_message(control_file):
return self._original_recv_message(_MockChrootFile(control_file, data_dir_path))
stem.socket.recv_message = _chroot_recv_message
if self.is_accessible():
self._owner_controller = self.get_tor_controller(True)
if test.Target.RELATIVE in self.attribute_targets:
os.chdir(original_cwd) # revert our cwd back to normal
except OSError as exc:
raise exc
def stop(self):
"""
Stops our tor test instance and cleans up any temporary resources.
"""
with self._runner_lock:
println('Shutting down tor... ', STATUS, NO_NL)
if self._owner_controller:
self._owner_controller.close()
self._owner_controller = None
if self._tor_process:
# if the tor process has stopped on its own then the following raises
# an OSError ([Errno 3] No such process)
try:
self._tor_process.kill()
except OSError:
pass
self._tor_process.wait() # blocks until the process is done
# if we've made a temporary data directory then clean it up
if self._test_dir and CONFIG['integ.test_directory'] == '':
shutil.rmtree(self._test_dir, ignore_errors = True)
# reverts any mocking of stem.socket.recv_message
if self._original_recv_message:
stem.socket.recv_message = self._original_recv_message
self._original_recv_message = None
# clean up our socket directory if we made one
socket_dir = os.path.dirname(CONTROL_SOCKET_PATH)
if os.path.exists(socket_dir):
shutil.rmtree(socket_dir, ignore_errors = True)
self._test_dir = ''
self._tor_cmd = None
self._tor_cwd = ''
self._torrc_contents = ''
self._custom_opts = None
self._tor_process = None
println('done', STATUS)
def is_running(self):
"""
Checks if we're running a tor test instance and that it's alive.
:returns: True if we have a running tor test instance, False otherwise
"""
with self._runner_lock:
# Check for an unexpected shutdown by calling subprocess.Popen.poll(),
# which returns the exit code or None if we're still running.
if self._tor_process and self._tor_process.poll() is not None:
# clean up the temporary resources and note the unexpected shutdown
self.stop()
println('tor shut down unexpectedly', ERROR)
return bool(self._tor_process)
def is_accessible(self):
"""
Checks if our tor instance has a method of being connected to or not.
:returns: True if tor has a control socket or port, False otherwise
"""
return Torrc.PORT in self._custom_opts or Torrc.SOCKET in self._custom_opts
def get_options(self):
"""
Provides the custom torrc options our tor instance is running with.
:returns: list of Torrc enumerations being used by our test instance
"""
return self._custom_opts
def get_test_dir(self, resource = None):
"""
Provides the absolute path for our testing directory or a file within it.
:param str resource: file within our test directory to provide the path for
:returns: str with our test directory's absolute path or that of a file within it
:raises: :class:`test.runner.RunnerStopped` if we aren't running
"""
if resource:
return os.path.join(self._get('_test_dir'), resource)
else:
return self._get('_test_dir')
def get_torrc_path(self, ignore_chroot = False):
"""
Provides the absolute path for where our testing torrc resides.
:param bool ignore_chroot: provides the real path, rather than the one that tor expects if True
:returns: str with our torrc path
:raises: RunnerStopped if we aren't running
"""
test_dir = self._get('_test_dir')
torrc_path = os.path.join(test_dir, 'torrc')
if not ignore_chroot and self._chroot_path and torrc_path.startswith(self._chroot_path):
torrc_path = torrc_path[len(self._chroot_path):]
return torrc_path
def get_torrc_contents(self):
"""
Provides the contents of our torrc.
:returns: str with the contents of our torrc, lines are newline separated
:raises: :class:`test.runner.RunnerStopped` if we aren't running
"""
return self._get('_torrc_contents')
def get_auth_cookie_path(self):
"""
Provides the absolute path for our authentication cookie if we have one.
If running with an emulated chroot this is uneffected, still providing the
real path.
:returns: str with our auth cookie path
:raises: :class:`test.runner.RunnerStopped` if we aren't running
"""
test_dir = self._get('_test_dir')
return os.path.join(test_dir, 'control_auth_cookie')
def get_tor_cwd(self):
"""
Provides the current working directory of our tor process.
"""
return self._get('_tor_cwd')
def get_chroot(self):
"""
Provides the path we're using to emulate a chroot environment. This is None
if we aren't emulating a chroot setup.
:returns: str with the path of our emulated chroot
"""
return self._chroot_path
def get_pid(self):
"""
Provides the process id of the tor process.
:returns: int pid for the tor process
:raises: :class:`test.runner.RunnerStopped` if we aren't running
"""
tor_process = self._get('_tor_process')
return tor_process.pid
def get_tor_socket(self, authenticate = True):
"""
Provides a socket connected to our tor test instance.
:param bool authenticate: if True then the socket is authenticated
:returns: :class:`stem.socket.ControlSocket` connected with our testing instance
:raises: :class:`test.runner.TorInaccessable` if tor can't be connected to
"""
if Torrc.PORT in self._custom_opts:
control_socket = stem.socket.ControlPort(port = CONTROL_PORT)
elif Torrc.SOCKET in self._custom_opts:
control_socket = stem.socket.ControlSocketFile(CONTROL_SOCKET_PATH)
else:
raise TorInaccessable('Unable to connect to tor')
if authenticate:
stem.connection.authenticate(control_socket, CONTROL_PASSWORD, self.get_chroot())
return control_socket
def get_tor_controller(self, authenticate = True):
"""
Provides a controller connected to our tor test instance.
:param bool authenticate: if True then the socket is authenticated
:returns: :class:`stem.socket.Controller` connected with our testing instance
:raises: :class: `test.runner.TorInaccessable` if tor can't be connected to
"""
control_socket = self.get_tor_socket(False)
controller = stem.control.Controller(control_socket)
if authenticate:
controller.authenticate(password = CONTROL_PASSWORD, chroot_path = self.get_chroot())
return controller
def get_tor_command(self, base_cmd = False):
"""
Provides the command used to run our tor instance.
:param bool base_cmd: provides just the command name if true rather than
the full '--tor path' argument
"""
return os.path.basename(self._get('_tor_cmd')) if base_cmd else self._get('_tor_cmd')
def _get(self, attr):
"""
Fetches one of our attributes in a thread safe manner, raising if we aren't
running.
:param str attr: class variable that we want to fetch
:returns: value of the fetched variable
:returns: :class:`test.runner.RunnerStopped` if we aren't running
"""
with self._runner_lock:
if self.is_running():
return self.__dict__[attr]
else:
raise RunnerStopped()
def _run_setup(self):
"""
Makes a temporary runtime resources of our integration test instance.
:raises: OSError if unsuccessful
"""
# makes a temporary data directory if needed
try:
println(' making test directory (%s)... ' % self._test_dir, STATUS, NO_NL)
if os.path.exists(self._test_dir):
println('skipped', STATUS)
else:
os.makedirs(self._test_dir)
println('done', STATUS)
except OSError as exc:
println('failed (%s)' % exc, ERROR)
raise exc
# Tor checks during startup that the directory a control socket resides in
# is only accessible by the tor user (and refuses to finish starting if it
# isn't).
if Torrc.SOCKET in self._custom_opts:
try:
socket_dir = os.path.dirname(CONTROL_SOCKET_PATH)
println(' making control socket directory (%s)... ' % socket_dir, STATUS, NO_NL)
if os.path.exists(socket_dir) and stat.S_IMODE(os.stat(socket_dir).st_mode) == 0o700:
println('skipped', STATUS)
else:
if not os.path.exists(socket_dir):
os.makedirs(socket_dir)
os.chmod(socket_dir, 0o700)
println('done', STATUS)
except OSError as exc:
println('failed (%s)' % exc, ERROR)
raise exc
# configures logging
logging_path = CONFIG['integ.log']
if logging_path:
logging_path = stem.util.system.expand_path(logging_path, test.STEM_BASE)
println(' configuring logger (%s)... ' % logging_path, STATUS, NO_NL)
# delete the old log
if os.path.exists(logging_path):
os.remove(logging_path)
logging.basicConfig(
filename = logging_path,
level = logging.DEBUG,
format = '%(asctime)s [%(levelname)s] %(message)s',
datefmt = '%D %H:%M:%S',
)
println('done', STATUS)
else:
println(' configuring logger... skipped', STATUS)
# writes our testing torrc
torrc_dst = os.path.join(self._test_dir, 'torrc')
try:
println(' writing torrc (%s)... ' % torrc_dst, STATUS, NO_NL)
torrc_file = open(torrc_dst, 'w')
torrc_file.write(self._torrc_contents)
torrc_file.close()
println('done', STATUS)
for line in self._torrc_contents.strip().splitlines():
println(' %s' % line.strip(), SUBSTATUS)
println()
except Exception as exc:
println('failed (%s)\n' % exc, ERROR)
raise OSError(exc)
def _start_tor(self, tor_cmd):
"""
Initializes a tor process. This blocks until initialization completes or we
error out.
:param str tor_cmd: command to start tor with
:raises: OSError if we either fail to create the tor process or reached a timeout without success
"""
println('Starting %s...\n' % tor_cmd, STATUS)
start_time = time.time()
try:
self._tor_process = stem.process.launch_tor(
tor_cmd = tor_cmd,
torrc_path = os.path.join(self._test_dir, 'torrc'),
completion_percent = 100 if test.Target.ONLINE in self.attribute_targets else 5,
init_msg_handler = lambda line: println(' %s' % line, SUBSTATUS),
take_ownership = True,
)
runtime = time.time() - start_time
println(' done (%i seconds)\n' % runtime, STATUS)
except OSError as exc:
println(' failed to start tor: %s\n' % exc, ERROR)
raise exc
stem-1.6.0/test/output.py 0000664 0001750 0001750 00000016423 13146362451 016114 0 ustar atagar atagar 0000000 0000000 # Copyright 2011-2017, Damian Johnson and The Tor Project
# See LICENSE for licensing information
"""
Variety of filters for the python unit testing output, which can be chained
together for improved readability.
"""
import re
import sys
import stem.util.enum
import stem.util.test_tools
from stem.util import system, term
COLOR_SUPPORT = sys.stdout.isatty() and not system.is_windows()
DIVIDER = '=' * 70
HEADER_ATTR = (term.Color.CYAN, term.Attr.BOLD)
CATEGORY_ATTR = (term.Color.GREEN, term.Attr.BOLD)
NO_NL = 'no newline'
STDERR = 'stderr'
# formatting for various categories of messages
STATUS = (term.Color.BLUE, term.Attr.BOLD)
SUBSTATUS = (term.Color.BLUE, )
SUCCESS = (term.Color.GREEN, term.Attr.BOLD)
ERROR = (term.Color.RED, term.Attr.BOLD)
LineType = stem.util.enum.Enum('OK', 'FAIL', 'ERROR', 'SKIPPED', 'CONTENT')
LINE_ENDINGS = {
' ... ok': LineType.OK,
' ... FAIL': LineType.FAIL,
' ... ERROR': LineType.ERROR,
' ... skipped': LineType.SKIPPED,
}
LINE_ATTR = {
LineType.OK: (term.Color.GREEN,),
LineType.FAIL: (term.Color.RED, term.Attr.BOLD),
LineType.ERROR: (term.Color.RED, term.Attr.BOLD),
LineType.SKIPPED: (term.Color.BLUE,),
LineType.CONTENT: (term.Color.CYAN,),
}
SUPPRESS_STDOUT = False # prevent anything from being printed to stdout
def println(msg = '', *attr):
if SUPPRESS_STDOUT and STDERR not in attr:
return
attr = _flatten(attr)
no_newline = False
stream = sys.stderr if STDERR in attr else sys.stdout
if NO_NL in attr:
no_newline = True
attr.remove(NO_NL)
if STDERR in attr:
attr.remove(STDERR)
if COLOR_SUPPORT and attr:
msg = term.format(msg, *attr)
if not no_newline:
msg += '\n'
stream.write(msg)
stream.flush()
def print_divider(msg, is_header = False):
attr = HEADER_ATTR if is_header else CATEGORY_ATTR
println('%s\n%s\n%s\n' % (DIVIDER, msg.center(70), DIVIDER), *attr)
def print_logging(logging_buffer):
if SUPPRESS_STDOUT:
return
if not logging_buffer.is_empty():
for entry in logging_buffer:
println(entry.replace('\n', '\n '), term.Color.MAGENTA)
print('')
def apply_filters(testing_output, *filters):
"""
Gets the tests results, possibly processed through a series of filters. The
filters are applied in order, each getting the output of the previous.
A filter's input arguments should be the line's (type, content) and the
output is either a string with the new content or None if the line should be
omitted.
:param str testing_output: output from the unit testing
:param list filters: functors to be applied to each line of the results
:returns: str with the processed test results
"""
results = []
for line in testing_output.splitlines():
# determine the type of the line
line_type = LineType.CONTENT
for ending in LINE_ENDINGS:
if ending in line:
line_type = LINE_ENDINGS[ending]
break
for result_filter in filters:
line = result_filter(line_type, line)
if line is None:
break
if line is not None:
results.append(line)
return '\n'.join(results) + '\n'
def colorize(line_type, line_content):
"""
Applies escape sequences so each line is colored according to its type.
"""
if COLOR_SUPPORT:
line_content = term.format(line_content, *LINE_ATTR[line_type])
return line_content
def strip_module(line_type, line_content):
"""
Removes the module name from testing output. This information tends to be
repetitive, and redundant with the headers.
"""
m = re.match('.*( \(test\..*?\)).*', line_content)
if m:
line_content = line_content.replace(m.groups()[0], '', 1)
return line_content
def runtimes(line_type, line_content):
"""
Provides test runtimes if showing verbose results.
"""
m = re.search('(test\.[^)]*)', line_content)
if m and line_type == LineType.OK:
test = '%s.%s' % (m.group(0), line_content.split()[0])
runtime = stem.util.test_tools.test_runtimes().get(test)
if runtime is None:
pass
if runtime >= 1.0:
line_content = '%s (%0.2fs)' % (line_content, runtime)
else:
line_content = '%s (%i ms)' % (line_content, runtime * 1000)
return line_content
def align_results(line_type, line_content):
"""
Strips the normal test results, and adds a right aligned variant instead with
a bold attribute.
"""
if line_type == LineType.CONTENT:
return line_content
# strip our current ending
for ending in LINE_ENDINGS:
if LINE_ENDINGS[ending] == line_type:
line_content = line_content.replace(ending, '', 1)
break
# right align runtimes
if line_content.endswith('s)'):
div = line_content.rfind(' (')
line_content = '%-53s%6s ' % (line_content[:div], line_content[div + 2:-1])
# skipped tests have extra single quotes around the reason
if line_type == LineType.SKIPPED:
line_content = line_content.replace("'(", "(", 1).replace(")'", ")", 1)
if line_type == LineType.OK:
new_ending = 'SUCCESS'
elif line_type in (LineType.FAIL, LineType.ERROR):
new_ending = 'FAILURE'
elif line_type == LineType.SKIPPED:
new_ending = 'SKIPPED'
else:
assert False, 'Unexpected line type: %s' % line_type
return line_content
if COLOR_SUPPORT:
return '%-61s[%s]' % (line_content, term.format(new_ending, term.Attr.BOLD))
else:
return '%-61s[%s]' % (line_content, term.format(new_ending))
class ErrorTracker(object):
"""
Stores any failure or error results we've encountered.
"""
def __init__(self):
self._errors = []
self._error_modules = set()
self._category = None
self._error_noted = False
def register_error(self):
"""
If called then has_errors_occured() will report that an error has occured,
even if we haven't encountered an error message in the tests.
"""
self._error_noted = True
def set_category(self, category):
"""
Optional label that will be presented with testing failures until another
category is specified. If set to None then no category labels are included.
For tests with a lot of output this is intended to help narrow the haystack
in which the user needs to look for failures. In practice this is mostly
used to specify the integ target we're running under.
:param str category: category to label errors as being under
"""
self._category = category
def has_errors_occured(self):
return self._error_noted or bool(self._errors)
def get_filter(self):
def _error_tracker(line_type, line_content):
if line_type in (LineType.FAIL, LineType.ERROR):
if self._category:
self._errors.append('[%s] %s' % (self._category, line_content))
else:
self._errors.append(line_content)
module_match = re.match('.*\((test\.\S+)\.\S+\).*', line_content)
if module_match:
self._error_modules.add(module_match.group(1))
return line_content
return _error_tracker
def get_modules(self):
return self._error_modules
def __iter__(self):
for error_line in self._errors:
yield error_line
def _flatten(seq):
# Flattens nested collections into a single list. For instance...
#
# >>> _flatten([1, [2, 3], 4])
# [1, 2, 3, 4]
result = []
for item in seq:
if (isinstance(item, (tuple, list))):
result.extend(_flatten(item))
else:
result.append(item)
return result
stem-1.6.0/test/unit/ 0000775 0001750 0001750 00000000000 13177674754 015173 5 ustar atagar atagar 0000000 0000000 stem-1.6.0/test/unit/tor_man_with_unknown 0000664 0001750 0001750 00000004577 13115423200 021346 0 ustar atagar atagar 0000000 0000000 '\" t
.\" Title: tor
.\" Author: [see the "AUTHORS" section]
.\" Generator: DocBook XSL Stylesheets v1.76.1
.\" Date: 10/03/2015
.\" Manual: Tor Manual
.\" Source: Tor
.\" Language: English
.\"
.TH "TOR" "1" "10/03/2015" "Tor" "Tor Manual"
.\" -----------------------------------------------------------------
.\" * Define some portability stuff
.\" -----------------------------------------------------------------
.\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.\" http://bugs.debian.org/507673
.\" http://lists.gnu.org/archive/html/groff/2009-02/msg00013.html
.\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.\" -----------------------------------------------------------------
.\" * set default formatting
.\" -----------------------------------------------------------------
.\" disable hyphenation
.nh
.\" disable justification (adjust text to left margin only)
.ad l
.\" -----------------------------------------------------------------
.\" * MAIN CONTENT STARTS HERE *
.\" -----------------------------------------------------------------
.SH "NAME"
tor \- The second\-generation onion router
.SH "GENERAL OPTIONS"
.PP
\fBBandwidthRate\fR \fIN\fR \fBbytes\fR|\fBKBytes\fR|\fBMBytes\fR|\fBGBytes\fR|\fBKBits\fR|\fBMBits\fR|\fBGBits\fR
.RS 4
A token bucket limits the average incoming bandwidth usage on this node to the specified number of bytes per second, and the average outgoing bandwidth usage to that same value\&. If you want to run a relay in the public network, this needs to be
\fIat the very least\fR
30 KBytes (that is, 30720 bytes)\&. (Default: 1 GByte)
With this option, and in other options that take arguments in bytes, KBytes, and so on, other formats are also supported\&. Notably, "KBytes" can also be written as "kilobytes" or "kb"; "MBytes" can be written as "megabytes" or "MB"; "kbits" can be written as "kilobits"; and so forth\&. Tor also accepts "byte" and "bit" in the singular\&. The prefixes "tera" and "T" are also recognized\&. If no units are given, we default to bytes\&. To avoid confusion, we recommend writing "bytes" or "bits" explicitly, since it\(cqs easy to forget that "B" means bytes, not bits\&.
.RE
.PP
.SH "NEW OPTIONS"
.PP
\fBSpiffyNewOption\fR \fItransport\fR exec \fIpath\-to\-binary\fR [options]
.RS 4
Description of this new option.
.RE
.PP
stem-1.6.0/test/unit/manual.py 0000664 0001750 0001750 00000042072 13165210554 017004 0 ustar atagar atagar 0000000 0000000 """
Unit testing for the stem.manual module.
"""
import io
import os
import re
import sqlite3
import tempfile
import unittest
import stem.prereq
import stem.manual
import stem.util.system
import test.require
try:
# account for urllib's change between python 2.x and 3.x
import urllib.request as urllib
except ImportError:
import urllib2 as urllib
try:
# added in python 3.3
from unittest.mock import Mock, patch
except ImportError:
from mock import Mock, patch
try:
# added in python 2.7
from collections import OrderedDict
except ImportError:
from stem.util.ordereddict import OrderedDict
URL_OPEN = 'urllib.request.urlopen' if stem.prereq.is_python_3() else 'urllib2.urlopen'
EXAMPLE_MAN_PATH = os.path.join(os.path.dirname(__file__), 'tor_man_example')
UNKNOWN_OPTIONS_MAN_PATH = os.path.join(os.path.dirname(__file__), 'tor_man_with_unknown')
EXPECTED_DESCRIPTION = 'Tor is a connection-oriented anonymizing communication service. Users choose a source-routed path through a set of nodes, and negotiate a "virtual circuit" through the network, in which each node knows its predecessor and successor, but no others. Traffic flowing down the circuit is unwrapped by a symmetric key at each node, which reveals the downstream node.'
EXPECTED_CLI_OPTIONS = {
'-f FILE': 'Specify a new configuration file to contain further Tor configuration options OR pass - to make Tor read its configuration from standard input. (Default: @CONFDIR@/torrc, or $HOME/.torrc if that file is not found)',
'-h, -help': 'Display a short help message and exit.',
'--allow-missing-torrc': 'Do not require that configuration file specified by -f exist if default torrc can be accessed.',
}
EXPECTED_SIGNALS = {
'SIGHUP': 'The signal instructs Tor to reload its configuration (including closing and reopening logs), and kill and restart its helper processes if applicable.',
'SIGTERM': 'Tor will catch this, clean up and sync to disk if necessary, and exit.',
'SIGINT': 'Tor clients behave as with SIGTERM; but Tor servers will do a controlled slow shutdown, closing listeners and waiting 30 seconds before exiting. (The delay can be configured with the ShutdownWaitLength config option.)',
}
EXPECTED_FILES = {
'@LOCALSTATEDIR@/lib/tor/': 'The tor process stores keys and other data here.',
'DataDirectory/cached-status/': 'The most recently downloaded network status document for each authority. Each file holds one such document; the filenames are the hexadecimal identity key fingerprints of the directory authorities. Mostly obsolete.',
'DataDirectory/cached-certs': 'This file holds downloaded directory key certificates that are used to verify authenticity of documents generated by Tor directory authorities.',
'DataDirectory/state': 'A set of persistent key-value mappings. These are documented in the file. These include: o The current entry guards and their status. o The current bandwidth accounting values (unused so far; see below). o When the file was last written o What version of Tor generated the state file o A short history of bandwidth usage, as produced in the server descriptors.',
'@CONFDIR@/torrc': 'The configuration file, which contains "option value" pairs.',
'DataDirectory/bw_accounting': "Used to track bandwidth accounting values (when the current period starts and ends; how much has been read and written so far this period). This file is obsolete, and the data is now stored in the 'state' file as well. Only used when bandwidth accounting is enabled.",
'$HOME/.torrc': 'Fallback location for torrc, if @CONFDIR@/torrc is not found.',
}
EXPECTED_CONFIG_OPTIONS = OrderedDict()
EXPECTED_CONFIG_OPTIONS['BandwidthRate'] = stem.manual.ConfigOption(
name = 'BandwidthRate',
category = 'General',
usage = 'N bytes|KBytes|MBytes|GBytes|KBits|MBits|GBits',
summary = 'Average bandwidth usage limit',
description = 'A token bucket limits the average incoming bandwidth usage on this node to the specified number of bytes per second, and the average outgoing bandwidth usage to that same value. If you want to run a relay in the public network, this needs to be at the very least 75 KBytes for a relay (that is, 600 kbits) or 50 KBytes for a bridge (400 kbits) -- but of course, more is better; we recommend at least 250 KBytes (2 mbits) if possible. (Default: 1 GByte)\n\nWith this option, and in other options that take arguments in bytes, KBytes, and so on, other formats are also supported. Notably, "KBytes" can also be written as "kilobytes" or "kb"; "MBytes" can be written as "megabytes" or "MB"; "kbits" can be written as "kilobits"; and so forth. Tor also accepts "byte" and "bit" in the singular. The prefixes "tera" and "T" are also recognized. If no units are given, we default to bytes. To avoid confusion, we recommend writing "bytes" or "bits" explicitly, since it\'s easy to forget that "B" means bytes, not bits.')
EXPECTED_CONFIG_OPTIONS['BandwidthBurst'] = stem.manual.ConfigOption(
name = 'BandwidthBurst',
category = 'General',
usage = 'N bytes|KBytes|MBytes|GBytes|KBits|MBits|GBits',
summary = 'Maximum bandwidth usage limit',
description = 'Limit the maximum token bucket size (also known as the burst) to the given number of bytes in each direction. (Default: 1 GByte)')
EXPECTED_CONFIG_OPTIONS['MaxAdvertisedBandwidth'] = stem.manual.ConfigOption(
name = 'MaxAdvertisedBandwidth',
category = 'General',
usage = 'N bytes|KBytes|MBytes|GBytes|KBits|MBits|GBits',
summary = 'Limit for the bandwidth we advertise as being available for relaying',
description = 'If set, we will not advertise more than this amount of bandwidth for our BandwidthRate. Server operators who want to reduce the number of clients who ask to build circuits through them (since this is proportional to advertised bandwidth rate) can thus reduce the CPU demands on their server without impacting network performance.')
EXPECTED_CONFIG_OPTIONS['Bridge'] = stem.manual.ConfigOption(
name = 'Bridge',
category = 'Client',
usage = '[transport] IP:ORPort [fingerprint]',
summary = 'Available bridges',
description = 'When set along with UseBridges, instructs Tor to use the relay at "IP:ORPort" as a "bridge" relaying into the Tor network. If "fingerprint" is provided (using the same format as for DirAuthority), we will verify that the relay running at that location has the right fingerprint. We also use fingerprint to look up the bridge descriptor at the bridge authority, if it\'s provided and if UpdateBridgesFromAuthority is set too.\n\nIf "transport" is provided, and matches to a ClientTransportPlugin line, we use that pluggable transports proxy to transfer data to the bridge.')
CACHED_MANUAL = None
def _cached_manual():
global CACHED_MANUAL
if CACHED_MANUAL is None:
CACHED_MANUAL = stem.manual.Manual.from_cache()
return CACHED_MANUAL
class TestManual(unittest.TestCase):
def test_query(self):
self.assertEqual("If set, this option overrides the default location and file name for Tor's cookie file. (See CookieAuthentication above.)", stem.manual.query('SELECT description FROM torrc WHERE name="CookieAuthFile"').fetchone()[0])
self.assertEqual("If set, this option overrides the default location and file name for Tor's cookie file. (See CookieAuthentication above.)", stem.manual.query('SELECT description FROM torrc WHERE name=?', 'CookieAuthFile').fetchone()[0])
def test_query_on_failure(self):
self.assertRaisesRegexp(sqlite3.OperationalError, 'near "hello": syntax error', stem.manual.query, 'hello world')
def test_has_all_summaries(self):
"""
Check that we have brief, human readable summaries for all of tor's
configuration options. If you add a new config entry then please take a sec
to write a little summary. They're located in 'stem/settings.cfg'.
"""
manual = _cached_manual()
present = set(manual.config_options.keys())
expected = set([key[15:] for key in stem.manual._config(lowercase = False) if key.startswith('manual.summary.')])
missing_options = present.difference(expected)
extra_options = expected.difference(present)
if missing_options:
self.fail("Ran cache_manual.py? Please update Stem's settings.cfg with summaries of the following config options: %s" % ', '.join(missing_options))
elif extra_options:
self.fail("Ran cache_manual.py? Please remove the following summaries from Stem's settings.cfg: %s" % ', '.join(extra_options))
def test_is_important(self):
self.assertTrue(stem.manual.is_important('ExitPolicy'))
self.assertTrue(stem.manual.is_important('exitpolicy'))
self.assertTrue(stem.manual.is_important('EXITPOLICY'))
self.assertFalse(stem.manual.is_important('ConstrainedSockSize'))
def test_minimal_config_option(self):
blank = stem.manual.ConfigOption('UnknownOption')
self.assertEqual(stem.manual.Category.UNKNOWN, blank.category)
self.assertEqual('UnknownOption', blank.name)
self.assertEqual('', blank.usage)
self.assertEqual('', blank.summary)
self.assertEqual('', blank.description)
@test.require.command('man')
def test_parsing_with_example(self):
"""
Read a trimmed copy of tor's man page. This gives a good exercise of our
parser with static content. As new oddball man oddities appear feel free to
expand our example (or add another).
"""
if not stem.manual.HAS_ENCODING_ARG:
self.skipTest('(man lacks --encoding arg on OSX, BSD, and Slackware #18660)')
return
manual = stem.manual.Manual.from_man(EXAMPLE_MAN_PATH)
self.assertEqual('tor - The second-generation onion router', manual.name)
self.assertEqual('tor [OPTION value]...', manual.synopsis)
self.assertEqual(EXPECTED_DESCRIPTION, manual.description)
self.assertEqual(EXPECTED_CLI_OPTIONS, manual.commandline_options)
self.assertEqual(EXPECTED_SIGNALS, manual.signals)
self.assertEqual(EXPECTED_FILES, manual.files)
self.assertEqual(EXPECTED_CONFIG_OPTIONS, manual.config_options)
@test.require.command('man')
def test_parsing_with_unknown_options(self):
"""
Check that we can read a local mock man page that contains unrecognized
options. Unlike most other tests this doesn't require network access.
"""
if not stem.manual.HAS_ENCODING_ARG:
self.skipTest('(man lacks --encoding arg on OSX and BSD and Slackware, #18660)')
return
manual = stem.manual.Manual.from_man(UNKNOWN_OPTIONS_MAN_PATH)
self.assertEqual('tor - The second-generation onion router', manual.name)
self.assertEqual('', manual.synopsis)
self.assertEqual('', manual.description)
self.assertEqual({}, manual.commandline_options)
self.assertEqual({}, manual.signals)
self.assertEqual({}, manual.files)
self.assertEqual(2, len(manual.config_options))
option = [entry for entry in manual.config_options.values() if entry.category == stem.manual.Category.UNKNOWN][0]
self.assertEqual(stem.manual.Category.UNKNOWN, option.category)
self.assertEqual('SpiffyNewOption', option.name)
self.assertEqual('transport exec path-to-binary [options]', option.usage)
self.assertEqual('', option.summary)
self.assertEqual('Description of this new option.', option.description)
@test.require.command('man')
def test_saving_manual_as_config(self):
"""
Check that we can save and reload manuals as a config.
"""
if not stem.manual.HAS_ENCODING_ARG:
self.skipTest('(man lacks --encoding arg on OSX, BSD and Slackware, #18660)')
return
manual = stem.manual.Manual.from_man(EXAMPLE_MAN_PATH)
with tempfile.NamedTemporaryFile(prefix = 'saved_test_manual.') as tmp:
manual.save(tmp.name)
loaded_manual = stem.manual.Manual.from_cache(tmp.name)
self.assertEqual(manual, loaded_manual)
@test.require.command('man')
def test_saving_manual_as_sqlite(self):
"""
Check that we can save and reload manuals as sqlite.
"""
if not stem.manual.HAS_ENCODING_ARG:
self.skipTest('(man lacks --encoding arg on OSX, BSD, and Slackware #18660)')
return
manual = stem.manual.Manual.from_man(EXAMPLE_MAN_PATH)
with tempfile.NamedTemporaryFile(prefix = 'saved_test_manual.', suffix = '.sqlite') as tmp:
manual.save(tmp.name)
loaded_manual = stem.manual.Manual.from_cache(tmp.name)
self.assertEqual(manual, loaded_manual)
def test_cached_manual(self):
manual = _cached_manual()
self.assertEqual('tor - The second-generation onion router', manual.name)
self.assertEqual('tor [OPTION value]...', manual.synopsis)
self.assertTrue(manual.description.startswith(EXPECTED_DESCRIPTION))
self.assertTrue(len(manual.commandline_options) > 10)
self.assertTrue(len(manual.signals) > 5)
self.assertTrue(len(manual.files) > 20)
self.assertTrue(len(manual.config_options) > 200)
def test_download_man_page_without_arguments(self):
exc_msg = "Either the path or file_handle we're saving to must be provided"
self.assertRaisesRegexp(ValueError, exc_msg, stem.manual.download_man_page)
@patch('stem.util.system.is_available', Mock(return_value = False))
def test_download_man_page_requires_a2x(self):
exc_msg = 'We require a2x from asciidoc to provide a man page'
self.assertRaisesRegexp(IOError, exc_msg, stem.manual.download_man_page, '/tmp/no_such_file')
@patch('tempfile.mkdtemp', Mock(return_value = '/no/such/path'))
@patch('shutil.rmtree', Mock())
@patch('stem.manual.open', Mock(side_effect = IOError('unable to write to file')), create = True)
@patch('stem.util.system.is_available', Mock(return_value = True))
def test_download_man_page_when_unable_to_write(self):
exc_msg = "Unable to download tor's manual from https://gitweb.torproject.org/tor.git/plain/doc/tor.1.txt to /no/such/path/tor.1.txt: unable to write to file"
self.assertRaisesRegexp(IOError, re.escape(exc_msg), stem.manual.download_man_page, '/tmp/no_such_file')
@patch('tempfile.mkdtemp', Mock(return_value = '/no/such/path'))
@patch('shutil.rmtree', Mock())
@patch('stem.manual.open', Mock(return_value = io.BytesIO()), create = True)
@patch('stem.util.system.is_available', Mock(return_value = True))
@patch(URL_OPEN, Mock(side_effect = urllib.URLError('')))
def test_download_man_page_when_download_fails(self):
exc_msg = "Unable to download tor's manual from https://www.atagar.com/foo/bar to /no/such/path/tor.1.txt: >"
self.assertRaisesRegexp(IOError, re.escape(exc_msg), stem.manual.download_man_page, '/tmp/no_such_file', url = 'https://www.atagar.com/foo/bar')
@patch('tempfile.mkdtemp', Mock(return_value = '/no/such/path'))
@patch('shutil.rmtree', Mock())
@patch('stem.manual.open', Mock(return_value = io.BytesIO()), create = True)
@patch('stem.util.system.call', Mock(side_effect = stem.util.system.CallError('call failed', 'a2x -f manpage /no/such/path/tor.1.txt', 1, None, None, 'call failed')))
@patch('stem.util.system.is_available', Mock(return_value = True))
@patch(URL_OPEN, Mock(return_value = io.BytesIO(b'test content')))
def test_download_man_page_when_a2x_fails(self):
exc_msg = "Unable to run 'a2x -f manpage /no/such/path/tor.1.txt': call failed"
self.assertRaisesRegexp(IOError, exc_msg, stem.manual.download_man_page, '/tmp/no_such_file', url = 'https://www.atagar.com/foo/bar')
@patch('tempfile.mkdtemp', Mock(return_value = '/no/such/path'))
@patch('shutil.rmtree', Mock())
@patch('stem.manual.open', create = True)
@patch('stem.util.system.call')
@patch('stem.util.system.is_available', Mock(return_value = True))
@patch('os.path.exists', Mock(return_value = True))
@patch(URL_OPEN, Mock(return_value = io.BytesIO(b'test content')))
def test_download_man_page_when_successful(self, call_mock, open_mock):
open_mock.side_effect = lambda path, *args: {
'/no/such/path/tor.1.txt': io.BytesIO(),
'/no/such/path/tor.1': io.BytesIO(b'a2x output'),
}[path]
call_mock.return_value = Mock()
output = io.BytesIO()
stem.manual.download_man_page(file_handle = output)
self.assertEqual(b'a2x output', output.getvalue())
call_mock.assert_called_once_with('a2x -f manpage /no/such/path/tor.1.txt')
@patch('stem.manual.HAS_ENCODING_ARG', Mock(return_value = True))
@patch('stem.util.system.is_mac', Mock(return_value = False))
@patch('stem.util.system.is_bsd', Mock(return_value = False))
@patch('stem.util.system.is_slackware', Mock(return_value = False))
@patch('stem.util.system.call', Mock(side_effect = OSError('man --encoding=ascii -P cat tor returned exit status 16')))
def test_from_man_when_manual_is_unavailable(self):
exc_msg = "Unable to run 'man --encoding=ascii -P cat tor': man --encoding=ascii -P cat tor returned exit status 16"
self.assertRaisesRegexp(IOError, exc_msg, stem.manual.Manual.from_man)
@patch('stem.util.system.call', Mock(return_value = []))
def test_when_man_is_empty(self):
manual = stem.manual.Manual.from_man()
self.assertEqual('', manual.name)
self.assertEqual('', manual.synopsis)
self.assertEqual('', manual.description)
self.assertEqual({}, manual.commandline_options)
self.assertEqual({}, manual.signals)
self.assertEqual({}, manual.files)
self.assertEqual(OrderedDict(), manual.config_options)
stem-1.6.0/test/unit/__init__.py 0000664 0001750 0001750 00000000575 13124757510 017273 0 ustar atagar atagar 0000000 0000000 """
Unit tests for the stem library.
"""
import os
import test
__all__ = [
'connection',
'control',
'descriptor',
'exit_policy',
'socket',
'util',
'version',
]
def exec_documentation_example(filename):
path = os.path.join(test.STEM_BASE, 'docs', '_static', 'example', filename)
with open(path) as f:
code = compile(f.read(), path, 'exec')
exec(code)
stem-1.6.0/test/unit/version.py 0000664 0001750 0001750 00000022320 13165210535 017205 0 ustar atagar atagar 0000000 0000000 """
Unit tests for the stem.version.Version parsing and class.
"""
import unittest
import stem.util.system
import stem.version
from stem.version import Version
try:
# added in python 3.3
from unittest.mock import patch
except ImportError:
from mock import patch
TOR_VERSION_OUTPUT = """Mar 22 23:09:37.088 [notice] Tor v0.2.2.35 \
(git-73ff13ab3cc9570d). This is experimental software. Do not rely on it for \
strong anonymity. (Running on Linux i686)
Tor version 0.2.2.35 (git-73ff13ab3cc9570d)."""
class TestVersion(unittest.TestCase):
@patch('stem.util.system.call')
@patch.dict(stem.version.VERSION_CACHE)
def test_get_system_tor_version(self, call_mock):
call_mock.return_value = TOR_VERSION_OUTPUT.splitlines()
version = stem.version.get_system_tor_version('tor_unit')
self.assert_versions_match(version, 0, 2, 2, 35, None, 'git-73ff13ab3cc9570d')
self.assertEqual('73ff13ab3cc9570d', version.git_commit)
call_mock.assert_called_once_with('tor_unit --version')
self.assertEqual(stem.version.VERSION_CACHE['tor_unit'], version)
def test_parsing(self):
"""
Tests parsing by the Version class constructor.
"""
# valid versions with various number of compontents to the version
version = Version('0.1.2.3-tag')
self.assert_versions_match(version, 0, 1, 2, 3, 'tag', None)
version = Version('0.1.2.3')
self.assert_versions_match(version, 0, 1, 2, 3, None, None)
version = Version('0.1.2-tag')
self.assert_versions_match(version, 0, 1, 2, None, 'tag', None)
version = Version('0.1.2')
self.assert_versions_match(version, 0, 1, 2, None, None, None)
# checks an empty tag
version = Version('0.1.2.3-')
self.assert_versions_match(version, 0, 1, 2, 3, '', None)
version = Version('0.1.2-')
self.assert_versions_match(version, 0, 1, 2, None, '', None)
# check with extra informaton
version = Version('0.1.2.3-tag (git-73ff13ab3cc9570d)')
self.assert_versions_match(version, 0, 1, 2, 3, 'tag', 'git-73ff13ab3cc9570d')
self.assertEqual('73ff13ab3cc9570d', version.git_commit)
version = Version('0.1.2.3-tag ()')
self.assert_versions_match(version, 0, 1, 2, 3, 'tag', '')
version = Version('0.1.2 (git-73ff13ab3cc9570d)')
self.assert_versions_match(version, 0, 1, 2, None, None, 'git-73ff13ab3cc9570d')
# checks invalid version strings
self.assertRaises(ValueError, stem.version.Version, '')
self.assertRaises(ValueError, stem.version.Version, '1.2.3.4nodash')
self.assertRaises(ValueError, stem.version.Version, '1.2.3.a')
self.assertRaises(ValueError, stem.version.Version, '1.2.a.4')
self.assertRaises(ValueError, stem.version.Version, '1x2x3x4')
self.assertRaises(ValueError, stem.version.Version, '12.3')
self.assertRaises(ValueError, stem.version.Version, '1.-2.3')
def test_with_multiple_extra(self):
"""
Parse a version with multiple 'extra' fields.
"""
version = Version('0.1.2 (release) (git-73ff13ab3cc9570d)')
self.assert_versions_match(version, 0, 1, 2, None, None, 'release')
self.assertEqual(['release', 'git-73ff13ab3cc9570d'], version.all_extra)
self.assertEqual('73ff13ab3cc9570d', version.git_commit)
def test_comparison(self):
"""
Tests comparision between Version instances.
"""
# check for basic incrementing in each portion
self.assert_version_is_greater('1.1.2.3-tag', '0.1.2.3-tag')
self.assert_version_is_greater('0.2.2.3-tag', '0.1.2.3-tag')
self.assert_version_is_greater('0.1.3.3-tag', '0.1.2.3-tag')
self.assert_version_is_greater('0.1.2.4-tag', '0.1.2.3-tag')
self.assert_version_is_greater('0.1.2.3-ugg', '0.1.2.3-tag')
self.assert_version_is_equal('0.1.2.3-tag', '0.1.2.3-tag')
# check with common tags
self.assert_version_is_greater('0.1.2.3-beta', '0.1.2.3-alpha')
self.assert_version_is_greater('0.1.2.3-rc', '0.1.2.3-beta')
# checks that a missing patch level equals zero
self.assert_version_is_equal('0.1.2', '0.1.2.0')
self.assert_version_is_equal('0.1.2-tag', '0.1.2.0-tag')
# checks for missing patch or status
self.assert_version_is_greater('0.1.2.3-tag', '0.1.2.3')
self.assert_version_is_greater('0.1.2.3-tag', '0.1.2-tag')
self.assert_version_is_greater('0.1.2.3-tag', '0.1.2')
self.assert_version_is_equal('0.1.2.3', '0.1.2.3')
self.assert_version_is_equal('0.1.2', '0.1.2')
def test_nonversion_comparison(self):
"""
Checks that we can be compared with other types.
In python 3 on only equality comparisons work, greater than and less than
comparisons result in a TypeError.
"""
test_version = Version('0.1.2.3')
self.assertNotEqual(test_version, None)
self.assertNotEqual(test_version, 5)
def test_string(self):
"""
Tests the Version -> string conversion.
"""
# checks conversion with various numbers of arguments
self.assert_string_matches('0.1.2.3-tag')
self.assert_string_matches('0.1.2.3')
self.assert_string_matches('0.1.2')
def test_requirements_greater_than(self):
"""
Checks a VersionRequirements with a single greater_than rule.
"""
requirements = stem.version._VersionRequirements()
requirements.greater_than(Version('0.2.2.36'))
self.assertTrue(Version('0.2.2.36') >= requirements)
self.assertTrue(Version('0.2.2.37') >= requirements)
self.assertTrue(Version('0.2.3.36') >= requirements)
self.assertFalse(Version('0.2.2.35') >= requirements)
self.assertFalse(Version('0.2.1.38') >= requirements)
requirements = stem.version._VersionRequirements()
requirements.greater_than(Version('0.2.2.36'), False)
self.assertFalse(Version('0.2.2.35') >= requirements)
self.assertFalse(Version('0.2.2.36') >= requirements)
self.assertTrue(Version('0.2.2.37') >= requirements)
def test_requirements_less_than(self):
"""
Checks a VersionRequirements with a single less_than rule.
"""
requirements = stem.version._VersionRequirements()
requirements.less_than(Version('0.2.2.36'))
self.assertTrue(Version('0.2.2.36') >= requirements)
self.assertTrue(Version('0.2.2.35') >= requirements)
self.assertTrue(Version('0.2.1.38') >= requirements)
self.assertFalse(Version('0.2.2.37') >= requirements)
self.assertFalse(Version('0.2.3.36') >= requirements)
requirements = stem.version._VersionRequirements()
requirements.less_than(Version('0.2.2.36'), False)
self.assertFalse(Version('0.2.2.37') >= requirements)
self.assertFalse(Version('0.2.2.36') >= requirements)
self.assertTrue(Version('0.2.2.35') >= requirements)
def test_requirements_in_range(self):
"""
Checks a VersionRequirements with a single in_range rule.
"""
requirements = stem.version._VersionRequirements()
requirements.in_range(Version('0.2.2.36'), Version('0.2.2.38'))
self.assertFalse(Version('0.2.2.35') >= requirements)
self.assertTrue(Version('0.2.2.36') >= requirements)
self.assertTrue(Version('0.2.2.37') >= requirements)
self.assertFalse(Version('0.2.2.38') >= requirements)
# rule for 'anything in the 0.2.2.x series'
requirements = stem.version._VersionRequirements()
requirements.in_range(Version('0.2.2.0'), Version('0.2.3.0'))
for index in range(0, 100):
self.assertTrue(Version('0.2.2.%i' % index) >= requirements)
def test_requirements_multiple_rules(self):
"""
Checks a VersionRequirements is the logical 'or' when it has multiple rules.
"""
# rule to say 'anything but the 0.2.2.x series'
requirements = stem.version._VersionRequirements()
requirements.greater_than(Version('0.2.3.0'))
requirements.less_than(Version('0.2.2.0'), False)
self.assertTrue(Version('0.2.3.0') >= requirements)
self.assertFalse(Version('0.2.2.0') >= requirements)
for index in range(0, 100):
self.assertFalse(Version('0.2.2.%i' % index) >= requirements)
def assert_versions_match(self, version, major, minor, micro, patch, status, extra):
"""
Asserts that the values for a types.Version instance match the given
values.
"""
self.assertEqual(major, version.major)
self.assertEqual(minor, version.minor)
self.assertEqual(micro, version.micro)
self.assertEqual(patch, version.patch)
self.assertEqual(status, version.status)
self.assertEqual(extra, version.extra)
if extra is None:
self.assertEqual([], version.all_extra)
self.assertEqual(None, version.git_commit)
def assert_version_is_greater(self, first_version, second_version):
"""
Asserts that the parsed version of the first version is greate than the
second (also checking the inverse).
"""
version1 = Version(first_version)
version2 = Version(second_version)
self.assertEqual(version1 > version2, True)
self.assertEqual(version1 < version2, False)
def assert_version_is_equal(self, first_version, second_version):
"""
Asserts that the parsed version of the first version equals the second.
"""
version1 = Version(first_version)
version2 = Version(second_version)
self.assertEqual(version1, version2)
def assert_string_matches(self, version):
"""
Parses the given version string then checks that its string representation
matches the input.
"""
self.assertEqual(version, str(Version(version)))
stem-1.6.0/test/unit/exit_policy/ 0000775 0001750 0001750 00000000000 13177674754 017523 5 ustar atagar atagar 0000000 0000000 stem-1.6.0/test/unit/exit_policy/rule.py 0000664 0001750 0001750 00000034036 13124757510 021032 0 ustar atagar atagar 0000000 0000000 """
Unit tests for the stem.exit_policy.ExitPolicyRule class.
"""
import re
import unittest
from stem.exit_policy import AddressType, ExitPolicyRule, MicroExitPolicy
class TestExitPolicyRule(unittest.TestCase):
def test_accept_or_reject(self):
self.assertTrue(ExitPolicyRule('accept *:*').is_accept)
self.assertFalse(ExitPolicyRule('reject *:*').is_accept)
invalid_inputs = (
'accept',
'reject',
'accept\t*:*',
'accept\n*:*',
'acceptt *:*',
'rejectt *:*',
'blarg *:*',
' *:*',
'*:*',
'',
)
for rule_arg in invalid_inputs:
self.assertRaises(ValueError, ExitPolicyRule, rule_arg)
def test_with_multiple_spaces(self):
rule = ExitPolicyRule('accept *:80')
self.assertEqual('accept *:80', str(rule))
policy = MicroExitPolicy('accept 80,443')
self.assertTrue(policy.can_exit_to('75.119.206.243', 80))
def test_str_unchanged(self):
# provides a series of test inputs where the str() representation should
# match the input rule
test_inputs = (
'accept *:*',
'reject *:*',
'accept *:80',
'accept *:80-443',
'accept 127.0.0.1:80',
'accept 87.0.0.1/24:80',
'accept 156.5.38.3/255.255.0.255:80',
'accept [FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF]:80',
'accept [FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF]/32:80',
)
for rule_arg in test_inputs:
rule = ExitPolicyRule(rule_arg)
self.assertEqual(rule_arg, str(rule))
def test_str_changed(self):
# some instances where our rule is valid but won't match our str() representation
test_inputs = {
'accept 10.0.0.1/32:80': 'accept 10.0.0.1:80',
'accept 192.168.0.1/255.255.255.0:80': 'accept 192.168.0.1/24:80',
'accept [::]/32:*': 'accept [0000:0000:0000:0000:0000:0000:0000:0000]/32:*',
'accept [::]/128:*': 'accept [0000:0000:0000:0000:0000:0000:0000:0000]:*',
'accept6 *:*': 'accept [0000:0000:0000:0000:0000:0000:0000:0000]/0:*',
'reject6 *:*': 'reject [0000:0000:0000:0000:0000:0000:0000:0000]/0:*',
'accept6 [FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF]:*': 'accept [FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF]:*',
'reject6 [FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF]:*': 'reject [FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF]:*',
'accept *4:*': 'accept 0.0.0.0/0:*',
'accept *6:*': 'accept [0000:0000:0000:0000:0000:0000:0000:0000]/0:*',
'accept6 *4:*': 'accept 0.0.0.0/0:*',
'accept6 *6:*': 'accept [0000:0000:0000:0000:0000:0000:0000:0000]/0:*',
}
for rule_arg, expected_str in test_inputs.items():
rule = ExitPolicyRule(rule_arg)
self.assertEqual(expected_str, str(rule))
def test_valid_wildcard(self):
test_inputs = {
'reject *:*': (True, True),
'reject *:80': (True, False),
'accept 192.168.0.1:*': (False, True),
'accept 192.168.0.1:80': (False, False),
'reject *4:*': (False, True),
'reject *6:*': (False, True),
'reject6 *4:*': (False, True),
'reject6 *6:*': (False, True),
'reject 127.0.0.1/0:*': (False, True),
'reject 127.0.0.1/0.0.0.0:*': (False, True),
'reject 127.0.0.1/16:*': (False, True),
'reject 127.0.0.1/32:*': (False, True),
'reject [0000:0000:0000:0000:0000:0000:0000:0000]/0:80': (False, False),
'reject [0000:0000:0000:0000:0000:0000:0000:0000]/64:80': (False, False),
'reject [0000:0000:0000:0000:0000:0000:0000:0000]/128:80': (False, False),
'reject6 *:*': (False, True),
'reject6 *:80': (False, False),
'reject6 [0000:0000:0000:0000:0000:0000:0000:0000]/128:80': (False, False),
'accept 192.168.0.1:0-65535': (False, True),
'accept 192.168.0.1:1-65535': (False, True),
'accept 192.168.0.1:2-65535': (False, False),
'accept 192.168.0.1:1-65534': (False, False),
}
for rule_arg, attr in test_inputs.items():
is_address_wildcard, is_port_wildcard = attr
rule = ExitPolicyRule(rule_arg)
self.assertEqual(is_address_wildcard, rule.is_address_wildcard(), '%s (wildcard expected %s and actually %s)' % (rule_arg, is_address_wildcard, rule.is_address_wildcard()))
self.assertEqual(is_port_wildcard, rule.is_port_wildcard())
# check that when appropriate a /0 is reported as *not* being a wildcard
rule = ExitPolicyRule('reject 127.0.0.1/0:*')
rule._submask_wildcard = False
self.assertEqual(False, rule.is_address_wildcard())
rule = ExitPolicyRule('reject [0000:0000:0000:0000:0000:0000:0000:0000]/0:80')
rule._submask_wildcard = False
self.assertEqual(False, rule.is_address_wildcard())
def test_invalid_wildcard(self):
test_inputs = (
'reject */16:*',
'reject 127.0.0.1/*:*',
'reject *:0-*',
'reject *:*-15',
)
for rule_arg in test_inputs:
self.assertRaises(ValueError, ExitPolicyRule, rule_arg)
def test_wildcard_attributes(self):
rule = ExitPolicyRule('reject *:*')
self.assertEqual(AddressType.WILDCARD, rule.get_address_type())
self.assertEqual(None, rule.address)
self.assertEqual(None, rule.get_mask())
self.assertEqual(None, rule.get_masked_bits())
self.assertEqual(1, rule.min_port)
self.assertEqual(65535, rule.max_port)
def test_valid_ipv4_addresses(self):
test_inputs = {
'0.0.0.0': ('0.0.0.0', '255.255.255.255', 32),
'127.0.0.1/32': ('127.0.0.1', '255.255.255.255', 32),
'192.168.0.50/24': ('192.168.0.50', '255.255.255.0', 24),
'255.255.255.255/0': ('255.255.255.255', '0.0.0.0', 0),
}
for rule_addr, attr in test_inputs.items():
address, mask, masked_bits = attr
rule = ExitPolicyRule('accept %s:*' % rule_addr)
self.assertEqual(AddressType.IPv4, rule.get_address_type())
self.assertEqual(address, rule.address)
self.assertEqual(mask, rule.get_mask())
self.assertEqual(masked_bits, rule.get_masked_bits())
def test_invalid_ipv4_addresses(self):
test_inputs = (
'256.0.0.0',
'-1.0.0.0',
'0.0.0',
'0.0.0.',
'0.0.0.a',
'127.0.0.1/-1',
'127.0.0.1/33',
)
for rule_addr in test_inputs:
self.assertRaises(ValueError, ExitPolicyRule, 'accept %s:*' % rule_addr)
def test_valid_ipv6_addresses(self):
test_inputs = {
'[fe80:0000:0000:0000:0202:b3ff:fe1e:8329]':
('FE80:0000:0000:0000:0202:B3FF:FE1E:8329',
'FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF', 128),
'[FE80::0202:b3ff:fe1e:8329]':
('FE80:0000:0000:0000:0202:B3FF:FE1E:8329',
'FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF', 128),
'[0000:0000:0000:0000:0000:0000:0000:0000]/0':
('0000:0000:0000:0000:0000:0000:0000:0000',
'0000:0000:0000:0000:0000:0000:0000:0000', 0),
'[::]':
('0000:0000:0000:0000:0000:0000:0000:0000',
'FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF', 128),
}
for rule_addr, attr in test_inputs.items():
address, mask, masked_bits = attr
rule = ExitPolicyRule('accept %s:*' % rule_addr)
self.assertEqual(AddressType.IPv6, rule.get_address_type())
self.assertEqual(address, rule.address)
self.assertEqual(mask, rule.get_mask())
self.assertEqual(masked_bits, rule.get_masked_bits())
def test_invalid_ipv6_addresses(self):
test_inputs = (
'fe80::0202:b3ff:fe1e:8329',
'[fe80::0202:b3ff:fe1e:8329',
'fe80::0202:b3ff:fe1e:8329]',
'[fe80::0202:b3ff:fe1e:832g]',
'[fe80:::b3ff:fe1e:8329]',
'[fe80::b3ff::fe1e:8329]',
'[fe80::0202:b3ff:fe1e:8329]/-1',
'[fe80::0202:b3ff:fe1e:8329]/129',
)
for rule_addr in test_inputs:
self.assertRaises(ValueError, ExitPolicyRule, 'accept %s:*' % rule_addr)
def test_valid_ports(self):
test_inputs = {
'0': (0, 0),
'1': (1, 1),
'80': (80, 80),
'80-443': (80, 443),
}
for rule_port, attr in test_inputs.items():
min_port, max_port = attr
rule = ExitPolicyRule('accept 127.0.0.1:%s' % rule_port)
self.assertEqual(min_port, rule.min_port)
self.assertEqual(max_port, rule.max_port)
def test_invalid_ports(self):
test_inputs = (
'65536',
'a',
'5-3',
'5-',
'-3',
)
for rule_port in test_inputs:
self.assertRaises(ValueError, ExitPolicyRule, 'accept 127.0.0.1:%s' % rule_port)
def test_is_match_wildcard(self):
test_inputs = {
'reject *:*': {
('192.168.0.1', 80): True,
('0.0.0.0', 80): True,
('255.255.255.255', 80): True,
('FE80:0000:0000:0000:0202:B3FF:FE1E:8329', 80): True,
('[FE80:0000:0000:0000:0202:B3FF:FE1E:8329]', 80): True,
('192.168.0.1', None): True,
(None, 80, False): True,
(None, 80, True): True,
(None, None, False): True,
(None, None, True): True,
},
'reject 255.255.255.255/0:*': {
('192.168.0.1', 80): True,
('0.0.0.0', 80): True,
('255.255.255.255', 80): True,
('FE80:0000:0000:0000:0202:B3FF:FE1E:8329', 80): False,
('[FE80:0000:0000:0000:0202:B3FF:FE1E:8329]', 80): False,
('192.168.0.1', None): True,
(None, 80, False): False,
(None, 80, True): True,
(None, None, False): False,
(None, None, True): True,
},
'reject *4:*': {
('192.168.0.1', 80): True,
('FE80:0000:0000:0000:0202:B3FF:FE1E:8329', 80): False,
},
'reject *6:*': {
('192.168.0.1', 80): False,
('FE80:0000:0000:0000:0202:B3FF:FE1E:8329', 80): True,
},
}
for rule_arg, matches in test_inputs.items():
rule = ExitPolicyRule(rule_arg)
rule._submask_wildcard = False
for match_args, expected_result in matches.items():
self.assertEqual(expected_result, rule.is_match(*match_args))
# port zero is special in that exit policies can include it, but it's not
# something that we can match against
rule = ExitPolicyRule('reject *:*')
self.assertRaises(ValueError, rule.is_match, '127.0.0.1', 0)
def test_is_match_ipv4(self):
test_inputs = {
'reject 192.168.0.50:*': {
('192.168.0.50', 80): True,
('192.168.0.51', 80): False,
('192.168.0.49', 80): False,
(None, 80, False): False,
(None, 80, True): True,
('192.168.0.50', None): True,
},
'reject 0.0.0.0/24:*': {
('0.0.0.0', 80): True,
('0.0.0.1', 80): True,
('0.0.0.255', 80): True,
('0.0.1.0', 80): False,
('0.1.0.0', 80): False,
('1.0.0.0', 80): False,
(None, 80, False): False,
(None, 80, True): True,
('0.0.0.0', None): True,
},
}
for rule_arg, matches in test_inputs.items():
rule = ExitPolicyRule(rule_arg)
for match_args, expected_result in matches.items():
self.assertEqual(expected_result, rule.is_match(*match_args))
def test_is_match_ipv6(self):
test_inputs = {
'reject [FE80:0000:0000:0000:0202:B3FF:FE1E:8329]:*': {
('FE80:0000:0000:0000:0202:B3FF:FE1E:8329', 80): True,
('fe80:0000:0000:0000:0202:b3ff:fe1e:8329', 80): True,
('[FE80:0000:0000:0000:0202:B3FF:FE1E:8329]', 80): True,
('FE80:0000:0000:0000:0202:B3FF:FE1E:8330', 80): False,
('FE80:0000:0000:0000:0202:B3FF:FE1E:8328', 80): False,
(None, 80, False): False,
(None, 80, True): True,
('FE80:0000:0000:0000:0202:B3FF:FE1E:8329', None): True,
},
'reject [FE80:0000:0000:0000:0202:B3FF:FE1E:8329]/112:*': {
('FE80:0000:0000:0000:0202:B3FF:FE1E:8329', 80): True,
('FE80:0000:0000:0000:0202:B3FF:FE1E:0000', 80): True,
('FE80:0000:0000:0000:0202:B3FF:FE1E:FFFF', 80): True,
('FE80:0000:0000:0000:0202:B3FF:FE1F:8329', 80): False,
('FE81:0000:0000:0000:0202:B3FF:FE1E:8329', 80): False,
(None, 80, False): False,
(None, 80, True): True,
('FE80:0000:0000:0000:0202:B3FF:FE1E:8329', None, False): True,
('FE80:0000:0000:0000:0202:B3FF:FE1E:8329', None, True): True,
},
}
for rule_arg, matches in test_inputs.items():
rule = ExitPolicyRule(rule_arg)
for match_args, expected_result in matches.items():
self.assertEqual(expected_result, rule.is_match(*match_args))
def test_is_match_port(self):
test_inputs = {
'reject *:80': {
('192.168.0.50', 80): True,
('192.168.0.50', 81): False,
('192.168.0.50', 79): False,
(None, 80): True,
('192.168.0.50', None, False): False,
('192.168.0.50', None, True): True,
},
'reject *:80-85': {
('192.168.0.50', 79): False,
('192.168.0.50', 80): True,
('192.168.0.50', 83): True,
('192.168.0.50', 85): True,
('192.168.0.50', 86): False,
(None, 83): True,
('192.168.0.50', None, False): False,
('192.168.0.50', None, True): True,
},
}
for rule_arg, matches in test_inputs.items():
rule = ExitPolicyRule(rule_arg)
for match_args, expected_result in matches.items():
self.assertEqual(expected_result, rule.is_match(*match_args))
def test_missing_port(self):
exc_msg = "An exitpattern must be of the form 'addrspec:portspec': accept6 192.168.0.1/0"
self.assertRaisesRegexp(ValueError, re.escape(exc_msg), ExitPolicyRule, 'accept6 192.168.0.1/0')
exc_msg = "An exitpattern must be of the form 'addrspec:portspec': reject6 [2a00:1450:4001:081e:0000:0000:0000:200e]"
self.assertRaisesRegexp(ValueError, re.escape(exc_msg), ExitPolicyRule, 'reject6 [2a00:1450:4001:081e:0000:0000:0000:200e]')
def test_ipv6_only_entries(self):
# accept6/reject6 shouldn't match anything when given an ipv4 addresses
rule = ExitPolicyRule('accept6 192.168.0.1/0:*')
self.assertTrue(rule._skip_rule)
self.assertFalse(rule.is_match('192.168.0.1'))
self.assertFalse(rule.is_match('FE80:0000:0000:0000:0202:B3FF:FE1E:8329'))
self.assertFalse(rule.is_match())
rule = ExitPolicyRule('accept6 *4:*')
self.assertTrue(rule._skip_rule)
# wildcards match all ipv6 but *not* ipv4
rule = ExitPolicyRule('accept6 *:*')
self.assertTrue(rule.is_match('FE80:0000:0000:0000:0202:B3FF:FE1E:8329', 443))
self.assertFalse(rule.is_match('192.168.0.1', 443))
stem-1.6.0/test/unit/exit_policy/__init__.py 0000664 0001750 0001750 00000000123 13115423200 021572 0 ustar atagar atagar 0000000 0000000 """
Unit tests for stem.exit_policy.py contents.
"""
__all__ = ['policy', 'rule']
stem-1.6.0/test/unit/exit_policy/policy.py 0000664 0001750 0001750 00000026534 13124757510 021366 0 ustar atagar atagar 0000000 0000000 """
Unit tests for the stem.exit_policy.ExitPolicy class.
"""
import pickle
import unittest
try:
# added in python 3.3
from unittest.mock import Mock, patch
except ImportError:
from mock import Mock, patch
from stem.exit_policy import (
DEFAULT_POLICY_RULES,
get_config_policy,
ExitPolicy,
MicroExitPolicy,
ExitPolicyRule,
)
class TestExitPolicy(unittest.TestCase):
def test_example(self):
# tests the ExitPolicy and MicroExitPolicy pydoc examples
policy = ExitPolicy('accept *:80', 'accept *:443', 'reject *:*')
self.assertEqual('accept *:80, accept *:443, reject *:*', str(policy))
self.assertEqual('accept 80, 443', policy.summary())
self.assertTrue(policy.can_exit_to('75.119.206.243', 80))
policy = MicroExitPolicy('accept 80,443')
self.assertTrue(policy.can_exit_to('75.119.206.243', 80))
def test_constructor(self):
# The ExitPolicy constructor takes a series of string or ExitPolicyRule
# entries. Extra whitespace is ignored to make csvs easier to handle.
expected_policy = ExitPolicy(
ExitPolicyRule('accept *:80'),
ExitPolicyRule('accept *:443'),
ExitPolicyRule('reject *:*'),
)
policy = ExitPolicy('accept *:80', 'accept *:443', 'reject *:*')
self.assertEqual(expected_policy, policy)
policy = ExitPolicy(*'accept *:80, accept *:443, reject *:*'.split(','))
self.assertEqual(expected_policy, policy)
# checks that we truncate after getting a catch-all policy
policy = ExitPolicy(*'accept *:80, accept *:443, reject *:*, accept *:20-50'.split(','))
self.assertEqual(expected_policy, policy)
# checks that we compress redundant policies
policy = ExitPolicy(*'reject *:80, reject *:443, reject *:*'.split(','))
self.assertEqual(ExitPolicy('reject *:*'), policy)
def test_can_exit_to(self):
# Basic sanity test for our can_exit_to() method. Most of the interesting
# use cases (ip masks, wildcards, etc) are covered by the ExitPolicyRule
# tests.
policy = ExitPolicy('accept *:80', 'accept *:443', 'reject *:*')
for index in range(1, 100):
ip_addr = '%i.%i.%i.%i' % (index / 2, index / 2, index / 2, index / 2)
expected_result = index in (80, 443)
self.assertEqual(expected_result, policy.can_exit_to(ip_addr, index))
self.assertEqual(expected_result, policy.can_exit_to(port = index))
def test_can_exit_to_strictness(self):
# Check our 'strict' argument.
policy = ExitPolicy('reject 1.0.0.0/8:80', 'accept *:*')
self.assertEqual(False, policy.can_exit_to(None, 80, strict = True)) # can't exit to *all* instances of port 80
self.assertEqual(True, policy.can_exit_to(None, 80, strict = False)) # can exit to *an* instance of port 80
policy = ExitPolicy('accept 1.0.0.0/8:80', 'reject *:*')
self.assertEqual(False, policy.can_exit_to(None, 80, strict = True)) # can't exit to *all* instances of port 80
self.assertEqual(True, policy.can_exit_to(None, 80, strict = False)) # can exit to *an* instance of port 80
def test_is_exiting_allowed(self):
test_inputs = {
(): True,
('accept *:*', ): True,
('reject *:*', ): False,
('accept *:80', 'reject *:*'): True,
('reject *:80', 'accept *:80', 'reject *:*'): False,
('reject *:50-90', 'accept *:80', 'reject *:*'): False,
('reject *:2-65535', 'accept *:80-65535', 'reject *:*'): False,
('reject *:2-65535', 'accept 127.0.0.0:1', 'reject *:*'): True,
('reject 127.0.0.1:*', 'accept *:80', 'reject *:*'): True,
}
for rules, expected_result in test_inputs.items():
policy = ExitPolicy(*rules)
self.assertEqual(expected_result, policy.is_exiting_allowed())
def test_summary_examples(self):
# checks the summary() method's pydoc examples
policy = ExitPolicy('accept *:80', 'accept *:443', 'reject *:*')
self.assertEqual('accept 80, 443', policy.summary())
policy = ExitPolicy('accept *:443', 'reject *:1-1024', 'accept *:*')
self.assertEqual('reject 1-442, 444-1024', policy.summary())
def test_summary_large_ranges(self):
# checks the summary() method when the policy includes very large port ranges
policy = ExitPolicy('reject *:80-65535', 'accept *:1-65533', 'reject *:*')
self.assertEqual('accept 1-79', policy.summary())
def test_without_port(self):
policy = get_config_policy('accept 216.58.193.78, reject *')
self.assertEqual([ExitPolicyRule('accept 216.58.193.78:*'), ExitPolicyRule('reject *:*')], list(policy))
policy = get_config_policy('reject6 [2a00:1450:4001:081e:0000:0000:0000:200e]')
self.assertEqual([ExitPolicyRule('reject [2a00:1450:4001:081e:0000:0000:0000:200e]:*')], list(policy))
def test_non_private_non_default_policy(self):
policy = get_config_policy('reject *:80-65535, accept *:1-65533, reject *:*')
for rule in policy:
self.assertFalse(rule.is_private())
self.assertFalse(rule.is_default())
self.assertFalse(policy.has_private())
self.assertFalse(policy.has_default())
self.assertEqual(policy, policy.strip_private())
self.assertEqual(policy, policy.strip_default())
def test_all_private_policy(self):
for port in ('*', '80', '1-1024'):
private_policy = get_config_policy('reject private:%s' % port, '12.34.56.78')
for rule in private_policy:
self.assertTrue(rule.is_private())
self.assertEqual(ExitPolicy(), private_policy.strip_private())
# though not commonly done, technically private policies can be accept rules too
private_policy = get_config_policy('accept private:*')
self.assertEqual(ExitPolicy(), private_policy.strip_private())
@patch('socket.gethostname', Mock(side_effect = IOError('no address')))
def test_all_private_policy_without_network(self):
for rule in get_config_policy('reject private:80, accept *:80'):
# all rules except the ending accept are part of the private policy
self.assertEqual(str(rule) != 'accept *:80', rule.is_private())
def test_all_default_policy(self):
policy = ExitPolicy(*DEFAULT_POLICY_RULES)
for rule in policy:
self.assertTrue(rule.is_default())
self.assertTrue(policy.has_default())
self.assertEqual(ExitPolicy(), policy.strip_default())
def test_mixed_private_policy(self):
policy = get_config_policy('accept *:80, reject private:1-65533, accept *:*')
for rule in policy:
self.assertTrue(rule.is_accept != rule.is_private()) # only reject rules are the private ones
self.assertEqual(get_config_policy('accept *:80, accept *:*'), policy.strip_private())
def test_mixed_default_policy(self):
policy = ExitPolicy('accept *:80', 'accept 127.0.0.1:1-65533', *DEFAULT_POLICY_RULES)
for rule in policy:
# only accept-all and reject rules are the default ones
self.assertTrue(rule.is_accept != rule.is_default() or (rule.is_accept and rule.is_address_wildcard() and rule.is_port_wildcard()))
self.assertEqual(get_config_policy('accept *:80, accept 127.0.0.1:1-65533'), policy.strip_default())
def test_get_config_policy_with_ipv6(self):
# ensure our constructor accepts addresses both with and without brackets
self.assertTrue(get_config_policy('reject private:80', 'fe80:0000:0000:0000:0202:b3ff:fe1e:8329').is_exiting_allowed())
self.assertTrue(get_config_policy('reject private:80', '[fe80:0000:0000:0000:0202:b3ff:fe1e:8329]').is_exiting_allowed())
def test_str(self):
# sanity test for our __str__ method
policy = ExitPolicy(' accept *:80\n', '\taccept *:443')
self.assertEqual('accept *:80, accept *:443', str(policy))
policy = ExitPolicy('reject 0.0.0.0/255.255.255.0:*', 'accept *:*')
self.assertEqual('reject 0.0.0.0/24:*, accept *:*', str(policy))
def test_iter(self):
# sanity test for our __iter__ method
rules = [
ExitPolicyRule('accept *:80'),
ExitPolicyRule('accept *:443'),
ExitPolicyRule('reject *:*'),
]
self.assertEqual(rules, list(ExitPolicy(*rules)))
self.assertEqual(rules, list(ExitPolicy('accept *:80', 'accept *:443', 'reject *:*')))
def test_microdescriptor_parsing(self):
# mapping between inputs and if they should succeed or not
test_inputs = {
'accept 80': True,
'accept 80,443': True,
'': False,
'accept': False,
'accept ': False,
'accept\t80,443': False,
'accept 80, 443': False,
'accept 80,\t443': False,
'80,443': False,
'accept 80,-443': False,
'accept 80,+443': False,
'accept 80,66666': False,
'reject 80,foo': False,
'bar 80,443': False,
}
for policy_arg, expect_success in test_inputs.items():
try:
policy = MicroExitPolicy(policy_arg)
if expect_success:
self.assertEqual(policy_arg, str(policy))
else:
self.fail()
except ValueError:
if expect_success:
self.fail()
def test_microdescriptor_attributes(self):
# checks that its is_accept attribute is properly set
# single port
policy = MicroExitPolicy('accept 443')
self.assertTrue(policy.is_accept)
# multiple ports
policy = MicroExitPolicy('accept 80,443')
self.assertTrue(policy.is_accept)
# port range
policy = MicroExitPolicy('reject 1-1024')
self.assertFalse(policy.is_accept)
def test_microdescriptor_can_exit_to(self):
test_inputs = {
'accept 443': {442: False, 443: True, 444: False},
'reject 443': {442: True, 443: False, 444: True},
'accept 80,443': {80: True, 443: True, 10: False},
'reject 1-1024': {1: False, 1024: False, 1025: True},
}
for policy_arg, attr in test_inputs.items():
policy = MicroExitPolicy(policy_arg)
for port, expected_value in attr.items():
self.assertEqual(expected_value, policy.can_exit_to(port = port))
# address argument should be ignored
policy = MicroExitPolicy('accept 80,443')
self.assertFalse(policy.can_exit_to('127.0.0.1', 79))
self.assertTrue(policy.can_exit_to('127.0.0.1', 80))
def test_get_config_policy(self):
test_inputs = {
'': ExitPolicy(),
'reject *': ExitPolicy('reject *:*'),
'reject *:*': ExitPolicy('reject *:*'),
'reject private': ExitPolicy(
'reject 0.0.0.0/8:*',
'reject 169.254.0.0/16:*',
'reject 127.0.0.0/8:*',
'reject 192.168.0.0/16:*',
'reject 10.0.0.0/8:*',
'reject 172.16.0.0/12:*',
'reject 12.34.56.78:*',
),
'accept *:80, reject *': ExitPolicy(
'accept *:80',
'reject *:*',
),
' accept *:80, reject * ': ExitPolicy(
'accept *:80',
'reject *:*',
),
}
for test_input, expected in test_inputs.items():
self.assertEqual(expected, get_config_policy(test_input, '12.34.56.78'))
test_inputs = (
'blarg',
'accept *:*:*',
'acceptt *:80',
'accept 257.0.0.1:80',
'accept *:999999',
)
for test_input in test_inputs:
self.assertRaises(ValueError, get_config_policy, test_input)
def test_pickleability(self):
"""
Checks that we can unpickle ExitPolicy instances.
"""
policy = ExitPolicy('accept *:80', 'accept *:443', 'reject *:*')
self.assertTrue(policy.can_exit_to('74.125.28.106', 80))
encoded_policy = pickle.dumps(policy)
restored_policy = pickle.loads(encoded_policy)
self.assertEqual(policy, restored_policy)
self.assertTrue(restored_policy.is_exiting_allowed())
self.assertTrue(restored_policy.can_exit_to('74.125.28.106', 80))
stem-1.6.0/test/unit/doctest.py 0000664 0001750 0001750 00000007202 13124757510 017173 0 ustar atagar atagar 0000000 0000000 """
Tests examples from our documentation.
"""
from __future__ import absolute_import
import doctest
import os
import unittest
import stem.descriptor.router_status_entry
import stem.util.connection
import stem.util.str_tools
import stem.util.system
import stem.version
import test
from stem.response import ControlMessage
try:
# added in python 3.3
from unittest.mock import Mock, patch
except ImportError:
from mock import Mock, patch
EXPECTED_CIRCUIT_STATUS = """\
20 EXTENDED $718BCEA286B531757ACAFF93AE04910EA73DE617=KsmoinOK,$649F2D0ACF418F7CFC6539AB2257EB2D5297BAFA=Eskimo BUILD_FLAGS=NEED_CAPACITY PURPOSE=GENERAL TIME_CREATED=2012-12-06T13:51:11.433755
19 BUILT $718BCEA286B531757ACAFF93AE04910EA73DE617=KsmoinOK,$30BAB8EE7606CBD12F3CC269AE976E0153E7A58D=Pascal1,$2765D8A8C4BBA3F89585A9FFE0E8575615880BEB=Anthracite PURPOSE=GENERAL TIME_CREATED=2012-12-06T13:50:56.969938\
"""
ADD_ONION_RESPONSE = """\
250-ServiceID=oekn5sqrvcu4wote
250-ClientAuth=bob:nKwfvVPmTNr2k2pG0pzV4g
250 OK
"""
class TestDocumentation(unittest.TestCase):
def test_examples(self):
stem_dir = os.path.join(test.STEM_BASE, 'stem')
is_failed = False
for path in stem.util.system.files_with_suffix(stem_dir, '.py'):
args = {'module_relative': False}
test_run = None
if path.endswith('/stem/util/conf.py'):
with patch('stem.util.conf.get_config') as get_config_mock:
config = Mock()
config.load.return_value = None
get_config_mock.return_value = config
test_run = doctest.testfile(path, **args)
elif path.endswith('/stem/descriptor/router_status_entry.py'):
args['globs'] = {
'_base64_to_hex': stem.descriptor.router_status_entry._base64_to_hex,
}
test_run = doctest.testfile(path, **args)
elif path.endswith('/stem/util/connection.py'):
args['globs'] = {
'expand_ipv6_address': stem.util.connection.expand_ipv6_address,
}
test_run = doctest.testfile(path, **args)
elif path.endswith('/stem/util/str_tools.py'):
args['globs'] = {
'_to_camel_case': stem.util.str_tools._to_camel_case,
'_split_by_length': stem.util.str_tools._split_by_length,
'crop': stem.util.str_tools.crop,
'size_label': stem.util.str_tools.size_label,
'time_label': stem.util.str_tools.time_label,
'time_labels': stem.util.str_tools.time_labels,
'short_time_label': stem.util.str_tools.short_time_label,
'parse_short_time_label': stem.util.str_tools.parse_short_time_label,
}
test_run = doctest.testfile(path, **args)
elif path.endswith('/stem/response/__init__.py'):
pass # the escaped slashes seem to be confusing doctest
elif path.endswith('/stem/control.py'):
controller = Mock()
controller.extend_circuit.side_effect = [19, 20]
controller.get_info.side_effect = lambda arg: {
'circuit-status': EXPECTED_CIRCUIT_STATUS,
}[arg]
response = ControlMessage.from_str(ADD_ONION_RESPONSE, 'ADD_ONION', normalize = True)
controller.create_ephemeral_hidden_service.return_value = response
args['globs'] = {'controller': controller}
test_run = doctest.testfile(path, **args)
elif path.endswith('/stem/version.py'):
with patch('stem.version.get_system_tor_version', Mock(return_value = stem.version.Version('0.2.1.30'))):
test_run = doctest.testfile(path, **args)
else:
test_run = doctest.testfile(path, **args)
if test_run and test_run.failed > 0:
is_failed = True
if is_failed:
self.fail('doctests encountered errors')
stem-1.6.0/test/unit/response/ 0000775 0001750 0001750 00000000000 13177674754 017031 5 ustar atagar atagar 0000000 0000000 stem-1.6.0/test/unit/response/add_onion.py 0000664 0001750 0001750 00000010143 13124757510 021314 0 ustar atagar atagar 0000000 0000000 """
Unit tests for the stem.response.add_onion.AddOnionResponse class.
"""
import unittest
import stem
import stem.response
import stem.response.add_onion
from stem.response import ControlMessage
WITH_PRIVATE_KEY = """250-ServiceID=gfzprpioee3hoppz
250-PrivateKey=RSA1024:MIICXgIBAAKBgQDZvYVxvKPTWhId/8Ss9fVxjAoFDsrJ3pk6HjHrEFRm3ypkK/vArbG9BrupzzYcyms+lO06O8b/iOSHuZI5mUEGkrYqQ+hpB2SkPUEzW7vcp8SQQivna3+LfkWH4JDqfiwZutU6MMEvU6g1OqK4Hll6uHbLpsfxkS/mGjyu1C9a9wIDAQABAoGBAJxsC3a25xZJqaRFfxwmIiptSTFy+/nj4T4gPQo6k/fHMKP/+P7liT9bm+uUwbITNNIjmPzxvrcKt+pNRR/92fizxr8QXr8l0ciVOLerbvdqvVUaQ/K1IVsblOLbactMvXcHactmqqLFUaZU9PPSDla7YkzikLDIUtHXQBEt4HEhAkEA/c4n+kpwi4odCaF49ESPbZC/Qejh7U9Tq10vAHzfrrGgQjnLw2UGDxJQXc9P12fGTvD2q3Q3VaMI8TKKFqZXsQJBANufh1zfP+xX/UfxJ4QzDUCHCu2gnyTDj3nG9Bc80E5g7NwR2VBXF1R+QQCK9GZcXd2y6vBYgrHOSUiLbVjGrycCQQDpOcs0zbjUEUuTsQUT+fiO50dJSrZpus6ZFxz85sMppeItWSzsVeYWbW7adYnZ2Gu72OPjM/0xPYsXEakhHSRRAkAxlVauNQjthv/72god4pi/VL224GiNmEkwKSa6iFRPHbrcBHuXk9IElWx/ft+mrHvUraw1DwaStgv9gNzzCghJAkEA08RegCRnIzuGvgeejLk4suIeCMD/11AvmSvxbRWS5rq1leSVo7uGLSnqDbwlzE4dGb5kH15NNAp14/l2Fu/yZg==
250 OK"""
WITH_CLIENT_AUTH = """250-ServiceID=oekn5sqrvcu4wote
250-ClientAuth=bob:lhwLVFt0Kd5/0Gy9DkKoyA
250-ClientAuth=alice:T9UADxtrvqx2HnLKWp/fWQ
250 OK
"""
WITHOUT_PRIVATE_KEY = """250-ServiceID=gfzprpioee3hoppz
250 OK"""
WRONG_FIRST_KEY = """250-MyKey=gfzprpioee3hoppz
250-ServiceID=gfzprpioee3hoppz
250 OK"""
MISSING_KEY_TYPE = """250-ServiceID=gfzprpioee3hoppz
250-PrivateKey=MIICXgIBAAKBgQDZvYVxvKPTWhId/8Ss9fVxj
250 OK"""
class TestAddOnionResponse(unittest.TestCase):
def test_convert(self):
"""
Exercises functionality of the convert method both when it works and
there's an error.
"""
# working case
response = ControlMessage.from_str(WITH_PRIVATE_KEY, 'ADD_ONION', normalize = True)
# now this should be a AddOnionResponse (ControlMessage subclass)
self.assertTrue(isinstance(response, stem.response.ControlMessage))
self.assertTrue(isinstance(response, stem.response.add_onion.AddOnionResponse))
# exercise some of the ControlMessage functionality
raw_content = (WITH_PRIVATE_KEY + '\n').replace('\n', '\r\n')
self.assertEqual(raw_content, response.raw_content())
self.assertTrue(str(response).startswith('ServiceID='))
def test_with_private_key(self):
"""
Checks a response when there's a private key.
"""
response = ControlMessage.from_str(WITH_PRIVATE_KEY, 'ADD_ONION', normalize = True)
self.assertEqual('gfzprpioee3hoppz', response.service_id)
self.assertTrue(response.private_key.startswith('MIICXgIBAAKB'))
self.assertEqual('RSA1024', response.private_key_type)
self.assertEqual({}, response.client_auth)
def test_with_client_auth(self):
"""
Checks a response when there's client credentials.
"""
response = ControlMessage.from_str(WITH_CLIENT_AUTH, 'ADD_ONION', normalize = True)
self.assertEqual('oekn5sqrvcu4wote', response.service_id)
self.assertEqual(None, response.private_key)
self.assertEqual(None, response.private_key_type)
self.assertEqual({'bob': 'lhwLVFt0Kd5/0Gy9DkKoyA', 'alice': 'T9UADxtrvqx2HnLKWp/fWQ'}, response.client_auth)
def test_without_private_key(self):
"""
Checks a response without a private key.
"""
response = ControlMessage.from_str(WITHOUT_PRIVATE_KEY, 'ADD_ONION', normalize = True)
self.assertEqual('gfzprpioee3hoppz', response.service_id)
self.assertEqual(None, response.private_key)
self.assertEqual(None, response.private_key_type)
def test_without_service_id(self):
"""
Checks a response that lack an initial service id.
"""
response = ControlMessage.from_str(WRONG_FIRST_KEY, normalize = True)
self.assertRaisesRegexp(stem.ProtocolError, 'ADD_ONION response should start with', stem.response.convert, 'ADD_ONION', response)
def test_no_key_type(self):
"""
Checks a response that's missing the private key type.
"""
response = ControlMessage.from_str(MISSING_KEY_TYPE, normalize = True)
self.assertRaisesRegexp(stem.ProtocolError, 'ADD_ONION PrivateKey lines should be of the form', stem.response.convert, 'ADD_ONION', response)
stem-1.6.0/test/unit/response/authchallenge.py 0000664 0001750 0001750 00000004165 13124757510 022175 0 ustar atagar atagar 0000000 0000000 """
Unit tests for the stem.response.authchallenge.AuthChallengeResponse class.
"""
import unittest
import stem.response
import stem.response.authchallenge
import stem.socket
from stem.response import ControlMessage
VALID_RESPONSE = '250 AUTHCHALLENGE \
SERVERHASH=B16F72DACD4B5ED1531F3FCC04B593D46A1E30267E636EA7C7F8DD7A2B7BAA05 \
SERVERNONCE=653574272ABBB49395BD1060D642D653CFB7A2FCE6A4955BCFED819703A9998C'
VALID_HASH = b'\xb1or\xda\xcdK^\xd1S\x1f?\xcc\x04\xb5\x93\xd4j\x1e0&~cn\xa7\xc7\xf8\xddz+{\xaa\x05'
VALID_NONCE = b"e5t'*\xbb\xb4\x93\x95\xbd\x10`\xd6B\xd6S\xcf\xb7\xa2\xfc\xe6\xa4\x95[\xcf\xed\x81\x97\x03\xa9\x99\x8c"
INVALID_RESPONSE = '250 AUTHCHALLENGE \
SERVERHASH=FOOBARB16F72DACD4B5ED1531F3FCC04B593D46A1E30267E636EA7C7F8DD7A2B7BAA05 \
SERVERNONCE=FOOBAR653574272ABBB49395BD1060D642D653CFB7A2FCE6A4955BCFED819703A9998C'
class TestAuthChallengeResponse(unittest.TestCase):
def test_valid_response(self):
"""
Parses valid AUTHCHALLENGE responses.
"""
control_message = ControlMessage.from_str(VALID_RESPONSE, 'AUTHCHALLENGE', normalize = True)
# now this should be a AuthChallengeResponse (ControlMessage subclass)
self.assertTrue(isinstance(control_message, stem.response.ControlMessage))
self.assertTrue(isinstance(control_message, stem.response.authchallenge.AuthChallengeResponse))
self.assertEqual(VALID_HASH, control_message.server_hash)
self.assertEqual(VALID_NONCE, control_message.server_nonce)
def test_invalid_responses(self):
"""
Tries to parse various malformed responses and checks it they raise
appropriate exceptions.
"""
auth_challenge_comp = VALID_RESPONSE.split()
for index in range(1, len(auth_challenge_comp)):
# Attempts to parse a message without this item. The first item is
# skipped because, without the 250 code, the message won't be
# constructed.
remaining_comp = auth_challenge_comp[:index] + auth_challenge_comp[index + 1:]
control_message = ControlMessage.from_str(' '.join(remaining_comp), normalize = True)
self.assertRaises(stem.ProtocolError, stem.response.convert, 'AUTHCHALLENGE', control_message)
stem-1.6.0/test/unit/response/singleline.py 0000664 0001750 0001750 00000001503 13124757510 021513 0 ustar atagar atagar 0000000 0000000 """
Unit tests for the stem.response.SingleLineResponse class.
"""
import unittest
import stem
from stem.response import ControlMessage
class TestSingleLineResponse(unittest.TestCase):
def test_single_line_response(self):
message = ControlMessage.from_str('552 NOTOK\r\n', 'SINGLELINE')
self.assertEqual(False, message.is_ok())
message = ControlMessage.from_str('250 KK\r\n', 'SINGLELINE')
self.assertEqual(True, message.is_ok())
message = ControlMessage.from_str('250 OK\r\n', 'SINGLELINE')
self.assertEqual(True, message.is_ok(True))
message = ControlMessage.from_str('250 HMM\r\n', 'SINGLELINE')
self.assertEqual(False, message.is_ok(True))
def test_multi_line_response(self):
self.assertRaises(stem.ProtocolError, ControlMessage.from_str, '250-MULTI\r\n250 LINE\r\n', 'SINGLELINE')
stem-1.6.0/test/unit/response/__init__.py 0000664 0001750 0001750 00000000267 13115423200 021111 0 ustar atagar atagar 0000000 0000000 """
Unit tests for stem.response.
"""
__all__ = [
'control_message',
'control_line',
'events',
'getinfo',
'getconf',
'protocolinfo',
'authchallenge',
'singleline',
]
stem-1.6.0/test/unit/response/protocolinfo.py 0000664 0001750 0001750 00000013016 13126502377 022102 0 ustar atagar atagar 0000000 0000000 """
Unit tests for the stem.response.protocolinfo.ProtocolInfoResponse class.
"""
import unittest
import stem.response
import stem.response.protocolinfo
import stem.socket
import stem.util.proc
import stem.util.system
import stem.version
from stem.response import ControlMessage
from stem.response.protocolinfo import AuthMethod
try:
# added in python 3.3
from unittest.mock import Mock, patch
except ImportError:
from mock import Mock, patch
NO_AUTH = """250-PROTOCOLINFO 1
250-AUTH METHODS=NULL
250-VERSION Tor="0.2.1.30"
250 OK"""
PASSWORD_AUTH = """250-PROTOCOLINFO 1
250-AUTH METHODS=HASHEDPASSWORD
250-VERSION Tor="0.2.1.30"
250 OK"""
COOKIE_AUTH = r"""250-PROTOCOLINFO 1
250-AUTH METHODS=COOKIE COOKIEFILE="/tmp/my data\\\"dir//control_auth_cookie"
250-VERSION Tor="0.2.1.30"
250 OK"""
MULTIPLE_AUTH = """250-PROTOCOLINFO 1
250-AUTH METHODS=COOKIE,HASHEDPASSWORD COOKIEFILE="/home/atagar/.tor/control_auth_cookie"
250-VERSION Tor="0.2.1.30"
250 OK"""
UNKNOWN_AUTH = """250-PROTOCOLINFO 1
250-AUTH METHODS=MAGIC,HASHEDPASSWORD,PIXIE_DUST
250-VERSION Tor="0.2.1.30"
250 OK"""
MINIMUM_RESPONSE = """250-PROTOCOLINFO 5
250 OK"""
UNICODE_COOKIE_PATH = r"""250-PROTOCOLINFO 1
250-AUTH METHODS=COOKIE COOKIEFILE="/home/user/\346\226\207\346\241\243/tor-browser_en-US/Browser/TorBrowser/Data/Tor/control_auth_cookie"
250-VERSION Tor="0.2.1.30"
250 OK"""
RELATIVE_COOKIE_PATH = r"""250-PROTOCOLINFO 1
250-AUTH METHODS=COOKIE COOKIEFILE="./tor-browser_en-US/Data/control_auth_cookie"
250-VERSION Tor="0.2.1.30"
250 OK"""
EXPECTED_UNICODE_PATH = b"/home/user/\346\226\207\346\241\243/tor-browser_en-US/Browser/TorBrowser/Data/Tor/control_auth_cookie".decode('utf-8')
class TestProtocolInfoResponse(unittest.TestCase):
def test_convert(self):
"""
Exercises functionality of the convert method both when it works and
there's an error.
"""
# working case
control_message = ControlMessage.from_str(NO_AUTH, 'PROTOCOLINFO', normalize = True)
# now this should be a ProtocolInfoResponse (ControlMessage subclass)
self.assertTrue(isinstance(control_message, stem.response.ControlMessage))
self.assertTrue(isinstance(control_message, stem.response.protocolinfo.ProtocolInfoResponse))
# exercise some of the ControlMessage functionality
raw_content = (NO_AUTH + '\n').replace('\n', '\r\n')
self.assertEqual(raw_content, control_message.raw_content())
self.assertTrue(str(control_message).startswith('PROTOCOLINFO 1'))
# attempt to convert the wrong type
self.assertRaises(TypeError, stem.response.convert, 'PROTOCOLINFO', 'hello world')
# attempt to convert a different message type
self.assertRaises(stem.ProtocolError, ControlMessage.from_str, '650 BW 32326 2856\r\n', 'PROTOCOLINFO')
def test_no_auth(self):
"""
Checks a response when there's no authentication.
"""
control_message = ControlMessage.from_str(NO_AUTH, 'PROTOCOLINFO', normalize = True)
self.assertEqual(1, control_message.protocol_version)
self.assertEqual(stem.version.Version('0.2.1.30'), control_message.tor_version)
self.assertEqual((AuthMethod.NONE, ), control_message.auth_methods)
self.assertEqual((), control_message.unknown_auth_methods)
self.assertEqual(None, control_message.cookie_path)
def test_password_auth(self):
"""
Checks a response with password authentication.
"""
control_message = ControlMessage.from_str(PASSWORD_AUTH, 'PROTOCOLINFO', normalize = True)
self.assertEqual((AuthMethod.PASSWORD, ), control_message.auth_methods)
def test_cookie_auth(self):
"""
Checks a response with cookie authentication and a path including escape
characters.
"""
control_message = ControlMessage.from_str(COOKIE_AUTH, 'PROTOCOLINFO', normalize = True)
self.assertEqual((AuthMethod.COOKIE, ), control_message.auth_methods)
self.assertEqual('/tmp/my data\\"dir//control_auth_cookie', control_message.cookie_path)
def test_multiple_auth(self):
"""
Checks a response with multiple authentication methods.
"""
control_message = ControlMessage.from_str(MULTIPLE_AUTH, 'PROTOCOLINFO', normalize = True)
self.assertEqual((AuthMethod.COOKIE, AuthMethod.PASSWORD), control_message.auth_methods)
self.assertEqual('/home/atagar/.tor/control_auth_cookie', control_message.cookie_path)
def test_unknown_auth(self):
"""
Checks a response with an unrecognized authtentication method.
"""
control_message = ControlMessage.from_str(UNKNOWN_AUTH, 'PROTOCOLINFO', normalize = True)
self.assertEqual((AuthMethod.UNKNOWN, AuthMethod.PASSWORD), control_message.auth_methods)
self.assertEqual(('MAGIC', 'PIXIE_DUST'), control_message.unknown_auth_methods)
def test_minimum_response(self):
"""
Checks a PROTOCOLINFO response that only contains the minimum amount of
information to be a valid response.
"""
control_message = ControlMessage.from_str(MINIMUM_RESPONSE, 'PROTOCOLINFO', normalize = True)
self.assertEqual(5, control_message.protocol_version)
self.assertEqual(None, control_message.tor_version)
self.assertEqual((), control_message.auth_methods)
self.assertEqual((), control_message.unknown_auth_methods)
self.assertEqual(None, control_message.cookie_path)
@patch('sys.getfilesystemencoding', Mock(return_value = 'UTF-8'))
def test_unicode_cookie(self):
"""
Checks an authentication cookie with a unicode path.
"""
control_message = ControlMessage.from_str(UNICODE_COOKIE_PATH, 'PROTOCOLINFO', normalize = True)
self.assertEqual(EXPECTED_UNICODE_PATH, control_message.cookie_path)
stem-1.6.0/test/unit/response/getconf.py 0000664 0001750 0001750 00000006301 13124757510 021010 0 ustar atagar atagar 0000000 0000000 """
Unit tests for the stem.response.getconf.GetConfResponse class.
"""
import unittest
import stem.response
import stem.response.getconf
import stem.socket
from stem.response import ControlMessage
SINGLE_RESPONSE = """\
250 DataDirectory=/home/neena/.tor"""
BATCH_RESPONSE = """\
250-CookieAuthentication=0
250-ControlPort=9100
250-DataDirectory=/tmp/fake dir
250 DirPort"""
MULTIVALUE_RESPONSE = """\
250-ControlPort=9100
250-ExitPolicy=accept 34.3.4.5
250-ExitPolicy=accept 3.4.53.3
250-ExitPolicy=accept 3.4.53.3
250 ExitPolicy=reject 23.245.54.3"""
UNRECOGNIZED_KEY_RESPONSE = '''552-Unrecognized configuration key "brickroad"
552 Unrecognized configuration key "submarine"'''
INVALID_RESPONSE = """\
123-FOO
232 BAR"""
class TestGetConfResponse(unittest.TestCase):
def test_empty_response(self):
"""
Parses a GETCONF reply without options (just calling "GETCONF").
"""
control_message = ControlMessage.from_str('250 OK\r\n', 'GETCONF')
# now this should be a GetConfResponse (ControlMessage subclass)
self.assertTrue(isinstance(control_message, stem.response.ControlMessage))
self.assertTrue(isinstance(control_message, stem.response.getconf.GetConfResponse))
self.assertEqual({}, control_message.entries)
def test_single_response(self):
"""
Parses a GETCONF reply response for a single parameter.
"""
control_message = ControlMessage.from_str(SINGLE_RESPONSE, 'GETCONF', normalize = True)
self.assertEqual({'DataDirectory': ['/home/neena/.tor']}, control_message.entries)
def test_batch_response(self):
"""
Parses a GETCONF reply for muiltiple parameters.
"""
expected = {
'CookieAuthentication': ['0'],
'ControlPort': ['9100'],
'DataDirectory': ['/tmp/fake dir'],
'DirPort': [],
}
control_message = ControlMessage.from_str(BATCH_RESPONSE, 'GETCONF', normalize = True)
self.assertEqual(expected, control_message.entries)
def test_multivalue_response(self):
"""
Parses a GETCONF reply containing a single key with multiple parameters.
"""
expected = {
'ControlPort': ['9100'],
'ExitPolicy': ['accept 34.3.4.5', 'accept 3.4.53.3', 'accept 3.4.53.3', 'reject 23.245.54.3']
}
control_message = ControlMessage.from_str(MULTIVALUE_RESPONSE, 'GETCONF', normalize = True)
self.assertEqual(expected, control_message.entries)
def test_unrecognized_key_response(self):
"""
Parses a GETCONF reply that contains an error code with an unrecognized key.
"""
try:
control_message = ControlMessage.from_str(UNRECOGNIZED_KEY_RESPONSE, normalize = True)
stem.response.convert('GETCONF', control_message)
self.fail('expected a stem.InvalidArguments to be raised')
except stem.InvalidArguments as exc:
self.assertEqual(exc.arguments, ['brickroad', 'submarine'])
def test_invalid_content(self):
"""
Parses a malformed GETCONF reply that contains an invalid response code.
This is a proper controller message, but malformed according to the
GETCONF's spec.
"""
control_message = ControlMessage.from_str(INVALID_RESPONSE, normalize = True)
self.assertRaises(stem.ProtocolError, stem.response.convert, 'GETCONF', control_message)
stem-1.6.0/test/unit/response/events.py 0000664 0001750 0001750 00000201421 13165210535 020663 0 ustar atagar atagar 0000000 0000000 """
Unit tests for the stem.response.events classes.
"""
import datetime
import threading
import unittest
import stem.response
import stem.response.events
import stem.util.log
from stem import * # enums and exceptions
from stem.response import ControlMessage
from stem.descriptor.router_status_entry import RouterStatusEntryV3
try:
# added in python 3.3
from unittest.mock import Mock
except ImportError:
from mock import Mock
# ADDRMAP event
ADDRMAP = '650 ADDRMAP www.atagar.com 75.119.206.243 "2012-11-19 00:50:13" \
EXPIRES="2012-11-19 08:50:13"'
ADDRMAP_NO_EXPIRATION = '650 ADDRMAP www.atagar.com 75.119.206.243 NEVER'
ADDRMAP_ERROR_EVENT = '650 ADDRMAP www.atagar.com "2012-11-19 00:50:13" \
error=yes EXPIRES="2012-11-19 08:50:13"'
ADDRMAP_BAD_1 = '650 ADDRMAP www.atagar.com 75.119.206.243 2012-11-19 00:50:13" \
EXPIRES="2012-11-19 08:50:13"'
ADDRMAP_BAD_2 = '650 ADDRMAP www.atagar.com 75.119.206.243 "2012-11-19 00:50:13 \
EXPIRES="2012-11-19 08:50:13"'
ADDRMAP_CACHED = '650 ADDRMAP example.com 192.0.43.10 "2013-04-03 22:31:22" \
EXPIRES="2013-04-03 20:31:22" \
CACHED="YES"'
ADDRMAP_NOT_CACHED = '650 ADDRMAP example.com 192.0.43.10 "2013-04-03 22:29:11" \
EXPIRES="2013-04-03 20:29:11" \
CACHED="NO"'
ADDRMAP_CACHED_MALFORMED = '650 ADDRMAP example.com 192.0.43.10 "2013-04-03 22:29:11" \
CACHED="KINDA"'
AUTHDIR_NEWDESC = """\
650+AUTHDIR_NEWDESCS
DROPPED
Not replacing router descriptor; no information has changed since the last one with this identity.
@uploaded-at 2017-05-25 04:46:21
@source "127.0.0.1"
router test002r 127.0.0.1 5002 0 7002
identity-ed25519
-----BEGIN ED25519 CERT-----
AQQABlm9AXVOPVG4KHqFmShWRFPU2oXO15yaS+J8c6SrLBMpnB1vAQAgBABBic8D
+GIdBzNCezf1Lfw8NSpbDL7S4ExBuMXvi6WvEoN1gIGwEwddLvUF91l6BXL5yoXf
xg7fDhYZ7CDwtVBHSfvmsIKR/QnQyylbDpVllsV9Wz6JLz52JgFGQaNjAgA=
-----END ED25519 CERT-----
master-key-ed25519 QYnPA/hiHQczQns39S38PDUqWwy+0uBMQbjF74ulrxI
platform Tor 0.3.0.7 on Darwin
proto Cons=1-2 Desc=1-2 DirCache=1 HSDir=1-2 HSIntro=3-4 HSRend=1-2 Link=1-4 LinkAuth=1,3 Microdesc=1-2 Relay=1-2
published 2017-05-25 04:46:20
fingerprint 3482 25F8 3C85 4796 B2DD 6364 E65C B189 B33B D696
uptime 9
bandwidth 1073741824 1073741824 0
extra-info-digest 114E4F433D6E08D9E73621BB418EBF02336EB578 i6+wKCBaNqUbNZIM9DFacEM74R5m0lkh8//h6R/nbGc
onion-key
-----BEGIN RSA PUBLIC KEY-----
MIGJAoGBAMMpf3fLSUEme0aQfN2RUfAPhZJMXVSqMpFRdBoKA4AYlz78VA9zxfj3
Nyir2G2HFaTzeS82p74obc8RufJQcGoDUwDnPlHtjb2ezmr018j8i3fTEvPwj5xC
5001FRwUVcOaLnxZKSDzpTyKRWGnSQBSbGcyXwMRtySKf0P5yjHDAgMBAAE=
-----END RSA PUBLIC KEY-----
signing-key
-----BEGIN RSA PUBLIC KEY-----
MIGJAoGBAM7Rn1kQffaJE8rbtgTiMxNY67i27hCBzzr0gE558jARizOJo8lf7get
rxz92mzYPYskM1V/j16QhoRlrruMn319/l6o97+/ta6qIwlSPXZ1jd/BGs3yqS4X
2N+N9qW8zC6km88K/YZuIsqYyXL7oHoIGqbERYLmp/JqLlAR52JJAgMBAAE=
-----END RSA PUBLIC KEY-----
onion-key-crosscert
-----BEGIN CROSSCERT-----
t4BE0PcPra9o5HBJHr9+h1MgP76XY3UGLQX8FGfEPHfNBMtVxRKkhOZ7Ki01+dkK
IpfkdQn3bOXTIa+FYGvzpyADYx4RDpbHG9/Tna/xR+6LhAQfvcLrlBxsjyntXjkX
FwE+AemijIU4DM4F1FHIkFz6OgT9B1/G0mr5QggzXS8=
-----END CROSSCERT-----
ntor-onion-key-crosscert 0
-----BEGIN ED25519 CERT-----
AQoABleVAUGJzwP4Yh0HM0J7N/Ut/Dw1KlsMvtLgTEG4xe+Lpa8SAKgaOVGouOKa
N64AOq7FdJJM/qgI1r2+jqTj1Mk/a19kfTIQ8hhWxoaJhg1xo8BasnjNQ+4Cm7ds
vMW0fhwJjQ4=
-----END ED25519 CERT-----
hidden-service-dir
ntor-onion-key tic6dEvMWt3kaUtDhwULgb3qBnc2wd5GkJ5cv0TelhA=
accept *:*
ipv6-policy accept 1-65535
tunnelled-dir-server
router-sig-ed25519 t7XJfoMOKbk1167f4p5+z5nlXxr+8us3qaLdNeQ1nGpUyQtM4G8Ie9P/oeqwhV4lCx5pQN1vIy3lUaN811WqAA
router-signature
-----BEGIN SIGNATURE-----
j/mMC1JuChvJH/ZP/Ayy0ZAV2P6VoxpRhHJfZMC07rr2ctmxfDTwLQhbYrqJ2adW
FYy5QTzxYzSFoTA7FvsadJuyvsGlaRfgFs+AxYjBUUoK7Dcd2ri+Y11NmwCBLFqF
4cYG3pqPHb38gXLj89QXfMJUDbOwrxvkVxIFbdwDCsE=
-----END SIGNATURE-----
.
650 OK
"""
# BUILDTIMEOUT_SET event from tor 0.2.3.16.
BUILD_TIMEOUT_EVENT = '650 BUILDTIMEOUT_SET COMPUTED \
TOTAL_TIMES=124 \
TIMEOUT_MS=9019 \
XM=1375 \
ALPHA=0.855662 \
CUTOFF_QUANTILE=0.800000 \
TIMEOUT_RATE=0.137097 \
CLOSE_MS=21850 \
CLOSE_RATE=0.072581'
BUILD_TIMEOUT_EVENT_BAD_1 = '650 BUILDTIMEOUT_SET COMPUTED \
TOTAL_TIMES=one_twenty_four \
TIMEOUT_MS=9019 \
XM=1375 \
ALPHA=0.855662 \
CUTOFF_QUANTILE=0.800000 \
TIMEOUT_RATE=0.137097 \
CLOSE_MS=21850 \
CLOSE_RATE=0.072581'
BUILD_TIMEOUT_EVENT_BAD_2 = '650 BUILDTIMEOUT_SET COMPUTED \
TOTAL_TIMES=124 \
TIMEOUT_MS=9019 \
XM=1375 \
ALPHA=0.855662 \
CUTOFF_QUANTILE=zero_point_eight \
TIMEOUT_RATE=0.137097 \
CLOSE_MS=21850 \
CLOSE_RATE=0.072581'
# CIRC events from tor v0.2.3.16
CIRC_LAUNCHED = '650 CIRC 7 LAUNCHED \
BUILD_FLAGS=NEED_CAPACITY \
PURPOSE=GENERAL \
TIME_CREATED=2012-11-08T16:48:38.417238'
CIRC_LAUNCHED_BAD_1 = '650 CIRC 7 LAUNCHED \
BUILD_FLAGS=NEED_CAPACITY \
PURPOSE=GENERAL \
TIME_CREATED=20121108T164838417238'
CIRC_LAUNCHED_BAD_2 = '650 CIRC toolong8901234567 LAUNCHED \
BUILD_FLAGS=NEED_CAPACITY \
PURPOSE=GENERAL \
TIME_CREATED=2012-11-08T16:48:38.417238'
CIRC_EXTENDED = '650 CIRC 7 EXTENDED \
$999A226EBED397F331B612FE1E4CFAE5C1F201BA=piyaz \
BUILD_FLAGS=NEED_CAPACITY \
PURPOSE=GENERAL \
TIME_CREATED=2012-11-08T16:48:38.417238'
CIRC_FAILED = '650 CIRC 5 FAILED \
$E57A476CD4DFBD99B4EE52A100A58610AD6E80B9=ergebnisoffen \
BUILD_FLAGS=NEED_CAPACITY \
PURPOSE=GENERAL \
TIME_CREATED=2012-11-08T16:48:36.400959 \
REASON=DESTROYED \
REMOTE_REASON=OR_CONN_CLOSED'
CIRC_WITH_CREDENTIALS = '650 CIRC 7 LAUNCHED \
SOCKS_USERNAME="It\'s a me, Mario!" \
SOCKS_PASSWORD="your princess is in another castle"'
# CIRC events from tor v0.2.1.30 without the VERBOSE_NAMES feature
CIRC_LAUNCHED_OLD = '650 CIRC 4 LAUNCHED'
CIRC_EXTENDED_OLD = '650 CIRC 1 EXTENDED \
$E57A476CD4DFBD99B4EE52A100A58610AD6E80B9,hamburgerphone'
CIRC_BUILT_OLD = '650 CIRC 1 BUILT \
$E57A476CD4DFBD99B4EE52A100A58610AD6E80B9,hamburgerphone,PrivacyRepublic14'
# CIRC_MINOR event from tor 0.2.3.16.
CIRC_MINOR_EVENT = '650 CIRC_MINOR 7 PURPOSE_CHANGED \
$67B2BDA4264D8A189D9270E28B1D30A262838243~europa1 \
BUILD_FLAGS=IS_INTERNAL,NEED_CAPACITY \
PURPOSE=MEASURE_TIMEOUT \
TIME_CREATED=2012-12-03T16:45:33.409602 \
OLD_PURPOSE=TESTING'
CIRC_MINOR_EVENT_BAD_1 = '650 CIRC_MINOR 7 PURPOSE_CHANGED \
$67B2BDA4264D8A189D9270E28B1D30A262838243~europa1 \
BUILD_FLAGS=IS_INTERNAL,NEED_CAPACITY \
PURPOSE=MEASURE_TIMEOUT \
TIME_CREATED=20121203T164533409602 \
OLD_PURPOSE=TESTING'
CIRC_MINOR_EVENT_BAD_2 = '650 CIRC_MINOR toolong8901234567 PURPOSE_CHANGED \
$67B2BDA4264D8A189D9270E28B1D30A262838243~europa1 \
BUILD_FLAGS=IS_INTERNAL,NEED_CAPACITY \
PURPOSE=MEASURE_TIMEOUT \
TIME_CREATED=2012-12-03T16:45:33.409602 \
OLD_PURPOSE=TESTING'
# CLIENTS_SEEN example from the spec
CLIENTS_SEEN_EVENT = '650 CLIENTS_SEEN \
TimeStarted="2008-12-25 23:50:43" \
CountrySummary=us=16,de=8,uk=8 \
IPVersions=v4=16,v6=40'
CLIENTS_SEEN_EVENT_BAD_1 = '650 CLIENTS_SEEN \
TimeStarted="2008-12-25 23:50:43" \
CountrySummary=us:16,de:8,uk:8 \
IPVersions=v4=16,v6=40'
CLIENTS_SEEN_EVENT_BAD_2 = '650 CLIENTS_SEEN \
TimeStarted="2008-12-25 23:50:43" \
CountrySummary=usa=16,unitedkingdom=8 \
IPVersions=v4=16,v6=40'
CLIENTS_SEEN_EVENT_BAD_3 = '650 CLIENTS_SEEN \
TimeStarted="2008-12-25 23:50:43" \
CountrySummary=us=16,de=8,uk=eight \
IPVersions=v4=16,v6=40'
CLIENTS_SEEN_EVENT_BAD_4 = '650 CLIENTS_SEEN \
TimeStarted="2008-12-25 23:50:43" \
CountrySummary=au=16,au=8,au=8 \
IPVersions=v4=16,v6=40'
CLIENTS_SEEN_EVENT_BAD_5 = '650 CLIENTS_SEEN \
TimeStarted="2008-12-25 23:50:43" \
CountrySummary=us=16,de=8,uk=8 \
IPVersions=v4:16,v6:40'
CLIENTS_SEEN_EVENT_BAD_6 = '650 CLIENTS_SEEN \
TimeStarted="2008-12-25 23:50:43" \
CountrySummary=us=16,de=8,uk=8 \
IPVersions=v4=sixteen,v6=40'
# CONF_CHANGED event from tor 0.2.3.16.
CONF_CHANGED_EVENT = """650-CONF_CHANGED
650-ExitNodes=caerSidi
650-ExitPolicy
650-MaxCircuitDirtiness=20
650 OK
"""
# GUARD events from tor v0.2.1.30.
GUARD_NEW = '650 GUARD ENTRY $36B5DBA788246E8369DBAF58577C6BC044A9A374 NEW'
GUARD_GOOD = '650 GUARD ENTRY $5D0034A368E0ABAF663D21847E1C9B6CFA09752A GOOD'
GUARD_BAD = '650 GUARD ENTRY $5D0034A368E0ABAF663D21847E1C9B6CFA09752A=caerSidi BAD'
HS_DESC_EVENT = '650 HS_DESC REQUESTED ajhb7kljbiru65qo NO_AUTH \
$67B2BDA4264D8A189D9270E28B1D30A262838243=europa1 b3oeducbhjmbqmgw2i3jtz4fekkrinwj'
HS_DESC_NO_DESC_ID = '650 HS_DESC REQUESTED ajhb7kljbiru65qo NO_AUTH \
$67B2BDA4264D8A189D9270E28B1D30A262838243'
HS_DESC_NOT_FOUND = '650 HS_DESC REQUESTED ajhb7kljbiru65qo NO_AUTH UNKNOWN'
HS_DESC_FAILED = '650 HS_DESC FAILED ajhb7kljbiru65qo NO_AUTH \
$67B2BDA4264D8A189D9270E28B1D30A262838243 \
b3oeducbhjmbqmgw2i3jtz4fekkrinwj REASON=NOT_FOUND'
# HS_DESC_CONTENT from 0.2.7.1
HS_DESC_CONTENT_EVENT = """\
650+HS_DESC_CONTENT facebookcorewwwi riwvyw6njgvs4koel4heqs7w4bssnmlw $8A30C9E8F5954EE286D29BD65CADEA6991200804~YorkshireTOR
rendezvous-service-descriptor riwvyw6njgvs4koel4heqs7w4bssnmlw
version 2
permanent-key
-----BEGIN RSA PUBLIC KEY-----
MIGJAoGBALfng/krEfrBcvblDiM3PAkowkiAKxLoTsXt3nPEzyTP6Cw+Gdr0ODje
hmxTngN1pKiH7szk4Q1p2RabOrUHWwXmGXeDDNs00fcyU6HupgqsCoKOqCsmPac6
/58apC64A7xHeS02wtfWJp6qiZ8i6GGu6xWXRWux+ShPgcHvkajRAgMahU8=
-----END RSA PUBLIC KEY-----
secret-id-part vnb2j6ftvkvghypd4yyypsl3qmpjyq3j
publication-time 2015-03-13 19:00:00
protocol-versions 2,3
introduction-points
-----BEGIN MESSAGE-----
aW50cm9kdWN0aW9uLXBvaW50IHNqbm1xbmdraXl3YmtkeXBjb2FqdHY2dmNtNjY2
NmR6CmlwLWFkZHJlc3MgMTk4LjIzLjE4Ny4xNTgKb25pb24tcG9ydCA0NDMKb25p
b24ta2V5Ci0tLS0tQkVHSU4gUlNBIFBVQkxJQyBLRVktLS0tLQpNSUdKQW9HQkFO
MUNsaERkZDNSWHdwT0hMZUNNYlVvNFlISDNEMUxnR0pXbEFPVnBxQ3ZSbDhEbjIv
UWpGeHNVCnJGaG9WUzRDUjlNVFIzMnlsSnJ0R2JTcWxRVm1HY3M3bnZ5RDU5YVky
em9RVGhIdm1lWVUwS0ZzQTc5ZFNyTW0KUDR5WnZFSkZmdkpQODRySWd0TlVtZ3R4
aHQzVzNiR2FVMUNBNGU4bjBza2hYWXdRRzg1MUFnTUJBQUU9Ci0tLS0tRU5EIFJT
QSBQVUJMSUMgS0VZLS0tLS0Kc2VydmljZS1rZXkKLS0tLS1CRUdJTiBSU0EgUFVC
TElDIEtFWS0tLS0tCk1JR0pBb0dCQUxDajREUTJPaTJhRjF4WE1iNjhsSHFJQnN5
NjRSbXFjTUpNb1d3THF4WTFiREcwbnE0Nlk5eHYKVnBPVzAxTmQrYnF3b3BIa0J2
TzllSGVKTm9NN1BYMmtVWmQ5RlFQSUJHbWdCZ0dxenV6a2lQTEFpbHhtWHRQbwpN
cnRheGdzRTR6MjlWYnJUV2Q0SHFKSDJFOWNybDdzeHhiTGVvSDFLRjZzSm5lMFlP
WGlyQWdNQkFBRT0KLS0tLS1FTkQgUlNBIFBVQkxJQyBLRVktLS0tLQppbnRyb2R1
Y3Rpb24tcG9pbnQgYmhzbjVhdDNzaDIzZGZ5cmNxYnU1bDV6NGs1Z3RueHAKaXAt
YWRkcmVzcyAxMDQuMTI4Ljc4LjEwNwpvbmlvbi1wb3J0IDMwMDIKb25pb24ta2V5
Ci0tLS0tQkVHSU4gUlNBIFBVQkxJQyBLRVktLS0tLQpNSUdKQW9HQkFMYk5HdU0v
RlNnZEJjOTFZSjNQOXRoVC9vQzRWOFZDZzZBcjk5WlFHRldhVGlRdXRjNGZLWC9F
CnR1TGRjdzBsRmxVbXhPZXNXMVduaVkxaVFDOW9yUkQ3cGE1amNES0EyRThDb3kv
WmYzYTlXNFNRRzYxakswUzcKYlNGVk9LUHQ3TDUvT21pK05icStsSnB5MmdCTnFU
TWt0U0k0YklPUlY1aUpWWkRWU21qVkFnTUJBQUU9Ci0tLS0tRU5EIFJTQSBQVUJM
SUMgS0VZLS0tLS0Kc2VydmljZS1rZXkKLS0tLS1CRUdJTiBSU0EgUFVCTElDIEtF
WS0tLS0tCk1JR0pBb0dCQU5YOVJobHRobkZkbXFXQVRsNGJ4dTBrR0UyNWlYcm83
VzFvM05GV3Q4cG8rL25oU080aVZRMHQKWVZzSGwyZEdGSVNKcWxqK3FaTXh1emVL
ZmNwV3dHQnZMR1FaTDZJYUxJMUxkWSt6YzBaNjFFdWx5dXRFWFl5bAo3djFwRWN2
dGFJSDhuRXdzQnZlU1ZWUVJ5cFI4b3BnbXhmMWFKWmdzZVdMSE5hZ0JwNW81QWdN
QkFBRT0KLS0tLS1FTkQgUlNBIFBVQkxJQyBLRVktLS0tLQppbnRyb2R1Y3Rpb24t
cG9pbnQgbTVibGd0dHRscWc1Mno1emJlcW82a2ViczQ0bG1wd2EKaXAtYWRkcmVz
cyAxNzYuMzEuMzUuMTQ5Cm9uaW9uLXBvcnQgNDQzCm9uaW9uLWtleQotLS0tLUJF
R0lOIFJTQSBQVUJMSUMgS0VZLS0tLS0KTUlHSkFvR0JBTnVORFlobFF2RG9TNEFa
cE5nUkFLUmZhRjAzVWFSM0JrSXo3UC8zOVB4NjZueDc1bG5wQ1pMYwpkSHl4cGJu
UWp2ekE0UzdjUUVnYXUyQkgyeUtzU1NBL3ZXUHk4OVJBWUVhaUV2TlZQS1hRWmNw
cnY0WXdmejU0CmRuY2VJNG51NVFQM0E3SUpkSi9PYTlHMklhdHA3OVBlTzJkN2Rq
L3pzWFNKMkxvRXgyZWRBZ01CQUFFPQotLS0tLUVORCBSU0EgUFVCTElDIEtFWS0t
LS0tCnNlcnZpY2Uta2V5Ci0tLS0tQkVHSU4gUlNBIFBVQkxJQyBLRVktLS0tLQpN
SUdKQW9HQkFLbU9KbXB6ZVphVkZXaVlkMms5SHY1TWpidUY0eDBUKzlXclV4Z041
N2o2Uk1CVFZQZ0lVM2hUCkdCY3dwWjR2NDduNitIbVg4VHFNTFlVZkc1bTg1cm8x
SHNKMWVObnh2cW9iZVFVMW13TXdHdDMwbkJ6Y0F2NzMKbWFsYmlYRkxiOVdsK1hl
OTBRdXZhbjZTenhERkx5STFPbzA2aGVUeVZwQ3d0QVVvejhCVEFnTUJBQUU9Ci0t
LS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0KCg==
-----END MESSAGE-----
signature
-----BEGIN SIGNATURE-----
s9Z0zWHsoPuqLw3GOwA6cv68vCFybnitIK4vD0MbNKF5qrOfKydZzGWfN09PuShy
H8Gr6aBYoz7HmlD7KyeGz9xdwRdouP+LW5YREOCORuwYnu5chWtB4iVJ0N1tIzSp
nHIs1lSrV7Ux2WQ3qSVj505fTGSCmaQRBX726ZlTPW0=
-----END SIGNATURE-----
.
650 OK
"""
HS_DESC_CONTENT_EMPTY_EVENT = """\
650+HS_DESC_CONTENT 3g2upl4pq6kufc4n 255tjwttk3wi7r2df57nuprs72j2daa3 $D7A0C3262724F2BC9646F6836E967A2777A3AF83~tsunaminitor
.
650 OK
"""
# NEWCONSENSUS event from v0.2.1.30.
NEWCONSENSUS_EVENT = """650+NEWCONSENSUS
r Beaver /96bKo4soysolMgKn5Hex2nyFSY pAJH9dSBp/CG6sPhhVY/5bLaVPM 2012-12-02 22:02:45 77.223.43.54 9001 0
s Fast Named Running Stable Valid
r Unnamed /+fJRWjmIGNAL2C5rRZHq3R91tA 7AnpZjfdBpYzXnMNm+w1bTsFF6Y 2012-12-02 17:51:10 91.121.184.87 9001 0
s Fast Guard Running Stable Valid
.
650 OK
"""
# NEWDESC events. I've never actually seen multiple descriptors in an event,
# but the spec allows for it.
NEWDESC_SINGLE = '650 NEWDESC $B3FA3110CC6F42443F039220C134CBD2FC4F0493=Sakura'
NEWDESC_MULTIPLE = '650 NEWDESC $BE938957B2CA5F804B3AFC2C1EE6673170CDBBF8=Moonshine \
$B4BE08B22D4D2923EDC3970FD1B93D0448C6D8FF~Unnamed'
# NS event from tor v0.2.1.30.
NS_EVENT = """650+NS
r whnetz dbBxYcJriTTrcxsuy4PUZcMRwCA VStM7KAIH/mXXoGDUpoGB1OXufg 2012-12-02 21:03:56 141.70.120.13 9001 9030
s Fast HSDir Named Stable V2Dir Valid
.
650 OK
"""
# ORCONN events from starting tor 0.2.2.39 via TBB
ORCONN_CLOSED = '650 ORCONN $A1130635A0CDA6F60C276FBF6994EFBD4ECADAB1~tama CLOSED REASON=DONE'
ORCONN_CONNECTED = '650 ORCONN 127.0.0.1:9000 CONNECTED NCIRCS=20 ID=18'
ORCONN_LAUNCHED = '650 ORCONN $7ED90E2833EE38A75795BA9237B0A4560E51E1A0=GreenDragon LAUNCHED'
ORCONN_BAD_1 = '650 ORCONN $7ED90E2833EE38A75795BA9237B0A4560E5=GreenD LAUNCHED'
ORCONN_BAD_2 = '650 ORCONN 127.0.0.1:001 CONNECTED'
ORCONN_BAD_3 = '650 ORCONN 127.0.0.1:9000 CONNECTED NCIRCS=too_many'
ORCONN_BAD_4 = '650 ORCONN 127.0.0.1:9000 CONNECTED NCIRCS=20 ID=30635A0CDA6F60C276FBF6994EFBD4ECADA'
# STATUS_* events that I was able to easily trigger. Most came from starting
# TBB, then listening while it bootstrapped.
STATUS_GENERAL_CONSENSUS_ARRIVED = '650 STATUS_GENERAL NOTICE CONSENSUS_ARRIVED'
STATUS_CLIENT_ENOUGH_DIR_INFO = '650 STATUS_CLIENT NOTICE ENOUGH_DIR_INFO'
STATUS_CLIENT_CIRC_ESTABLISHED = '650 STATUS_CLIENT NOTICE CIRCUIT_ESTABLISHED'
STATUS_CLIENT_BOOTSTRAP_DESCRIPTORS = '650 STATUS_CLIENT NOTICE BOOTSTRAP \
PROGRESS=53 \
TAG=loading_descriptors \
SUMMARY="Loading relay descriptors"'
STATUS_CLIENT_BOOTSTRAP_STUCK = '650 STATUS_CLIENT WARN BOOTSTRAP \
PROGRESS=80 \
TAG=conn_or \
SUMMARY="Connecting to the Tor network" \
WARNING="Network is unreachable" \
REASON=NOROUTE \
COUNT=5 \
RECOMMENDATION=warn'
STATUS_CLIENT_BOOTSTRAP_CONNECTING = '650 STATUS_CLIENT NOTICE BOOTSTRAP \
PROGRESS=80 \
TAG=conn_or \
SUMMARY="Connecting to the Tor network"'
STATUS_CLIENT_BOOTSTRAP_FIRST_HANDSHAKE = '650 STATUS_CLIENT NOTICE BOOTSTRAP \
PROGRESS=85 \
TAG=handshake_or \
SUMMARY="Finishing handshake with first hop"'
STATUS_CLIENT_BOOTSTRAP_ESTABLISHED = '650 STATUS_CLIENT NOTICE BOOTSTRAP \
PROGRESS=90 \
TAG=circuit_create \
SUMMARY="Establishing a Tor circuit"'
STATUS_CLIENT_BOOTSTRAP_DONE = '650 STATUS_CLIENT NOTICE BOOTSTRAP \
PROGRESS=100 \
TAG=done \
SUMMARY="Done"'
STATUS_SERVER_CHECK_REACHABILITY = '650 STATUS_SERVER NOTICE CHECKING_REACHABILITY \
ORADDRESS=71.35.143.230:9050'
STATUS_SERVER_DNS_TIMEOUT = '650 STATUS_SERVER NOTICE NAMESERVER_STATUS \
NS=205.171.3.25 \
STATUS=DOWN \
ERR="request timed out."'
STATUS_SERVER_DNS_DOWN = '650 STATUS_SERVER WARN NAMESERVER_ALL_DOWN'
STATUS_SERVER_DNS_UP = '650 STATUS_SERVER NOTICE NAMESERVER_STATUS \
NS=205.171.3.25 \
STATUS=UP'
# unknown STATUS_* event type
STATUS_SPECIFIC_CONSENSUS_ARRIVED = '650 STATUS_SPECIFIC NOTICE CONSENSUS_ARRIVED'
# STREAM events from tor 0.2.3.16 for visiting the google front page
STREAM_NEW = '650 STREAM 18 NEW 0 \
encrypted.google.com:443 \
SOURCE_ADDR=127.0.0.1:47849 \
PURPOSE=USER'
STREAM_SENTCONNECT = '650 STREAM 18 SENTCONNECT 26 encrypted.google.com:443'
STREAM_REMAP = '650 STREAM 18 REMAP 26 74.125.227.129:443 SOURCE=EXIT'
STREAM_SUCCEEDED = '650 STREAM 18 SUCCEEDED 26 74.125.227.129:443'
STREAM_CLOSED_RESET = '650 STREAM 21 CLOSED 26 74.125.227.129:443 REASON=CONNRESET'
STREAM_CLOSED_DONE = '650 STREAM 25 CLOSED 26 199.7.52.72:80 REASON=DONE'
STREAM_DIR_FETCH = '650 STREAM 14 NEW 0 \
176.28.51.238.$649F2D0ACF418F7CFC6539AB2257EB2D5297BAFA.exit:443 \
SOURCE_ADDR=(Tor_internal):0 PURPOSE=DIR_FETCH'
STREAM_DNS_REQUEST = '650 STREAM 1113 NEW 0 www.google.com:0 \
SOURCE_ADDR=127.0.0.1:15297 \
PURPOSE=DNS_REQUEST'
STREAM_SENTCONNECT_BAD_1 = '650 STREAM 18 SENTCONNECT 26'
STREAM_SENTCONNECT_BAD_2 = '650 STREAM 18 SENTCONNECT 26 encrypted.google.com'
STREAM_SENTCONNECT_BAD_3 = '650 STREAM 18 SENTCONNECT 26 encrypted.google.com:https'
STREAM_DNS_REQUEST_BAD_1 = '650 STREAM 1113 NEW 0 www.google.com:0 \
SOURCE_ADDR=127.0.0.1 \
PURPOSE=DNS_REQUEST'
STREAM_DNS_REQUEST_BAD_2 = '650 STREAM 1113 NEW 0 www.google.com:0 \
SOURCE_ADDR=127.0.0.1:dns \
PURPOSE=DNS_REQUEST'
STREAM_NEWRESOLVE_IP6 = '650 STREAM 23 NEWRESOLVE 0 2001:db8::1:0 PURPOSE=DNS_REQUEST'
TRANSPORT_LAUNCHED = '650 TRANSPORT_LAUNCHED server obfs1 127.0.0.1 1111'
TRANSPORT_LAUNCHED_BAD_TYPE = '650 TRANSPORT_LAUNCHED unicorn obfs1 127.0.0.1 1111'
TRANSPORT_LAUNCHED_BAD_ADDRESS = '650 TRANSPORT_LAUNCHED server obfs1 127.0.x.y 1111'
TRANSPORT_LAUNCHED_BAD_PORT = '650 TRANSPORT_LAUNCHED server obfs1 127.0.0.1 my_port'
CONN_BW = '650 CONN_BW ID=11 TYPE=DIR READ=272 WRITTEN=817'
CONN_BW_BAD_WRITTEN_VALUE = '650 CONN_BW ID=11 TYPE=DIR READ=272 WRITTEN=817.7'
CONN_BW_BAD_MISSING_ID = '650 CONN_BW TYPE=DIR READ=272 WRITTEN=817'
CIRC_BW = '650 CIRC_BW ID=11 READ=272 WRITTEN=817'
CIRC_BW_WITH_TIMESTAMP = '650 CIRC_BW ID=11 READ=272 WRITTEN=817 TIME=2012-12-06T13:51:11.433755'
CIRC_BW_BAD_WRITTEN_VALUE = '650 CIRC_BW ID=11 READ=272 WRITTEN=817.7'
CIRC_BW_BAD_MISSING_ID = '650 CIRC_BW READ=272 WRITTEN=817'
CIRC_BW_MALFORMED_TIMESTAMP = '650 CIRC_BW ID=11 READ=272 WRITTEN=817 TIME=boom'
CELL_STATS_1 = '650 CELL_STATS ID=14 \
OutboundQueue=19403 OutboundConn=15 \
OutboundAdded=create_fast:1,relay_early:2 \
OutboundRemoved=create_fast:1,relay_early:2 \
OutboundTime=create_fast:0,relay_early:0'
CELL_STATS_2 = '650 CELL_STATS \
InboundQueue=19403 InboundConn=32 \
InboundAdded=relay:1,created_fast:1 \
InboundRemoved=relay:1,created_fast:1 \
InboundTime=relay:0,created_fast:0 \
OutboundQueue=6710 OutboundConn=18 \
OutboundAdded=create:1,relay_early:1 \
OutboundRemoved=create:1,relay_early:1 \
OutboundTime=create:0,relay_early:0'
CELL_STATS_BAD_1 = '650 CELL_STATS OutboundAdded=create_fast:-1,relay_early:2'
CELL_STATS_BAD_2 = '650 CELL_STATS OutboundAdded=create_fast:arg,relay_early:-2'
CELL_STATS_BAD_3 = '650 CELL_STATS OutboundAdded=create_fast!:1,relay_early:-2'
TB_EMPTY_1 = '650 TB_EMPTY ORCONN ID=16 READ=0 WRITTEN=0 LAST=100'
TB_EMPTY_2 = '650 TB_EMPTY GLOBAL READ=93 WRITTEN=93 LAST=100'
TB_EMPTY_3 = '650 TB_EMPTY RELAY READ=93 WRITTEN=93 LAST=100'
TB_EMPTY_BAD_1 = '650 TB_EMPTY GLOBAL READ=93 WRITTEN=blarg LAST=100'
TB_EMPTY_BAD_2 = '650 TB_EMPTY GLOBAL READ=93 WRITTEN=93 LAST=-100'
def _get_event(content):
return ControlMessage.from_str(content, 'EVENT', normalize = True)
class TestEvents(unittest.TestCase):
def test_example(self):
"""
Exercises the add_event_listener() pydoc example, but without the sleep().
"""
import time
from stem.control import Controller, EventType
def print_bw(event):
msg = 'sent: %i, received: %i' % (event.written, event.read)
self.assertEqual('sent: 25, received: 15', msg)
def event_sender():
for index in range(3):
print_bw(_get_event('650 BW 15 25'))
time.sleep(0.0005)
controller = Mock(spec = Controller)
controller.authenticate()
controller.add_event_listener(print_bw, EventType.BW)
events_thread = threading.Thread(target = event_sender)
events_thread.start()
time.sleep(0.002)
events_thread.join()
def test_event(self):
# synthetic, contrived message construction to reach the blank event check
self.assertRaises(ProtocolError, stem.response.convert, 'EVENT', stem.response.ControlMessage([('', '', '')], ''))
# Event._parse_message() on an unknown event type
event = _get_event('650 NONE SOLID "NON SENSE" condition=MEH quoted="1 2 3"')
self.assertEqual('NONE', event.type)
self.assertEqual(['SOLID', '"NON', 'SENSE"'], event.positional_args)
self.assertEqual({'condition': 'MEH', 'quoted': '1 2 3'}, event.keyword_args)
def test_log_events(self):
event = _get_event('650 DEBUG connection_edge_process_relay_cell(): Got an extended cell! Yay.')
self.assertTrue(isinstance(event, stem.response.events.LogEvent))
self.assertEqual('DEBUG', event.runlevel)
self.assertEqual('connection_edge_process_relay_cell(): Got an extended cell! Yay.', event.message)
event = _get_event('650 INFO circuit_finish_handshake(): Finished building circuit hop:')
self.assertTrue(isinstance(event, stem.response.events.LogEvent))
self.assertEqual('INFO', event.runlevel)
self.assertEqual('circuit_finish_handshake(): Finished building circuit hop:', event.message)
event = _get_event('650+WARN\na multi-line\nwarning message\n.\n650 OK\n')
self.assertTrue(isinstance(event, stem.response.events.LogEvent))
self.assertEqual('WARN', event.runlevel)
self.assertEqual('a multi-line\nwarning message', event.message)
def test_addrmap_event(self):
event = _get_event(ADDRMAP)
self.assertTrue(isinstance(event, stem.response.events.AddrMapEvent))
self.assertEqual(ADDRMAP.lstrip('650 '), str(event))
self.assertEqual('www.atagar.com', event.hostname)
self.assertEqual('75.119.206.243', event.destination)
self.assertEqual(datetime.datetime(2012, 11, 19, 0, 50, 13), event.expiry)
self.assertEqual(None, event.error)
self.assertEqual(datetime.datetime(2012, 11, 19, 8, 50, 13), event.utc_expiry)
event = _get_event(ADDRMAP_NO_EXPIRATION)
self.assertTrue(isinstance(event, stem.response.events.AddrMapEvent))
self.assertEqual(ADDRMAP_NO_EXPIRATION.lstrip('650 '), str(event))
self.assertEqual('www.atagar.com', event.hostname)
self.assertEqual('75.119.206.243', event.destination)
self.assertEqual(None, event.expiry)
self.assertEqual(None, event.error)
self.assertEqual(None, event.utc_expiry)
event = _get_event(ADDRMAP_ERROR_EVENT)
self.assertTrue(isinstance(event, stem.response.events.AddrMapEvent))
self.assertEqual(ADDRMAP_ERROR_EVENT.lstrip('650 '), str(event))
self.assertEqual('www.atagar.com', event.hostname)
self.assertEqual(None, event.destination)
self.assertEqual(datetime.datetime(2012, 11, 19, 0, 50, 13), event.expiry)
self.assertEqual('yes', event.error)
self.assertEqual(datetime.datetime(2012, 11, 19, 8, 50, 13), event.utc_expiry)
self.assertEqual(None, event.cached)
# malformed content where quotes are missing
self.assertRaises(ProtocolError, _get_event, ADDRMAP_BAD_1)
self.assertRaises(ProtocolError, _get_event, ADDRMAP_BAD_2)
# check the CACHED flag
event = _get_event(ADDRMAP_CACHED)
self.assertTrue(isinstance(event, stem.response.events.AddrMapEvent))
self.assertEqual('example.com', event.hostname)
self.assertEqual(True, event.cached)
event = _get_event(ADDRMAP_NOT_CACHED)
self.assertTrue(isinstance(event, stem.response.events.AddrMapEvent))
self.assertEqual('example.com', event.hostname)
self.assertEqual(False, event.cached)
# the CACHED argument should only allow YES or NO
self.assertRaises(ProtocolError, _get_event, ADDRMAP_CACHED_MALFORMED)
def test_authdir_newdesc_event(self):
minimal_event = _get_event('650+AUTHDIR_NEWDESCS\nAction\nMessage\nDescriptor\n.\n650 OK\n')
self.assertTrue(isinstance(minimal_event, stem.response.events.AuthDirNewDescEvent))
self.assertEqual('Action', minimal_event.action)
self.assertEqual('Message', minimal_event.message)
self.assertEqual('Descriptor', minimal_event.descriptor)
event = _get_event(AUTHDIR_NEWDESC)
self.assertTrue(isinstance(event, stem.response.events.AuthDirNewDescEvent))
self.assertEqual('DROPPED', event.action)
self.assertEqual('Not replacing router descriptor; no information has changed since the last one with this identity.', event.message)
self.assertTrue('Descripto', event.descriptor.startswith('@uploaded-at 2017-05-25 04:46:21'))
def test_build_timeout_set_event(self):
event = _get_event(BUILD_TIMEOUT_EVENT)
self.assertTrue(isinstance(event, stem.response.events.BuildTimeoutSetEvent))
self.assertEqual(BUILD_TIMEOUT_EVENT.lstrip('650 '), str(event))
self.assertEqual(TimeoutSetType.COMPUTED, event.set_type)
self.assertEqual(124, event.total_times)
self.assertEqual(9019, event.timeout)
self.assertEqual(1375, event.xm)
self.assertEqual(0.855662, event.alpha)
self.assertEqual(0.8, event.quantile)
self.assertEqual(0.137097, event.timeout_rate)
self.assertEqual(21850, event.close_timeout)
self.assertEqual(0.072581, event.close_rate)
# malformed content where we get non-numeric values
self.assertRaises(ProtocolError, _get_event, BUILD_TIMEOUT_EVENT_BAD_1)
self.assertRaises(ProtocolError, _get_event, BUILD_TIMEOUT_EVENT_BAD_2)
def test_bw_event(self):
event = _get_event('650 BW 15 25')
self.assertTrue(isinstance(event, stem.response.events.BandwidthEvent))
self.assertEqual(15, event.read)
self.assertEqual(25, event.written)
event = _get_event('650 BW 0 0')
self.assertEqual(0, event.read)
self.assertEqual(0, event.written)
# BW events are documented as possibly having various keywords including
# DIR, OR, EXIT, and APP in the future. This is kinda a pointless note
# since tor doesn't actually do it yet (and likely never will), but might
# as well sanity test that it'll be ok.
event = _get_event('650 BW 10 20 OR=5 EXIT=500')
self.assertEqual(10, event.read)
self.assertEqual(20, event.written)
self.assertEqual({'OR': '5', 'EXIT': '500'}, event.keyword_args)
self.assertRaises(ProtocolError, _get_event, '650 BW')
self.assertRaises(ProtocolError, _get_event, '650 BW 15')
self.assertRaises(ProtocolError, _get_event, '650 BW -15 25')
self.assertRaises(ProtocolError, _get_event, '650 BW 15 -25')
self.assertRaises(ProtocolError, _get_event, '650 BW x 25')
def test_circ_event(self):
event = _get_event(CIRC_LAUNCHED)
self.assertTrue(isinstance(event, stem.response.events.CircuitEvent))
self.assertEqual(CIRC_LAUNCHED.lstrip('650 '), str(event))
self.assertEqual('7', event.id)
self.assertEqual(CircStatus.LAUNCHED, event.status)
self.assertEqual((), event.path)
self.assertEqual((CircBuildFlag.NEED_CAPACITY,), event.build_flags)
self.assertEqual(CircPurpose.GENERAL, event.purpose)
self.assertEqual(None, event.hs_state)
self.assertEqual(None, event.rend_query)
self.assertEqual(datetime.datetime(2012, 11, 8, 16, 48, 38, 417238), event.created)
self.assertEqual(None, event.reason)
self.assertEqual(None, event.remote_reason)
self.assertEqual(None, event.socks_username)
self.assertEqual(None, event.socks_password)
event = _get_event(CIRC_EXTENDED)
self.assertTrue(isinstance(event, stem.response.events.CircuitEvent))
self.assertEqual(CIRC_EXTENDED.lstrip('650 '), str(event))
self.assertEqual('7', event.id)
self.assertEqual(CircStatus.EXTENDED, event.status)
self.assertEqual((('999A226EBED397F331B612FE1E4CFAE5C1F201BA', 'piyaz'),), event.path)
self.assertEqual((CircBuildFlag.NEED_CAPACITY,), event.build_flags)
self.assertEqual(CircPurpose.GENERAL, event.purpose)
self.assertEqual(None, event.hs_state)
self.assertEqual(None, event.rend_query)
self.assertEqual(datetime.datetime(2012, 11, 8, 16, 48, 38, 417238), event.created)
self.assertEqual(None, event.reason)
self.assertEqual(None, event.remote_reason)
event = _get_event(CIRC_FAILED)
self.assertTrue(isinstance(event, stem.response.events.CircuitEvent))
self.assertEqual(CIRC_FAILED.lstrip('650 '), str(event))
self.assertEqual('5', event.id)
self.assertEqual(CircStatus.FAILED, event.status)
self.assertEqual((('E57A476CD4DFBD99B4EE52A100A58610AD6E80B9', 'ergebnisoffen'),), event.path)
self.assertEqual((CircBuildFlag.NEED_CAPACITY,), event.build_flags)
self.assertEqual(CircPurpose.GENERAL, event.purpose)
self.assertEqual(None, event.hs_state)
self.assertEqual(None, event.rend_query)
self.assertEqual(datetime.datetime(2012, 11, 8, 16, 48, 36, 400959), event.created)
self.assertEqual(CircClosureReason.DESTROYED, event.reason)
self.assertEqual(CircClosureReason.OR_CONN_CLOSED, event.remote_reason)
event = _get_event(CIRC_WITH_CREDENTIALS)
self.assertTrue(isinstance(event, stem.response.events.CircuitEvent))
self.assertEqual(CIRC_WITH_CREDENTIALS.lstrip('650 '), str(event))
self.assertEqual('7', event.id)
self.assertEqual(CircStatus.LAUNCHED, event.status)
self.assertEqual((), event.path)
self.assertEqual(None, event.build_flags)
self.assertEqual(None, event.purpose)
self.assertEqual(None, event.hs_state)
self.assertEqual(None, event.rend_query)
self.assertEqual(None, event.created)
self.assertEqual(None, event.reason)
self.assertEqual(None, event.remote_reason)
self.assertEqual("It's a me, Mario!", event.socks_username)
self.assertEqual('your princess is in another castle', event.socks_password)
event = _get_event(CIRC_LAUNCHED_OLD)
self.assertTrue(isinstance(event, stem.response.events.CircuitEvent))
self.assertEqual(CIRC_LAUNCHED_OLD.lstrip('650 '), str(event))
self.assertEqual('4', event.id)
self.assertEqual(CircStatus.LAUNCHED, event.status)
self.assertEqual((), event.path)
self.assertEqual(None, event.build_flags)
self.assertEqual(None, event.purpose)
self.assertEqual(None, event.hs_state)
self.assertEqual(None, event.rend_query)
self.assertEqual(None, event.created)
self.assertEqual(None, event.reason)
self.assertEqual(None, event.remote_reason)
event = _get_event(CIRC_EXTENDED_OLD)
self.assertTrue(isinstance(event, stem.response.events.CircuitEvent))
self.assertEqual(CIRC_EXTENDED_OLD.lstrip('650 '), str(event))
self.assertEqual('1', event.id)
self.assertEqual(CircStatus.EXTENDED, event.status)
self.assertEqual((('E57A476CD4DFBD99B4EE52A100A58610AD6E80B9', None), (None, 'hamburgerphone')), event.path)
self.assertEqual(None, event.build_flags)
self.assertEqual(None, event.purpose)
self.assertEqual(None, event.hs_state)
self.assertEqual(None, event.rend_query)
self.assertEqual(None, event.created)
self.assertEqual(None, event.reason)
self.assertEqual(None, event.remote_reason)
event = _get_event(CIRC_BUILT_OLD)
self.assertTrue(isinstance(event, stem.response.events.CircuitEvent))
self.assertEqual(CIRC_BUILT_OLD.lstrip('650 '), str(event))
self.assertEqual('1', event.id)
self.assertEqual(CircStatus.BUILT, event.status)
self.assertEqual((('E57A476CD4DFBD99B4EE52A100A58610AD6E80B9', None), (None, 'hamburgerphone'), (None, 'PrivacyRepublic14')), event.path)
self.assertEqual(None, event.build_flags)
self.assertEqual(None, event.purpose)
self.assertEqual(None, event.hs_state)
self.assertEqual(None, event.rend_query)
self.assertEqual(None, event.created)
self.assertEqual(None, event.reason)
self.assertEqual(None, event.remote_reason)
# malformed TIME_CREATED timestamp
self.assertRaises(ProtocolError, _get_event, CIRC_LAUNCHED_BAD_1)
# invalid circuit id
self.assertRaises(ProtocolError, _get_event, CIRC_LAUNCHED_BAD_2)
def test_circ_minor_event(self):
event = _get_event(CIRC_MINOR_EVENT)
self.assertTrue(isinstance(event, stem.response.events.CircMinorEvent))
self.assertEqual(CIRC_MINOR_EVENT.lstrip('650 '), str(event))
self.assertEqual('7', event.id)
self.assertEqual(CircEvent.PURPOSE_CHANGED, event.event)
self.assertEqual((('67B2BDA4264D8A189D9270E28B1D30A262838243', 'europa1'),), event.path)
self.assertEqual((CircBuildFlag.IS_INTERNAL, CircBuildFlag.NEED_CAPACITY), event.build_flags)
self.assertEqual(CircPurpose.MEASURE_TIMEOUT, event.purpose)
self.assertEqual(None, event.hs_state)
self.assertEqual(None, event.rend_query)
self.assertEqual(datetime.datetime(2012, 12, 3, 16, 45, 33, 409602), event.created)
self.assertEqual(CircPurpose.TESTING, event.old_purpose)
self.assertEqual(None, event.old_hs_state)
# malformed TIME_CREATED timestamp
self.assertRaises(ProtocolError, _get_event, CIRC_MINOR_EVENT_BAD_1)
# invalid circuit id
self.assertRaises(ProtocolError, _get_event, CIRC_MINOR_EVENT_BAD_2)
def test_clients_seen_event(self):
event = _get_event(CLIENTS_SEEN_EVENT)
self.assertTrue(isinstance(event, stem.response.events.ClientsSeenEvent))
self.assertEqual(CLIENTS_SEEN_EVENT.lstrip('650 '), str(event))
self.assertEqual(datetime.datetime(2008, 12, 25, 23, 50, 43), event.start_time)
self.assertEqual({'us': 16, 'de': 8, 'uk': 8}, event.locales)
self.assertEqual({'v4': 16, 'v6': 40}, event.ip_versions)
# CountrySummary's 'key=value' mappings are replaced with 'key:value'
self.assertRaises(ProtocolError, _get_event, CLIENTS_SEEN_EVENT_BAD_1)
# CountrySummary's country codes aren't two letters
self.assertRaises(ProtocolError, _get_event, CLIENTS_SEEN_EVENT_BAD_2)
# CountrySummary's mapping contains a non-numeric value
self.assertRaises(ProtocolError, _get_event, CLIENTS_SEEN_EVENT_BAD_3)
# CountrySummary has duplicate country codes (multiple 'au=' mappings)
self.assertRaises(ProtocolError, _get_event, CLIENTS_SEEN_EVENT_BAD_4)
# IPVersions's 'key=value' mappings are replaced with 'key:value'
self.assertRaises(ProtocolError, _get_event, CLIENTS_SEEN_EVENT_BAD_5)
# IPVersions's mapping contains a non-numeric value
self.assertRaises(ProtocolError, _get_event, CLIENTS_SEEN_EVENT_BAD_6)
def test_conf_changed(self):
event = _get_event(CONF_CHANGED_EVENT)
expected_config = {
'ExitNodes': 'caerSidi',
'MaxCircuitDirtiness': '20',
'ExitPolicy': None,
}
self.assertTrue(isinstance(event, stem.response.events.ConfChangedEvent))
self.assertEqual(expected_config, event.config)
def test_descchanged_event(self):
# all we can check for is that the event is properly parsed as a
# DescChangedEvent instance
event = _get_event('650 DESCCHANGED')
self.assertTrue(isinstance(event, stem.response.events.DescChangedEvent))
self.assertEqual('DESCCHANGED', str(event))
self.assertEqual([], event.positional_args)
self.assertEqual({}, event.keyword_args)
def test_guard_event(self):
event = _get_event(GUARD_NEW)
self.assertTrue(isinstance(event, stem.response.events.GuardEvent))
self.assertEqual(GUARD_NEW.lstrip('650 '), str(event))
self.assertEqual(GuardType.ENTRY, event.guard_type)
self.assertEqual('$36B5DBA788246E8369DBAF58577C6BC044A9A374', event.endpoint)
self.assertEqual('36B5DBA788246E8369DBAF58577C6BC044A9A374', event.endpoint_fingerprint)
self.assertEqual(None, event.endpoint_nickname)
self.assertEqual(GuardStatus.NEW, event.status)
event = _get_event(GUARD_GOOD)
self.assertEqual(GuardType.ENTRY, event.guard_type)
self.assertEqual('$5D0034A368E0ABAF663D21847E1C9B6CFA09752A', event.endpoint)
self.assertEqual('5D0034A368E0ABAF663D21847E1C9B6CFA09752A', event.endpoint_fingerprint)
self.assertEqual(None, event.endpoint_nickname)
self.assertEqual(GuardStatus.GOOD, event.status)
event = _get_event(GUARD_BAD)
self.assertEqual(GuardType.ENTRY, event.guard_type)
self.assertEqual('$5D0034A368E0ABAF663D21847E1C9B6CFA09752A=caerSidi', event.endpoint)
self.assertEqual('5D0034A368E0ABAF663D21847E1C9B6CFA09752A', event.endpoint_fingerprint)
self.assertEqual('caerSidi', event.endpoint_nickname)
self.assertEqual(GuardStatus.BAD, event.status)
def test_hs_desc_event(self):
event = _get_event(HS_DESC_EVENT)
self.assertTrue(isinstance(event, stem.response.events.HSDescEvent))
self.assertEqual(HS_DESC_EVENT.lstrip('650 '), str(event))
self.assertEqual(HSDescAction.REQUESTED, event.action)
self.assertEqual('ajhb7kljbiru65qo', event.address)
self.assertEqual(HSAuth.NO_AUTH, event.authentication)
self.assertEqual('$67B2BDA4264D8A189D9270E28B1D30A262838243=europa1', event.directory)
self.assertEqual('67B2BDA4264D8A189D9270E28B1D30A262838243', event.directory_fingerprint)
self.assertEqual('europa1', event.directory_nickname)
self.assertEqual('b3oeducbhjmbqmgw2i3jtz4fekkrinwj', event.descriptor_id)
self.assertEqual(None, event.reason)
event = _get_event(HS_DESC_NO_DESC_ID)
self.assertEqual('$67B2BDA4264D8A189D9270E28B1D30A262838243', event.directory)
self.assertEqual('67B2BDA4264D8A189D9270E28B1D30A262838243', event.directory_fingerprint)
self.assertEqual(None, event.directory_nickname)
self.assertEqual(None, event.descriptor_id)
self.assertEqual(None, event.reason)
event = _get_event(HS_DESC_NOT_FOUND)
self.assertEqual('UNKNOWN', event.directory)
self.assertEqual(None, event.directory_fingerprint)
self.assertEqual(None, event.directory_nickname)
self.assertEqual(None, event.descriptor_id)
self.assertEqual(None, event.reason)
event = _get_event(HS_DESC_FAILED)
self.assertTrue(isinstance(event, stem.response.events.HSDescEvent))
self.assertEqual(HS_DESC_FAILED.lstrip('650 '), str(event))
self.assertEqual(HSDescAction.FAILED, event.action)
self.assertEqual('ajhb7kljbiru65qo', event.address)
self.assertEqual(HSAuth.NO_AUTH, event.authentication)
self.assertEqual('$67B2BDA4264D8A189D9270E28B1D30A262838243', event.directory)
self.assertEqual('67B2BDA4264D8A189D9270E28B1D30A262838243', event.directory_fingerprint)
self.assertEqual(None, event.directory_nickname)
self.assertEqual('b3oeducbhjmbqmgw2i3jtz4fekkrinwj', event.descriptor_id)
self.assertEqual(HSDescReason.NOT_FOUND, event.reason)
def test_hs_desc_content_event(self):
event = _get_event(HS_DESC_CONTENT_EVENT)
self.assertTrue(isinstance(event, stem.response.events.HSDescContentEvent))
self.assertEqual('facebookcorewwwi', event.address)
self.assertEqual('riwvyw6njgvs4koel4heqs7w4bssnmlw', event.descriptor_id)
self.assertEqual('$8A30C9E8F5954EE286D29BD65CADEA6991200804~YorkshireTOR', event.directory)
self.assertEqual('8A30C9E8F5954EE286D29BD65CADEA6991200804', event.directory_fingerprint)
self.assertEqual('YorkshireTOR', event.directory_nickname)
desc = event.descriptor
self.assertEqual('riwvyw6njgvs4koel4heqs7w4bssnmlw', desc.descriptor_id)
self.assertEqual(2, desc.version)
self.assertTrue('MIGJAoGBALf' in desc.permanent_key)
self.assertEqual('vnb2j6ftvkvghypd4yyypsl3qmpjyq3j', desc.secret_id_part)
self.assertEqual(datetime.datetime(2015, 3, 13, 19, 0, 0), desc.published)
self.assertEqual([2, 3], desc.protocol_versions)
self.assertEqual(3, len(desc.introduction_points()))
self.assertTrue('s9Z0zWHsoPu' in desc.signature)
event = _get_event(HS_DESC_CONTENT_EMPTY_EVENT)
self.assertTrue(isinstance(event, stem.response.events.HSDescContentEvent))
self.assertEqual('3g2upl4pq6kufc4n', event.address)
self.assertEqual('255tjwttk3wi7r2df57nuprs72j2daa3', event.descriptor_id)
self.assertEqual('$D7A0C3262724F2BC9646F6836E967A2777A3AF83~tsunaminitor', event.directory)
self.assertEqual('D7A0C3262724F2BC9646F6836E967A2777A3AF83', event.directory_fingerprint)
self.assertEqual('tsunaminitor', event.directory_nickname)
self.assertEqual(None, event.descriptor)
def test_newdesc_event(self):
event = _get_event(NEWDESC_SINGLE)
expected_relays = (('B3FA3110CC6F42443F039220C134CBD2FC4F0493', 'Sakura'),)
self.assertTrue(isinstance(event, stem.response.events.NewDescEvent))
self.assertEqual(NEWDESC_SINGLE.lstrip('650 '), str(event))
self.assertEqual(expected_relays, event.relays)
event = _get_event(NEWDESC_MULTIPLE)
expected_relays = (('BE938957B2CA5F804B3AFC2C1EE6673170CDBBF8', 'Moonshine'),
('B4BE08B22D4D2923EDC3970FD1B93D0448C6D8FF', 'Unnamed'))
self.assertTrue(isinstance(event, stem.response.events.NewDescEvent))
self.assertEqual(NEWDESC_MULTIPLE.lstrip('650 '), str(event))
self.assertEqual(expected_relays, event.relays)
def test_network_liveness_event(self):
event = _get_event('650 NETWORK_LIVENESS UP')
self.assertTrue(isinstance(event, stem.response.events.NetworkLivenessEvent))
self.assertEqual('NETWORK_LIVENESS UP', str(event))
self.assertEqual('UP', event.status)
event = _get_event('650 NETWORK_LIVENESS DOWN')
self.assertEqual('DOWN', event.status)
event = _get_event('650 NETWORK_LIVENESS OTHER_STATUS key=value')
self.assertEqual('OTHER_STATUS', event.status)
def test_new_consensus_event(self):
expected_desc = []
expected_desc.append(RouterStatusEntryV3.create({
'r': 'Beaver /96bKo4soysolMgKn5Hex2nyFSY pAJH9dSBp/CG6sPhhVY/5bLaVPM 2012-12-02 22:02:45 77.223.43.54 9001 0',
's': 'Fast Named Running Stable Valid',
}))
expected_desc.append(RouterStatusEntryV3.create({
'r': 'Unnamed /+fJRWjmIGNAL2C5rRZHq3R91tA 7AnpZjfdBpYzXnMNm+w1bTsFF6Y 2012-12-02 17:51:10 91.121.184.87 9001 0',
's': 'Fast Guard Running Stable Valid',
}))
event = _get_event(NEWCONSENSUS_EVENT)
self.assertTrue(isinstance(event, stem.response.events.NewConsensusEvent))
self.assertEqual(expected_desc, event.desc)
def test_ns_event(self):
expected_desc = RouterStatusEntryV3.create({
'r': 'whnetz dbBxYcJriTTrcxsuy4PUZcMRwCA VStM7KAIH/mXXoGDUpoGB1OXufg 2012-12-02 21:03:56 141.70.120.13 9001 9030',
's': 'Fast HSDir Named Stable V2Dir Valid',
})
event = _get_event(NS_EVENT)
self.assertTrue(isinstance(event, stem.response.events.NetworkStatusEvent))
self.assertEqual([expected_desc], event.desc)
def test_orconn_event(self):
event = _get_event(ORCONN_CLOSED)
self.assertTrue(isinstance(event, stem.response.events.ORConnEvent))
self.assertEqual(ORCONN_CLOSED.lstrip('650 '), str(event))
self.assertEqual('$A1130635A0CDA6F60C276FBF6994EFBD4ECADAB1~tama', event.endpoint)
self.assertEqual('A1130635A0CDA6F60C276FBF6994EFBD4ECADAB1', event.endpoint_fingerprint)
self.assertEqual('tama', event.endpoint_nickname)
self.assertEqual(None, event.endpoint_address)
self.assertEqual(None, event.endpoint_port)
self.assertEqual(ORStatus.CLOSED, event.status)
self.assertEqual(ORClosureReason.DONE, event.reason)
self.assertEqual(None, event.circ_count)
self.assertEqual(None, event.id)
event = _get_event(ORCONN_CONNECTED)
self.assertTrue(isinstance(event, stem.response.events.ORConnEvent))
self.assertEqual(ORCONN_CONNECTED.lstrip('650 '), str(event))
self.assertEqual('127.0.0.1:9000', event.endpoint)
self.assertEqual(None, event.endpoint_fingerprint)
self.assertEqual(None, event.endpoint_nickname)
self.assertEqual('127.0.0.1', event.endpoint_address)
self.assertEqual(9000, event.endpoint_port)
self.assertEqual(ORStatus.CONNECTED, event.status)
self.assertEqual(None, event.reason)
self.assertEqual(20, event.circ_count)
self.assertEqual('18', event.id)
event = _get_event(ORCONN_LAUNCHED)
self.assertTrue(isinstance(event, stem.response.events.ORConnEvent))
self.assertEqual(ORCONN_LAUNCHED.lstrip('650 '), str(event))
self.assertEqual('$7ED90E2833EE38A75795BA9237B0A4560E51E1A0=GreenDragon', event.endpoint)
self.assertEqual('7ED90E2833EE38A75795BA9237B0A4560E51E1A0', event.endpoint_fingerprint)
self.assertEqual('GreenDragon', event.endpoint_nickname)
self.assertEqual(None, event.endpoint_address)
self.assertEqual(None, event.endpoint_port)
self.assertEqual(ORStatus.LAUNCHED, event.status)
self.assertEqual(None, event.reason)
self.assertEqual(None, event.circ_count)
# malformed fingerprint
self.assertRaises(ProtocolError, _get_event, ORCONN_BAD_1)
# invalid port number ('001')
self.assertRaises(ProtocolError, _get_event, ORCONN_BAD_2)
# non-numeric NCIRCS
self.assertRaises(ProtocolError, _get_event, ORCONN_BAD_3)
# invalid connection id
self.assertRaises(ProtocolError, _get_event, ORCONN_BAD_4)
def test_signal_event(self):
event = _get_event('650 SIGNAL DEBUG')
self.assertTrue(isinstance(event, stem.response.events.SignalEvent))
self.assertEqual('SIGNAL DEBUG', str(event))
self.assertEqual(Signal.DEBUG, event.signal)
event = _get_event('650 SIGNAL DUMP')
self.assertEqual(Signal.DUMP, event.signal)
def test_status_event_consensus_arrived(self):
event = _get_event(STATUS_GENERAL_CONSENSUS_ARRIVED)
self.assertTrue(isinstance(event, stem.response.events.StatusEvent))
self.assertEqual(STATUS_GENERAL_CONSENSUS_ARRIVED.lstrip('650 '), str(event))
self.assertEqual(StatusType.GENERAL, event.status_type)
self.assertEqual(Runlevel.NOTICE, event.runlevel)
self.assertEqual('CONSENSUS_ARRIVED', event.action)
def test_status_event_enough_dir_info(self):
event = _get_event(STATUS_CLIENT_ENOUGH_DIR_INFO)
self.assertEqual(StatusType.CLIENT, event.status_type)
self.assertEqual(Runlevel.NOTICE, event.runlevel)
self.assertEqual('ENOUGH_DIR_INFO', event.action)
def test_status_event_circuit_established(self):
event = _get_event(STATUS_CLIENT_CIRC_ESTABLISHED)
self.assertEqual(StatusType.CLIENT, event.status_type)
self.assertEqual(Runlevel.NOTICE, event.runlevel)
self.assertEqual('CIRCUIT_ESTABLISHED', event.action)
def test_status_event_bootstrap_descriptors(self):
event = _get_event(STATUS_CLIENT_BOOTSTRAP_DESCRIPTORS)
self.assertEqual(StatusType.CLIENT, event.status_type)
self.assertEqual(Runlevel.NOTICE, event.runlevel)
self.assertEqual('BOOTSTRAP', event.action)
expected_attr = {
'PROGRESS': '53',
'TAG': 'loading_descriptors',
'SUMMARY': 'Loading relay descriptors',
}
self.assertEqual(expected_attr, event.arguments)
def test_status_event_bootstrap_stuck(self):
event = _get_event(STATUS_CLIENT_BOOTSTRAP_STUCK)
self.assertEqual(StatusType.CLIENT, event.status_type)
self.assertEqual(Runlevel.WARN, event.runlevel)
self.assertEqual('BOOTSTRAP', event.action)
expected_attr = {
'PROGRESS': '80',
'TAG': 'conn_or',
'SUMMARY': 'Connecting to the Tor network',
'WARNING': 'Network is unreachable',
'REASON': 'NOROUTE',
'COUNT': '5',
'RECOMMENDATION': 'warn',
}
self.assertEqual(expected_attr, event.keyword_args)
def test_status_event_bootstrap_connecting(self):
event = _get_event(STATUS_CLIENT_BOOTSTRAP_CONNECTING)
self.assertEqual(StatusType.CLIENT, event.status_type)
self.assertEqual(Runlevel.NOTICE, event.runlevel)
self.assertEqual('BOOTSTRAP', event.action)
expected_attr = {
'PROGRESS': '80',
'TAG': 'conn_or',
'SUMMARY': 'Connecting to the Tor network',
}
self.assertEqual(expected_attr, event.keyword_args)
def test_status_event_bootstrap_first_handshake(self):
event = _get_event(STATUS_CLIENT_BOOTSTRAP_FIRST_HANDSHAKE)
self.assertEqual(StatusType.CLIENT, event.status_type)
self.assertEqual(Runlevel.NOTICE, event.runlevel)
self.assertEqual('BOOTSTRAP', event.action)
expected_attr = {
'PROGRESS': '85',
'TAG': 'handshake_or',
'SUMMARY': 'Finishing handshake with first hop',
}
self.assertEqual(expected_attr, event.keyword_args)
def test_status_event_bootstrap_established(self):
event = _get_event(STATUS_CLIENT_BOOTSTRAP_ESTABLISHED)
self.assertEqual(StatusType.CLIENT, event.status_type)
self.assertEqual(Runlevel.NOTICE, event.runlevel)
self.assertEqual('BOOTSTRAP', event.action)
expected_attr = {
'PROGRESS': '90',
'TAG': 'circuit_create',
'SUMMARY': 'Establishing a Tor circuit',
}
self.assertEqual(expected_attr, event.keyword_args)
def test_status_event_bootstrap_done(self):
event = _get_event(STATUS_CLIENT_BOOTSTRAP_DONE)
self.assertEqual(StatusType.CLIENT, event.status_type)
self.assertEqual(Runlevel.NOTICE, event.runlevel)
self.assertEqual('BOOTSTRAP', event.action)
expected_attr = {
'PROGRESS': '100',
'TAG': 'done',
'SUMMARY': 'Done',
}
self.assertEqual(expected_attr, event.keyword_args)
def test_status_event_bootstrap_check_reachability(self):
event = _get_event(STATUS_SERVER_CHECK_REACHABILITY)
self.assertEqual(StatusType.SERVER, event.status_type)
self.assertEqual(Runlevel.NOTICE, event.runlevel)
self.assertEqual('CHECKING_REACHABILITY', event.action)
expected_attr = {
'ORADDRESS': '71.35.143.230:9050',
}
self.assertEqual(expected_attr, event.keyword_args)
def test_status_event_dns_timeout(self):
event = _get_event(STATUS_SERVER_DNS_TIMEOUT)
self.assertEqual(StatusType.SERVER, event.status_type)
self.assertEqual(Runlevel.NOTICE, event.runlevel)
self.assertEqual('NAMESERVER_STATUS', event.action)
expected_attr = {
'NS': '205.171.3.25',
'STATUS': 'DOWN',
'ERR': 'request timed out.',
}
self.assertEqual(expected_attr, event.keyword_args)
def test_status_event_dns_down(self):
event = _get_event(STATUS_SERVER_DNS_DOWN)
self.assertEqual(StatusType.SERVER, event.status_type)
self.assertEqual(Runlevel.WARN, event.runlevel)
self.assertEqual('NAMESERVER_ALL_DOWN', event.action)
def test_status_event_dns_up(self):
event = _get_event(STATUS_SERVER_DNS_UP)
self.assertEqual(StatusType.SERVER, event.status_type)
self.assertEqual(Runlevel.NOTICE, event.runlevel)
self.assertEqual('NAMESERVER_STATUS', event.action)
expected_attr = {
'NS': '205.171.3.25',
'STATUS': 'UP',
}
self.assertEqual(expected_attr, event.keyword_args)
def test_status_event_bug(self):
# briefly insert a fake value in EVENT_TYPE_TO_CLASS
stem.response.events.EVENT_TYPE_TO_CLASS['STATUS_SPECIFIC'] = stem.response.events.StatusEvent
self.assertRaises(ValueError, _get_event, STATUS_SPECIFIC_CONSENSUS_ARRIVED)
del stem.response.events.EVENT_TYPE_TO_CLASS['STATUS_SPECIFIC']
def test_stream_event(self):
event = _get_event(STREAM_NEW)
self.assertTrue(isinstance(event, stem.response.events.StreamEvent))
self.assertEqual(STREAM_NEW.lstrip('650 '), str(event))
self.assertEqual('18', event.id)
self.assertEqual(StreamStatus.NEW, event.status)
self.assertEqual(None, event.circ_id)
self.assertEqual('encrypted.google.com:443', event.target)
self.assertEqual('encrypted.google.com', event.target_address)
self.assertEqual(443, event.target_port)
self.assertEqual(None, event.reason)
self.assertEqual(None, event.remote_reason)
self.assertEqual(None, event.source)
self.assertEqual('127.0.0.1:47849', event.source_addr)
self.assertEqual('127.0.0.1', event.source_address)
self.assertEqual(47849, event.source_port)
self.assertEqual(StreamPurpose.USER, event.purpose)
event = _get_event(STREAM_SENTCONNECT)
self.assertTrue(isinstance(event, stem.response.events.StreamEvent))
self.assertEqual(STREAM_SENTCONNECT.lstrip('650 '), str(event))
self.assertEqual('18', event.id)
self.assertEqual(StreamStatus.SENTCONNECT, event.status)
self.assertEqual('26', event.circ_id)
self.assertEqual('encrypted.google.com:443', event.target)
self.assertEqual('encrypted.google.com', event.target_address)
self.assertEqual(443, event.target_port)
self.assertEqual(None, event.reason)
self.assertEqual(None, event.remote_reason)
self.assertEqual(None, event.source)
self.assertEqual(None, event.source_addr)
self.assertEqual(None, event.source_address)
self.assertEqual(None, event.source_port)
self.assertEqual(None, event.purpose)
event = _get_event(STREAM_REMAP)
self.assertTrue(isinstance(event, stem.response.events.StreamEvent))
self.assertEqual(STREAM_REMAP.lstrip('650 '), str(event))
self.assertEqual('18', event.id)
self.assertEqual(StreamStatus.REMAP, event.status)
self.assertEqual('26', event.circ_id)
self.assertEqual('74.125.227.129:443', event.target)
self.assertEqual('74.125.227.129', event.target_address)
self.assertEqual(443, event.target_port)
self.assertEqual(None, event.reason)
self.assertEqual(None, event.remote_reason)
self.assertEqual(StreamSource.EXIT, event.source)
self.assertEqual(None, event.source_addr)
self.assertEqual(None, event.source_address)
self.assertEqual(None, event.source_port)
self.assertEqual(None, event.purpose)
event = _get_event(STREAM_SUCCEEDED)
self.assertTrue(isinstance(event, stem.response.events.StreamEvent))
self.assertEqual(STREAM_SUCCEEDED.lstrip('650 '), str(event))
self.assertEqual('18', event.id)
self.assertEqual(StreamStatus.SUCCEEDED, event.status)
self.assertEqual('26', event.circ_id)
self.assertEqual('74.125.227.129:443', event.target)
self.assertEqual('74.125.227.129', event.target_address)
self.assertEqual(443, event.target_port)
self.assertEqual(None, event.reason)
self.assertEqual(None, event.remote_reason)
self.assertEqual(None, event.source)
self.assertEqual(None, event.source_addr)
self.assertEqual(None, event.source_address)
self.assertEqual(None, event.source_port)
self.assertEqual(None, event.purpose)
event = _get_event(STREAM_CLOSED_RESET)
self.assertTrue(isinstance(event, stem.response.events.StreamEvent))
self.assertEqual(STREAM_CLOSED_RESET.lstrip('650 '), str(event))
self.assertEqual('21', event.id)
self.assertEqual(StreamStatus.CLOSED, event.status)
self.assertEqual('26', event.circ_id)
self.assertEqual('74.125.227.129:443', event.target)
self.assertEqual('74.125.227.129', event.target_address)
self.assertEqual(443, event.target_port)
self.assertEqual(StreamClosureReason.CONNRESET, event.reason)
self.assertEqual(None, event.remote_reason)
self.assertEqual(None, event.source)
self.assertEqual(None, event.source_addr)
self.assertEqual(None, event.source_address)
self.assertEqual(None, event.source_port)
self.assertEqual(None, event.purpose)
event = _get_event(STREAM_CLOSED_DONE)
self.assertTrue(isinstance(event, stem.response.events.StreamEvent))
self.assertEqual(STREAM_CLOSED_DONE.lstrip('650 '), str(event))
self.assertEqual('25', event.id)
self.assertEqual(StreamStatus.CLOSED, event.status)
self.assertEqual('26', event.circ_id)
self.assertEqual('199.7.52.72:80', event.target)
self.assertEqual('199.7.52.72', event.target_address)
self.assertEqual(80, event.target_port)
self.assertEqual(StreamClosureReason.DONE, event.reason)
self.assertEqual(None, event.remote_reason)
self.assertEqual(None, event.source)
self.assertEqual(None, event.source_addr)
self.assertEqual(None, event.source_address)
self.assertEqual(None, event.source_port)
self.assertEqual(None, event.purpose)
event = _get_event(STREAM_DIR_FETCH)
self.assertTrue(isinstance(event, stem.response.events.StreamEvent))
self.assertEqual(STREAM_DIR_FETCH.lstrip('650 '), str(event))
self.assertEqual('14', event.id)
self.assertEqual(StreamStatus.NEW, event.status)
self.assertEqual(None, event.circ_id)
self.assertEqual('176.28.51.238.$649F2D0ACF418F7CFC6539AB2257EB2D5297BAFA.exit:443', event.target)
self.assertEqual('176.28.51.238.$649F2D0ACF418F7CFC6539AB2257EB2D5297BAFA.exit', event.target_address)
self.assertEqual(443, event.target_port)
self.assertEqual(None, event.reason)
self.assertEqual(None, event.remote_reason)
self.assertEqual(None, event.source)
self.assertEqual('(Tor_internal):0', event.source_addr)
self.assertEqual('(Tor_internal)', event.source_address)
self.assertEqual(0, event.source_port)
self.assertEqual(StreamPurpose.DIR_FETCH, event.purpose)
event = _get_event(STREAM_DNS_REQUEST)
self.assertTrue(isinstance(event, stem.response.events.StreamEvent))
self.assertEqual(STREAM_DNS_REQUEST.lstrip('650 '), str(event))
self.assertEqual('1113', event.id)
self.assertEqual(StreamStatus.NEW, event.status)
self.assertEqual(None, event.circ_id)
self.assertEqual('www.google.com:0', event.target)
self.assertEqual('www.google.com', event.target_address)
self.assertEqual(0, event.target_port)
self.assertEqual(None, event.reason)
self.assertEqual(None, event.remote_reason)
self.assertEqual(None, event.source)
self.assertEqual('127.0.0.1:15297', event.source_addr)
self.assertEqual('127.0.0.1', event.source_address)
self.assertEqual(15297, event.source_port)
self.assertEqual(StreamPurpose.DNS_REQUEST, event.purpose)
# missing target
self.assertRaises(ProtocolError, _get_event, STREAM_SENTCONNECT_BAD_1)
# target is missing a port
self.assertRaises(ProtocolError, _get_event, STREAM_SENTCONNECT_BAD_2)
# target's port is malformed
self.assertRaises(ProtocolError, _get_event, STREAM_SENTCONNECT_BAD_3)
# SOURCE_ADDR is missing a port
self.assertRaises(ProtocolError, _get_event, STREAM_DNS_REQUEST_BAD_1)
# SOURCE_ADDR's port is malformed
self.assertRaises(ProtocolError, _get_event, STREAM_DNS_REQUEST_BAD_2)
# IPv6 address
event = _get_event(STREAM_NEWRESOLVE_IP6)
self.assertTrue(isinstance(event, stem.response.events.StreamEvent))
self.assertEqual(STREAM_NEWRESOLVE_IP6.lstrip('650 '), str(event))
self.assertEqual('23', event.id)
self.assertEqual(StreamStatus.NEWRESOLVE, event.status)
self.assertEqual(None, event.circ_id)
self.assertEqual('2001:db8::1:0', event.target)
self.assertEqual('2001:db8::1', event.target_address)
self.assertEqual(0, event.target_port)
self.assertEqual(None, event.reason)
self.assertEqual(None, event.remote_reason)
self.assertEqual(None, event.source)
self.assertEqual(None, event.source_addr)
self.assertEqual(None, event.source_address)
self.assertEqual(None, event.source_port)
self.assertEqual(StreamPurpose.DNS_REQUEST, event.purpose)
def test_stream_bw_event(self):
event = _get_event('650 STREAM_BW 2 15 25')
self.assertTrue(isinstance(event, stem.response.events.StreamBwEvent))
self.assertEqual('2', event.id)
self.assertEqual(15, event.written)
self.assertEqual(25, event.read)
self.assertEqual(None, event.time)
event = _get_event('650 STREAM_BW Stream02 0 0')
self.assertEqual('Stream02', event.id)
self.assertEqual(0, event.written)
self.assertEqual(0, event.read)
self.assertEqual(None, event.time)
event = _get_event('650 STREAM_BW Stream02 0 0 2012-12-06T13:51:11.433755')
self.assertEqual('Stream02', event.id)
self.assertEqual(0, event.written)
self.assertEqual(0, event.read)
self.assertEqual(datetime.datetime(2012, 12, 6, 13, 51, 11, 433755), event.time)
self.assertRaises(ProtocolError, _get_event, '650 STREAM_BW')
self.assertRaises(ProtocolError, _get_event, '650 STREAM_BW 2')
self.assertRaises(ProtocolError, _get_event, '650 STREAM_BW 2 15')
self.assertRaises(ProtocolError, _get_event, '650 STREAM_BW - 15 25')
self.assertRaises(ProtocolError, _get_event, '650 STREAM_BW 12345678901234567 15 25')
self.assertRaises(ProtocolError, _get_event, '650 STREAM_BW 2 -15 25')
self.assertRaises(ProtocolError, _get_event, '650 STREAM_BW 2 15 -25')
self.assertRaises(ProtocolError, _get_event, '650 STREAM_BW 2 x 25')
def test_transport_launched_event(self):
event = _get_event(TRANSPORT_LAUNCHED)
self.assertTrue(isinstance(event, stem.response.events.TransportLaunchedEvent))
self.assertEqual(TRANSPORT_LAUNCHED.lstrip('650 '), str(event))
self.assertEqual('server', event.type)
self.assertEqual('obfs1', event.name)
self.assertEqual('127.0.0.1', event.address)
self.assertEqual(1111, event.port)
self.assertRaises(ProtocolError, _get_event, TRANSPORT_LAUNCHED_BAD_TYPE)
self.assertRaises(ProtocolError, _get_event, TRANSPORT_LAUNCHED_BAD_ADDRESS)
self.assertRaises(ProtocolError, _get_event, TRANSPORT_LAUNCHED_BAD_PORT)
def test_conn_bw_event(self):
event = _get_event(CONN_BW)
self.assertTrue(isinstance(event, stem.response.events.ConnectionBandwidthEvent))
self.assertEqual(CONN_BW.lstrip('650 '), str(event))
self.assertEqual('11', event.id)
self.assertEqual(stem.ConnectionType.DIR, event.conn_type)
self.assertEqual(272, event.read)
self.assertEqual(817, event.written)
self.assertRaises(ProtocolError, _get_event, CONN_BW_BAD_WRITTEN_VALUE)
self.assertRaises(ProtocolError, _get_event, CONN_BW_BAD_MISSING_ID)
def test_circ_bw_event(self):
event = _get_event(CIRC_BW)
self.assertTrue(isinstance(event, stem.response.events.CircuitBandwidthEvent))
self.assertEqual(CIRC_BW.lstrip('650 '), str(event))
self.assertEqual('11', event.id)
self.assertEqual(272, event.read)
self.assertEqual(817, event.written)
self.assertEqual(None, event.time)
event = _get_event(CIRC_BW_WITH_TIMESTAMP)
self.assertEqual('11', event.id)
self.assertEqual(272, event.read)
self.assertEqual(817, event.written)
self.assertEqual(datetime.datetime(2012, 12, 6, 13, 51, 11, 433755), event.time)
self.assertRaises(ProtocolError, _get_event, CIRC_BW_BAD_WRITTEN_VALUE)
self.assertRaises(ProtocolError, _get_event, CIRC_BW_BAD_MISSING_ID)
self.assertRaises(ProtocolError, _get_event, CIRC_BW_MALFORMED_TIMESTAMP)
def test_cell_stats_event(self):
event = _get_event(CELL_STATS_1)
self.assertTrue(isinstance(event, stem.response.events.CellStatsEvent))
self.assertEqual(CELL_STATS_1.lstrip('650 '), str(event))
self.assertEqual('14', event.id)
self.assertEqual(None, event.inbound_queue)
self.assertEqual(None, event.inbound_connection)
self.assertEqual(None, event.inbound_added)
self.assertEqual(None, event.inbound_removed)
self.assertEqual(None, event.inbound_time)
self.assertEqual('19403', event.outbound_queue)
self.assertEqual('15', event.outbound_connection)
self.assertEqual({'create_fast': 1, 'relay_early': 2}, event.outbound_added)
self.assertEqual({'create_fast': 1, 'relay_early': 2}, event.outbound_removed)
self.assertEqual({'create_fast': 0, 'relay_early': 0}, event.outbound_time)
event = _get_event(CELL_STATS_2)
self.assertTrue(isinstance(event, stem.response.events.CellStatsEvent))
self.assertEqual(CELL_STATS_2.lstrip('650 '), str(event))
self.assertEqual(None, event.id)
self.assertEqual('19403', event.inbound_queue)
self.assertEqual('32', event.inbound_connection)
self.assertEqual({'relay': 1, 'created_fast': 1}, event.inbound_added)
self.assertEqual({'relay': 1, 'created_fast': 1}, event.inbound_removed)
self.assertEqual({'relay': 0, 'created_fast': 0}, event.inbound_time)
self.assertEqual('6710', event.outbound_queue)
self.assertEqual('18', event.outbound_connection)
self.assertEqual({'create': 1, 'relay_early': 1}, event.outbound_added)
self.assertEqual({'create': 1, 'relay_early': 1}, event.outbound_removed)
self.assertEqual({'create': 0, 'relay_early': 0}, event.outbound_time)
# check a few invalid mappings (bad key or value)
self.assertRaises(ProtocolError, _get_event, CELL_STATS_BAD_1)
self.assertRaises(ProtocolError, _get_event, CELL_STATS_BAD_2)
self.assertRaises(ProtocolError, _get_event, CELL_STATS_BAD_3)
def test_token_bucket_empty_event(self):
event = _get_event(TB_EMPTY_1)
self.assertTrue(isinstance(event, stem.response.events.TokenBucketEmptyEvent))
self.assertEqual(TB_EMPTY_1.lstrip('650 '), str(event))
self.assertEqual(stem.TokenBucket.ORCONN, event.bucket)
self.assertEqual('16', event.id)
self.assertEqual(0, event.read)
self.assertEqual(0, event.written)
self.assertEqual(100, event.last_refill)
event = _get_event(TB_EMPTY_2)
self.assertTrue(isinstance(event, stem.response.events.TokenBucketEmptyEvent))
self.assertEqual(TB_EMPTY_2.lstrip('650 '), str(event))
self.assertEqual(stem.TokenBucket.GLOBAL, event.bucket)
self.assertEqual(None, event.id)
self.assertEqual(93, event.read)
self.assertEqual(93, event.written)
self.assertEqual(100, event.last_refill)
event = _get_event(TB_EMPTY_3)
self.assertTrue(isinstance(event, stem.response.events.TokenBucketEmptyEvent))
self.assertEqual(TB_EMPTY_3.lstrip('650 '), str(event))
self.assertEqual(stem.TokenBucket.RELAY, event.bucket)
self.assertEqual(None, event.id)
self.assertEqual(93, event.read)
self.assertEqual(93, event.written)
self.assertEqual(100, event.last_refill)
self.assertRaises(ProtocolError, _get_event, TB_EMPTY_BAD_1)
self.assertRaises(ProtocolError, _get_event, TB_EMPTY_BAD_2)
def test_unrecognized_enum_logging(self):
"""
Checks that when event parsing gets a value that isn't recognized by stem's
enumeration of the attribute that we log a message.
"""
stem_logger = stem.util.log.get_logger()
logging_buffer = stem.util.log.LogBuffer(stem.util.log.INFO)
stem_logger.addHandler(logging_buffer)
# Try parsing a valid event. We shouldn't log anything.
_get_event(STATUS_GENERAL_CONSENSUS_ARRIVED)
self.assertTrue(logging_buffer.is_empty())
self.assertEqual([], list(logging_buffer))
# Parse an invalid runlevel.
_get_event(STATUS_GENERAL_CONSENSUS_ARRIVED.replace('NOTICE', 'OMEGA_CRITICAL!!!'))
logged_events = list(logging_buffer)
self.assertEqual(1, len(logged_events))
self.assertTrue('STATUS_GENERAL event had an unrecognized runlevel' in logged_events[0])
stem_logger.removeHandler(logging_buffer)
stem-1.6.0/test/unit/response/getinfo.py 0000664 0001750 0001750 00000007452 13124757510 021026 0 ustar atagar atagar 0000000 0000000 """
Unit tests for the stem.response.getinfo.GetInfoResponse class.
"""
import unittest
import stem.response
import stem.response.getinfo
import stem.socket
import stem.util.str_tools
from stem.response import ControlMessage
SINGLE_RESPONSE = """\
250-version=0.2.3.11-alpha-dev
250 OK"""
BATCH_RESPONSE = """\
250-version=0.2.3.11-alpha-dev
250-address=67.137.76.214
250-fingerprint=5FDE0422045DF0E1879A3738D09099EB4A0C5BA0
250 OK"""
MULTILINE_RESPONSE = """\
250-version=0.2.3.11-alpha-dev (git-ef0bc7f8f26a917c)
250+config-text=
ControlPort 9051
DataDirectory /home/atagar/.tor
ExitPolicy reject *:*
Log notice stdout
Nickname Unnamed
ORPort 9050
.
250 OK"""
NON_KEY_VALUE_ENTRY = """\
250-version=0.2.3.11-alpha-dev
250-address 67.137.76.214
250 OK"""
UNRECOGNIZED_KEY_ENTRY = """\
552 Unrecognized key "blackhole"
"""
MISSING_MULTILINE_NEWLINE = """\
250+config-text=ControlPort 9051
DataDirectory /home/atagar/.tor
.
250 OK"""
class TestGetInfoResponse(unittest.TestCase):
def test_empty_response(self):
"""
Parses a GETINFO reply without options (just calling "GETINFO").
"""
control_message = ControlMessage.from_str('250 OK\r\n', 'GETINFO')
# now this should be a GetInfoResponse (ControlMessage subclass)
self.assertTrue(isinstance(control_message, stem.response.ControlMessage))
self.assertTrue(isinstance(control_message, stem.response.getinfo.GetInfoResponse))
self.assertEqual({}, control_message.entries)
def test_single_response(self):
"""
Parses a GETINFO reply response for a single parameter.
"""
control_message = ControlMessage.from_str(SINGLE_RESPONSE, 'GETINFO', normalize = True)
self.assertEqual({'version': b'0.2.3.11-alpha-dev'}, control_message.entries)
def test_batch_response(self):
"""
Parses a GETINFO reply for muiltiple parameters.
"""
expected = {
'version': b'0.2.3.11-alpha-dev',
'address': b'67.137.76.214',
'fingerprint': b'5FDE0422045DF0E1879A3738D09099EB4A0C5BA0',
}
control_message = ControlMessage.from_str(BATCH_RESPONSE, 'GETINFO', normalize = True)
self.assertEqual(expected, control_message.entries)
def test_multiline_response(self):
"""
Parses a GETINFO reply for multiple parameters including a multi-line
value.
"""
expected = {
'version': b'0.2.3.11-alpha-dev (git-ef0bc7f8f26a917c)',
'config-text': b'\n'.join(stem.util.str_tools._to_bytes(MULTILINE_RESPONSE).splitlines()[2:8]),
}
control_message = ControlMessage.from_str(MULTILINE_RESPONSE, 'GETINFO', normalize = True)
self.assertEqual(expected, control_message.entries)
def test_invalid_non_mapping_content(self):
"""
Parses a malformed GETINFO reply containing a line that isn't a key=value
entry.
"""
control_message = ControlMessage.from_str(NON_KEY_VALUE_ENTRY, normalize = True)
self.assertRaises(stem.ProtocolError, stem.response.convert, 'GETINFO', control_message)
def test_unrecognized_key_response(self):
"""
Parses a GETCONF reply that contains an error code with an unrecognized key.
"""
try:
control_message = ControlMessage.from_str(UNRECOGNIZED_KEY_ENTRY, normalize = True)
stem.response.convert('GETINFO', control_message)
self.fail('expected a stem.InvalidArguments to be raised')
except stem.InvalidArguments as exc:
self.assertEqual(exc.arguments, ['blackhole'])
def test_invalid_multiline_content(self):
"""
Parses a malformed GETINFO reply with a multi-line entry missing a newline
between its key and value. This is a proper controller message, but
malformed according to the GETINFO's spec.
"""
control_message = ControlMessage.from_str(MISSING_MULTILINE_NEWLINE, normalize = True)
self.assertRaises(stem.ProtocolError, stem.response.convert, 'GETINFO', control_message)
stem-1.6.0/test/unit/response/mapaddress.py 0000664 0001750 0001750 00000004665 13124757510 021521 0 ustar atagar atagar 0000000 0000000 """
Unit tests for the stem.response.mapaddress.MapAddressResponse class.
"""
import unittest
import stem.response
import stem.response.mapaddress
import stem.socket
from stem.response import ControlMessage
BATCH_RESPONSE = """\
250-foo=bar
250-baz=quux
250-gzzz=bzz
250 120.23.23.2=torproject.org"""
INVALID_EMPTY_RESPONSE = '250 OK'
INVALID_RESPONSE = '250 foo is bar'
PARTIAL_FAILURE_RESPONSE = """512-syntax error: mapping '2389' is not of expected form 'foo=bar'
512-syntax error: mapping '23' is not of expected form 'foo=bar'.
250 23=324"""
UNRECOGNIZED_KEYS_RESPONSE = "512 syntax error: mapping '2389' is not of expected form 'foo=bar'"
FAILED_RESPONSE = '451 Resource exhausted'
class TestMapAddressResponse(unittest.TestCase):
def test_single_response(self):
"""
Parses a MAPADDRESS reply response with a single address mapping.
"""
control_message = ControlMessage.from_str('250 foo=bar\r\n', 'MAPADDRESS')
self.assertEqual({'foo': 'bar'}, control_message.entries)
def test_batch_response(self):
"""
Parses a MAPADDRESS reply with multiple address mappings
"""
expected = {
'foo': 'bar',
'baz': 'quux',
'gzzz': 'bzz',
'120.23.23.2': 'torproject.org'
}
control_message = ControlMessage.from_str(BATCH_RESPONSE, 'MAPADDRESS', normalize = True)
self.assertEqual(expected, control_message.entries)
def test_invalid_requests(self):
"""
Parses a MAPADDRESS replies that contain an error code due to hostname syntax errors.
"""
control_message = ControlMessage.from_str(UNRECOGNIZED_KEYS_RESPONSE, normalize = True)
self.assertRaises(stem.InvalidRequest, stem.response.convert, 'MAPADDRESS', control_message)
control_message = ControlMessage.from_str(PARTIAL_FAILURE_RESPONSE, 'MAPADDRESS', normalize = True)
self.assertEqual({'23': '324'}, control_message.entries)
def test_invalid_response(self):
"""
Parses a malformed MAPADDRESS reply that contains an invalid response code.
This is a proper controller message, but malformed according to the
MAPADDRESS's spec.
"""
control_message = ControlMessage.from_str(INVALID_EMPTY_RESPONSE, normalize = True)
self.assertRaises(stem.ProtocolError, stem.response.convert, 'MAPADDRESS', control_message)
control_message = ControlMessage.from_str(INVALID_RESPONSE, normalize = True)
self.assertRaises(stem.ProtocolError, stem.response.convert, 'MAPADDRESS', control_message)
stem-1.6.0/test/unit/response/control_line.py 0000664 0001750 0001750 00000015145 13115423200 022042 0 ustar atagar atagar 0000000 0000000 """
Unit tests for the stem.response.ControlLine class.
"""
import unittest
import stem.response
# response made by having 'DataDirectory /tmp/my data\"dir/' in the torrc
PROTOCOLINFO_RESPONSE = (
'PROTOCOLINFO 1',
'AUTH METHODS=COOKIE COOKIEFILE="/tmp/my data\\\\\\"dir//control_auth_cookie"',
'VERSION Tor="0.2.1.30"',
'OK',
)
class TestControlLine(unittest.TestCase):
def test_pop_examples(self):
"""
Checks that the pop method's pydoc examples are correct.
"""
line = stem.response.ControlLine("\"We're all mad here.\" says the grinning cat.")
self.assertEqual(line.pop(True), "We're all mad here.")
self.assertEqual(line.pop(), 'says')
self.assertEqual(line.remainder(), 'the grinning cat.')
line = stem.response.ControlLine('"this has a \\" and \\\\ in it" foo=bar more_data')
self.assertEqual(line.pop(True, True), 'this has a " and \\ in it')
def test_string(self):
"""
Basic checks that we behave as a regular immutable string.
"""
line = stem.response.ControlLine(PROTOCOLINFO_RESPONSE[0])
self.assertEqual(line, 'PROTOCOLINFO 1')
self.assertTrue(line.startswith('PROTOCOLINFO '))
# checks that popping items doesn't effect us
line.pop()
self.assertEqual(line, 'PROTOCOLINFO 1')
self.assertTrue(line.startswith('PROTOCOLINFO '))
def test_general_usage(self):
"""
Checks a basic use case for the popping entries.
"""
# pops a series of basic, space separated entries
line = stem.response.ControlLine(PROTOCOLINFO_RESPONSE[0])
self.assertEqual(line.remainder(), 'PROTOCOLINFO 1')
self.assertFalse(line.is_empty())
self.assertFalse(line.is_next_quoted())
self.assertFalse(line.is_next_mapping())
self.assertEqual(None, line.peek_key())
self.assertRaises(ValueError, line.pop_mapping)
self.assertEqual(line.pop(), 'PROTOCOLINFO')
self.assertEqual(line.remainder(), '1')
self.assertFalse(line.is_empty())
self.assertFalse(line.is_next_quoted())
self.assertFalse(line.is_next_mapping())
self.assertEqual(None, line.peek_key())
self.assertRaises(ValueError, line.pop_mapping)
self.assertEqual(line.pop(), '1')
self.assertEqual(line.remainder(), '')
self.assertTrue(line.is_empty())
self.assertFalse(line.is_next_quoted())
self.assertFalse(line.is_next_mapping())
self.assertEqual(None, line.peek_key())
self.assertRaises(IndexError, line.pop_mapping)
self.assertRaises(IndexError, line.pop)
self.assertEqual(line.remainder(), '')
self.assertTrue(line.is_empty())
self.assertFalse(line.is_next_quoted())
self.assertFalse(line.is_next_mapping())
self.assertEqual(None, line.peek_key())
def test_pop_mapping(self):
"""
Checks use cases when parsing KEY=VALUE mappings.
"""
# version entry with a space
version_entry = 'Tor="0.2.1.30 (0a083b0188cacd2f07838ff0446113bd5211a024)"'
line = stem.response.ControlLine(version_entry)
self.assertEqual(line.remainder(), version_entry)
self.assertFalse(line.is_empty())
self.assertFalse(line.is_next_quoted())
self.assertTrue(line.is_next_mapping())
self.assertTrue(line.is_next_mapping(key = 'Tor'))
self.assertTrue(line.is_next_mapping(key = 'Tor', quoted = True))
self.assertTrue(line.is_next_mapping(quoted = True))
self.assertEqual('Tor', line.peek_key())
# try popping this as a non-quoted mapping
self.assertEqual(line.pop_mapping(), ('Tor', '"0.2.1.30'))
self.assertEqual(line.remainder(), '(0a083b0188cacd2f07838ff0446113bd5211a024)"')
self.assertFalse(line.is_empty())
self.assertFalse(line.is_next_quoted())
self.assertFalse(line.is_next_mapping())
self.assertRaises(ValueError, line.pop_mapping)
self.assertEqual(None, line.peek_key())
# try popping this as a quoted mapping
line = stem.response.ControlLine(version_entry)
self.assertEqual(line.pop_mapping(True), ('Tor', '0.2.1.30 (0a083b0188cacd2f07838ff0446113bd5211a024)'))
self.assertEqual(line.remainder(), '')
self.assertTrue(line.is_empty())
self.assertFalse(line.is_next_quoted())
self.assertFalse(line.is_next_mapping())
self.assertEqual(None, line.peek_key())
def test_escapes(self):
"""
Checks that we can parse quoted values with escaped quotes in it. This
explicitely comes up with the COOKIEFILE attribute of PROTOCOLINFO
responses.
"""
auth_line = PROTOCOLINFO_RESPONSE[1]
line = stem.response.ControlLine(auth_line)
self.assertEqual(line, auth_line)
self.assertEqual(line.remainder(), auth_line)
self.assertEqual(line.pop(), 'AUTH')
self.assertEqual(line.pop_mapping(), ('METHODS', 'COOKIE'))
self.assertEqual(line.remainder(), r'COOKIEFILE="/tmp/my data\\\"dir//control_auth_cookie"')
self.assertTrue(line.is_next_mapping())
self.assertTrue(line.is_next_mapping(key = 'COOKIEFILE'))
self.assertTrue(line.is_next_mapping(quoted = True))
self.assertTrue(line.is_next_mapping(quoted = True, escaped = True))
cookie_file_entry = line.remainder()
# try a general pop
self.assertEqual(line.pop(), 'COOKIEFILE="/tmp/my')
self.assertEqual(line.pop(), r'data\\\"dir//control_auth_cookie"')
self.assertTrue(line.is_empty())
# try a general pop with escapes
line = stem.response.ControlLine(cookie_file_entry)
self.assertEqual(line.pop(escaped = True), 'COOKIEFILE="/tmp/my')
self.assertEqual(line.pop(escaped = True), r'data\"dir//control_auth_cookie"')
self.assertTrue(line.is_empty())
# try a mapping pop
line = stem.response.ControlLine(cookie_file_entry)
self.assertEqual(line.pop_mapping(), ('COOKIEFILE', '"/tmp/my'))
self.assertEqual(line.remainder(), r'data\\\"dir//control_auth_cookie"')
self.assertFalse(line.is_empty())
# try a quoted mapping pop (this should trip up on the escaped quote)
line = stem.response.ControlLine(cookie_file_entry)
self.assertEqual(line.pop_mapping(True), ('COOKIEFILE', '/tmp/my data\\\\\\'))
self.assertEqual(line.remainder(), 'dir//control_auth_cookie"')
self.assertFalse(line.is_empty())
# try an escaped quoted mapping pop
line = stem.response.ControlLine(cookie_file_entry)
self.assertEqual(line.pop_mapping(True, True), ('COOKIEFILE', r'/tmp/my data\"dir//control_auth_cookie'))
self.assertTrue(line.is_empty())
# try an escaped slash followed by a character that could be part of an
# escape sequence
line = stem.response.ControlLine(r'COOKIEFILE="C:\\Users\\Atagar\\AppData\\tor\\control_auth_cookie"')
self.assertEqual(line.pop_mapping(True, True), ('COOKIEFILE', r'C:\Users\Atagar\AppData\tor\control_auth_cookie'))
self.assertTrue(line.is_empty())
stem-1.6.0/test/unit/response/control_message.py 0000664 0001750 0001750 00000016207 13165210535 022551 0 ustar atagar atagar 0000000 0000000 """
Unit tests for the stem.response.ControlMessage parsing and class.
"""
import io
import socket
import unittest
import stem.socket
import stem.response
import stem.response.getinfo
import stem.util.str_tools
OK_REPLY = '250 OK\r\n'
EVENT_BW = '650 BW 32326 2856\r\n'
EVENT_CIRC_TIMEOUT = '650 CIRC 5 FAILED PURPOSE=GENERAL REASON=TIMEOUT\r\n'
EVENT_CIRC_LAUNCHED = '650 CIRC 9 LAUNCHED PURPOSE=GENERAL\r\n'
EVENT_CIRC_EXTENDED = '650 CIRC 5 EXTENDED $A200F527C82C59A25CCA44884B49D3D65B122652=faktor PURPOSE=MEASURE_TIMEOUT\r\n'
GETINFO_VERSION = """250-version=0.2.2.23-alpha (git-b85eb949b528f4d7)
250 OK
""".replace('\n', '\r\n')
GETINFO_INFONAMES = """250+info/names=
accounting/bytes -- Number of bytes read/written so far in the accounting interval.
accounting/bytes-left -- Number of bytes left to write/read so far in the accounting interval.
accounting/enabled -- Is accounting currently enabled?
accounting/hibernating -- Are we hibernating or awake?
stream-status -- List of current streams.
version -- The current version of Tor.
.
250 OK
""".replace('\n', '\r\n')
class TestControlMessage(unittest.TestCase):
def test_from_str(self):
msg = stem.response.ControlMessage.from_str(GETINFO_VERSION)
self.assertTrue(isinstance(msg, stem.response.ControlMessage))
self.assertEqual('version=0.2.2.23-alpha (git-b85eb949b528f4d7)\nOK', str(msg))
msg = stem.response.ControlMessage.from_str(GETINFO_VERSION, 'GETINFO')
self.assertTrue(isinstance(msg, stem.response.getinfo.GetInfoResponse))
self.assertEqual({'version': b'0.2.2.23-alpha (git-b85eb949b528f4d7)'}, msg.entries)
def test_ok_response(self):
"""
Checks the basic 'OK' response that we get for most commands.
"""
message = self._assert_message_parses(OK_REPLY)
self.assertEqual('OK', str(message))
contents = message.content()
self.assertEqual(1, len(contents))
self.assertEqual(('250', ' ', 'OK'), contents[0])
def test_event_response(self):
"""
Checks parsing of actual events.
"""
# BW event
message = self._assert_message_parses(EVENT_BW)
self.assertEqual('BW 32326 2856', str(message))
contents = message.content()
self.assertEqual(1, len(contents))
self.assertEqual(('650', ' ', 'BW 32326 2856'), contents[0])
# few types of CIRC events
for circ_content in (EVENT_CIRC_TIMEOUT, EVENT_CIRC_LAUNCHED, EVENT_CIRC_EXTENDED):
message = self._assert_message_parses(circ_content)
self.assertEqual(circ_content[4:-2], str(message))
contents = message.content()
self.assertEqual(1, len(contents))
self.assertEqual(('650', ' ', str(message)), contents[0])
def test_getinfo_response(self):
"""
Checks parsing of actual GETINFO responses.
"""
# GETINFO version (basic single-line results)
message = self._assert_message_parses(GETINFO_VERSION)
self.assertEqual(2, len(list(message)))
self.assertEqual(2, len(str(message).splitlines()))
# manually checks the contents
contents = message.content()
self.assertEqual(2, len(contents))
self.assertEqual(('250', '-', 'version=0.2.2.23-alpha (git-b85eb949b528f4d7)'), contents[0])
self.assertEqual(('250', ' ', 'OK'), contents[1])
# GETINFO info/names (data entry)
message = self._assert_message_parses(GETINFO_INFONAMES)
self.assertEqual(2, len(list(message)))
self.assertEqual(8, len(str(message).splitlines()))
# manually checks the contents
contents = message.content()
self.assertEqual(2, len(contents))
first_entry = (contents[0][0], contents[0][1], contents[0][2][:contents[0][2].find('\n')])
self.assertEqual(('250', '+', 'info/names='), first_entry)
self.assertEqual(('250', ' ', 'OK'), contents[1])
def test_no_crlf(self):
"""
Checks that we get a ProtocolError when we don't have both a carriage
return and newline for line endings. This doesn't really check for
newlines (since that's what readline would break on), but not the end of
the world.
"""
# Replaces each of the CRLF entries with just LF, confirming that this
# causes a parsing error. This should test line endings for both data
# entry parsing and non-data.
infonames_lines = [line + '\n' for line in GETINFO_INFONAMES.splitlines()]
for index, line in enumerate(infonames_lines):
# replace the CRLF for the line
infonames_lines[index] = line.rstrip('\r\n') + '\n'
test_socket_file = io.BytesIO(stem.util.str_tools._to_bytes(''.join(infonames_lines)))
self.assertRaises(stem.ProtocolError, stem.socket.recv_message, test_socket_file)
# puts the CRLF back
infonames_lines[index] = infonames_lines[index].rstrip('\n') + '\r\n'
# sanity check the above test isn't broken due to leaving infonames_lines
# with invalid data
self._assert_message_parses(''.join(infonames_lines))
def test_malformed_prefix(self):
"""
Checks parsing for responses where the header is missing a digit or divider.
"""
for index in range(len(EVENT_BW)):
# makes test input with that character missing or replaced
removal_test_input = EVENT_BW[:index] + EVENT_BW[index + 1:]
replacement_test_input = EVENT_BW[:index] + '#' + EVENT_BW[index + 1:]
if index < 4 or index >= (len(EVENT_BW) - 2):
# dropping the character should cause an error if...
# - this is part of the message prefix
# - this is disrupting the line ending
self.assertRaises(stem.ProtocolError, stem.socket.recv_message, io.BytesIO(stem.util.str_tools._to_bytes(removal_test_input)))
self.assertRaises(stem.ProtocolError, stem.socket.recv_message, io.BytesIO(stem.util.str_tools._to_bytes(replacement_test_input)))
else:
# otherwise the data will be malformed, but this goes undetected
self._assert_message_parses(removal_test_input)
self._assert_message_parses(replacement_test_input)
def test_disconnected_socket(self):
"""
Tests when the read function is given a file derived from a disconnected
socket.
"""
control_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
control_socket_file = control_socket.makefile()
self.assertRaises(stem.SocketClosed, stem.socket.recv_message, control_socket_file)
def _assert_message_parses(self, controller_reply):
"""
Performs some basic sanity checks that a reply mirrors its parsed result.
Returns:
stem.response.ControlMessage for the given input
"""
message = stem.socket.recv_message(io.BytesIO(stem.util.str_tools._to_bytes(controller_reply)))
# checks that the raw_content equals the input value
self.assertEqual(controller_reply, message.raw_content())
# checks that the contents match the input
message_lines = str(message).splitlines()
controller_lines = controller_reply.split('\r\n')
controller_lines.pop() # the ControlMessage won't have a trailing newline
while controller_lines:
line = controller_lines.pop(0)
# mismatching lines with just a period are probably data termination
if line == '.' and (not message_lines or line != message_lines[0]):
continue
self.assertTrue(line.endswith(message_lines.pop(0)))
return message
stem-1.6.0/test/unit/descriptor/ 0000775 0001750 0001750 00000000000 13177674754 017351 5 ustar atagar atagar 0000000 0000000 stem-1.6.0/test/unit/descriptor/networkstatus/ 0000775 0001750 0001750 00000000000 13177674754 022306 5 ustar atagar atagar 0000000 0000000 stem-1.6.0/test/unit/descriptor/networkstatus/__init__.py 0000664 0001750 0001750 00000000235 13115423200 024361 0 ustar atagar atagar 0000000 0000000 """
Unit tests for stem.descriptor.networkstatus.
"""
__all__ = ['bridge_document', 'directory_authority', 'key_certificate', 'document_v2', 'document_v3']
stem-1.6.0/test/unit/descriptor/networkstatus/key_certificate.py 0000664 0001750 0001750 00000030161 13125773507 026000 0 ustar atagar atagar 0000000 0000000 """
Unit tests for the KeyCertificate of stem.descriptor.networkstatus.
"""
import datetime
import unittest
import stem.descriptor
import test.require
from stem.descriptor.networkstatus import KeyCertificate
from test.unit.descriptor import get_resource
class TestKeyCertificate(unittest.TestCase):
def test_minimal(self):
"""
Parses a minimal key certificate.
"""
certificate = KeyCertificate.create()
self.assertEqual(3, certificate.version)
self.assertEqual(None, certificate.address)
self.assertEqual(None, certificate.dir_port)
self.assertEqual(40, len(certificate.fingerprint))
self.assertEqual(None, certificate.crosscert)
self.assertEqual([], certificate.get_unrecognized_lines())
def test_real_certificates(self):
"""
Checks that key certificates from chutney can be properly parsed.
"""
expected_identity_key = """\
-----BEGIN RSA PUBLIC KEY-----
MIIBigKCAYEAxfTHG1b3Sxe8n3JQ/nIk4+1/chj7+jAyLLK+WrEBiP1vnDxTXMuo
x26ntWEjOaxjtKB12k5wMQW94/KvE754Gn98uQRFBHqLkrS4hUnn4/MqiBQVd2y3
UtE6KDSRhJZ5LfFH+dCKwu5+695PyJp/pfCUSOyPj0HQbFOnAOqdHPok8dtdfsy0
LaI7ycpzqAalzgrlwFP5KwwLtL+VapUGN4QOZlIXgL4W5e7OAG42lZhHt0b7/zdt
oIegZM1y8tK2l75ijqsvbetddQcFlnVaYzNwlQAUIZuxJOGfnPfTo+WrjCgrK2ur
ed5NiQMrEbZn5uCUscs+xLlKl4uKW0XXo1EIL45yBrVbmlP6V3/9diTHk64W9+m8
2G4ToDyH8J7LvnYPsmD0cCaQEceebxYVlmmwgqdORH/ixbeGF7JalTwtWBQYo2r0
VZAqjRwxR9dri6m1MIpzmzWmrbXghZ1IzJEL1rpB0okA/bE8AUGRx61eKnbI415O
PmO06JMpvkxxAgMBAAE=
-----END RSA PUBLIC KEY-----"""
expected_signing_key = """\
-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEAvzugxJl1gc7BgXarBO5IWejNZC30U1xVjZ/myQTzxtiKkPU0agQh
sPqn4vVsaW6ZnWjJ2pSOq0/jg8WgFyGHGQ9cG8tv2TlpObeb/tI7iANxWx+MXJAh
/CnFDBQ1ifKntJrs2IcRKMivfobqaHAL3Pz93noLWOTQunWjZ8D6kovYvUXe+yUQ
tZEROrmXJx7ZIIJF6BNKYBTc+iEkYtkWlJVs0my7yP/bbS075QyBsr6CfT+O2yU4
mgIg43QuqcFRbjyUvGI/gap06QNlB6yj8pqeE5rWo++5EpEvMK76fK6ymYuTN2SN
Oil+Fo7qgG8UP/fv0GelSz6Tk7pBoeHJlQIDAQAB
-----END RSA PUBLIC KEY-----"""
expected_crosscert = """\
-----BEGIN ID SIGNATURE-----
Oz+rvXDzlxLgQSb3nS5/4hrHVWgGCy0OnuNmFsyw8bi2eBst5Yj79dQ+D25giZke
81FRGIFU4eS6dshB+pJ+z0hc9ozlRTYh/qevY6l6o0amvuhHyk/cQXrh8oYU9Ihe
XQ1yVItvxC24HENsoGIGbr5uxc85FOcNs+R9qTLYA/56TjvAU4WUje3nTZE1awml
lj/Y6DM7ruMF6UoYJZPTklukZ+XHZg4Z2eE55e/oIaD7bfU/lFWU/alMyTV/J5oT
sxaD2XBLBScYiKypUmgrZ50W4ZqsXaYk76ClrudZnDbce+FuugVxok+jKYGjMu75
2es2ucuik7iuO7QPdPIXfg==
-----END ID SIGNATURE-----"""
expected_key_cert = """\
-----BEGIN SIGNATURE-----
I86FTQ5ZyCZUzm19HVAQWByrrRgUmddoRBfNiCj0iTGN3kdIq9OfuNLhWAqz71xP
8Nn0Vun8Uj3/vBq/odIFpnngL3mKI6OEKcNDr0D5hEV9Yjrxe8msMoaUZT+LHzUW
1q3pzxfMx6EmlSilMhuzSsa4YEbXMZzMqASKANSJHo2fzUkzQOpPw2SlWSTIgyqw
wAOB6QOvFfP3c0NTwxXrYE/iT+r90wZBuzS+v7r9B94alNAkE1KZQKnq2QTTIznP
iF9LWMsZcMHCjoTxszK4jF4MRMN/S4Xl8yQo0/z6FoqBz4RIXzFtJoG/rbXdKfkE
nJK9iEhaZbS1IN0o+uIGtvOm2rQSu9gS8merurr5GDSK3szjesPVJuF00mCNgOx4
hAYPN9N8HAL4zGE/l1UM7BGg3L84A0RMpDxnpXePd9mlHLhl4UV2lrkkf8S9Z6fX
PPc3r7zKlL/jEGHwz+C7kE88HIvkVnKLLn//40b6HxitHSOCkZ1vtp8YyXae6xnU
-----END SIGNATURE-----"""
with open(get_resource('cached-certs'), 'rb') as cert_file:
cert = next(stem.descriptor.parse_file(cert_file, 'dir-key-certificate-3 1.0'))
self.assertEqual(3, cert.version)
self.assertEqual('127.0.0.1', cert.address)
self.assertEqual(7000, cert.dir_port)
self.assertEqual('BCB380A633592C218757BEE11E630511A485658A', cert.fingerprint)
self.assertEqual(expected_identity_key, cert.identity_key)
self.assertEqual(datetime.datetime(2017, 5, 25, 4, 45, 52), cert.published)
self.assertEqual(datetime.datetime(2018, 5, 25, 4, 45, 52), cert.expires)
self.assertEqual(expected_signing_key, cert.signing_key)
self.assertEqual(expected_crosscert, cert.crosscert)
self.assertEqual(expected_key_cert, cert.certification)
self.assertEqual([], cert.get_unrecognized_lines())
def test_metrics_certificate(self):
"""
Checks if consensus documents from Metrics are parsed properly.
"""
expected_identity_key = """-----BEGIN RSA PUBLIC KEY-----
MIIBigKCAYEA7cZXvDRxfjDYtr9/9UsQ852+6cmHMr8VVh8GkLwbq3RzqjkULwQ2
R9mFvG4FnqMcMKXi62rYYA3fZL1afhT804cpvyp/D3dPM8QxW88fafFAgIFP4LiD
0JYjnF8cva5qZ0nzlWnMXLb32IXSvsGSE2FRyAV0YN9a6k967LSgCfUnZ+IKMezW
1vhL9YK4QIfsDowgtVsavg63GzGmA7JvZmn77+/J5wKz11vGr7Wttf8XABbH2taX
O9j/KGBOX2OKhoF3mXfZSmUO2dV9NMwtkJ7zD///Ny6sfApWV6kVP4O9TdG3bAsl
+fHCoCKgF/jAAWzh6VckQTOPzQZaH5aMWfXrDlzFWg17MjonI+bBTD2Ex2pHczzJ
bN7coDMRH2SuOXv8wFf27KdUxZ/GcrXSRGzlRLygxqlripUanjVGN2JvrVQVr0kz
pjNjiZl2z8ZyZ5d4zQuBi074JPGgx62xAstP37v1mPw14sIWfLgY16ewYuS5bCxV
lyS28jsPht9VAgMBAAE=
-----END RSA PUBLIC KEY-----"""
expected_signing_key = """-----BEGIN RSA PUBLIC KEY-----
MIGJAoGBAOeE3Qr1Km97gTgiB3io0EU0fqHW2ESMXVHeQuNDtCWBa0XSCEG6gx4B
ZkkHjfVWqGQ7TmmzjYP9L9uCgtoKfhSvJA2w9NUMtMl8sgZmF4lcGpXXvGY9a566
Bn+3wP0lMhb/I8CPVPX+NWEjgl1noZxo1C59SO/iALGQOpxRYgmbAgMBAAE=
-----END RSA PUBLIC KEY-----"""
expected_key_cert = """-----BEGIN SIGNATURE-----
asvWwaMq34OfHoWUhAwh4+JDOuEUZJVIHQnedOYfQH8asS2QvW3Ma93OhrwVOC6b
FyKmTJmJsl0MJGiC7tcEOlL6knsKE4CsuIw/PEcu2Rnm+R9zWxQuMYiHvGQMoDxl
giOhLLs4LlzAAJlbfbd3hjF4STVAtTwmxYuIjb1Mq/JfAsx/wH3TLXgVZwj32w9s
zUd9KZwwLzFiiHpC+U7zh6+wRsZfo2tlpmcaP1dTSINgVbdzPJ/DOUlx9nwTCBsE
AQpUx2DpAikwrpw0zDqpQvYulcQlNLWFN/y/PkmiK8mIJk0OBMiQA7JgqWamnnk4
PwqaGv483LkBF+25JFGJmnUVve3RMc+s61+2kBcjfUMed4QaHkeCMHqlRqpfQVkk
RY22NXCwrJvSMEwiy7acC8FGysqwHRyE356+Rw6TB43g3Tno9KaHEK7MHXjSHwNs
GM9hAsAMRX9Ogqhq5UjDNqEsvDKuyVeyh7unSZEOip9Zr6K/+7VsVPNb8vfBRBjo
-----END SIGNATURE-----"""
with open(get_resource('metrics_cert'), 'rb') as cert_file:
cert = next(stem.descriptor.parse_file(cert_file))
self.assertEqual(3, cert.version)
self.assertEqual(None, cert.address)
self.assertEqual(None, cert.dir_port)
self.assertEqual('14C131DFC5C6F93646BE72FA1401C02A8DF2E8B4', cert.fingerprint)
self.assertEqual(expected_identity_key, cert.identity_key)
self.assertEqual(datetime.datetime(2008, 5, 9, 21, 13, 26), cert.published)
self.assertEqual(datetime.datetime(2009, 5, 9, 21, 13, 26), cert.expires)
self.assertEqual(expected_signing_key, cert.signing_key)
self.assertEqual(None, cert.crosscert)
self.assertEqual(expected_key_cert, cert.certification)
self.assertEqual([], cert.get_unrecognized_lines())
@test.require.cryptography
def test_descriptor_signing(self):
self.assertRaisesRegexp(NotImplementedError, 'Signing of KeyCertificate not implemented', KeyCertificate.create, sign = True)
def test_unrecognized_line(self):
"""
Includes unrecognized content in the descriptor.
"""
certificate = KeyCertificate.create({'pepperjack': 'is oh so tasty!'})
self.assertEqual(['pepperjack is oh so tasty!'], certificate.get_unrecognized_lines())
def test_first_and_last_lines(self):
"""
Includes a non-mandatory field before the 'dir-key-certificate-version'
line or after the 'dir-key-certification' line.
"""
content = KeyCertificate.content()
for cert_text in (b'dir-address 127.0.0.1:80\n' + content,
content + b'\ndir-address 127.0.0.1:80'):
self.assertRaises(ValueError, KeyCertificate, cert_text, True)
certificate = KeyCertificate(cert_text, False)
self.assertEqual('127.0.0.1', certificate.address)
self.assertEqual(80, certificate.dir_port)
def test_missing_fields(self):
"""
Parse a key certificate where a mandatory field is missing.
"""
mandatory_fields = (
'dir-key-certificate-version',
'fingerprint',
'dir-key-published',
'dir-key-expires',
'dir-identity-key',
'dir-signing-key',
'dir-key-certification',
)
for excluded_field in mandatory_fields:
content = KeyCertificate.content(exclude = (excluded_field,))
self.assertRaises(ValueError, KeyCertificate, content, True)
certificate = KeyCertificate(content, False)
if excluded_field == 'fingerprint':
self.assertEqual(3, certificate.version)
else:
self.assertEqual(40, len(certificate.fingerprint))
def test_blank_lines(self):
"""
Includes blank lines, which should be ignored.
"""
certificate = KeyCertificate.create({'dir-key-published': '2011-11-28 21:51:04\n\n\n'})
self.assertEqual(datetime.datetime(2011, 11, 28, 21, 51, 4), certificate.published)
def test_version(self):
"""
Parses the dir-key-certificate-version field, including trying to handle a
different certificate version with the v3 parser.
"""
certificate = KeyCertificate.create({'dir-key-certificate-version': '3'})
self.assertEqual(3, certificate.version)
content = KeyCertificate.content({'dir-key-certificate-version': '4'})
self.assertRaises(ValueError, KeyCertificate, content, True)
self.assertEqual(4, KeyCertificate(content, False).version)
content = KeyCertificate.content({'dir-key-certificate-version': 'boo'})
self.assertRaises(ValueError, KeyCertificate, content, True)
self.assertEqual(None, KeyCertificate(content, False).version)
def test_dir_address(self):
"""
Parses the dir-address field.
"""
certificate = KeyCertificate.create({'dir-address': '127.0.0.1:80'})
self.assertEqual('127.0.0.1', certificate.address)
self.assertEqual(80, certificate.dir_port)
test_values = (
(''),
(' '),
('127.0.0.1'),
('127.0.0.1:'),
('80'),
(':80'),
('127.0.0.1a:80'),
('127.0.0.1:80a'),
)
for test_value in test_values:
content = KeyCertificate.content({'dir-address': test_value})
self.assertRaises(ValueError, KeyCertificate, content, True)
certificate = KeyCertificate(content, False)
self.assertEqual(None, certificate.address)
self.assertEqual(None, certificate.dir_port)
def test_fingerprint(self):
"""
Parses the fingerprint field.
"""
test_values = (
'',
' ',
'27B6B5996C426270A5C95488AA5BCEB6BCC8695',
'27B6B5996C426270A5C95488AA5BCEB6BCC869568',
)
for test_value in test_values:
content = KeyCertificate.content({'fingerprint': test_value})
self.assertRaises(ValueError, KeyCertificate, content, True)
certificate = KeyCertificate(content, False)
self.assertEqual(None, certificate.fingerprint)
def test_time_fields(self):
"""
Parses the dir-key-published and dir-key-expires fields, which both have
datetime content.
"""
test_values = (
'',
' ',
'2012-12-12',
'2012-12-12 01:01:',
'2012-12-12 01:a1:01',
)
for field, attr in (('dir-key-published', 'published'), ('dir-key-expires', 'expires')):
for test_value in test_values:
content = KeyCertificate.content({field: test_value})
self.assertRaises(ValueError, KeyCertificate, content, True)
certificate = KeyCertificate(content, False)
self.assertEqual(None, getattr(certificate, attr))
def test_key_blocks(self):
"""
Parses the dir-identity-key, dir-signing-key, dir-key-crosscert, and
dir-key-certification fields which all just have signature content.
"""
# the only non-mandatory field that we haven't exercised yet is dir-key-crosscert
certificate = KeyCertificate.create({'dir-key-crosscert': '\n-----BEGIN ID SIGNATURE-----%s-----END ID SIGNATURE-----' % stem.descriptor.CRYPTO_BLOB})
self.assertTrue(stem.descriptor.CRYPTO_BLOB in certificate.crosscert)
test_value = '\n-----BEGIN ID SIGNATURE-----%s-----END UGABUGA SIGNATURE-----' % stem.descriptor.CRYPTO_BLOB
for field, attr in (('dir-identity-key', 'identity_key'),
('dir-signing-key', 'signing_key'),
('dir-key-crosscert', 'crosscert'),
('dir-key-certification', 'certification')):
content = KeyCertificate.content({field: test_value})
self.assertRaises(ValueError, KeyCertificate, content, True)
certificate = KeyCertificate(content, False)
self.assertEqual(None, getattr(certificate, attr))
def test_wrong_block_type(self):
"""
Checks that we validate the type of crypto content we receive.
"""
content = KeyCertificate.content({'dir-identity-key': '\n-----BEGIN MD5SUM-----%s-----END MD5SUM-----' % stem.descriptor.CRYPTO_BLOB})
self.assertRaises(ValueError, KeyCertificate, content, True)
stem-1.6.0/test/unit/descriptor/networkstatus/bridge_document.py 0000664 0001750 0001750 00000004331 13124757510 025773 0 ustar atagar atagar 0000000 0000000 """
Unit tests for the BridgeNetworkStatusDocument of stem.descriptor.networkstatus.
"""
import datetime
import unittest
import stem.descriptor
from stem.descriptor.networkstatus import BridgeNetworkStatusDocument
from test.unit.descriptor import get_resource
DOCUMENT = b"""\
published 2012-06-01 04:07:04
r Unnamed ABSiBVJ42z6w5Z6nAXQUFq8YVVg FI74aFuNJZZQrgln0f+OaocMd0M 2012-05-31 15:57:00 10.97.236.247 443 0
s Valid
w Bandwidth=55
p reject 1-65535
r TolFuin AFn9TveYjdtZEsgh7QsWp3qC5kU 1Sw8RPx2Tq/w+VHL+pZipiJUG5k 2012-05-31 18:12:39 10.99.47.37 80 0
s Fast Guard Running Stable Valid
w Bandwidth=32
p reject 1-65535
"""
class TestBridgeNetworkStatusDocument(unittest.TestCase):
def test_metrics_bridge_consensus(self):
"""
Checks if the bridge documents from Metrics are parsed properly.
"""
consensus_path = get_resource('bridge_network_status')
with open(consensus_path, 'rb') as descriptor_file:
router = next(stem.descriptor.parse_file(descriptor_file))
self.assertEqual('Unnamed', router.nickname)
self.assertEqual('0014A2055278DB3EB0E59EA701741416AF185558', router.fingerprint)
self.assertEqual('148EF8685B8D259650AE0967D1FF8E6A870C7743', router.digest)
self.assertEqual(datetime.datetime(2012, 5, 31, 15, 57, 0), router.published)
self.assertEqual('10.97.236.247', router.address)
self.assertEqual(443, router.or_port)
self.assertEqual(None, router.dir_port)
def test_empty_document(self):
"""
Parse a document without any router status entries.
"""
document = BridgeNetworkStatusDocument(b'published 2012-06-01 04:07:04')
self.assertEqual(datetime.datetime(2012, 6, 1, 4, 7, 4), document.published)
self.assertEqual({}, document.routers)
self.assertEqual([], document.get_unrecognized_lines())
def test_document(self):
"""
Parse a document with router status entries.
"""
document = BridgeNetworkStatusDocument(DOCUMENT)
self.assertEqual(datetime.datetime(2012, 6, 1, 4, 7, 4), document.published)
self.assertEqual(2, len(document.routers))
self.assertEqual(set(['Unnamed', 'TolFuin']), set([desc.nickname for desc in document.routers.values()]))
self.assertEqual([], document.get_unrecognized_lines())
stem-1.6.0/test/unit/descriptor/networkstatus/document_v2.py 0000664 0001750 0001750 00000011671 13125773507 025100 0 ustar atagar atagar 0000000 0000000 """
Unit tests for the NetworkStatusDocumentV2 of stem.descriptor.networkstatus.
"""
import datetime
import unittest
import test.require
from stem.descriptor.networkstatus import NetworkStatusDocumentV2
from test.unit.descriptor import get_resource
class TestNetworkStatusDocument(unittest.TestCase):
def test_consensus_v2(self):
"""
Checks that version 2 consensus documents are properly parsed.
"""
expected_signing_key = """-----BEGIN RSA PUBLIC KEY-----
MIGJAoGBAOcrht/y5rkaahfX7sMe2qnpqoPibsjTSJaDvsUtaNP/Bq0MgNDGOR48
rtwfqTRff275Edkp/UYw3G3vSgKCJr76/bqOHCmkiZrnPV1zxNfrK18gNw2Cxre0
nTA+fD8JQqpPtb8b0SnG9kwy75eS//sRu7TErie2PzGMxrf9LH0LAgMBAAE=
-----END RSA PUBLIC KEY-----"""
expected_signature = """-----BEGIN SIGNATURE-----
2nXCxVje3wzn6HrIFRNMc0nc48AhMVpHZyPwRKGXkuYfTQG55uvwQDaFgJHud4RT
27QhWltau3K1evhnzhKcpbTXwkVv1TBYJSzL6rEeAn8cQ7ZiCyqf4EJCaNcem3d2
TpQQk3nNQF8z6UIvdlvP+DnJV4izWVkQEZgUZgIVM0E=
-----END SIGNATURE-----"""
with open(get_resource('cached-consensus-v2'), 'rb') as descriptor_file:
descriptor_file.readline() # strip header
document = NetworkStatusDocumentV2(descriptor_file.read())
self.assertEqual(2, document.version)
self.assertEqual('18.244.0.114', document.hostname)
self.assertEqual('18.244.0.114', document.address)
self.assertEqual(80, document.dir_port)
self.assertEqual('719BE45DE224B607C53707D0E2143E2D423E74CF', document.fingerprint)
self.assertEqual('arma at mit dot edu', document.contact)
self.assertEqual(expected_signing_key, document.signing_key)
self.assertEqual(67, len(document.client_versions))
self.assertEqual('0.0.9rc2', document.client_versions[0])
self.assertEqual('0.1.1.10-alpha-cvs', document.client_versions[-1])
self.assertEqual(67, len(document.server_versions))
self.assertEqual('0.0.9rc2', document.server_versions[0])
self.assertEqual('0.1.1.10-alpha-cvs', document.server_versions[-1])
self.assertEqual(datetime.datetime(2005, 12, 16, 0, 13, 46), document.published)
self.assertEqual(['Names', 'Versions'], document.options)
self.assertEqual('moria2', document.signing_authority)
self.assertEqual(expected_signature, document.signature)
self.assertEqual([], document.get_unrecognized_lines())
self.assertEqual(3, len(document.routers))
router1 = document.routers['719BE45DE224B607C53707D0E2143E2D423E74CF']
self.assertEqual('moria2', router1.nickname)
self.assertEqual('719BE45DE224B607C53707D0E2143E2D423E74CF', router1.fingerprint)
self.assertEqual('B7F3F0975B87889DD1285FD57A1B1BB617F65432', router1.digest)
self.assertEqual(datetime.datetime(2005, 12, 15, 6, 57, 18), router1.published)
self.assertEqual('18.244.0.114', router1.address)
self.assertEqual(443, router1.or_port)
self.assertEqual(80, router1.dir_port)
self.assertEqual(set(['Authority', 'Fast', 'Named', 'Running', 'Valid', 'V2Dir']), set(router1.flags))
router2 = document.routers['0928BA467056C4A689FEE4EF5D71482B6289C3D5']
self.assertEqual('stnv', router2.nickname)
self.assertEqual('0928BA467056C4A689FEE4EF5D71482B6289C3D5', router2.fingerprint)
self.assertEqual('22D1A7ED4199BDA7ED6C416EECD769C18E1F2A5A', router2.digest)
self.assertEqual(datetime.datetime(2005, 12, 15, 16, 24, 42), router2.published)
self.assertEqual('84.16.236.173', router2.address)
self.assertEqual(9001, router2.or_port)
self.assertEqual(None, router2.dir_port)
self.assertEqual(set(['Named', 'Valid']), set(router2.flags))
router3 = document.routers['09E8582FF0E6F85E2B8E41C0DC0B9C9DC46E6968']
self.assertEqual('nggrplz', router3.nickname)
self.assertEqual('09E8582FF0E6F85E2B8E41C0DC0B9C9DC46E6968', router3.fingerprint)
self.assertEqual('B302C2B01C94F398E3EF38939526B0651F824DD6', router3.digest)
self.assertEqual(datetime.datetime(2005, 12, 15, 23, 25, 50), router3.published)
self.assertEqual('194.109.109.109', router3.address)
self.assertEqual(9001, router3.or_port)
self.assertEqual(None, router3.dir_port)
self.assertEqual(set(['Fast', 'Stable', 'Running', 'Valid']), set(router3.flags))
def test_minimal_document(self):
"""
Parses a minimal v2 network status document.
"""
document = NetworkStatusDocumentV2.create()
self.assertEqual({}, document.routers)
self.assertEqual(2, document.version)
self.assertEqual(80, document.dir_port)
self.assertEqual(40, len(document.fingerprint))
self.assertEqual('arma at mit dot edu', document.contact)
self.assertEqual([], document.client_versions)
self.assertEqual([], document.server_versions)
self.assertEqual([], document.options)
self.assertEqual('moria2', document.signing_authority)
@test.require.cryptography
def test_descriptor_signing(self):
self.assertRaisesRegexp(NotImplementedError, 'Signing of NetworkStatusDocumentV2 not implemented', NetworkStatusDocumentV2.create, sign = True)
stem-1.6.0/test/unit/descriptor/networkstatus/directory_authority.py 0000664 0001750 0001750 00000022476 13126263511 026762 0 ustar atagar atagar 0000000 0000000 """
Unit tests for the DirectoryAuthority of stem.descriptor.networkstatus.
"""
import unittest
import test.require
from stem.descriptor.networkstatus import (
DirectoryAuthority,
KeyCertificate,
)
DIR_SOURCE_LINE = 'turtles 27B6B5996C426270A5C95488AA5BCEB6BCC86956 no.place.com 76.73.17.194 9030 9090'
class TestDirectoryAuthority(unittest.TestCase):
def test_minimal_consensus_authority(self):
"""
Parses a minimal directory authority for a consensus.
"""
authority = DirectoryAuthority.create()
self.assertTrue(authority.nickname.startswith('Unnamed'))
self.assertEqual(40, len(authority.fingerprint))
self.assertEqual('no.place.com', authority.hostname)
self.assertEqual(9030, authority.dir_port)
self.assertEqual(9090, authority.or_port)
self.assertEqual(False, authority.is_legacy)
self.assertEqual('Mike Perry ', authority.contact)
self.assertEqual(40, len(authority.vote_digest))
self.assertEqual(None, authority.legacy_dir_key)
self.assertEqual(None, authority.key_certificate)
self.assertEqual([], authority.get_unrecognized_lines())
def test_minimal_vote_authority(self):
"""
Parses a minimal directory authority for a vote.
"""
authority = DirectoryAuthority.create(is_vote = True)
self.assertTrue(authority.nickname.startswith('Unnamed'))
self.assertEqual(40, len(authority.fingerprint))
self.assertEqual('no.place.com', authority.hostname)
self.assertEqual(9030, authority.dir_port)
self.assertEqual(9090, authority.or_port)
self.assertEqual(False, authority.is_legacy)
self.assertEqual('Mike Perry ', authority.contact)
self.assertEqual(None, authority.vote_digest)
self.assertEqual(None, authority.legacy_dir_key)
self.assertEqual([], authority.get_unrecognized_lines())
@test.require.cryptography
def test_descriptor_signing(self):
self.assertRaisesRegexp(NotImplementedError, 'Signing of DirectoryAuthority not implemented', DirectoryAuthority.create, sign = True)
def test_unrecognized_line(self):
"""
Includes unrecognized content in the descriptor.
"""
authority = DirectoryAuthority.create({'pepperjack': 'is oh so tasty!'})
self.assertEqual(['pepperjack is oh so tasty!'], authority.get_unrecognized_lines())
def test_legacy_authority(self):
"""
Parses an authority using the '-legacy' format.
"""
content = 'dir-source gabelmoo-legacy 81349FC1F2DBA2C2C11B45CB9706637D480AB913 131.188.40.189 131.188.40.189 80 443'
authority = DirectoryAuthority(content, is_vote = False)
self.assertEqual('gabelmoo-legacy', authority.nickname)
self.assertEqual('81349FC1F2DBA2C2C11B45CB9706637D480AB913', authority.fingerprint)
self.assertEqual('131.188.40.189', authority.hostname)
self.assertEqual('131.188.40.189', authority.address)
self.assertEqual(80, authority.dir_port)
self.assertEqual(443, authority.or_port)
self.assertEqual(True, authority.is_legacy)
self.assertEqual(None, authority.contact)
self.assertEqual(None, authority.vote_digest)
self.assertEqual(None, authority.legacy_dir_key)
self.assertEqual(None, authority.key_certificate)
self.assertEqual([], authority.get_unrecognized_lines())
def test_first_line(self):
"""
Includes a non-mandatory field before the 'dir-source' line.
"""
content = b'ho-hum 567\n' + DirectoryAuthority.content()
self.assertRaises(ValueError, DirectoryAuthority, content, True)
authority = DirectoryAuthority(content, False)
self.assertTrue(authority.nickname.startswith('Unnamed'))
self.assertEqual(['ho-hum 567'], authority.get_unrecognized_lines())
def test_missing_fields(self):
"""
Parse an authority where a mandatory field is missing.
"""
for excluded_field in ('dir-source', 'contact'):
content = DirectoryAuthority.content(exclude = (excluded_field,))
self.assertRaises(ValueError, DirectoryAuthority, content, True)
authority = DirectoryAuthority(content, False)
if excluded_field == 'dir-source':
self.assertEqual('Mike Perry ', authority.contact)
else:
self.assertTrue(authority.nickname.startswith('Unnamed'))
def test_blank_lines(self):
"""
Includes blank lines, which should be ignored.
"""
authority = DirectoryAuthority.create({'dir-source': DIR_SOURCE_LINE + '\n\n\n'})
self.assertEqual('Mike Perry ', authority.contact)
def test_duplicate_lines(self):
"""
Duplicates linesin the entry.
"""
lines = DirectoryAuthority.content().split(b'\n')
for index, duplicate_line in enumerate(lines):
content = b'\n'.join(lines[:index] + [duplicate_line] + lines[index:])
self.assertRaises(ValueError, DirectoryAuthority, content, True)
authority = DirectoryAuthority(content, False)
self.assertTrue(authority.nickname.startswith('Unnamed'))
def test_missing_dir_source_field(self):
"""
Excludes fields from the 'dir-source' line.
"""
for missing_value in DIR_SOURCE_LINE.split(' '):
dir_source = DIR_SOURCE_LINE.replace(missing_value, '').replace(' ', ' ')
content = DirectoryAuthority.content({'dir-source': dir_source})
self.assertRaises(ValueError, DirectoryAuthority, content, True)
authority = DirectoryAuthority(content, False)
self.assertEqual(None, authority.nickname)
self.assertEqual(None, authority.fingerprint)
self.assertEqual(None, authority.hostname)
self.assertEqual(None, authority.address)
self.assertEqual(None, authority.dir_port)
self.assertEqual(None, authority.or_port)
def test_malformed_fingerprint(self):
"""
Includes a malformed fingerprint on the 'dir-source' line.
"""
test_values = (
'',
'zzzzz',
'zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz',
)
for value in test_values:
dir_source = DIR_SOURCE_LINE.replace('27B6B5996C426270A5C95488AA5BCEB6BCC86956', value)
content = DirectoryAuthority.content({'dir-source': dir_source})
self.assertRaises(ValueError, DirectoryAuthority, content, True)
authority = DirectoryAuthority(content, False)
self.assertEqual(None, authority.fingerprint)
def test_malformed_address(self):
"""
Includes a malformed ip address on the 'dir-source' line.
"""
test_values = (
'',
'71.35.150.',
'71.35..29',
'71.35.150',
'71.35.150.256',
'[fd9f:2e19:3bcf::02:9970]',
)
for value in test_values:
dir_source = DIR_SOURCE_LINE.replace('76.73.17.194', value)
content = DirectoryAuthority.content({'dir-source': dir_source})
self.assertRaises(ValueError, DirectoryAuthority, content, True)
authority = DirectoryAuthority(content, False)
self.assertEqual(None, authority.address)
def test_malformed_port(self):
"""
Includes a malformed orport or dirport on the 'dir-source' line.
"""
test_values = (
'',
'-1',
'399482',
'blarg',
)
for value in test_values:
for include_or_port in (False, True):
for include_dir_port in (False, True):
if not include_or_port and not include_dir_port:
continue
dir_source = DIR_SOURCE_LINE
if include_or_port:
dir_source = dir_source.replace('9090', value)
if include_dir_port:
dir_source = dir_source.replace('9030', value)
content = DirectoryAuthority.content({'dir-source': dir_source})
self.assertRaises(ValueError, DirectoryAuthority, content, True)
authority = DirectoryAuthority(content, False)
actual_value = authority.or_port if include_or_port else authority.dir_port
self.assertEqual(None, actual_value)
def test_legacy_dir_key(self):
"""
Includes a 'legacy-dir-key' line with both valid and invalid content.
"""
test_value = '65968CCB6BECB5AA88459C5A072624C6995B6B72'
authority = DirectoryAuthority.create({'legacy-dir-key': test_value}, is_vote = True)
self.assertEqual(test_value, authority.legacy_dir_key)
# check that we'll fail if legacy-dir-key appears in a consensus
content = DirectoryAuthority.content({'legacy-dir-key': test_value})
self.assertRaises(ValueError, DirectoryAuthority, content, True)
test_values = (
'',
'zzzzz',
'zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz',
)
for value in test_values:
content = DirectoryAuthority.content({'legacy-dir-key': value})
self.assertRaises(ValueError, DirectoryAuthority, content, True)
authority = DirectoryAuthority(content, False)
self.assertEqual(None, authority.legacy_dir_key)
def test_key_certificate(self):
"""
Includes or exclude a key certificate from the directory entry.
"""
key_cert = KeyCertificate.content()
# include a key cert with a consensus
content = DirectoryAuthority.content() + b'\n' + key_cert
self.assertRaises(ValueError, DirectoryAuthority, content, True)
authority = DirectoryAuthority(content, False)
self.assertTrue(authority.nickname.startswith('Unnamed'))
# exclude key cert from a vote
content = b'\n'.join(DirectoryAuthority.content(is_vote = True).splitlines()[:-5])
self.assertRaises(ValueError, DirectoryAuthority, content, True, True)
authority = DirectoryAuthority(content, False, True)
self.assertTrue(authority.nickname.startswith('Unnamed'))
stem-1.6.0/test/unit/descriptor/networkstatus/document_v3.py 0000664 0001750 0001750 00000150736 13146346524 025105 0 ustar atagar atagar 0000000 0000000 """
Unit tests for the NetworkStatusDocumentV3 of stem.descriptor.networkstatus.
"""
import datetime
import io
import re
import unittest
import stem.descriptor
import stem.version
import test.require
from stem import Flag
from stem.util import str_type
from stem.descriptor import CRYPTO_BLOB
from stem.descriptor.networkstatus import (
HEADER_STATUS_DOCUMENT_FIELDS,
FOOTER_STATUS_DOCUMENT_FIELDS,
DEFAULT_PARAMS,
PackageVersion,
DirectoryAuthority,
NetworkStatusDocumentV3,
_parse_file,
)
from stem.descriptor.router_status_entry import (
RouterStatusEntryV3,
RouterStatusEntryMicroV3,
)
from test.unit.descriptor import get_resource
try:
# added in python 2.7
from collections import OrderedDict
except ImportError:
from stem.util.ordereddict import OrderedDict
BANDWIDTH_WEIGHT_ENTRIES = (
'Wbd', 'Wbe', 'Wbg', 'Wbm',
'Wdb',
'Web', 'Wed', 'Wee', 'Weg', 'Wem',
'Wgb', 'Wgd', 'Wgg', 'Wgm',
'Wmb', 'Wmd', 'Wme', 'Wmg', 'Wmm',
)
class TestNetworkStatusDocument(unittest.TestCase):
def test_metrics_consensus(self):
"""
Checks if consensus documents from Metrics are parsed properly.
"""
consensus_path = get_resource('metrics_consensus')
for specify_type in (True, False):
with open(consensus_path, 'rb') as descriptor_file:
if specify_type:
descriptors = stem.descriptor.parse_file(descriptor_file, 'network-status-consensus-3 1.0')
else:
descriptors = stem.descriptor.parse_file(descriptor_file)
router = next(descriptors)
self.assertEqual('sumkledi', router.nickname)
self.assertEqual('0013D22389CD50D0B784A3E4061CB31E8CE8CEB5', router.fingerprint)
self.assertEqual('F260ABF1297B445E04354E236F4159140FF7768F', router.digest)
self.assertEqual(datetime.datetime(2012, 7, 12, 4, 1, 55), router.published)
self.assertEqual('178.218.213.229', router.address)
self.assertEqual(80, router.or_port)
self.assertEqual(None, router.dir_port)
def test_real_consensus(self):
"""
Checks that version 3 consensus documents from chutney can be properly
parsed.
"""
expected_flags = set(
['Authority', 'Exit', 'Fast', 'Guard', 'HSDir',
'Running', 'Stable', 'V2Dir', 'Valid', 'NoEdConsensus'])
expected_bandwidth_weights = {
'Web': 10000, 'Wdb': 10000, 'Weg': 3333, 'Wee': 10000, 'Wed': 3333,
'Wgd': 3333, 'Wgb': 10000, 'Wgg': 10000, 'Wem': 10000, 'Wbg': 0,
'Wbd': 3333, 'Wbe': 0, 'Wmm': 10000, 'Wmb': 10000, 'Wgm': 10000,
'Wbm': 10000, 'Wmg': 0, 'Wme': 0, 'Wmd': 3333
}
expected_signature = """\
-----BEGIN SIGNATURE-----
Ho0rLojfLHs9cSPFxe6znuGuFU8BvRr6gnH1gULTjUZO0NSQvo5N628KFeAsq+pT
ElieQeV6UfwnYN1U2tomhBYv3+/p1xBxYS5oTDAITxLUYvH4pLYz09VutwFlFFtU
r/satajuOMST0M3wCCBC4Ru5o5FSklwJTPJ/tWRXDCEHv/N5ZUUkpnNdn+7tFSZ9
eFrPxPcQvB05BESo7C4/+ZnZVO/wduObSYu04eWwTEog2gkSWmsztKoXpx1QGrtG
sNL22Ws9ySGDO/ykFFyxkcuyB5A8oPyedR7DrJUfCUYyB8o+XLNwODkCFxlmtFOj
ci356fosgLiM1sVqCUkNdA==
-----END SIGNATURE-----"""
with open(get_resource('cached-consensus'), 'rb') as descriptor_file:
document = stem.descriptor.networkstatus.NetworkStatusDocumentV3(descriptor_file.read(), default_params = False)
self.assertEqual(3, document.version)
self.assertEqual(None, document.version_flavor)
self.assertEqual(True, document.is_consensus)
self.assertEqual(False, document.is_vote)
self.assertEqual(False, document.is_microdescriptor)
self.assertEqual(datetime.datetime(2017, 5, 25, 4, 46, 30), document.valid_after)
self.assertEqual(datetime.datetime(2017, 5, 25, 4, 46, 40), document.fresh_until)
self.assertEqual(datetime.datetime(2017, 5, 25, 4, 46, 50), document.valid_until)
self.assertEqual(2, document.vote_delay)
self.assertEqual(2, document.dist_delay)
self.assertEqual([], document.client_versions)
self.assertEqual([], document.server_versions)
self.assertEqual(expected_flags, set(document.known_flags))
self.assertEqual([], document.packages)
self.assertEqual({}, document.params)
self.assertEqual(26, document.consensus_method)
self.assertEqual(expected_bandwidth_weights, document.bandwidth_weights)
self.assertEqual([], document.consensus_methods)
self.assertEqual(None, document.published)
self.assertEqual([], document.get_unrecognized_lines())
router = document.routers['348225F83C854796B2DD6364E65CB189B33BD696']
self.assertEqual('test002r', router.nickname)
self.assertEqual('348225F83C854796B2DD6364E65CB189B33BD696', router.fingerprint)
self.assertEqual('533429F8413C1B46022AD365655CBEDE1E6DBF44', router.digest)
self.assertEqual(datetime.datetime(2017, 5, 25, 4, 46, 11), router.published)
self.assertEqual('127.0.0.1', router.address)
self.assertEqual(5002, router.or_port)
self.assertEqual(7002, router.dir_port)
self.assertEqual(set(['Exit', 'Fast', 'Running', 'Valid', 'V2Dir', 'Guard', 'HSDir', 'Stable']), set(router.flags))
authority = document.directory_authorities[0]
self.assertEqual(2, len(document.directory_authorities))
self.assertEqual('test001a', authority.nickname)
self.assertEqual('596CD48D61FDA4E868F4AA10FF559917BE3B1A35', authority.fingerprint)
self.assertEqual('127.0.0.1', authority.hostname)
self.assertEqual('127.0.0.1', authority.address)
self.assertEqual(7001, authority.dir_port)
self.assertEqual(5001, authority.or_port)
self.assertEqual('auth1@test.test', authority.contact)
self.assertEqual('2E7177224BBA39B505F7608FF376C07884CF926F', authority.vote_digest)
self.assertEqual(None, authority.legacy_dir_key)
self.assertEqual(None, authority.key_certificate)
signature = document.signatures[0]
self.assertEqual(2, len(document.signatures))
self.assertEqual('sha1', signature.method)
self.assertEqual('596CD48D61FDA4E868F4AA10FF559917BE3B1A35', signature.identity)
self.assertEqual('9FBF54D6A62364320308A615BF4CF6B27B254FAD', signature.key_digest)
self.assertEqual(expected_signature, signature.signature)
def test_metrics_vote(self):
"""
Checks if vote documents from Metrics are parsed properly.
"""
vote_path = get_resource('metrics_vote')
with open(vote_path, 'rb') as descriptor_file:
descriptors = stem.descriptor.parse_file(descriptor_file)
router = next(descriptors)
self.assertEqual('sumkledi', router.nickname)
self.assertEqual('0013D22389CD50D0B784A3E4061CB31E8CE8CEB5', router.fingerprint)
self.assertEqual('0799F806200B005F01E40A9A7F1A21C988AE8FB1', router.digest)
self.assertEqual(datetime.datetime(2012, 7, 11, 4, 22, 53), router.published)
self.assertEqual('178.218.213.229', router.address)
self.assertEqual(80, router.or_port)
self.assertEqual(None, router.dir_port)
def test_vote(self):
"""
Checks that vote documents are properly parsed.
"""
expected_flags = set(
['Authority', 'BadExit', 'Exit', 'Fast', 'Guard', 'HSDir',
'Running', 'Stable', 'V2Dir', 'Valid'])
expected_identity_key = """-----BEGIN RSA PUBLIC KEY-----
MIIBigKCAYEA6uSmsoxj2MiJ3qyZq0qYXlRoG8o82SNqg+22m+t1c7MlQOZWPJYn
XeMcBCt8xrTeIt2ZI+Q/Kt2QJSeD9WZRevTKk/kn5Tg2+xXPogalUU47y5tUohGz
+Q8+CxtRSXpDxBHL2P8rLHvGrI69wbNHGoQkce/7gJy9vw5Ie2qzbyXk1NG6V8Fb
pr6A885vHo6TbhUnolz2Wqt/kN+UorjLkN2H3fV+iGcQFv42SyHYGDLa0WwL3PJJ
r/veu36S3VaHBrfhutfioi+d3d4Ya0bKwiWi5Lm2CHuuRTgMpHLU9vlci8Hunuxq
HsULe2oMsr4VEic7sW5SPC5Obpx6hStHdNv1GxoSEm3/vIuPM8pINpU5ZYAyH9yO
Ef22ZHeiVMMKmpV9TtFyiFqvlI6GpQn3mNbsQqF1y3XCA3Q4vlRAkpgJVUSvTxFP
2bNDobOyVCpCM/rwxU1+RCNY5MFJ/+oktUY+0ydvTen3gFdZdgNqCYjKPLfBNm9m
RGL7jZunMUNvAgMBAAE=
-----END RSA PUBLIC KEY-----"""
expected_signing_key = """-----BEGIN RSA PUBLIC KEY-----
MIGJAoGBAJ5itcJRYNEM3Qf1OVWLRkwjqf84oXPc2ZusaJ5zOe7TVvBMra9GNyc0
NM9y6zVkHCAePAjr4KbW/8P1olA6FUE2LV9bozaU1jFf6K8B2OELKs5FUEW+n+ic
GM0x6MhngyXonWOcKt5Gj+mAu5lrno9tpNbPkz2Utr/Pi0nsDhWlAgMBAAE=
-----END RSA PUBLIC KEY-----"""
expected_key_crosscert = """-----BEGIN ID SIGNATURE-----
RHYImGTwg36wmEdAn7qaRg2sAfql7ZCtPIL/O3lU5OIdXXp0tNn/K00Bamqohjk+
Tz4FKsKXGDlbGv67PQcZPOK6NF0GRkNh4pk89prrDO4XwtEn7rkHHdBH6/qQ7IRG
GdDZHtZ1a69oFZvPWD3hUaB50xeIe7GoKdKIfdNNJ+8=
-----END ID SIGNATURE-----"""
expected_key_certification = """-----BEGIN SIGNATURE-----
fasWOGyUZ3iMCYpDfJ+0JcMiTH25sXPWzvlHorEOyOMbaMqRYpZU4GHzt1jLgdl6
AAoR6KdamsLg5VE8xzst48a4UFuzHFlklZ5O8om2rcvDd5DhSnWWYZnYJecqB+bo
dNisPmaIVSAWb29U8BpNRj4GMC9KAgGYUj8aE/KtutAeEekFfFEHTfWZ2fFp4j3m
9rY8FWraqyiF+Emq1T8pAAgMQ+79R3oZxq0TXS42Z4Anhms735ccauKhI3pDKjbl
tD5vAzIHOyjAOXj7a6jY/GrnaBNuJ4qe/4Hf9UmzK/jKKwG95BPJtPTT4LoFwEB0
KG2OUeQUNoCck4nDpsZwFqPlrWCHcHfTV2iDYFV1HQWDTtZz/qf+GtB8NXsq+I1w
brADmvReM2BD6p/13h0QURCI5hq7ZYlIKcKrBa0jn1d9cduULl7vgKsRCJDls/ID
emBZ6pUxMpBmV0v+PrA3v9w4DlE7GHAq61FF/zju2kpqj6MInbEvI/E+e438sWsL
-----END SIGNATURE-----"""
expected_signature = """-----BEGIN SIGNATURE-----
fskXN84wB3mXfo+yKGSt0AcDaaPuU3NwMR3ROxWgLN0KjAaVi2eV9PkPCsQkcgw3
JZ/1HL9sHyZfo6bwaC6YSM9PNiiY6L7rnGpS7UkHiFI+M96VCMorvjm5YPs3FioJ
DnN5aFtYKiTc19qIC7Nmo+afPdDEf0MlJvEOP5EWl3w=
-----END SIGNATURE-----"""
with open(get_resource('unparseable/vote'), 'rb') as descriptor_file:
document = stem.descriptor.networkstatus.NetworkStatusDocumentV3(descriptor_file.read(), default_params = False)
self.assertEqual(3, document.version)
self.assertEqual(None, document.version_flavor)
self.assertEqual(False, document.is_consensus)
self.assertEqual(True, document.is_vote)
self.assertEqual(False, document.is_microdescriptor)
self.assertEqual(datetime.datetime(2012, 7, 12, 0, 0, 0), document.valid_after)
self.assertEqual(datetime.datetime(2012, 7, 12, 1, 0, 0), document.fresh_until)
self.assertEqual(datetime.datetime(2012, 7, 12, 3, 0, 0), document.valid_until)
self.assertEqual(300, document.vote_delay)
self.assertEqual(300, document.dist_delay)
self.assertEqual([], document.client_versions)
self.assertEqual([], document.server_versions)
self.assertEqual(expected_flags, set(document.known_flags))
self.assertEqual([], document.packages)
self.assertEqual({'CircuitPriorityHalflifeMsec': 30000, 'bwauthpid': 1}, document.params)
self.assertEqual(None, document.consensus_method)
self.assertEqual({}, document.bandwidth_weights)
self.assertEqual(list(range(1, 13)), document.consensus_methods)
self.assertEqual(datetime.datetime(2012, 7, 11, 23, 50, 1), document.published)
self.assertEqual([], document.get_unrecognized_lines())
router = document.routers['0013D22389CD50D0B784A3E4061CB31E8CE8CEB5']
self.assertEqual('sumkledi', router.nickname)
self.assertEqual('0013D22389CD50D0B784A3E4061CB31E8CE8CEB5', router.fingerprint)
self.assertEqual('0799F806200B005F01E40A9A7F1A21C988AE8FB1', router.digest)
self.assertEqual(datetime.datetime(2012, 7, 11, 4, 22, 53), router.published)
self.assertEqual('178.218.213.229', router.address)
self.assertEqual(80, router.or_port)
self.assertEqual(None, router.dir_port)
authority = document.directory_authorities[0]
self.assertEqual(1, len(document.directory_authorities))
self.assertEqual('turtles', authority.nickname)
self.assertEqual('27B6B5996C426270A5C95488AA5BCEB6BCC86956', authority.fingerprint)
self.assertEqual('76.73.17.194', authority.hostname)
self.assertEqual('76.73.17.194', authority.address)
self.assertEqual(9030, authority.dir_port)
self.assertEqual(9090, authority.or_port)
self.assertEqual('Mike Perry ', authority.contact)
self.assertEqual(None, authority.vote_digest)
self.assertEqual(None, authority.legacy_dir_key)
certificate = authority.key_certificate
self.assertEqual(3, certificate.version)
self.assertEqual(None, certificate.address)
self.assertEqual(None, certificate.dir_port)
self.assertEqual('27B6B5996C426270A5C95488AA5BCEB6BCC86956', certificate.fingerprint)
self.assertEqual(expected_identity_key, certificate.identity_key)
self.assertEqual(datetime.datetime(2011, 11, 28, 21, 51, 4), certificate.published)
self.assertEqual(datetime.datetime(2012, 11, 28, 21, 51, 4), certificate.expires)
self.assertEqual(expected_signing_key, certificate.signing_key)
self.assertEqual(expected_key_crosscert, certificate.crosscert)
self.assertEqual(expected_key_certification, certificate.certification)
signature = document.signatures[0]
self.assertEqual(1, len(document.signatures))
self.assertEqual('sha1', signature.method)
self.assertEqual('27B6B5996C426270A5C95488AA5BCEB6BCC86956', signature.identity)
self.assertEqual('D5C30C15BB3F1DA27669C2D88439939E8F418FCF', signature.key_digest)
self.assertEqual(expected_signature, signature.signature)
def test_minimal_consensus(self):
"""
Parses a minimal network status document.
"""
document = NetworkStatusDocumentV3.create()
expected_known_flags = [
Flag.AUTHORITY, Flag.BADEXIT, Flag.EXIT,
Flag.FAST, Flag.GUARD, Flag.HSDIR, Flag.NAMED, Flag.RUNNING,
Flag.STABLE, Flag.UNNAMED, Flag.V2DIR, Flag.VALID]
self.assertEqual({}, document.routers)
self.assertEqual(3, document.version)
self.assertEqual(None, document.version_flavor)
self.assertEqual(True, document.is_consensus)
self.assertEqual(False, document.is_vote)
self.assertEqual(False, document.is_microdescriptor)
self.assertEqual(9, document.consensus_method)
self.assertEqual([], document.consensus_methods)
self.assertEqual(None, document.published)
self.assertEqual(300, document.vote_delay)
self.assertEqual(300, document.dist_delay)
self.assertEqual([], document.client_versions)
self.assertEqual([], document.server_versions)
self.assertEqual(expected_known_flags, document.known_flags)
self.assertEqual([], document.packages)
self.assertEqual({}, document.flag_thresholds)
self.assertEqual(False, document.is_shared_randomness_participate)
self.assertEqual([], document.shared_randomness_commitments)
self.assertEqual(None, document.shared_randomness_previous_reveal_count)
self.assertEqual(None, document.shared_randomness_previous_value)
self.assertEqual(None, document.shared_randomness_current_reveal_count)
self.assertEqual(None, document.shared_randomness_current_value)
self.assertEqual({}, document.recommended_client_protocols)
self.assertEqual({}, document.recommended_relay_protocols)
self.assertEqual({}, document.required_client_protocols)
self.assertEqual({}, document.required_relay_protocols)
self.assertEqual(DEFAULT_PARAMS, document.params)
self.assertEqual((), document.directory_authorities)
self.assertEqual({}, document.bandwidth_weights)
self.assertEqual([], document.get_unrecognized_lines())
def test_minimal_vote(self):
"""
Parses a minimal network status document.
"""
document = NetworkStatusDocumentV3.create({'vote-status': 'vote'})
expected_known_flags = [
Flag.AUTHORITY, Flag.BADEXIT, Flag.EXIT,
Flag.FAST, Flag.GUARD, Flag.HSDIR, Flag.NAMED, Flag.RUNNING,
Flag.STABLE, Flag.UNNAMED, Flag.V2DIR, Flag.VALID]
self.assertEqual({}, document.routers)
self.assertEqual(3, document.version)
self.assertEqual(False, document.is_consensus)
self.assertEqual(True, document.is_vote)
self.assertEqual(None, document.consensus_method)
self.assertEqual([1, 9], document.consensus_methods)
self.assertEqual(300, document.vote_delay)
self.assertEqual(300, document.dist_delay)
self.assertEqual([], document.client_versions)
self.assertEqual([], document.server_versions)
self.assertEqual(expected_known_flags, document.known_flags)
self.assertEqual([], document.packages)
self.assertEqual({}, document.flag_thresholds)
self.assertEqual(False, document.is_shared_randomness_participate)
self.assertEqual([], document.shared_randomness_commitments)
self.assertEqual(None, document.shared_randomness_previous_reveal_count)
self.assertEqual(None, document.shared_randomness_previous_value)
self.assertEqual(None, document.shared_randomness_current_reveal_count)
self.assertEqual(None, document.shared_randomness_current_value)
self.assertEqual(DEFAULT_PARAMS, document.params)
self.assertEqual({}, document.bandwidth_weights)
self.assertEqual([], document.get_unrecognized_lines())
@test.require.cryptography
def test_descriptor_signing(self):
self.assertRaisesRegexp(NotImplementedError, 'Signing of NetworkStatusDocumentV3 not implemented', NetworkStatusDocumentV3.create, sign = True)
def test_examples(self):
"""
Run something similar to the examples in the header pydocs.
"""
# makes a consensus with a couple routers, both with the same nickname
entry1 = RouterStatusEntryV3.create({'s': 'Fast'})
entry2 = RouterStatusEntryV3.create({'s': 'Valid'})
content = NetworkStatusDocumentV3.content(routers = (entry1, entry2))
# first example: parsing via the NetworkStatusDocumentV3 constructor
consensus_file = io.BytesIO(content)
consensus = NetworkStatusDocumentV3(consensus_file.read())
consensus_file.close()
for router in consensus.routers.values():
self.assertTrue(router.nickname.startswith('Unnamed'))
# second example: using stem.descriptor.parse_file
with io.BytesIO(content) as consensus_file:
for router in stem.descriptor.parse_file(consensus_file, 'network-status-consensus-3 1.0'):
self.assertTrue(router.nickname.startswith('Unnamed'))
@test.require.cryptography
def test_signature_validation(self):
"""
Check that we can validate the consensus with its certificates.
"""
with open(get_resource('cached-consensus'), 'rb') as descriptor_file:
consensus_content = descriptor_file.read()
with open(get_resource('cached-certs'), 'rb') as cert_file:
certs = list(stem.descriptor.parse_file(cert_file, 'dir-key-certificate-3 1.0'))
consensus = stem.descriptor.networkstatus.NetworkStatusDocumentV3(consensus_content)
consensus.validate_signatures(certs)
# change a relay's nickname in the consensus so it's no longer validly signed
consensus = stem.descriptor.networkstatus.NetworkStatusDocumentV3(consensus_content.replace(b'test002r', b'different_nickname'))
self.assertRaisesRegexp(ValueError, 'Network Status Document has 0 valid signatures out of 2 total, needed 1', consensus.validate_signatures, certs)
def test_handlers(self):
"""
Try parsing a document with DocumentHandler.DOCUMENT and
DocumentHandler.BARE_DOCUMENT.
"""
# Simple sanity check that they provide the right type, and that the
# document includes or excludes the router status entries as appropriate.
entry1 = RouterStatusEntryV3.create({'s': 'Fast'})
entry2 = RouterStatusEntryV3.create({
'r': 'Nightfae AWt0XNId/OU2xX5xs5hVtDc5Mes 6873oEfM7fFIbxYtwllw9GPDwkA 2013-02-20 11:12:27 85.177.66.233 9001 9030',
's': 'Valid',
})
content = NetworkStatusDocumentV3.content(routers = (entry1, entry2))
descriptors = list(stem.descriptor.parse_file(io.BytesIO(content), 'network-status-consensus-3 1.0', document_handler = stem.descriptor.DocumentHandler.DOCUMENT))
self.assertEqual(1, len(descriptors))
self.assertTrue(isinstance(descriptors[0], NetworkStatusDocumentV3))
self.assertEqual(2, len(descriptors[0].routers))
descriptors = list(stem.descriptor.parse_file(io.BytesIO(content), 'network-status-consensus-3 1.0', document_handler = stem.descriptor.DocumentHandler.BARE_DOCUMENT))
self.assertEqual(1, len(descriptors))
self.assertTrue(isinstance(descriptors[0], NetworkStatusDocumentV3))
self.assertEqual(0, len(descriptors[0].routers))
def test_parse_file(self):
"""
Try parsing a document via the _parse_file() function.
"""
entry1 = RouterStatusEntryV3.create({'s': 'Fast'})
entry2 = RouterStatusEntryV3.create({'s': 'Valid'})
content = NetworkStatusDocumentV3.content(routers = (entry1, entry2))
# the document that the entries refer to should actually be the minimal
# descriptor (ie, without the entries)
descriptor_file = io.BytesIO(content)
entries = list(_parse_file(descriptor_file))
self.assertEqual(entry1, entries[0])
self.assertEqual(entry2, entries[1])
def test_missing_fields(self):
"""
Excludes mandatory fields from both a vote and consensus document.
"""
for is_consensus in (True, False):
attr = {'vote-status': 'consensus'} if is_consensus else {'vote-status': 'vote'}
is_vote = not is_consensus
for entries in (HEADER_STATUS_DOCUMENT_FIELDS, FOOTER_STATUS_DOCUMENT_FIELDS):
for field, in_votes, in_consensus, is_mandatory in entries:
if is_mandatory and field != 'vote-status' and ((is_consensus and in_consensus) or (is_vote and in_votes)):
content = NetworkStatusDocumentV3.content(attr, exclude = (field,))
self.assertRaises(ValueError, NetworkStatusDocumentV3, content, True)
NetworkStatusDocumentV3(content, False) # constructs without validation
def test_unrecognized_line(self):
"""
Includes unrecognized content in the document.
"""
document = NetworkStatusDocumentV3.create({'pepperjack': 'is oh so tasty!'})
self.assertEqual(['pepperjack is oh so tasty!'], document.get_unrecognized_lines())
def test_duplicate_fields(self):
"""
Almost all fields can only appear once. Checking that duplicates cause
validation errors.
"""
for is_consensus in (True, False):
attr = {'vote-status': 'consensus'} if is_consensus else {'vote-status': 'vote'}
lines = NetworkStatusDocumentV3.content(attr).split(b'\n')
for index, line in enumerate(lines):
if not is_consensus and lines[index].startswith(b'dir-source'):
break
# Stop when we hit the 'directory-signature' for a couple reasons...
# - that is the one field that can validly appear multiple times
# - after it is a crypto blob, which won't trigger this kind of
# validation failure
test_lines = list(lines)
if line.startswith(b'directory-signature '):
break
# duplicates the line
test_lines.insert(index, line)
content = b'\n'.join(test_lines)
self.assertRaises(ValueError, NetworkStatusDocumentV3, content, True)
NetworkStatusDocumentV3(content, False) # constructs without validation
def test_version(self):
"""
Parses the network-status-version field, including trying to handle a
different document version with the v3 parser.
"""
document = NetworkStatusDocumentV3.create({'network-status-version': '3'})
self.assertEqual(3, document.version)
self.assertEqual(None, document.version_flavor)
self.assertEqual(False, document.is_microdescriptor)
document = NetworkStatusDocumentV3.create({'network-status-version': '3 microdesc'})
self.assertEqual(3, document.version)
self.assertEqual('microdesc', document.version_flavor)
self.assertEqual(True, document.is_microdescriptor)
content = NetworkStatusDocumentV3.content({'network-status-version': '4'})
self.assertRaises(ValueError, NetworkStatusDocumentV3, content, True)
document = NetworkStatusDocumentV3(content, False)
self.assertEqual(4, document.version)
self.assertEqual(None, document.version_flavor)
self.assertEqual(False, document.is_microdescriptor)
def test_vote_status(self):
"""
Parses the vote-status field.
"""
document = NetworkStatusDocumentV3.create({'vote-status': 'vote'})
self.assertEqual(False, document.is_consensus)
self.assertEqual(True, document.is_vote)
content = NetworkStatusDocumentV3.content({'vote-status': 'consensus'})
document = NetworkStatusDocumentV3(content)
self.assertEqual(True, document.is_consensus)
self.assertEqual(False, document.is_vote)
test_values = (
'',
' ',
'votee',
)
for test_value in test_values:
content = NetworkStatusDocumentV3.content({'vote-status': test_value})
self.assertRaises(ValueError, NetworkStatusDocumentV3, content, True)
document = NetworkStatusDocumentV3(content, False)
self.assertEqual(True, document.is_consensus)
self.assertEqual(False, document.is_vote)
def test_consensus_methods(self):
"""
Parses the consensus-methods field.
"""
document = NetworkStatusDocumentV3.create({'vote-status': 'vote', 'consensus-methods': '12 3 1 780'})
self.assertEqual([12, 3, 1, 780], document.consensus_methods)
# check that we default to including consensus-method 1
content = NetworkStatusDocumentV3.content({'vote-status': 'vote'}, ('consensus-methods',))
document = NetworkStatusDocumentV3(content, False)
self.assertEqual([1], document.consensus_methods)
self.assertEqual(None, document.consensus_method)
test_values = (
(''),
(' '),
('1 2 3 a 5'),
('1 2 3 4.0 5'),
('2 3 4'), # spec says version one must be included
)
for test_value in test_values:
content = NetworkStatusDocumentV3.content({'vote-status': 'vote', 'consensus-methods': test_value})
self.assertRaises(ValueError, NetworkStatusDocumentV3, content, True)
expected_value = [2, 3, 4] if test_value == '2 3 4' else [1]
document = NetworkStatusDocumentV3(content, False)
self.assertEqual(expected_value, document.consensus_methods)
def test_consensus_method(self):
"""
Parses the consensus-method field.
"""
document = NetworkStatusDocumentV3.create({'consensus-method': '12'})
self.assertEqual(12, document.consensus_method)
# check that we default to being consensus-method 1
content = NetworkStatusDocumentV3.content(exclude = ('consensus-method',))
document = NetworkStatusDocumentV3(content, False)
self.assertEqual(1, document.consensus_method)
self.assertEqual([], document.consensus_methods)
test_values = (
'',
' ',
'a',
'1 2',
'2.0',
)
for test_value in test_values:
content = NetworkStatusDocumentV3.content({'consensus-method': test_value})
self.assertRaises(ValueError, NetworkStatusDocumentV3, content, True)
document = NetworkStatusDocumentV3(content, False)
self.assertEqual(1, document.consensus_method)
def test_time_fields(self):
"""
Parses invalid published, valid-after, fresh-until, and valid-until fields.
All are simply datetime values.
"""
expected = datetime.datetime(2012, 9, 2, 22, 0, 0)
test_value = '2012-09-02 22:00:00'
document = NetworkStatusDocumentV3.create({
'vote-status': 'vote',
'published': test_value,
'valid-after': test_value,
'fresh-until': test_value,
'valid-until': test_value,
})
self.assertEqual(expected, document.published)
self.assertEqual(expected, document.valid_after)
self.assertEqual(expected, document.fresh_until)
self.assertEqual(expected, document.valid_until)
test_values = (
'',
' ',
'2012-12-12',
'2012-12-12 01:01:',
'2012-12-12 01:a1:01',
)
for test_value in test_values:
content = NetworkStatusDocumentV3.content({'vote-status': 'vote', 'published': test_value})
self.assertRaises(ValueError, NetworkStatusDocumentV3, content, True)
document = NetworkStatusDocumentV3(content, False)
self.assertEqual(None, document.published)
def test_voting_delay(self):
"""
Parses the voting-delay field.
"""
document = NetworkStatusDocumentV3.create({'voting-delay': '12 345'})
self.assertEqual(12, document.vote_delay)
self.assertEqual(345, document.dist_delay)
test_values = (
'',
' ',
'1 a',
'1\t2',
'1 2.0',
)
for test_value in test_values:
content = NetworkStatusDocumentV3.content({'voting-delay': test_value})
self.assertRaises(ValueError, NetworkStatusDocumentV3, content, True)
document = NetworkStatusDocumentV3(content, False)
self.assertEqual(None, document.vote_delay)
self.assertEqual(None, document.dist_delay)
def test_version_lists(self):
"""
Parses client-versions and server-versions fields. Both are comma separated
lists of tor versions.
"""
expected = [stem.version.Version('1.2.3.4'), stem.version.Version('56.789.12.34-alpha')]
test_value = '1.2.3.4,56.789.12.34-alpha'
document = NetworkStatusDocumentV3.create({'client-versions': test_value, 'server-versions': test_value})
self.assertEqual(expected, document.client_versions)
self.assertEqual(expected, document.server_versions)
test_values = (
(''),
(' '),
('1.2.3.4,'),
('1.2.3.4,1.2.3.a'),
)
for field in ('client-versions', 'server-versions'):
attr = field.replace('-', '_')
for test_value in test_values:
content = NetworkStatusDocumentV3.content({field: test_value})
self.assertRaises(ValueError, NetworkStatusDocumentV3, content, True)
document = NetworkStatusDocumentV3(content, False)
self.assertEqual([], getattr(document, attr))
def test_packages(self):
"""
Parse the package line. These can appear multiple times, and have any
number of digests.
"""
test_values = (
(['Stem 1.3.0 https://stem.torproject.org/'],
[PackageVersion('Stem', '1.3.0', 'https://stem.torproject.org/', {})]),
(['Stem 1.3.0 https://stem.torproject.org/ sha1=5d676c8124b4be1f52ddc8e15ca143cad211eeb4 md5=600ad5e2fc4caf585c1bdaaa532b7e82'],
[PackageVersion('Stem', '1.3.0', 'https://stem.torproject.org/', {'sha1': '5d676c8124b4be1f52ddc8e15ca143cad211eeb4', 'md5': '600ad5e2fc4caf585c1bdaaa532b7e82'})]),
(['Stem 1.3.0 https://stem.torproject.org/', 'Txtorcon 0.13.0 https://github.com/meejah/txtorcon'],
[PackageVersion('Stem', '1.3.0', 'https://stem.torproject.org/', {}),
PackageVersion('Txtorcon', '0.13.0', 'https://github.com/meejah/txtorcon', {})]),
)
for test_value, expected_value in test_values:
document = NetworkStatusDocumentV3.create({'package': '\npackage '.join(test_value)})
self.assertEqual(expected_value, document.packages)
test_values = (
'',
' ',
'Stem',
'Stem 1.3.0',
'Stem 1.3.0 https://stem.torproject.org/ keyword_field',
'Stem 1.3.0 https://stem.torproject.org/ keyword_field key=value',
'Stem 1.3.0 https://stem.torproject.org/ key=value keyword_field',
)
for test_value in test_values:
content = NetworkStatusDocumentV3.content({'package': test_value})
self.assertRaises(ValueError, NetworkStatusDocumentV3, content, True)
document = NetworkStatusDocumentV3(content, False)
self.assertEqual([], document.packages)
def test_known_flags(self):
"""
Parses some known-flag entries. Just exercising the field, there's not much
to test here.
"""
test_values = (
('', []),
(' ', []),
('BadExit', [Flag.BADEXIT]),
('BadExit ', [Flag.BADEXIT]),
('BadExit ', [Flag.BADEXIT]),
('BadExit Fast', [Flag.BADEXIT, Flag.FAST]),
('BadExit Unrecognized Fast', [Flag.BADEXIT, 'Unrecognized', Flag.FAST]),
)
for test_value, expected_value in test_values:
document = NetworkStatusDocumentV3.create({'known-flags': test_value})
self.assertEqual(expected_value, document.known_flags)
def test_flag_thresholds(self):
"""
Parses the flag-thresholds entry.
"""
test_values = (
('', {}),
('fast-speed=40960', {str_type('fast-speed'): 40960}), # numeric value
('guard-wfu=94.669%', {str_type('guard-wfu'): 0.94669}), # percentage value
('guard-wfu=94.669% guard-tk=691200', {str_type('guard-wfu'): 0.94669, str_type('guard-tk'): 691200}), # multiple values
('stable-uptime=0 stable-mtbf=0 fast-speed=0 guard-wfu=0.000% guard-tk=0 guard-bw-inc-exits=0 guard-bw-exc-exits=0 enough-mtbf=1 ignoring-advertised-bws=0', {
str_type('stable-uptime'): 0,
str_type('stable-mtbf'): 0,
str_type('fast-speed'): 0,
str_type('guard-wfu'): 0.0,
str_type('guard-tk'): 0,
str_type('guard-bw-inc-exits'): 0,
str_type('guard-bw-exc-exits'): 0,
str_type('enough-mtbf'): 1,
str_type('ignoring-advertised-bws'): 0,
}),
)
for test_value, expected_value in test_values:
document = NetworkStatusDocumentV3.create({'vote-status': 'vote', 'flag-thresholds': test_value})
self.assertEqual(expected_value, document.flag_thresholds)
# parses a full entry found in an actual vote
full_line = 'stable-uptime=693369 stable-mtbf=153249 fast-speed=40960 guard-wfu=94.669% guard-tk=691200 guard-bw-inc-exits=174080 guard-bw-exc-exits=184320 enough-mtbf=1'
expected_value = {
str_type('stable-uptime'): 693369,
str_type('stable-mtbf'): 153249,
str_type('fast-speed'): 40960,
str_type('guard-wfu'): 0.94669,
str_type('guard-tk'): 691200,
str_type('guard-bw-inc-exits'): 174080,
str_type('guard-bw-exc-exits'): 184320,
str_type('enough-mtbf'): 1,
}
document = NetworkStatusDocumentV3.create({'vote-status': 'vote', 'flag-thresholds': full_line})
self.assertEqual(expected_value, document.flag_thresholds)
test_values = (
'stable-uptime 693369', # not a key=value mapping
'stable-uptime=a693369', # non-numeric value
'guard-wfu=94.669%%', # double quote
'stable-uptime=693369\tstable-mtbf=153249', # non-space divider
)
for test_value in test_values:
content = NetworkStatusDocumentV3.content({'vote-status': 'vote', 'flag-thresholds': test_value})
self.assertRaises(ValueError, NetworkStatusDocumentV3, content, True)
document = NetworkStatusDocumentV3(content, False)
self.assertEqual({}, document.flag_thresholds)
def test_parameters(self):
"""
Parses the parameters attributes.
"""
document = NetworkStatusDocumentV3.create(OrderedDict([
('vote-status', 'vote'),
('recommended-client-protocols', 'HSDir=1 HSIntro=3'),
('recommended-relay-protocols', 'Cons=1 Desc=1'),
('required-client-protocols', 'HSRend=1 Link=1-4 LinkAuth=1 Microdesc=1'),
('required-relay-protocols', 'DirCache=1'),
]))
self.assertEqual(2, len(document.recommended_client_protocols))
self.assertEqual(2, len(document.recommended_relay_protocols))
self.assertEqual(4, len(document.required_client_protocols))
self.assertEqual(1, len(document.required_relay_protocols))
def test_params(self):
"""
General testing for the 'params' line, exercising the happy cases.
"""
document = NetworkStatusDocumentV3.create({'params': 'CircuitPriorityHalflifeMsec=30000 bwauthpid=1 unrecognized=-122'})
self.assertEqual(30000, document.params['CircuitPriorityHalflifeMsec'])
self.assertEqual(1, document.params['bwauthpid'])
self.assertEqual(-122, document.params['unrecognized'])
# empty params line
content = NetworkStatusDocumentV3.content({'params': ''})
document = NetworkStatusDocumentV3(content, default_params = True)
self.assertEqual(DEFAULT_PARAMS, document.params)
content = NetworkStatusDocumentV3.content({'params': ''})
document = NetworkStatusDocumentV3(content, default_params = False)
self.assertEqual({}, document.params)
def test_params_malformed(self):
"""
Parses a 'params' line with malformed content.
"""
test_values = (
'foo=',
'foo=abc',
'foo=+123',
'foo=12\tbar=12',
)
for test_value in test_values:
content = NetworkStatusDocumentV3.content({'params': test_value})
self.assertRaises(ValueError, NetworkStatusDocumentV3, content, True)
document = NetworkStatusDocumentV3(content, False)
self.assertEqual(DEFAULT_PARAMS, document.params)
def test_params_range(self):
"""
Check both the furthest valid 'params' values and values that are out of
bounds.
"""
test_values = (
('foo=2147483648', {'foo': 2147483648}, False),
('foo=-2147483649', {'foo': -2147483649}, False),
('foo=2147483647', {'foo': 2147483647}, True),
('foo=-2147483648', {'foo': -2147483648}, True),
# param with special range constraints
('circwindow=99', {'circwindow': 99}, False),
('circwindow=1001', {'circwindow': 1001}, False),
('circwindow=500', {'circwindow': 500}, True),
# param that relies on another param for its constraints
('cbtclosequantile=79 cbtquantile=80', {'cbtclosequantile': 79, 'cbtquantile': 80}, False),
('cbtclosequantile=80 cbtquantile=80', {'cbtclosequantile': 80, 'cbtquantile': 80}, True),
)
for test_value, expected_value, is_ok in test_values:
content = NetworkStatusDocumentV3.content({'params': test_value})
if is_ok:
document = NetworkStatusDocumentV3(content, default_params = False)
else:
self.assertRaises(ValueError, NetworkStatusDocumentV3, content, True)
document = NetworkStatusDocumentV3(content, False, default_params = False)
self.assertEqual(expected_value, document.params)
def test_params_misordered(self):
"""
Check that the 'params' line is rejected if out of order.
"""
content = NetworkStatusDocumentV3.content({'params': 'unrecognized=-122 bwauthpid=1'})
self.assertRaises(ValueError, NetworkStatusDocumentV3, content, True)
document = NetworkStatusDocumentV3(content, False, default_params = False)
self.assertEqual({}, document.params)
def test_footer_consensus_method_requirement(self):
"""
Check that validation will notice if a footer appears before it was
introduced.
"""
content = NetworkStatusDocumentV3.content({'consensus-method': '8'})
self.assertRaises(ValueError, NetworkStatusDocumentV3, content, True)
document = NetworkStatusDocumentV3(content, False)
self.assertEqual([], document.get_unrecognized_lines())
# excludes a footer from a version that shouldn't have it
document = NetworkStatusDocumentV3.create({'consensus-method': '8'}, ('directory-footer', 'directory-signature'))
self.assertEqual([], document.signatures)
self.assertEqual([], document.get_unrecognized_lines())
# Prior to conensus method 9 votes can still have a signature in their
# footer...
#
# https://trac.torproject.org/7932
document = NetworkStatusDocumentV3.create(
{
'vote-status': 'vote',
'consensus-methods': '1 8',
},
exclude = ('directory-footer',),
authorities = (DirectoryAuthority.create(is_vote = True),)
)
self.assertEqual([], document.get_unrecognized_lines())
def test_footer_with_value(self):
"""
Tries to parse a descriptor with content on the 'directory-footer' line.
"""
content = NetworkStatusDocumentV3.content({'directory-footer': 'blarg'})
self.assertRaises(ValueError, NetworkStatusDocumentV3, content, True)
document = NetworkStatusDocumentV3(content, False)
self.assertEqual([], document.get_unrecognized_lines())
def test_bandwidth_wights_ok(self):
"""
Parses a properly formed 'bandwidth-wights' line. Negative bandwidth
weights might or might not be valid. The spec doesn't say, so making sure
that we accept them.
"""
weight_entries, expected = [], {}
for index, key in enumerate(BANDWIDTH_WEIGHT_ENTRIES):
weight_entries.append('%s=%i' % (key, index - 5))
expected[key] = index - 5
document = NetworkStatusDocumentV3.create({'bandwidth-weights': ' '.join(weight_entries)})
self.assertEqual(expected, document.bandwidth_weights)
def test_bandwidth_wights_malformed(self):
"""
Provides malformed content in the 'bandwidth-wights' line.
"""
test_values = (
'Wbe',
'Wbe=',
'Wbe=a',
'Wbe=+7',
)
base_weight_entry = ' '.join(['%s=5' % e for e in BANDWIDTH_WEIGHT_ENTRIES])
for test_value in test_values:
weight_entry = base_weight_entry.replace('Wbe=5', test_value)
content = NetworkStatusDocumentV3.content({'bandwidth-weights': weight_entry})
self.assertRaises(ValueError, NetworkStatusDocumentV3, content, True)
document = NetworkStatusDocumentV3(content, False)
self.assertEqual({}, document.bandwidth_weights)
def test_bandwidth_wights_misordered(self):
"""
Check that the 'bandwidth-wights' line is rejected if out of order.
"""
weight_entry = ' '.join(['%s=5' % e for e in reversed(BANDWIDTH_WEIGHT_ENTRIES)])
content = NetworkStatusDocumentV3.content({'bandwidth-weights': weight_entry})
self.assertRaises(ValueError, NetworkStatusDocumentV3, content, True)
document = NetworkStatusDocumentV3(content, False)
self.assertEqual({}, document.bandwidth_weights)
def test_bandwidth_wights_in_vote(self):
"""
Tries adding a 'bandwidth-wights' line to a vote.
"""
weight_entry = ' '.join(['%s=5' % e for e in BANDWIDTH_WEIGHT_ENTRIES])
expected = dict([(e, 5) for e in BANDWIDTH_WEIGHT_ENTRIES])
content = NetworkStatusDocumentV3.content({'vote-status': 'vote', 'bandwidth-weights': weight_entry})
self.assertRaises(ValueError, NetworkStatusDocumentV3, content, True)
document = NetworkStatusDocumentV3(content, False)
self.assertEqual(expected, document.bandwidth_weights)
def test_microdescriptor_signature(self):
"""
The 'directory-signature' lines both with and without a defined method for
the signature format.
"""
# including the signature method field should work
document = NetworkStatusDocumentV3.create({
'network-status-version': '3 microdesc',
'directory-signature': 'sha256 14C131DFC5C6F93646BE72FA1401C02A8DF2E8B4 BF112F1C6D5543CFD0A32215ACABD4197B5279AD\n-----BEGIN SIGNATURE-----%s-----END SIGNATURE-----' % CRYPTO_BLOB,
})
self.assertEqual('sha256', document.signatures[0].method)
# excluding the method should default to sha1
document = NetworkStatusDocumentV3.create({
'network-status-version': '3 microdesc',
})
self.assertEqual('sha1', document.signatures[0].method)
def test_malformed_signature(self):
"""
Provides malformed or missing content in the 'directory-signature' line.
"""
test_values = (
'',
'\n',
'blarg',
)
for test_value in test_values:
for test_attr in range(3):
attrs = [
'14C131DFC5C6F93646BE72FA1401C02A8DF2E8B4',
'BF112F1C6D5543CFD0A32215ACABD4197B5279AD',
'-----BEGIN SIGNATURE-----%s-----END SIGNATURE-----' % CRYPTO_BLOB,
]
attrs[test_attr] = test_value
content = NetworkStatusDocumentV3.content({'directory-signature': '%s %s\n%s' % tuple(attrs)})
self.assertRaises(ValueError, NetworkStatusDocumentV3, content, True)
NetworkStatusDocumentV3(content, False) # checks that it's still parsable without validation
def test_with_router_status_entries(self):
"""
Includes router status entries within the document. This isn't to test the
RouterStatusEntry parsing but rather the inclusion of it within the
document.
"""
entry1 = RouterStatusEntryV3.create({'s': 'Fast'})
entry2 = RouterStatusEntryV3.create({
'r': 'Nightfae AWt0XNId/OU2xX5xs5hVtDc5Mes 6873oEfM7fFIbxYtwllw9GPDwkA 2013-02-20 11:12:27 85.177.66.233 9001 9030',
's': 'Valid',
})
document = NetworkStatusDocumentV3.create(routers = (entry1, entry2))
self.assertTrue(entry1 in document.routers.values())
self.assertTrue(entry2 in document.routers.values())
# try with an invalid RouterStatusEntry
entry3 = RouterStatusEntryV3(RouterStatusEntryV3.content({'r': 'ugabuga'}), False)
content = NetworkStatusDocumentV3.content(routers = (entry3,))
self.assertRaises(ValueError, NetworkStatusDocumentV3, content, True)
document = NetworkStatusDocumentV3(content, False)
self.assertEqual([entry3], list(document.routers.values()))
# try including with a microdescriptor consensus
content = NetworkStatusDocumentV3.content({'network-status-version': '3 microdesc'}, routers = (entry1,))
self.assertRaises(ValueError, NetworkStatusDocumentV3, content, True)
document = NetworkStatusDocumentV3(content, False)
self.assertEqual([RouterStatusEntryMicroV3(str(entry1), False)], list(document.routers.values()))
def test_with_microdescriptor_router_status_entries(self):
"""
Includes microdescriptor flavored router status entries within the
document.
"""
entry1 = RouterStatusEntryMicroV3.create({'s': 'Fast'})
entry2 = RouterStatusEntryMicroV3.create({
'r': 'tornodeviennasil AcWxDFxrHetHYS5m6/MVt8ZN6AM 2013-03-13 22:09:13 78.142.142.246 443 80',
's': 'Valid',
})
document = NetworkStatusDocumentV3.create({'network-status-version': '3 microdesc'}, routers = (entry1, entry2))
self.assertTrue(entry1 in document.routers.values())
self.assertTrue(entry2 in document.routers.values())
# try with an invalid RouterStatusEntry
entry3 = RouterStatusEntryMicroV3(RouterStatusEntryMicroV3.content({'r': 'ugabuga'}), False)
content = NetworkStatusDocumentV3.content({'network-status-version': '3 microdesc'}, routers = (entry3,))
self.assertRaises(ValueError, NetworkStatusDocumentV3, content, True)
document = NetworkStatusDocumentV3(content, False)
self.assertEqual([entry3], list(document.routers.values()))
# try including microdescriptor entry in a normal consensus
content = NetworkStatusDocumentV3.content(routers = (entry1,))
self.assertRaises(ValueError, NetworkStatusDocumentV3, content, True)
document = NetworkStatusDocumentV3(content, False)
self.assertEqual([RouterStatusEntryV3(str(entry1), False)], list(document.routers.values()))
def test_with_directory_authorities(self):
"""
Includes a couple directory authorities in the document.
"""
for is_document_vote in (False, True):
for is_authorities_vote in (False, True):
authority1 = DirectoryAuthority.create({'contact': 'doctor jekyll'}, is_vote = is_authorities_vote)
authority2 = DirectoryAuthority.create({'contact': 'mister hyde'}, is_vote = is_authorities_vote)
vote_status = 'vote' if is_document_vote else 'consensus'
content = NetworkStatusDocumentV3.content({'vote-status': vote_status}, authorities = (authority1, authority2))
if is_document_vote == is_authorities_vote:
if is_document_vote:
# votes can only have a single authority
self.assertRaises(ValueError, NetworkStatusDocumentV3, content, True)
document = NetworkStatusDocumentV3(content, validate = False)
else:
document = NetworkStatusDocumentV3(content)
self.assertEqual((authority1, authority2), document.directory_authorities)
else:
# authority votes in a consensus or consensus authorities in a vote
self.assertRaises(ValueError, NetworkStatusDocumentV3, content, True)
document = NetworkStatusDocumentV3(content, validate = False)
self.assertEqual((authority1, authority2), document.directory_authorities)
def test_shared_randomness(self):
"""
Parses the shared randomness attributes.
"""
COMMITMENT_1 = '1 sha3-256 4CAEC248004A0DC6CE86EBD5F608C9B05500C70C AAAAAFd4/kAaklgYr4ijHZjXXy/B354jQfL31BFhhE46nuOHSPITyw== AAAAAFd4/kCpZeis3yJyr//rz8hXCeeAhHa4k3lAcAiMJd1vEMTPuw=='
COMMITMENT_2 = '1 sha3-256 598536A9DD4E6C0F18B4AD4B88C7875A0A29BA31 AAAAAFd4/kC7S920awC5/HF5RfX4fKZtYqjm6qMh9G91AcjZm13DQQ=='
authority = DirectoryAuthority.create(OrderedDict([
('shared-rand-participate', ''),
('shared-rand-commit', '%s\nshared-rand-commit %s' % (COMMITMENT_1, COMMITMENT_2)),
('shared-rand-previous-value', '8 hAQLxyt0U3gu7QR2owixRCbIltcyPrz3B0YBfUshOkE='),
('shared-rand-current-value', '7 KEIfSB7Db+ToasQIzJhbh0CtkeSePHLEehO+ams/RTU='),
]))
self.assertEqual(True, authority.is_shared_randomness_participate)
self.assertEqual(8, authority.shared_randomness_previous_reveal_count)
self.assertEqual('hAQLxyt0U3gu7QR2owixRCbIltcyPrz3B0YBfUshOkE=', authority.shared_randomness_previous_value)
self.assertEqual(7, authority.shared_randomness_current_reveal_count)
self.assertEqual('KEIfSB7Db+ToasQIzJhbh0CtkeSePHLEehO+ams/RTU=', authority.shared_randomness_current_value)
self.assertEqual(2, len(authority.shared_randomness_commitments))
first_commitment = authority.shared_randomness_commitments[0]
self.assertEqual(1, first_commitment.version)
self.assertEqual('sha3-256', first_commitment.algorithm)
self.assertEqual('4CAEC248004A0DC6CE86EBD5F608C9B05500C70C', first_commitment.identity)
self.assertEqual('AAAAAFd4/kAaklgYr4ijHZjXXy/B354jQfL31BFhhE46nuOHSPITyw==', first_commitment.commit)
self.assertEqual('AAAAAFd4/kCpZeis3yJyr//rz8hXCeeAhHa4k3lAcAiMJd1vEMTPuw==', first_commitment.reveal)
second_commitment = authority.shared_randomness_commitments[1]
self.assertEqual(1, second_commitment.version)
self.assertEqual('sha3-256', second_commitment.algorithm)
self.assertEqual('598536A9DD4E6C0F18B4AD4B88C7875A0A29BA31', second_commitment.identity)
self.assertEqual('AAAAAFd4/kC7S920awC5/HF5RfX4fKZtYqjm6qMh9G91AcjZm13DQQ==', second_commitment.commit)
self.assertEqual(None, second_commitment.reveal)
def test_shared_randomness_malformed(self):
"""
Checks shared randomness with malformed values.
"""
test_values = [
({'vote-status': 'vote', 'shared-rand-commit': 'hi sha3-256 598536A9DD4E6C0F18B4AD4B88C7875A0A29BA31 AAAAAFd4/kC7S920awC5/HF5RfX4fKZtYqjm6qMh9G91AcjZm13DQQ=='},
"The version on our 'shared-rand-commit' line wasn't an integer: hi sha3-256 598536A9DD4E6C0F18B4AD4B88C7875A0A29BA31 AAAAAFd4/kC7S920awC5/HF5RfX4fKZtYqjm6qMh9G91AcjZm13DQQ=="),
({'vote-status': 'vote', 'shared-rand-commit': 'sha3-256 598536A9DD4E6C0F18B4AD4B88C7875A0A29BA31 AAAAAFd4/kC7S920awC5/HF5RfX4fKZtYqjm6qMh9G91AcjZm13DQQ=='},
"'shared-rand-commit' must at least have a 'Version AlgName Identity Commit': sha3-256 598536A9DD4E6C0F18B4AD4B88C7875A0A29BA31 AAAAAFd4/kC7S920awC5/HF5RfX4fKZtYqjm6qMh9G91AcjZm13DQQ=="),
({'vote-status': 'vote', 'shared-rand-current-value': 'hi KEIfSB7Db+ToasQIzJhbh0CtkeSePHLEehO+ams/RTU='},
"A network status document's 'shared-rand-current-value' line must be a pair of values, the first an integer but was 'hi KEIfSB7Db+ToasQIzJhbh0CtkeSePHLEehO+ams/RTU='"),
({'vote-status': 'vote', 'shared-rand-current-value': 'KEIfSB7Db+ToasQIzJhbh0CtkeSePHLEehO+ams/RTU='},
"A network status document's 'shared-rand-current-value' line must be a pair of values, the first an integer but was 'KEIfSB7Db+ToasQIzJhbh0CtkeSePHLEehO+ams/RTU='"),
]
for attr, expected_exception in test_values:
content = DirectoryAuthority.content(attr)
self.assertRaisesRegexp(ValueError, re.escape(expected_exception), DirectoryAuthority, content, True)
authority = DirectoryAuthority(content, False)
self.assertEqual([], authority.shared_randomness_commitments)
self.assertEqual(None, authority.shared_randomness_previous_reveal_count)
self.assertEqual(None, authority.shared_randomness_previous_value)
self.assertEqual(None, authority.shared_randomness_current_reveal_count)
self.assertEqual(None, authority.shared_randomness_current_value)
def test_with_legacy_directory_authorities(self):
"""
Includes both normal authorities and those following the '-legacy' format.
"""
legacy_content = 'dir-source gabelmoo-legacy 81349FC1F2DBA2C2C11B45CB9706637D480AB913 131.188.40.189 131.188.40.189 80 443'
authority1 = DirectoryAuthority.create({'contact': 'doctor jekyll'}, is_vote = False)
authority2 = DirectoryAuthority(legacy_content, validate = True, is_vote = False)
authority3 = DirectoryAuthority.create({'contact': 'mister hyde'}, is_vote = False)
document = NetworkStatusDocumentV3.create({'vote-status': 'consensus'}, authorities = (authority1, authority2, authority3))
self.assertEqual((authority1, authority2, authority3), document.directory_authorities)
def test_authority_validation_flag_propagation(self):
"""
Includes invalid certificate content in an authority entry. This is testing
that the 'validate' flag propagages from the document to authority, and
authority to certificate classes.
"""
# make the dir-key-published field of the certiciate be malformed
authority_content = DirectoryAuthority.content(is_vote = True)
authority_content = authority_content.replace(b'dir-key-published 2', b'dir-key-published b2')
authority = DirectoryAuthority(authority_content, False, True)
content = NetworkStatusDocumentV3.content({'vote-status': 'vote'}, authorities = (authority,))
self.assertRaises(ValueError, NetworkStatusDocumentV3, content, True)
document = NetworkStatusDocumentV3(content, validate = False)
self.assertEqual((authority,), document.directory_authorities)
stem-1.6.0/test/unit/descriptor/microdescriptor.py 0000664 0001750 0001750 00000016733 13125773507 023132 0 ustar atagar atagar 0000000 0000000 """
Unit tests for stem.descriptor.microdescriptor.
"""
import unittest
import stem.descriptor
import stem.exit_policy
import test.require
from stem.util import str_type
from stem.descriptor.microdescriptor import Microdescriptor
from test.unit.descriptor import get_resource
FIRST_ONION_KEY = """\
-----BEGIN RSA PUBLIC KEY-----
MIGJAoGBAMhPQtZPaxP3ukybV5LfofKQr20/ljpRk0e9IlGWWMSTkfVvBcHsa6IM
H2KE6s4uuPHp7FqhakXAzJbODobnPHY8l1E4efyrqMQZXEQk2IMhgSNtG6YqUrVF
CxdSKSSy0mmcBe2TOyQsahlGZ9Pudxfnrey7KcfqnArEOqNH09RpAgMBAAE=
-----END RSA PUBLIC KEY-----\
"""
SECOND_ONION_KEY = """\
-----BEGIN RSA PUBLIC KEY-----
MIGJAoGBALCOxZdpMI2WO496njSQ2M7b4IgAGATqpJmH3So7lXOa25sK6o7JipgP
qQE83K/t/xsMIpxQ/hHkft3G78HkeXXFc9lVUzH0HmHwYEu0M+PMVULSkG36MfEl
7WeSZzaG+Tlnh9OySAzVyTsv1ZJsTQFHH9V8wuM0GOMo9X8DFC+NAgMBAAE=
-----END RSA PUBLIC KEY-----\
"""
THIRD_ONION_KEY = """\
-----BEGIN RSA PUBLIC KEY-----
MIGJAoGBAOWFQHxO+5kGuhwPUX5jB7wJCrTbSU0fZwolNV1t9UaDdjGDvIjIhdit
y2sMbyd9K8lbQO7x9rQjNst5ZicuaSOs854XQddSjm++vMdjYbOcVMqnKGSztvpd
w/1LVWFfhcBnsGi4JMGbmP+KUZG9A8kI9deSyJhfi35jA7UepiHHAgMBAAE=
-----END RSA PUBLIC KEY-----\
"""
class TestMicrodescriptor(unittest.TestCase):
def test_local_microdescriptors(self):
"""
Checks a small microdescriptor file with known contents.
"""
descriptor_path = get_resource('cached-microdescs')
with open(descriptor_path, 'rb') as descriptor_file:
descriptors = stem.descriptor.parse_file(descriptor_file, 'microdescriptor 1.0')
router = next(descriptors)
self.assertEqual(FIRST_ONION_KEY, router.onion_key)
self.assertEqual(None, router.ntor_onion_key)
self.assertEqual([], router.or_addresses)
self.assertEqual([], router.family)
self.assertEqual(stem.exit_policy.MicroExitPolicy('reject 1-65535'), router.exit_policy)
self.assertEqual({b'@last-listed': b'2013-02-24 00:18:36'}, router.get_annotations())
self.assertEqual([b'@last-listed 2013-02-24 00:18:36'], router.get_annotation_lines())
router = next(descriptors)
self.assertEqual(SECOND_ONION_KEY, router.onion_key)
self.assertEqual(str_type('r5572HzD+PMPBbXlZwBhsm6YEbxnYgis8vhZ1jmdI2k='), router.ntor_onion_key)
self.assertEqual([], router.or_addresses)
self.assertEqual(['$6141629FA0D15A6AEAEF3A1BEB76E64C767B3174'], router.family)
self.assertEqual(stem.exit_policy.MicroExitPolicy('reject 1-65535'), router.exit_policy)
self.assertEqual({b'@last-listed': b'2013-02-24 00:18:37'}, router.get_annotations())
self.assertEqual([b'@last-listed 2013-02-24 00:18:37'], router.get_annotation_lines())
router = next(descriptors)
self.assertEqual(THIRD_ONION_KEY, router.onion_key)
self.assertEqual(None, router.ntor_onion_key)
self.assertEqual([(str_type('2001:6b0:7:125::242'), 9001, True)], router.or_addresses)
self.assertEqual([], router.family)
self.assertEqual(stem.exit_policy.MicroExitPolicy('accept 80,443'), router.exit_policy)
self.assertEqual({b'@last-listed': b'2013-02-24 00:18:36'}, router.get_annotations())
self.assertEqual([b'@last-listed 2013-02-24 00:18:36'], router.get_annotation_lines())
def test_minimal_microdescriptor(self):
"""
Basic sanity check that we can parse a microdescriptor with minimal
attributes.
"""
desc = Microdescriptor.create()
self.assertEqual(None, desc.ntor_onion_key)
self.assertEqual([], desc.or_addresses)
self.assertEqual([], desc.family)
self.assertEqual(stem.exit_policy.MicroExitPolicy('reject 1-65535'), desc.exit_policy)
self.assertEqual(None, desc.exit_policy_v6)
self.assertEqual({}, desc.identifiers)
self.assertEqual(None, desc.identifier_type)
self.assertEqual(None, desc.identifier)
self.assertEqual({}, desc.protocols)
self.assertEqual([], desc.get_unrecognized_lines())
@test.require.cryptography
def test_descriptor_signing(self):
self.assertRaisesRegexp(NotImplementedError, 'Signing of Microdescriptor not implemented', Microdescriptor.create, sign = True)
def test_unrecognized_line(self):
"""
Includes unrecognized content in the descriptor.
"""
desc = Microdescriptor.create({'pepperjack': 'is oh so tasty!'})
self.assertEqual(['pepperjack is oh so tasty!'], desc.get_unrecognized_lines())
def test_proceeding_line(self):
"""
Includes a line prior to the 'onion-key' entry.
"""
desc_text = b'family Amunet1\n' + Microdescriptor.content()
self.assertRaises(ValueError, Microdescriptor, desc_text, True)
desc = Microdescriptor(desc_text, validate = False)
self.assertEqual(['Amunet1'], desc.family)
def test_a_line(self):
"""
Sanity test with both an IPv4 and IPv6 address.
"""
desc_text = Microdescriptor.content()
desc_text += b'\na 10.45.227.253:9001'
desc_text += b'\na [fd9f:2e19:3bcf::02:9970]:9001'
expected = [
('10.45.227.253', 9001, False),
('fd9f:2e19:3bcf::02:9970', 9001, True),
]
desc = Microdescriptor(desc_text)
self.assertEqual(expected, desc.or_addresses)
def test_family(self):
"""
Check the family line.
"""
desc = Microdescriptor.create({'family': 'Amunet1 Amunet2 Amunet3'})
self.assertEqual(['Amunet1', 'Amunet2', 'Amunet3'], desc.family)
# try multiple family lines
desc_text = Microdescriptor.content()
desc_text += b'\nfamily Amunet1'
desc_text += b'\nfamily Amunet2'
self.assertRaises(ValueError, Microdescriptor, desc_text, True)
# family entries will overwrite each other
desc = Microdescriptor(desc_text, validate = False)
self.assertEqual(1, len(desc.family))
def test_exit_policy(self):
"""
Basic check for 'p' lines. The router status entries contain an identical
field so we're not investing much effort here.
"""
desc = Microdescriptor.create({'p': 'accept 80,110,143,443'})
self.assertEqual(stem.exit_policy.MicroExitPolicy('accept 80,110,143,443'), desc.exit_policy)
def test_protocols(self):
"""
Basic check for 'pr' lines.
"""
desc = Microdescriptor.create({'pr': 'Cons=1 Desc=1 DirCache=1 HSDir=1 HSIntro=3 HSRend=1 Link=1-4 LinkAuth=1 Microdesc=1 Relay=1-2'})
self.assertEqual(10, len(desc.protocols))
def test_identifier(self):
"""
Basic check for 'id' lines.
"""
desc = Microdescriptor.create({'id': 'rsa1024 Cd47okjCHD83YGzThGBDptXs9Z4'})
self.assertEqual({'rsa1024': 'Cd47okjCHD83YGzThGBDptXs9Z4'}, desc.identifiers)
self.assertEqual('rsa1024', desc.identifier_type)
self.assertEqual('Cd47okjCHD83YGzThGBDptXs9Z4', desc.identifier)
# check when there's multiple key types
desc_text = b'\n'.join((
Microdescriptor.content(),
b'id rsa1024 Cd47okjCHD83YGzThGBDptXs9Z4',
b'id ed25519 50f6ddbecdc848dcc6b818b14d1',
))
desc = Microdescriptor(desc_text, validate = True)
self.assertEqual({'rsa1024': 'Cd47okjCHD83YGzThGBDptXs9Z4', 'ed25519': '50f6ddbecdc848dcc6b818b14d1'}, desc.identifiers)
self.assertEqual('ed25519', desc.identifier_type)
self.assertEqual('50f6ddbecdc848dcc6b818b14d1', desc.identifier)
# check when there's conflicting keys
desc_text = b'\n'.join((
Microdescriptor.content(),
b'id rsa1024 Cd47okjCHD83YGzThGBDptXs9Z4',
b'id rsa1024 50f6ddbecdc848dcc6b818b14d1',
))
desc = Microdescriptor(desc_text)
self.assertEqual({}, desc.identifiers)
exc_msg = "There can only be one 'id' line per a key type, but 'rsa1024' appeared multiple times"
self.assertRaisesRegexp(ValueError, exc_msg, Microdescriptor, desc_text, validate = True)
stem-1.6.0/test/unit/descriptor/__init__.py 0000664 0001750 0001750 00000002674 13124757510 021453 0 ustar atagar atagar 0000000 0000000 """
Unit tests for stem.descriptor.
"""
import os
__all__ = [
'export',
'extrainfo_descriptor',
'microdescriptor',
'networkstatus',
'reader',
'router_status_entry',
'server_descriptor',
]
DESCRIPTOR_TEST_DATA = os.path.join(os.path.dirname(__file__), 'data')
def get_resource(filename):
"""
Provides the path for a file in our descriptor data directory.
"""
return os.path.join(DESCRIPTOR_TEST_DATA, filename)
def base_expect_invalid_attr(cls, default_attr, default_value, test, desc_attrs, attr = None, expected_value = None):
return base_expect_invalid_attr_for_text(cls, default_attr, default_value, test, cls.content(desc_attrs), attr, expected_value)
def base_expect_invalid_attr_for_text(cls, default_attr, default_prefix, test, desc_text, attr = None, expected_value = None):
"""
Asserts that construction will fail due to desc_text having a malformed
attribute. If an attr is provided then we check that it matches an expected
value when we're constructed without validation.
"""
test.assertRaises(ValueError, cls, desc_text, True)
desc = cls(desc_text, validate = False)
if attr:
# check that the invalid attribute matches the expected value when
# constructed without validation
test.assertEqual(expected_value, getattr(desc, attr))
elif default_attr and default_prefix:
test.assertTrue(getattr(desc, default_attr).startswith(default_prefix)) # check a default attribute
return desc
stem-1.6.0/test/unit/descriptor/remote.py 0000664 0001750 0001750 00000015446 13115423200 021172 0 ustar atagar atagar 0000000 0000000 """
Unit tests for stem.descriptor.remote.
"""
import io
import socket
import unittest
import stem.prereq
import stem.descriptor.remote
try:
# added in python 3.3
from unittest.mock import patch
except ImportError:
from mock import patch
# The urlopen() method is in a different location depending on if we're using
# python 2.x or 3.x. The 2to3 converter accounts for this in imports, but not
# mock annotations.
URL_OPEN = 'urllib.request.urlopen' if stem.prereq.is_python_3() else 'urllib2.urlopen'
# Output from requesting moria1's descriptor from itself...
# % curl http://128.31.0.39:9131/tor/server/fp/9695DFC35FFEB861329B9F1AB04C46397020CE31
TEST_DESCRIPTOR = b"""\
router moria1 128.31.0.34 9101 0 9131
platform Tor 0.2.5.0-alpha-dev on Linux
protocols Link 1 2 Circuit 1
published 2013-07-05 23:48:52
fingerprint 9695 DFC3 5FFE B861 329B 9F1A B04C 4639 7020 CE31
uptime 1818933
bandwidth 512000 62914560 1307929
extra-info-digest 17D0142F6EBCDF60160EB1794FA6C9717D581F8C
caches-extra-info
onion-key
-----BEGIN RSA PUBLIC KEY-----
MIGJAoGBALzd4bhz1usB7wpoaAvP+BBOnNIk7mByAKV6zvyQ0p1M09oEmxPMc3qD
AAm276oJNf0eq6KWC6YprzPWFsXEIdXSqA6RWXCII1JG/jOoy6nt478BkB8TS9I9
1MJW27ppRaqnLiTmBmM+qzrsgJGwf+onAgUKKH2GxlVgahqz8x6xAgMBAAE=
-----END RSA PUBLIC KEY-----
signing-key
-----BEGIN RSA PUBLIC KEY-----
MIGJAoGBALtJ9uD7cD7iHjqNA3AgsX9prES5QN+yFQyr2uOkxzhvunnaf6SNhzWW
bkfylnMrRm/qCz/czcjZO6N6EKHcXmypehvP566B7gAQ9vDsb+l7VZVWgXvzNc2s
tl3P7qpC08rgyJh1GqmtQTCesIDqkEyWxwToympCt09ZQRq+fIttAgMBAAE=
-----END RSA PUBLIC KEY-----
hidden-service-dir
contact 1024D/28988BF5 arma mit edu
ntor-onion-key 9ZVjNkf/iLEnD685SpC5kcDytQ7u5ViiI9JOftdbE0k=
reject *:*
router-signature
-----BEGIN SIGNATURE-----
Y8Tj2e7mPbFJbguulkPEBVYzyO57p4btpWEXvRMD6vxIh/eyn25pehg5dUVBtZlL
iO3EUE0AEYah2W9gdz8t+i3Dtr0zgqLS841GC/TyDKCm+MKmN8d098qnwK0NGF9q
01NZPuSqXM1b6hnl2espFzL7XL8XEGRU+aeg+f/ukw4=
-----END SIGNATURE-----
"""
FALLBACK_DIR_CONTENT = b"""\
/* Trial fallbacks for 0.2.8.1-alpha with ADDRESS_AND_PORT_STABLE_DAYS = 30
* This works around an issue where relays post a descriptor without a DirPort
* when restarted. If these relays stay up, they will have been up for 120 days
* by the 0.2.8 stable release -- teor */
"5.175.233.86:80 orport=443 id=5525D0429BFE5DC4F1B0E9DE47A4CFA169661E33"
" weight=43680",
"62.210.124.124:9130 orport=9101 id=2EBD117806EE43C3CC885A8F1E4DC60F207E7D3E"
" ipv6=[2001:bc8:3f23:100::1]:9101"
" weight=43680",
"""
class TestDescriptorDownloader(unittest.TestCase):
@patch(URL_OPEN)
def test_query_download(self, urlopen_mock):
"""
Check Query functionality when we successfully download a descriptor.
"""
urlopen_mock.return_value = io.BytesIO(TEST_DESCRIPTOR)
query = stem.descriptor.remote.Query(
'/tor/server/fp/9695DFC35FFEB861329B9F1AB04C46397020CE31',
'server-descriptor 1.0',
endpoints = [('128.31.0.39', 9131)],
validate = True,
)
expeced_url = 'http://128.31.0.39:9131/tor/server/fp/9695DFC35FFEB861329B9F1AB04C46397020CE31'
self.assertEqual(expeced_url, query._pick_url())
descriptors = list(query)
self.assertEqual(1, len(descriptors))
desc = descriptors[0]
self.assertEqual('moria1', desc.nickname)
self.assertEqual('128.31.0.34', desc.address)
self.assertEqual('9695DFC35FFEB861329B9F1AB04C46397020CE31', desc.fingerprint)
self.assertEqual(TEST_DESCRIPTOR.strip(), desc.get_bytes())
urlopen_mock.assert_called_once_with(expeced_url, timeout = None)
@patch(URL_OPEN)
def test_query_with_malformed_content(self, urlopen_mock):
"""
Query with malformed descriptor content.
"""
descriptor_content = b'some malformed stuff'
urlopen_mock.return_value = io.BytesIO(descriptor_content)
query = stem.descriptor.remote.Query(
'/tor/server/fp/9695DFC35FFEB861329B9F1AB04C46397020CE31',
'server-descriptor 1.0',
endpoints = [('128.31.0.39', 9131)],
validate = True,
)
# checking via the iterator
expected_error_msg = 'Content conform to being a server descriptor:\nsome malformed stuff'
descriptors = list(query)
self.assertEqual(0, len(descriptors))
self.assertEqual(ValueError, type(query.error))
self.assertEqual(expected_error_msg, str(query.error))
# check via the run() method
self.assertRaises(ValueError, query.run)
@patch(URL_OPEN)
def test_query_with_timeout(self, urlopen_mock):
urlopen_mock.side_effect = socket.timeout('connection timed out')
query = stem.descriptor.remote.Query(
'/tor/server/fp/9695DFC35FFEB861329B9F1AB04C46397020CE31',
'server-descriptor 1.0',
endpoints = [('128.31.0.39', 9131)],
fall_back_to_authority = False,
timeout = 5,
validate = True,
)
self.assertRaises(socket.timeout, query.run)
urlopen_mock.assert_called_with(
'http://128.31.0.39:9131/tor/server/fp/9695DFC35FFEB861329B9F1AB04C46397020CE31',
timeout = 5,
)
self.assertEqual(3, urlopen_mock.call_count)
@patch(URL_OPEN)
def test_can_iterate_multiple_times(self, urlopen_mock):
urlopen_mock.return_value = io.BytesIO(TEST_DESCRIPTOR)
query = stem.descriptor.remote.Query(
'/tor/server/fp/9695DFC35FFEB861329B9F1AB04C46397020CE31',
'server-descriptor 1.0',
endpoints = [('128.31.0.39', 9131)],
validate = True,
)
# check that iterating over the query provides the descriptors each time
self.assertEqual(1, len(list(query)))
self.assertEqual(1, len(list(query)))
self.assertEqual(1, len(list(query)))
def test_using_authorities_in_hash(self):
# ensure our DirectoryAuthority instances can be used in hashes
{stem.descriptor.remote.get_authorities()['moria1']: 'hello'}
def test_fallback_directories_from_cache(self):
# quick sanity test that we can load cached content
fallback_directories = stem.descriptor.remote.FallbackDirectory.from_cache()
self.assertTrue(len(fallback_directories) > 10)
self.assertEqual('5.39.92.199', fallback_directories['0BEA4A88D069753218EAAAD6D22EA87B9A1319D6'].address)
@patch(URL_OPEN)
def test_fallback_directories_from_remote(self, urlopen_mock):
urlopen_mock.return_value = io.BytesIO(FALLBACK_DIR_CONTENT)
fallback_directories = stem.descriptor.remote.FallbackDirectory.from_remote()
expected = {
'5525D0429BFE5DC4F1B0E9DE47A4CFA169661E33': stem.descriptor.remote.FallbackDirectory(
address = '5.175.233.86',
or_port = 443,
dir_port = 80,
fingerprint = '5525D0429BFE5DC4F1B0E9DE47A4CFA169661E33',
),
'2EBD117806EE43C3CC885A8F1E4DC60F207E7D3E': stem.descriptor.remote.FallbackDirectory(
address = '62.210.124.124',
or_port = 9101,
dir_port = 9130,
fingerprint = '2EBD117806EE43C3CC885A8F1E4DC60F207E7D3E',
orport_v6 = ('2001:bc8:3f23:100::1', 9101),
),
}
self.assertEqual(expected, fallback_directories)
stem-1.6.0/test/unit/descriptor/export.py 0000664 0001750 0001750 00000005767 13124757510 021243 0 ustar atagar atagar 0000000 0000000 """
Unit tests for stem.descriptor.export.
"""
import unittest
try:
from StringIO import StringIO
except ImportError:
from io import StringIO
import stem.prereq
from stem.descriptor.server_descriptor import RelayDescriptor, BridgeDescriptor
from stem.descriptor.export import export_csv, export_csv_file
class TestExport(unittest.TestCase):
def test_minimal_descriptor(self):
"""
Exports a single minimal tor server descriptor.
"""
if stem.prereq._is_python_26():
self.skipTest('(header added in python 2.7)')
return
desc = RelayDescriptor.create({
'router': 'caerSidi 71.35.133.197 9001 0 0',
'published': '2012-03-01 17:15:27',
})
desc_csv = export_csv(desc, included_fields = ('nickname', 'address', 'published'), header = False)
expected = 'caerSidi,71.35.133.197,2012-03-01 17:15:27\n'
self.assertEqual(expected, desc_csv)
desc_csv = export_csv(desc, included_fields = ('nickname', 'address', 'published'), header = True)
expected = 'nickname,address,published\n' + expected
self.assertEqual(expected, desc_csv)
def test_multiple_descriptors(self):
"""
Exports multiple descriptors, making sure that we get them back in the same
order.
"""
nicknames = ('relay1', 'relay3', 'relay2', 'caerSidi', 'zeus')
descriptors = []
for nickname in nicknames:
router_line = '%s 71.35.133.197 9001 0 0' % nickname
descriptors.append(RelayDescriptor.create({'router': router_line}))
expected = '\n'.join(nicknames) + '\n'
self.assertEqual(expected, export_csv(descriptors, included_fields = ('nickname',), header = False))
def test_file_output(self):
"""
Basic test for the export_csv_file() function, checking that it provides
the same output as export_csv().
"""
desc = RelayDescriptor.create()
desc_csv = export_csv(desc)
csv_buffer = StringIO()
export_csv_file(csv_buffer, desc)
self.assertEqual(desc_csv, csv_buffer.getvalue())
def test_excludes_private_attr(self):
"""
Checks that the default attributes for our csv output doesn't include private fields.
"""
if stem.prereq._is_python_26():
self.skipTest('(header added in python 2.7)')
return
desc = RelayDescriptor.create()
desc_csv = export_csv(desc)
self.assertTrue(',signature' in desc_csv)
self.assertFalse(',_digest' in desc_csv)
self.assertFalse(',_annotation_lines' in desc_csv)
def test_empty_input(self):
"""
Exercises when we don't provide any descriptors.
"""
self.assertEqual('', export_csv([]))
def test_invalid_attributes(self):
"""
Attempts to make a csv with attributes that don't exist.
"""
desc = RelayDescriptor.create()
self.assertRaises(ValueError, export_csv, desc, ('nickname', 'blarg!'))
def test_multiple_descriptor_types(self):
"""
Attempts to make a csv with multiple descriptor types.
"""
self.assertRaises(ValueError, export_csv, (RelayDescriptor.create(), BridgeDescriptor.create()))
stem-1.6.0/test/unit/descriptor/server_descriptor.py 0000664 0001750 0001750 00000116102 13167173763 023461 0 ustar atagar atagar 0000000 0000000 """
Unit tests for stem.descriptor.server_descriptor.
"""
import datetime
import functools
import io
import pickle
import tarfile
import time
import unittest
import stem.descriptor
import stem.descriptor.router_status_entry
import stem.descriptor.server_descriptor
import stem.exit_policy
import stem.prereq
import stem.version
import stem.util.str_tools
import test.require
from stem.util import str_type
from stem.descriptor.certificate import CertType, ExtensionType
from stem.descriptor.server_descriptor import BridgeDistribution, RelayDescriptor, BridgeDescriptor
from test.unit.descriptor import (
get_resource,
base_expect_invalid_attr,
base_expect_invalid_attr_for_text,
)
try:
# Added in 2.7
from collections import OrderedDict
except ImportError:
from stem.util.ordereddict import OrderedDict
try:
# added in python 3.3
from unittest.mock import Mock, patch
except ImportError:
from mock import Mock, patch
TARFILE_FINGERPRINTS = set([
str_type('B6D83EC2D9E18B0A7A33428F8CFA9C536769E209'),
str_type('E0BD57A11F00041A9789577C53A1B784473669E4'),
str_type('1F43EE37A0670301AD9CB555D94AFEC2C89FDE86'),
])
expect_invalid_attr = functools.partial(base_expect_invalid_attr, RelayDescriptor, 'nickname', 'Unnamed')
expect_invalid_attr_for_text = functools.partial(base_expect_invalid_attr_for_text, RelayDescriptor, 'nickname', 'Unnamed')
class TestServerDescriptor(unittest.TestCase):
def test_with_tarfile_path(self):
"""
Fetch server descriptors via parse_file() for a tarfile path.
"""
descriptors = list(stem.descriptor.parse_file(get_resource('descriptor_archive.tar')))
self.assertEqual(3, len(descriptors))
fingerprints = set([desc.fingerprint for desc in descriptors])
self.assertEqual(TARFILE_FINGERPRINTS, fingerprints)
def test_with_tarfile_object(self):
"""
Fetch server descriptors via parse_file() for a tarfile object.
"""
# TODO: When dropping python 2.6 support we can go back to using the 'with'
# keyword here.
tar_file = tarfile.open(get_resource('descriptor_archive.tar'))
descriptors = list(stem.descriptor.parse_file(tar_file))
self.assertEqual(3, len(descriptors))
fingerprints = set([desc.fingerprint for desc in descriptors])
self.assertEqual(TARFILE_FINGERPRINTS, fingerprints)
tar_file.close()
def test_metrics_descriptor(self):
"""
Parses and checks our results against a server descriptor from metrics.
"""
expected_family = set([
'$0CE3CFB1E9CC47B63EA8869813BF6FAB7D4540C1',
'$1FD187E8F69A9B74C9202DC16A25B9E7744AB9F6',
'$74FB5EFA6A46DE4060431D515DC9A790E6AD9A7C',
'$77001D8DA9BF445B0F81AA427A675F570D222E6A',
'$B6D83EC2D9E18B0A7A33428F8CFA9C536769E209',
'$D2F37F46182C23AB747787FD657E680B34EAF892',
'$E0BD57A11F00041A9789577C53A1B784473669E4',
'$E5E3E9A472EAF7BE9682B86E92305DB4C71048EF',
])
expected_onion_key = """-----BEGIN RSA PUBLIC KEY-----
MIGJAoGBAJv5IIWQ+WDWYUdyA/0L8qbIkEVH/cwryZWoIaPAzINfrw1WfNZGtBmg
skFtXhOHHqTRN4GPPrZsAIUOQGzQtGb66IQgT4tO/pj+P6QmSCCdTfhvGfgTCsC+
WPi4Fl2qryzTb3QO5r5x7T8OsG2IBUET1bLQzmtbC560SYR49IvVAgMBAAE=
-----END RSA PUBLIC KEY-----"""
expected_signing_key = """-----BEGIN RSA PUBLIC KEY-----
MIGJAoGBAKwvOXyztVKnuYvpTKt+nS3XIKeO8dVungi8qGoeS+6gkR6lDtGfBTjd
uE9UIkdAl9zi8/1Ic2wsUNHE9jiS0VgeupITGZY8YOyMJJ/xtV1cqgiWhq1dUYaq
51TOtUogtAPgXPh4J+V8HbFFIcCzIh3qCO/xXo+DSHhv7SSif1VpAgMBAAE=
-----END RSA PUBLIC KEY-----"""
expected_signature = """-----BEGIN SIGNATURE-----
dskLSPz8beUW7bzwDjR6EVNGpyoZde83Ejvau+5F2c6cGnlu91fiZN3suE88iE6e
758b9ldq5eh5mapb8vuuV3uO+0Xsud7IEOqfxdkmk0GKnUX8ouru7DSIUzUL0zqq
Qlx9HNCqCY877ztFRC624ja2ql6A2hBcuoYMbkHjcQ4=
-----END SIGNATURE-----"""
with open(get_resource('example_descriptor'), 'rb') as descriptor_file:
desc = next(stem.descriptor.parse_file(descriptor_file, 'server-descriptor 1.0'))
self.assertEqual('caerSidi', desc.nickname)
self.assertEqual('A7569A83B5706AB1B1A9CB52EFF7D2D32E4553EB', desc.fingerprint)
self.assertEqual('71.35.133.197', desc.address)
self.assertEqual(9001, desc.or_port)
self.assertEqual(None, desc.socks_port)
self.assertEqual(None, desc.dir_port)
self.assertEqual(None, desc.certificate)
self.assertEqual(None, desc.ed25519_certificate)
self.assertEqual(None, desc.ed25519_master_key)
self.assertEqual(None, desc.ed25519_signature)
self.assertEqual(b'Tor 0.2.1.30 on Linux x86_64', desc.platform)
self.assertEqual(stem.version.Version('0.2.1.30'), desc.tor_version)
self.assertEqual('Linux x86_64', desc.operating_system)
self.assertEqual(588217, desc.uptime)
self.assertEqual(datetime.datetime(2012, 3, 1, 17, 15, 27), desc.published)
self.assertEqual(b'www.atagar.com/contact', desc.contact)
self.assertEqual(['1', '2'], desc.link_protocols)
self.assertEqual(['1'], desc.circuit_protocols)
self.assertEqual(False, desc.hibernating)
self.assertEqual(False, desc.allow_single_hop_exits)
self.assertEqual(False, desc.allow_tunneled_dir_requests)
self.assertEqual(False, desc.extra_info_cache)
self.assertEqual('D225B728768D7EA4B5587C13A7A9D22EBBEE6E66', desc.extra_info_digest)
self.assertEqual(None, desc.extra_info_sha256_digest)
self.assertEqual(['2'], desc.hidden_service_dir)
self.assertEqual(BridgeDistribution.ANY, desc.bridge_distribution)
self.assertEqual(expected_family, desc.family)
self.assertEqual(153600, desc.average_bandwidth)
self.assertEqual(256000, desc.burst_bandwidth)
self.assertEqual(104590, desc.observed_bandwidth)
self.assertEqual(stem.exit_policy.ExitPolicy('reject *:*'), desc.exit_policy)
self.assertEqual(expected_onion_key, desc.onion_key)
self.assertEqual(None, desc.onion_key_crosscert)
self.assertEqual(None, desc.ntor_onion_key_crosscert)
self.assertEqual(None, desc.onion_key_crosscert)
self.assertEqual(expected_signing_key, desc.signing_key)
self.assertEqual(expected_signature, desc.signature)
self.assertEqual([], desc.get_unrecognized_lines())
self.assertEqual('2C7B27BEAB04B4E2459D89CA6D5CD1CC5F95A689', desc.digest())
def test_metrics_descriptor_multiple(self):
"""
Parses and checks our results against a server descriptor from metrics.
"""
with open(get_resource('metrics_server_desc_multiple'), 'rb') as descriptor_file:
descriptors = list(stem.descriptor.parse_file(descriptor_file, 'server-descriptor 1.0'))
self.assertEqual(2, len(descriptors))
self.assertEqual('anonion', descriptors[0].nickname)
self.assertEqual('9A5EC5BB866517E53962AF4D3E776536694B069E', descriptors[0].fingerprint)
self.assertEqual('Unnamed', descriptors[1].nickname)
self.assertEqual('5366F1D198759F8894EA6E5FF768C667F59AFD24', descriptors[1].fingerprint)
def test_old_descriptor(self):
"""
Parses a relay server descriptor from 2005.
"""
with open(get_resource('old_descriptor'), 'rb') as descriptor_file:
desc = next(stem.descriptor.parse_file(descriptor_file, 'server-descriptor 1.0', validate = True))
self.assertEqual('krypton', desc.nickname)
self.assertEqual('3E2F63E2356F52318B536A12B6445373808A5D6C', desc.fingerprint)
self.assertEqual('212.37.39.59', desc.address)
self.assertEqual(8000, desc.or_port)
self.assertEqual(None, desc.socks_port)
self.assertEqual(None, desc.dir_port)
self.assertEqual(b'Tor 0.1.0.14 on FreeBSD i386', desc.platform)
self.assertEqual(stem.version.Version('0.1.0.14'), desc.tor_version)
self.assertEqual('FreeBSD i386', desc.operating_system)
self.assertEqual(64820, desc.uptime)
self.assertEqual(datetime.datetime(2005, 12, 16, 18, 1, 3), desc.published)
self.assertEqual(None, desc.contact)
self.assertEqual(None, desc.link_protocols)
self.assertEqual(None, desc.circuit_protocols)
self.assertEqual(True, desc.hibernating)
self.assertEqual(False, desc.allow_single_hop_exits)
self.assertEqual(False, desc.allow_tunneled_dir_requests)
self.assertEqual(False, desc.extra_info_cache)
self.assertEqual(None, desc.extra_info_digest)
self.assertEqual(None, desc.extra_info_sha256_digest)
self.assertEqual(None, desc.hidden_service_dir)
self.assertEqual(BridgeDistribution.ANY, desc.bridge_distribution)
self.assertEqual(set(), desc.family)
self.assertEqual(102400, desc.average_bandwidth)
self.assertEqual(10485760, desc.burst_bandwidth)
self.assertEqual(0, desc.observed_bandwidth)
self.assertEqual(datetime.datetime(2005, 12, 16, 18, 0, 48), desc.read_history_end)
self.assertEqual(900, desc.read_history_interval)
self.assertEqual(datetime.datetime(2005, 12, 16, 18, 0, 48), desc.write_history_end)
self.assertEqual(900, desc.write_history_interval)
self.assertEqual([], desc.get_unrecognized_lines())
# The read-history and write-history lines are pretty long so just checking
# the initial contents for the line and parsed values.
read_values_start = [20774, 489973, 510022, 511163, 20949]
self.assertEqual(read_values_start, desc.read_history_values[:5])
write_values_start = [81, 8848, 8927, 8927, 83, 8848, 8931, 8929, 81, 8846]
self.assertEqual(write_values_start, desc.write_history_values[:10])
def test_non_ascii_descriptor(self):
"""
Parses a descriptor with non-ascii content.
"""
with open(get_resource('non-ascii_descriptor'), 'rb') as descriptor_file:
desc = next(stem.descriptor.parse_file(descriptor_file, 'server-descriptor 1.0', validate = True))
self.assertEqual('Coruscant', desc.nickname)
self.assertEqual('0B9821545C48E496AEED9ECC0DB506C49FF8158D', desc.fingerprint)
self.assertEqual('88.182.161.122', desc.address)
self.assertEqual(9001, desc.or_port)
self.assertEqual(None, desc.socks_port)
self.assertEqual(9030, desc.dir_port)
self.assertEqual(b'Tor 0.2.3.25 on Linux', desc.platform)
self.assertEqual(stem.version.Version('0.2.3.25'), desc.tor_version)
self.assertEqual('Linux', desc.operating_system)
self.assertEqual(259738, desc.uptime)
self.assertEqual(datetime.datetime(2013, 5, 18, 11, 16, 19), desc.published)
self.assertEqual(b'1024D/04D2E818 L\xc3\xa9na\xc3\xafc Huard ', desc.contact)
self.assertEqual(['1', '2'], desc.link_protocols)
self.assertEqual(['1'], desc.circuit_protocols)
self.assertEqual(False, desc.hibernating)
self.assertEqual(False, desc.allow_single_hop_exits)
self.assertEqual(False, desc.allow_tunneled_dir_requests)
self.assertEqual(False, desc.extra_info_cache)
self.assertEqual('56403D838DE152421CD401B8E57DAD4483A3D56B', desc.extra_info_digest)
self.assertEqual(None, desc.extra_info_sha256_digest)
self.assertEqual(['2'], desc.hidden_service_dir)
self.assertEqual(BridgeDistribution.ANY, desc.bridge_distribution)
self.assertEqual(set(), desc.family)
self.assertEqual(102400, desc.average_bandwidth)
self.assertEqual(204800, desc.burst_bandwidth)
self.assertEqual(122818, desc.observed_bandwidth)
self.assertEqual(stem.exit_policy.ExitPolicy('reject *:*'), desc.exit_policy)
self.assertEqual([], desc.get_unrecognized_lines())
# Make sure that we can get a string representation for this descriptor
# (having non-unicode content risks a UnicodeEncodeError)...
#
# https://trac.torproject.org/8265
self.assertTrue(isinstance(str(desc), str))
@test.require.cryptography
def test_descriptor_signing(self):
RelayDescriptor.create(sign = True)
self.assertRaisesRegexp(NotImplementedError, 'Signing of BridgeDescriptor not implemented', BridgeDescriptor.create, sign = True)
def test_router_status_entry(self):
"""
Tests creation of router status entries.
"""
desc_without_fingerprint = RelayDescriptor.create()
exc_msg = 'Server descriptor lacks a fingerprint. This is an optional field, but required to make a router status entry.'
self.assertRaisesRegexp(ValueError, exc_msg, desc_without_fingerprint.make_router_status_entry)
desc = RelayDescriptor.create(OrderedDict((
('router', 'caerSidi 71.35.133.197 9001 0 0'),
('published', '2012-02-29 04:03:19'),
('fingerprint', '4F0C 867D F0EF 6816 0568 C826 838F 482C EA7C FE44'),
('or-address', ['71.35.133.197:9001', '[12ab:2e19:3bcf::02:9970]:9001']),
('onion-key', '\n-----BEGIN RSA PUBLIC KEY-----%s-----END RSA PUBLIC KEY-----' % stem.descriptor.CRYPTO_BLOB),
('signing-key', '\n-----BEGIN RSA PUBLIC KEY-----%s-----END RSA PUBLIC KEY-----' % stem.descriptor.CRYPTO_BLOB),
))).make_router_status_entry()
self.assertEqual(stem.descriptor.router_status_entry.RouterStatusEntryV3, type(desc))
self.assertEqual('caerSidi', desc.nickname)
self.assertEqual('4F0C867DF0EF68160568C826838F482CEA7CFE44', desc.fingerprint)
self.assertEqual(datetime.datetime(2012, 2, 29, 4, 3, 19), desc.published)
self.assertEqual('71.35.133.197', desc.address)
self.assertEqual(9001, desc.or_port)
self.assertEqual(None, desc.dir_port)
self.assertEqual(['Fast', 'Named', 'Running', 'Stable', 'Valid'], desc.flags)
self.assertEqual(None, desc.version)
self.assertEqual(None, desc.version_line)
self.assertEqual([('71.35.133.197', 9001, False), ('12ab:2e19:3bcf::02:9970', 9001, True)], desc.or_addresses)
self.assertEqual(None, desc.identifier_type)
self.assertEqual(None, desc.identifier)
self.assertEqual('A863EFE8395C41C880782B89B850D20EDD242BDA', desc.digest)
self.assertEqual(153600, desc.bandwidth)
self.assertEqual(None, desc.measured)
self.assertEqual(False, desc.is_unmeasured)
self.assertEqual([], desc.unrecognized_bandwidth_entries)
self.assertEqual(stem.exit_policy.MicroExitPolicy('reject 1-65535'), desc.exit_policy)
self.assertEqual([], desc.microdescriptor_hashes)
def test_make_router_status_entry_with_live_descriptor(self):
"""
Tests creation of router status entries with a live server descriptor.
"""
with open(get_resource('server_descriptor_with_ed25519'), 'rb') as descriptor_file:
desc = next(stem.descriptor.parse_file(descriptor_file, validate = True)).make_router_status_entry()
self.assertEqual(stem.descriptor.router_status_entry.RouterStatusEntryV3, type(desc))
self.assertEqual('destiny', desc.nickname)
self.assertEqual('F65E0196C94DFFF48AFBF2F5F9E3E19AAE583FD0', desc.fingerprint)
self.assertEqual(datetime.datetime(2015, 8, 22, 15, 21, 45), desc.published)
self.assertEqual('94.242.246.23', desc.address)
self.assertEqual(9001, desc.or_port)
self.assertEqual(443, desc.dir_port)
self.assertEqual(['Fast', 'Named', 'Running', 'Stable', 'Valid'], desc.flags)
self.assertEqual(stem.version.Version('0.2.7.2-alpha-dev'), desc.version)
self.assertEqual('Tor 0.2.7.2-alpha-dev', desc.version_line)
self.assertEqual([('2a01:608:ffff:ff07::1:23', 9003, True)], desc.or_addresses)
self.assertEqual('ed25519', desc.identifier_type)
self.assertEqual('pbYagEQPUiNjcDp/oY2oESXkDzd8PZlr26kaR7nUkao', desc.identifier)
self.assertEqual('B5E441051D139CCD84BC765D130B01E44DAC29AD', desc.digest)
self.assertEqual(149715200, desc.bandwidth)
self.assertEqual(None, desc.measured)
self.assertEqual(False, desc.is_unmeasured)
self.assertEqual([], desc.unrecognized_bandwidth_entries)
self.assertEqual(stem.exit_policy.MicroExitPolicy('reject 25,465,587,10000,14464'), desc.exit_policy)
self.assertEqual([], desc.microdescriptor_hashes)
@patch('time.time', Mock(return_value = time.mktime(datetime.date(2010, 1, 1).timetuple())))
def test_with_ed25519(self):
"""
Parses a descriptor with a ed25519 identity key, as added by proposal 228
(cross certification onionkeys).
"""
with open(get_resource('server_descriptor_with_ed25519'), 'rb') as descriptor_file:
desc = next(stem.descriptor.parse_file(descriptor_file, validate = True))
family = set([
'$379FB450010D17078B3766C2273303C358C3A442',
'$3EB46C1D8D8B1C0BBCB6E4F08301EF68B7F5308D',
'$B0279A521375F3CB2AE210BDBFC645FDD2E1973A',
'$EC116BCB80565A408CE67F8EC3FE3B0B02C3A065',
])
self.assertEqual(1, desc.certificate.version)
self.assertEqual(CertType.SIGNING, desc.certificate.type)
self.assertEqual(datetime.datetime(2015, 8, 28, 17, 0, 0), desc.certificate.expiration)
self.assertEqual(1, desc.certificate.key_type)
self.assertTrue(desc.certificate.key.startswith(b'\xa5\xb6\x1a\x80D\x0f'))
self.assertTrue(desc.certificate.signature.startswith(b'\xc6\x8e\xd3\xae\x0b'))
self.assertEqual(1, len(desc.certificate.extensions))
self.assertTrue('bWPo2fIzo3uOywfoM' in desc.certificate.encoded)
extension = desc.certificate.extensions[0]
self.assertEqual(ExtensionType.HAS_SIGNING_KEY, extension.type)
self.assertEqual([], extension.flags)
self.assertEqual(0, extension.flag_int)
self.assertTrue(extension.data.startswith(b'g\xa6\xb5Q\xa6\xd2'))
self.assertEqual('destiny', desc.nickname)
self.assertEqual('F65E0196C94DFFF48AFBF2F5F9E3E19AAE583FD0', desc.fingerprint)
self.assertEqual('94.242.246.23', desc.address)
self.assertEqual(9001, desc.or_port)
self.assertEqual(None, desc.socks_port)
self.assertEqual(443, desc.dir_port)
self.assertTrue('bWPo2fIzo3uOywfoM' in desc.ed25519_certificate)
self.assertEqual('Z6a1UabSK+N21j6NnyM6N7jssH6DK68qa6W5uB4QpGQ', desc.ed25519_master_key)
self.assertEqual('w+cKNZTlL7vz/4WgYdFUblzJy3VdTw0mfFK4N3SPFCt20fNKt9SgiZ5V/2ai3kgGsc6oCsyUesSiYtPcTXMLCw', desc.ed25519_signature)
self.assertEqual(b'Tor 0.2.7.2-alpha-dev on Linux', desc.platform)
self.assertEqual(stem.version.Version('0.2.7.2-alpha-dev'), desc.tor_version)
self.assertEqual('Linux', desc.operating_system)
self.assertEqual(1362680, desc.uptime)
self.assertEqual(datetime.datetime(2015, 8, 22, 15, 21, 45), desc.published)
self.assertEqual(b'0x02225522 Frenn vun der Enn (FVDE) ', desc.contact)
self.assertEqual(['1', '2'], desc.link_protocols)
self.assertEqual(['1'], desc.circuit_protocols)
self.assertEqual(False, desc.hibernating)
self.assertEqual(False, desc.allow_single_hop_exits)
self.assertEqual(False, desc.allow_tunneled_dir_requests)
self.assertEqual(False, desc.extra_info_cache)
self.assertEqual('44E9B679AF0B4EB09296985BAF4066AE9CA5BB93', desc.extra_info_digest)
self.assertEqual('r+roMxhsjd1GPpn5knQoBvtE9Rhsv8zQHCqiYL6u2CA', desc.extra_info_sha256_digest)
self.assertEqual(['2'], desc.hidden_service_dir)
self.assertEqual(family, desc.family)
self.assertEqual(149715200, desc.average_bandwidth)
self.assertEqual(1048576000, desc.burst_bandwidth)
self.assertEqual(51867731, desc.observed_bandwidth)
self.assertTrue(desc.exit_policy is not None)
self.assertEqual(stem.exit_policy.MicroExitPolicy('reject 25,465,587,10000,14464'), desc.exit_policy_v6)
self.assertTrue('MIGJAoGBAKpPOe' in desc.onion_key)
self.assertTrue('iW8BqwH5VKqZai' in desc.onion_key_crosscert)
self.assertTrue('AQoABhtwAWemtV' in desc.ntor_onion_key_crosscert)
self.assertEqual('0', desc.ntor_onion_key_crosscert_sign)
self.assertTrue('MIGJAoGBAOUS7x' in desc.signing_key)
self.assertTrue('y72z1dZOYxVQVL' in desc.signature)
self.assertEqual('B5E441051D139CCD84BC765D130B01E44DAC29AD', desc.digest())
self.assertEqual([], desc.get_unrecognized_lines())
@patch('time.time', Mock(return_value = time.mktime(datetime.date(2020, 1, 1).timetuple())))
def test_with_ed25519_expired_cert(self):
"""
Parses a server descriptor with an expired ed25519 certificate
"""
desc_text = open(get_resource('bridge_descriptor_with_ed25519'), 'rb').read()
desc_iter = stem.descriptor.server_descriptor._parse_file(io.BytesIO(desc_text), validate = True)
self.assertRaises(ValueError, list, desc_iter)
def test_bridge_with_ed25519(self):
"""
Parses a bridge descriptor with ed25519.
"""
with open(get_resource('bridge_descriptor_with_ed25519'), 'rb') as descriptor_file:
desc = next(stem.descriptor.parse_file(descriptor_file, validate = True))
self.assertEqual('ChandlerObfs11', desc.nickname)
self.assertEqual('678912ABD7398DF8EFC8FA2BC7DEF610710360C4', desc.fingerprint)
self.assertEqual('10.162.85.172', desc.address)
self.assertFalse(hasattr(desc, 'ed25519_certificate'))
self.assertEqual('lgIuiAJCoXPRwWoHgG4ZAoKtmrv47aPr4AsbmESj8AA', desc.ed25519_certificate_hash)
self.assertEqual('OB/fqLD8lYmjti09R+xXH/D4S2qlizxdZqtudnsunxE', desc.router_digest_sha256)
self.assertEqual([], desc.get_unrecognized_lines())
def test_cr_in_contact_line(self):
"""
Parses a descriptor with a huge contact line containing anomalous carriage
returns ('\r' entries).
"""
with open(get_resource('cr_in_contact_line'), 'rb') as descriptor_file:
desc = next(stem.descriptor.parse_file(descriptor_file, 'server-descriptor 1.0', validate = True))
self.assertEqual('pogonip', desc.nickname)
self.assertEqual('6DABD62BC65D4E6FE620293157FC76968DAB9C9B', desc.fingerprint)
self.assertEqual('75.5.248.48', desc.address)
# the contact info block is huge so just checking the start and end,
# including some of the embedded carriage returns
contact_start = b'jie1 at pacbell dot net -----BEGIN PGP PUBLIC KEY BLOCK-----\rVersion:'
contact_end = b'YFRk3NhCY=\r=Xaw3\r-----END PGP PUBLIC KEY BLOCK-----'
self.assertTrue(desc.contact.startswith(contact_start))
self.assertTrue(desc.contact.endswith(contact_end))
def test_negative_uptime(self):
"""
Parses a descriptor where we are tolerant of a negative uptime, and another
where we shouldn't be.
"""
with open(get_resource('negative_uptime'), 'rb') as descriptor_file:
desc = next(stem.descriptor.parse_file(descriptor_file, 'server-descriptor 1.0', validate = True))
self.assertEqual('TipTor', desc.nickname)
self.assertEqual('137962D4931DBF08A24E843288B8A155D6D2AEDD', desc.fingerprint)
self.assertEqual('62.99.247.83', desc.address)
# modify the relay version so it's after when the negative uptime bug
# should appear
descriptor_contents = desc.get_bytes().replace(b'Tor 0.1.1.25', b'Tor 0.1.2.7')
self.assertRaises(ValueError, stem.descriptor.server_descriptor.RelayDescriptor, descriptor_contents, True)
def test_bridge_descriptor(self):
"""
Parses a bridge descriptor.
"""
with open(get_resource('bridge_descriptor'), 'rb') as descriptor_file:
desc = next(stem.descriptor.parse_file(descriptor_file, 'bridge-server-descriptor 1.0', validate = True))
self.assertEqual('Unnamed', desc.nickname)
self.assertEqual('4ED573582B16ACDAF6E42AA044A038F83A7F6333', desc.fingerprint)
self.assertEqual('10.18.111.71', desc.address)
self.assertEqual(9001, desc.or_port)
self.assertEqual(None, desc.socks_port)
self.assertEqual(None, desc.dir_port)
self.assertEqual(b'Tor 0.2.0.26-rc (r14597) on Linux i686', desc.platform)
self.assertEqual(stem.version.Version('0.2.0.26-rc'), desc.tor_version)
self.assertEqual('Linux i686', desc.operating_system)
self.assertEqual(204, desc.uptime)
self.assertEqual(datetime.datetime(2008, 5, 20, 19, 45, 0), desc.published)
self.assertEqual(None, desc.contact)
self.assertEqual(['1', '2'], desc.link_protocols)
self.assertEqual(['1'], desc.circuit_protocols)
self.assertEqual(False, desc.hibernating)
self.assertEqual(False, desc.allow_single_hop_exits)
self.assertEqual(False, desc.allow_tunneled_dir_requests)
self.assertEqual(True, desc.extra_info_cache)
self.assertEqual('BB1F13AA431421BEA29B840A2E33BB1C31C2990B', desc.extra_info_digest)
self.assertEqual(None, desc.extra_info_sha256_digest)
self.assertEqual(None, desc.hidden_service_dir)
self.assertEqual(BridgeDistribution.ANY, desc.bridge_distribution)
self.assertEqual(set(), desc.family)
self.assertEqual(3220480, desc.average_bandwidth)
self.assertEqual(6441984, desc.burst_bandwidth)
self.assertEqual(59408, desc.observed_bandwidth)
self.assertEqual(stem.exit_policy.ExitPolicy('reject *:*'), desc.exit_policy)
self.assertEqual('00F1CD29AD308A59A9AB5A88B49ECB46E0F215FD', desc.digest())
self.assertEqual([], desc.get_unrecognized_lines())
def test_minimal_relay_descriptor(self):
"""
Basic sanity check that we can parse a relay server descriptor with minimal
attributes.
"""
desc = RelayDescriptor.create({'router': 'caerSidi 71.35.133.197 9001 0 0'})
self.assertEqual('caerSidi', desc.nickname)
self.assertEqual('71.35.133.197', desc.address)
self.assertEqual(None, desc.fingerprint)
def test_with_opt(self):
"""
Includes an 'opt ' entry.
"""
desc = RelayDescriptor.create({'opt': 'contact www.atagar.com/contact/'})
self.assertEqual(b'www.atagar.com/contact/', desc.contact)
def test_unrecognized_line(self):
"""
Includes unrecognized content in the descriptor.
"""
desc = RelayDescriptor.create({'pepperjack': 'is oh so tasty!'})
self.assertEqual(['pepperjack is oh so tasty!'], desc.get_unrecognized_lines())
def test_proceeding_line(self):
"""
Includes a line prior to the 'router' entry.
"""
desc_text = b'hibernate 1\n' + RelayDescriptor.content()
expect_invalid_attr_for_text(self, desc_text)
def test_trailing_line(self):
"""
Includes a line after the 'router-signature' entry.
"""
desc_text = RelayDescriptor.content() + b'\nhibernate 1'
expect_invalid_attr_for_text(self, desc_text)
def test_nickname_missing(self):
"""
Constructs with a malformed router entry.
"""
expect_invalid_attr(self, {'router': ' 71.35.133.197 9001 0 0'}, 'nickname')
def test_nickname_too_long(self):
"""
Constructs with a nickname that is an invalid length.
"""
expect_invalid_attr(self, {'router': 'saberrider2008ReallyLongNickname 71.35.133.197 9001 0 0'}, 'nickname')
def test_nickname_invalid_char(self):
"""
Constructs with an invalid relay nickname.
"""
expect_invalid_attr(self, {'router': '$aberrider2008 71.35.133.197 9001 0 0'}, 'nickname')
def test_address_malformed(self):
"""
Constructs with an invalid ip address.
"""
expect_invalid_attr(self, {'router': 'caerSidi 371.35.133.197 9001 0 0'}, 'address')
def test_port_too_high(self):
"""
Constructs with an ORPort that is too large.
"""
expect_invalid_attr(self, {'router': 'caerSidi 71.35.133.197 900001 0 0'}, 'or_port')
def test_port_malformed(self):
"""
Constructs with an ORPort that isn't numeric.
"""
expect_invalid_attr(self, {'router': 'caerSidi 71.35.133.197 900a1 0 0'}, 'or_port')
def test_port_newline(self):
"""
Constructs with a newline replacing the ORPort.
"""
expect_invalid_attr(self, {'router': 'caerSidi 71.35.133.197 \n 0 0'}, 'or_port')
def test_platform_empty(self):
"""
Constructs with an empty platform entry.
"""
desc_text = RelayDescriptor.content({'platform': ''})
desc = RelayDescriptor(desc_text, validate = False)
self.assertEqual(b'', desc.platform)
# does the same but with 'platform ' replaced with 'platform'
desc_text = desc_text.replace(b'platform ', b'platform')
desc = RelayDescriptor(desc_text, validate = False)
self.assertEqual(b'', desc.platform)
def test_platform_for_node_tor(self):
"""
Parse a platform line belonging to a node-Tor relay.
"""
desc = RelayDescriptor.create({'platform': 'node-Tor 0.1.0 on Linux x86_64'})
self.assertEqual(b'node-Tor 0.1.0 on Linux x86_64', desc.platform)
self.assertEqual(stem.version.Version('0.1.0'), desc.tor_version)
self.assertEqual('Linux x86_64', desc.operating_system)
def test_protocols_no_circuit_versions(self):
"""
Constructs with a protocols line without circuit versions.
"""
expect_invalid_attr(self, {'opt': 'protocols Link 1 2'}, 'circuit_protocols')
@patch('stem.prereq.is_crypto_available', Mock(return_value = False))
def test_published_leap_year(self):
"""
Constructs with a published entry for a leap year, and when the date is
invalid.
"""
expect_invalid_attr(self, {'published': '2011-02-29 04:03:19'}, 'published')
desc = RelayDescriptor.create({'published': '2012-02-29 04:03:19'})
self.assertEqual(datetime.datetime(2012, 2, 29, 4, 3, 19), desc.published)
def test_published_no_time(self):
"""
Constructs with a published entry without a time component.
"""
expect_invalid_attr(self, {'published': '2012-01-01'}, 'published')
def test_read_and_write_history(self):
"""
Parses a read-history and write-history entry. This is now a deprecated
field for relay server descriptors but is still found in archives and
extra-info descriptors.
"""
for field in ('read-history', 'write-history'):
value = '2005-12-16 18:00:48 (900 s) 81,8848,8927,8927,83,8848'
desc = RelayDescriptor.create({'opt %s' % field: value})
if field == 'read-history':
attr = (desc.read_history_end, desc.read_history_interval, desc.read_history_values)
else:
attr = (desc.write_history_end, desc.write_history_interval, desc.write_history_values)
expected_end = datetime.datetime(2005, 12, 16, 18, 0, 48)
expected_values = [81, 8848, 8927, 8927, 83, 8848]
self.assertEqual(expected_end, attr[0])
self.assertEqual(900, attr[1])
self.assertEqual(expected_values, attr[2])
def test_read_history_empty(self):
"""
Parses a read-history with an empty value.
"""
desc = RelayDescriptor.create({'opt read-history': '2005-12-17 01:23:11 (900 s) '})
self.assertEqual(datetime.datetime(2005, 12, 17, 1, 23, 11), desc.read_history_end)
self.assertEqual(900, desc.read_history_interval)
self.assertEqual([], desc.read_history_values)
@patch('stem.prereq.is_crypto_available', Mock(return_value = False))
def test_annotations(self):
"""
Checks that content before a descriptor are parsed as annotations.
"""
desc_text = b'@pepperjack very tasty\n@mushrooms not so much\n'
desc_text += RelayDescriptor.content()
desc_text += b'\ntrailing text that should be invalid, ho hum'
# running _parse_file should provide an iterator with a single descriptor
desc_iter = stem.descriptor.server_descriptor._parse_file(io.BytesIO(desc_text), validate = True)
self.assertRaises(ValueError, list, desc_iter)
desc_text = b'@pepperjack very tasty\n@mushrooms not so much\n'
desc_text += RelayDescriptor.content({'router': 'caerSidi 71.35.133.197 9001 0 0'})
desc_iter = stem.descriptor.server_descriptor._parse_file(io.BytesIO(desc_text))
desc_entries = list(desc_iter)
self.assertEqual(1, len(desc_entries))
desc = desc_entries[0]
self.assertEqual('caerSidi', desc.nickname)
self.assertEqual(b'@pepperjack very tasty', desc.get_annotation_lines()[0])
self.assertEqual(b'@mushrooms not so much', desc.get_annotation_lines()[1])
self.assertEqual({b'@pepperjack': b'very tasty', b'@mushrooms': b'not so much'}, desc.get_annotations())
self.assertEqual([], desc.get_unrecognized_lines())
def test_duplicate_field(self):
"""
Constructs with a field appearing twice.
"""
desc_text = RelayDescriptor.content({'': ''})
desc_text = desc_text.replace(b'', b'contact foo\ncontact bar')
expect_invalid_attr_for_text(self, desc_text, 'contact', b'foo')
def test_missing_required_attr(self):
"""
Test making a descriptor with a missing required attribute.
"""
for attr in stem.descriptor.server_descriptor.REQUIRED_FIELDS:
desc_text = RelayDescriptor.content(exclude = [attr])
self.assertRaises(ValueError, RelayDescriptor, desc_text, True)
# check that we can still construct it without validation
desc = RelayDescriptor(desc_text, validate = False)
# for one of them checks that the corresponding values are None
if attr == 'router':
self.assertEqual(None, desc.nickname)
self.assertEqual(None, desc.address)
self.assertEqual(None, desc.or_port)
self.assertEqual(None, desc.socks_port)
self.assertEqual(None, desc.dir_port)
def test_fingerprint_invalid(self):
"""
Checks that, with a correctly formed fingerprint, we'll fail validation if
it doesn't match the hash of our signing key.
"""
fingerprint = '4F0C 867D F0EF 6816 0568 C826 838F 482C EA7C FE45'
expect_invalid_attr(self, {'opt fingerprint': fingerprint}, 'fingerprint', fingerprint.replace(' ', ''))
def test_with_bridge_distribution(self):
"""
Include a preferred method of bridge distribution.
"""
desc = RelayDescriptor.create({'bridge-distribution-request': 'email'})
self.assertEqual(BridgeDistribution.EMAIL, desc.bridge_distribution)
def test_ipv6_policy(self):
"""
Checks a 'ipv6-policy' line.
"""
desc = RelayDescriptor.create({'ipv6-policy': 'accept 22-23,53,80,110'})
self.assertEqual(stem.exit_policy.MicroExitPolicy('accept 22-23,53,80,110'), desc.exit_policy_v6)
def test_extrainfo_sha256_digest(self):
"""
Extrainfo descriptor line with both a hex and base64 encoded sha256 digest.
"""
desc = RelayDescriptor.create({'extra-info-digest': '03272BF7C68484AFBDA508DAE3734D809E4A5BC4 DWMz1AEdqPlcroubwx3lPEoGbT+oX7S2BH653sPIqfI'})
self.assertEqual('03272BF7C68484AFBDA508DAE3734D809E4A5BC4', desc.extra_info_digest)
self.assertEqual('DWMz1AEdqPlcroubwx3lPEoGbT+oX7S2BH653sPIqfI', desc.extra_info_sha256_digest)
def test_protocols(self):
"""
Checks a 'proto' line.
"""
desc = RelayDescriptor.create({'proto': 'Cons=1 Desc=1 DirCache=1 HSDir=1 HSIntro=3 HSRend=1 Link=1-4 LinkAuth=1 Microdesc=1 Relay=1-2'})
self.assertEqual({'Cons': [1], 'Desc': [1], 'DirCache': [1], 'HSDir': [1], 'HSIntro': [3], 'HSRend': [1], 'Link': [1, 2, 3, 4], 'LinkAuth': [1], 'Microdesc': [1], 'Relay': [1, 2]}, desc.protocols)
def test_protocols_with_no_mapping(self):
"""
Checks a 'proto' line when it's not key=value pairs.
"""
exc_msg = "Protocol entires are expected to be a series of 'key=value' pairs but was: proto Desc Link=1-4"
self.assertRaisesRegexp(ValueError, exc_msg, RelayDescriptor.create, {'proto': 'Desc Link=1-4'})
def test_parse_with_non_int_version(self):
"""
Checks a 'proto' line with non-numeric content.
"""
exc_msg = 'Protocol values should be a number or number range, but was: proto Desc=hi Link=1-4'
self.assertRaisesRegexp(ValueError, exc_msg, RelayDescriptor.create, {'proto': 'Desc=hi Link=1-4'})
def test_ntor_onion_key(self):
"""
Checks a 'ntor-onion-key' line.
"""
desc = RelayDescriptor.create({'ntor-onion-key': 'Od2Sj3UXFyDjwESLXk6fhatqW9z/oBL/vAKJ+tbDqUU='})
self.assertEqual('Od2Sj3UXFyDjwESLXk6fhatqW9z/oBL/vAKJ+tbDqUU=', desc.ntor_onion_key)
def test_minimal_bridge_descriptor(self):
"""
Basic sanity check that we can parse a descriptor with minimal attributes.
"""
desc = BridgeDescriptor.create()
self.assertTrue(desc.nickname.startswith('Unnamed'))
self.assertEqual(None, desc.fingerprint)
self.assertEqual('006FD96BA35E7785A6A3B8B75FE2E2435A13BDB4', desc.digest())
# check that we don't have crypto fields
self.assertRaises(AttributeError, getattr, desc, 'onion_key')
self.assertRaises(AttributeError, getattr, desc, 'signing_key')
self.assertRaises(AttributeError, getattr, desc, 'signature')
def test_bridge_unsanitized(self):
"""
Targeted check that individual unsanitized attributes will be detected.
"""
unsanitized_attr = [
{'router': 'Unnamed 75.45.227.253 9001 0 0'},
{'contact': 'Damian'},
{'or-address': '71.35.133.197:9001'},
{'or-address': '[12ab:2e19:3bcf::02:9970]:9001'},
{'onion-key': '\n-----BEGIN RSA PUBLIC KEY-----%s-----END RSA PUBLIC KEY-----' % stem.descriptor.CRYPTO_BLOB},
{'signing-key': '\n-----BEGIN RSA PUBLIC KEY-----%s-----END RSA PUBLIC KEY-----' % stem.descriptor.CRYPTO_BLOB},
{'router-signature': '\n-----BEGIN SIGNATURE-----%s-----END SIGNATURE-----' % stem.descriptor.CRYPTO_BLOB},
]
for attr in unsanitized_attr:
desc = BridgeDescriptor.create(attr)
self.assertFalse(desc.is_scrubbed())
def test_bridge_unsanitized_relay(self):
"""
Checks that parsing a normal relay descriptor as a bridge will fail due to
its unsanatized content.
"""
desc_text = RelayDescriptor.content({'router-digest': '006FD96BA35E7785A6A3B8B75FE2E2435A13BDB4'})
desc = BridgeDescriptor(desc_text)
self.assertFalse(desc.is_scrubbed())
def test_router_digest(self):
"""
Constructs with a router-digest line with both valid and invalid contents.
"""
# checks with valid content
router_digest = '068A2E28D4C934D9490303B7A645BA068DCA0504'
desc = BridgeDescriptor.create({'router-digest': router_digest})
self.assertEqual(router_digest, desc.digest())
# checks when missing
desc_text = BridgeDescriptor.content(exclude = ['router-digest'])
self.assertRaises(ValueError, BridgeDescriptor, desc_text, True)
# check that we can still construct it without validation
desc = BridgeDescriptor(desc_text, validate = False)
self.assertEqual(None, desc.digest())
# checks with invalid content
test_values = (
'',
'006FD96BA35E7785A6A3B8B75FE2E2435A13BDB44',
'006FD96BA35E7785A6A3B8B75FE2E2435A13BDB',
'006FD96BA35E7785A6A3B8B75FE2E2435A13BDBH',
)
for value in test_values:
desc_text = BridgeDescriptor.content({'router-digest': value})
self.assertRaises(ValueError, BridgeDescriptor, desc_text, True)
desc = BridgeDescriptor(desc_text, validate = False)
self.assertEqual(None, desc.digest())
def test_or_address_v4(self):
"""
Constructs a bridge descriptor with a sanatized IPv4 or-address entry.
"""
desc = BridgeDescriptor.create({'or-address': '10.45.227.253:9001'})
self.assertEqual([('10.45.227.253', 9001, False)], desc.or_addresses)
def test_or_address_v6(self):
"""
Constructs a bridge descriptor with a sanatized IPv6 or-address entry.
"""
desc = BridgeDescriptor.create({'or-address': '[fd9f:2e19:3bcf::02:9970]:9001'})
self.assertEqual([('fd9f:2e19:3bcf::02:9970', 9001, True)], desc.or_addresses)
def test_or_address_multiple(self):
"""
Constructs a bridge descriptor with multiple or-address entries and multiple ports.
"""
desc_text = b'\n'.join((
BridgeDescriptor.content(),
b'or-address 10.45.227.253:9001',
b'or-address [fd9f:2e19:3bcf::02:9970]:443',
))
expected_or_addresses = [
('10.45.227.253', 9001, False),
('fd9f:2e19:3bcf::02:9970', 443, True),
]
desc = BridgeDescriptor(desc_text)
self.assertEqual(expected_or_addresses, desc.or_addresses)
def test_pickleability(self):
"""
Checks that we can unpickle lazy loaded server descriptors.
"""
with open(get_resource('example_descriptor'), 'rb') as descriptor_file:
desc = next(stem.descriptor.parse_file(descriptor_file, 'server-descriptor 1.0'))
encoded_desc = pickle.dumps(desc)
restored_desc = pickle.loads(encoded_desc)
self.assertEqual('caerSidi', restored_desc.nickname)
self.assertEqual('A7569A83B5706AB1B1A9CB52EFF7D2D32E4553EB', restored_desc.fingerprint)
self.assertEqual('71.35.133.197', restored_desc.address)
self.assertEqual(9001, restored_desc.or_port)
self.assertEqual(None, restored_desc.socks_port)
stem-1.6.0/test/unit/descriptor/extrainfo_descriptor.py 0000664 0001750 0001750 00000070713 13125773507 024155 0 ustar atagar atagar 0000000 0000000 """
Unit tests for stem.descriptor.extrainfo_descriptor.
"""
import datetime
import functools
import re
import unittest
import stem.descriptor
import test.require
from stem.descriptor.extrainfo_descriptor import (
RelayExtraInfoDescriptor,
BridgeExtraInfoDescriptor,
DirResponse,
DirStat,
)
from test.unit.descriptor import (
get_resource,
base_expect_invalid_attr,
base_expect_invalid_attr_for_text,
)
expect_invalid_attr = functools.partial(base_expect_invalid_attr, RelayExtraInfoDescriptor, 'nickname', 'Unnamed')
expect_invalid_attr_for_text = functools.partial(base_expect_invalid_attr_for_text, RelayExtraInfoDescriptor, 'nickname', 'Unnamed')
class TestExtraInfoDescriptor(unittest.TestCase):
def test_metrics_relay_descriptor(self):
"""
Parses and checks our results against an extrainfo descriptor from metrics.
"""
descriptor_file = open(get_resource('extrainfo_relay_descriptor'), 'rb')
expected_signature = """-----BEGIN SIGNATURE-----
K5FSywk7qvw/boA4DQcqkls6Ize5vcBYfhQ8JnOeRQC9+uDxbnpm3qaYN9jZ8myj
k0d2aofcVbHr4fPQOSST0LXDrhFl5Fqo5um296zpJGvRUeO6S44U/EfJAGShtqWw
7LZqklu+gVvhMKREpchVqlAwXkWR44VENm24Hs+mT3M=
-----END SIGNATURE-----"""
desc = next(stem.descriptor.parse_file(descriptor_file, 'extra-info 1.0'))
self.assertEqual('NINJA', desc.nickname)
self.assertEqual('B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48', desc.fingerprint)
self.assertEqual(datetime.datetime(2012, 5, 5, 17, 3, 50), desc.published)
self.assertEqual(datetime.datetime(2012, 5, 5, 17, 2, 45), desc.read_history_end)
self.assertEqual(900, desc.read_history_interval)
self.assertEqual(datetime.datetime(2012, 5, 5, 17, 2, 45), desc.write_history_end)
self.assertEqual(900, desc.write_history_interval)
self.assertEqual(datetime.datetime(2012, 5, 5, 17, 2, 45), desc.dir_read_history_end)
self.assertEqual(900, desc.dir_read_history_interval)
self.assertEqual(datetime.datetime(2012, 5, 5, 17, 2, 45), desc.dir_write_history_end)
self.assertEqual(900, desc.dir_write_history_interval)
self.assertEqual(expected_signature, desc.signature)
self.assertEqual('00A57A9AAB5EA113898E2DD02A755E31AFC27227', desc.digest())
self.assertEqual([], desc.get_unrecognized_lines())
# The read-history, write-history, dirreq-read-history, and
# dirreq-write-history lines are pretty long so just checking
# the initial contents for the line and parsed values.
read_values_start = [3309568, 9216, 41984, 27648, 123904]
self.assertEqual(read_values_start, desc.read_history_values[:5])
write_values_start = [1082368, 19456, 50176, 272384, 485376]
self.assertEqual(write_values_start, desc.write_history_values[:5])
dir_read_values_start = [0, 0, 0, 0, 33792, 27648, 48128]
self.assertEqual(dir_read_values_start, desc.dir_read_history_values[:7])
dir_write_values_start = [0, 0, 0, 227328, 349184, 382976, 738304]
self.assertEqual(dir_write_values_start, desc.dir_write_history_values[:7])
def test_metrics_bridge_descriptor(self):
"""
Parses and checks our results against an extrainfo bridge descriptor from
metrics.
"""
descriptor_file = open(get_resource('extrainfo_bridge_descriptor'), 'rb')
expected_dir_v2_responses = {
DirResponse.OK: 0,
DirResponse.UNAVAILABLE: 0,
DirResponse.NOT_FOUND: 0,
DirResponse.NOT_MODIFIED: 0,
DirResponse.BUSY: 0,
}
expected_dir_v3_responses = {
DirResponse.OK: 72,
DirResponse.NOT_ENOUGH_SIGS: 0,
DirResponse.UNAVAILABLE: 0,
DirResponse.NOT_FOUND: 0,
DirResponse.NOT_MODIFIED: 0,
DirResponse.BUSY: 0,
}
desc = next(stem.descriptor.parse_file(descriptor_file, 'bridge-extra-info 1.0'))
self.assertEqual('ec2bridgereaac65a3', desc.nickname)
self.assertEqual('1EC248422B57D9C0BD751892FE787585407479A4', desc.fingerprint)
self.assertEqual(datetime.datetime(2012, 6, 8, 2, 21, 27), desc.published)
self.assertEqual(datetime.datetime(2012, 6, 8, 2, 10, 38), desc.read_history_end)
self.assertEqual(900, desc.read_history_interval)
self.assertEqual(datetime.datetime(2012, 6, 8, 2, 10, 38), desc.write_history_end)
self.assertEqual(900, desc.write_history_interval)
self.assertEqual(datetime.datetime(2012, 6, 8, 2, 10, 38), desc.dir_read_history_end)
self.assertEqual(900, desc.dir_read_history_interval)
self.assertEqual(datetime.datetime(2012, 6, 8, 2, 10, 38), desc.dir_write_history_end)
self.assertEqual(900, desc.dir_write_history_interval)
self.assertEqual('00A2AECCEAD3FEE033CFE29893387143146728EC', desc.digest())
self.assertEqual([], desc.get_unrecognized_lines())
read_values_start = [337920, 437248, 3995648, 48726016]
self.assertEqual(read_values_start, desc.read_history_values[:4])
write_values_start = [343040, 991232, 5649408, 49548288]
self.assertEqual(write_values_start, desc.write_history_values[:4])
dir_read_values_start = [0, 71680, 99328, 25600]
self.assertEqual(dir_read_values_start, desc.dir_read_history_values[:4])
dir_write_values_start = [5120, 664576, 2419712, 578560]
self.assertEqual(dir_write_values_start, desc.dir_write_history_values[:4])
self.assertEqual({}, desc.dir_v2_requests)
self.assertEqual({}, desc.dir_v3_requests)
self.assertEqual(expected_dir_v2_responses, desc.dir_v2_responses)
self.assertEqual(expected_dir_v3_responses, desc.dir_v3_responses)
self.assertEqual({}, desc.dir_v2_responses_unknown)
self.assertEqual({}, desc.dir_v2_responses_unknown)
@test.require.cryptography
def test_descriptor_signing(self):
RelayExtraInfoDescriptor.create(sign = True)
self.assertRaisesRegexp(NotImplementedError, 'Signing of BridgeExtraInfoDescriptor not implemented', BridgeExtraInfoDescriptor.create, sign = True)
def test_multiple_metrics_bridge_descriptors(self):
"""
Check that we can read bridge descriptors when there's multiple in a file.
"""
descriptor_file = open(get_resource('extrainfo_bridge_descriptor_multiple'), 'rb')
desc_list = list(stem.descriptor.parse_file(descriptor_file))
self.assertEqual(6, len(desc_list))
self.assertEqual('909B07DB17E21D263C55794AB815BF1DB195FDD9', desc_list[0].fingerprint)
self.assertEqual('7F7798A3CBB0F643B1CFCE3FD4F2B7C553764498', desc_list[1].fingerprint)
self.assertEqual('B4869206C1EEA4A090FE614155BD6942701F80F1', desc_list[2].fingerprint)
self.assertEqual('C18896EB6274DC8123491FAE1DD17E1769C54C4F', desc_list[3].fingerprint)
self.assertEqual('478B4CB438302981DE9AAF246F48DBE57F69050A', desc_list[4].fingerprint)
self.assertEqual('25D9D52A0350B42E69C8AB7CE945DB1CA38DA0CF', desc_list[5].fingerprint)
def test_with_ed25519(self):
"""
Parses a descriptor with a ed25519 identity key.
"""
with open(get_resource('extrainfo_descriptor_with_ed25519'), 'rb') as descriptor_file:
desc = next(stem.descriptor.parse_file(descriptor_file, validate = True))
self.assertEqual('silverfoxden', desc.nickname)
self.assertEqual('4970B1DC3DBC8D82D7F1E43FF44B28DBF4765A4E', desc.fingerprint)
self.assertTrue('AQQABhz0AQFcf5tGWLvPvr' in desc.ed25519_certificate)
self.assertEqual('g6Zg7Er8K7C1etmt7p20INE1ExIvMRPvhwt6sjbLqEK+EtQq8hT+86hQ1xu7cnz6bHee+Zhhmcc4JamV4eiMAw', desc.ed25519_signature)
self.assertEqual([], desc.get_unrecognized_lines())
def test_bridge_with_ed25519(self):
"""
Parses a bridge descriptor with a ed25519 identity key.
"""
with open(get_resource('bridge_extrainfo_descriptor_with_ed25519'), 'rb') as descriptor_file:
desc = next(stem.descriptor.parse_file(descriptor_file, validate = True))
self.assertEqual('Unnamed', desc.nickname)
self.assertEqual('B8AB331047F1C1637EFE07FB1B94CCC0FE0ABFFA', desc.fingerprint)
self.assertFalse(hasattr(desc, 'ed25519_certificate'))
self.assertEqual('VigmhxML9uw8CT1XeGqZ8KLMhKk6AOKnChQt24usBbI', desc.ed25519_certificate_hash)
self.assertEqual('7DSOQz9eGgjDX6GT7qcrVViK8yqJD4aoEnuhdAgYtgA', desc.router_digest_sha256)
self.assertEqual([], desc.get_unrecognized_lines())
def test_nonascii_v3_reqs(self):
"""
Malformed descriptor with non-ascii content for the 'dirreq-v3-reqs' line.
"""
with open(get_resource('unparseable/extrainfo_nonascii_v3_reqs'), 'rb') as descriptor_file:
desc_generator = stem.descriptor.parse_file(descriptor_file, 'extra-info 1.0', validate = True)
exc_msg = "'dirreq-v3-reqs' line had non-ascii content: S?=4026597208,S?=4026597208,S?=4026597208,S?=4026597208,S?=4026597208,S?=4026597208,??=4026591624,6?=4026537520,6?=4026537520,6?=4026537520,us=8"
self.assertRaisesRegexp(ValueError, re.escape(exc_msg), next, desc_generator)
def test_minimal_extrainfo_descriptor(self):
"""
Basic sanity check that we can parse an extrainfo descriptor with minimal
attributes.
"""
desc = RelayExtraInfoDescriptor.create()
self.assertTrue(desc.nickname.startswith('Unnamed'))
def test_unrecognized_line(self):
"""
Includes unrecognized content in the descriptor.
"""
desc = RelayExtraInfoDescriptor.create({'pepperjack': 'is oh so tasty!'})
self.assertEqual(['pepperjack is oh so tasty!'], desc.get_unrecognized_lines())
def test_proceeding_line(self):
"""
Includes a line prior to the 'extra-info' entry.
"""
expect_invalid_attr_for_text(self, b'exit-streams-opened port=80\n' + RelayExtraInfoDescriptor.content())
def test_trailing_line(self):
"""
Includes a line after the 'router-signature' entry.
"""
expect_invalid_attr_for_text(self, RelayExtraInfoDescriptor.content() + b'\nexit-streams-opened port=80')
def test_extrainfo_line_missing_fields(self):
"""
Checks that validation catches when the extra-info line is missing fields
and that without validation both the nickname and fingerprint are left as
None.
"""
test_entries = (
'ninja',
'ninja ',
'B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48',
' B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48',
)
for entry in test_entries:
desc = expect_invalid_attr(self, {'extra-info': entry}, 'nickname')
self.assertEqual(None, desc.nickname)
self.assertEqual(None, desc.fingerprint)
def test_geoip_db_digest(self):
"""
Parses the geoip-db-digest and geoip6-db-digest lines with valid and
invalid data.
"""
geoip_db_digest = '916A3CA8B7DF61473D5AE5B21711F35F301CE9E8'
desc = RelayExtraInfoDescriptor.create({'geoip-db-digest': geoip_db_digest})
self.assertEqual(geoip_db_digest, desc.geoip_db_digest)
desc = RelayExtraInfoDescriptor.create({'geoip6-db-digest': geoip_db_digest})
self.assertEqual(geoip_db_digest, desc.geoip6_db_digest)
test_entries = (
'',
'916A3CA8B7DF61473D5AE5B21711F35F301CE9E',
'916A3CA8B7DF61473D5AE5B21711F35F301CE9E88',
'916A3CA8B7DF61473D5AE5B21711F35F301CE9EG',
'916A3CA8B7DF61473D5AE5B21711F35F301CE9E-',
)
for entry in test_entries:
expect_invalid_attr(self, {'geoip-db-digest': entry}, 'geoip_db_digest')
expect_invalid_attr(self, {'geoip6-db-digest': entry}, 'geoip6_db_digest')
def test_cell_circuits_per_decile(self):
"""
Parses the cell-circuits-per-decile line with valid and invalid data.
"""
test_entries = (
('0', 0),
('11', 11),
)
for entry in ('0', '11', '25'):
desc = RelayExtraInfoDescriptor.create({'cell-circuits-per-decile': entry})
self.assertEqual(int(entry), desc.cell_circuits_per_decile)
test_entries = (
'',
' ',
'-5',
'blarg',
)
for entry in test_entries:
expect_invalid_attr(self, {'cell-circuits-per-decile': entry}, 'cell_circuits_per_decile')
def test_dir_response_lines(self):
"""
Parses the dirreq-v2-resp and dirreq-v3-resp lines with valid and invalid
data.
"""
for keyword in ('dirreq-v2-resp', 'dirreq-v3-resp'):
attr = keyword.replace('-', '_').replace('dirreq', 'dir').replace('resp', 'responses')
unknown_attr = attr + '_unknown'
test_value = 'ok=0,unavailable=0,not-found=984,not-modified=0,something-new=7'
desc = RelayExtraInfoDescriptor.create({keyword: test_value})
self.assertEqual(0, getattr(desc, attr)[DirResponse.OK])
self.assertEqual(0, getattr(desc, attr)[DirResponse.UNAVAILABLE])
self.assertEqual(984, getattr(desc, attr)[DirResponse.NOT_FOUND])
self.assertEqual(0, getattr(desc, attr)[DirResponse.NOT_MODIFIED])
self.assertEqual(7, getattr(desc, unknown_attr)['something-new'])
test_entries = (
'ok=-4',
'ok:4',
'ok=4.not-found=3',
)
for entry in test_entries:
desc = expect_invalid_attr(self, {keyword: entry})
self.assertEqual(None, getattr(desc, attr))
self.assertEqual(None, getattr(desc, unknown_attr))
def test_dir_stat_lines(self):
"""
Parses the dirreq-v2-direct-dl, dirreq-v3-direct-dl, dirreq-v2-tunneled-dl,
and dirreq-v3-tunneled-dl lines with valid and invalid data.
"""
for keyword in ('dirreq-v2-direct-dl', 'dirreq-v2-direct-dl', 'dirreq-v2-tunneled-dl', 'dirreq-v2-tunneled-dl'):
attr = keyword.replace('-', '_').replace('dirreq', 'dir')
unknown_attr = attr + '_unknown'
test_value = 'complete=2712,timeout=32,running=4,min=741,d1=14507,d2=22702,q1=28881,d3=38277,d4=73729,md=111455,d6=168231,d7=257218,q3=319833,d8=390507,d9=616301,something-new=11,max=29917857'
desc = RelayExtraInfoDescriptor.create({keyword: test_value})
self.assertEqual(2712, getattr(desc, attr)[DirStat.COMPLETE])
self.assertEqual(32, getattr(desc, attr)[DirStat.TIMEOUT])
self.assertEqual(4, getattr(desc, attr)[DirStat.RUNNING])
self.assertEqual(741, getattr(desc, attr)[DirStat.MIN])
self.assertEqual(14507, getattr(desc, attr)[DirStat.D1])
self.assertEqual(22702, getattr(desc, attr)[DirStat.D2])
self.assertEqual(28881, getattr(desc, attr)[DirStat.Q1])
self.assertEqual(38277, getattr(desc, attr)[DirStat.D3])
self.assertEqual(73729, getattr(desc, attr)[DirStat.D4])
self.assertEqual(111455, getattr(desc, attr)[DirStat.MD])
self.assertEqual(168231, getattr(desc, attr)[DirStat.D6])
self.assertEqual(257218, getattr(desc, attr)[DirStat.D7])
self.assertEqual(319833, getattr(desc, attr)[DirStat.Q3])
self.assertEqual(390507, getattr(desc, attr)[DirStat.D8])
self.assertEqual(616301, getattr(desc, attr)[DirStat.D9])
self.assertEqual(29917857, getattr(desc, attr)[DirStat.MAX])
self.assertEqual(11, getattr(desc, unknown_attr)['something-new'])
test_entries = (
'complete=-4',
'complete:4',
'complete=4.timeout=3',
)
for entry in test_entries:
desc = expect_invalid_attr(self, {keyword: entry})
self.assertEqual(None, getattr(desc, attr))
self.assertEqual(None, getattr(desc, unknown_attr))
def test_conn_bi_direct(self):
"""
Parses the conn-bi-direct line with valid and invalid data.
"""
desc = RelayExtraInfoDescriptor.create({'conn-bi-direct': '2012-05-03 12:07:50 (500 s) 277431,12089,0,2134'})
self.assertEqual(datetime.datetime(2012, 5, 3, 12, 7, 50), desc.conn_bi_direct_end)
self.assertEqual(500, desc.conn_bi_direct_interval)
self.assertEqual(277431, desc.conn_bi_direct_below)
self.assertEqual(12089, desc.conn_bi_direct_read)
self.assertEqual(0, desc.conn_bi_direct_write)
self.assertEqual(2134, desc.conn_bi_direct_both)
test_entries = (
'',
'2012-05-03',
'2012-05-03 12:07:60 (500 s)',
'2012-05-03 12:07:50 (500 s',
'2012-05-03 12:07:50 (500s)',
'2012-05-03 12:07:50 (500 s)11',
'2012-05-03 12:07:50 (500 s) 277431,12089,0',
'2012-05-03 12:07:50 (500 s) 277431,12089,0a,2134',
'2012-05-03 12:07:50 (500 s) -277431,12089,0,2134',
)
for entry in test_entries:
desc = expect_invalid_attr(self, {'conn-bi-direct': entry})
self.assertEqual(None, desc.conn_bi_direct_end)
self.assertEqual(None, desc.conn_bi_direct_interval)
self.assertEqual(None, desc.conn_bi_direct_below)
self.assertEqual(None, desc.conn_bi_direct_read)
self.assertEqual(None, desc.conn_bi_direct_write)
self.assertEqual(None, desc.conn_bi_direct_both)
def test_percentage_lines(self):
"""
Uses valid and invalid data to tests lines of the form...
"" num%
"""
for keyword in ('dirreq-v2-share', 'dirreq-v3-share'):
attr = keyword.replace('-', '_').replace('dirreq', 'dir')
test_entries = (
('0.00%', 0.0),
('0.01%', 0.0001),
('50%', 0.5),
('100.0%', 1.0),
)
for test_value, expected_value in test_entries:
desc = RelayExtraInfoDescriptor.create({keyword: test_value})
self.assertEqual(expected_value, getattr(desc, attr))
test_entries = (
(''),
(' '),
('100'),
('-5%'),
)
for entry in test_entries:
expect_invalid_attr(self, {keyword: entry}, attr)
def test_number_list_lines(self):
"""
Uses valid and invalid data to tests lines of the form...
"" num,...,num
"""
for keyword in ('cell-processed-cells', 'cell-queued-cells', 'cell-time-in-queue'):
attr = keyword.replace('-', '_')
test_entries = (
('', []),
(' ', []),
('0,0,0', [0.0, 0.0, 0.0]),
('2.3,-4.6,8.9,16.12,32.15', [2.3, -4.6, 8.9, 16.12, 32.15]),
)
for test_value, expected_value in test_entries:
desc = RelayExtraInfoDescriptor.create({keyword: test_value})
self.assertEqual(expected_value, getattr(desc, attr))
test_entries = (
(',,11', [11.0]),
('abc,5.7,def', [5.7]),
('blarg', []),
)
for entry, expected in test_entries:
expect_invalid_attr(self, {keyword: entry}, attr, expected)
def test_timestamp_lines(self):
"""
Uses valid and invalid data to tests lines of the form...
"" YYYY-MM-DD HH:MM:SS
"""
for keyword in ('published', 'geoip-start-time'):
attr = keyword.replace('-', '_')
desc = RelayExtraInfoDescriptor.create({keyword: '2012-05-03 12:07:50'})
self.assertEqual(datetime.datetime(2012, 5, 3, 12, 7, 50), getattr(desc, attr))
test_entries = (
'',
'2012-05-03 12:07:60',
'2012-05-03 ',
'2012-05-03',
)
for entry in test_entries:
expect_invalid_attr(self, {keyword: entry}, attr)
def test_timestamp_and_interval_lines(self):
"""
Uses valid and invalid data to tests lines of the form...
"" YYYY-MM-DD HH:MM:SS (NSEC s)
"""
for keyword in ('cell-stats-end', 'entry-stats-end', 'exit-stats-end', 'bridge-stats-end', 'dirreq-stats-end'):
end_attr = keyword.replace('-', '_').replace('dirreq', 'dir')
interval_attr = end_attr[:-4] + '_interval'
desc = RelayExtraInfoDescriptor.create({keyword: '2012-05-03 12:07:50 (500 s)'})
self.assertEqual(datetime.datetime(2012, 5, 3, 12, 7, 50), getattr(desc, end_attr))
self.assertEqual(500, getattr(desc, interval_attr))
test_entries = (
'',
'2012-05-03 ',
'2012-05-03',
'2012-05-03 12:07:60 (500 s)',
'2012-05-03 12:07:50 (500s)',
'2012-05-03 12:07:50 (500 s',
'2012-05-03 12:07:50 (500 )',
)
for entry in test_entries:
desc = expect_invalid_attr(self, {'entry-stats-end': entry})
self.assertEqual(None, desc.entry_stats_end)
self.assertEqual(None, desc.entry_stats_interval)
def test_timestamp_interval_and_value_lines(self):
"""
Uses valid and invalid data to tests lines of the form...
"" YYYY-MM-DD HH:MM:SS (NSEC s) NUM,NUM,NUM,NUM,NUM...
"""
for keyword in ('read-history', 'write-history', 'dirreq-read-history', 'dirreq-write-history'):
base_attr = keyword.replace('-', '_').replace('dirreq', 'dir')
end_attr = base_attr + '_end'
interval_attr = base_attr + '_interval'
values_attr = base_attr + '_values'
desc = RelayExtraInfoDescriptor.create({keyword: '2012-05-03 12:07:50 (500 s) 50,11,5'})
self.assertEqual(datetime.datetime(2012, 5, 3, 12, 7, 50), getattr(desc, end_attr))
self.assertEqual(500, getattr(desc, interval_attr))
self.assertEqual([50, 11, 5], getattr(desc, values_attr))
for test_value in ('', ' '):
desc = RelayExtraInfoDescriptor.create({'write-history': '2012-05-03 12:07:50 (500 s)%s' % test_value})
self.assertEqual(datetime.datetime(2012, 5, 3, 12, 7, 50), desc.write_history_end)
self.assertEqual(500, desc.write_history_interval)
self.assertEqual([], desc.write_history_values)
test_entries = (
'',
'2012-05-03',
'2012-05-03 12:07:60 (500 s)',
'2012-05-03 12:07:50 (500s)',
'2012-05-03 12:07:50 (500 s',
'2012-05-03 12:07:50 (500 s)11',
)
for entry in test_entries:
desc = expect_invalid_attr(self, {'write-history': entry})
self.assertEqual(None, desc.write_history_end)
self.assertEqual(None, desc.write_history_interval)
self.assertEqual(None, desc.write_history_values)
def test_port_mapping_lines(self):
"""
Uses valid and invalid data to tests lines of the form...
"" port=N,port=N,...
"""
for keyword in ('exit-kibibytes-written', 'exit-kibibytes-read', 'exit-streams-opened'):
attr = keyword.replace('-', '_')
test_entries = (
('', {}),
('443=100,other=111', {443: 100, 'other': 111}),
('80=115533759,443=1777,995=690', {80: 115533759, 443: 1777, 995: 690}),
)
for test_value, expected_value in test_entries:
desc = RelayExtraInfoDescriptor.create({keyword: test_value})
self.assertEqual(expected_value, getattr(desc, attr))
test_entries = (
'8000000=115533759',
'-80=115533759',
'80=-115533759',
'=115533759',
'80=',
'80,115533759',
)
for entry in test_entries:
expect_invalid_attr(self, {keyword: entry}, attr)
def test_hidden_service_stats_end(self):
"""
Exercise the hidserv-stats-end, which should be a simple date.
"""
desc = RelayExtraInfoDescriptor.create({'hidserv-stats-end': '2012-05-03 12:07:50'})
self.assertEqual(datetime.datetime(2012, 5, 3, 12, 7, 50), desc.hs_stats_end)
test_entries = (
'',
'2012',
'2012-05',
'2012-05-03',
'2012-05-03 12',
'2012-05-03 12:07',
'2012-05-03 12:07:-50',
)
for entry in test_entries:
expect_invalid_attr(self, {'hidserv-stats-end': entry}, 'hs_stats_end')
def test_hidden_service_stats(self):
"""
Check the 'hidserv-rend-relayed-cells' and 'hidserv-dir-onions-seen', which
share the same format.
"""
attributes = (
('hidserv-rend-relayed-cells', 'hs_rend_cells', 'hs_rend_cells_attr'),
('hidserv-dir-onions-seen', 'hs_dir_onions_seen', 'hs_dir_onions_seen_attr'),
)
test_entries = (
'',
'hello',
' key=value',
'40 key',
'40 key value',
'40 key key=value',
)
for keyword, stat_attr, extra_attr in attributes:
# just the numeric stat (no extra attributes)
desc = RelayExtraInfoDescriptor.create({keyword: '345'})
self.assertEqual(345, getattr(desc, stat_attr))
self.assertEqual({}, getattr(desc, extra_attr))
# values can be negative (#15276)
desc = RelayExtraInfoDescriptor.create({keyword: '-345'})
self.assertEqual(-345, getattr(desc, stat_attr))
self.assertEqual({}, getattr(desc, extra_attr))
# with extra attributes
desc = RelayExtraInfoDescriptor.create({keyword: '345 spiffy=true snowmen=neat'})
self.assertEqual(345, getattr(desc, stat_attr))
self.assertEqual({'spiffy': 'true', 'snowmen': 'neat'}, getattr(desc, extra_attr))
for entry in test_entries:
expect_invalid_attr(self, {keyword: entry}, stat_attr)
expect_invalid_attr(self, {keyword: entry}, extra_attr, {})
def test_padding_counts(self):
"""
Check the 'hidserv-dir-onions-seen' lines.
"""
desc = RelayExtraInfoDescriptor.create({'padding-counts': '2017-05-17 11:02:58 (86400 s) bin-size=10000 write-drop=0 write-pad=10000 write-total=10000 read-drop=0 read-pad=10000 read-total=3780000 enabled-read-pad=0 enabled-read-total=0 enabled-write-pad=0 enabled-write-total=0 max-chanpad-timers=0 non-numeric=test'})
self.assertEqual({
'bin-size': 10000,
'write-drop': 0,
'write-pad': 10000,
'write-total': 10000,
'read-drop': 0,
'read-pad': 10000,
'read-total': 3780000,
'enabled-read-pad': 0,
'enabled-read-total': 0,
'enabled-write-pad': 0,
'enabled-write-total': 0,
'max-chanpad-timers': 0,
'non-numeric': 'test', # presently all values are ints but the spec allows for anything
}, desc.padding_counts)
self.assertEqual(datetime.datetime(2017, 5, 17, 11, 2, 58), desc.padding_counts_end)
self.assertEqual(86400, desc.padding_counts_interval)
test_entries = (
'',
'2012-05-03',
'2012-05-03 12:07:60 (500 s)',
'2012-05-03 12:07:50 (500 s',
'2012-05-03 12:07:50 (500s)',
'2012-05-03 12:07:50 (500 s)bin-size=10',
'2012-05-03 12:07:50 (500 s) bin-size',
'2012-05-03 12:07:50 (500 s) bin-size=',
)
for entry in test_entries:
desc = expect_invalid_attr(self, {'padding-counts': entry})
self.assertEqual({}, desc.padding_counts)
self.assertEqual(None, desc.padding_counts_end)
self.assertEqual(None, desc.padding_counts_interval)
def test_locale_mapping_lines(self):
"""
Uses valid and invalid data to tests lines of the form...
"" CC=N,CC=N,...
"""
for keyword in ('dirreq-v2-ips', 'dirreq-v3-ips', 'dirreq-v2-reqs', 'dirreq-v3-reqs', 'geoip-client-origins', 'entry-ips', 'bridge-ips'):
attr = keyword.replace('-', '_').replace('dirreq', 'dir').replace('reqs', 'requests')
test_entries = (
('', {}),
('uk=5,de=3,jp=2', {'uk': 5, 'de': 3, 'jp': 2}),
)
for test_value, expected_value in test_entries:
desc = RelayExtraInfoDescriptor.create({keyword: test_value})
self.assertEqual(expected_value, getattr(desc, attr))
test_entries = (
'uk=-4',
'uki=4',
'uk:4',
'uk=4.de=3',
)
for entry in test_entries:
expect_invalid_attr(self, {keyword: entry}, attr)
def test_minimal_bridge_descriptor(self):
"""
Basic sanity check that we can parse a descriptor with minimal attributes.
"""
desc = BridgeExtraInfoDescriptor.create()
self.assertEqual('ec2bridgereaac65a3', desc.nickname)
self.assertEqual([], desc.get_unrecognized_lines())
# check that we don't have crypto fields
self.assertRaises(AttributeError, getattr, desc, 'signature')
def test_bridge_ip_versions_line(self):
"""
Parses the 'bridge-ip-versions' line, which only appears in bridges.
"""
desc = BridgeExtraInfoDescriptor.create({'bridge-ip-versions': 'v4=16,v6=40'})
self.assertEqual({'v4': 16, 'v6': 40}, desc.ip_versions)
desc = BridgeExtraInfoDescriptor.create({'bridge-ip-versions': ''})
self.assertEqual({}, desc.ip_versions)
desc_text = BridgeExtraInfoDescriptor.content({'bridge-ip-versions': 'v4=24.5'})
self.assertRaises(ValueError, RelayExtraInfoDescriptor, desc_text, True)
def test_bridge_ip_transports_line(self):
"""
Parses the 'bridge-ip-transports' line, which only appears in bridges.
"""
desc = BridgeExtraInfoDescriptor.create({'bridge-ip-transports': '=16,?>=40'})
self.assertEqual({'': 16, '?>': 40}, desc.ip_transports)
desc = BridgeExtraInfoDescriptor.create({'bridge-ip-transports': ''})
self.assertEqual({}, desc.ip_transports)
desc_text = BridgeExtraInfoDescriptor.content({'bridge-ip-transports': '=24.5'})
self.assertRaises(ValueError, RelayExtraInfoDescriptor, desc_text, True)
def test_transport_line(self):
"""
Basic exercise for both a bridge and relay's transport entry.
"""
desc = BridgeExtraInfoDescriptor.create({'transport': 'obfs3'})
self.assertEqual({'obfs3': (None, None, None)}, desc.transport)
self.assertEqual([], desc.get_unrecognized_lines())
desc = RelayExtraInfoDescriptor.create({'transport': 'obfs2 83.212.96.201:33570'})
self.assertEqual({'obfs2': ('83.212.96.201', 33570, [])}, desc.transport)
self.assertEqual([], desc.get_unrecognized_lines())
# multiple transport lines
desc = BridgeExtraInfoDescriptor.create({'transport': 'obfs3\ntransport obfs4'})
self.assertEqual({'obfs3': (None, None, None), 'obfs4': (None, None, None)}, desc.transport)
self.assertEqual([], desc.get_unrecognized_lines())
stem-1.6.0/test/unit/descriptor/tordnsel.py 0000664 0001750 0001750 00000005667 13115423200 021535 0 ustar atagar atagar 0000000 0000000 """
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
TEST_DESC = b"""\
@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
"""
MALFORMED_ENTRY_1 = b"""\
ExitNode 030B22437D99B2DB2908B747B6962EAD13AB4038
Published Today!
LastStatus 2013-08-18 13:02:57
ExitAddress 46.10.211.205 2013-08-18 13:18:48
"""
MALFORMED_ENTRY_2 = b"""\
@type tordnsel 1.0
ExitNode 030B22437D99B2DB2908B747B6962EAD13AB4038
Published Today!
LastStatus 2013-08-18 13:02:57
ExitAddress 46.10.211.205 2013-08-18 Never
"""
class TestTorDNSELDescriptor(unittest.TestCase):
def test_parse_file(self):
"""
Try parsing a document via the _parse_file() function.
"""
# parse file and assert values
descriptors = list(_parse_file(io.BytesIO(TEST_DESC)))
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 = b'ExtraContent goes here\n'
descriptors = _parse_file(io.BytesIO(TEST_DESC + extra), validate = True)
self.assertRaises(ValueError, list, descriptors)
# malformed fingerprint raises value errors
extra = b'ExitNode 030B22437D99B2DB2908B747B6'
self.assertRaises(ValueError, list, _parse_file(io.BytesIO(TEST_DESC + extra), validate = True))
# malformed date raises value errors
self.assertRaises(ValueError, list, _parse_file(io.BytesIO(TEST_DESC + MALFORMED_ENTRY_1), validate = True))
# skip exit address if malformed date and validate is False
desc = next(_parse_file(io.BytesIO(MALFORMED_ENTRY_2), validate=False))
self.assertTrue(is_valid_fingerprint(desc.fingerprint))
self.assertEqual('030B22437D99B2DB2908B747B6962EAD13AB4038', desc.fingerprint)
self.assertEqual(0, len(desc.exit_addresses))
stem-1.6.0/test/unit/descriptor/data/ 0000775 0001750 0001750 00000000000 13177674754 020262 5 ustar atagar atagar 0000000 0000000 stem-1.6.0/test/unit/descriptor/data/metrics_server_desc_multiple 0000664 0001750 0001750 00000005314 13115423200 026117 0 ustar atagar atagar 0000000 0000000 @type server-descriptor 1.0
router anonion 31.54.58.167 443 0 0
platform Tor 0.2.3.22-rc on Windows XP
opt protocols Link 1 2 Circuit 1
published 2012-09-17 07:28:01
opt fingerprint 9A5E C5BB 8665 17E5 3962 AF4D 3E77 6536 694B 069E
uptime 0
bandwidth 8388608 8388608 442368
opt extra-info-digest 0DED759EDA005BFD735E91D3E2DBF22D2D685F12
onion-key
-----BEGIN RSA PUBLIC KEY-----
MIGJAoGBAM+vu//ysOgcbCwri0RnunH8xNwswnTPbbusad7im2lXJuPjMbTFlkcn
3yPxbL1aD3p/PHPfLKOOPcFDVlCma/N8tJ6CEeJn6pZcrvgsU/d1EASQyrhpXFgL
oH/+zfg4ir52dZp4gS03P16i9m4HB4YTewo62cIuZoZKq1smReMzAgMBAAE=
-----END RSA PUBLIC KEY-----
signing-key
-----BEGIN RSA PUBLIC KEY-----
MIGJAoGBALSrtq1TqL5TImtctKm/5CvZHb54n5JpkmD8jtmdajgpHF0eaSaaw/Qw
IKaoQn17etuGzbeXF1vgphv2T4xyIm04MiLOszbLGXK1Fsa5uAO+qs1bIy6mLpwg
XrYhyuddYyw1s4U5ZNBNK/0rlwo7hoIZjxcej0AvTGewFx+Rv6UlAgMBAAE=
-----END RSA PUBLIC KEY-----
opt hidden-service-dir
contact anonion at nym dot hush dot com
reject 0.0.0.0/8:*
reject 169.254.0.0/16:*
reject 127.0.0.0/8:*
reject 192.168.0.0/16:*
reject 10.0.0.0/8:*
reject 172.16.0.0/12:*
reject 31.54.58.167:*
accept *:53
accept *:80
accept *:443
accept *:3128
accept *:8080
accept *:8000
accept *:50
accept *:51
accept *:389
accept *:636
accept *:2123
accept *:2152
accept *:11370
accept *:11371
accept *:2301
accept *:2381
accept *:6518
accept *:9100
accept *:1433
accept *:1434
accept *:3389
accept *:5901
reject *:*
router-signature
-----BEGIN SIGNATURE-----
XzcWkD6QV6CHtcdKuM6mVGGl+m9JsmLtYhpPMxRANxbRLMLaI1+XkO58QS0zvl6f
0vhmifJhJSu+FV+mOkg/aCsPyleSMd8vLOjCzhVv0yCzcn252ujsjGc0HrmyPE1R
p2bQE2u2SpgLKw6oylyr6qLcZkGIWDYhnnyZTg2lXm8=
-----END SIGNATURE-----
router Unnamed 122.60.235.157 9001 0 0
platform Tor 0.2.2.37 (git-fce6eb1c44e87bc2) on Darwin x86_64
opt protocols Link 1 2 Circuit 1
published 2012-09-17 14:57:28
opt fingerprint 5366 F1D1 9875 9F88 94EA 6E5F F768 C667 F59A FD24
uptime 542717
bandwidth 32768 65536 58012
opt extra-info-digest 33C8C462BFA5A9C6E825A0A717BD63072AB68E59
onion-key
-----BEGIN RSA PUBLIC KEY-----
MIGJAoGBAKCt4oloDEzaHKzuljXGIJujbTbQfl/XZPjc/84eWp8Ka2Vmpa9DL3cV
+bO3wttgA6ZtkGUB6d6AHcfOoRT7tP/wBeQSHSDxh9OmKzGKZiHB76HNxVny8aJd
ngVPgMmolXajdnzCIAIRLdUW6SHvIinyeghLuVSwLZU+eEN5XSXrAgMBAAE=
-----END RSA PUBLIC KEY-----
signing-key
-----BEGIN RSA PUBLIC KEY-----
MIGJAoGBAJ5avJflNjXZjGukC0aACUeTMHsQEHsQKCFPwXFVBCX6gDmrfivqT+EB
efT8XTktk44EFJbCAWj65kuj+aoXBfUvrr21at4WAEJ33BgmitwCrzwkKWVX/wLM
+NMIGObvs88HYyaTsoyZ/TToLE5nPQJj77bNyabHgNvYEXyFxooJAgMBAAE=
-----END RSA PUBLIC KEY-----
opt hidden-service-dir
reject *:*
router-signature
-----BEGIN SIGNATURE-----
m0Q/dr4HvyGShKG8DdKyabcTO4lZtGMXNGX+TEM1D43+5mw4D5UgiIdaK9a2tMQA
sBGFCymv3ZHpmPRtOhSMRafwcZDYXeCSdvZXmfGzpyIRrBde3uRIJj97eqA+oOHd
5TlnK2Z0tF+M1bUD0xYHOde5a/lAaiQaw5+mley6i6M=
-----END SIGNATURE-----
stem-1.6.0/test/unit/descriptor/data/cached-consensus 0000664 0001750 0001750 00000006377 13124757510 023427 0 ustar atagar atagar 0000000 0000000 network-status-version 3
vote-status consensus
consensus-method 26
valid-after 2017-05-25 04:46:30
fresh-until 2017-05-25 04:46:40
valid-until 2017-05-25 04:46:50
voting-delay 2 2
client-versions
server-versions
known-flags Authority Exit Fast Guard HSDir NoEdConsensus Running Stable V2Dir Valid
recommended-client-protocols Cons=1-2 Desc=1-2 DirCache=1 HSDir=1 HSIntro=3 HSRend=1 Link=4 LinkAuth=1 Microdesc=1-2 Relay=2
recommended-relay-protocols Cons=1-2 Desc=1-2 DirCache=1 HSDir=1 HSIntro=3 HSRend=1 Link=4 LinkAuth=1 Microdesc=1-2 Relay=2
required-client-protocols Cons=1-2 Desc=1-2 DirCache=1 HSDir=1 HSIntro=3 HSRend=1 Link=4 LinkAuth=1 Microdesc=1-2 Relay=2
required-relay-protocols Cons=1 Desc=1 DirCache=1 HSDir=1 HSIntro=3 HSRend=1 Link=3-4 LinkAuth=1 Microdesc=1 Relay=1-2
dir-source test001a 596CD48D61FDA4E868F4AA10FF559917BE3B1A35 127.0.0.1 127.0.0.1 7001 5001
contact auth1@test.test
vote-digest 2E7177224BBA39B505F7608FF376C07884CF926F
dir-source test000a BCB380A633592C218757BEE11E630511A485658A 127.0.0.1 127.0.0.1 7000 5000
contact auth0@test.test
vote-digest 5DD41617166FFB82882A117EEFDA0353A2794DC5
r test002r NIIl+DyFR5ay3WNk5lyxibM71pY UzQp+EE8G0YCKtNlZVy+3h5tv0Q 2017-05-25 04:46:11 127.0.0.1 5002 7002
s Exit Fast Guard HSDir Running Stable V2Dir Valid
v Tor 0.3.0.7
pr Cons=1-2 Desc=1-2 DirCache=1 HSDir=1-2 HSIntro=3-4 HSRend=1-2 Link=1-4 LinkAuth=1,3 Microdesc=1-2 Relay=1-2
w Bandwidth=0 Unmeasured=1
p accept 1-65535
r test001a qgzRpIKSW809FnL4tntRtWgOiwo x8yR5mi/DBbLg46qwGQ96Dno+nc 2017-05-25 04:46:12 127.0.0.1 5001 7001
s Authority Exit Fast Guard HSDir Running V2Dir Valid
v Tor 0.3.0.7
pr Cons=1-2 Desc=1-2 DirCache=1 HSDir=1-2 HSIntro=3-4 HSRend=1-2 Link=1-4 LinkAuth=1,3 Microdesc=1-2 Relay=1-2
w Bandwidth=0 Unmeasured=1
p reject 1-65535
r test000a 3nJC+LvtNmx6kw23x1WE90pyIj4 Hg3NyPqDZoRQN8hVI5Vi6B+pofw 2017-05-25 04:46:12 127.0.0.1 5000 7000
s Authority Exit Fast Guard HSDir Running Stable V2Dir Valid
v Tor 0.3.0.7
pr Cons=1-2 Desc=1-2 DirCache=1 HSDir=1-2 HSIntro=3-4 HSRend=1-2 Link=1-4 LinkAuth=1,3 Microdesc=1-2 Relay=1-2
w Bandwidth=0 Unmeasured=1
p reject 1-65535
directory-footer
bandwidth-weights Wbd=3333 Wbe=0 Wbg=0 Wbm=10000 Wdb=10000 Web=10000 Wed=3333 Wee=10000 Weg=3333 Wem=10000 Wgb=10000 Wgd=3333 Wgg=10000 Wgm=10000 Wmb=10000 Wmd=3333 Wme=0 Wmg=0 Wmm=10000
directory-signature 596CD48D61FDA4E868F4AA10FF559917BE3B1A35 9FBF54D6A62364320308A615BF4CF6B27B254FAD
-----BEGIN SIGNATURE-----
Ho0rLojfLHs9cSPFxe6znuGuFU8BvRr6gnH1gULTjUZO0NSQvo5N628KFeAsq+pT
ElieQeV6UfwnYN1U2tomhBYv3+/p1xBxYS5oTDAITxLUYvH4pLYz09VutwFlFFtU
r/satajuOMST0M3wCCBC4Ru5o5FSklwJTPJ/tWRXDCEHv/N5ZUUkpnNdn+7tFSZ9
eFrPxPcQvB05BESo7C4/+ZnZVO/wduObSYu04eWwTEog2gkSWmsztKoXpx1QGrtG
sNL22Ws9ySGDO/ykFFyxkcuyB5A8oPyedR7DrJUfCUYyB8o+XLNwODkCFxlmtFOj
ci356fosgLiM1sVqCUkNdA==
-----END SIGNATURE-----
directory-signature BCB380A633592C218757BEE11E630511A485658A 9CA027E05B0CE1500D90DA13FFDA8EDDCD40A734
-----BEGIN SIGNATURE-----
uiAt8Ir27pYFX5fNKiVZDoa6ELVEtg/E3YeYHAnlSSRzpacLMMTN/HhF//Zvv8Zj
FKT95v77xKvE6b8s7JjB3ep6coiW4tkLqjDiONG6iDRKBmy6D+RZgf1NMxl3gWaZ
ShINORJMW9nglnBbysP7egPiX49w1igVZQLM1C2ppphK6uO5EGcK6nDJF4LVDJ7B
Fvt2yhY+gsiG3oSrhsP0snQnFfvEeUFO/r2gRVJ1FoMXUttaOCtmj268xS08eZ0m
MS+u6gHEM1dYkwpA+LzE9G4akPRhvRjMDaF0RMuLQ7pY5v44uE5OX5n/GKWRgzVZ
DH+ubl6BuqpQxYQXaHZ5iw==
-----END SIGNATURE-----
stem-1.6.0/test/unit/descriptor/data/extrainfo_relay_descriptor 0000664 0001750 0001750 00000002451 13115423200 025602 0 ustar atagar atagar 0000000 0000000 @type extra-info 1.0
extra-info NINJA B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
published 2012-05-05 17:03:50
write-history 2012-05-05 17:02:45 (900 s) 1082368,19456,50176,272384,485376,1850368,1132544,1790976,2459648,4091904,6310912,13701120,3209216,3871744,7873536,5440512,7287808,10561536,9979904,11247616,11982848,7590912,10611712,20728832,38534144,6839296,3173376,16678912
read-history 2012-05-05 17:02:45 (900 s) 3309568,9216,41984,27648,123904,2004992,364544,576512,1607680,3808256,4672512,12783616,2938880,2562048,7348224,3574784,6488064,10954752,9359360,4438016,6286336,6438912,4502528,10720256,38165504,1524736,2336768,8186880
dirreq-write-history 2012-05-05 17:02:45 (900 s) 0,0,0,227328,349184,382976,738304,1171456,850944,657408,1675264,987136,702464,1335296,587776,1941504,893952,533504,695296,6828032,6326272,1287168,6310912,10085376,1048576,5372928,894976,8610816
dirreq-read-history 2012-05-05 17:02:45 (900 s) 0,0,0,0,33792,27648,48128,46080,60416,51200,63488,64512,45056,27648,37888,48128,57344,34816,46080,50176,37888,51200,25600,33792,39936,32768,28672,30720
router-signature
-----BEGIN SIGNATURE-----
K5FSywk7qvw/boA4DQcqkls6Ize5vcBYfhQ8JnOeRQC9+uDxbnpm3qaYN9jZ8myj
k0d2aofcVbHr4fPQOSST0LXDrhFl5Fqo5um296zpJGvRUeO6S44U/EfJAGShtqWw
7LZqklu+gVvhMKREpchVqlAwXkWR44VENm24Hs+mT3M=
-----END SIGNATURE-----
stem-1.6.0/test/unit/descriptor/data/hidden_service_stealth_auth 0000664 0001750 0001750 00000006361 13115423200 025675 0 ustar atagar atagar 0000000 0000000 @type hidden-service-descriptor 1.0
rendezvous-service-descriptor ubf3xeibzlfil6s4larq6y5peup2z3oj
version 2
permanent-key
-----BEGIN RSA PUBLIC KEY-----
MIGJAoGBAL1f7GdEObH+xMhf4GsaTCtfVH7ZpanegC65jn0/Kz9wlkpF+SQdIvTu
Ha2iZB34GDT2PvTy98chSxz+E3Kv2h45pQWbrwLN3Fj4qa+klclIXWcIa7GT4Pct
YZzAvHIh9t9EAe6ejYu8I+h4yL6QNAA2bYOi1d66+qCNCAFBgAqpAgMBAAE=
-----END RSA PUBLIC KEY-----
secret-id-part jczvydhzetbpdiylj3d5nsnjvaigs7xm
publication-time 2015-02-24 20:00:00
protocol-versions 2,3
introduction-points
-----BEGIN MESSAGE-----
AgEdbps604RR6lqeyoZBzOb6+HvlL2cDt63w8vBtyRaLirq5ZD5GDnr+R0ePj71C
nC7qmRWuwBmzSdSd0lOTaSApBvIifbJksHUeT/rq03dpnnRHdHSVqSvig6bukcWJ
LgJmrRd3ES13LXVHenD3C6AZMHuL9TG+MjLO2PIHu0mFO18aAHVnWY32Dmt144IY
c2eTVZbsKobjjwCYvDf0PBZI+B6H0PZWkDX/ykYjArpLDwydeZyp+Zwj4+k0+nRr
RPlzbHYoBY9pFYDUXDXWdL+vTsgFTG0EngLGlgUWSY5U1T1Db5HfOqc7hbqklgs/
ULG8NUY1k41Wb+dleJI28/+ZOM9zOpHcegNx4Cn8UGbw/Yv3Tj+yki+TMeOtJyhK
PQP8NWq8zThiVhBrfpmVjMYkNeVNyVNoxRwS6rxCQjoLWSJit2Mpf57zY1AOvT1S
EqqFbsX+slD2Uk67imALh4pMtjX29VLIujpum3drLhoTHDszBRhIH61A2eAZqdJy
7JkJd1x/8x7U0l8xNWhnj/bhUHdt3OrCvlN+n8x6BwmMNoLF8JIsskTuGHOaAKSQ
WK3z0rHjgIrEjkQeuQtfmptiIgRB9LnNr+YahRnRR6XIOJGaIoVLVM2Uo2RG4MS1
2KC3DRJ87WdMv2yNWha3w+lWt/mOALahYrvuNMU8wEuNXSi5yCo1OKirv+d5viGe
hAgVZjRymBQF+vd30zMdOG9qXNoQFUN49JfS8z5FjWmdHRt2MHlqD2isxoeabERY
T4Q50fFH8XHkRRomKBEbCwy/4t2DiqcTOSLGOSbTtf7qlUACp2bRth/g0ySAW8X/
CaWVm53z1vdgF2+t6j1CnuIqf0dUygZ07HEAHgu3rMW0YTk04QkvR3jiKAKijvGH
3YcMJz1aJ7psWSsgiwn8a8Cs4fAcLNJcdTrnyxhQI4PMST/QLfp8nPYrhKEeifTc
vYkC4CtGuEFkWyRifIGbeD7FcjkL1zqVNu31vgo3EIVbHzylERgpgTIYBRv7aV7W
X7XAbrrgXL0zgpI0orOyPkr2KRs6CcoEqcc2MLyB6gJ5fYAm69Ige+6gWtRT6qvZ
tJXagfKZivLj73dRD6sUqTCX4tmgo7Q8WFSeNscDAVm/p4dVsw6SOoFcRgaH20yX
MBa3oLNTUNAaGbScUPx2Ja3MQS0UITwk0TFTF7hL++NhTvTp6IdgQW4DG+/bVJ3M
BRR+hsvSz5BSQQj2FUIAsJ+WoVK9ImbgsBbYxSH60jCvxTIdeh2IeUzS2T1bU9AU
jOLzcJZmNh95Nj2Qdrc8/0gin9KpgPmuPQ6CyH3TPFy88lf19v9jHUMO4SKEr7am
DAjbX3D7APKgHyZ61CkuoB3gylIRb8rRJD2ote38M6A1+04yJL/jG+PCL1UnMWdL
yJ4f4LzI9c4ksnGyl9neq0IHnA0Nlky6dmgmE+vLi6OCbEEs2v132wc5PIxRY+TW
8JWu+3wUA4tj5uQvQRqU9/lmoHG/Jxubx/HwdD9Ri17G+qX8re5sySmmq7rcZEGJ
LVrlFuvA0NdoTM4AZY23iR6trJ/Ba2Q4pQk4SfOEMSoZJmf0UbxIP0Ez6Fb+Dxzk
WKXfI+D0ScuVjzV0bs8iXTrCcynztRKndNbtpd39hGAR0rNqvnHyQGYV75bWm5dS
0S0PQ6DOzicLxjNXZFicQvwfieg9VyJikWLFLu4zAbzHnuoRk6b2KbSU4UCG/BCz
mHqz4y6GfsncsNkmFmsD5Gn9UrloWcEWgIDL05yIikL+L9DPLnNlSYtehDfxlhvh
xHzY/Rad4Nzxe62yXhSxhROLTXIolllyOFJgqZ4hBlXybBqJH7sZUll6PUpDwZdu
BK14pzMIpfxq2eYp8jI7fh4lU9YrkuSUM0Ewa7HfrltAgxMhHyaFjfINt61P9OlO
s3nuBY17+KokaSWjACkCimVLH13H5DRhfX8OBRT4LeRMUspX3cyKbccwpOmoBf4y
WPM9QXw7nQy2hwnuX6NiK5QfeCGfY64M06J2tBGcCDmjPSIcJgMcyY7jfH9yPlDt
SKyyXpZnFOJplS2v28A/1csPSGy9kk/uGN0hfFULH4VvyAgNDYzmeOd8FvrbfHH2
8BUTI/Tq2pckxwCYBWHcjSdXRAj5moCNSxCUMtK3kWFdxLFYzoiKuiZwq171qb5L
yCHMwNDIWEMeC75XSMswHaBsK6ON0UUg5oedQkOK+II9L/DVyTs3UYJOsWDfM67E
312O9/bmsoHvr+rofF7HEc74dtUAcaDGJNyNiB+O4UmWbtEpCfuLmq2vaZa9J7Y0
hXlD2pcibC9CWpKR58cRL+dyYHZGJ4VKg6OHlJlF+JBPeLzObNDz/zQuEt9aL9Ae
QByamqGDGcaVMVZ/A80fRoUUgHbh3bLoAmxLCvMbJ0YMtRujdtGm8ZD0WvLXQA/U
dNmQ6tsP6pyVorWVa/Ma5CR7Em5q7M6639T8WPcu7ETTO19MnWud2lPJ5A==
-----END MESSAGE-----
signature
-----BEGIN SIGNATURE-----
c8HgXcZesCwAzgDlE3kRYsq059yCIE7MH7r2jBHqJVYPRrtm/HF/mTUykwFPzwsY
ulcuoNlPfgGMKS8qBL4kFVZ9uR2Y6P4zLchoVS6wjL+cNYOQfeQs3sNZkiIrOjbb
590tr1/yrt0qUtITGhUGhBZVs9gvkuqaThTIXleEseI=
-----END SIGNATURE-----
stem-1.6.0/test/unit/descriptor/data/descriptor_archive.tar 0000664 0001750 0001750 00000050000 13115423200 024606 0 ustar atagar atagar 0000000 0000000 descriptor_archive/0/2/02c311d3d789f3f55c0880b5c85f3c196343552c 0000644 0001750 0001750 00000004615 11755476646 021637 0 ustar atagar atagar @type server-descriptor 1.0
router Amunet1 199.48.147.35 443 0 80
platform Tor 0.2.2.34 (git-f0e1ee98af443107) on Linux x86_64
opt protocols Link 1 2 Circuit 1
published 2012-03-02 09:06:14
opt fingerprint B6D8 3EC2 D9E1 8B0A 7A33 428F 8CFA 9C53 6769 E209
uptime 2370398
bandwidth 104857600 209715200 8610541
opt extra-info-digest F1A0DE598AAD783548C6185909585FB9CDF736FE
onion-key
-----BEGIN RSA PUBLIC KEY-----
MIGJAoGBALEN3N09XeXVQgfQxku+swowfQiUoeX22HxneJYdHWBxgpY40Vke+Fa8
lDr2Xw2PVHChgmUl4xEXF2NPt6UD03RDZHfHVMKRYpnpJmJBaI58rJkw7DjHgDuP
FSlbXDhQMWbFT8bvbhqaaTMvlfQx1cnqIhc+ZAREdeTjlJysqYndAgMBAAE=
-----END RSA PUBLIC KEY-----
signing-key
-----BEGIN RSA PUBLIC KEY-----
MIGJAoGBANOGfEY3n/HmoyVAbgPC1OURB9PXR/nUQPdbeAVrDA17FXbw98OF7otR
9nWfi1vPVn8H0ODXrn7YEaqxYDjSMvacchNCKC4vFxgkpdVEzSsiYBLYsxGXaDys
4FOPQAix2EZaEKon4tuUMQAHSjLcjH5VBkixh4WXj9C4h1l9YMmnAgMBAAE=
-----END RSA PUBLIC KEY-----
family $0CE3CFB1E9CC47B63EA8869813BF6FAB7D4540C1 $1FD187E8F69A9B74C9202DC16A25B9E7744AB9F6 $74FB5EFA6A46DE4060431D515DC9A790E6AD9A7C $77001D8DA9BF445B0F81AA427A675F570D222E6A $A7569A83B5706AB1B1A9CB52EFF7D2D32E4553EB $D2F37F46182C23AB747787FD657E680B34EAF892 $E0BD57A11F00041A9789577C53A1B784473669E4 $E5E3E9A472EAF7BE9682B86E92305DB4C71048EF
opt hidden-service-dir
contact tor-admin on formlessnetworking of net
reject 0.0.0.0/8:*
reject 169.254.0.0/16:*
reject 127.0.0.0/8:*
reject 192.168.0.0/16:*
reject 10.0.0.0/8:*
reject 172.16.0.0/12:*
reject 199.48.147.35:*
accept *:20-23
accept *:43
accept *:53
accept *:79-81
accept *:88
accept *:110
accept *:143
accept *:194
accept *:443
accept *:464-465
accept *:543-544
accept *:563
accept *:587
accept *:706
accept *:749
accept *:873
accept *:902-904
accept *:981
accept *:989-993
accept *:995
accept *:1194
accept *:1220
accept *:1293
accept *:1500
accept *:1723
accept *:1863
accept *:2082-2083
accept *:2086-2087
accept *:2095-2096
accept *:3389
accept *:3690
accept *:4321
accept *:4643
accept *:5050
accept *:5190
accept *:5222-5223
accept *:5900
accept *:6666-6667
accept *:6679
accept *:6697
accept *:8000
accept *:8008
accept *:8080
accept *:8087-8088
accept *:8443
accept *:8888
accept *:9418
accept *:9999
accept *:10000
accept *:19638
reject *:*
router-signature
-----BEGIN SIGNATURE-----
TolLBBD13jT8USQ2AoZ3O7j73bknqj4WtqENF+akuJh3QKCBIHIUagcRH4q/Xt6y
mNx8A/3ip/sygZwPEX080JFt03rJjF50hK/lpM2zvpn6Fw6lHFh65DV7wSjFfk7Y
cqG4hkUqinotSj413JFhVihJ6bz/gOOmovU1ufpDuJw=
-----END SIGNATURE-----
descriptor_archive/1/b/1bb798cae15e21479db0bc700767eee4733e9d4a 0000644 0001750 0001750 00000004667 11755476655 022307 0 ustar atagar atagar @type server-descriptor 1.0
router Amunet11 199.48.147.45 22 0 80
platform Tor 0.2.2.34 (git-f0e1ee98af443107) on Linux x86_64
opt protocols Link 1 2 Circuit 1
published 2012-03-01 02:58:10
opt fingerprint 1F43 EE37 A067 0301 AD9C B555 D94A FEC2 C89F DE86
uptime 2261883
bandwidth 104857600 209715200 9197333
opt extra-info-digest 93B4C7E9205430DED49FD0BEA30F647BD20DB55D
onion-key
-----BEGIN RSA PUBLIC KEY-----
MIGJAoGBALFhXdB6tJJw5z5kwRhZjYtoysIaWv5GA9mRnz5CaI9g/kH2TZS6/z7p
zEXrtsDd6RzSTRXBdCFbXwvt4ftLqtZj+3ezGaerfyFNNgHp37Wsqol+CYVRpam1
VQTjYMUXGplUbgc4801WJKZ+sH7Kq4V2pkJiAFmNsVWsoToQHu6tAgMBAAE=
-----END RSA PUBLIC KEY-----
signing-key
-----BEGIN RSA PUBLIC KEY-----
MIGJAoGBAO12H34IhpknafbmuKCO7kOmsxhGv86KQffFoFETKMLDduxnfyKH9d6H
AK4+Ries93WxGIZFxTc5BSaGcIfpXTY58kYS0o/OoUBR2nbTzlwrjCcmO0+5x7D5
xrbm9y/Mt/cCm98T4XYQXR45FCdVo7TU0xmrsz0YopoxhhSzy/nxAgMBAAE=
-----END RSA PUBLIC KEY-----
family $0CE3CFB1E9CC47B63EA8869813BF6FAB7D4540C1 $1FD187E8F69A9B74C9202DC16A25B9E7744AB9F6 $74FB5EFA6A46DE4060431D515DC9A790E6AD9A7C $77001D8DA9BF445B0F81AA427A675F570D222E6A $A7569A83B5706AB1B1A9CB52EFF7D2D32E4553EB $B6D83EC2D9E18B0A7A33428F8CFA9C536769E209 $D2F37F46182C23AB747787FD657E680B34EAF892 $E0BD57A11F00041A9789577C53A1B784473669E4 $E5E3E9A472EAF7BE9682B86E92305DB4C71048EF
opt hidden-service-dir
contact tor-admin on formlessnetworking of net
reject 0.0.0.0/8:*
reject 169.254.0.0/16:*
reject 127.0.0.0/8:*
reject 192.168.0.0/16:*
reject 10.0.0.0/8:*
reject 172.16.0.0/12:*
reject 199.48.147.45:*
accept *:20-23
accept *:43
accept *:53
accept *:79-81
accept *:88
accept *:110
accept *:143
accept *:194
accept *:443
accept *:464-465
accept *:543-544
accept *:563
accept *:587
accept *:706
accept *:749
accept *:873
accept *:902-904
accept *:981
accept *:989-993
accept *:995
accept *:1194
accept *:1220
accept *:1293
accept *:1500
accept *:1723
accept *:1863
accept *:2082-2083
accept *:2086-2087
accept *:2095-2096
accept *:3389
accept *:3690
accept *:4321
accept *:4643
accept *:5050
accept *:5190
accept *:5222-5223
accept *:5900
accept *:6666-6667
accept *:6679
accept *:6697
accept *:8000
accept *:8008
accept *:8080
accept *:8087-8088
accept *:8443
accept *:8888
accept *:9418
accept *:9999
accept *:10000
accept *:19638
reject *:*
router-signature
-----BEGIN SIGNATURE-----
q+N3s+00x5WNzPjU/bOVDBOoZZVGiMExMW+gBmqgzHxJQs3lLshoEEf8eNOuTcfr
OTu/sT4pMO106QtmrQfyE1npAVzm75AINnN5tzj0MsfnvusSyUxIjHHAAyb2M9Mg
j4QHz7OlqNndBAVvdfQVydSefSKKUV+eGUJpeeA0Pls=
-----END SIGNATURE-----
descriptor_archive/1/b/1ef75fef564180d8b3f72c6f8635ff0cd855f92c 0000644 0001750 0001750 00000004616 11755476662 022331 0 ustar atagar atagar @type server-descriptor 1.0
router Amunet3 199.48.147.37 443 0 80
platform Tor 0.2.2.34 (git-f0e1ee98af443107) on Linux x86_64
opt protocols Link 1 2 Circuit 1
published 2012-03-01 02:58:38
opt fingerprint E0BD 57A1 1F00 041A 9789 577C 53A1 B784 4736 69E4
uptime 2261941
bandwidth 104857600 209715200 11605015
opt extra-info-digest D4397EC97CD5187EB08A5FFBE6E1A4BC267A389F
onion-key
-----BEGIN RSA PUBLIC KEY-----
MIGJAoGBAKuPBv5zCgw2y6qBW+Nv5ra4C74l3kleGoSMamXcs4+uu0ibybi8pgxm
bq3RXukufp0ZcH+bHDRnCWuqJ7tXdeyD/mEH5rN87OwK9B8gZ5jGqFG65YlzXC+y
hRDvbuZfaEpd9Ka8AnsgKK4k4goq80nkGr+kPG3aAoVjYNoceM7lAgMBAAE=
-----END RSA PUBLIC KEY-----
signing-key
-----BEGIN RSA PUBLIC KEY-----
MIGJAoGBAMNpCKOPaXVYt4NYsVfGRl0zdknlaf/gsl85HErpJMsFYr8VeBw9e46w
4WF6brxKZqbDlyukBAjv8BOD1KIeg9f9EfVY6UuhZbSrDUxfkEDmMIFs2Fcx67hy
4P+blGoxb+n0XfekSbyPhUzXz6EnTqAbSEYQM1Zke4tvZGRA6wm9AgMBAAE=
-----END RSA PUBLIC KEY-----
family $0CE3CFB1E9CC47B63EA8869813BF6FAB7D4540C1 $1FD187E8F69A9B74C9202DC16A25B9E7744AB9F6 $74FB5EFA6A46DE4060431D515DC9A790E6AD9A7C $77001D8DA9BF445B0F81AA427A675F570D222E6A $A7569A83B5706AB1B1A9CB52EFF7D2D32E4553EB $B6D83EC2D9E18B0A7A33428F8CFA9C536769E209 $D2F37F46182C23AB747787FD657E680B34EAF892 $E5E3E9A472EAF7BE9682B86E92305DB4C71048EF
opt hidden-service-dir
contact tor-admin on formlessnetworking of net
reject 0.0.0.0/8:*
reject 169.254.0.0/16:*
reject 127.0.0.0/8:*
reject 192.168.0.0/16:*
reject 10.0.0.0/8:*
reject 172.16.0.0/12:*
reject 199.48.147.37:*
accept *:20-23
accept *:43
accept *:53
accept *:79-81
accept *:88
accept *:110
accept *:143
accept *:194
accept *:443
accept *:464-465
accept *:543-544
accept *:563
accept *:587
accept *:706
accept *:749
accept *:873
accept *:902-904
accept *:981
accept *:989-993
accept *:995
accept *:1194
accept *:1220
accept *:1293
accept *:1500
accept *:1723
accept *:1863
accept *:2082-2083
accept *:2086-2087
accept *:2095-2096
accept *:3389
accept *:3690
accept *:4321
accept *:4643
accept *:5050
accept *:5190
accept *:5222-5223
accept *:5900
accept *:6666-6667
accept *:6679
accept *:6697
accept *:8000
accept *:8008
accept *:8080
accept *:8087-8088
accept *:8443
accept *:8888
accept *:9418
accept *:9999
accept *:10000
accept *:19638
reject *:*
router-signature
-----BEGIN SIGNATURE-----
rvoIDm+hzveUtq7TAXz1fVnz5AZfms+rN1o6Ju+guHvdgCRao7iuHIZD+eR2eBUU
+oh+T8KLzUA+llQquGGP3YNBdVOTuRR7lWmvjoalmLgxu5phhdYW6+mM4uhUlHHj
qxZ72rvQ2GyMJiAGCHNLCCcX605RiEtbgkMDNG+bgz4=
-----END SIGNATURE-----
descriptor_archive/0/2/ 0000755 0001750 0001750 00000000000 11755476646 014542 5 ustar atagar atagar descriptor_archive/1/b/ 0000755 0001750 0001750 00000000000 11755476662 014621 5 ustar atagar atagar descriptor_archive/0/ 0000755 0001750 0001750 00000000000 11727534307 014365 5 ustar atagar atagar descriptor_archive/1/ 0000755 0001750 0001750 00000000000 11727534336 014370 5 ustar atagar atagar descriptor_archive/ 0000755 0001750 0001750 00000000000 11727534336 014230 5 ustar atagar atagar stem-1.6.0/test/unit/descriptor/data/cached-consensus-v2 0000664 0001750 0001750 00000005443 13115423200 023727 0 ustar atagar atagar 0000000 0000000 @type network-status-2 1.0
network-status-version 2
dir-source 18.244.0.114 18.244.0.114 80
fingerprint 719BE45DE224B607C53707D0E2143E2D423E74CF
contact arma at mit dot edu
published 2005-12-16 00:13:46
dir-options Names Versions
client-versions 0.0.9rc2,0.0.9rc3,0.0.9rc4-cvs,0.0.9rc4,0.0.9rc5-cvs,0.0.9rc5,0.0.9rc6-cvs,0.0.9rc6,0.0.9rc7-cvs,0.0.9rc7,0.0.9,0.0.9.1,0.0.9.2,0.0.9.3,0.0.9.4,0.0.9.5,0.0.9.6,0.0.9.7,0.0.9.8,0.0.9.9,0.0.9.10,0.1.0.0-alpha-cvs,0.1.0.1-rc,0.1.0.1-rc-cvs,0.1.0.2-rc,0.1.0.2-rc-cvs,0.1.0.3-rc,0.1.0.3-rc-cvs,0.1.0.4-rc,0.1.0.4-rc-cvs,0.1.0.5-rc,0.1.0.5-rc-cvs,0.1.0.6-rc,0.1.0.6-rc-cvs,0.1.0.7-rc,0.1.0.7-rc-cvs,0.1.0.8-rc,0.1.0.8-rc-cvs,0.1.0.9-rc,0.1.0.10,0.1.0.11,0.1.0.12,0.1.0.13,0.1.0.14,0.1.0.15,0.1.0.16,0.1.1.0-alpha-cvs,0.1.1.1-alpha,0.1.1.1-alpha-cvs,0.1.1.2-alpha,0.1.1.2-alpha-cvs,0.1.1.3-alpha,0.1.1.3-alpha-cvs,0.1.1.4-alpha,0.1.1.4-alpha-cvs,0.1.1.5-alpha,0.1.1.5-alpha-cvs,0.1.1.6-alpha,0.1.1.6-alpha-cvs,0.1.1.7-alpha,0.1.1.7-alpha-cvs,0.1.1.8-alpha,0.1.1.8-alpha-cvs,0.1.1.9-alpha,0.1.1.9-alpha-cvs,0.1.1.10-alpha,0.1.1.10-alpha-cvs
server-versions 0.0.9rc2,0.0.9rc3,0.0.9rc4-cvs,0.0.9rc4,0.0.9rc5-cvs,0.0.9rc5,0.0.9rc6-cvs,0.0.9rc6,0.0.9rc7-cvs,0.0.9rc7,0.0.9,0.0.9.1,0.0.9.2,0.0.9.3,0.0.9.4,0.0.9.5,0.0.9.6,0.0.9.7,0.0.9.8,0.0.9.9,0.0.9.10,0.1.0.0-alpha-cvs,0.1.0.1-rc,0.1.0.1-rc-cvs,0.1.0.2-rc,0.1.0.2-rc-cvs,0.1.0.3-rc,0.1.0.3-rc-cvs,0.1.0.4-rc,0.1.0.4-rc-cvs,0.1.0.5-rc,0.1.0.5-rc-cvs,0.1.0.6-rc,0.1.0.6-rc-cvs,0.1.0.7-rc,0.1.0.7-rc-cvs,0.1.0.8-rc,0.1.0.8-rc-cvs,0.1.0.9-rc,0.1.0.10,0.1.0.11,0.1.0.12,0.1.0.13,0.1.0.14,0.1.0.15,0.1.0.16,0.1.1.0-alpha-cvs,0.1.1.1-alpha,0.1.1.1-alpha-cvs,0.1.1.2-alpha,0.1.1.2-alpha-cvs,0.1.1.3-alpha,0.1.1.3-alpha-cvs,0.1.1.4-alpha,0.1.1.4-alpha-cvs,0.1.1.5-alpha,0.1.1.5-alpha-cvs,0.1.1.6-alpha,0.1.1.6-alpha-cvs,0.1.1.7-alpha,0.1.1.7-alpha-cvs,0.1.1.8-alpha,0.1.1.8-alpha-cvs,0.1.1.9-alpha,0.1.1.9-alpha-cvs,0.1.1.10-alpha,0.1.1.10-alpha-cvs
dir-signing-key
-----BEGIN RSA PUBLIC KEY-----
MIGJAoGBAOcrht/y5rkaahfX7sMe2qnpqoPibsjTSJaDvsUtaNP/Bq0MgNDGOR48
rtwfqTRff275Edkp/UYw3G3vSgKCJr76/bqOHCmkiZrnPV1zxNfrK18gNw2Cxre0
nTA+fD8JQqpPtb8b0SnG9kwy75eS//sRu7TErie2PzGMxrf9LH0LAgMBAAE=
-----END RSA PUBLIC KEY-----
r moria2 cZvkXeIktgfFNwfQ4hQ+LUI+dM8 t/Pwl1uHiJ3RKF/Vehsbthf2VDI 2005-12-15 06:57:18 18.244.0.114 443 80
s Authority Fast Named Running Valid V2Dir
r stnv CSi6RnBWxKaJ/uTvXXFIK2KJw9U ItGn7UGZvaftbEFu7NdpwY4fKlo 2005-12-15 16:24:42 84.16.236.173 9001 0
s Named Valid
r nggrplz CehYL/Dm+F4rjkHA3AucncRuaWg swLCsByU85jj7ziTlSawZR+CTdY 2005-12-15 23:25:50 194.109.109.109 9001 0
s Fast Stable Running Valid
directory-signature moria2
-----BEGIN SIGNATURE-----
2nXCxVje3wzn6HrIFRNMc0nc48AhMVpHZyPwRKGXkuYfTQG55uvwQDaFgJHud4RT
27QhWltau3K1evhnzhKcpbTXwkVv1TBYJSzL6rEeAn8cQ7ZiCyqf4EJCaNcem3d2
TpQQk3nNQF8z6UIvdlvP+DnJV4izWVkQEZgUZgIVM0E=
-----END SIGNATURE-----
stem-1.6.0/test/unit/descriptor/data/server_descriptor_with_ed25519 0000664 0001750 0001750 00000005424 13115423200 026031 0 ustar atagar atagar 0000000 0000000 @type server-descriptor 1.0
router destiny 94.242.246.23 9001 0 443
identity-ed25519
-----BEGIN ED25519 CERT-----
AQQABhtZAaW2GoBED1IjY3A6f6GNqBEl5A83fD2Za9upGke51JGqAQAgBABnprVR
ptIr43bWPo2fIzo3uOywfoMrryprpbm4HhCkZMaO064LP+1KNuLvlc8sGG8lTjx1
g4k3ELuWYgHYWU5rAia7nl4gUfBZOEfHAfKES7l3d63dBEjEX98Ljhdp2w4=
-----END ED25519 CERT-----
master-key-ed25519 Z6a1UabSK+N21j6NnyM6N7jssH6DK68qa6W5uB4QpGQ
or-address [2a01:608:ffff:ff07::1:23]:9003
platform Tor 0.2.7.2-alpha-dev on Linux
protocols Link 1 2 Circuit 1
published 2015-08-22 15:21:45
fingerprint F65E 0196 C94D FFF4 8AFB F2F5 F9E3 E19A AE58 3FD0
uptime 1362680
bandwidth 149715200 1048576000 51867731
extra-info-digest 44E9B679AF0B4EB09296985BAF4066AE9CA5BB93 r+roMxhsjd1GPpn5knQoBvtE9Rhsv8zQHCqiYL6u2CA
onion-key
-----BEGIN RSA PUBLIC KEY-----
MIGJAoGBAKpPOeBPFBZhH32k0CmIVsXMi4mbbkpEAYpZD0Z3/zLc9k05qAvhE55h
+LXqG6C6k23JnR7H1a4EtFU0UQVWxUa4xUL9pi/0tj3Zsu842Z18K3sL8hYWDw6x
b6afVdSKIcY6guG5fevmobUd/6437oSwM7IeXrWy28s0PtWKHhQzAgMBAAE=
-----END RSA PUBLIC KEY-----
signing-key
-----BEGIN RSA PUBLIC KEY-----
MIGJAoGBAOUS7xm+1d/FAk7VHx2SaYzjYoGpNaCHHWXlmDz2+iWEqcDRjjnVFekV
sfAPysNnB0a/lHdrqzyKjCkzAoeut5Ts3bj6eMrF3psFian2IqdlqsFaAcBov7fo
J6ipwr8lP72LOMHlB2AwP3BEWtHZX7nmARV7ekbPs21R06lEhzLLAgMBAAE=
-----END RSA PUBLIC KEY-----
onion-key-crosscert
-----BEGIN CROSSCERT-----
iW8BqwH5VKqZaiMgPcuHIQFpiQnRsd2b1zc+PXVN3AFT0cQx6J4rZhIdxiqHeNqj
fVEoi4+iHkbksGABZKlB/x7Kv2Kvbj3ZH46m22KEASkRL+i9EhCYdf3Ju7czIi/7
U/jQTwhn7+o8LCLsLhw3aV/v/sXEtbxePhMbCMHI7hE=
-----END CROSSCERT-----
ntor-onion-key-crosscert 0
-----BEGIN ED25519 CERT-----
AQoABhtwAWemtVGm0ivjdtY+jZ8jOje47LB+gyuvKmulubgeEKRkAHj4IPqm+osx
vbKfvRHeZ0uaghFPZr76UVPYwuK4N+VcW75yq2vuFSsFTCJqamPB3PIdSz6rbx4U
4F3iroztLAQ=
-----END ED25519 CERT-----
family $379FB450010D17078B3766C2273303C358C3A442 $3EB46C1D8D8B1C0BBCB6E4F08301EF68B7F5308D $B0279A521375F3CB2AE210BDBFC645FDD2E1973A $EC116BCB80565A408CE67F8EC3FE3B0B02C3A065
hidden-service-dir
contact 0x02225522 Frenn vun der Enn (FVDE)
ntor-onion-key JCj8BOqk0Khfp1hfoJaDbSTzNgeA/u2pSAXnaR3vhl0=
reject 0.0.0.0/8:*
reject 169.254.0.0/16:*
reject 127.0.0.0/8:*
reject 192.168.0.0/16:*
reject 10.0.0.0/8:*
reject 172.16.0.0/12:*
reject 94.242.246.23:*
reject *:25
reject *:587
reject *:465
reject 176.67.160.187:*
reject 185.35.77.160:*
reject 185.35.77.250:*
reject *:10000
reject *:14464
reject 94.100.180.202:*
reject 217.69.139.215:*
reject 217.69.140.233:*
accept *:*
ipv6-policy reject 25,465,587,10000,14464
router-sig-ed25519 w+cKNZTlL7vz/4WgYdFUblzJy3VdTw0mfFK4N3SPFCt20fNKt9SgiZ5V/2ai3kgGsc6oCsyUesSiYtPcTXMLCw
router-signature
-----BEGIN SIGNATURE-----
y72z1dZOYxVQVLRMvEJOn9lOFxBsjojpwiYxw+3vWFHnhkOdGqolxJ6gTLhiIXNu
ckBPqxjbpFbmt6qgk0oeivwyLo9o4nZT737d3tx1EuBmxo+gqzNtukXWzJzZFIj5
xE0eo9e/zKPSCF/LK6zv0FSefdBpnEkYYFuGN0BCrZo=
-----END SIGNATURE-----
stem-1.6.0/test/unit/descriptor/data/metrics_consensus 0000664 0001750 0001750 00000014537 13115423200 023727 0 ustar atagar atagar 0000000 0000000 @type network-status-consensus-3 1.0
network-status-version 3
vote-status consensus
consensus-method 12
valid-after 2012-07-12 10:00:00
fresh-until 2012-07-12 11:00:00
valid-until 2012-07-12 13:00:00
voting-delay 300 300
client-versions 0.2.2.35,0.2.2.36,0.2.2.37,0.2.3.10-alpha,0.2.3.11-alpha,0.2.3.12-alpha,0.2.3.13-alpha,0.2.3.14-alpha,0.2.3.15-alpha,0.2.3.16-alpha,0.2.3.17-beta,0.2.3.18-rc,0.2.3.19-rc
server-versions 0.2.2.35,0.2.2.36,0.2.2.37,0.2.3.10-alpha,0.2.3.11-alpha,0.2.3.12-alpha,0.2.3.13-alpha,0.2.3.14-alpha,0.2.3.15-alpha,0.2.3.16-alpha,0.2.3.17-beta,0.2.3.18-rc,0.2.3.19-rc
known-flags Authority BadExit Exit Fast Guard HSDir Named Running Stable Unnamed V2Dir Valid
params CircuitPriorityHalflifeMsec=30000 bwauthpid=1
dir-source tor26 14C131DFC5C6F93646BE72FA1401C02A8DF2E8B4 86.59.21.38 86.59.21.38 80 443
contact Peter Palfrader
vote-digest 0B6D1E9A300B895AA2D0B427F92917B6995C3C1C
dir-source turtles 27B6B5996C426270A5C95488AA5BCEB6BCC86956 76.73.17.194 76.73.17.194 9030 9090
contact Mike Perry
vote-digest 904B1974B9879D02B4ADFB81D7E9B4E07D768A5A
dir-source maatuska 49015F787433103580E3B66A1707A00E60F2D15B 171.25.193.9 171.25.193.9 443 80
contact 4096R/23291265 Linus Nordberg
vote-digest A8839355BAC373320B8CEDD0A0A09DAAA1637E3A
dir-source dannenberg 585769C78764D58426B8B52B6651A5A71137189A dannenberg.ccc.de 193.23.244.244 80 443
contact Andreas Lehner
vote-digest 416B73C49E717B0A5D61A4F634DCCF94611802E7
dir-source urras 80550987E1D626E3EBA5E5E75A458DE0626D088C 208.83.223.34 208.83.223.34 443 80
contact 4096R/E012B42D Jacob Appelbaum
vote-digest 08B1F8E4910F136E7FB7DFD52ABB2A9EDE939F0B
dir-source moria1 D586D18309DED4CD6D57C18FDB97EFA96D330566 128.31.0.34 128.31.0.34 9131 9101
contact 1024D/28988BF5 arma mit edu
vote-digest 5006931FB78F7AE42B602697591DBA82AACEF533
dir-source dizum E8A9C45EDE6D711294FADF8E7951F4DE6CA56B58 194.109.206.212 194.109.206.212 80 443
contact 1024R/8D56913D Alex de Joode
vote-digest 3F1F1E071EC5F54115CB8EA9723E30A9386AB8CA
dir-source gabelmoo ED03BB616EB2F60BEC80151114BB25CEF515B226 212.112.245.170 212.112.245.170 80 443
contact 4096R/C5AA446D Sebastian Hahn
vote-digest DF2EC9AD207831DED1D01BB889A9C4478DE2CFB9
r sumkledi ABPSI4nNUNC3hKPkBhyzHozozrU 8mCr8Sl7RF4ENU4jb0FZFA/3do8 2012-07-12 04:01:55 178.218.213.229 80 0
s Exit Fast Named Running Valid
v Tor 0.2.2.35
w Bandwidth=38
p accept 80,443
r Unnamed AEXri4INxBAZeyi0wvJZoC58nZs 9t4U465paxGASh0x5Tds8a8YiKo 2012-07-12 04:48:09 79.139.135.90 443 22
s Fast HSDir Running V2Dir Valid
v Tor 0.2.2.37
w Bandwidth=35
p reject 1-65535
r ANONIONROUTER AHhuQ8zFQJdT8l42Axxc6m6kNwI uI7+jQ/T3kFVnl7H7TYE/7WJxi4 2012-07-12 04:40:31 93.128.55.236 24051 24052
s Fast Named Running V2Dir Valid
v Tor 0.2.2.37
w Bandwidth=108
p reject 1-65535
r ph3x AMLCoWrttR1eX7fWFo/GazQ9gi8 ZJJnmKK6+9B2KOUSIPV49+Vprxs 2012-07-11 19:44:22 86.59.119.83 443 80
s Fast Guard HSDir Named Running Stable V2Dir Valid
v Tor 0.2.3.18-rc
w Bandwidth=55300
p reject 1-65535
r nargothrond ANi/r5RGhUxfZ3simlDXFrf2O68 DsP6REKOns/vAUYNp3rfkCOSJFM 2012-07-11 18:25:37 173.11.83.10 9001 0
s Fast Guard Named Running Stable Valid
v Tor 0.2.3.18-rc
w Bandwidth=543
p reject 1-65535
r default AN1sc6ymJ4WcSJ95VITqL0B5wDQ I9HQ2zph5Nuvf4FKANoKDf5vPV8 2012-07-11 18:48:22 82.243.60.52 443 9030
s Fast Running V2Dir Valid
v Tor 0.2.2.35
w Bandwidth=92
p reject 1-65535
r catfesh AOTNBUkB8Lob/wiz7h9gtuDoT2Q 0Ycp54MgG+Ns+oEd3BIubFJdGGw 2012-07-12 08:26:51 80.177.151.82 9001 9030
s Fast HSDir Running V2Dir Valid
v Tor 0.2.2.37
w Bandwidth=61
p reject 1-65535
directory-footer
bandwidth-weights Wbd=3335 Wbe=0 Wbg=3536 Wbm=10000 Wdb=10000 Web=10000 Wed=3329 Wee=10000 Weg=3329 Wem=10000 Wgb=10000 Wgd=3335 Wgg=6464 Wgm=6464 Wmb=10000 Wmd=3335 Wme=0 Wmg=3536 Wmm=10000
directory-signature 14C131DFC5C6F93646BE72FA1401C02A8DF2E8B4 BF112F1C6D5543CFD0A32215ACABD4197B5279AD
-----BEGIN SIGNATURE-----
HFXB4497LzESysYJ/4jJY83E5vLjhv+igIxD9LU6lf6ftkGeF+lNmIAIEKaMts8H
mfWcW0b+jsrXcJoCxV5IrwCDF3u1aC3diwZY6yiG186pwWbOwE41188XI2DeYPwE
I/TJmV928na7RLZe2mGHCAW3VQOvV+QkCfj05VZ8CsY=
-----END SIGNATURE-----
directory-signature 27B6B5996C426270A5C95488AA5BCEB6BCC86956 D5C30C15BB3F1DA27669C2D88439939E8F418FCF
-----BEGIN SIGNATURE-----
VAL+VWqcJiJtTZjFDz5/rS4WLfh8dOSnU2HYUb1ZgqM8PR1rFsoxpvaK9USrtkx9
Byctu/flD3YOqGg+GpYQwU8w9tm7BGelD+dqg97DkJXmlPaXe/Z0nKW1UnCN9m93
svyWCAqglEzxlK4H7ZfMlQbkMu7EFjXGzrn1gRVGOwg=
-----END SIGNATURE-----
directory-signature 49015F787433103580E3B66A1707A00E60F2D15B 1C915B9493589F97BAC764D1885A34BFC18C7E26
-----BEGIN SIGNATURE-----
fHAC5vdqotMtTVIxfqoNrlob2jAi3PP/urvsVA0xmaOzgYtJFIjY2iEWrrU4fRwe
0M1vyCw+oztBrPKYukedkefE9ly/R30KVW2ezo5WpOO4y6oZpelb/jRKFoSRfbyB
WdKsHSe2xlXPA0ySu1klpuMOZiQ8wgxh4x3oLGXnL5Q=
-----END SIGNATURE-----
directory-signature 585769C78764D58426B8B52B6651A5A71137189A 499D7CE5A1356045D629F43271EBF600D6F2CC9C
-----BEGIN SIGNATURE-----
IDOUDykw+tdCyyVmPSGUDahIeEEPMWxarEoH2gPuyExDqZkUc0ah6Eh736rVSD5Z
R4nCjDNTQNr5byDfJk6cMDN9A/5P8uz421pnmLfs9SasLUjTdJt921jxJnSvSBeF
hSZPNi5wl++Uw3j2zeclOXvAkkAEGi9Pi5Zf6QNlWFI=
-----END SIGNATURE-----
directory-signature 80550987E1D626E3EBA5E5E75A458DE0626D088C 2B9B419BB44728A5BE01651D6D1553FD14B6CFFB
-----BEGIN SIGNATURE-----
D2wVGni7cYHyXNqt9RvW/CUd8r7TgkfEp9OAJKojUyweiGMJOMEqDBE01e4Ov9Pd
O9D46fjxWYGE9fN72xvD8CGoNcQgTtLpvypEfB96tKM3JYr5j4MCsdcOrQBkKGp7
qf1Qfiw7aXahk8IfbgvmAvROlAMAxln7nVE0qenQWu4=
-----END SIGNATURE-----
directory-signature D586D18309DED4CD6D57C18FDB97EFA96D330566 8F0DEA35D1732D867FB94F5130364927DBCCBB8E
-----BEGIN SIGNATURE-----
cmrV1VUfCo9Smlc8BUblkBuSFqJdQkX/gd3ROOnpdRfogbsylq6xA7srCBGp1Z39
To5Vk71AI0PIy031S6gKfOLgn9E5Bl0Agr60diFxjNn0ejR49MKJTjoDI+DmBlv4
do+Bej+D8afl27LNH/QIYyzSkOl0eXSGtOEEuCQg/3A=
-----END SIGNATURE-----
directory-signature E8A9C45EDE6D711294FADF8E7951F4DE6CA56B58 9BE9A4CF520B6880EB42C5702BC521F03A6CF0FC
-----BEGIN SIGNATURE-----
UVXzEFkkjCpszLmoqQxAxSU83IS+fqrkIC4DCQZCEjRcXEvx3c56HUyTsew5WTFR
XANCJn+V3DaxYLuL6L8xW7r9xOQNU970nGwocuJckxyDcLHloL8E226vIAn6mLmt
a1Z6y8NzaQpv4fhdqhT7ETJo+chmf8bSX8qLLmaCIac=
-----END SIGNATURE-----
directory-signature ED03BB616EB2F60BEC80151114BB25CEF515B226 845CF1D0B370CA443A8579D18E7987E7E532F639
-----BEGIN SIGNATURE-----
DILsRCrtn6rDbNo3DF+L1/VVAd+V86PdZKg3Q9QooqVOGgU/7HrspV/K4lFbWcTT
Zm+quRQfuKmB4xljwXpeRlABQR5eainlZBtrTFg056/dDrJqYXSwV/C391tAIDZs
2TANs/4uLi94q6Ov+zE9zYUiF8jwnyXl/q/jKOYM8bE=
-----END SIGNATURE-----
stem-1.6.0/test/unit/descriptor/data/bridge_descriptor_with_ed25519 0000664 0001750 0001750 00000001330 13115423200 025747 0 ustar atagar atagar 0000000 0000000 @type bridge-server-descriptor 1.1
router ChandlerObfs11 10.162.85.172 443 0 0
or-address [fd9f:2e19:3bcf::3d:b3f9]:443
master-key-ed25519 lgIuiAJCoXPRwWoHgG4ZAoKtmrv47aPr4AsbmESj8AA
platform Tor 0.2.7.1-alpha-dev on Linux
protocols Link 1 2 Circuit 1
published 2015-08-20 20:09:42
fingerprint 6789 12AB D739 8DF8 EFC8 FA2B C7DE F610 7103 60C4
uptime 2693146
bandwidth 1073741824 1073741824 779171
extra-info-digest 5229E98EAB41A32E83159FA45BED21B5B405BF3B lTSVvW/NMR2HsLaJwhndqTULAtsaaSilmvm/orV9y/s
hidden-service-dir
contact somebody
ntor-onion-key bVvxUCi8drAVRkFPuFQ4DYBsV1m2M+GpHOUReOopXEk=
reject *:*
router-digest-sha256 OB/fqLD8lYmjti09R+xXH/D4S2qlizxdZqtudnsunxE
router-digest 402AC6E2BB371923E637A1D23C2D8EA55D6EB7D5
stem-1.6.0/test/unit/descriptor/data/unparseable/ 0000775 0001750 0001750 00000000000 13177674754 022563 5 ustar atagar atagar 0000000 0000000 stem-1.6.0/test/unit/descriptor/data/unparseable/extrainfo_nonascii_v3_reqs 0000664 0001750 0001750 00000003424 13124757510 030015 0 ustar atagar atagar 0000000 0000000 extra-info newton C871C91489886D5E2E94C13EA1A5FDC4B6DC5204
identity-ed25519
-----BEGIN ED25519 CERT-----
AQQABjBoAS6cmIIt3t0HjlInkuk+asDf7yAuTXhg20Gdy4uesKH3AQAgBAC6lLwz
7+yzoMFGDKic6l7kOjyGlV3H01GWtErUWy0PIquP5kMVHnrvdhsBwX0zwSA19Bo+
tdKPIKrMV+89QBGX2KSa7RRXqIdnGBnGdodglrvMFPGgO/LYHOb9YogFJgA=
-----END ED25519 CERT-----
published 2016-03-26 07:47:53
write-history 2016-03-26 05:42:08 (14400 s) 682447872,1737454592,1207077888,1278699520,1157439488,635725824
read-history 2016-03-26 05:42:08 (14400 s) 672482304,1680056320,1198174208,1233503232,1105783808,620540928
dirreq-write-history 2016-03-26 05:42:08 (14400 s) 1960960,46080,2144256,218112,737280,3273728
dirreq-read-history 2016-03-26 05:42:08 (14400 s) 322560,13312,323584,8192,46080,440320
geoip-db-digest 6346E26E2BC96F8511588CE2695E9B0339A75D32
geoip6-db-digest 43CCB43DBC653D8CC16396A882C5F116A6004F0C
dirreq-stats-end 2016-03-25 19:53:29 (86400 s)
dirreq-v3-ips us=8
dirreq-v3-reqs Sÿ=4026597208,Sÿ=4026597208,Sÿ=4026597208,Sÿ=4026597208,Sÿ=4026597208,Sÿ=4026597208,¥þ=4026591624,6=4026537520,6=4026537520,6=4026537520,us=8
dirreq-v3-resp ok=8,not-enough-sigs=0,unavailable=0,not-found=0,not-modified=0,busy=0
dirreq-v3-direct-dl complete=0,timeout=0,running=0
dirreq-v3-tunneled-dl complete=8,timeout=4,running=0
hidserv-stats-end 2016-03-25 19:53:29 (86400 s)
hidserv-rend-relayed-cells 5238 delta_f=2048 epsilon=0.30 bin_size=1024
hidserv-dir-onions-seen -21 delta_f=8 epsilon=0.30 bin_size=8
router-sig-ed25519 TeDQDiRfI/k31H+fxciE8q02/ddm/qIi4H7wpQ9A8wRwWBoFUBdwrjZu+pACiglomOuc/I2uSt0++/pmZulZDg
router-signature
-----BEGIN SIGNATURE-----
OVkz0CKUwWSXjhUMEsYATzcfT+D24QR8w6IjUtb6Mcz5P5CMggqmSFhL6SzUFXWS
JwipCWcd2/1NsJ7/JLyXKcG0Ak6FPNP3arweah8EwWkVo7rvscagI+u+Uq2FqtO/
OTNjnriDWTZdSA9FOcQueBZi7Qf7xwA0LEjtadtCjUI=
-----END SIGNATURE-----
stem-1.6.0/test/unit/descriptor/data/unparseable/cached-microdesc-consensus_with_carriage_returns 0000664 0001750 0001750 00000020477 13124757510 034345 0 ustar atagar atagar 0000000 0000000 network-status-version 3 microdesc
vote-status consensus
consensus-method 20
valid-after 2015-09-15 18:00:00
fresh-until 2015-09-15 19:00:00
valid-until 2015-09-15 21:00:00
voting-delay 300 300
client-versions 0.2.4.23,0.2.4.24,0.2.4.25,0.2.4.26,0.2.4.27,0.2.5.8-rc,0.2.5.9-rc,0.2.5.10,0.2.5.11,0.2.5.12,0.2.6.5-rc,0.2.6.6,0.2.6.7,0.2.6.8,0.2.6.9,0.2.6.10,0.2.7.1-alpha,0.2.7.2-alpha
server-versions 0.2.4.23,0.2.4.24,0.2.4.25,0.2.4.26,0.2.4.27,0.2.5.8-rc,0.2.5.9-rc,0.2.5.10,0.2.5.11,0.2.5.12,0.2.6.5-rc,0.2.6.6,0.2.6.7,0.2.6.8,0.2.6.9,0.2.6.10,0.2.7.1-alpha,0.2.7.2-alpha
known-flags Authority BadExit Exit Fast Guard HSDir Running Stable V2Dir Valid
params CircuitPriorityHalflifeMsec=30000 NumDirectoryGuards=3 NumEntryGuards=1 NumNTorsPerTAP=100 Support022HiddenServices=0 UseNTorHandshake=1 UseOptimisticData=1 bwauthpid=1 cbttestfreq=1000 pb_disablepct=0 usecreatefast=0
dir-source tor26 14C131DFC5C6F93646BE72FA1401C02A8DF2E8B4 86.59.21.38 86.59.21.38 80 443
contact Peter Palfrader
vote-digest 7DD2661BBBA3E600CB9D641CFFF27D14D77EDCCE
dir-source longclaw 23D15D965BC35114467363C165C4F724B64B4F66 longclaw.riseup.net 199.254.238.52 80 443
contact Riseup Networks - 1nNzekuHGGzBYRzyjfjFEfeisNvxkn4RT
vote-digest 2EBB0C5EFEFE51ECE8C630A45F607201491D7E09
dir-source maatuska 49015F787433103580E3B66A1707A00E60F2D15B 171.25.193.9 171.25.193.9 443 80
contact 4096R/23291265 Linus Nordberg
vote-digest 7FC1487C8D7C810C6FA8037AE6EC81ACECC408B8
dir-source dannenberg 585769C78764D58426B8B52B6651A5A71137189A dannenberg.torauth.de 193.23.244.244 80 443
contact Andreas Lehner
vote-digest 32F82FE133ADAAED686A7360297C56B327994799
dir-source urras 80550987E1D626E3EBA5E5E75A458DE0626D088C 208.83.223.34 208.83.223.34 443 80
contact 4096R/D255D3F5C868227F Jacob Appelbaum
vote-digest 86348CCEC68457D5C1777978D1043B211FA44010
dir-source moria1 D586D18309DED4CD6D57C18FDB97EFA96D330566 128.31.0.34 128.31.0.34 9131 9101
contact 1024D/28988BF5 arma mit edu
vote-digest 9D18B15C2C039CB0B18B51496F8ACBDA2836DE19
dir-source dizum E8A9C45EDE6D711294FADF8E7951F4DE6CA56B58 194.109.206.212 194.109.206.212 80 443
contact 1024R/8D56913D Alex de Joode
vote-digest 5473986DC071B8840F9E8C72AED3251B71A8244F
dir-source gabelmoo ED03BB616EB2F60BEC80151114BB25CEF515B226 131.188.40.189 131.188.40.189 80 443
contact 4096R/261C5FBE77285F88FB0C343266C8C2D7C5AA446D Sebastian Hahn - 12NbRAjAG5U3LLWETSF7fSTcdaz32Mu5CN
vote-digest 6EF25161284DB1AD19411EBF52E96927DFD66F0C
dir-source Faravahar EFCBE720AB3A82B99F9E953CD5BF50F7EEFC7B97 154.35.175.225 154.35.175.225 80 443
contact 0x0B47D56D Sina Rabbani (inf0)
vote-digest 76466BEF6525FCEE0B9FDDB205B527CBB89FAB17
r PDrelay1 AAFJ5u9xAqrKlpDW6N0pMhJLlKs 2015-09-15 04:53:47 95.215.44.189 8080 0
m iH2s4cVPTmThmmx1phnjDd6oGpHAEP7FDK38AFYRV74
s Fast Running Stable Valid
v Tor 0.2.7.2-alpha-dev
w Bandwidth=456
r seele AAoQ1DAR6kkoo19hBAX5K0QztNw 2015-09-15 04:43:45 73.15.150.172 9001 0
m LqTE9J4jxtMVm2IPDxmUKYyrzajcjqwYqLkX6JNOEpY
s Fast Running Stable Valid
v Tor 0.2.6.10
w Bandwidth=15
r TorNinurtaName AA8YrCza5McQugiY3J4h5y4BF9g 2015-09-15 17:06:16 151.236.6.198 443 80
m YxghdOEPO/uPv0N1GZV+6gCuYR5NZPaDseU45R/Yyns
s Fast HSDir Running Stable V2Dir Valid
v Tor 0.2.6.10
w Bandwidth=1750
r CalyxInstitute14 ABG9JIWtRdmE7EFZyI/AZuXjMA4 2015-09-15 16:12:03 162.247.72.201 443 80
m 6PnpSoAVS1oaJCqs4uk8y706JSZ6rdN8uUM78Oohgo0
s Exit Fast Guard HSDir Running Stable V2Dir Valid
v Tor 0.2.6.10
w Bandwidth=11400
directory-footer
bandwidth-weights Wbd=0 Wbe=0 Wbg=3944 Wbm=10000 Wdb=10000 Web=10000 Wed=10000 Wee=10000 Weg=10000 Wem=10000 Wgb=10000 Wgd=0 Wgg=6056 Wgm=6056 Wmb=10000 Wmd=0 Wme=0 Wmg=3944 Wmm=10000
directory-signature sha256 14C131DFC5C6F93646BE72FA1401C02A8DF2E8B4 0B49FD2F55D02B630E7A500E34FD5B6F98FDA9D2
-----BEGIN SIGNATURE-----
OYThM95E2Pcj4RWZeND9+llOUlyHLwGtHqF+n92xKTsZdc7ogigEosdqVEB1GaeJ
D/GWSESN9VLEiiw5VKBG4UVvZUaFVRUnZSJVS4gNgckH5LD0VUJb0C/tIeBHJ7ea
cTOwsb0vHau/W8hkAciEWLfjmAtcWutdT6lljSJ65NWyZynDPHTmtKnsB65kKT+2
p5W9GBxnMUH+Pnt2JkFqyKAsE8GgppMuk4O+AX+BtQgzK4K+pTcKFko6e/NmrBh3
W6AV/fR9/PdSUNYb6+uH/LsUhVePFVbke44u6bBN39LkvxWErQdd3yA91lUdqwz8
zR/uAngvj05Bt1zmH9eG1Kd02dgL4PqNmTI0VKZjWamf4k/xMmecU0xfrHLv52Rw
/hync6FsEIrfI+B942sjIEGfv+T6UuIMNoMvkwrby6nw6WL7lAoJJq5pyBYLzlFj
xLW6e567FFjqf4siHpGCzWCHoXUU+NmygDSakh0orgGoHdVW6JcKbs0QieUyUu+X
-----END SIGNATURE-----
directory-signature sha256 23D15D965BC35114467363C165C4F724B64B4F66 3C12B8EE0B3DC3AEDD8CD27CCB02C564281BE765
-----BEGIN SIGNATURE-----
doESbeqw4jQagxP8o0Yr6VVUdB2Hnmct4F5sH1pBntPYOqIeT3GbYuFT1TdYr9SG
xSpI5+aRcbSQrKryvF+qSgOJS4131YuCmL7zlwhQrQcFtltyRS8UTopubrLbvAJ1
njJt05lM8NWg6NN3uZ4Zy4L/qzn9qZB9l8WbxKZJnxott6+Rp1H19tsl77ENB4GJ
Bfhc9C46LOKUfmV5IB0f+0nFOO3PDhbRFEcDw5EEGPx6xLk+EH9YHvP98VIY/YPi
vtYN9G5G2PplvqG4hyIhGAYy/hm3ZTHf9eIsKvsBgHadBeFCupA/l6tf36nK94Ts
d68QhqPo8QhrjM5FTddGOA==
-----END SIGNATURE-----
directory-signature sha256 49015F787433103580E3B66A1707A00E60F2D15B 2467B90267EEEDDD614694F16A523B0E6EBF6FAA
-----BEGIN SIGNATURE-----
NVXfCo512TGvfHr+9XB7nVTDMRurSlwrz/vHfXjSzmJ/6dbIK6PA98lBgvX3i83N
2AZoUcIc5wasYjUleRrP4k2jn5sBArCr3g64FsZ4/RJMLg2pWxEw4xDY2DLjKpyZ
BfndyOG5KSEtIszXw0szS0kkTyz7oI76L1vu016Mh35YjaeqK0Vu+P2gxJtKxNyt
pLPZwTWVQfjqLAebtXD2Bi5Csty7TSAhsNe9KE5pUB7+DyzvbamuJzyhUFbuOgii
Y1nguoknHVgHnrlVGNaF1T3x2QFb3SR5Q4v8p2u1j0zqktkYTZ693gHYzkjWuyGk
W/MyhtG1cE7knxu/no+HBw==
-----END SIGNATURE-----
directory-signature sha256 585769C78764D58426B8B52B6651A5A71137189A 02958C9BE542EEA43130E54FE6634B6E874B196D
-----BEGIN SIGNATURE-----
XAZPIaIQSTj+8QWZm5dAtiQz2CnEeuM7WMcJikol7CL53n1ewG6k6vYgtFPCJ5uh
EDGTM4eojUB0VXNpc+jeOv207Y5z2r6tjAknr1eMxPX65z2BT4WkeBZLnc68RRrM
LUAe2IbOVeaqTnQmPAtSejO8xxIkJhfZAssrxdLtvagCpkcBYwAbHiVtYWv/G+r0
pyqVzvxIaEoEFShe77bJqfjOnlLOgcTsxdmU3qcDad/5k8ewXfLVcqR8YsjoNF9D
9xbHyqkBZs0EfcNoDhqWsbuq184zMjlSH0Q3QElK/0ZoQQ1y6vlSicKM9rl8vxmf
hRx2hnKyx+yhBT4fpL22ew==
-----END SIGNATURE-----
directory-signature sha256 80550987E1D626E3EBA5E5E75A458DE0626D088C 7C5D0700D9C266B7D3F93E7C904A62FEC6B30A60
-----BEGIN SIGNATURE-----
WwrdpjcjuzxmV+E1ULuuFo98nMiN0iMw6MLAs7+kuOdjbW+ayKzizZvnDPjlMv1p
2cJM7qAwnD6aSxzn1wpc0ecdaVae1cMYqEnP3BxFFY9Xh4dxu8JdlNhB7W3rlIva
lptekvZQnRBb2J4ULws4455AP6oNTCO9x3UpGNd5tbc=
-----END SIGNATURE-----
directory-signature sha256 D586D18309DED4CD6D57C18FDB97EFA96D330566 F1E67374A96F51A0BB4C8C24748200885EFF8645
-----BEGIN SIGNATURE-----
fgn1RZSa2xtYrOiG9YUtQdPl9RMZ8YQsxImg7D53AoXrctC8Ul48CNnIz/JWdIts
qcczwUtSSixuEDD5gqBcFn0Fi7S1E2F8VcmCUAh7AMBtVSn5vUKkmaQcSy+zG0N+
ospb0wQYbJ2DcsVTqL9z4TLpNB09BLN0kOzS64MJAqH9thE/1GHGNeXRFHupYgTO
PERdyFvFEAmS9XnSxSRitgf6ptG5sAlFbzQrueKmnlrVe51gJBTEZNgVb1wFTjEw
7nHTvTz9c0AQfgW5if1nIh2RdQ9AeOo3FxCbFMeT53QzHiEZWSDsCZebE/EHf/lz
od6P497tWLK6TIxctbWAXw==
-----END SIGNATURE-----
directory-signature sha256 E8A9C45EDE6D711294FADF8E7951F4DE6CA56B58 292252D59020B903943C025D96134575E1245ADF
-----BEGIN SIGNATURE-----
LzZynAMcx4EWSAqaXRaL2iHCET9T+T8e15kDiPyefxTc5U00Ko6ASKJPpmaHHHAI
SsYQm54TauFX7AOb7Q+9Fx8ujxA0AR85DglLXftAicM27L48E4d9E3qFtzi+0ZHs
At0Wp9odW7ZeFcBNHWFGB/KfqOwHYkukRvqR3vBlKG8nvQ7cC+9o9aXhl4CLoA8X
ViigiEINXCC1BFibg/XA/XZqDB7uxoeazririKp0qZTTZPp8sLSQBnrSvA3OL1I5
c0m4xogooiG+YhGBC7N+MR+hDA9nyV+njVABpy/89/6Zy+y4iydqqO0qSRWXio9W
3T36ECUvEQ9l+FONnfk0cQ==
-----END SIGNATURE-----
directory-signature sha256 ED03BB616EB2F60BEC80151114BB25CEF515B226 5E04DFE1E5E0852F56E524B7C81D1E5FAFDBA650
-----BEGIN SIGNATURE-----
UtcSi7Qb7CijqBhpMsFeqlhRxMTOGqldmdiDCp8ZLJFOCQGIHA9m9c62mlTIleDH
iz/LdCn7RdGm7VoX7+jd3FfU86dajnyREDcwvvTHZ8eIUx6Bz+Q7zmv6yRBctmNL
hqYcK0WHxmELYxwICljLPeu6QOaRE+4Ku1YbNoEFL/YJ4GmgAPODxxy35N+t3uRr
MzWK7N+Ssdo0cRvz47cMBZqRQOKzn3a5/KMnRcBmZbhmJbhi5O9C3tZVA+XFa3fF
jRGbtUir/OE23WmykdXKGmsymXX3b/io+w6UQjBUVzGHQtJLszMTKNE24eUnVOFD
9Yc8aj5LgGkmJbNxyiF0mQ==
-----END SIGNATURE-----
directory-signature sha256 EFCBE720AB3A82B99F9E953CD5BF50F7EEFC7B97 F22B7C6FB4E6DA30A3C63D265A1D4AB374CB34E3
-----BEGIN SIGNATURE-----
uh/7AVwA+T8EWm9GgQxYzPPBibvb2/FP5Ny6p9x95oJRWjWeKEM77hnojJ540N6u
IyyIJTUB5JgUrsN2BXEnyUqZu0+OWB9hYxFIzVjwUxtuL2uaPeBfXqv5iJNEqG9w
Gl1ubnFUbVZXM83e61ClgwaswhvVPzfgirqpmD+L9m/DaZj+EmzZtoquqaBNtv1g
qU/yZgFG4AFApjvSaP6kkttZ8EYTVtGMq6muIxLeBDlVRP2dmC+R5FwGmsbj92is
uxaCO4raz5BM6yyDS44vzJwFePOyQhiYNV52g8dod/xtrhZOlWQtCbX3jXxv4ofV
nfX9MJA9VJ5WbLSpwU1p/w==
-----END SIGNATURE-----
stem-1.6.0/test/unit/descriptor/data/unparseable/vote 0000664 0001750 0001750 00000010774 13124757510 023454 0 ustar atagar atagar 0000000 0000000 network-status-version 3
vote-status vote
consensus-methods 1 2 3 4 5 6 7 8 9 10 11 12
published 2012-07-11 23:50:01
valid-after 2012-07-12 00:00:00
fresh-until 2012-07-12 01:00:00
valid-until 2012-07-12 03:00:00
voting-delay 300 300
known-flags Authority BadExit Exit Fast Guard HSDir Running Stable V2Dir Valid
params CircuitPriorityHalflifeMsec=30000 bwauthpid=1
dir-source turtles 27B6B5996C426270A5C95488AA5BCEB6BCC86956 76.73.17.194 76.73.17.194 9030 9090
contact Mike Perry
dir-key-certificate-version 3
fingerprint 27B6B5996C426270A5C95488AA5BCEB6BCC86956
dir-key-published 2011-11-28 21:51:04
dir-key-expires 2012-11-28 21:51:04
dir-identity-key
-----BEGIN RSA PUBLIC KEY-----
MIIBigKCAYEA6uSmsoxj2MiJ3qyZq0qYXlRoG8o82SNqg+22m+t1c7MlQOZWPJYn
XeMcBCt8xrTeIt2ZI+Q/Kt2QJSeD9WZRevTKk/kn5Tg2+xXPogalUU47y5tUohGz
+Q8+CxtRSXpDxBHL2P8rLHvGrI69wbNHGoQkce/7gJy9vw5Ie2qzbyXk1NG6V8Fb
pr6A885vHo6TbhUnolz2Wqt/kN+UorjLkN2H3fV+iGcQFv42SyHYGDLa0WwL3PJJ
r/veu36S3VaHBrfhutfioi+d3d4Ya0bKwiWi5Lm2CHuuRTgMpHLU9vlci8Hunuxq
HsULe2oMsr4VEic7sW5SPC5Obpx6hStHdNv1GxoSEm3/vIuPM8pINpU5ZYAyH9yO
Ef22ZHeiVMMKmpV9TtFyiFqvlI6GpQn3mNbsQqF1y3XCA3Q4vlRAkpgJVUSvTxFP
2bNDobOyVCpCM/rwxU1+RCNY5MFJ/+oktUY+0ydvTen3gFdZdgNqCYjKPLfBNm9m
RGL7jZunMUNvAgMBAAE=
-----END RSA PUBLIC KEY-----
dir-signing-key
-----BEGIN RSA PUBLIC KEY-----
MIGJAoGBAJ5itcJRYNEM3Qf1OVWLRkwjqf84oXPc2ZusaJ5zOe7TVvBMra9GNyc0
NM9y6zVkHCAePAjr4KbW/8P1olA6FUE2LV9bozaU1jFf6K8B2OELKs5FUEW+n+ic
GM0x6MhngyXonWOcKt5Gj+mAu5lrno9tpNbPkz2Utr/Pi0nsDhWlAgMBAAE=
-----END RSA PUBLIC KEY-----
dir-key-crosscert
-----BEGIN ID SIGNATURE-----
RHYImGTwg36wmEdAn7qaRg2sAfql7ZCtPIL/O3lU5OIdXXp0tNn/K00Bamqohjk+
Tz4FKsKXGDlbGv67PQcZPOK6NF0GRkNh4pk89prrDO4XwtEn7rkHHdBH6/qQ7IRG
GdDZHtZ1a69oFZvPWD3hUaB50xeIe7GoKdKIfdNNJ+8=
-----END ID SIGNATURE-----
dir-key-certification
-----BEGIN SIGNATURE-----
fasWOGyUZ3iMCYpDfJ+0JcMiTH25sXPWzvlHorEOyOMbaMqRYpZU4GHzt1jLgdl6
AAoR6KdamsLg5VE8xzst48a4UFuzHFlklZ5O8om2rcvDd5DhSnWWYZnYJecqB+bo
dNisPmaIVSAWb29U8BpNRj4GMC9KAgGYUj8aE/KtutAeEekFfFEHTfWZ2fFp4j3m
9rY8FWraqyiF+Emq1T8pAAgMQ+79R3oZxq0TXS42Z4Anhms735ccauKhI3pDKjbl
tD5vAzIHOyjAOXj7a6jY/GrnaBNuJ4qe/4Hf9UmzK/jKKwG95BPJtPTT4LoFwEB0
KG2OUeQUNoCck4nDpsZwFqPlrWCHcHfTV2iDYFV1HQWDTtZz/qf+GtB8NXsq+I1w
brADmvReM2BD6p/13h0QURCI5hq7ZYlIKcKrBa0jn1d9cduULl7vgKsRCJDls/ID
emBZ6pUxMpBmV0v+PrA3v9w4DlE7GHAq61FF/zju2kpqj6MInbEvI/E+e438sWsL
-----END SIGNATURE-----
r sumkledi ABPSI4nNUNC3hKPkBhyzHozozrU B5n4BiALAF8B5AqafxohyYiuj7E 2012-07-11 04:22:53 178.218.213.229 80 0
s Exit Valid
opt v Tor 0.2.2.35
w Bandwidth=51 Measured=36
p accept 80,443
m 8,9,10,11,12 sha256=g1vx9si329muxV3tquWIXXySNOIwRGMeAESKs/v4DWs
r Unnamed AEXri4INxBAZeyi0wvJZoC58nZs csTseFzwBV0RLsEdlyxDw6jTZ1o 2012-07-11 10:47:26 79.139.135.90 443 22
s Fast HSDir Running V2Dir Valid
opt v Tor 0.2.2.37
w Bandwidth=92 Measured=15
p reject 1-65535
m 8,9,10,11,12 sha256=iZfmvEGdBG/qc9KYZUh4JsWEHbn9Bw2jy5i0AtCoRB0
r default AFU4pMWKyq+eTPuBO0D9W179kQQ OZzEaq9N5+VX5yxzi051zqyEcPk 2012-07-11 08:17:21 2.90.115.167 443 9030
s V2Dir Valid
opt v Tor 0.2.3.15-alpha
w Bandwidth=53
p reject 1-65535
m 8,9,10,11,12 sha256=m/RouK3TqoaYBBHCxgmzMPuGGCIEr0ufSTB1i85zicM
r satoshi11 AFzRyEo3Ibdko7vl4rReoU3BDfM rWaYhhU9Kym4sXAVe+uE99VGpJs 2012-07-11 08:10:01 80.218.153.44 443 9030
s V2Dir Valid
opt v Tor 0.2.2.35
w Bandwidth=196 Measured=113
p reject 1-65535
m 8,9,10,11,12 sha256=yxuOPz7IraIzzmU/qeZRJHOJrutNmvPwDwj2AmsHZSI
r JapanAnon AGw/p8P246zRPQ3ZsQx9+pM8I3s FpQ4fP9FE2j4AnpZTcPiUbaIjwQ 2012-07-11 15:55:42 220.0.231.71 443 9030
s Exit V2Dir Valid
opt v Tor 0.2.2.37
w Bandwidth=41 Measured=5
p accept 110,143,443,706,993,995,1863,5050,5190,5222-5223,6660-6669,6697,7000-7001,8300,8888
m 8,9,10,11,12 sha256=fdV6McsFl7ZaX8YJjRCchKa7B7ImTr6++AE3e7zCkK8
r ANONIONROUTER AHhuQ8zFQJdT8l42Axxc6m6kNwI UUUhUJVXZ5b+8Lizh1ghVkUTGo0 2012-07-11 20:13:16 93.128.114.184 24051 24052
s Fast Running V2Dir Valid
opt v Tor 0.2.2.37
w Bandwidth=196 Measured=100
p reject 1-65535
m 8,9,10,11,12 sha256=ooi27oMS4TBXes4FY4JyULgZYZcFzAyvB1JicaQ8zxs
r pornosteffi AJrkZLNAIMRi7J3Q5os1iBJTM38 tV2vC76eZNwVpSFO8+r9lCqJSVw 2012-07-11 11:00:29 88.78.83.210 443 0
s Fast Running Valid
opt v Tor 0.2.2.35
w Bandwidth=32 Measured=14
p reject 1-65535
m 8,9,10,11,12 sha256=JiARdiiTbey4aLkt4x1DUgEU3oaD2oDB2dtP7SGl3EU
directory-footer
directory-signature 27B6B5996C426270A5C95488AA5BCEB6BCC86956 D5C30C15BB3F1DA27669C2D88439939E8F418FCF
-----BEGIN SIGNATURE-----
fskXN84wB3mXfo+yKGSt0AcDaaPuU3NwMR3ROxWgLN0KjAaVi2eV9PkPCsQkcgw3
JZ/1HL9sHyZfo6bwaC6YSM9PNiiY6L7rnGpS7UkHiFI+M96VCMorvjm5YPs3FioJ
DnN5aFtYKiTc19qIC7Nmo+afPdDEf0MlJvEOP5EWl3w=
-----END SIGNATURE-----
stem-1.6.0/test/unit/descriptor/data/unparseable/tiny.png 0000664 0001750 0001750 00000000274 13124757510 024237 0 ustar atagar atagar 0000000 0000000 ‰PNG
IHDR
PXê sRGB ®Îé pHYs šœ tIMEÜ)ô¿a tEXtComment Created with GIMPW )IDATÓc``````øÏÀðŸ“ÍÄ€0B2Âô¡± èf²v ÿ¡qƒ IEND®B`‚ stem-1.6.0/test/unit/descriptor/data/unparseable/new_metrics_type 0000664 0001750 0001750 00000000125 13124757510 026044 0 ustar atagar atagar 0000000 0000000 @type non-existant-type 1.0
valid metrics header, but a type we shouldn't recognize
stem-1.6.0/test/unit/descriptor/data/unparseable/riddle 0000664 0001750 0001750 00000001067 13124757510 023735 0 ustar atagar atagar 0000000 0000000 Riddle by Damian Johnson (May, 2009)
Curtains raise, they take the stage,
mute actors that never age.
The clouds are cotton, the floor is pine,
the souls bound up in fine twine.
Crowds will cheer, the players dance,
flowing scene to scene as in a trance.
The world is perfect, they never whine,
for there's not a single will, save mine.
The play has ended, we take a bow,
but the viewers are the actors now.
When freed from decision, of thought and blame,
we'll walk to another drum just the same.
answer:
http://www.atagar.com/riddles/answer10.php
stem-1.6.0/test/unit/descriptor/data/descriptor_archive.tar.gz 0000664 0001750 0001750 00000005472 13115423200 025242 0 ustar atagar atagar 0000000 0000000 ‹ Ó}¶O íZYÓâ¸Ùík~…/ú"ŠF’µNUª>¯l/ûò7)Û`¼°ýúÈ=5i¿™îLϤ§çKŇ,éÑæGztŽÁóÓMž³8ù»“lváÕo‚&j´Q!ôTq¨!À9pɆ“@Ý@AU¬‚6ï¾@‚b\|CF@ù» ¦¼ƒ2‡`F)¦ï ‚TeïðUÿ‡ÈÓÌIå“9['ù²Ý¯•ÿ—âÿ²ÇÙWR?¹úIÃûçbPàPKâ<óE;呟A
ñó³*Q0V pP;,ˆ““2“ÕÀ$_*Vþ²
³F |èû‚;´†€ýU‰#å%Œò»rçôï×âs¦œ“8‹7ñ1-Š
Tb„É&3Öι{Óï)@Ô j ˆýþT=£Ÿœ“0Êš\Q-)¦° Âu )LSU#n+ܰ5EDU(£B±µüœ…'_A*ªà5׉¼[èe;Ì £ ÈŽƒÉ+N! ~ìÖ¿g‰Ó£ nxáÖO3ņ0-"¸¦™Œ«sƒBN„[†i3•ÚV-ŽÂ8jüGQ@·Z2™jÊh®¿t¥g>ÔúVW‹[ºöb
ÔK¹oƒñý×Ó[|Æá<ö—µï‘ß]yíWý¾=¯0XüºíðÚÑLÐò†F‹¶±ÛžæG|·–6Œ2:7:1×í ½è÷&«stºÓ!<énÌÜ··f>ªÙÓ£»4wãþ«kϸ{uwÇ™õ¯G9¸‰.ݦ¾Ö&–çÏöÇî#½¬"OÛöuM³þöÓì¬ùÙ¹¥á6’žûM·a0lÖJšíSüXhîvdÀá|¢‹ÑrÒŒæã‘çúÚ"15Èì¥{|h³8›ÔDô„ð:ZD¼
†æ2‰ØÊr.÷•¹Ÿö¯Îf³=_íûöpöÖsš†+ýe•Þ[KÇ|¤5lGc-¼#kíX½8ÂY>ïµötÿ²Ù·ÉB?„÷~]î…wð(VýSôu·!pNáñ¡¼†¥¶-a˜éTµ4ΩàPÕmjk:31ÁÀ€Ê{h›3‹ÛThBgØ Ó€TCDckº°©òža['–Q
SÓ€¹M‰i `QÍ”†´d2$›Ü”
ÚØjFL£ŒØ„!$Í•÷#²_®ê2“j:Ô¡& ˶™‰LY˜ÕÒ•÷&²UfËàΑT9Ìg¶I ³(ºŠ-Íæ)ï- ›„iÚÅi ”‡aLnT
êŒc,·
––ÄR-¡a†d]¦[‚r¤sj ¤bêØ`Ŷµì;tzž5ŠØn|¹K“Ú&Ž2g“)2Â5ïFE@*‚×ÑOSänqr‹R‰E¦j‰¿÷¥5øðñÕä?þðs¤â"øc6¤¥|Ä>c,ÐHù/?×0+l2Eå&J±WfË5ëË)þð#
¤~JâÒ5)]3ÑàðS’óOׂR¢Ü ¸Ôr¹SÜÀ””úÂjƒà’9¡å‘pV
¥¥Q±RPC€R‹¢<ÁECˆ²¹ å)•G*Ï•«AÊe¬|/!/ÏŽòãm-²X9K™%JSTU^š£J(»Á7wµ|Ó )YX®GävlȲ¹(Ï„J4䛕³˜(§D©Œƒre™âåSÆYC~”
Þ,
ÎËeÃrJ¢tÁ›^RÉ^ï?Kÿ#ýh…“å‰_>&¦Ö@›Í'ÖOQt_tÝ„ê~ÆçÓ1Òâµ:d{¦º‡è²Ç¯ÙÅØuçwwê¸gèvgîl7“6¾4—}ÔNƒ;ךjxn¦íú6²–rÖ];jÒÝÛìzÍ㹞×sDí=¶í%æ‚ݦ{;8°UmsiáÝa~ £8›î1T»önîºÔ}6·Ãá)¾ÎaœÍ¼{+ÿ2?ÿy¿äÿ°é6¡ë2Á7މdÔžÜ<%e¾ïˈ¬úÂÃÎWõñ«üŸ²OüŸÉÿe8¯øÿwÁ×óÿ7 ¡?‰ÿC y¤Èøñþm©J,KeŠ(S€*%Ç1"5Ö»P¶bZœþ“ÿ#ÉSdlÿ÷ü_@!¾úþ/Ô‚€H."5‚*•€‰…mÝÒT`SÉéLL9ó÷ð{·ôtšu»7ò$‡Ûd·Þ¯²ø‘vœ×+iiâ4‰žÄp:bÛ<´Ñl=¥Í';מÖ2ÉRÓ£“çt6Yêža»ËÛ5ÃAörÉÖûºê?[ŽŸ{0ضÏ*{M/ñ±n¬“³s‚µÅx¶_õçËÖù8w·Ì|íöÖõ´Íz¼@çC7ÔìÓ ]¼¦ñ,·sšýqüQ[ÅÝù9{Ê{Ɔ§ô¾k]9íƒÀŽmkÖ뿘^~‚G¯-<Ú®i=\Ÿ„~*Ô×{«³¶ï³
ѧNkÓ ÎËÙŠðÃj
âæ0žë¹³çñ–ìÍiêäÎLR»'îI<šý¬¹1N‚Ïðr5^N0±
o³ÙÜOIú«øßw»éóÑŒîÿëü¿Ðâ…/”x!Ä^ÈðB…"¼Ðà…¯”Â¥p¥*¥P)…7ûR¨i€;y