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 tqdm import tqdm
from twisted.internet import reactor from twisted.internet import reactor
from twisted.internet.defer import inlineCallbacks, returnValue from twisted.internet.defer import inlineCallbacks, returnValue
from twisted.python import log
from ..wormhole import wormhole from ..wormhole import wormhole
from ..transit import TransitReceiver from ..transit import TransitReceiver
from ..errors import TransferError from ..errors import TransferError, WormholeClosedError
APPID = u"lothar.com/wormhole/text-or-file-xfer" APPID = u"lothar.com/wormhole/text-or-file-xfer"
@ -29,24 +30,26 @@ class TwistedReceiver:
assert isinstance(args.relay_url, type(u"")) assert isinstance(args.relay_url, type(u""))
self.args = args self.args = args
self._reactor = reactor self._reactor = reactor
self._tor_manager = None
def msg(self, *args, **kwargs): def msg(self, *args, **kwargs):
print(*args, file=self.args.stdout, **kwargs) print(*args, file=self.args.stdout, **kwargs)
@inlineCallbacks @inlineCallbacks
def go(self): def go(self):
tor_manager = None
if self.args.tor: if self.args.tor:
with self.args.timing.add("import", which="tor_manager"): with self.args.timing.add("import", which="tor_manager"):
from ..tor_manager import TorManager 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 # For now, block everything until Tor has started. Soon: launch
# tor in parallel with everything else, make sure the TorManager # tor in parallel with everything else, make sure the TorManager
# can lazy-provide an endpoint, and overlap the startup process # can lazy-provide an endpoint, and overlap the startup process
# with the user handing off the wormhole code # 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, 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: # I wanted to do this instead:
# #
# try: # try:
@ -57,41 +60,60 @@ class TwistedReceiver:
# but when _go had a UsageError, the stacktrace was always displayed # but when _go had a UsageError, the stacktrace was always displayed
# as coming from the "yield self._go" line, which wasn't very useful # as coming from the "yield self._go" line, which wasn't very useful
# for tracking it down. # for tracking it down.
d = self._go(w, tor_manager) d = self._go(w)
d.addBoth(w.close) d.addBoth(w.close)
yield d yield d
@inlineCallbacks @inlineCallbacks
def _go(self, w, tor_manager): def _go(self, w):
yield self.handle_code(w) yield self.handle_code(w)
verifier = yield w.verify() verifier = yield w.verify()
self.show_verifier(verifier) self.show_verifier(verifier)
them_d = yield self.get_data(w)
try: want_offer = True
if "message" in them_d: done = False
self.handle_text(them_d, w)
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) returnValue(None)
if "file" in them_d: log.msg("unrecognized message %r" % (them_d,))
f = self.handle_file(them_d) raise TransferError("expected offer, got none")
rp = yield self.establish_transit(w, them_d, tor_manager)
yield self.transfer_data(rp, f) @inlineCallbacks
self.write_file(f) def parse_offer(self, them_d, w):
yield self.close_transit(rp) if "message" in them_d:
elif "directory" in them_d: self.handle_text(them_d, w)
f = self.handle_directory(them_d) returnValue(None)
rp = yield self.establish_transit(w, them_d, tor_manager) if "file" in them_d:
yield self.transfer_data(rp, f) f = self.handle_file(them_d)
self.write_directory(f) rp = yield self.establish_transit(w, them_d)
yield self.close_transit(rp) yield self.transfer_data(rp, f)
else: self.write_file(f)
self.msg(u"I don't know what they're offering\n") yield self.close_transit(rp)
self.msg(u"Offer details:", them_d) elif "directory" in them_d:
raise RespondError("unknown offer type") f = self.handle_directory(them_d)
except RespondError as r: rp = yield self.establish_transit(w, them_d)
data = json.dumps({"error": r.response}).encode("utf-8") yield self.transfer_data(rp, f)
w.send(data) self.write_directory(f)
raise TransferError(r.response) yield self.close_transit(rp)
returnValue(None) 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 @inlineCallbacks
def handle_code(self, w): def handle_code(self, w):
@ -122,7 +144,7 @@ class TwistedReceiver:
def handle_text(self, them_d, w): def handle_text(self, them_d, w):
# we're receiving a text message # we're receiving a text message
self.msg(them_d["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) w.send(data)
def handle_file(self, them_d): def handle_file(self, them_d):
@ -181,10 +203,10 @@ class TwistedReceiver:
t.detail(answer="yes") t.detail(answer="yes")
@inlineCallbacks @inlineCallbacks
def establish_transit(self, w, them_d, tor_manager): def establish_transit(self, w, them_d):
transit_receiver = TransitReceiver(self.args.transit_helper, transit_receiver = TransitReceiver(self.args.transit_helper,
no_listen=self.args.no_listen, no_listen=self.args.no_listen,
tor_manager=tor_manager, tor_manager=self._tor_manager,
reactor=self._reactor, reactor=self._reactor,
timing=self.args.timing) timing=self.args.timing)
transit_key = w.derive_key(APPID+u"/transit-key", transit_key = w.derive_key(APPID+u"/transit-key",
@ -193,10 +215,12 @@ class TwistedReceiver:
direct_hints = yield transit_receiver.get_direct_hints() direct_hints = yield transit_receiver.get_direct_hints()
relay_hints = yield transit_receiver.get_relay_hints() relay_hints = yield transit_receiver.get_relay_hints()
data = json.dumps({ data = json.dumps({
"file_ack": "ok", "answer": {
"transit": { "file_ack": "ok",
"direct_connection_hints": direct_hints, "transit": {
"relay_connection_hints": relay_hints, "direct_connection_hints": direct_hints,
"relay_connection_hints": relay_hints,
},
}, },
}).encode("utf-8") }).encode("utf-8")
w.send(data) w.send(data)

View File

@ -1,10 +1,11 @@
from __future__ import print_function from __future__ import print_function
import os, sys, json, binascii, six, tempfile, zipfile import os, sys, json, binascii, six, tempfile, zipfile
from tqdm import tqdm from tqdm import tqdm
from twisted.python import log
from twisted.protocols import basic from twisted.protocols import basic
from twisted.internet import reactor from twisted.internet import reactor
from twisted.internet.defer import inlineCallbacks, returnValue from twisted.internet.defer import inlineCallbacks, returnValue
from ..errors import TransferError from ..errors import TransferError, WormholeClosedError
from ..wormhole import wormhole from ..wormhole import wormhole
from ..transit import TransitSender 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, print(u"On the other computer, please run: %s" % other_cmd,
file=args.stdout) 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 transit_sender = None
if fd_to_send: if fd_to_send:
transit_sender = TransitSender(args.transit_helper, 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() direct_hints = yield transit_sender.get_direct_hints()
transit_data["direct_connection_hints"] = direct_hints transit_data["direct_connection_hints"] = direct_hints
if args.code: # TODO: move this down below w.get()
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:
transit_key = w.derive_key(APPID+"/transit-key", transit_key = w.derive_key(APPID+"/transit-key",
transit_sender.TRANSIT_KEY_LENGTH) transit_sender.TRANSIT_KEY_LENGTH)
transit_sender.set_transit_key(transit_key) 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) w.send(my_offer_bytes)
# this may raise WrongPasswordError want_answer = True
them_answer_bytes = yield w.get() done = False
them_answer = json.loads(them_answer_bytes.decode("utf-8")) while True:
try:
if fd_to_send is None: them_d_bytes = yield w.get()
if them_answer["message_ack"] == "ok": except WormholeClosedError:
print(u"text message sent", file=args.stdout) if done:
returnValue(None) # terminates this function returnValue(None)
raise TransferError("error sending text: %r" % (them_answer,)) raise TransferError("unexpected close")
# TODO: get() fired, so now it's safe to use w.derive_key()
if "error" in them_answer: them_d = json.loads(them_d_bytes.decode("utf-8"))
raise TransferError("remote error, transfer abandoned: %s" if u"answer" in them_d:
% them_answer["error"]) if not want_answer:
if them_answer.get("file_ack") != "ok": raise TransferError("duplicate answer")
raise TransferError("ambiguous response from remote, " them_answer = them_d[u"answer"]
"transfer abandoned: %s" % (them_answer,)) yield handle_answer(them_answer, args, fd_to_send, transit_sender)
tdata = them_answer["transit"] done = True
# XXX the downside of closing above, rather than here, is that it leaves returnValue(None)
# the channel claimed for a longer time log.msg("unrecognized message %r" % (them_d,))
#yield w.close()
yield _send_file_twisted(tdata, transit_sender, fd_to_send,
args.stdout, args.hide_progress, args.timing)
returnValue(None)
def build_offer(args): def build_offer(args):
offer = {} offer = {}
@ -199,6 +194,27 @@ def build_offer(args):
raise TypeError("'%s' is neither file nor directory" % args.what) 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 @inlineCallbacks
def _send_file_twisted(tdata, transit_sender, fd_to_send, def _send_file_twisted(tdata, transit_sender, fd_to_send,
stdout, hide_progress, timing): stdout, hide_progress, timing):