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.
This commit is contained in:
Brian Warner 2016-05-24 17:27:26 -07:00
parent 96f25ec7a2
commit ac1db705fe
2 changed files with 131 additions and 91 deletions

View File

@ -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)
want_offer = True
done = False
while True:
try:
if "message" in them_d:
self.handle_text(them_d, w)
them_d = yield self.get_data(w)
except WormholeClosedError:
if done:
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")
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)
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,11 +215,13 @@ class TwistedReceiver:
direct_hints = yield transit_receiver.get_direct_hints()
relay_hints = yield transit_receiver.get_relay_hints()
data = json.dumps({
"answer": {
"file_ack": "ok",
"transit": {
"direct_connection_hints": direct_hints,
"relay_connection_hints": relay_hints,
},
},
}).encode("utf-8")
w.send(data)

View File

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