From ac1db705fe9559a1921198b55f1bde467d25dbed Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Tue, 24 May 2016 17:27:26 -0700 Subject: [PATCH] INCOMPATIBLE CHANGE: put offer/answer in their own keys This moves us slowly towards a file-transfer protocol that exchanges multiple messages, with a single offer (sender->receiver) and answer (receiver->sender), and one or more connection hint messages (in either direction) that appear gradually over time as connection providers come online. At present the protocol still expects the whole hint list to be present in the offer/answer message. --- src/wormhole/cli/cmd_receive.py | 102 ++++++++++++++++----------- src/wormhole/cli/cmd_send.py | 120 ++++++++++++++++++-------------- 2 files changed, 131 insertions(+), 91 deletions(-) diff --git a/src/wormhole/cli/cmd_receive.py b/src/wormhole/cli/cmd_receive.py index e7fa2ab..873c497 100644 --- a/src/wormhole/cli/cmd_receive.py +++ b/src/wormhole/cli/cmd_receive.py @@ -3,9 +3,10 @@ import os, sys, json, binascii, six, tempfile, zipfile from tqdm import tqdm from twisted.internet import reactor from twisted.internet.defer import inlineCallbacks, returnValue +from twisted.python import log from ..wormhole import wormhole from ..transit import TransitReceiver -from ..errors import TransferError +from ..errors import TransferError, WormholeClosedError APPID = u"lothar.com/wormhole/text-or-file-xfer" @@ -29,24 +30,26 @@ class TwistedReceiver: assert isinstance(args.relay_url, type(u"")) self.args = args self._reactor = reactor + self._tor_manager = None def msg(self, *args, **kwargs): print(*args, file=self.args.stdout, **kwargs) @inlineCallbacks def go(self): - tor_manager = None if self.args.tor: with self.args.timing.add("import", which="tor_manager"): from ..tor_manager import TorManager - tor_manager = TorManager(self._reactor, timing=self.args.timing) + self._tor_manager = TorManager(self._reactor, + timing=self.args.timing) # For now, block everything until Tor has started. Soon: launch # tor in parallel with everything else, make sure the TorManager # can lazy-provide an endpoint, and overlap the startup process # with the user handing off the wormhole code - yield tor_manager.start() + yield self._tor_manager.start() + w = wormhole(APPID, self.args.relay_url, self._reactor, - tor_manager, timing=self.args.timing) + self._tor_manager, timing=self.args.timing) # I wanted to do this instead: # # try: @@ -57,41 +60,60 @@ class TwistedReceiver: # but when _go had a UsageError, the stacktrace was always displayed # as coming from the "yield self._go" line, which wasn't very useful # for tracking it down. - d = self._go(w, tor_manager) + d = self._go(w) d.addBoth(w.close) yield d @inlineCallbacks - def _go(self, w, tor_manager): + def _go(self, w): yield self.handle_code(w) verifier = yield w.verify() self.show_verifier(verifier) - them_d = yield self.get_data(w) - try: - if "message" in them_d: - self.handle_text(them_d, w) + + want_offer = True + done = False + + while True: + try: + them_d = yield self.get_data(w) + except WormholeClosedError: + if done: + returnValue(None) + raise TransferError("unexpected close") + if u"offer" in them_d: + if not want_offer: + raise TransferError("duplicate offer") + try: + self.parse_offer(them_d[u"offer"], w) + except RespondError as r: + data = json.dumps({"error": r.response}).encode("utf-8") + w.send(data) + raise TransferError(r.response) returnValue(None) - if "file" in them_d: - f = self.handle_file(them_d) - rp = yield self.establish_transit(w, them_d, tor_manager) - yield self.transfer_data(rp, f) - self.write_file(f) - yield self.close_transit(rp) - elif "directory" in them_d: - f = self.handle_directory(them_d) - rp = yield self.establish_transit(w, them_d, tor_manager) - yield self.transfer_data(rp, f) - self.write_directory(f) - yield self.close_transit(rp) - else: - self.msg(u"I don't know what they're offering\n") - self.msg(u"Offer details:", them_d) - raise RespondError("unknown offer type") - except RespondError as r: - data = json.dumps({"error": r.response}).encode("utf-8") - w.send(data) - raise TransferError(r.response) - returnValue(None) + log.msg("unrecognized message %r" % (them_d,)) + raise TransferError("expected offer, got none") + + @inlineCallbacks + def parse_offer(self, them_d, w): + if "message" in them_d: + self.handle_text(them_d, w) + returnValue(None) + if "file" in them_d: + f = self.handle_file(them_d) + rp = yield self.establish_transit(w, them_d) + yield self.transfer_data(rp, f) + self.write_file(f) + yield self.close_transit(rp) + elif "directory" in them_d: + f = self.handle_directory(them_d) + rp = yield self.establish_transit(w, them_d) + yield self.transfer_data(rp, f) + self.write_directory(f) + yield self.close_transit(rp) + else: + self.msg(u"I don't know what they're offering\n") + self.msg(u"Offer details: %r" % (them_d,)) + raise RespondError("unknown offer type") @inlineCallbacks def handle_code(self, w): @@ -122,7 +144,7 @@ class TwistedReceiver: def handle_text(self, them_d, w): # we're receiving a text message self.msg(them_d["message"]) - data = json.dumps({"message_ack": "ok"}).encode("utf-8") + data = json.dumps({"answer": {"message_ack": "ok"}}).encode("utf-8") w.send(data) def handle_file(self, them_d): @@ -181,10 +203,10 @@ class TwistedReceiver: t.detail(answer="yes") @inlineCallbacks - def establish_transit(self, w, them_d, tor_manager): + def establish_transit(self, w, them_d): transit_receiver = TransitReceiver(self.args.transit_helper, no_listen=self.args.no_listen, - tor_manager=tor_manager, + tor_manager=self._tor_manager, reactor=self._reactor, timing=self.args.timing) transit_key = w.derive_key(APPID+u"/transit-key", @@ -193,10 +215,12 @@ class TwistedReceiver: direct_hints = yield transit_receiver.get_direct_hints() relay_hints = yield transit_receiver.get_relay_hints() data = json.dumps({ - "file_ack": "ok", - "transit": { - "direct_connection_hints": direct_hints, - "relay_connection_hints": relay_hints, + "answer": { + "file_ack": "ok", + "transit": { + "direct_connection_hints": direct_hints, + "relay_connection_hints": relay_hints, + }, }, }).encode("utf-8") w.send(data) diff --git a/src/wormhole/cli/cmd_send.py b/src/wormhole/cli/cmd_send.py index cb1f8f5..a620f65 100644 --- a/src/wormhole/cli/cmd_send.py +++ b/src/wormhole/cli/cmd_send.py @@ -1,10 +1,11 @@ from __future__ import print_function import os, sys, json, binascii, six, tempfile, zipfile from tqdm import tqdm +from twisted.python import log from twisted.protocols import basic from twisted.internet import reactor from twisted.internet.defer import inlineCallbacks, returnValue -from ..errors import TransferError +from ..errors import TransferError, WormholeClosedError from ..wormhole import wormhole from ..transit import TransitSender @@ -56,6 +57,30 @@ def _send(reactor, w, args, tor_manager): print(u"On the other computer, please run: %s" % other_cmd, file=args.stdout) + if args.code: + w.set_code(args.code) + code = args.code + else: + code = yield w.get_code(args.code_length) + + if not args.zeromode: + print(u"Wormhole code is: %s" % code, file=args.stdout) + print(u"", file=args.stdout) + + # TODO: don't stall on w.verify() unless they want it + verifier_bytes = yield w.verify() # this may raise WrongPasswordError + if args.verify: + verifier = binascii.hexlify(verifier_bytes).decode("ascii") + 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 = json.dumps({"error": err}).encode("utf-8") + w.send(reject_data) + raise TransferError(err) + transit_sender = None if fd_to_send: transit_sender = TransitSender(args.transit_helper, @@ -68,64 +93,34 @@ def _send(reactor, w, args, tor_manager): direct_hints = yield transit_sender.get_direct_hints() transit_data["direct_connection_hints"] = direct_hints - if args.code: - w.set_code(args.code) - code = args.code - else: - code = yield w.get_code(args.code_length) - - if not args.zeromode: - print(u"Wormhole code is: %s" % code, file=args.stdout) - print(u"", file=args.stdout) - - # get the verifier, because that also lets us derive the transit key, - # which we want to set before revealing the connection hints to the far - # side, so we'll be ready for them when they connect - verifier_bytes = yield w.verify() - verifier = binascii.hexlify(verifier_bytes).decode("ascii") - - if args.verify: - 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 = json.dumps({"error": err}).encode("utf-8") - w.send(reject_data) - raise TransferError(err) - if fd_to_send is not None: + # TODO: move this down below w.get() transit_key = w.derive_key(APPID+"/transit-key", transit_sender.TRANSIT_KEY_LENGTH) transit_sender.set_transit_key(transit_key) - my_offer_bytes = json.dumps(offer).encode("utf-8") + my_offer_bytes = json.dumps({"offer": offer}).encode("utf-8") w.send(my_offer_bytes) - # this may raise WrongPasswordError - them_answer_bytes = yield w.get() + want_answer = True + done = False - them_answer = json.loads(them_answer_bytes.decode("utf-8")) - - if fd_to_send is None: - if them_answer["message_ack"] == "ok": - print(u"text message sent", file=args.stdout) - returnValue(None) # terminates this function - raise TransferError("error sending text: %r" % (them_answer,)) - - if "error" in them_answer: - raise TransferError("remote error, transfer abandoned: %s" - % them_answer["error"]) - if them_answer.get("file_ack") != "ok": - raise TransferError("ambiguous response from remote, " - "transfer abandoned: %s" % (them_answer,)) - tdata = them_answer["transit"] - # XXX the downside of closing above, rather than here, is that it leaves - # the channel claimed for a longer time - #yield w.close() - yield _send_file_twisted(tdata, transit_sender, fd_to_send, - args.stdout, args.hide_progress, args.timing) - returnValue(None) + while True: + try: + them_d_bytes = yield w.get() + except WormholeClosedError: + if done: + returnValue(None) + raise TransferError("unexpected close") + # TODO: get() fired, so now it's safe to use w.derive_key() + them_d = json.loads(them_d_bytes.decode("utf-8")) + if u"answer" in them_d: + if not want_answer: + raise TransferError("duplicate answer") + them_answer = them_d[u"answer"] + yield handle_answer(them_answer, args, fd_to_send, transit_sender) + done = True + returnValue(None) + log.msg("unrecognized message %r" % (them_d,)) def build_offer(args): offer = {} @@ -199,6 +194,27 @@ def build_offer(args): raise TypeError("'%s' is neither file nor directory" % args.what) +@inlineCallbacks +def handle_answer(them_answer, args, fd_to_send, transit_sender): + if fd_to_send is None: + if them_answer["message_ack"] == "ok": + print(u"text message sent", file=args.stdout) + returnValue(None) # terminates this function + raise TransferError("error sending text: %r" % (them_answer,)) + + if "error" in them_answer: + raise TransferError("remote error, transfer abandoned: %s" + % them_answer["error"]) + if them_answer.get("file_ack") != "ok": + raise TransferError("ambiguous response from remote, " + "transfer abandoned: %s" % (them_answer,)) + + tdata = them_answer["transit"] + yield _send_file_twisted(tdata, transit_sender, fd_to_send, + args.stdout, args.hide_progress, + args.timing) + + @inlineCallbacks def _send_file_twisted(tdata, transit_sender, fd_to_send, stdout, hide_progress, timing):