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):