コード例 #1
0
class ProtocolTests(unittest.TestCase):
    def setUp(self):
        self.protocol = TorControlProtocol()
        self.protocol.connectionMade = lambda: None
        self.transport = proto_helpers.StringTransport()
        self.protocol.makeConnection(self.transport)

    def tearDown(self):
        self.protocol = None

    def send(self, line):
        assert type(line) == bytes
        self.protocol.dataReceived(line.strip() + b"\r\n")

    def test_statemachine_broadcast_no_code(self):
        try:
            self.protocol._broadcast_response("foo")
            self.fail()
        except RuntimeError as e:
            self.assertTrue('No code set yet' in str(e))

    def test_statemachine_broadcast_unknown_code(self):
        try:
            self.protocol.code = 999
            self.protocol._broadcast_response("foo")
            self.fail()
        except RuntimeError as e:
            self.assertTrue('Unknown code' in str(e))

    def test_statemachine_is_finish(self):
        self.assertTrue(not self.protocol._is_finish_line(''))
        self.assertTrue(self.protocol._is_finish_line('.'))
        self.assertTrue(self.protocol._is_finish_line('300 '))
        self.assertTrue(not self.protocol._is_finish_line('250-'))

    def test_statemachine_singleline(self):
        self.assertTrue(not self.protocol._is_single_line_response('foo'))

    def test_statemachine_continuation(self):
        try:
            self.protocol.code = 250
            self.protocol._is_continuation_line("123 ")
            self.fail()
        except RuntimeError as e:
            self.assertTrue('Unexpected code' in str(e))

    def test_statemachine_multiline(self):
        try:
            self.protocol.code = 250
            self.protocol._is_multi_line("123 ")
            self.fail()
        except RuntimeError as e:
            self.assertTrue('Unexpected code' in str(e))

    def test_response_with_no_request(self):
        with self.assertRaises(RuntimeError) as ctx:
            self.protocol.code = 200
            self.protocol._broadcast_response('200 OK')
        self.assertTrue("didn't issue a command" in str(ctx.exception))

    def auth_failed(self, msg):
        self.assertEqual(str(msg.value), '551 go away')
        self.got_auth_failed = True

    def test_authenticate_fail(self):
        self.got_auth_failed = False
        self.protocol._auth_failed = self.auth_failed

        self.protocol.password_function = lambda: 'foo'
        self.protocol._do_authenticate('''PROTOCOLINFO 1
AUTH METHODS=HASHEDPASSWORD
VERSION Tor="0.2.2.35"
OK''')
        self.send(b'551 go away\r\n')
        self.assertTrue(self.got_auth_failed)

    def test_authenticate_no_auth_line(self):
        try:
            self.protocol._do_authenticate('''PROTOCOLINFO 1
FOOAUTH METHODS=COOKIE,SAFECOOKIE COOKIEFILE="/dev/null"
VERSION Tor="0.2.2.35"
OK''')
            self.assertTrue(False)
        except RuntimeError as e:
            self.assertTrue('find AUTH line' in str(e))

    def test_authenticate_not_enough_cookie_data(self):
        with tempfile.NamedTemporaryFile() as cookietmp:
            cookietmp.write(b'x' * 35)  # too much data
            cookietmp.flush()

            try:
                self.protocol._do_authenticate('''PROTOCOLINFO 1
AUTH METHODS=COOKIE COOKIEFILE="%s"
VERSION Tor="0.2.2.35"
OK''' % cookietmp.name)
                self.assertTrue(False)
            except RuntimeError as e:
                self.assertTrue('cookie to be 32' in str(e))

    def test_authenticate_not_enough_safecookie_data(self):
        with tempfile.NamedTemporaryFile() as cookietmp:
            cookietmp.write(b'x' * 35)  # too much data
            cookietmp.flush()

            try:
                self.protocol._do_authenticate('''PROTOCOLINFO 1
AUTH METHODS=SAFECOOKIE COOKIEFILE="%s"
VERSION Tor="0.2.2.35"
OK''' % cookietmp.name)
                self.assertTrue(False)
            except RuntimeError as e:
                self.assertTrue('cookie to be 32' in str(e))

    def test_authenticate_safecookie(self):
        with tempfile.NamedTemporaryFile() as cookietmp:
            cookiedata = bytes(bytearray([0] * 32))
            cookietmp.write(cookiedata)
            cookietmp.flush()

            self.protocol._do_authenticate('''PROTOCOLINFO 1
AUTH METHODS=SAFECOOKIE COOKIEFILE="{}"
VERSION Tor="0.2.2.35"
OK'''.format(cookietmp.name))
            self.assertTrue(
                b'AUTHCHALLENGE SAFECOOKIE ' in self.transport.value())
            x = self.transport.value().split()[-1]
            client_nonce = a2b_hex(x)
            self.transport.clear()
            server_nonce = bytes(bytearray([0] * 32))
            server_hash = hmac_sha256(
                b"Tor safe cookie authentication server-to-controller hash",
                cookiedata + client_nonce + server_nonce,
            )

            self.send(b'250 AUTHCHALLENGE SERVERHASH=' +
                      base64.b16encode(server_hash) + b' SERVERNONCE=' +
                      base64.b16encode(server_nonce) + b'\r\n')
            self.assertTrue(b'AUTHENTICATE ' in self.transport.value())

    def test_authenticate_cookie_without_reading(self):
        server_nonce = bytes(bytearray([0] * 32))
        server_hash = bytes(bytearray([0] * 32))
        try:
            self.protocol._safecookie_authchallenge(
                '250 AUTHCHALLENGE SERVERHASH=%s SERVERNONCE=%s' %
                (base64.b16encode(server_hash),
                 base64.b16encode(server_nonce)))
            self.assertTrue(False)
        except RuntimeError as e:
            self.assertTrue('not read' in str(e))

    def test_authenticate_unexisting_cookie_file(self):
        unexisting_file = __file__ + "-unexisting"
        try:
            self.protocol._do_authenticate('''PROTOCOLINFO 1
AUTH METHODS=COOKIE COOKIEFILE="%s"
VERSION Tor="0.2.2.35"
OK''' % unexisting_file)
            self.assertTrue(False)
        except RuntimeError:
            pass

    def test_authenticate_unexisting_safecookie_file(self):
        unexisting_file = __file__ + "-unexisting"
        try:
            self.protocol._do_authenticate('''PROTOCOLINFO 1
AUTH METHODS=SAFECOOKIE COOKIEFILE="{}"
VERSION Tor="0.2.2.35"
OK'''.format(unexisting_file))
            self.assertTrue(False)
        except RuntimeError:
            pass

    def test_authenticate_dont_send_cookiefile(self):
        try:
            self.protocol._do_authenticate('''PROTOCOLINFO 1
AUTH METHODS=SAFECOOKIE
VERSION Tor="0.2.2.35"
OK''')
            self.assertTrue(False)
        except RuntimeError:
            pass

    def test_authenticate_password_when_cookie_unavailable(self):
        unexisting_file = __file__ + "-unexisting"
        self.protocol.password_function = lambda: 'foo'
        self.protocol._do_authenticate('''PROTOCOLINFO 1
AUTH METHODS=COOKIE,HASHEDPASSWORD COOKIEFILE="{}"
VERSION Tor="0.2.2.35"
OK'''.format(unexisting_file))
        self.assertEqual(
            self.transport.value(),
            b'AUTHENTICATE ' + b2a_hex(b'foo') + b'\r\n',
        )

    def test_authenticate_password_when_safecookie_unavailable(self):
        unexisting_file = __file__ + "-unexisting"
        self.protocol.password_function = lambda: 'foo'
        self.protocol._do_authenticate('''PROTOCOLINFO 1
AUTH METHODS=SAFECOOKIE,HASHEDPASSWORD COOKIEFILE="{}"
VERSION Tor="0.2.2.35"
OK'''.format(unexisting_file))
        self.assertEqual(
            self.transport.value(),
            b'AUTHENTICATE ' + b2a_hex(b'foo') + b'\r\n',
        )

    def test_authenticate_safecookie_wrong_hash(self):
        cookiedata = bytes(bytearray([0] * 32))
        server_nonce = bytes(bytearray([0] * 32))
        server_hash = bytes(bytearray([0] * 32))

        # pretend we already did PROTOCOLINFO and read the cookie
        # file
        self.protocol._cookie_data = cookiedata
        self.protocol.client_nonce = server_nonce  # all 0's anyway
        try:
            self.protocol._safecookie_authchallenge(
                '250 AUTHCHALLENGE SERVERHASH={} SERVERNONCE={}'.format(
                    b2a_hex(server_hash).decode('ascii'),
                    b2a_hex(server_nonce).decode('ascii'),
                ))
            self.assertTrue(False)
        except RuntimeError as e:
            self.assertTrue('hash not expected' in str(e))

    def confirm_version_events(self, arg):
        self.assertEqual(self.protocol.version, 'foo')
        events = 'GUARD STREAM CIRC NS NEWCONSENSUS ORCONN NEWDESC ADDRMAP STATUS_GENERAL'.split(
        )
        self.assertEqual(len(self.protocol.valid_events), len(events))
        self.assertTrue(all(x in self.protocol.valid_events for x in events))

    def test_bootstrap_callback(self):
        d = self.protocol.post_bootstrap
        d.addCallback(CallbackChecker(self.protocol))
        d.addCallback(self.confirm_version_events)

        events = b'GUARD STREAM CIRC NS NEWCONSENSUS ORCONN NEWDESC ADDRMAP STATUS_GENERAL'
        self.protocol._bootstrap()

        # answer all the requests generated by boostrapping etc.
        self.send(b"250-signal/names=")
        self.send(b"250 OK")

        self.send(b"250-version=foo")
        self.send(b"250 OK")

        self.send(b"250-events/names=" + events)
        self.send(b"250 OK")

        self.send(b"250 OK")  # for USEFEATURE

        return d

    def test_bootstrap_tor_does_not_support_signal_names(self):
        self.protocol._bootstrap()
        self.send(b'552 Unrecognized key "signal/names"')
        valid_signals = ["RELOAD", "DUMP", "DEBUG", "NEWNYM", "CLEARDNSCACHE"]
        self.assertEqual(self.protocol.valid_signals, valid_signals)

    def test_async(self):
        """
        test the example from control-spec.txt to see that we
        handle interleaved async notifications properly.
        """
        self.protocol._set_valid_events('CIRC')
        self.protocol.add_event_listener('CIRC', lambda _: None)
        self.send(b"250 OK")

        d = self.protocol.get_conf("SOCKSPORT ORPORT")
        self.send(b"650 CIRC 1000 EXTENDED moria1,moria2")
        self.send(b"250-SOCKSPORT=9050")
        self.send(b"250 ORPORT=0")
        return d

    def test_async_multiline(self):
        # same as above, but i think the 650's can be multline,
        # too. Like:
        # 650-CIRC 1000 EXTENDED moria1,moria2 0xBEEF
        # 650-EXTRAMAGIC=99
        # 650 ANONYMITY=high

        self.protocol._set_valid_events('CIRC')
        self.protocol.add_event_listener(
            'CIRC',
            CallbackChecker(
                "1000 EXTENDED moria1,moria2\nEXTRAMAGIC=99\nANONYMITY=high"))
        self.send(b"250 OK")

        d = self.protocol.get_conf("SOCKSPORT ORPORT")
        d.addCallback(CallbackChecker({"ORPORT": "0", "SOCKSPORT": "9050"}))
        self.send(b"650-CIRC 1000 EXTENDED moria1,moria2")
        self.send(b"650-EXTRAMAGIC=99")
        self.send(b"650 ANONYMITY=high")
        self.send(b"250-SOCKSPORT=9050")
        self.send(b"250 ORPORT=0")
        return d

    def test_multiline_plus(self):
        """
        """

        d = self.protocol.get_info("FOO")
        d.addCallback(CallbackChecker({"FOO": "\na\nb\nc"}))
        self.send(b"250+FOO=")
        self.send(b"a")
        self.send(b"b")
        self.send(b"c")
        self.send(b".")
        self.send(b"250 OK")
        return d

    def test_multiline_plus_embedded_equals(self):
        """
        """

        d = self.protocol.get_info("FOO")
        d.addCallback(CallbackChecker({"FOO": "\na="}))
        self.send(b"250+FOO=")
        self.send(b"a=")
        self.send(b".")
        self.send(b"250 OK")
        return d

    def incremental_check(self, expected, actual):
        if '=' in actual:
            return
        self.assertEqual(expected, actual)

    def test_getinfo_incremental(self):
        d = self.protocol.get_info_incremental(
            "FOO", functools.partial(self.incremental_check, "bar"))
        self.send(b"250+FOO=")
        self.send(b"bar")
        self.send(b"bar")
        self.send(b".")
        self.send(b"250 OK")
        return d

    def test_getinfo_incremental_continuation(self):
        d = self.protocol.get_info_incremental(
            "FOO", functools.partial(self.incremental_check, "bar"))
        self.send(b"250-FOO=")
        self.send(b"250-bar")
        self.send(b"250-bar")
        self.send(b"250 OK")
        return d

    def test_getinfo_one_line(self):
        d = self.protocol.get_info("foo", )
        self.send(b'250 foo=bar')
        d.addCallback(
            lambda _: functools.partial(self.incremental_check, "bar"))
        return d

    def test_getconf(self):
        d = self.protocol.get_conf("SOCKSPORT ORPORT")
        d.addCallback(CallbackChecker({'SocksPort': '9050', 'ORPort': '0'}))
        self.send(b"250-SocksPort=9050")
        self.send(b"250 ORPort=0")
        return d

    def test_getconf_raw(self):
        d = self.protocol.get_conf_raw("SOCKSPORT ORPORT")
        d.addCallback(CallbackChecker('SocksPort=9050\nORPort=0'))
        self.send(b"250-SocksPort=9050")
        self.send(b"250 ORPort=0")
        return d

    def test_getconf_single(self):
        d = self.protocol.get_conf_single("SOCKSPORT")
        d.addCallback(CallbackChecker('9050'))
        self.send(b"250 SocksPort=9050")
        return d

    def response_ok(self, v):
        self.assertEqual(v, '')

    def test_setconf(self):
        d = self.protocol.set_conf("foo", "bar").addCallback(
            functools.partial(self.response_ok))
        self.send(b"250 OK")
        self._wait(d)
        self.assertEqual(self.transport.value(), b"SETCONF foo=bar\r\n")

    def test_setconf_with_space(self):
        d = self.protocol.set_conf("foo", "a value with a space")
        d.addCallback(functools.partial(self.response_ok))
        self.send(b"250 OK")
        self._wait(d)
        self.assertEqual(self.transport.value(),
                         b'SETCONF foo="a value with a space"\r\n')

    def test_setconf_multi(self):
        d = self.protocol.set_conf("foo", "bar", "baz", 1)
        self.send(b"250 OK")
        self._wait(d)
        self.assertEqual(
            self.transport.value(),
            b"SETCONF foo=bar baz=1\r\n",
        )

    def test_quit(self):
        d = self.protocol.quit()
        self.send(b"250 OK")
        self._wait(d)
        self.assertEqual(
            self.transport.value(),
            b"QUIT\r\n",
        )

    def test_dot(self):
        # just checking we don't expode
        self.protocol.graphviz_data()

    def test_debug(self):
        self.protocol.start_debug()
        self.assertTrue(exists('txtorcon-debug.log'))

    def error(self, failure):
        print("ERROR", failure)
        self.assertTrue(False)

    def test_twocommands(self):
        "Two commands on the wire before first response."
        d1 = self.protocol.get_conf("FOO")
        ht = {"a": "one", "b": "two"}
        d1.addCallback(CallbackChecker(ht)).addErrback(log.err)

        d2 = self.protocol.get_info_raw("BAR")
        d2.addCallback(CallbackChecker("bar")).addErrback(log.err)

        self.send(b"250-a=one")
        self.send(b"250-b=two")
        self.send(b"250 OK")
        self.send(b"250 bar")

        return d2

    def test_signal_error(self):
        try:
            self.protocol.signal('FOO')
            self.fail()
        except Exception as e:
            self.assertTrue('Invalid signal' in str(e))

    def test_signal(self):
        self.protocol.valid_signals = ['NEWNYM']
        self.protocol.signal('NEWNYM')
        self.assertEqual(
            self.transport.value(),
            b'SIGNAL NEWNYM\r\n',
        )

    def test_650_after_authenticate(self):
        self.protocol._set_valid_events('CONF_CHANGED')
        self.protocol.add_event_listener('CONF_CHANGED',
                                         CallbackChecker("Foo=bar"))
        self.send(b"250 OK")

        self.send(b"650-CONF_CHANGED")
        self.send(b"650-Foo=bar")

    def test_notify_after_getinfo(self):
        self.protocol._set_valid_events('CIRC')
        self.protocol.add_event_listener(
            'CIRC', CallbackChecker("1000 EXTENDED moria1,moria2"))
        self.send(b"250 OK")

        d = self.protocol.get_info("a")
        d.addCallback(CallbackChecker({'a': 'one'})).addErrback(self.fail)
        self.send(b"250-a=one")
        self.send(b"250 OK")
        self.send(b"650 CIRC 1000 EXTENDED moria1,moria2")
        return d

    def test_notify_error(self):
        self.protocol._set_valid_events('CIRC')
        self.send(b"650 CIRC 1000 EXTENDED moria1,moria2")

    def test_getinfo(self):
        d = self.protocol.get_info("version")
        d.addCallback(CallbackChecker({'version': '0.2.2.34'}))
        d.addErrback(self.fail)

        self.send(b"250-version=0.2.2.34")
        self.send(b"250 OK")

        self.assertEqual(
            self.transport.value(),
            b"GETINFO version\r\n",
        )
        return d

    def test_getinfo_single(self):
        d = self.protocol.get_info_single("version")
        d.addCallback(CallbackChecker('0.2.2.34'))
        d.addErrback(self.fail)

        self.send(b"250-version=0.2.2.34")
        self.send(b"250 OK")

        self.assertEqual(
            self.transport.value(),
            b"GETINFO version\r\n",
        )
        return d

    def test_getinfo_for_descriptor(self):
        descriptor_info = b"""250+desc/name/moria1=
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-----
.
250 OK"""
        d = self.protocol.get_info("desc/name/moria1")
        d.addCallback(
            CallbackChecker({
                'desc/name/moria1':
                '\n' +
                '\n'.join(descriptor_info.decode('ascii').split('\n')[1:-2])
            }))
        d.addErrback(self.fail)

        for line in descriptor_info.split(b'\n'):
            self.send(line)
        return d

    def test_getinfo_multiline(self):
        descriptor_info = b"""250+desc/name/moria1=
router moria1 128.31.0.34 9101 0 9131
platform Tor 0.2.5.0-alpha-dev on Linux
.
250 OK"""
        d = self.protocol.get_info("desc/name/moria1")
        gold = "\nrouter moria1 128.31.0.34 9101 0 9131\nplatform Tor 0.2.5.0-alpha-dev on Linux"
        d.addCallback(CallbackChecker({'desc/name/moria1': gold}))
        d.addErrback(self.fail)

        for line in descriptor_info.split(b'\n'):
            self.send(line)
        return d

    def test_addevent(self):
        self.protocol._set_valid_events('FOO BAR')

        self.protocol.add_event_listener('FOO', lambda _: None)
        # is it dangerous/ill-advised to depend on internal state of
        # class under test?
        d = self.protocol.defer
        self.send(b"250 OK")
        self._wait(d)
        self.assertEqual(self.transport.value().split(b'\r\n')[-2],
                         b"SETEVENTS FOO")
        self.transport.clear()

        self.protocol.add_event_listener('BAR', lambda _: None)
        d = self.protocol.defer
        self.send(b"250 OK")
        self.assertTrue(self.transport.value() == b"SETEVENTS FOO BAR\r\n"
                        or self.transport.value() == b"SETEVENTS BAR FOO\r\n")
        self._wait(d)

        try:
            self.protocol.add_event_listener('SOMETHING_INVALID',
                                             lambda _: None)
            self.assertTrue(False)
        except Exception:
            pass

    def test_eventlistener(self):
        self.protocol._set_valid_events('STREAM')

        class EventListener(object):
            stream_events = 0

            def __call__(self, data):
                self.stream_events += 1

        listener = EventListener()
        self.protocol.add_event_listener('STREAM', listener)

        d = self.protocol.defer
        self.send(b"250 OK")
        self._wait(d)
        self.send(b"650 STREAM 1234 NEW 4321 1.2.3.4:555 REASON=MISC")
        self.send(b"650 STREAM 2345 NEW 4321 2.3.4.5:666 REASON=MISC")
        self.assertEqual(listener.stream_events, 2)

    def test_eventlistener_error(self):
        self.protocol._set_valid_events('STREAM')

        class EventListener(object):
            stream_events = 0
            do_error = False

            def __call__(self, data):
                self.stream_events += 1
                if self.do_error:
                    raise Exception("the bad thing happened")

        # we make sure the first listener has the errors to prove the
        # second one still gets called.
        listener0 = EventListener()
        listener0.do_error = True
        listener1 = EventListener()
        self.protocol.add_event_listener('STREAM', listener0)
        self.protocol.add_event_listener('STREAM', listener1)

        d = self.protocol.defer
        self.send(b"250 OK")
        self._wait(d)
        self.send(b"650 STREAM 1234 NEW 4321 1.2.3.4:555 REASON=MISC")
        self.send(b"650 STREAM 2345 NEW 4321 2.3.4.5:666 REASON=MISC")
        self.assertEqual(listener0.stream_events, 2)
        self.assertEqual(listener1.stream_events, 2)

        # should have logged the two errors
        logged = self.flushLoggedErrors()
        self.assertEqual(2, len(logged))
        self.assertTrue("the bad thing happened" in str(logged[0]))
        self.assertTrue("the bad thing happened" in str(logged[1]))

    def test_remove_eventlistener(self):
        self.protocol._set_valid_events('STREAM')

        class EventListener(object):
            stream_events = 0

            def __call__(self, data):
                self.stream_events += 1

        listener = EventListener()
        self.protocol.add_event_listener('STREAM', listener)
        self.assertEqual(self.transport.value(), b'SETEVENTS STREAM\r\n')
        self.protocol.lineReceived(b"250 OK")
        self.transport.clear()
        self.protocol.remove_event_listener('STREAM', listener)
        self.assertEqual(self.transport.value(), b'SETEVENTS \r\n')

    def test_remove_eventlistener_multiple(self):
        self.protocol._set_valid_events('STREAM')

        class EventListener(object):
            stream_events = 0

            def __call__(self, data):
                self.stream_events += 1

        listener0 = EventListener()
        listener1 = EventListener()
        self.protocol.add_event_listener('STREAM', listener0)
        self.assertEqual(self.transport.value(), b'SETEVENTS STREAM\r\n')
        self.protocol.lineReceived(b"250 OK")
        self.transport.clear()
        # add another one, shouldn't issue a tor command
        self.protocol.add_event_listener('STREAM', listener1)
        self.assertEqual(self.transport.value(), b'')

        # remove one, should still not issue a tor command
        self.protocol.remove_event_listener('STREAM', listener0)
        self.assertEqual(self.transport.value(), b'')

        # remove the other one, NOW should issue a command
        self.protocol.remove_event_listener('STREAM', listener1)
        self.assertEqual(self.transport.value(), b'SETEVENTS \r\n')

        # try removing invalid event
        try:
            self.protocol.remove_event_listener('FOO', listener0)
            self.fail()
        except Exception as e:
            self.assertTrue('FOO' in str(e))

    def test_continuation_line(self):
        d = self.protocol.get_info_raw("key")

        def check_continuation(v):
            self.assertEqual(v, "key=\nvalue0\nvalue1")

        d.addCallback(check_continuation)

        self.send(b"250+key=")
        self.send(b"value0")
        self.send(b"value1")
        self.send(b".")
        self.send(b"250 OK")

        return d

    def test_newdesc(self):
        """
        FIXME: this test is now maybe a little silly, it's just testing
        multiline GETINFO...  (Real test is in
        TorStateTests.test_newdesc_parse)
        """

        self.protocol.get_info_raw(
            'ns/id/624926802351575FF7E4E3D60EFA3BFB56E67E8A')
        d = self.protocol.defer
        d.addCallback(
            CallbackChecker("""ns/id/624926802351575FF7E4E3D60EFA3BFB56E67E8A=
r fake YkkmgCNRV1/35OPWDvo7+1bmfoo tanLV/4ZfzpYQW0xtGFqAa46foo 2011-12-12 16:29:16 12.45.56.78 443 80
s Exit Fast Guard HSDir Named Running Stable V2Dir Valid
w Bandwidth=518000
p accept 43,53,79-81,110,143,194,220,443,953,989-990,993,995,1194,1293,1723,1863,2082-2083,2086-2087,2095-2096,3128,4321,5050,5190,5222-5223,6679,6697,7771,8000,8008,8080-8081,8090,8118,8123,8181,8300,8443,8888"""
                            ))

        self.send(b"250+ns/id/624926802351575FF7E4E3D60EFA3BFB56E67E8A=")
        self.send(
            b"r fake YkkmgCNRV1/35OPWDvo7+1bmfoo tanLV/4ZfzpYQW0xtGFqAa46foo 2011-12-12 16:29:16 12.45.56.78 443 80"
        )
        self.send(b"s Exit Fast Guard HSDir Named Running Stable V2Dir Valid")
        self.send(b"w Bandwidth=518000")
        self.send(
            b"p accept 43,53,79-81,110,143,194,220,443,953,989-990,993,995,1194,1293,1723,1863,2082-2083,2086-2087,2095-2096,3128,4321,5050,5190,5222-5223,6679,6697,7771,8000,8008,8080-8081,8090,8118,8123,8181,8300,8443,8888"
        )
        self.send(b".")
        self.send(b"250 OK")

        return d

    def test_plus_line_no_command(self):
        self.protocol.lineReceived(b"650+NS\r\n")
        self.protocol.lineReceived(
            b"r Gabor gFpAHsFOHGATy12ZUswRf0ZrqAU GG6GDp40cQfR3ODvkBT0r+Q09kw 2012-05-12 16:54:56 91.219.238.71 443 80\r\n"
        )

    def test_minus_line_no_command(self):
        """
        haven't seen 600's use - "in the wild" but don't see why it's not
        possible
        """
        self.protocol._set_valid_events('NS')
        self.protocol.add_event_listener('NS', lambda _: None)
        self.protocol.lineReceived(b"650-NS\r\n")
        self.protocol.lineReceived(b"650 OK\r\n")
コード例 #2
0
class ProtocolTests(unittest.TestCase):

    def setUp(self):
        self.protocol = TorControlProtocol()
        self.protocol.connectionMade = lambda: None
        self.transport = proto_helpers.StringTransport()
        self.protocol.makeConnection(self.transport)

    def tearDown(self):
        self.protocol = None

    def send(self, line):
        assert type(line) == bytes
        self.protocol.dataReceived(line.strip() + b"\r\n")

    def test_statemachine_broadcast_no_code(self):
        try:
            self.protocol._broadcast_response("foo")
            self.fail()
        except RuntimeError as e:
            self.assertTrue('No code set yet' in str(e))

    def test_statemachine_broadcast_unknown_code(self):
        try:
            self.protocol.code = 999
            self.protocol._broadcast_response("foo")
            self.fail()
        except RuntimeError as e:
            self.assertTrue('Unknown code' in str(e))

    def test_statemachine_is_finish(self):
        self.assertTrue(not self.protocol._is_finish_line(''))
        self.assertTrue(self.protocol._is_finish_line('.'))
        self.assertTrue(self.protocol._is_finish_line('300 '))
        self.assertTrue(not self.protocol._is_finish_line('250-'))

    def test_statemachine_singleline(self):
        self.assertTrue(not self.protocol._is_single_line_response('foo'))

    def test_statemachine_continuation(self):
        try:
            self.protocol.code = 250
            self.protocol._is_continuation_line("123 ")
            self.fail()
        except RuntimeError as e:
            self.assertTrue('Unexpected code' in str(e))

    def test_statemachine_multiline(self):
        try:
            self.protocol.code = 250
            self.protocol._is_multi_line("123 ")
            self.fail()
        except RuntimeError as e:
            self.assertTrue('Unexpected code' in str(e))

    def test_response_with_no_request(self):
        with self.assertRaises(RuntimeError) as ctx:
            self.protocol.code = 200
            self.protocol._broadcast_response('200 OK')
        self.assertTrue(
            "didn't issue a command" in str(ctx.exception)
        )

    def auth_failed(self, msg):
        self.assertEqual(str(msg.value), '551 go away')
        self.got_auth_failed = True

    def test_authenticate_fail(self):
        self.got_auth_failed = False
        self.protocol._auth_failed = self.auth_failed

        self.protocol.password_function = lambda: 'foo'
        self.protocol._do_authenticate('''PROTOCOLINFO 1
AUTH METHODS=HASHEDPASSWORD
VERSION Tor="0.2.2.35"
OK''')
        self.send(b'551 go away\r\n')
        self.assertTrue(self.got_auth_failed)

    def test_authenticate_no_auth_line(self):
        try:
            self.protocol._do_authenticate('''PROTOCOLINFO 1
FOOAUTH METHODS=COOKIE,SAFECOOKIE COOKIEFILE="/dev/null"
VERSION Tor="0.2.2.35"
OK''')
            self.assertTrue(False)
        except RuntimeError as e:
            self.assertTrue('find AUTH line' in str(e))

    def test_authenticate_not_enough_cookie_data(self):
        with tempfile.NamedTemporaryFile() as cookietmp:
            cookietmp.write(b'x' * 35)  # too much data
            cookietmp.flush()

            try:
                self.protocol._do_authenticate('''PROTOCOLINFO 1
AUTH METHODS=COOKIE COOKIEFILE="%s"
VERSION Tor="0.2.2.35"
OK''' % cookietmp.name)
                self.assertTrue(False)
            except RuntimeError as e:
                self.assertTrue('cookie to be 32' in str(e))

    def test_authenticate_not_enough_safecookie_data(self):
        with tempfile.NamedTemporaryFile() as cookietmp:
            cookietmp.write(b'x' * 35)  # too much data
            cookietmp.flush()

            try:
                self.protocol._do_authenticate('''PROTOCOLINFO 1
AUTH METHODS=SAFECOOKIE COOKIEFILE="%s"
VERSION Tor="0.2.2.35"
OK''' % cookietmp.name)
                self.assertTrue(False)
            except RuntimeError as e:
                self.assertTrue('cookie to be 32' in str(e))

    def test_authenticate_safecookie(self):
        with tempfile.NamedTemporaryFile() as cookietmp:
            cookiedata = bytes(bytearray([0] * 32))
            cookietmp.write(cookiedata)
            cookietmp.flush()

            self.protocol._do_authenticate('''PROTOCOLINFO 1
AUTH METHODS=SAFECOOKIE COOKIEFILE="{}"
VERSION Tor="0.2.2.35"
OK'''.format(cookietmp.name))
            self.assertTrue(
                b'AUTHCHALLENGE SAFECOOKIE ' in self.transport.value()
            )
            x = self.transport.value().split()[-1]
            client_nonce = a2b_hex(x)
            self.transport.clear()
            server_nonce = bytes(bytearray([0] * 32))
            server_hash = hmac_sha256(
                b"Tor safe cookie authentication server-to-controller hash",
                cookiedata + client_nonce + server_nonce,
            )

            self.send(
                b'250 AUTHCHALLENGE SERVERHASH=' +
                base64.b16encode(server_hash) + b' SERVERNONCE=' +
                base64.b16encode(server_nonce) + b'\r\n'
            )
            self.assertTrue(b'AUTHENTICATE ' in self.transport.value())

    def test_authenticate_cookie_without_reading(self):
        server_nonce = bytes(bytearray([0] * 32))
        server_hash = bytes(bytearray([0] * 32))
        try:
            self.protocol._safecookie_authchallenge(
                '250 AUTHCHALLENGE SERVERHASH=%s SERVERNONCE=%s' %
                (base64.b16encode(server_hash), base64.b16encode(server_nonce))
            )
            self.assertTrue(False)
        except RuntimeError as e:
            self.assertTrue('not read' in str(e))

    def test_authenticate_unexisting_cookie_file(self):
        unexisting_file = __file__ + "-unexisting"
        try:
            self.protocol._do_authenticate('''PROTOCOLINFO 1
AUTH METHODS=COOKIE COOKIEFILE="%s"
VERSION Tor="0.2.2.35"
OK''' % unexisting_file)
            self.assertTrue(False)
        except RuntimeError:
            pass

    def test_authenticate_unexisting_safecookie_file(self):
        unexisting_file = __file__ + "-unexisting"
        try:
            self.protocol._do_authenticate('''PROTOCOLINFO 1
AUTH METHODS=SAFECOOKIE COOKIEFILE="{}"
VERSION Tor="0.2.2.35"
OK'''.format(unexisting_file))
            self.assertTrue(False)
        except RuntimeError:
            pass

    def test_authenticate_dont_send_cookiefile(self):
        try:
            self.protocol._do_authenticate('''PROTOCOLINFO 1
AUTH METHODS=SAFECOOKIE
VERSION Tor="0.2.2.35"
OK''')
            self.assertTrue(False)
        except RuntimeError:
            pass

    def test_authenticate_password_when_cookie_unavailable(self):
        unexisting_file = __file__ + "-unexisting"
        self.protocol.password_function = lambda: 'foo'
        self.protocol._do_authenticate('''PROTOCOLINFO 1
AUTH METHODS=COOKIE,HASHEDPASSWORD COOKIEFILE="{}"
VERSION Tor="0.2.2.35"
OK'''.format(unexisting_file))
        self.assertEqual(
            self.transport.value(),
            b'AUTHENTICATE ' + b2a_hex(b'foo') + b'\r\n',
        )

    def test_authenticate_password_when_safecookie_unavailable(self):
        unexisting_file = __file__ + "-unexisting"
        self.protocol.password_function = lambda: 'foo'
        self.protocol._do_authenticate('''PROTOCOLINFO 1
AUTH METHODS=SAFECOOKIE,HASHEDPASSWORD COOKIEFILE="{}"
VERSION Tor="0.2.2.35"
OK'''.format(unexisting_file))
        self.assertEqual(
            self.transport.value(),
            b'AUTHENTICATE ' + b2a_hex(b'foo') + b'\r\n',
        )

    def test_authenticate_safecookie_wrong_hash(self):
        cookiedata = bytes(bytearray([0] * 32))
        server_nonce = bytes(bytearray([0] * 32))
        server_hash = bytes(bytearray([0] * 32))

        # pretend we already did PROTOCOLINFO and read the cookie
        # file
        self.protocol._cookie_data = cookiedata
        self.protocol.client_nonce = server_nonce  # all 0's anyway
        try:
            self.protocol._safecookie_authchallenge(
                '250 AUTHCHALLENGE SERVERHASH={} SERVERNONCE={}'.format(
                    b2a_hex(server_hash).decode('ascii'),
                    b2a_hex(server_nonce).decode('ascii'),
                )
            )
            self.assertTrue(False)
        except RuntimeError as e:
            self.assertTrue('hash not expected' in str(e))

    def confirm_version_events(self, arg):
        self.assertEqual(self.protocol.version, 'foo')
        events = 'GUARD STREAM CIRC NS NEWCONSENSUS ORCONN NEWDESC ADDRMAP STATUS_GENERAL'.split()
        self.assertEqual(len(self.protocol.valid_events), len(events))
        self.assertTrue(all(x in self.protocol.valid_events for x in events))

    def test_bootstrap_callback(self):
        d = self.protocol.post_bootstrap
        d.addCallback(CallbackChecker(self.protocol))
        d.addCallback(self.confirm_version_events)

        events = b'GUARD STREAM CIRC NS NEWCONSENSUS ORCONN NEWDESC ADDRMAP STATUS_GENERAL'
        self.protocol._bootstrap()

        # answer all the requests generated by boostrapping etc.
        self.send(b"250-signal/names=")
        self.send(b"250 OK")

        self.send(b"250-version=foo")
        self.send(b"250 OK")

        self.send(b"250-events/names=" + events)
        self.send(b"250 OK")

        self.send(b"250 OK")  # for USEFEATURE

        return d

    def test_bootstrap_tor_does_not_support_signal_names(self):
        self.protocol._bootstrap()
        self.send(b'552 Unrecognized key "signal/names"')
        valid_signals = ["RELOAD", "DUMP", "DEBUG", "NEWNYM", "CLEARDNSCACHE"]
        self.assertEqual(self.protocol.valid_signals, valid_signals)

    def test_async(self):
        """
        test the example from control-spec.txt to see that we
        handle interleaved async notifications properly.
        """
        self.protocol._set_valid_events('CIRC')
        self.protocol.add_event_listener('CIRC', lambda _: None)
        self.send(b"250 OK")

        d = self.protocol.get_conf("SOCKSPORT ORPORT")
        self.send(b"650 CIRC 1000 EXTENDED moria1,moria2")
        self.send(b"250-SOCKSPORT=9050")
        self.send(b"250 ORPORT=0")
        return d

    def test_async_multiline(self):
        # same as above, but i think the 650's can be multline,
        # too. Like:
        # 650-CIRC 1000 EXTENDED moria1,moria2 0xBEEF
        # 650-EXTRAMAGIC=99
        # 650 ANONYMITY=high

        self.protocol._set_valid_events('CIRC')
        self.protocol.add_event_listener(
            'CIRC',
            CallbackChecker(
                "1000 EXTENDED moria1,moria2\nEXTRAMAGIC=99\nANONYMITY=high"
            )
        )
        self.send(b"250 OK")

        d = self.protocol.get_conf("SOCKSPORT ORPORT")
        d.addCallback(CallbackChecker({"ORPORT": "0", "SOCKSPORT": "9050"}))
        self.send(b"650-CIRC 1000 EXTENDED moria1,moria2")
        self.send(b"650-EXTRAMAGIC=99")
        self.send(b"650 ANONYMITY=high")
        self.send(b"250-SOCKSPORT=9050")
        self.send(b"250 ORPORT=0")
        return d

    def test_multiline_plus(self):
        """
        """

        d = self.protocol.get_info("FOO")
        d.addCallback(CallbackChecker({"FOO": "\na\nb\nc"}))
        self.send(b"250+FOO=")
        self.send(b"a")
        self.send(b"b")
        self.send(b"c")
        self.send(b".")
        self.send(b"250 OK")
        return d

    def test_multiline_plus_embedded_equals(self):
        """
        """

        d = self.protocol.get_info("FOO")
        d.addCallback(CallbackChecker({"FOO": "\na="}))
        self.send(b"250+FOO=")
        self.send(b"a=")
        self.send(b".")
        self.send(b"250 OK")
        return d

    def incremental_check(self, expected, actual):
        if '=' in actual:
            return
        self.assertEqual(expected, actual)

    def test_getinfo_incremental(self):
        d = self.protocol.get_info_incremental(
            "FOO",
            functools.partial(self.incremental_check, "bar")
        )
        self.send(b"250+FOO=")
        self.send(b"bar")
        self.send(b"bar")
        self.send(b".")
        self.send(b"250 OK")
        return d

    def test_getinfo_incremental_continuation(self):
        d = self.protocol.get_info_incremental(
            "FOO",
            functools.partial(self.incremental_check, "bar")
        )
        self.send(b"250-FOO=")
        self.send(b"250-bar")
        self.send(b"250-bar")
        self.send(b"250 OK")
        return d

    def test_getinfo_one_line(self):
        d = self.protocol.get_info(
            "foo",
        )
        self.send(b'250 foo=bar')
        d.addCallback(lambda _: functools.partial(self.incremental_check, "bar"))
        return d

    def test_getconf(self):
        d = self.protocol.get_conf("SOCKSPORT ORPORT")
        d.addCallback(CallbackChecker({'SocksPort': '9050', 'ORPort': '0'}))
        self.send(b"250-SocksPort=9050")
        self.send(b"250 ORPort=0")
        return d

    def test_getconf_raw(self):
        d = self.protocol.get_conf_raw("SOCKSPORT ORPORT")
        d.addCallback(CallbackChecker('SocksPort=9050\nORPort=0'))
        self.send(b"250-SocksPort=9050")
        self.send(b"250 ORPort=0")
        return d

    def test_getconf_single(self):
        d = self.protocol.get_conf_single("SOCKSPORT")
        d.addCallback(CallbackChecker('9050'))
        self.send(b"250 SocksPort=9050")
        return d

    def response_ok(self, v):
        self.assertEqual(v, '')

    def test_setconf(self):
        d = self.protocol.set_conf("foo", "bar").addCallback(
            functools.partial(self.response_ok)
        )
        self.send(b"250 OK")
        self._wait(d)
        self.assertEqual(self.transport.value(), b"SETCONF foo=bar\r\n")

    def test_setconf_with_space(self):
        d = self.protocol.set_conf("foo", "a value with a space")
        d.addCallback(functools.partial(self.response_ok))
        self.send(b"250 OK")
        self._wait(d)
        self.assertEqual(
            self.transport.value(),
            b'SETCONF foo="a value with a space"\r\n'
        )

    def test_setconf_multi(self):
        d = self.protocol.set_conf("foo", "bar", "baz", 1)
        self.send(b"250 OK")
        self._wait(d)
        self.assertEqual(
            self.transport.value(),
            b"SETCONF foo=bar baz=1\r\n",
        )

    def test_quit(self):
        d = self.protocol.quit()
        self.send(b"250 OK")
        self._wait(d)
        self.assertEqual(
            self.transport.value(),
            b"QUIT\r\n",
        )

    def test_dot(self):
        # just checking we don't expode
        self.protocol.graphviz_data()

    def test_debug(self):
        self.protocol.start_debug()
        self.assertTrue(exists('txtorcon-debug.log'))

    def error(self, failure):
        print("ERROR", failure)
        self.assertTrue(False)

    def test_twocommands(self):
        "Two commands on the wire before first response."
        d1 = self.protocol.get_conf("FOO")
        ht = {"a": "one", "b": "two"}
        d1.addCallback(CallbackChecker(ht)).addErrback(log.err)

        d2 = self.protocol.get_info_raw("BAR")
        d2.addCallback(CallbackChecker("bar")).addErrback(log.err)

        self.send(b"250-a=one")
        self.send(b"250-b=two")
        self.send(b"250 OK")
        self.send(b"250 bar")

        return d2

    def test_signal_error(self):
        try:
            self.protocol.signal('FOO')
            self.fail()
        except Exception as e:
            self.assertTrue('Invalid signal' in str(e))

    def test_signal(self):
        self.protocol.valid_signals = ['NEWNYM']
        self.protocol.signal('NEWNYM')
        self.assertEqual(
            self.transport.value(),
            b'SIGNAL NEWNYM\r\n',
        )

    def test_650_after_authenticate(self):
        self.protocol._set_valid_events('CONF_CHANGED')
        self.protocol.add_event_listener(
            'CONF_CHANGED',
            CallbackChecker("Foo=bar")
        )
        self.send(b"250 OK")

        self.send(b"650-CONF_CHANGED")
        self.send(b"650-Foo=bar")

    def test_notify_after_getinfo(self):
        self.protocol._set_valid_events('CIRC')
        self.protocol.add_event_listener(
            'CIRC',
            CallbackChecker("1000 EXTENDED moria1,moria2")
        )
        self.send(b"250 OK")

        d = self.protocol.get_info("a")
        d.addCallback(CallbackChecker({'a': 'one'})).addErrback(self.fail)
        self.send(b"250-a=one")
        self.send(b"250 OK")
        self.send(b"650 CIRC 1000 EXTENDED moria1,moria2")
        return d

    def test_notify_error(self):
        self.protocol._set_valid_events('CIRC')
        self.send(b"650 CIRC 1000 EXTENDED moria1,moria2")

    def test_getinfo(self):
        d = self.protocol.get_info("version")
        d.addCallback(CallbackChecker({'version': '0.2.2.34'}))
        d.addErrback(self.fail)

        self.send(b"250-version=0.2.2.34")
        self.send(b"250 OK")

        self.assertEqual(
            self.transport.value(),
            b"GETINFO version\r\n",
        )
        return d

    def test_getinfo_single(self):
        d = self.protocol.get_info_single("version")
        d.addCallback(CallbackChecker('0.2.2.34'))
        d.addErrback(self.fail)

        self.send(b"250-version=0.2.2.34")
        self.send(b"250 OK")

        self.assertEqual(
            self.transport.value(),
            b"GETINFO version\r\n",
        )
        return d

    def test_getinfo_for_descriptor(self):
        descriptor_info = b"""250+desc/name/moria1=
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-----
.
250 OK"""
        d = self.protocol.get_info("desc/name/moria1")
        d.addCallback(CallbackChecker({'desc/name/moria1': '\n' + '\n'.join(descriptor_info.decode('ascii').split('\n')[1:-2])}))
        d.addErrback(self.fail)

        for line in descriptor_info.split(b'\n'):
            self.send(line)
        return d

    def test_getinfo_multiline(self):
        descriptor_info = b"""250+desc/name/moria1=
router moria1 128.31.0.34 9101 0 9131
platform Tor 0.2.5.0-alpha-dev on Linux
.
250 OK"""
        d = self.protocol.get_info("desc/name/moria1")
        gold = "\nrouter moria1 128.31.0.34 9101 0 9131\nplatform Tor 0.2.5.0-alpha-dev on Linux"
        d.addCallback(CallbackChecker({'desc/name/moria1': gold}))
        d.addErrback(self.fail)

        for line in descriptor_info.split(b'\n'):
            self.send(line)
        return d

    def test_addevent(self):
        self.protocol._set_valid_events('FOO BAR')

        self.protocol.add_event_listener('FOO', lambda _: None)
        # is it dangerous/ill-advised to depend on internal state of
        # class under test?
        d = self.protocol.defer
        self.send(b"250 OK")
        self._wait(d)
        self.assertEqual(
            self.transport.value().split(b'\r\n')[-2],
            b"SETEVENTS FOO"
        )
        self.transport.clear()

        self.protocol.add_event_listener('BAR', lambda _: None)
        d = self.protocol.defer
        self.send(b"250 OK")
        self.assertTrue(
            self.transport.value() == b"SETEVENTS FOO BAR\r\n" or
            self.transport.value() == b"SETEVENTS BAR FOO\r\n"
        )
        self._wait(d)

        try:
            self.protocol.add_event_listener(
                'SOMETHING_INVALID', lambda _: None
            )
            self.assertTrue(False)
        except Exception:
            pass

    def test_eventlistener(self):
        self.protocol._set_valid_events('STREAM')

        class EventListener(object):
            stream_events = 0

            def __call__(self, data):
                self.stream_events += 1

        listener = EventListener()
        self.protocol.add_event_listener('STREAM', listener)

        d = self.protocol.defer
        self.send(b"250 OK")
        self._wait(d)
        self.send(b"650 STREAM 1234 NEW 4321 1.2.3.4:555 REASON=MISC")
        self.send(b"650 STREAM 2345 NEW 4321 2.3.4.5:666 REASON=MISC")
        self.assertEqual(listener.stream_events, 2)

    def test_eventlistener_error(self):
        self.protocol._set_valid_events('STREAM')

        class EventListener(object):
            stream_events = 0
            do_error = False

            def __call__(self, data):
                self.stream_events += 1
                if self.do_error:
                    raise Exception("the bad thing happened")

        # we make sure the first listener has the errors to prove the
        # second one still gets called.
        listener0 = EventListener()
        listener0.do_error = True
        listener1 = EventListener()
        self.protocol.add_event_listener('STREAM', listener0)
        self.protocol.add_event_listener('STREAM', listener1)

        d = self.protocol.defer
        self.send(b"250 OK")
        self._wait(d)
        self.send(b"650 STREAM 1234 NEW 4321 1.2.3.4:555 REASON=MISC")
        self.send(b"650 STREAM 2345 NEW 4321 2.3.4.5:666 REASON=MISC")
        self.assertEqual(listener0.stream_events, 2)
        self.assertEqual(listener1.stream_events, 2)

        # should have logged the two errors
        logged = self.flushLoggedErrors()
        self.assertEqual(2, len(logged))
        self.assertTrue("the bad thing happened" in str(logged[0]))
        self.assertTrue("the bad thing happened" in str(logged[1]))

    def test_remove_eventlistener(self):
        self.protocol._set_valid_events('STREAM')

        class EventListener(object):
            stream_events = 0

            def __call__(self, data):
                self.stream_events += 1

        listener = EventListener()
        self.protocol.add_event_listener('STREAM', listener)
        self.assertEqual(self.transport.value(), b'SETEVENTS STREAM\r\n')
        self.protocol.lineReceived(b"250 OK")
        self.transport.clear()
        self.protocol.remove_event_listener('STREAM', listener)
        self.assertEqual(self.transport.value(), b'SETEVENTS \r\n')

    def test_remove_eventlistener_multiple(self):
        self.protocol._set_valid_events('STREAM')

        class EventListener(object):
            stream_events = 0

            def __call__(self, data):
                self.stream_events += 1

        listener0 = EventListener()
        listener1 = EventListener()
        self.protocol.add_event_listener('STREAM', listener0)
        self.assertEqual(self.transport.value(), b'SETEVENTS STREAM\r\n')
        self.protocol.lineReceived(b"250 OK")
        self.transport.clear()
        # add another one, shouldn't issue a tor command
        self.protocol.add_event_listener('STREAM', listener1)
        self.assertEqual(self.transport.value(), b'')

        # remove one, should still not issue a tor command
        self.protocol.remove_event_listener('STREAM', listener0)
        self.assertEqual(self.transport.value(), b'')

        # remove the other one, NOW should issue a command
        self.protocol.remove_event_listener('STREAM', listener1)
        self.assertEqual(self.transport.value(), b'SETEVENTS \r\n')

        # try removing invalid event
        try:
            self.protocol.remove_event_listener('FOO', listener0)
            self.fail()
        except Exception as e:
            self.assertTrue('FOO' in str(e))

    def test_continuation_line(self):
        d = self.protocol.get_info_raw("key")

        def check_continuation(v):
            self.assertEqual(v, "key=\nvalue0\nvalue1")
        d.addCallback(check_continuation)

        self.send(b"250+key=")
        self.send(b"value0")
        self.send(b"value1")
        self.send(b".")
        self.send(b"250 OK")

        return d

    def test_newdesc(self):
        """
        FIXME: this test is now maybe a little silly, it's just testing
        multiline GETINFO...  (Real test is in
        TorStateTests.test_newdesc_parse)
        """

        self.protocol.get_info_raw('ns/id/624926802351575FF7E4E3D60EFA3BFB56E67E8A')
        d = self.protocol.defer
        d.addCallback(CallbackChecker("""ns/id/624926802351575FF7E4E3D60EFA3BFB56E67E8A=
r fake YkkmgCNRV1/35OPWDvo7+1bmfoo tanLV/4ZfzpYQW0xtGFqAa46foo 2011-12-12 16:29:16 12.45.56.78 443 80
s Exit Fast Guard HSDir Named Running Stable V2Dir Valid
w Bandwidth=518000
p accept 43,53,79-81,110,143,194,220,443,953,989-990,993,995,1194,1293,1723,1863,2082-2083,2086-2087,2095-2096,3128,4321,5050,5190,5222-5223,6679,6697,7771,8000,8008,8080-8081,8090,8118,8123,8181,8300,8443,8888"""))

        self.send(b"250+ns/id/624926802351575FF7E4E3D60EFA3BFB56E67E8A=")
        self.send(b"r fake YkkmgCNRV1/35OPWDvo7+1bmfoo tanLV/4ZfzpYQW0xtGFqAa46foo 2011-12-12 16:29:16 12.45.56.78 443 80")
        self.send(b"s Exit Fast Guard HSDir Named Running Stable V2Dir Valid")
        self.send(b"w Bandwidth=518000")
        self.send(b"p accept 43,53,79-81,110,143,194,220,443,953,989-990,993,995,1194,1293,1723,1863,2082-2083,2086-2087,2095-2096,3128,4321,5050,5190,5222-5223,6679,6697,7771,8000,8008,8080-8081,8090,8118,8123,8181,8300,8443,8888")
        self.send(b".")
        self.send(b"250 OK")

        return d

    def test_plus_line_no_command(self):
        self.protocol.lineReceived(b"650+NS\r\n")
        self.protocol.lineReceived(b"r Gabor gFpAHsFOHGATy12ZUswRf0ZrqAU GG6GDp40cQfR3ODvkBT0r+Q09kw 2012-05-12 16:54:56 91.219.238.71 443 80\r\n")

    def test_minus_line_no_command(self):
        """
        haven't seen 600's use - "in the wild" but don't see why it's not
        possible
        """
        self.protocol._set_valid_events('NS')
        self.protocol.add_event_listener('NS', lambda _: None)
        self.protocol.lineReceived(b"650-NS\r\n")
        self.protocol.lineReceived(b"650 OK\r\n")