diff --git a/src/wormhole/cli/cmd_receive.py b/src/wormhole/cli/cmd_receive.py index bc1e1b6..eae17a0 100644 --- a/src/wormhole/cli/cmd_receive.py +++ b/src/wormhole/cli/cmd_receive.py @@ -13,7 +13,9 @@ from ..util import (dict_to_bytes, bytes_to_dict, bytes_to_hexstr, from .welcome import CLIWelcomeHandler APPID = u"lothar.com/wormhole/text-or-file-xfer" -VERIFY_TIMER = 1 + +KEY_TIMER = 1.0 +VERIFY_TIMER = 1.0 class RespondError(Exception): def __init__(self, response): @@ -106,17 +108,45 @@ class TwistedReceiver: @inlineCallbacks def _go(self, w): yield self._handle_code(w) - verifier = yield w.when_verified() - def on_slow_connection(): - print(u"Key established, waiting for confirmation...", - file=self.args.stderr) - notify = self._reactor.callLater(VERIFY_TIMER, on_slow_connection) + + def on_slow_key(): + print(u"Waiting for sender...", file=self.args.stderr) + notify = self._reactor.callLater(KEY_TIMER, on_slow_key) try: - yield w.when_version() + # We wait here until we connect to the server and see the senders + # PAKE message. If we used set_code() in the "human-selected + # offline codes" mode, then the sender might not have even + # started yet, so we might be sitting here for a while. Because + # of that possibility, it's probably not appropriate to give up + # automatically after some timeout. The user can express their + # impatience by quitting the program with control-C. + yield w.when_key() finally: if not notify.called: notify.cancel() - self._show_verifier(verifier) + + def on_slow_verification(): + print(u"Key established, waiting for confirmation...", + file=self.args.stderr) + notify = self._reactor.callLater(VERIFY_TIMER, on_slow_verification) + try: + # We wait here until we've seen their VERSION message (which they + # send after seeing our PAKE message, and has the side-effect of + # verifying that we both share the same key). There is a + # round-trip between these two events, and we could experience a + # significant delay here if: + # * the relay server is being restarted + # * the network is very slow + # * the sender is very slow + # * the sender has quit (in which case we may wait forever) + + # It would be reasonable to give up after waiting here for too + # long. + verifier_bytes = yield w.when_verified() + finally: + if not notify.called: + notify.cancel() + self._show_verifier(verifier_bytes) want_offer = True done = False @@ -177,8 +207,8 @@ class TwistedReceiver: file=self.args.stderr) yield w.when_code() - def _show_verifier(self, verifier): - verifier_hex = bytes_to_hexstr(verifier) + def _show_verifier(self, verifier_bytes): + verifier_hex = bytes_to_hexstr(verifier_bytes) if self.args.verify: self._msg(u"Verifier %s." % verifier_hex) diff --git a/src/wormhole/cli/cmd_send.py b/src/wormhole/cli/cmd_send.py index cb8bf22..c51a115 100644 --- a/src/wormhole/cli/cmd_send.py +++ b/src/wormhole/cli/cmd_send.py @@ -118,30 +118,34 @@ class Sender: args.stderr.flush() print(u"", file=args.stderr) + # We don't print a "waiting" message for when_key() here, even though + # we do that in cmd_receive.py, because it's not at all surprising to + # we waiting here for a long time. We'll sit in when_key() until the + # receiver has typed in the code and their PAKE message makes it to + # us. + yield w.when_key() + + # TODO: don't stall on w.verify() unless they want it def on_slow_connection(): print(u"Key established, waiting for confirmation...", file=args.stderr) - #notify = self._reactor.callLater(VERIFY_TIMER, on_slow_connection) - - # TODO: don't stall on w.verify() unless they want it - #try: - # verifier_bytes = yield w.when_verified() # might WrongPasswordError - #finally: - # if not notify.called: - # notify.cancel() - verifier_bytes = yield w.when_verified() + notify = self._reactor.callLater(VERIFY_TIMER, on_slow_connection) + try: + # The usual sender-chooses-code sequence means the receiver's + # PAKE should be followed immediately by their VERSION, so + # w.when_verified() should fire right away. However if we're + # using the offline-codes sequence, and the receiver typed in + # their code first, and then they went offline, we might be + # sitting here for a while, so printing the "waiting" message + # seems like a good idea. It might even be appropriate to give up + # after a while. + verifier_bytes = yield w.when_verified() # might WrongPasswordError + finally: + if not notify.called: + notify.cancel() if args.verify: - verifier = bytes_to_hexstr(verifier_bytes) - while True: - ok = six.moves.input("Verifier %s. ok? (yes/no): " % verifier) - if ok.lower() == "yes": - break - if ok.lower() == "no": - err = "sender rejected verification check, abandoned transfer" - reject_data = dict_to_bytes({"error": err}) - w.send(reject_data) - raise TransferError(err) + self._check_verifier(w, verifier_bytes) # blocks, can TransferError if self._fd_to_send: ts = TransitSender(args.transit_helper, @@ -197,6 +201,18 @@ class Sender: if not recognized: log.msg("unrecognized message %r" % (them_d,)) + def _check_verifier(self, w, verifier_bytes): + verifier = bytes_to_hexstr(verifier_bytes) + while True: + ok = six.moves.input("Verifier %s. ok? (yes/no): " % verifier) + if ok.lower() == "yes": + break + if ok.lower() == "no": + err = "sender rejected verification check, abandoned transfer" + reject_data = dict_to_bytes({"error": err}) + w.send(reject_data) + raise TransferError(err) + def _handle_transit(self, receiver_transit): ts = self._transit_sender ts.add_connection_hints(receiver_transit.get("hints-v1", [])) diff --git a/src/wormhole/test/test_cli.py b/src/wormhole/test/test_cli.py index eb9872b..fb8fd10 100644 --- a/src/wormhole/test/test_cli.py +++ b/src/wormhole/test/test_cli.py @@ -281,7 +281,8 @@ class PregeneratedCode(ServerBase, ScriptsBase, unittest.TestCase): def _do_test(self, as_subprocess=False, mode="text", addslash=False, override_filename=False, fake_tor=False, overwrite=False, mock_accept=False): - assert mode in ("text", "file", "empty-file", "directory", "slow-text") + assert mode in ("text", "file", "empty-file", "directory", + "slow-text", "slow-sender-text") if fake_tor: assert not as_subprocess send_cfg = config("send") @@ -302,7 +303,7 @@ class PregeneratedCode(ServerBase, ScriptsBase, unittest.TestCase): receive_dir = self.mktemp() os.mkdir(receive_dir) - if mode in ("text", "slow-text"): + if mode in ("text", "slow-text", "slow-sender-text"): send_cfg.text = message elif mode in ("file", "empty-file"): @@ -428,20 +429,22 @@ class PregeneratedCode(ServerBase, ScriptsBase, unittest.TestCase): ) as mrx_tm: receive_d = cmd_receive.receive(recv_cfg) else: - send_d = cmd_send.send(send_cfg) - receive_d = cmd_receive.receive(recv_cfg) + KEY_TIMER = 0 if mode == "slow-sender-text" else 1.0 + with mock.patch.object(cmd_receive, "KEY_TIMER", KEY_TIMER): + send_d = cmd_send.send(send_cfg) + receive_d = cmd_receive.receive(recv_cfg) # The sender might fail, leaving the receiver hanging, or vice # versa. Make sure we don't wait on one side exclusively - if mode == "slow-text": - with mock.patch.object(cmd_send, "VERIFY_TIMER", 0), \ - mock.patch.object(cmd_receive, "VERIFY_TIMER", 0): - yield gatherResults([send_d, receive_d], True) - elif mock_accept: - with mock.patch.object(cmd_receive.six.moves, 'input', return_value='y'): - yield gatherResults([send_d, receive_d], True) - else: - yield gatherResults([send_d, receive_d], True) + VERIFY_TIMER = 0 if mode == "slow-text" else 1.0 + with mock.patch.object(cmd_receive, "VERIFY_TIMER", VERIFY_TIMER): + with mock.patch.object(cmd_send, "VERIFY_TIMER", VERIFY_TIMER): + if mock_accept: + with mock.patch.object(cmd_receive.six.moves, + 'input', return_value='y'): + yield gatherResults([send_d, receive_d], True) + else: + yield gatherResults([send_d, receive_d], True) if fake_tor: expected_endpoints = [("127.0.0.1", self.relayport)] @@ -512,9 +515,14 @@ class PregeneratedCode(ServerBase, ScriptsBase, unittest.TestCase): .format(NL=NL), send_stderr) # check receiver - if mode == "text" or mode == "slow-text": + if mode in ("text", "slow-text", "slow-sender-text"): self.assertEqual(receive_stdout, message+NL) - self.assertEqual(receive_stderr, key_established) + if mode == "text": + self.assertEqual(receive_stderr, "") + elif mode == "slow-text": + self.assertEqual(receive_stderr, key_established) + elif mode == "slow-sender-text": + self.assertEqual(receive_stderr, "Waiting for sender...\n") elif mode == "file": self.failUnlessEqual(receive_stdout, "") self.failUnlessIn("Receiving file ({size:s}) into: {name}" @@ -578,7 +586,8 @@ class PregeneratedCode(ServerBase, ScriptsBase, unittest.TestCase): def test_slow_text(self): return self._do_test(mode="slow-text") - test_slow_text.skip = "pending rethink" + def test_slow_sender_text(self): + return self._do_test(mode="slow-sender-text") @inlineCallbacks def _do_test_fail(self, mode, failmode):