magic-wormhole/tests/test_twisted.py
Brian Warner 34f4c284b0 txwormhole: use websockets, not HTTP
This should speed up the protocol, since we don't have to wait for
acks (HTTP responses) unless we really want to. It also makes it easier
to have multiple messages in flight at once. The protocol is still
compatible with the old HTTP version (which is still used by the
blocking flavor), but requires an updated Rendezvous server that speaks
websockets.

set_code() no longer touches the network: it just stores the code and
channelid for later. We hold off doing 'claim' and 'watch' until we need
messages, triggered by get_verifier() or get_data() or send_data().

We check for error before sleeping, not just after waking. This makes it
possible to detect a WrongPasswordError in get_data() even if the other
side hasn't done a corresponding send_data(), as long as the other side
finished PAKE (and thus sent a CONFIRM message). The unit test was doing
just this, and was hanging.
2016-04-20 18:14:47 -07:00

250 lines
10 KiB
Python

from __future__ import print_function
import json
from twisted.trial import unittest
from twisted.internet.defer import gatherResults, inlineCallbacks
from txwormhole.transcribe import Wormhole, UsageError, WrongPasswordError
from .common import ServerBase
APPID = u"appid"
class Basic(ServerBase, unittest.TestCase):
def doBoth(self, d1, d2):
return gatherResults([d1, d2], True)
@inlineCallbacks
def test_basic(self):
w1 = Wormhole(APPID, self.relayurl)
w2 = Wormhole(APPID, self.relayurl)
code = yield w1.get_code()
w2.set_code(code)
yield self.doBoth(w1.send_data(b"data1"), w2.send_data(b"data2"))
dl = yield self.doBoth(w1.get_data(), w2.get_data())
(dataX, dataY) = dl
self.assertEqual(dataX, b"data2")
self.assertEqual(dataY, b"data1")
yield self.doBoth(w1.close(), w2.close())
@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_data(b"data"), w2.send_data(b"data"))
dl = yield self.doBoth(w1.get_data(), w2.get_data())
(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_data(b"data1"), w2.get_data())
(_, dataY) = res
self.assertEqual(dataY, b"data1")
dl = yield self.doBoth(w1.get_data(), w2.send_data(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_data(b"data1"), w2.send_data(b"data2"))
dl = yield self.doBoth(w1.get_data(), w2.get_data())
(dataX, dataY) = dl
self.assertEqual(dataX, b"data2")
self.assertEqual(dataY, b"data1")
yield self.doBoth(w1.close(), w2.close())
@inlineCallbacks
def test_phases(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_data(b"data1", u"p1"),
w2.send_data(b"data2", u"p1"))
yield self.doBoth(w1.send_data(b"data3", u"p2"),
w2.send_data(b"data4", u"p2"))
dl = yield self.doBoth(w1.get_data(u"p2"),
w2.get_data(u"p1"))
(dataX, dataY) = dl
self.assertEqual(dataX, b"data4")
self.assertEqual(dataY, b"data1")
dl = yield self.doBoth(w1.get_data(u"p1"),
w2.get_data(u"p2"))
(dataX, dataY) = dl
self.assertEqual(dataX, b"data2")
self.assertEqual(dataY, 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_data. So we need both sides to be
# running at the same time for this test.
d1 = w1.send_data(b"data1")
# at this point, w1 should be waiting for w2.PAKE
yield self.assertFailure(w2.get_data(), 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.
# * 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
yield d1
# When we ask w1 to get_data(), one of two things might happen:
# * if w2.CONFIRM arrived already, it will have recorded the error.
# When w1.get_data() 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_data(), 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_data(), 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_data(b"data1"), w2.get_data())
self.assertEqual(dl[1], b"data1")
dl = yield self.doBoth(w1.get_data(), w2.send_data(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_data(b"data1"), w2.send_data(b"data2"))
dl = yield self.doBoth(w1.get_data(), w2.get_data())
(dataX, dataY) = dl
self.assertEqual(dataX, b"data2")
self.assertEqual(dataY, b"data1")
yield self.doBoth(w1.close(), w2.close())
@inlineCallbacks
def test_verifier_mismatch(self):
w1 = Wormhole(APPID, self.relayurl)
w2 = Wormhole(APPID, self.relayurl)
# we must disable confirmation messages, else the wormholes will
# figure out the mismatch by themselves and throw WrongPasswordError.
w1._send_confirm = w2._send_confirm = False
code = yield w1.get_code()
w2.set_code(code+"not")
res = yield self.doBoth(w1.get_verifier(), w2.get_verifier())
v1, v2 = res
self.failUnlessEqual(type(v1), type(b""))
self.failIfEqual(v1, v2)
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_data(b"data"), UsageError)
yield self.assertFailure(w1.get_data(), 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())
@inlineCallbacks
def test_repeat_phases(self):
w1 = Wormhole(APPID, self.relayurl)
w1.set_code(u"123-purple-elephant")
w2 = Wormhole(APPID, self.relayurl)
w2.set_code(u"123-purple-elephant")
# we must let them establish a key before we can send data
yield self.doBoth(w1.get_verifier(), w2.get_verifier())
yield w1.send_data(b"data1", phase=u"1")
# underscore-prefixed phases are reserved
yield self.assertFailure(w1.send_data(b"data1", phase=u"_1"),
UsageError)
yield self.assertFailure(w1.get_data(phase=u"_1"), UsageError)
# you can't send twice to the same phase
yield self.assertFailure(w1.send_data(b"data1", phase=u"1"),
UsageError)
# but you can send to a different one
yield w1.send_data(b"data2", phase=u"2")
res = yield w2.get_data(phase=u"1")
self.failUnlessEqual(res, b"data1")
# and you can't read twice from the same phase
yield self.assertFailure(w2.get_data(phase=u"1"), UsageError)
# but you can read from a different one
res = yield w2.get_data(phase=u"2")
self.failUnlessEqual(res, b"data2")
yield self.doBoth(w1.close(), w2.close())
@inlineCallbacks
def test_serialize(self):
w1 = Wormhole(APPID, self.relayurl)
self.assertRaises(UsageError, w1.serialize) # too early
w2 = Wormhole(APPID, self.relayurl)
code = yield w1.get_code()
self.assertRaises(UsageError, w2.serialize) # too early
w2.set_code(code)
w2.serialize() # ok
s = w1.serialize()
self.assertEqual(type(s), type(""))
unpacked = json.loads(s) # this is supposed to be JSON
self.assertEqual(type(unpacked), dict)
self.new_w1 = Wormhole.from_serialized(s)
yield self.doBoth(self.new_w1.send_data(b"data1"),
w2.send_data(b"data2"))
dl = yield self.doBoth(self.new_w1.get_data(), w2.get_data())
(dataX, dataY) = dl
self.assertEqual((dataX, dataY), (b"data2", b"data1"))
self.assertRaises(UsageError, w2.serialize) # too late
yield gatherResults([w1.close(), w2.close(), self.new_w1.close()],
True)