add "wormhole" entrypoint script. requires twisted.

I'm using Twisted for the subcommand argument parsing. It might be nice
to use something smaller.
This commit is contained in:
Brian Warner 2015-03-02 00:32:21 -08:00
parent 5682ddff8e
commit 7a99c04d64
11 changed files with 277 additions and 183 deletions

View File

@ -1,73 +0,0 @@
from __future__ import print_function
import sys, os, json
from nacl.secret import SecretBox
from wormhole.blocking.transcribe import Receiver, WrongPasswordError
from wormhole.blocking.transit import TransitReceiver
APPID = "lothar.com/wormhole/file-xfer"
# we're receiving
transit_receiver = TransitReceiver()
mydata = json.dumps({
"transit": {
"direct_connection_hints": transit_receiver.get_direct_hints(),
"relay_connection_hints": transit_receiver.get_relay_hints(),
},
}).encode("utf-8")
r = Receiver(APPID, mydata)
r.set_code(r.input_code("Enter receive-file wormhole code: "))
try:
data = json.loads(r.get_data().decode("utf-8"))
except WrongPasswordError as e:
print("ERROR: " + e.explain(), file=sys.stderr)
sys.exit(1)
#print("their data: %r" % (data,))
file_data = data["file"]
xfer_key = r.derive_key(APPID+"/xfer-key", SecretBox.KEY_SIZE)
filename = os.path.basename(file_data["filename"]) # unicode
filesize = file_data["filesize"]
encrypted_filesize = filesize + SecretBox.NONCE_SIZE+16
# now receive the rest of the owl
tdata = data["transit"]
transit_key = r.derive_key(APPID+"/transit-key")
transit_receiver.set_transit_key(transit_key)
transit_receiver.add_their_direct_hints(tdata["direct_connection_hints"])
transit_receiver.add_their_relay_hints(tdata["relay_connection_hints"])
skt = transit_receiver.establish_connection()
print("Receiving %d bytes.." % filesize)
encrypted = b""
while len(encrypted) < encrypted_filesize:
more = skt.recv(encrypted_filesize - len(encrypted))
if not more:
print("Connection dropped before full file received")
print("got %d bytes, wanted %d" % (len(encrypted), encrypted_filesize))
sys.exit(1)
encrypted += more
assert len(encrypted) == encrypted_filesize
decrypted = SecretBox(xfer_key).decrypt(encrypted)
# only write to the current directory, and never overwrite anything
here = os.path.abspath(os.getcwd())
target = os.path.abspath(os.path.join(here, filename))
if os.path.dirname(target) != here:
print("Error: suggested filename (%s) would be outside current directory"
% (filename,))
skt.send("bad filename\n")
skt.close()
sys.exit(1)
if os.path.exists(target):
print("Error: refusing to overwrite existing file %s" % (filename,))
skt.send("file already exists\n")
skt.close()
sys.exit(1)
with open(target, "wb") as f:
f.write(decrypted)
print("Received file written to %s" % filename)
skt.send("ok\n")
skt.close()
sys.exit(0)

View File

@ -1,19 +0,0 @@
from __future__ import print_function
import sys, time, json
from wormhole.blocking.transcribe import Receiver, WrongPasswordError
APPID = "lothar.com/wormhole/text-xfer"
# we're receiving
data = json.dumps({"message": "ok"}).encode("utf-8")
r = Receiver(APPID, data)
r.set_code(r.input_code("Enter receive-text wormhole code: "))
start = time.time()
try:
them_bytes = r.get_data()
except WrongPasswordError as e:
print("ERROR: " + e.explain(), file=sys.stderr)
sys.exit(1)
them_d = json.loads(them_bytes.decode("utf-8"))
print(them_d["message"])
print("elapsed time: %.2f" % (time.time() - start))

View File

@ -1,68 +0,0 @@
from __future__ import print_function
import os, sys, json
from nacl.secret import SecretBox
from wormhole.blocking.transcribe import Initiator, WrongPasswordError
from wormhole.blocking.transit import TransitSender
APPID = "lothar.com/wormhole/file-xfer"
# we're sending
filename = sys.argv[1]
assert os.path.isfile(filename)
transit_sender = TransitSender()
filesize = os.stat(filename).st_size
data = json.dumps({
"file": {
"filename": os.path.basename(filename),
"filesize": filesize,
},
"transit": {
"direct_connection_hints": transit_sender.get_direct_hints(),
"relay_connection_hints": transit_sender.get_relay_hints(),
},
}).encode("utf-8")
i = Initiator(APPID, data)
code = i.get_code()
print("On the other computer, please run: receive_file")
print("Wormhole code is '%s'" % code)
print("")
try:
them_bytes = i.get_data()
except WrongPasswordError as e:
print("ERROR: " + e.explain(), file=sys.stderr)
sys.exit(1)
them_d = json.loads(them_bytes.decode("utf-8"))
#print("them: %r" % (them_d,))
xfer_key = i.derive_key(APPID+"/xfer-key", SecretBox.KEY_SIZE)
box = SecretBox(xfer_key)
with open(filename, "rb") as f:
plaintext = f.read()
nonce = os.urandom(SecretBox.NONCE_SIZE)
encrypted = box.encrypt(plaintext, nonce)
tdata = them_d["transit"]
transit_key = i.derive_key(APPID+"/transit-key")
transit_sender.set_transit_key(transit_key)
transit_sender.add_their_direct_hints(tdata["direct_connection_hints"])
transit_sender.add_their_relay_hints(tdata["relay_connection_hints"])
skt = transit_sender.establish_connection()
print("Sending %d bytes.." % filesize)
sent = 0
while sent < len(encrypted):
more = skt.send(encrypted[sent:])
sent += more
print("File sent.. waiting for confirmation")
# ack is a short newline-terminated string, followed by socket close. A long
# read is probably good enough.
ack = skt.recv(300)
if ack == "ok\n":
print("Confirmation received. Transfer complete.")
sys.exit(0)
else:
print("Transfer failed (remote says: %r)" % ack)
sys.exit(1)

View File

@ -1,22 +0,0 @@
from __future__ import print_function
import sys, json
from wormhole.blocking.transcribe import Initiator, WrongPasswordError
APPID = "lothar.com/wormhole/text-xfer"
# we're sending
message = sys.argv[1]
data = json.dumps({"message": message,
}).encode("utf-8")
i = Initiator(APPID, data)
code = i.get_code()
print("On the other computer, please run: receive_text")
print("Wormhole code is: %s" % code)
print("")
try:
them_bytes = i.get_data()
except WrongPasswordError as e:
print("ERROR: " + e.explain(), file=sys.stderr)
sys.exit(1)
them_d = json.loads(them_bytes.decode("utf-8"))
print("them: %r" % (them_d,))

View File

@ -19,7 +19,9 @@ setup(name="wormhole-sync",
url="https://github.com/warner/wormhole-sync",
package_dir={"": "src"},
packages=["wormhole"],
install_requires=["spake2", "pynacl", "requests"],
entry_points={"console_scripts":
["wormhole = wormhole.scripts.runner:entry"]},
install_requires=["spake2", "pynacl", "requests", "twisted"],
test_suite="wormhole.test",
cmdclass=commands,
)

View File

View File

@ -0,0 +1,74 @@
from __future__ import print_function
import sys, os, json
from nacl.secret import SecretBox
from wormhole.blocking.transcribe import Receiver, WrongPasswordError
from wormhole.blocking.transit import TransitReceiver
APPID = "lothar.com/wormhole/file-xfer"
def receive_file(so):
# we're receiving
transit_receiver = TransitReceiver()
mydata = json.dumps({
"transit": {
"direct_connection_hints": transit_receiver.get_direct_hints(),
"relay_connection_hints": transit_receiver.get_relay_hints(),
},
}).encode("utf-8")
r = Receiver(APPID, mydata)
r.set_code(r.input_code("Enter receive-file wormhole code: "))
try:
data = json.loads(r.get_data().decode("utf-8"))
except WrongPasswordError as e:
print("ERROR: " + e.explain(), file=sys.stderr)
return 1
#print("their data: %r" % (data,))
file_data = data["file"]
xfer_key = r.derive_key(APPID+"/xfer-key", SecretBox.KEY_SIZE)
filename = os.path.basename(file_data["filename"]) # unicode
filesize = file_data["filesize"]
encrypted_filesize = filesize + SecretBox.NONCE_SIZE+16
# now receive the rest of the owl
tdata = data["transit"]
transit_key = r.derive_key(APPID+"/transit-key")
transit_receiver.set_transit_key(transit_key)
transit_receiver.add_their_direct_hints(tdata["direct_connection_hints"])
transit_receiver.add_their_relay_hints(tdata["relay_connection_hints"])
skt = transit_receiver.establish_connection()
print("Receiving %d bytes.." % filesize)
encrypted = b""
while len(encrypted) < encrypted_filesize:
more = skt.recv(encrypted_filesize - len(encrypted))
if not more:
print("Connection dropped before full file received")
print("got %d bytes, wanted %d" % (len(encrypted), encrypted_filesize))
return 1
encrypted += more
assert len(encrypted) == encrypted_filesize
decrypted = SecretBox(xfer_key).decrypt(encrypted)
# only write to the current directory, and never overwrite anything
here = os.path.abspath(os.getcwd())
target = os.path.abspath(os.path.join(here, filename))
if os.path.dirname(target) != here:
print("Error: suggested filename (%s) would be outside current directory"
% (filename,))
skt.send("bad filename\n")
skt.close()
return 1
if os.path.exists(target):
print("Error: refusing to overwrite existing file %s" % (filename,))
skt.send("file already exists\n")
skt.close()
return 1
with open(target, "wb") as f:
f.write(decrypted)
print("Received file written to %s" % filename)
skt.send("ok\n")
skt.close()
return 0

View File

@ -0,0 +1,20 @@
from __future__ import print_function
import sys, time, json
from wormhole.blocking.transcribe import Receiver, WrongPasswordError
APPID = "lothar.com/wormhole/text-xfer"
def receive_text(so):
# we're receiving
data = json.dumps({"message": "ok"}).encode("utf-8")
r = Receiver(APPID, data)
r.set_code(r.input_code("Enter receive-text wormhole code: "))
start = time.time()
try:
them_bytes = r.get_data()
except WrongPasswordError as e:
print("ERROR: " + e.explain(), file=sys.stderr)
return 1
them_d = json.loads(them_bytes.decode("utf-8"))
print(them_d["message"])
print("elapsed time: %.2f" % (time.time() - start))

View File

@ -0,0 +1,69 @@
from __future__ import print_function
import os, sys, json
from nacl.secret import SecretBox
from wormhole.blocking.transcribe import Initiator, WrongPasswordError
from wormhole.blocking.transit import TransitSender
APPID = "lothar.com/wormhole/file-xfer"
def send_file(so):
# we're sending
filename = so["filename"]
assert os.path.isfile(filename)
transit_sender = TransitSender()
filesize = os.stat(filename).st_size
data = json.dumps({
"file": {
"filename": os.path.basename(filename),
"filesize": filesize,
},
"transit": {
"direct_connection_hints": transit_sender.get_direct_hints(),
"relay_connection_hints": transit_sender.get_relay_hints(),
},
}).encode("utf-8")
i = Initiator(APPID, data)
code = i.get_code()
print("On the other computer, please run: receive_file")
print("Wormhole code is '%s'" % code)
print("")
try:
them_bytes = i.get_data()
except WrongPasswordError as e:
print("ERROR: " + e.explain(), file=sys.stderr)
return 1
them_d = json.loads(them_bytes.decode("utf-8"))
#print("them: %r" % (them_d,))
xfer_key = i.derive_key(APPID+"/xfer-key", SecretBox.KEY_SIZE)
box = SecretBox(xfer_key)
with open(filename, "rb") as f:
plaintext = f.read()
nonce = os.urandom(SecretBox.NONCE_SIZE)
encrypted = box.encrypt(plaintext, nonce)
tdata = them_d["transit"]
transit_key = i.derive_key(APPID+"/transit-key")
transit_sender.set_transit_key(transit_key)
transit_sender.add_their_direct_hints(tdata["direct_connection_hints"])
transit_sender.add_their_relay_hints(tdata["relay_connection_hints"])
skt = transit_sender.establish_connection()
print("Sending %d bytes.." % filesize)
sent = 0
while sent < len(encrypted):
more = skt.send(encrypted[sent:])
sent += more
print("File sent.. waiting for confirmation")
# ack is a short newline-terminated string, followed by socket close. A long
# read is probably good enough.
ack = skt.recv(300)
if ack == "ok\n":
print("Confirmation received. Transfer complete.")
return 0
else:
print("Transfer failed (remote says: %r)" % ack)
return 1

View File

@ -0,0 +1,23 @@
from __future__ import print_function
import sys, json
from wormhole.blocking.transcribe import Initiator, WrongPasswordError
APPID = "lothar.com/wormhole/text-xfer"
def send_text(so):
# we're sending
message = so["text"]
data = json.dumps({"message": message,
}).encode("utf-8")
i = Initiator(APPID, data)
code = i.get_code()
print("On the other computer, please run: receive_text")
print("Wormhole code is: %s" % code)
print("")
try:
them_bytes = i.get_data()
except WrongPasswordError as e:
print("ERROR: " + e.explain(), file=sys.stderr)
return 1
them_d = json.loads(them_bytes.decode("utf-8"))
print("them: %r" % (them_d,))

View File

@ -0,0 +1,88 @@
import sys
from twisted.python import usage
class SendTextOptions(usage.Options):
def parseArgs(self, text):
self["text"] = text
synopsis = "TEXT"
class ReceiveTextOptions(usage.Options):
synopsis = ""
class SendFileOptions(usage.Options):
def parseArgs(self, filename):
self["filename"] = filename
synopsis = "FILENAME"
class ReceiveFileOptions(usage.Options):
synopsis = ""
class Options(usage.Options):
synopsis = "\nUsage: wormhole <command>"
subCommands = [("send-text", None, SendTextOptions, "Send a text message"),
("send-file", None, SendFileOptions, "Send a file"),
("receive-text", None, ReceiveTextOptions, "Receive a text message"),
("receive-file", None, ReceiveFileOptions, "Receive a file"),
]
def getUsage(self, **kwargs):
t = usage.Options.getUsage(self, **kwargs)
return t + "\nPlease run 'wormhole <command> --help' for more details on each command.\n"
def postOptions(self):
if not hasattr(self, 'subOptions'):
raise usage.UsageError("must specify a command")
def send_text(*args):
from . import cmd_send_text
return cmd_send_text.send_text(*args)
def receive_text(*args):
from . import cmd_receive_text
return cmd_receive_text.receive_text(*args)
def send_file(*args):
from . import cmd_send_file
return cmd_send_file.send_file(*args)
def receive_file(*args):
from . import cmd_receive_file
return cmd_receive_file.receive_file(*args)
DISPATCH = {"send-text": send_text,
"receive-text": receive_text,
"send-file": send_file,
"receive-file": receive_file,
}
def run(args, stdout, stderr, executable=None):
"""This is invoked directly by the 'wormhole' entry-point script. It can
also invoked by entry() below."""
config = Options()
try:
config.parseOptions(args)
except usage.error, e:
c = config
while hasattr(c, 'subOptions'):
c = c.subOptions
print >>stderr, str(c)
print >>stderr, e.args[0]
return 1
command = config.subCommand
so = config.subOptions
so["executable"] = executable
try:
#rc = DISPATCH[command](so, stdout, stderr)
rc = DISPATCH[command](so)
return rc
except ImportError, e:
print >>stderr, "--- ImportError ---"
print >>stderr, e
print >>stderr, "Please run 'python setup.py build'"
raise
return 1
def entry():
"""This is used by a setuptools entry_point. When invoked this way,
setuptools has already put the installed package on sys.path ."""
return run(sys.argv[1:], sys.stdout, sys.stderr, executable=sys.argv[0])