34f4c284b0
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.
250 lines
10 KiB
Python
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)
|
|
|