diff --git a/README.md b/README.md index b4a2077..0169319 100644 --- a/README.md +++ b/README.md @@ -98,11 +98,9 @@ provide information about alternatives. ## CLI tool -* `wormhole send-text TEXT` -* `wormhole receive-text` -* -* `wormhole send-file FILENAME` -* `wormhole receive-file` +* `wormhole send TEXT` +* `wormhole send FILENAME` +* `wormhole receive` All four commands accept: diff --git a/src/wormhole/scripts/cmd_receive_file.py b/src/wormhole/scripts/cmd_receive.py similarity index 57% rename from src/wormhole/scripts/cmd_receive_file.py rename to src/wormhole/scripts/cmd_receive.py index 796b9ff..3cb3dd5 100644 --- a/src/wormhole/scripts/cmd_receive_file.py +++ b/src/wormhole/scripts/cmd_receive.py @@ -1,57 +1,98 @@ from __future__ import print_function -import sys, os, json, binascii +import os, sys, json, binascii, six from ..errors import handle_server_error -APPID = b"lothar.com/wormhole/file-xfer" +APPID = b"lothar.com/wormhole/text-or-file-xfer" @handle_server_error -def receive_file(args): - # we're receiving +def receive(args): + # we're receiving text, or a file from ..blocking.transcribe import Wormhole, WrongPasswordError from ..blocking.transit import TransitReceiver, TransitError from .progress import start_progress, update_progress, finish_progress - transit_receiver = TransitReceiver(args.transit_helper) - w = Wormhole(APPID, args.relay_url) if args.zeromode: assert not args.code args.code = "0-" code = args.code if not code: - code = w.input_code("Enter receive-file wormhole code: ", - args.code_length) + code = w.input_code("Enter receive wormhole code: ", args.code_length) w.set_code(code) if args.verify: verifier = binascii.hexlify(w.get_verifier()).decode("ascii") print("Verifier %s." % verifier) - mydata = json.dumps({ + try: + them_bytes = w.get_data() + except WrongPasswordError as e: + print("ERROR: " + e.explain(), file=sys.stderr) + w.close() + return 1 + them_d = json.loads(them_bytes.decode("utf-8")) + if "error" in them_d: + print("ERROR: " + them_d["error"], file=sys.stderr) + w.close() + return 1 + + if "message" in them_d: + # we're receiving a text message + print(them_d["message"]) + data = json.dumps({"message_ack": "ok"}).encode("utf-8") + w.send_data(data) + w.close() + return 0 + + if not "file" in them_d: + print("I don't know what they're offering\n") + print(them_d) + w.close() + return 1 + + if "error" in them_d: + print("ERROR: " + data["error"], file=sys.stderr) + w.close() + return 1 + + file_data = them_d["file"] + # the basename() is intended to protect us against + # "~/.ssh/authorized_keys" and other attacks + filename = os.path.basename(file_data["filename"]) # unicode + filesize = file_data["filesize"] + + # get confirmation from the user before writing to the local directory + if os.path.exists(filename): + print("Error: refusing to overwrite existing file %s" % (filename,)) + data = json.dumps({"error": "file already exists"}).encode("utf-8") + w.send_data(data) + w.close() + return 1 + + print("Receiving file (%d bytes) into: %s" % (filesize, filename)) + while True and not args.accept_file: + ok = six.moves.input("ok? (y/n): ") + if ok.lower().startswith("y"): + break + print("transfer rejected", file=sys.stderr) + data = json.dumps({"error": "transfer rejected"}).encode("utf-8") + w.send_data(data) + w.close() + return 1 + + transit_receiver = TransitReceiver(args.transit_helper) + data = json.dumps({ + "file_ack": "ok", "transit": { "direct_connection_hints": transit_receiver.get_direct_hints(), "relay_connection_hints": transit_receiver.get_relay_hints(), }, }).encode("utf-8") - w.send_data(mydata) - try: - data = json.loads(w.get_data().decode("utf-8")) - except WrongPasswordError as e: - print("ERROR: " + e.explain(), file=sys.stderr) - return 1 - #print("their data: %r" % (data,)) + w.send_data(data) w.close() - if "error" in data: - print("ERROR: " + data["error"], file=sys.stderr) - return 1 - - file_data = data["file"] - filename = os.path.basename(file_data["filename"]) # unicode - filesize = file_data["filesize"] - # now receive the rest of the owl - tdata = data["transit"] + tdata = them_d["transit"] transit_key = w.derive_key(APPID+b"/transit-key") transit_receiver.set_transit_key(transit_key) transit_receiver.add_their_direct_hints(tdata["direct_connection_hints"]) @@ -60,26 +101,7 @@ def receive_file(args): print("Receiving %d bytes for '%s' (%s).." % (filesize, filename, transit_receiver.describe())) - - target = args.output_file - if not target: - # allow the sender to specify the filename, but 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,)) - record_pipe.send_record(b"bad filename\n") - record_pipe.close() - return 1 - if os.path.exists(target) and not args.overwrite: - print("Error: refusing to overwrite existing file %s" % (filename,)) - record_pipe.send_record(b"file already exists\n") - record_pipe.close() - return 1 - tmp = target + ".tmp" - + tmp = filename + ".tmp" with open(tmp, "wb") as f: received = 0 next_update = start_progress(filesize) @@ -97,9 +119,9 @@ def receive_file(args): finish_progress(filesize) assert received == filesize - os.rename(tmp, target) + os.rename(tmp, filename) - print("Received file written to %s" % target) + print("Received file written to %s" % filename) record_pipe.send_record(b"ok\n") record_pipe.close() return 0 diff --git a/src/wormhole/scripts/cmd_receive_text.py b/src/wormhole/scripts/cmd_receive_text.py deleted file mode 100644 index e474bdb..0000000 --- a/src/wormhole/scripts/cmd_receive_text.py +++ /dev/null @@ -1,38 +0,0 @@ -from __future__ import print_function -import sys, json, binascii -from ..errors import handle_server_error - -APPID = b"lothar.com/wormhole/text-xfer" - -@handle_server_error -def receive_text(args): - # we're receiving - from ..blocking.transcribe import Wormhole, WrongPasswordError - - w = Wormhole(APPID, args.relay_url) - if args.zeromode: - assert not args.code - args.code = "0-" - code = args.code - if not code: - code = w.input_code("Enter receive-text wormhole code: ", - args.code_length) - w.set_code(code) - - if args.verify: - verifier = binascii.hexlify(w.get_verifier()).decode("ascii") - print("Verifier %s." % verifier) - - data = json.dumps({"message": "ok"}).encode("utf-8") - w.send_data(data) - try: - them_bytes = w.get_data() - except WrongPasswordError as e: - print("ERROR: " + e.explain(), file=sys.stderr) - return 1 - w.close() - them_d = json.loads(them_bytes.decode("utf-8")) - if "error" in them_d: - print("ERROR: " + them_d["error"], file=sys.stderr) - return 1 - print(them_d["message"]) diff --git a/src/wormhole/scripts/cmd_send_file.py b/src/wormhole/scripts/cmd_send.py similarity index 54% rename from src/wormhole/scripts/cmd_send_file.py rename to src/wormhole/scripts/cmd_send.py index cd56ccd..1b5aa0e 100644 --- a/src/wormhole/scripts/cmd_send_file.py +++ b/src/wormhole/scripts/cmd_send.py @@ -2,18 +2,38 @@ from __future__ import print_function import os, sys, json, binascii, six from ..errors import handle_server_error -APPID = b"lothar.com/wormhole/file-xfer" +APPID = b"lothar.com/wormhole/text-or-file-xfer" @handle_server_error -def send_file(args): - # we're sending +def send(args): + # we're sending text, or a file from ..blocking.transcribe import Wormhole, WrongPasswordError from ..blocking.transit import TransitSender from .progress import start_progress, update_progress, finish_progress - filename = args.filename - assert os.path.isfile(filename) - transit_sender = TransitSender(args.transit_helper) + if os.path.isfile(args.what): + # we're sending a file + sending_message = False + filesize = os.stat(args.what).st_size + basename = os.path.basename(args.what) + print("Sending %d byte file named '%s'" % (filesize, basename)) + transit_sender = TransitSender(args.transit_helper) + phase1 = { + "file": { + "filename": basename, + "filesize": filesize, + }, + "transit": { + "direct_connection_hints": transit_sender.get_direct_hints(), + "relay_connection_hints": transit_sender.get_relay_hints(), + }, + } + else: + sending_message = True + print("Sending text message (%d bytes)" % len(args.what)) + phase1 = { + "message": args.what, + } w = Wormhole(APPID, args.relay_url) if args.zeromode: @@ -24,15 +44,15 @@ def send_file(args): code = args.code else: code = w.get_code(args.code_length) - other_cmd = "wormhole receive-file" + other_cmd = "wormhole receive" if args.verify: - other_cmd = "wormhole --verify receive-file" + other_cmd = "wormhole --verify receive" if args.zeromode: other_cmd += " -0" print("On the other computer, please run: %s" % other_cmd) if not args.zeromode: - print("Wormhole code is '%s'" % code) - print() + print("Wormhole code is: %s" % code) + print("") if args.verify: verifier = binascii.hexlify(w.get_verifier()).decode("ascii") @@ -49,29 +69,38 @@ def send_file(args): w.close() return 1 - 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") - w.send_data(data) + my_phase1_bytes = json.dumps(phase1).encode("utf-8") + w.send_data(my_phase1_bytes) try: - them_bytes = w.get_data() + them_phase1_bytes = w.get_data() except WrongPasswordError as e: print("ERROR: " + e.explain(), file=sys.stderr) + w.close() + return 1 + them_phase1 = json.loads(them_phase1_bytes.decode("utf-8")) + + if sending_message: + if them_phase1["message_ack"] == "ok": + print("text message sent") + w.close() + return 0 + print("error sending text: %r" % (them_phase1,)) + w.close() + return 1 + + if "error" in them_phase1: + print("remote error: %s" % them_phase1["error"]) + print("transfer abandoned") + w.close() + return 1 + if them_phase1.get("file_ack") != "ok": + print("ambiguous response from remote: %s" % (them_phase1,)) + print("transfer abandoned") + w.close() return 1 w.close() - them_d = json.loads(them_bytes.decode("utf-8")) - #print("them: %r" % (them_d,)) - - tdata = them_d["transit"] + tdata = them_phase1["transit"] transit_key = w.derive_key(APPID+b"/transit-key") transit_sender.set_transit_key(transit_key) transit_sender.add_their_direct_hints(tdata["direct_connection_hints"]) @@ -81,7 +110,7 @@ def send_file(args): print("Sending (%s).." % transit_sender.describe()) CHUNKSIZE = 64*1024 - with open(filename, "rb") as f: + with open(args.what, "rb") as f: sent = 0 next_update = start_progress(filesize) while sent < filesize: @@ -96,6 +125,6 @@ def send_file(args): if ack == b"ok\n": print("Confirmation received. Transfer complete.") return 0 - else: - print("Transfer failed (remote says: %r)" % ack) - return 1 + 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 deleted file mode 100644 index 1fff009..0000000 --- a/src/wormhole/scripts/cmd_send_text.py +++ /dev/null @@ -1,61 +0,0 @@ -from __future__ import print_function -import sys, json, binascii, six -from ..errors import handle_server_error - -APPID = b"lothar.com/wormhole/text-xfer" - -@handle_server_error -def send_text(args): - # we're sending - from ..blocking.transcribe import Wormhole, WrongPasswordError - - w = Wormhole(APPID, args.relay_url) - if args.zeromode: - assert not args.code - args.code = "0-" - if args.code: - w.set_code(args.code) - code = args.code - else: - code = w.get_code(args.code_length) - other_cmd = "wormhole receive-text" - if args.verify: - other_cmd = "wormhole --verify receive-text" - if args.zeromode: - other_cmd += " -0" - print("On the other computer, please run: %s" % other_cmd) - if not args.zeromode: - print("Wormhole code is: %s" % code) - print("") - - if args.verify: - verifier = binascii.hexlify(w.get_verifier()).decode("ascii") - while True: - ok = six.moves.input("Verifier %s. ok? (yes/no): " % verifier) - if ok.lower() == "yes": - break - if ok.lower() == "no": - print("verification rejected, abandoning transfer", - file=sys.stderr) - reject_data = json.dumps({"error": "verification rejected", - }).encode("utf-8") - w.send_data(reject_data) - w.close() - return 1 - - message = args.text - data = json.dumps({"message": message, - }).encode("utf-8") - w.send_data(data) - try: - them_bytes = w.get_data() - except WrongPasswordError as e: - print("ERROR: " + e.explain(), file=sys.stderr) - return 1 - w.close() - them_d = json.loads(them_bytes.decode("utf-8")) - if them_d["message"] == "ok": - print("text sent") - else: - print("error sending text: %r" % (them_d,)) - diff --git a/src/wormhole/scripts/runner.py b/src/wormhole/scripts/runner.py index c395dea..77b8df0 100644 --- a/src/wormhole/scripts/runner.py +++ b/src/wormhole/scripts/runner.py @@ -3,7 +3,7 @@ import sys, argparse from textwrap import dedent from .. import public_relay from .. import __version__ -from . import cmd_send_text, cmd_receive_text, cmd_send_file, cmd_receive_file +from . import cmd_send, cmd_receive from ..servers import cmd_server parser = argparse.ArgumentParser( @@ -62,58 +62,38 @@ sp_restart.add_argument("--advertise-version", metavar="VERSION", help="version to recommend to clients") sp_restart.set_defaults(func=cmd_server.restart_server) -# CLI: send-text -p = subparsers.add_parser("send-text", description="Send a text mesasge", - usage="wormhole send-text TEXT") +# CLI: send +p = subparsers.add_parser("send", + description="Send text message or file", + usage="wormhole send TEXT|FILENAME") p.add_argument("--code", metavar="CODE", help="human-generated code phrase") p.add_argument("-0", dest="zeromode", action="store_true", help="enable no-code anything-goes mode") -p.add_argument("text", metavar="TEXT", help="the message to send (a string)") -p.set_defaults(func=cmd_send_text.send_text) +p.add_argument("what", metavar="TEXT|FILENAME", + help="the message to send (a string), or a filename") +p.set_defaults(func=cmd_send.send) -# CLI: receive-text -p = subparsers.add_parser("receive-text", description="Receive a text message", - usage="wormhole receive-text [CODE]") +# CLI: receive +p = subparsers.add_parser("receive", + description="Receive a text message or file", + usage="wormhole receive [CODE]") p.add_argument("-0", dest="zeromode", action="store_true", help="enable no-code anything-goes mode") -p.add_argument("code", nargs="?", default=None, metavar="[CODE]", - help=dedent("""\ - The magic-wormhole code, from the sender. If omitted, the - program will ask for it, using tab-completion."""), - ) -p.set_defaults(func=cmd_receive_text.receive_text) - -# CLI: send-file -p = subparsers.add_parser("send-file", description="Send a file", - usage="wormhole send-file FILENAME") -p.add_argument("--code", metavar="CODE", help="human-generated code phrase") -p.add_argument("-0", dest="zeromode", action="store_true", - help="enable no-code anything-goes mode") -p.add_argument("filename", metavar="FILENAME", help="The file to be sent") -p.set_defaults(func=cmd_send_file.send_file) - -# CLI: receive-file -p = subparsers.add_parser("receive-file", description="Receive a file", - usage="wormhole receive-file [-o FILENAME] [CODE]") +p.add_argument("-t", "--only-text", dest="only_text", action="store_true", + help="refuse file transfers, only accept text transfers") +p.add_argument("--accept-file", dest="accept_file", action="store_true", + help="accept file transfer with prompting") p.add_argument("-o", "--output-file", default=None, metavar="FILENAME", help=dedent("""\ The file to create, overriding the filename suggested by the - sender"""), + sender."""), ) -p.add_argument("--overwrite", action="store_true", - help=dedent("""\ - Allow the output file to be overwritten. By default, if the - output file already exists, the program will refuse to - overwrite it."""), - ) -p.add_argument("-0", dest="zeromode", action="store_true", - help="enable no-code anything-goes mode") p.add_argument("code", nargs="?", default=None, metavar="[CODE]", help=dedent("""\ The magic-wormhole code, from the sender. If omitted, the program will ask for it, using tab-completion."""), ) -p.set_defaults(func=cmd_receive_file.receive_file) +p.set_defaults(func=cmd_receive.receive) diff --git a/src/wormhole/test/test_scripts.py b/src/wormhole/test/test_scripts.py index 8b004d2..9f8e3f5 100644 --- a/src/wormhole/test/test_scripts.py +++ b/src/wormhole/test/test_scripts.py @@ -85,13 +85,13 @@ class Scripts(ServerBase, ScriptsBase, unittest.TestCase): code = "1-abc" message = "test message" send_args = server_args + [ - "send-text", + "send", "--code", code, message, ] d1 = getProcessOutputAndValue(wormhole, send_args) receive_args = server_args + [ - "receive-text", + "receive", code, ] d2 = getProcessOutputAndValue(wormhole, receive_args) @@ -100,10 +100,11 @@ class Scripts(ServerBase, ScriptsBase, unittest.TestCase): out = out.decode("utf-8") err = err.decode("utf-8") self.failUnlessEqual(out, + "Sending text message (%d bytes)\n" "On the other computer, please run: " - "wormhole receive-text\n" + "wormhole receive\n" "Wormhole code is: %s\n\n" - "text sent\n" % code + "text message sent\n" % (len(message), code) ) self.failUnlessEqual(err, "") self.failUnlessEqual(rc, 0) @@ -121,26 +122,27 @@ class Scripts(ServerBase, ScriptsBase, unittest.TestCase): def test_send_file_pre_generated_code(self): code = "1-abc" + filename = "testfile" message = "test message" send_dir = self.mktemp() os.mkdir(send_dir) - with open(os.path.join(send_dir, "testfile"), "w") as f: + with open(os.path.join(send_dir, filename), "w") as f: f.write(message) wormhole = self.find_executable() server_args = ["--relay-url", self.relayurl] send_args = server_args + [ - "send-file", + "send", "--code", code, - "testfile", + filename, ] d1 = getProcessOutputAndValue(wormhole, send_args, path=send_dir) receive_dir = self.mktemp() os.mkdir(receive_dir) receive_args = server_args + [ - "receive-file", + "receive", "--accept-file", code, ] d2 = getProcessOutputAndValue(wormhole, receive_args, path=receive_dir) @@ -148,9 +150,11 @@ class Scripts(ServerBase, ScriptsBase, unittest.TestCase): out, err, rc = res out = out.decode("utf-8") err = err.decode("utf-8") + self.failUnlessIn("Sending %d byte file named '%s'\n" % + (len(message), filename), out) self.failUnlessIn("On the other computer, please run: " - "wormhole receive-file\n" - "Wormhole code is '%s'\n\n" % code, + "wormhole receive\n" + "Wormhole code is: %s\n\n" % code, out) self.failUnlessIn("File sent.. waiting for confirmation\n" "Confirmation received. Transfer complete.\n", @@ -163,12 +167,12 @@ class Scripts(ServerBase, ScriptsBase, unittest.TestCase): out, err, rc = res out = out.decode("utf-8") err = err.decode("utf-8") - self.failUnlessIn("Receiving %d bytes for 'testfile'" % len(message), - out) + self.failUnlessIn("Receiving %d bytes for '%s'" % + (len(message), filename), out) self.failUnlessIn("Received file written to ", out) self.failUnlessEqual(err, "") self.failUnlessEqual(rc, 0) - fn = os.path.join(receive_dir, "testfile") + fn = os.path.join(receive_dir, filename) self.failUnless(os.path.exists(fn)) with open(fn, "r") as f: self.failUnlessEqual(f.read(), message)