From b477354235af6577749175b885339cf58a8ef2a1 Mon Sep 17 00:00:00 2001 From: Mark Qvist Date: Mon, 18 Sep 2023 22:22:44 +0200 Subject: [PATCH] Added fetch mode to rncp --- RNS/Utilities/rncp.py | 281 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 274 insertions(+), 7 deletions(-) diff --git a/RNS/Utilities/rncp.py b/RNS/Utilities/rncp.py index 30deda2..f1614f1 100644 --- a/RNS/Utilities/rncp.py +++ b/RNS/Utilities/rncp.py @@ -35,8 +35,9 @@ APP_NAME = "rncp" allow_all = False allowed_identity_hashes = [] -def receive(configdir, verbosity = 0, quietness = 0, allowed = [], display_identity = False, limit = None, disable_auth = None, announce = False): +def listen(configdir, verbosity = 0, quietness = 0, allowed = [], display_identity = False, limit = None, disable_auth = None, announce = False): global allow_all, allowed_identity_hashes + from tempfile import TemporaryFile identity = None if announce < 0: announce = False @@ -86,7 +87,7 @@ def receive(configdir, verbosity = 0, quietness = 0, allowed = [], display_ident allowed = ali else: allowed.extend(ali) - if al == 1: + if len(ali) == 1: ms = "y" else: ms = "ies" @@ -113,7 +114,44 @@ def receive(configdir, verbosity = 0, quietness = 0, allowed = [], display_ident if len(allowed_identity_hashes) < 1 and not disable_auth: print("Warning: No allowed identities configured, rncp will not accept any files!") - destination.set_link_established_callback(receive_link_established) + def fetch_request(path, data, request_id, link_id, remote_identity, requested_at): + target_link = None + for link in RNS.Transport.active_links: + if link.link_id == link_id: + target_link = link + + file_path = os.path.expanduser(data) + if not os.path.isfile(file_path): + RNS.log("Client-requested file not found: "+str(file_path), RNS.LOG_VERBOSE) + return False + else: + if target_link != None: + RNS.log("Sending file "+str(file_path)+" to client", RNS.LOG_VERBOSE) + + temp_file = TemporaryFile() + real_file = open(file_path, "rb") + filename_bytes = os.path.basename(file_path).encode("utf-8") + filename_len = len(filename_bytes) + + if filename_len > 0xFFFF: + print("Filename exceeds max size, cannot send") + exit(1) + else: + print("Preparing file...", end=" ") + + temp_file.write(filename_len.to_bytes(2, "big")) + temp_file.write(filename_bytes) + temp_file.write(real_file.read()) + temp_file.seek(0) + + fetch_resource = RNS.Resource(temp_file, target_link) + return True + else: + return None + + + destination.set_link_established_callback(client_link_established) + destination.register_request_handler("fetch_file", response_generator=fetch_request, allow=RNS.Destination.ALLOW_LIST, allowed_list=allowed_identity_hashes) print("rncp listening on "+RNS.prettyhexrep(destination.hash)) if announce >= 0: @@ -129,7 +167,7 @@ def receive(configdir, verbosity = 0, quietness = 0, allowed = [], display_ident while True: time.sleep(1) -def receive_link_established(link): +def client_link_established(link): RNS.log("Incoming link established", RNS.LOG_VERBOSE) link.set_remote_identified_callback(receive_sender_identified) link.set_resource_strategy(RNS.Link.ACCEPT_APP) @@ -223,6 +261,220 @@ def sender_progress(resource): resource_done = True link = None +def fetch(configdir, verbosity = 0, quietness = 0, destination = None, file = None, timeout = RNS.Transport.PATH_REQUEST_TIMEOUT, silent=False): + global current_resource, resource_done, link, speed + targetloglevel = 3+verbosity-quietness + + try: + dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2 + if len(destination) != dest_len: + raise ValueError("Allowed destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2)) + try: + destination_hash = bytes.fromhex(destination) + except Exception as e: + raise ValueError("Invalid destination entered. Check your input.") + except Exception as e: + print(str(e)) + exit(1) + + reticulum = RNS.Reticulum(configdir=configdir, loglevel=targetloglevel) + + identity_path = RNS.Reticulum.identitypath+"/"+APP_NAME + if os.path.isfile(identity_path): + identity = RNS.Identity.from_file(identity_path) + if identity == None: + RNS.log("Could not load identity for rncp. The identity file at \""+str(identity_path)+"\" may be corrupt or unreadable.", RNS.LOG_ERROR) + exit(2) + else: + identity = None + + if identity == None: + RNS.log("No valid saved identity found, creating new...", RNS.LOG_INFO) + identity = RNS.Identity() + identity.to_file(identity_path) + + if not RNS.Transport.has_path(destination_hash): + RNS.Transport.request_path(destination_hash) + if silent: + print("Path to "+RNS.prettyhexrep(destination_hash)+" requested") + else: + print("Path to "+RNS.prettyhexrep(destination_hash)+" requested ", end=" ") + sys.stdout.flush() + + i = 0 + syms = "⢄⢂⢁⡁⡈⡐⡠" + estab_timeout = time.time()+timeout + while not RNS.Transport.has_path(destination_hash) and time.time() < estab_timeout: + if not silent: + time.sleep(0.1) + print(("\b\b"+syms[i]+" "), end="") + sys.stdout.flush() + i = (i+1)%len(syms) + + if not RNS.Transport.has_path(destination_hash): + if silent: + print("Path not found") + else: + print("\r \rPath not found") + exit(1) + else: + if silent: + print("Establishing link with "+RNS.prettyhexrep(destination_hash)) + else: + print("\r \rEstablishing link with "+RNS.prettyhexrep(destination_hash)+" ", end=" ") + + listener_identity = RNS.Identity.recall(destination_hash) + listener_destination = RNS.Destination( + listener_identity, + RNS.Destination.OUT, + RNS.Destination.SINGLE, + APP_NAME, + "receive" + ) + + link = RNS.Link(listener_destination) + while link.status != RNS.Link.ACTIVE and time.time() < estab_timeout: + if not silent: + time.sleep(0.1) + print(("\b\b"+syms[i]+" "), end="") + sys.stdout.flush() + i = (i+1)%len(syms) + + if not RNS.Transport.has_path(destination_hash): + if silent: + print("Could not establish link with "+RNS.prettyhexrep(destination_hash)) + else: + print("\r \rCould not establish link with "+RNS.prettyhexrep(destination_hash)) + exit(1) + else: + if silent: + print("Requesting file from remote...") + else: + print("\r \rRequesting file from remote ", end=" ") + + link.identify(identity) + + request_resolved = False + request_status = "unknown" + resource_resolved = False + resource_status = "unrequested" + current_resource = None + def request_response(request_receipt): + nonlocal request_resolved, request_status + if request_receipt.response == False: + request_status = "not_found" + elif request_receipt.response == None: + request_status = "remote_error" + else: + request_status = "found" + + request_resolved = True + + def request_failed(request_receipt): + nonlocal request_resolved, request_status + request_status = "unknown" + request_resolved = True + + def fetch_resource_started(resource): + nonlocal resource_status + current_resource = resource + current_resource.progress_callback(sender_progress) + resource_status = "started" + + def fetch_resource_concluded(resource): + nonlocal resource_resolved, resource_status + if resource.status == RNS.Resource.COMPLETE: + if resource.total_size > 4: + filename_len = int.from_bytes(resource.data.read(2), "big") + filename = resource.data.read(filename_len).decode("utf-8") + + counter = 0 + saved_filename = filename + while os.path.isfile(saved_filename): + counter += 1 + saved_filename = filename+"."+str(counter) + + file = open(saved_filename, "wb") + file.write(resource.data.read()) + file.close() + resource_status = "completed" + + else: + print("Invalid data received, ignoring resource") + resource_status = "invalid_data" + + else: + print("Resource failed") + resource_status = "failed" + + resource_resolved = True + + link.set_resource_strategy(RNS.Link.ACCEPT_ALL) + link.set_resource_started_callback(fetch_resource_started) + link.set_resource_concluded_callback(fetch_resource_concluded) + link.request("fetch_file", data=file, response_callback=request_response, failed_callback=request_failed) + + syms = "⢄⢂⢁⡁⡈⡐⡠" + while not request_resolved: + if not silent: + time.sleep(0.1) + print(("\b\b"+syms[i]+" "), end="") + sys.stdout.flush() + i = (i+1)%len(syms) + + if request_status == "not_found": + if not silent: print("\r \r", end="") + print("Fetch request failed, the file "+str(file)+" was not found on the remote") + link.teardown() + time.sleep(1) + exit(0) + elif request_status == "remote_error": + if not silent: print("\r \r", end="") + print("Fetch request failed due to an error on the remote system") + link.teardown() + time.sleep(1) + exit(0) + elif request_status == "unknown": + if not silent: print("\r \r", end="") + print("Fetch request failed due to an unknown error") + link.teardown() + time.sleep(1) + exit(0) + elif request_status == "found": + if not silent: print("\r \r", end="") + + while not resource_resolved: + if not silent: + time.sleep(0.1) + if current_resource: + prg = current_resource.get_progress() + percent = round(prg * 100.0, 1) + stat_str = str(percent)+"% - " + size_str(int(prg*current_resource.total_size)) + " of " + size_str(current_resource.total_size) + " - " +size_str(speed, "b")+"ps" + print("\r \rTransferring file "+syms[i]+" "+stat_str, end=" ") + else: + print("\r \rWaiting for transfer to start "+syms[i]+" ", end=" ") + sys.stdout.flush() + i = (i+1)%len(syms) + + if current_resource.status != RNS.Resource.COMPLETE: + if silent: + print("The transfer failed") + else: + print("\r \rThe transfer failed") + exit(1) + else: + if silent: + print(str(file_path)+" copied to "+RNS.prettyhexrep(destination_hash)) + else: + print("\r \r"+str(file)+" fetched from "+RNS.prettyhexrep(destination_hash)) + link.teardown() + time.sleep(0.25) + exit(0) + + link.teardown() + exit(0) + + def send(configdir, verbosity = 0, quietness = 0, destination = None, file = None, timeout = RNS.Transport.PATH_REQUEST_TIMEOUT, silent=False): global current_resource, resource_done, link, speed from tempfile import TemporaryFile @@ -399,9 +651,8 @@ def main(): parser.add_argument('-v', '--verbose', action='count', default=0, help="increase verbosity") parser.add_argument('-q', '--quiet', action='count', default=0, help="decrease verbosity") parser.add_argument("-S", '--silent', action='store_true', default=False, help="disable transfer progress output") - parser.add_argument('-p', '--print-identity', action='store_true', default=False, help="print identity and destination info and exit") parser.add_argument("-l", '--listen', action='store_true', default=False, help="listen for incoming transfer requests") - #parser.add_argument("-f", '--fetch', action='store_true', default=False, help="fetch file from remote listener") + parser.add_argument("-f", '--fetch', action='store_true', default=False, help="fetch file from remote listener") parser.add_argument("-b", action='store', metavar="seconds", default=-1, help="announce interval, 0 to only announce at startup", type=int) parser.add_argument('-a', metavar="allowed_hash", dest="allowed", action='append', help="accept from this identity", type=str) parser.add_argument('-n', '--no-auth', action='store_true', default=False, help="accept files from anyone") @@ -413,7 +664,7 @@ def main(): args = parser.parse_args() if args.listen or args.print_identity: - receive( + listen( configdir = args.config, verbosity=args.verbose, quietness=args.quiet, @@ -424,6 +675,22 @@ def main(): announce=args.b, ) + elif args.fetch: + if args.destination != None and args.file != None: + fetch( + configdir = args.config, + verbosity = args.verbose, + quietness = args.quiet, + destination = args.destination, + file = args.file, + timeout = args.w, + silent = args.silent, + ) + else: + print("") + parser.print_help() + print("") + elif args.destination != None and args.file != None: send( configdir = args.config,