diff --git a/bin/receive_file.py b/bin/receive_file.py deleted file mode 100644 index 455df9f..0000000 --- a/bin/receive_file.py +++ /dev/null @@ -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) diff --git a/bin/receive_text.py b/bin/receive_text.py deleted file mode 100644 index e4a1e6f..0000000 --- a/bin/receive_text.py +++ /dev/null @@ -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)) diff --git a/bin/send_file.py b/bin/send_file.py deleted file mode 100644 index 7aafc21..0000000 --- a/bin/send_file.py +++ /dev/null @@ -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) diff --git a/bin/send_text.py b/bin/send_text.py deleted file mode 100644 index a975ff3..0000000 --- a/bin/send_text.py +++ /dev/null @@ -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,)) diff --git a/setup.py b/setup.py index a547db9..55ab7bc 100644 --- a/setup.py +++ b/setup.py @@ -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, ) diff --git a/src/wormhole/scripts/__init__.py b/src/wormhole/scripts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/wormhole/scripts/cmd_receive_file.py b/src/wormhole/scripts/cmd_receive_file.py new file mode 100644 index 0000000..b51371c --- /dev/null +++ b/src/wormhole/scripts/cmd_receive_file.py @@ -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 diff --git a/src/wormhole/scripts/cmd_receive_text.py b/src/wormhole/scripts/cmd_receive_text.py new file mode 100644 index 0000000..4c1601a --- /dev/null +++ b/src/wormhole/scripts/cmd_receive_text.py @@ -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)) diff --git a/src/wormhole/scripts/cmd_send_file.py b/src/wormhole/scripts/cmd_send_file.py new file mode 100644 index 0000000..1188650 --- /dev/null +++ b/src/wormhole/scripts/cmd_send_file.py @@ -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 diff --git a/src/wormhole/scripts/cmd_send_text.py b/src/wormhole/scripts/cmd_send_text.py new file mode 100644 index 0000000..1943012 --- /dev/null +++ b/src/wormhole/scripts/cmd_send_text.py @@ -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,)) diff --git a/src/wormhole/scripts/runner.py b/src/wormhole/scripts/runner.py new file mode 100644 index 0000000..9fc6744 --- /dev/null +++ b/src/wormhole/scripts/runner.py @@ -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 " + 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 --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])