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:
parent
96f25ec7a2
commit
ac1db705fe
|
@ -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)
|
||||||
|
|
|
@ -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):
|
||||||
|
|
Loading…
Reference in New Issue
Block a user