2016-05-22 18:31:00 +00:00
|
|
|
from __future__ import print_function
|
2016-05-23 07:14:39 +00:00
|
|
|
import os, json, re, gc
|
2016-05-23 01:40:44 +00:00
|
|
|
from binascii import hexlify, unhexlify
|
2016-05-22 18:31:00 +00:00
|
|
|
import mock
|
|
|
|
from twisted.trial import unittest
|
|
|
|
from twisted.internet import reactor
|
2016-05-23 01:40:44 +00:00
|
|
|
from twisted.internet.defer import Deferred, gatherResults, inlineCallbacks
|
|
|
|
from .common import ServerBase
|
|
|
|
from .. import wormhole
|
|
|
|
from spake2 import SPAKE2_Symmetric
|
2016-05-22 18:31:00 +00:00
|
|
|
from ..timing import DebugTiming
|
|
|
|
|
|
|
|
APPID = u"appid"
|
|
|
|
|
|
|
|
class MockWebSocket:
|
|
|
|
def __init__(self):
|
|
|
|
self._payloads = []
|
|
|
|
def sendMessage(self, payload, is_binary):
|
|
|
|
assert not is_binary
|
|
|
|
self._payloads.append(payload)
|
|
|
|
|
|
|
|
def outbound(self):
|
|
|
|
out = []
|
|
|
|
while self._payloads:
|
|
|
|
p = self._payloads.pop(0)
|
|
|
|
out.append(json.loads(p.decode("utf-8")))
|
|
|
|
return out
|
|
|
|
|
|
|
|
def response(w, **kwargs):
|
|
|
|
payload = json.dumps(kwargs).encode("utf-8")
|
|
|
|
w._ws_dispatch_response(payload)
|
|
|
|
|
|
|
|
class Welcome(unittest.TestCase):
|
2016-05-23 01:40:44 +00:00
|
|
|
def test_tolerate_no_current_version(self):
|
|
|
|
w = wormhole._WelcomeHandler(u"relay_url", u"current_version", None)
|
2016-05-22 18:31:00 +00:00
|
|
|
w.handle_welcome({})
|
|
|
|
|
2016-05-23 01:40:44 +00:00
|
|
|
def test_print_motd(self):
|
|
|
|
w = wormhole._WelcomeHandler(u"relay_url", u"current_version", None)
|
|
|
|
with mock.patch("sys.stderr") as stderr:
|
|
|
|
w.handle_welcome({u"motd": u"message of\nthe day"})
|
|
|
|
self.assertEqual(stderr.method_calls,
|
|
|
|
[mock.call.write(u"Server (at relay_url) says:\n"
|
|
|
|
" message of\n the day"),
|
|
|
|
mock.call.write(u"\n")])
|
|
|
|
# motd is only displayed once
|
|
|
|
with mock.patch("sys.stderr") as stderr2:
|
|
|
|
w.handle_welcome({u"motd": u"second message"})
|
|
|
|
self.assertEqual(stderr2.method_calls, [])
|
|
|
|
|
|
|
|
def test_current_version(self):
|
|
|
|
w = wormhole._WelcomeHandler(u"relay_url", u"2.0", None)
|
|
|
|
with mock.patch("sys.stderr") as stderr:
|
|
|
|
w.handle_welcome({u"current_version": u"2.0"})
|
|
|
|
self.assertEqual(stderr.method_calls, [])
|
|
|
|
|
|
|
|
with mock.patch("sys.stderr") as stderr:
|
|
|
|
w.handle_welcome({u"current_version": u"3.0"})
|
|
|
|
exp1 = (u"Warning: errors may occur unless both sides are"
|
|
|
|
" running the same version")
|
|
|
|
exp2 = (u"Server claims 3.0 is current, but ours is 2.0")
|
|
|
|
self.assertEqual(stderr.method_calls,
|
|
|
|
[mock.call.write(exp1),
|
|
|
|
mock.call.write(u"\n"),
|
|
|
|
mock.call.write(exp2),
|
|
|
|
mock.call.write(u"\n"),
|
|
|
|
])
|
|
|
|
|
|
|
|
# warning is only displayed once
|
|
|
|
with mock.patch("sys.stderr") as stderr:
|
|
|
|
w.handle_welcome({u"current_version": u"3.0"})
|
|
|
|
self.assertEqual(stderr.method_calls, [])
|
|
|
|
|
|
|
|
def test_non_release_version(self):
|
|
|
|
w = wormhole._WelcomeHandler(u"relay_url", u"2.0-dirty", None)
|
|
|
|
with mock.patch("sys.stderr") as stderr:
|
|
|
|
w.handle_welcome({u"current_version": u"3.0"})
|
|
|
|
self.assertEqual(stderr.method_calls, [])
|
|
|
|
|
|
|
|
def test_signal_error(self):
|
|
|
|
se = mock.Mock()
|
|
|
|
w = wormhole._WelcomeHandler(u"relay_url", u"2.0", se)
|
|
|
|
w.handle_welcome({})
|
|
|
|
self.assertEqual(se.mock_calls, [])
|
|
|
|
|
|
|
|
w.handle_welcome({u"error": u"oops"})
|
2016-05-23 07:14:39 +00:00
|
|
|
self.assertEqual(len(se.mock_calls), 1)
|
|
|
|
self.assertEqual(len(se.mock_calls[0][1]), 1) # posargs
|
|
|
|
we = se.mock_calls[0][1][0]
|
|
|
|
self.assertIsInstance(we, wormhole.WelcomeError)
|
|
|
|
self.assertEqual(we.args, (u"oops",))
|
|
|
|
# alas WelcomeError instances don't compare against each other
|
|
|
|
#self.assertEqual(se.mock_calls,
|
|
|
|
# [mock.call(wormhole.WelcomeError(u"oops"))])
|
2016-05-23 01:40:44 +00:00
|
|
|
|
|
|
|
class InputCode(unittest.TestCase):
|
|
|
|
def test_list(self):
|
|
|
|
send_command = mock.Mock()
|
|
|
|
ic = wormhole._InputCode(None, u"prompt", 2, send_command,
|
|
|
|
DebugTiming())
|
|
|
|
d = ic._list()
|
|
|
|
self.assertNoResult(d)
|
|
|
|
self.assertEqual(send_command.mock_calls, [mock.call(u"list")])
|
|
|
|
ic._response_handle_nameplates({u"type": u"nameplates",
|
|
|
|
u"nameplates": [{u"id": u"123"}]})
|
|
|
|
res = self.successResultOf(d)
|
|
|
|
self.assertEqual(res, [u"123"])
|
|
|
|
|
|
|
|
class GetCode(unittest.TestCase):
|
|
|
|
def test_get(self):
|
|
|
|
send_command = mock.Mock()
|
|
|
|
gc = wormhole._GetCode(2, send_command, DebugTiming())
|
|
|
|
d = gc.go()
|
|
|
|
self.assertNoResult(d)
|
|
|
|
self.assertEqual(send_command.mock_calls, [mock.call(u"allocate")])
|
|
|
|
# TODO: nameplate attributes get added and checked here
|
|
|
|
gc._response_handle_allocated({u"type": u"allocated",
|
|
|
|
u"nameplate": u"123"})
|
|
|
|
code = self.successResultOf(d)
|
|
|
|
self.assertIsInstance(code, type(u""))
|
|
|
|
self.assert_(code.startswith(u"123-"))
|
|
|
|
pieces = code.split(u"-")
|
|
|
|
self.assertEqual(len(pieces), 3) # nameplate plus two words
|
|
|
|
self.assert_(re.search(r'^\d+-\w+-\w+$', code), code)
|
|
|
|
|
2016-05-22 18:31:00 +00:00
|
|
|
class Basic(unittest.TestCase):
|
2016-05-23 07:14:39 +00:00
|
|
|
def tearDown(self):
|
|
|
|
# flush out any errorful Deferreds left dangling in cycles
|
|
|
|
gc.collect()
|
2016-05-23 01:40:44 +00:00
|
|
|
|
|
|
|
def check_out(self, out, **kwargs):
|
|
|
|
# Assert that each kwarg is present in the 'out' dict. Ignore other
|
|
|
|
# keys ('msgid' in particular)
|
|
|
|
for key, value in kwargs.items():
|
|
|
|
self.assertIn(key, out)
|
|
|
|
self.assertEqual(out[key], value, (out, key, value))
|
|
|
|
|
|
|
|
def check_outbound(self, ws, types):
|
|
|
|
out = ws.outbound()
|
|
|
|
self.assertEqual(len(out), len(types), (out, types))
|
|
|
|
for i,t in enumerate(types):
|
|
|
|
self.assertEqual(out[i][u"type"], t, (i,t,out))
|
|
|
|
return out
|
2016-05-22 18:31:00 +00:00
|
|
|
|
2016-05-23 07:14:39 +00:00
|
|
|
def make_pake(self, code, side, msg1):
|
|
|
|
sp2 = SPAKE2_Symmetric(wormhole.to_bytes(code),
|
|
|
|
idSymmetric=wormhole.to_bytes(APPID))
|
|
|
|
msg2 = sp2.start()
|
|
|
|
msg2_hex = hexlify(msg2).decode("ascii")
|
|
|
|
key = sp2.finish(msg1)
|
|
|
|
return key, msg2_hex
|
|
|
|
|
|
|
|
def test_create(self):
|
|
|
|
wormhole._Wormhole(APPID, u"relay_url", reactor, None, None)
|
|
|
|
|
2016-05-22 18:31:00 +00:00
|
|
|
def test_basic(self):
|
|
|
|
# We don't call w._start(), so this doesn't create a WebSocket
|
2016-05-23 01:40:44 +00:00
|
|
|
# connection. We provide a mock connection instead. If we wanted to
|
|
|
|
# exercise _connect, we'd mock out WSFactory.
|
2016-05-22 18:31:00 +00:00
|
|
|
# w._connect = lambda self: None
|
|
|
|
# w._event_connected(mock_ws)
|
|
|
|
# w._event_ws_opened()
|
|
|
|
# w._ws_dispatch_response(payload)
|
2016-05-23 01:40:44 +00:00
|
|
|
|
|
|
|
timing = DebugTiming()
|
|
|
|
with mock.patch("wormhole.wormhole._WelcomeHandler") as wh_c:
|
|
|
|
w = wormhole._Wormhole(APPID, u"relay_url", reactor, None, timing)
|
|
|
|
wh = wh_c.return_value
|
2016-05-22 18:31:00 +00:00
|
|
|
self.assertEqual(w._ws_url, u"relay_url")
|
2016-05-23 01:40:44 +00:00
|
|
|
self.assertTrue(w._flag_need_nameplate)
|
|
|
|
self.assertTrue(w._flag_need_to_build_msg1)
|
|
|
|
self.assertTrue(w._flag_need_to_send_PAKE)
|
|
|
|
|
|
|
|
v = w.get_verifier()
|
|
|
|
|
|
|
|
w._drop_connection = mock.Mock()
|
2016-05-22 18:31:00 +00:00
|
|
|
ws = MockWebSocket()
|
|
|
|
w._event_connected(ws)
|
|
|
|
out = ws.outbound()
|
|
|
|
self.assertEqual(len(out), 0)
|
|
|
|
|
|
|
|
w._event_ws_opened(None)
|
|
|
|
out = ws.outbound()
|
|
|
|
self.assertEqual(len(out), 1)
|
2016-05-23 01:40:44 +00:00
|
|
|
self.check_out(out[0], type=u"bind", appid=APPID, side=w._side)
|
2016-05-22 18:31:00 +00:00
|
|
|
self.assertIn(u"id", out[0])
|
|
|
|
|
2016-05-23 01:40:44 +00:00
|
|
|
# WelcomeHandler should get called upon 'welcome' response. Its full
|
|
|
|
# behavior is exercised in 'Welcome' above.
|
2016-05-22 18:31:00 +00:00
|
|
|
WELCOME = {u"foo": u"bar"}
|
|
|
|
response(w, type="welcome", welcome=WELCOME)
|
|
|
|
self.assertEqual(wh.mock_calls, [mock.call.handle_welcome(WELCOME)])
|
|
|
|
|
2016-05-23 01:40:44 +00:00
|
|
|
# because we're connected, setting the code also claims the mailbox
|
|
|
|
CODE = u"123-foo-bar"
|
|
|
|
w.set_code(CODE)
|
|
|
|
self.assertFalse(w._flag_need_to_build_msg1)
|
|
|
|
out = ws.outbound()
|
|
|
|
self.assertEqual(len(out), 1)
|
|
|
|
self.check_out(out[0], type=u"claim", nameplate=u"123")
|
|
|
|
|
|
|
|
# the server reveals the linked mailbox
|
|
|
|
response(w, type=u"claimed", mailbox=u"mb456")
|
|
|
|
|
|
|
|
# that triggers event_learned_mailbox, which should send open() and
|
|
|
|
# PAKE
|
|
|
|
self.assertTrue(w._mailbox_opened)
|
|
|
|
out = ws.outbound()
|
|
|
|
self.assertEqual(len(out), 2)
|
|
|
|
self.check_out(out[0], type=u"open", mailbox=u"mb456")
|
|
|
|
self.check_out(out[1], type=u"add", phase=u"pake")
|
|
|
|
self.assertNoResult(v)
|
|
|
|
|
|
|
|
# server echoes back all "add" messages
|
|
|
|
response(w, type=u"message", phase=u"pake", body=out[1][u"body"],
|
|
|
|
side=w._side)
|
|
|
|
self.assertNoResult(v)
|
|
|
|
|
|
|
|
# next we build the simulated peer's PAKE operation
|
|
|
|
side2 = w._side + u"other"
|
|
|
|
msg1 = unhexlify(out[1][u"body"].encode("ascii"))
|
2016-05-23 07:14:39 +00:00
|
|
|
key, msg2_hex = self.make_pake(CODE, side2, msg1)
|
2016-05-23 01:40:44 +00:00
|
|
|
response(w, type=u"message", phase=u"pake", body=msg2_hex, side=side2)
|
|
|
|
|
|
|
|
# hearing the peer's PAKE (msg2) makes us release the nameplate, send
|
|
|
|
# the confirmation message, delivered the verifier, and sends any
|
|
|
|
# queued phase messages
|
|
|
|
self.assertFalse(w._flag_need_to_see_mailbox_used)
|
|
|
|
self.assertEqual(w._key, key)
|
|
|
|
out = ws.outbound()
|
|
|
|
self.assertEqual(len(out), 2, out)
|
|
|
|
self.check_out(out[0], type=u"release")
|
|
|
|
self.check_out(out[1], type=u"add", phase=u"confirm")
|
|
|
|
verifier = self.successResultOf(v)
|
|
|
|
self.assertEqual(verifier, w.derive_key(u"wormhole:verifier"))
|
|
|
|
|
|
|
|
# hearing a valid confirmation message doesn't throw an error
|
|
|
|
confkey = w.derive_key(u"wormhole:confirmation")
|
|
|
|
nonce = os.urandom(wormhole.CONFMSG_NONCE_LENGTH)
|
|
|
|
confirm2 = wormhole.make_confmsg(confkey, nonce)
|
|
|
|
confirm2_hex = hexlify(confirm2).decode("ascii")
|
|
|
|
response(w, type=u"message", phase=u"confirm", body=confirm2_hex,
|
|
|
|
side=side2)
|
|
|
|
|
|
|
|
# an outbound message can now be sent immediately
|
|
|
|
w.send(b"phase0-outbound")
|
|
|
|
out = ws.outbound()
|
|
|
|
self.assertEqual(len(out), 1)
|
|
|
|
self.check_out(out[0], type=u"add", phase=u"0")
|
|
|
|
# decrypt+check the outbound message
|
|
|
|
p0_outbound = unhexlify(out[0][u"body"].encode("ascii"))
|
|
|
|
msgkey0 = w.derive_key(u"wormhole:phase:0")
|
|
|
|
p0_plaintext = w._decrypt_data(msgkey0, p0_outbound)
|
|
|
|
self.assertEqual(p0_plaintext, b"phase0-outbound")
|
|
|
|
|
|
|
|
# get() waits for the inbound message to arrive
|
|
|
|
md = w.get()
|
|
|
|
self.assertNoResult(md)
|
|
|
|
self.assertIn(u"0", w._receive_waiters)
|
|
|
|
self.assertNotIn(u"0", w._received_messages)
|
|
|
|
p0_inbound = w._encrypt_data(msgkey0, b"phase0-inbound")
|
|
|
|
p0_inbound_hex = hexlify(p0_inbound).decode("ascii")
|
|
|
|
response(w, type=u"message", phase=u"0", body=p0_inbound_hex,
|
|
|
|
side=side2)
|
|
|
|
p0_in = self.successResultOf(md)
|
|
|
|
self.assertEqual(p0_in, b"phase0-inbound")
|
|
|
|
self.assertNotIn(u"0", w._receive_waiters)
|
|
|
|
self.assertIn(u"0", w._received_messages)
|
|
|
|
|
|
|
|
# receiving an inbound message will queue it until get() is called
|
|
|
|
msgkey1 = w.derive_key(u"wormhole:phase:1")
|
|
|
|
p1_inbound = w._encrypt_data(msgkey1, b"phase1-inbound")
|
|
|
|
p1_inbound_hex = hexlify(p1_inbound).decode("ascii")
|
|
|
|
response(w, type=u"message", phase=u"1", body=p1_inbound_hex,
|
|
|
|
side=side2)
|
|
|
|
self.assertIn(u"1", w._received_messages)
|
|
|
|
self.assertNotIn(u"1", w._receive_waiters)
|
|
|
|
p1_in = self.successResultOf(w.get())
|
|
|
|
self.assertEqual(p1_in, b"phase1-inbound")
|
|
|
|
self.assertIn(u"1", w._received_messages)
|
|
|
|
self.assertNotIn(u"1", w._receive_waiters)
|
|
|
|
|
|
|
|
w.close()
|
|
|
|
out = ws.outbound()
|
|
|
|
self.assertEqual(len(out), 1)
|
|
|
|
self.check_out(out[0], type=u"close", mood=u"happy")
|
|
|
|
self.assertEqual(w._drop_connection.mock_calls, [mock.call()])
|
|
|
|
|
2016-05-23 07:14:39 +00:00
|
|
|
def test_close_wait_0(self):
|
|
|
|
# close before even claiming the nameplate
|
|
|
|
timing = DebugTiming()
|
|
|
|
w = wormhole._Wormhole(APPID, u"relay_url", reactor, None, timing)
|
|
|
|
w._drop_connection = mock.Mock()
|
|
|
|
ws = MockWebSocket()
|
|
|
|
w._event_connected(ws)
|
|
|
|
w._event_ws_opened(None)
|
|
|
|
|
|
|
|
d = w.close(wait=True)
|
|
|
|
self.check_outbound(ws, [u"bind"])
|
|
|
|
self.assertNoResult(d)
|
|
|
|
self.assertEqual(w._drop_connection.mock_calls, [mock.call()])
|
|
|
|
self.assertNoResult(d)
|
|
|
|
|
|
|
|
w._ws_closed(True, None, None)
|
|
|
|
self.successResultOf(d)
|
|
|
|
|
2016-05-23 01:40:44 +00:00
|
|
|
def test_close_wait_1(self):
|
|
|
|
# close after claiming the nameplate, but before opening the mailbox
|
|
|
|
timing = DebugTiming()
|
|
|
|
w = wormhole._Wormhole(APPID, u"relay_url", reactor, None, timing)
|
|
|
|
w._drop_connection = mock.Mock()
|
|
|
|
ws = MockWebSocket()
|
|
|
|
w._event_connected(ws)
|
|
|
|
w._event_ws_opened(None)
|
|
|
|
CODE = u"123-foo-bar"
|
|
|
|
w.set_code(CODE)
|
|
|
|
self.check_outbound(ws, [u"bind", u"claim"])
|
|
|
|
|
|
|
|
d = w.close(wait=True)
|
|
|
|
self.check_outbound(ws, [u"release"])
|
|
|
|
self.assertNoResult(d)
|
|
|
|
self.assertEqual(w._drop_connection.mock_calls, [])
|
|
|
|
|
|
|
|
response(w, type=u"released")
|
|
|
|
self.assertEqual(w._drop_connection.mock_calls, [mock.call()])
|
2016-05-23 01:45:50 +00:00
|
|
|
self.assertNoResult(d)
|
|
|
|
|
|
|
|
w._ws_closed(True, None, None)
|
|
|
|
self.successResultOf(d)
|
2016-05-23 01:40:44 +00:00
|
|
|
|
|
|
|
def test_close_wait_2(self):
|
|
|
|
# close after both claiming the nameplate and opening the mailbox
|
|
|
|
timing = DebugTiming()
|
|
|
|
w = wormhole._Wormhole(APPID, u"relay_url", reactor, None, timing)
|
|
|
|
w._drop_connection = mock.Mock()
|
|
|
|
ws = MockWebSocket()
|
|
|
|
w._event_connected(ws)
|
|
|
|
w._event_ws_opened(None)
|
|
|
|
CODE = u"123-foo-bar"
|
|
|
|
w.set_code(CODE)
|
|
|
|
response(w, type=u"claimed", mailbox=u"mb456")
|
|
|
|
self.check_outbound(ws, [u"bind", u"claim", u"open", u"add"])
|
|
|
|
|
|
|
|
d = w.close(wait=True)
|
|
|
|
self.check_outbound(ws, [u"release", u"close"])
|
|
|
|
self.assertNoResult(d)
|
|
|
|
self.assertEqual(w._drop_connection.mock_calls, [])
|
|
|
|
|
|
|
|
response(w, type=u"released")
|
|
|
|
self.assertNoResult(d)
|
|
|
|
self.assertEqual(w._drop_connection.mock_calls, [])
|
2016-05-23 01:45:50 +00:00
|
|
|
|
2016-05-23 01:40:44 +00:00
|
|
|
response(w, type=u"closed")
|
2016-05-23 01:45:50 +00:00
|
|
|
self.assertNoResult(d)
|
2016-05-23 01:40:44 +00:00
|
|
|
self.assertEqual(w._drop_connection.mock_calls, [mock.call()])
|
|
|
|
|
2016-05-23 01:45:50 +00:00
|
|
|
w._ws_closed(True, None, None)
|
|
|
|
self.successResultOf(d)
|
|
|
|
|
2016-05-23 01:40:44 +00:00
|
|
|
def test_close_wait_3(self):
|
|
|
|
# close after claiming the nameplate, opening the mailbox, then
|
|
|
|
# releasing the nameplate
|
|
|
|
timing = DebugTiming()
|
|
|
|
w = wormhole._Wormhole(APPID, u"relay_url", reactor, None, timing)
|
|
|
|
w._drop_connection = mock.Mock()
|
|
|
|
ws = MockWebSocket()
|
|
|
|
w._event_connected(ws)
|
|
|
|
w._event_ws_opened(None)
|
|
|
|
CODE = u"123-foo-bar"
|
|
|
|
w.set_code(CODE)
|
|
|
|
response(w, type=u"claimed", mailbox=u"mb456")
|
|
|
|
|
|
|
|
w._key = b""
|
|
|
|
msgkey = w.derive_key(u"wormhole:phase:misc")
|
|
|
|
p1_inbound = w._encrypt_data(msgkey, b"")
|
|
|
|
p1_inbound_hex = hexlify(p1_inbound).decode("ascii")
|
|
|
|
response(w, type=u"message", phase=u"misc", side=u"side2",
|
|
|
|
body=p1_inbound_hex)
|
|
|
|
self.check_outbound(ws, [u"bind", u"claim", u"open", u"add",
|
|
|
|
u"release"])
|
|
|
|
|
|
|
|
d = w.close(wait=True)
|
|
|
|
self.check_outbound(ws, [u"close"])
|
|
|
|
self.assertNoResult(d)
|
|
|
|
self.assertEqual(w._drop_connection.mock_calls, [])
|
|
|
|
|
|
|
|
response(w, type=u"released")
|
|
|
|
self.assertNoResult(d)
|
|
|
|
self.assertEqual(w._drop_connection.mock_calls, [])
|
2016-05-23 01:45:50 +00:00
|
|
|
|
2016-05-23 01:40:44 +00:00
|
|
|
response(w, type=u"closed")
|
2016-05-23 01:45:50 +00:00
|
|
|
self.assertNoResult(d)
|
2016-05-23 01:40:44 +00:00
|
|
|
self.assertEqual(w._drop_connection.mock_calls, [mock.call()])
|
|
|
|
|
2016-05-23 01:45:50 +00:00
|
|
|
w._ws_closed(True, None, None)
|
|
|
|
self.successResultOf(d)
|
|
|
|
|
2016-05-23 01:40:44 +00:00
|
|
|
def test_get_code_mock(self):
|
|
|
|
timing = DebugTiming()
|
|
|
|
w = wormhole._Wormhole(APPID, u"relay_url", reactor, None, timing)
|
|
|
|
ws = MockWebSocket() # TODO: mock w._ws_send_command instead
|
|
|
|
w._event_connected(ws)
|
|
|
|
w._event_ws_opened(None)
|
|
|
|
self.check_outbound(ws, [u"bind"])
|
|
|
|
|
|
|
|
gc_c = mock.Mock()
|
|
|
|
gc = gc_c.return_value = mock.Mock()
|
|
|
|
gc_d = gc.go.return_value = Deferred()
|
|
|
|
with mock.patch("wormhole.wormhole._GetCode", gc_c):
|
|
|
|
d = w.get_code()
|
|
|
|
self.assertNoResult(d)
|
|
|
|
|
|
|
|
gc_d.callback(u"123-foo-bar")
|
|
|
|
code = self.successResultOf(d)
|
|
|
|
self.assertEqual(code, u"123-foo-bar")
|
|
|
|
|
|
|
|
def test_get_code_real(self):
|
|
|
|
timing = DebugTiming()
|
|
|
|
w = wormhole._Wormhole(APPID, u"relay_url", reactor, None, timing)
|
|
|
|
ws = MockWebSocket()
|
|
|
|
w._event_connected(ws)
|
|
|
|
w._event_ws_opened(None)
|
|
|
|
self.check_outbound(ws, [u"bind"])
|
|
|
|
|
|
|
|
d = w.get_code()
|
|
|
|
|
|
|
|
out = ws.outbound()
|
|
|
|
self.assertEqual(len(out), 1)
|
|
|
|
self.check_out(out[0], type=u"allocate")
|
|
|
|
# TODO: nameplate attributes go here
|
|
|
|
self.assertNoResult(d)
|
|
|
|
|
|
|
|
response(w, type=u"allocated", nameplate=u"123")
|
|
|
|
code = self.successResultOf(d)
|
|
|
|
self.assertIsInstance(code, type(u""))
|
|
|
|
self.assert_(code.startswith(u"123-"))
|
|
|
|
pieces = code.split(u"-")
|
|
|
|
self.assertEqual(len(pieces), 3) # nameplate plus two words
|
|
|
|
self.assert_(re.search(r'^\d+-\w+-\w+$', code), code)
|
|
|
|
|
2016-05-23 07:14:39 +00:00
|
|
|
def test_api_errors(self):
|
|
|
|
# doing things you're not supposed to do
|
|
|
|
pass
|
|
|
|
|
|
|
|
def test_welcome_error(self):
|
|
|
|
# A welcome message could arrive at any time, with an [error] key
|
|
|
|
# that should make us halt. In practice, though, this gets sent as
|
|
|
|
# soon as the connection is established, which limits the possible
|
|
|
|
# states in which we might see it.
|
|
|
|
|
|
|
|
timing = DebugTiming()
|
|
|
|
w = wormhole._Wormhole(APPID, u"relay_url", reactor, None, timing)
|
|
|
|
w._drop_connection = mock.Mock()
|
|
|
|
ws = MockWebSocket()
|
|
|
|
w._event_connected(ws)
|
|
|
|
w._event_ws_opened(None)
|
|
|
|
self.check_outbound(ws, [u"bind"])
|
|
|
|
|
|
|
|
WE = wormhole.WelcomeError
|
|
|
|
d1 = w.get()
|
|
|
|
d2 = w.get_verifier()
|
|
|
|
d3 = w.get_code()
|
|
|
|
# TODO (tricky): test w.input_code
|
|
|
|
|
|
|
|
self.assertNoResult(d1)
|
|
|
|
self.assertNoResult(d2)
|
|
|
|
self.assertNoResult(d3)
|
|
|
|
|
|
|
|
w._signal_error(WE(u"you are not actually welcome"))
|
|
|
|
self.failureResultOf(d1, WE)
|
|
|
|
self.failureResultOf(d2, WE)
|
|
|
|
self.failureResultOf(d3, WE)
|
|
|
|
|
|
|
|
# once the error is signalled, all API calls should fail
|
|
|
|
self.assertRaises(WE, w.send, u"foo")
|
|
|
|
self.assertRaises(WE, w.derive_key, u"foo")
|
|
|
|
self.failureResultOf(w.get(), WE)
|
|
|
|
self.failureResultOf(w.get_verifier(), WE)
|
|
|
|
|
|
|
|
def test_confirm_error(self):
|
|
|
|
# we should only receive the "confirm" message after we receive the
|
|
|
|
# PAKE message, by which point we should know the key. If the
|
|
|
|
# confirmation message doesn't decrypt, we signal an error.
|
|
|
|
timing = DebugTiming()
|
|
|
|
w = wormhole._Wormhole(APPID, u"relay_url", reactor, None, timing)
|
|
|
|
w._drop_connection = mock.Mock()
|
|
|
|
ws = MockWebSocket()
|
|
|
|
w._event_connected(ws)
|
|
|
|
w._event_ws_opened(None)
|
|
|
|
w.set_code(u"123-foo-bar")
|
|
|
|
response(w, type=u"claimed", mailbox=u"mb456")
|
|
|
|
|
|
|
|
WP = wormhole.WrongPasswordError
|
|
|
|
d1 = w.get()
|
|
|
|
d2 = w.get_verifier()
|
|
|
|
self.assertNoResult(d1)
|
|
|
|
self.assertNoResult(d2)
|
|
|
|
|
|
|
|
out = ws.outbound()
|
|
|
|
# [u"bind", u"claim", u"open", u"add"]
|
|
|
|
self.assertEqual(len(out), 4)
|
|
|
|
self.assertEqual(out[3][u"type"], u"add")
|
|
|
|
|
|
|
|
sp2 = SPAKE2_Symmetric(b"", idSymmetric=wormhole.to_bytes(APPID))
|
|
|
|
msg2 = sp2.start()
|
|
|
|
msg2_hex = hexlify(msg2).decode("ascii")
|
|
|
|
response(w, type=u"message", phase=u"pake", body=msg2_hex, side=u"s2")
|
|
|
|
self.assertNoResult(d1)
|
|
|
|
self.successResultOf(d2) # early get_verifier is unaffected
|
|
|
|
# TODO: get_verifier would be a lovely place to signal a confirmation
|
|
|
|
# error, but that's at odds with delivering the verifier as early as
|
|
|
|
# possible. The confirmation messages should be hot on the heels of
|
|
|
|
# the PAKE message that produced the verifier. Maybe get_verifier()
|
|
|
|
# should explicitly wait for confirm()?
|
|
|
|
|
|
|
|
# sending a random confirm message will cause a confirmation error
|
|
|
|
confkey = w.derive_key(u"WRONG")
|
|
|
|
nonce = os.urandom(wormhole.CONFMSG_NONCE_LENGTH)
|
|
|
|
badconfirm = wormhole.make_confmsg(confkey, nonce)
|
|
|
|
badconfirm_hex = hexlify(badconfirm).decode("ascii")
|
|
|
|
response(w, type=u"message", phase=u"confirm", body=badconfirm_hex,
|
|
|
|
side=u"s2")
|
|
|
|
|
|
|
|
self.failureResultOf(d1, WP)
|
|
|
|
|
|
|
|
# once the error is signalled, all API calls should fail
|
|
|
|
self.assertRaises(WP, w.send, u"foo")
|
|
|
|
self.assertRaises(WP, w.derive_key, u"foo")
|
|
|
|
self.failureResultOf(w.get(), WP)
|
|
|
|
self.failureResultOf(w.get_verifier(), WP)
|
|
|
|
|
|
|
|
|
2016-05-23 01:40:44 +00:00
|
|
|
# event orderings to exercise:
|
|
|
|
#
|
|
|
|
# * normal sender: set_code, send_phase1, connected, claimed, learn_msg2,
|
|
|
|
# learn_phase1
|
|
|
|
# * normal receiver (argv[2]=code): set_code, connected, learn_msg1,
|
|
|
|
# learn_phase1, send_phase1,
|
|
|
|
# * normal receiver (readline): connected, input_code
|
|
|
|
# *
|
|
|
|
# * set_code, then connected
|
|
|
|
# * connected, receive_pake, send_phase, set_code
|
|
|
|
|
|
|
|
class Wormholes(ServerBase, unittest.TestCase):
|
|
|
|
# integration test, with a real server
|
|
|
|
|
|
|
|
def doBoth(self, d1, d2):
|
|
|
|
return gatherResults([d1, d2], True)
|
|
|
|
|
|
|
|
@inlineCallbacks
|
|
|
|
def test_basic(self):
|
|
|
|
w1 = wormhole.wormhole(APPID, self.relayurl, reactor)
|
|
|
|
w2 = wormhole.wormhole(APPID, self.relayurl, reactor)
|
|
|
|
code = yield w1.get_code()
|
|
|
|
w2.set_code(code)
|
|
|
|
w1.send(b"data1")
|
|
|
|
w2.send(b"data2")
|
|
|
|
dataX = yield w1.get()
|
|
|
|
dataY = yield w2.get()
|
|
|
|
self.assertEqual(dataX, b"data2")
|
|
|
|
self.assertEqual(dataY, b"data1")
|
|
|
|
yield w1.close(wait=True)
|
|
|
|
yield w2.close(wait=True)
|
|
|
|
|
|
|
|
class Off:
|
|
|
|
|
|
|
|
@inlineCallbacks
|
|
|
|
def test_same_message(self):
|
|
|
|
# the two sides use random nonces for their messages, so it's ok for
|
|
|
|
# both to try and send the same body: they'll result in distinct
|
|
|
|
# encrypted messages
|
|
|
|
w1 = Wormhole(APPID, self.relayurl)
|
|
|
|
w2 = Wormhole(APPID, self.relayurl)
|
|
|
|
code = yield w1.get_code()
|
|
|
|
w2.set_code(code)
|
|
|
|
yield self.doBoth(w1.send(b"data"), w2.send(b"data"))
|
|
|
|
dl = yield self.doBoth(w1.get(), w2.get())
|
|
|
|
(dataX, dataY) = dl
|
|
|
|
self.assertEqual(dataX, b"data")
|
|
|
|
self.assertEqual(dataY, b"data")
|
|
|
|
yield self.doBoth(w1.close(), w2.close())
|
|
|
|
|
|
|
|
@inlineCallbacks
|
|
|
|
def test_interleaved(self):
|
|
|
|
w1 = Wormhole(APPID, self.relayurl)
|
|
|
|
w2 = Wormhole(APPID, self.relayurl)
|
|
|
|
code = yield w1.get_code()
|
|
|
|
w2.set_code(code)
|
|
|
|
res = yield self.doBoth(w1.send(b"data1"), w2.get())
|
|
|
|
(_, dataY) = res
|
|
|
|
self.assertEqual(dataY, b"data1")
|
|
|
|
dl = yield self.doBoth(w1.get(), w2.send(b"data2"))
|
|
|
|
(dataX, _) = dl
|
|
|
|
self.assertEqual(dataX, b"data2")
|
|
|
|
yield self.doBoth(w1.close(), w2.close())
|
|
|
|
|
|
|
|
@inlineCallbacks
|
|
|
|
def test_fixed_code(self):
|
|
|
|
w1 = Wormhole(APPID, self.relayurl)
|
|
|
|
w2 = Wormhole(APPID, self.relayurl)
|
|
|
|
w1.set_code(u"123-purple-elephant")
|
|
|
|
w2.set_code(u"123-purple-elephant")
|
|
|
|
yield self.doBoth(w1.send(b"data1"), w2.send(b"data2"))
|
|
|
|
dl = yield self.doBoth(w1.get(), w2.get())
|
|
|
|
(dataX, dataY) = dl
|
|
|
|
self.assertEqual(dataX, b"data2")
|
|
|
|
self.assertEqual(dataY, b"data1")
|
|
|
|
yield self.doBoth(w1.close(), w2.close())
|
|
|
|
|
|
|
|
|
|
|
|
@inlineCallbacks
|
|
|
|
def test_multiple_messages(self):
|
|
|
|
w1 = Wormhole(APPID, self.relayurl)
|
|
|
|
w2 = Wormhole(APPID, self.relayurl)
|
|
|
|
w1.set_code(u"123-purple-elephant")
|
|
|
|
w2.set_code(u"123-purple-elephant")
|
|
|
|
yield self.doBoth(w1.send(b"data1"), w2.send(b"data2"))
|
|
|
|
yield self.doBoth(w1.send(b"data3"), w2.send(b"data4"))
|
|
|
|
dl = yield self.doBoth(w1.get(), w2.get())
|
|
|
|
(dataX, dataY) = dl
|
|
|
|
self.assertEqual(dataX, b"data2")
|
|
|
|
self.assertEqual(dataY, b"data1")
|
|
|
|
dl = yield self.doBoth(w1.get(), w2.get())
|
|
|
|
(dataX, dataY) = dl
|
|
|
|
self.assertEqual(dataX, b"data4")
|
|
|
|
self.assertEqual(dataY, b"data3")
|
|
|
|
yield self.doBoth(w1.close(), w2.close())
|
|
|
|
|
|
|
|
@inlineCallbacks
|
|
|
|
def test_multiple_messages_2(self):
|
|
|
|
w1 = Wormhole(APPID, self.relayurl)
|
|
|
|
w2 = Wormhole(APPID, self.relayurl)
|
|
|
|
w1.set_code(u"123-purple-elephant")
|
|
|
|
w2.set_code(u"123-purple-elephant")
|
|
|
|
# TODO: set_code should be sufficient to kick things off, but for now
|
|
|
|
# we must also let both sides do at least one send() or get()
|
|
|
|
yield self.doBoth(w1.send(b"data1"), w2.send(b"ignored"))
|
|
|
|
yield w1.get()
|
|
|
|
yield w1.send(b"data2")
|
|
|
|
yield w1.send(b"data3")
|
|
|
|
data = yield w2.get()
|
|
|
|
self.assertEqual(data, b"data1")
|
|
|
|
data = yield w2.get()
|
|
|
|
self.assertEqual(data, b"data2")
|
|
|
|
data = yield w2.get()
|
|
|
|
self.assertEqual(data, b"data3")
|
|
|
|
yield self.doBoth(w1.close(), w2.close())
|
|
|
|
|
|
|
|
@inlineCallbacks
|
|
|
|
def test_wrong_password(self):
|
|
|
|
w1 = Wormhole(APPID, self.relayurl)
|
|
|
|
w2 = Wormhole(APPID, self.relayurl)
|
|
|
|
code = yield w1.get_code()
|
|
|
|
w2.set_code(code+"not")
|
|
|
|
|
|
|
|
# w2 can't throw WrongPasswordError until it sees a CONFIRM message,
|
|
|
|
# and w1 won't send CONFIRM until it sees a PAKE message, which w2
|
|
|
|
# won't send until we call get. So we need both sides to be
|
|
|
|
# running at the same time for this test.
|
|
|
|
d1 = w1.send(b"data1")
|
|
|
|
# at this point, w1 should be waiting for w2.PAKE
|
|
|
|
|
|
|
|
yield self.assertFailure(w2.get(), WrongPasswordError)
|
|
|
|
# * w2 will send w2.PAKE, wait for (and get) w1.PAKE, compute a key,
|
|
|
|
# send w2.CONFIRM, then wait for w1.DATA.
|
|
|
|
# * w1 will get w2.PAKE, compute a key, send w1.CONFIRM.
|
|
|
|
# * w1 might also get w2.CONFIRM, and may notice the error before it
|
|
|
|
# sends w1.CONFIRM, in which case the wait=True will signal an
|
|
|
|
# error inside _get_master_key() (inside send), and d1 will
|
|
|
|
# errback.
|
|
|
|
# * but w1 might not see w2.CONFIRM yet, in which case it won't
|
|
|
|
# errback until we do w1.get()
|
|
|
|
# * w2 gets w1.CONFIRM, notices the error, records it.
|
|
|
|
# * w2 (waiting for w1.DATA) wakes up, sees the error, throws
|
|
|
|
# * meanwhile w1 finishes sending its data. w2.CONFIRM may or may not
|
|
|
|
# have arrived by then
|
|
|
|
try:
|
|
|
|
yield d1
|
|
|
|
except WrongPasswordError:
|
|
|
|
pass
|
|
|
|
|
|
|
|
# When we ask w1 to get(), one of two things might happen:
|
|
|
|
# * if w2.CONFIRM arrived already, it will have recorded the error.
|
|
|
|
# When w1.get() sleeps (waiting for w2.DATA), we'll notice the
|
|
|
|
# error before sleeping, and throw WrongPasswordError
|
|
|
|
# * if w2.CONFIRM hasn't arrived yet, we'll sleep. When w2.CONFIRM
|
|
|
|
# arrives, we notice and record the error, and wake up, and throw
|
|
|
|
|
|
|
|
# Note that we didn't do w2.send(), so we're hoping that w1 will
|
|
|
|
# have enough information to detect the error before it sleeps
|
|
|
|
# (waiting for w2.DATA). Checking for the error both before sleeping
|
|
|
|
# and after waking up makes this happen.
|
|
|
|
|
|
|
|
# so now w1 should have enough information to throw too
|
|
|
|
yield self.assertFailure(w1.get(), WrongPasswordError)
|
|
|
|
|
|
|
|
# both sides are closed automatically upon error, but it's still
|
|
|
|
# legal to call .close(), and should be idempotent
|
|
|
|
yield self.doBoth(w1.close(), w2.close())
|
|
|
|
|
|
|
|
@inlineCallbacks
|
|
|
|
def test_no_confirm(self):
|
|
|
|
# newer versions (which check confirmations) should will work with
|
|
|
|
# older versions (that don't send confirmations)
|
|
|
|
w1 = Wormhole(APPID, self.relayurl)
|
|
|
|
w1._send_confirm = False
|
|
|
|
w2 = Wormhole(APPID, self.relayurl)
|
|
|
|
|
|
|
|
code = yield w1.get_code()
|
|
|
|
w2.set_code(code)
|
|
|
|
dl = yield self.doBoth(w1.send(b"data1"), w2.get())
|
|
|
|
self.assertEqual(dl[1], b"data1")
|
|
|
|
dl = yield self.doBoth(w1.get(), w2.send(b"data2"))
|
|
|
|
self.assertEqual(dl[0], b"data2")
|
|
|
|
yield self.doBoth(w1.close(), w2.close())
|
|
|
|
|
|
|
|
@inlineCallbacks
|
|
|
|
def test_verifier(self):
|
|
|
|
w1 = Wormhole(APPID, self.relayurl)
|
|
|
|
w2 = Wormhole(APPID, self.relayurl)
|
|
|
|
code = yield w1.get_code()
|
|
|
|
w2.set_code(code)
|
|
|
|
res = yield self.doBoth(w1.get_verifier(), w2.get_verifier())
|
|
|
|
v1, v2 = res
|
|
|
|
self.failUnlessEqual(type(v1), type(b""))
|
|
|
|
self.failUnlessEqual(v1, v2)
|
|
|
|
yield self.doBoth(w1.send(b"data1"), w2.send(b"data2"))
|
|
|
|
dl = yield self.doBoth(w1.get(), w2.get())
|
|
|
|
(dataX, dataY) = dl
|
|
|
|
self.assertEqual(dataX, b"data2")
|
|
|
|
self.assertEqual(dataY, b"data1")
|
|
|
|
yield self.doBoth(w1.close(), w2.close())
|
|
|
|
|
|
|
|
@inlineCallbacks
|
|
|
|
def test_errors(self):
|
|
|
|
w1 = Wormhole(APPID, self.relayurl)
|
|
|
|
yield self.assertFailure(w1.get_verifier(), UsageError)
|
|
|
|
yield self.assertFailure(w1.send(b"data"), UsageError)
|
|
|
|
yield self.assertFailure(w1.get(), UsageError)
|
|
|
|
w1.set_code(u"123-purple-elephant")
|
|
|
|
yield self.assertRaises(UsageError, w1.set_code, u"123-nope")
|
|
|
|
yield self.assertFailure(w1.get_code(), UsageError)
|
|
|
|
w2 = Wormhole(APPID, self.relayurl)
|
|
|
|
yield w2.get_code()
|
|
|
|
yield self.assertFailure(w2.get_code(), UsageError)
|
|
|
|
yield self.doBoth(w1.close(), w2.close())
|
|
|
|
|