rncp.py
1 #!/usr/bin/env python3 2 3 # Reticulum License 4 # 5 # Copyright (c) 2016-2025 Mark Qvist 6 # 7 # Permission is hereby granted, free of charge, to any person obtaining a copy 8 # of this software and associated documentation files (the "Software"), to deal 9 # in the Software without restriction, including without limitation the rights 10 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 # copies of the Software, and to permit persons to whom the Software is 12 # furnished to do so, subject to the following conditions: 13 # 14 # - The Software shall not be used in any kind of system which includes amongst 15 # its functions the ability to purposefully do harm to human beings. 16 # 17 # - The Software shall not be used, directly or indirectly, in the creation of 18 # an artificial intelligence, machine learning or language model training 19 # dataset, including but not limited to any use that contributes to the 20 # training or development of such a model or algorithm. 21 # 22 # - The above copyright notice and this permission notice shall be included in 23 # all copies or substantial portions of the Software. 24 # 25 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 26 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 27 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 28 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 29 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 30 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 31 # SOFTWARE. 32 33 import RNS 34 import argparse 35 import threading 36 import shutil 37 import time 38 import sys 39 import os 40 41 from RNS._version import __version__ 42 43 APP_NAME = "rncp" 44 allow_all = False 45 allow_fetch = False 46 allow_overwrite_on_receive = False 47 fetch_auto_compress = True 48 fetch_jail = None 49 save_path = None 50 show_phy_rates = False 51 allowed_identity_hashes = [] 52 identity = None 53 54 def prepare_identity(identity_path): 55 global identity 56 if identity_path == None: 57 identity_path = RNS.Reticulum.identitypath+"/"+APP_NAME 58 59 if os.path.isfile(identity_path): 60 identity = RNS.Identity.from_file(identity_path) 61 if identity == None: 62 RNS.log(f"Could not load identity for rncp. The identity file at \"{identity_path}\" may be corrupt or unreadable.", RNS.LOG_ERROR) 63 RNS.exit(2) 64 65 if identity == None: 66 RNS.log("No valid saved identity found, creating new...", RNS.LOG_INFO) 67 identity = RNS.Identity() 68 identity.to_file(identity_path) 69 70 REQ_FETCH_NOT_ALLOWED = 0xF0 71 72 es = " " 73 erase_str = "\33[2K\r" 74 75 def listen(configdir, identitypath = None, verbosity = 0, quietness = 0, allowed = [], display_identity = False, 76 limit = None, disable_auth = None, fetch_allowed = False, no_compress=False, 77 jail = None, save = None, announce = False, allow_overwrite=False): 78 79 global allow_all, allow_fetch, allowed_identity_hashes, fetch_jail, save_path, identity 80 global fetch_auto_compress, allow_overwrite_on_receive 81 82 allow_fetch = fetch_allowed 83 fetch_auto_compress = not no_compress 84 allow_overwrite_on_receive = allow_overwrite 85 identity = None 86 if announce < 0: 87 announce = False 88 89 targetloglevel = 3+verbosity-quietness 90 reticulum = RNS.Reticulum(configdir=configdir, loglevel=targetloglevel) 91 92 if jail != None: 93 fetch_jail = os.path.abspath(os.path.expanduser(jail)) 94 RNS.log("Restricting fetch requests to paths under \""+fetch_jail+"\"", RNS.LOG_VERBOSE) 95 96 if save != None: 97 sp = os.path.abspath(os.path.expanduser(save)) 98 if os.path.isdir(sp): 99 if os.access(sp, os.W_OK): 100 save_path = sp 101 else: 102 RNS.log("Output directory not writable", RNS.LOG_ERROR) 103 RNS.exit(4) 104 else: 105 RNS.log("Output directory not found", RNS.LOG_ERROR) 106 RNS.exit(3) 107 108 RNS.log("Saving received files in \""+save_path+"\"", RNS.LOG_VERBOSE) 109 110 prepare_identity(identitypath) 111 112 destination = RNS.Destination(identity, RNS.Destination.IN, RNS.Destination.SINGLE, APP_NAME, "receive") 113 114 if display_identity: 115 print("Identity : "+str(identity)) 116 print("Listening on : "+RNS.prettyhexrep(destination.hash)) 117 RNS.exit(0) 118 119 if disable_auth: 120 allow_all = True 121 else: 122 dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2 123 try: 124 allowed_file_name = "allowed_identities" 125 allowed_file = None 126 if os.path.isfile(os.path.expanduser("/etc/rncp/"+allowed_file_name)): 127 allowed_file = os.path.expanduser("/etc/rncp/"+allowed_file_name) 128 elif os.path.isfile(os.path.expanduser("~/.config/rncp/"+allowed_file_name)): 129 allowed_file = os.path.expanduser("~/.config/rncp/"+allowed_file_name) 130 elif os.path.isfile(os.path.expanduser("~/.rncp/"+allowed_file_name)): 131 allowed_file = os.path.expanduser("~/.rncp/"+allowed_file_name) 132 if allowed_file != None: 133 af = open(allowed_file, "r") 134 al = af.read().replace("\r", "").split("\n") 135 ali = [] 136 for a in al: 137 if len(a) == dest_len: 138 ali.append(a) 139 140 if len(ali) > 0: 141 if not allowed: 142 allowed = ali 143 else: 144 allowed.extend(ali) 145 if len(ali) == 1: 146 ms = "y" 147 else: 148 ms = "ies" 149 150 RNS.log("Loaded "+str(len(ali))+" allowed identit"+ms+" from "+str(allowed_file), RNS.LOG_VERBOSE) 151 152 except Exception as e: 153 RNS.log("Error while parsing allowed_identities file. The contained exception was: "+str(e), RNS.LOG_ERROR) 154 155 if allowed != None: 156 for a in allowed: 157 try: 158 if len(a) != dest_len: 159 raise ValueError("Allowed destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2)) 160 try: 161 destination_hash = bytes.fromhex(a) 162 allowed_identity_hashes.append(destination_hash) 163 except Exception as e: 164 raise ValueError("Invalid destination entered. Check your input.") 165 except Exception as e: 166 print(str(e)) 167 RNS.exit(1) 168 169 if len(allowed_identity_hashes) < 1 and not disable_auth: 170 print("Warning: No allowed identities configured, rncp will not accept any files!") 171 172 def fetch_request(path, data, request_id, link_id, remote_identity, requested_at): 173 global allow_fetch, fetch_jail, fetch_auto_compress 174 if not allow_fetch: 175 return REQ_FETCH_NOT_ALLOWED 176 177 if fetch_jail: 178 if data.startswith(fetch_jail+"/"): 179 data = data.replace(fetch_jail+"/", "") 180 file_path = os.path.abspath(os.path.expanduser(f"{fetch_jail}/{data}")) 181 if not file_path.startswith(fetch_jail+"/"): 182 RNS.log(f"Disallowing fetch request for {file_path} outside of fetch jail {fetch_jail}", RNS.LOG_WARNING) 183 return REQ_FETCH_NOT_ALLOWED 184 else: 185 file_path = os.path.abspath(os.path.expanduser(f"{data}")) 186 187 target_link = None 188 for link in RNS.Transport.active_links: 189 if link.link_id == link_id: 190 target_link = link 191 192 if not os.path.isfile(file_path): 193 RNS.log("Client-requested file not found: "+str(file_path), RNS.LOG_VERBOSE) 194 return False 195 else: 196 if target_link != None: 197 RNS.log("Sending file "+str(file_path)+" to client", RNS.LOG_VERBOSE) 198 199 try: 200 metadata = {"name": os.path.basename(file_path).encode("utf-8") } 201 fetch_resource = RNS.Resource(open(file_path, "rb"), target_link, metadata=metadata, auto_compress=fetch_auto_compress) 202 return True 203 204 except Exception as e: 205 RNS.log(f"Could not send file to client. The contained exception was: {e}", RNS.LOG_ERROR) 206 return False 207 208 else: 209 return None 210 211 212 destination.set_link_established_callback(client_link_established) 213 if allow_fetch: 214 if allow_all: 215 RNS.log("Allowing unauthenticated fetch requests", RNS.LOG_WARNING) 216 destination.register_request_handler("fetch_file", response_generator=fetch_request, allow=RNS.Destination.ALLOW_ALL) 217 else: 218 destination.register_request_handler("fetch_file", response_generator=fetch_request, allow=RNS.Destination.ALLOW_LIST, allowed_list=allowed_identity_hashes) 219 220 print("rncp listening on "+RNS.prettyhexrep(destination.hash)) 221 222 if announce >= 0: 223 def job(): 224 destination.announce() 225 if announce > 0: 226 while True: 227 time.sleep(announce) 228 destination.announce() 229 230 threading.Thread(target=job, daemon=True).start() 231 232 while True: time.sleep(1) 233 234 def client_link_established(link): 235 RNS.log("Incoming link established", RNS.LOG_VERBOSE) 236 link.set_remote_identified_callback(receive_sender_identified) 237 link.set_resource_strategy(RNS.Link.ACCEPT_APP) 238 link.set_resource_callback(receive_resource_callback) 239 link.set_resource_started_callback(receive_resource_started) 240 link.set_resource_concluded_callback(receive_resource_concluded) 241 242 def receive_sender_identified(link, identity): 243 global allow_all 244 245 if identity.hash in allowed_identity_hashes: 246 RNS.log("Authenticated sender", RNS.LOG_VERBOSE) 247 else: 248 if not allow_all: 249 RNS.log("Sender not allowed, tearing down link", RNS.LOG_VERBOSE) 250 link.teardown() 251 else: 252 pass 253 254 def receive_resource_callback(resource): 255 global allow_all 256 257 sender_identity = resource.link.get_remote_identity() 258 259 if sender_identity != None: 260 if sender_identity.hash in allowed_identity_hashes: 261 return True 262 263 if allow_all: 264 return True 265 266 return False 267 268 def receive_resource_started(resource): 269 if resource.link.get_remote_identity(): 270 id_str = " from "+RNS.prettyhexrep(resource.link.get_remote_identity().hash) 271 else: 272 id_str = "" 273 274 print("Starting resource transfer "+RNS.prettyhexrep(resource.hash)+id_str) 275 276 def receive_resource_concluded(resource): 277 global save_path, allow_overwrite_on_receive 278 if resource.status == RNS.Resource.COMPLETE: 279 print(str(resource)+" completed") 280 281 if resource.metadata == None: 282 print("Invalid data received, ignoring resource") 283 return 284 285 else: 286 try: 287 filename = os.path.basename(resource.metadata["name"].decode("utf-8")) 288 counter = 0 289 if save_path: 290 saved_filename = os.path.abspath(os.path.expanduser(save_path+"/"+filename)) 291 if not saved_filename.startswith(save_path+"/"): 292 RNS.log(f"Invalid save path {saved_filename}, ignoring", RNS.LOG_ERROR) 293 return 294 else: 295 saved_filename = filename 296 297 full_save_path = saved_filename 298 if allow_overwrite_on_receive: 299 if os.path.isfile(full_save_path): 300 try: os.unlink(full_save_path) 301 except Exception as e: 302 RNS.log(f"Could not overwrite existing file {full_save_path}, renaming instead", RNS.LOG_ERROR) 303 304 while os.path.isfile(full_save_path): 305 counter += 1 306 full_save_path = saved_filename+"."+str(counter) 307 308 shutil.move(resource.data.name, full_save_path) 309 310 except Exception as e: 311 RNS.log(f"An error occurred while saving received resource: {e}", RNS.LOG_ERROR) 312 return 313 314 else: 315 print("Resource failed") 316 317 resource_done = False 318 current_resource = None 319 stats = [] 320 speed = 0.0 321 phy_speed = 0.0 322 phy_got_total = 0 323 def sender_progress(resource): 324 stats_max = 32 325 global current_resource, stats, speed, phy_speed, phy_got_total, resource_done 326 current_resource = resource 327 328 now = time.time() 329 got = current_resource.get_progress()*current_resource.get_data_size() 330 phy_got = current_resource.get_segment_progress()*current_resource.get_transfer_size() 331 332 entry = [now, got, phy_got] 333 stats.append(entry) 334 335 while len(stats) > stats_max: 336 stats.pop(0) 337 338 span = now - stats[0][0] 339 if span == 0: 340 speed = 0 341 phy_speed = 0 342 343 else: 344 diff = got - stats[0][1] 345 speed = diff/span 346 347 phy_diff = phy_got - stats[0][2] 348 if phy_diff > 0: 349 phy_speed = phy_diff/span 350 # phy_got_total += phy_diff 351 352 if resource.status < RNS.Resource.COMPLETE: 353 resource_done = False 354 else: 355 resource_done = True 356 357 link = None 358 def fetch(configdir, identitypath = None, verbosity = 0, quietness = 0, destination = None, file = None, timeout = RNS.Transport.PATH_REQUEST_TIMEOUT, silent=False, phy_rates=False, save=None, allow_overwrite=False): 359 global current_resource, resource_done, link, speed, show_phy_rates, save_path, allow_overwrite_on_receive, identity 360 targetloglevel = 3+verbosity-quietness 361 show_phy_rates = phy_rates 362 allow_overwrite_on_receive = allow_overwrite 363 364 if save: 365 sp = os.path.abspath(os.path.expanduser(save)) 366 if os.path.isdir(sp): 367 if os.access(sp, os.W_OK): 368 save_path = sp 369 else: 370 RNS.log("Output directory not writable", RNS.LOG_ERROR) 371 RNS.exit(4) 372 else: 373 RNS.log("Output directory not found", RNS.LOG_ERROR) 374 RNS.exit(3) 375 376 try: 377 dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2 378 if len(destination) != dest_len: 379 raise ValueError("Allowed destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2)) 380 try: 381 destination_hash = bytes.fromhex(destination) 382 except Exception as e: 383 raise ValueError("Invalid destination entered. Check your input.") 384 except Exception as e: 385 print(str(e)) 386 RNS.exit(1) 387 388 reticulum = RNS.Reticulum(configdir=configdir, loglevel=targetloglevel) 389 390 if identity == None: 391 prepare_identity(identitypath) 392 393 if not RNS.Transport.has_path(destination_hash): 394 RNS.Transport.request_path(destination_hash) 395 if silent: 396 print("Path to "+RNS.prettyhexrep(destination_hash)+" requested") 397 else: 398 print("Path to "+RNS.prettyhexrep(destination_hash)+" requested ", end=es) 399 sys.stdout.flush() 400 401 i = 0 402 syms = "⢄⢂⢁⡁⡈⡐⡠" 403 estab_timeout = time.time()+timeout 404 while not RNS.Transport.has_path(destination_hash) and time.time() < estab_timeout: 405 if not silent: 406 time.sleep(0.1) 407 print(("\b\b"+syms[i]+" "), end="") 408 sys.stdout.flush() 409 i = (i+1)%len(syms) 410 411 if not RNS.Transport.has_path(destination_hash): 412 if silent: 413 print("Path not found") 414 else: 415 print(f"{erase_str}Path not found") 416 RNS.exit(1) 417 else: 418 if silent: 419 print("Establishing link with "+RNS.prettyhexrep(destination_hash)) 420 else: 421 print(f"{erase_str}Establishing link with "+RNS.prettyhexrep(destination_hash)+" ", end=es) 422 423 listener_identity = RNS.Identity.recall(destination_hash) 424 listener_destination = RNS.Destination( 425 listener_identity, 426 RNS.Destination.OUT, 427 RNS.Destination.SINGLE, 428 APP_NAME, 429 "receive" 430 ) 431 432 link = RNS.Link(listener_destination) 433 while link.status != RNS.Link.ACTIVE and time.time() < estab_timeout: 434 if not silent: 435 time.sleep(0.1) 436 print(("\b\b"+syms[i]+" "), end="") 437 sys.stdout.flush() 438 i = (i+1)%len(syms) 439 440 if not RNS.Transport.has_path(destination_hash): 441 if silent: 442 print("Could not establish link with "+RNS.prettyhexrep(destination_hash)) 443 else: 444 print(f"{erase_str}Could not establish link with "+RNS.prettyhexrep(destination_hash)) 445 RNS.exit(1) 446 else: 447 if silent: 448 print("Requesting file from remote...") 449 else: 450 print(f"{erase_str}Requesting file from remote ", end=es) 451 452 link.identify(identity) 453 454 request_resolved = False 455 request_status = "unknown" 456 resource_resolved = False 457 resource_status = "unrequested" 458 current_resource = None 459 current_transfer_started = None 460 def request_response(request_receipt): 461 nonlocal request_resolved, request_status 462 if request_receipt.response == False: 463 request_status = "not_found" 464 elif request_receipt.response == None: 465 request_status = "remote_error" 466 elif request_receipt.response == REQ_FETCH_NOT_ALLOWED: 467 request_status = "fetch_not_allowed" 468 else: 469 request_status = "found" 470 471 request_resolved = True 472 473 def request_failed(request_receipt): 474 nonlocal request_resolved, request_status 475 request_status = "unknown" 476 request_resolved = True 477 478 def fetch_resource_started(resource): 479 nonlocal resource_status, current_transfer_started 480 current_resource = resource 481 current_resource.progress_callback(sender_progress) 482 resource_status = "started" 483 if not current_transfer_started: current_transfer_started = time.time() 484 485 def fetch_resource_concluded(resource): 486 nonlocal resource_resolved, resource_status 487 global save_path, allow_overwrite_on_receive 488 if resource.status == RNS.Resource.COMPLETE: 489 if resource.metadata == None: 490 print("Invalid data received, ignoring resource") 491 return 492 493 else: 494 try: 495 filename = os.path.basename(resource.metadata["name"].decode("utf-8")) 496 counter = 0 497 if save_path: 498 saved_filename = os.path.abspath(os.path.expanduser(save_path+"/"+filename)) 499 if not saved_filename.startswith(save_path+"/"): 500 print(f"Invalid save path {saved_filename}, ignoring") 501 return 502 else: 503 saved_filename = filename 504 505 full_save_path = saved_filename 506 if allow_overwrite_on_receive: 507 if os.path.isfile(full_save_path): 508 try: os.unlink(full_save_path) 509 except Exception as e: 510 print(f"Could not overwrite existing file {full_save_path}, renaming instead") 511 512 while os.path.isfile(full_save_path): 513 counter += 1 514 full_save_path = saved_filename+"."+str(counter) 515 516 shutil.move(resource.data.name, full_save_path) 517 518 except Exception as e: 519 print(f"An error occurred while saving received resource: {e}") 520 return 521 522 else: 523 print("Resource failed") 524 resource_status = "failed" 525 526 resource_resolved = True 527 528 link.set_resource_strategy(RNS.Link.ACCEPT_ALL) 529 link.set_resource_started_callback(fetch_resource_started) 530 link.set_resource_concluded_callback(fetch_resource_concluded) 531 link.request("fetch_file", data=file, response_callback=request_response, failed_callback=request_failed) 532 533 syms = "⢄⢂⢁⡁⡈⡐⡠" 534 while not request_resolved: 535 if not silent: 536 time.sleep(0.1) 537 print(("\b\b"+syms[i]+" "), end="") 538 sys.stdout.flush() 539 i = (i+1)%len(syms) 540 541 if request_status == "fetch_not_allowed": 542 if not silent: print(f"{erase_str}", end="") 543 print("Fetch request failed, fetching the file "+str(file)+" was not allowed by the remote") 544 link.teardown() 545 time.sleep(0.15) 546 RNS.exit(0) 547 elif request_status == "not_found": 548 if not silent: print(f"{erase_str}", end="") 549 print("Fetch request failed, the file "+str(file)+" was not found on the remote") 550 link.teardown() 551 time.sleep(0.15) 552 RNS.exit(0) 553 elif request_status == "remote_error": 554 if not silent: print(f"{erase_str}", end="") 555 print("Fetch request failed due to an error on the remote system") 556 link.teardown() 557 time.sleep(0.15) 558 RNS.exit(0) 559 elif request_status == "unknown": 560 if not silent: print(f"{erase_str}", end="") 561 print("Fetch request failed due to an unknown error (probably not authorised)") 562 link.teardown() 563 time.sleep(0.15) 564 RNS.exit(0) 565 elif request_status == "found": 566 if not silent: print(f"{erase_str}", end="") 567 568 while not resource_resolved: 569 if not silent: 570 time.sleep(0.1) 571 if current_resource: 572 prg = current_resource.get_progress() 573 percent = round(prg * 100.0, 1) 574 if show_phy_rates: 575 pss = size_str(phy_speed, "b") 576 phy_str = f" ({pss}ps at physical layer)" 577 else: 578 phy_str = "" 579 ps = size_str(int(prg*current_resource.total_size)) 580 ts = size_str(current_resource.total_size) 581 ss = size_str(speed, "b") 582 stat_str = f"{percent}% - {ps} of {ts} - {ss}ps{phy_str}" 583 if prg != 1.0: 584 print(f"{erase_str}Transferring file {syms[i]} {stat_str}", end=es) 585 else: 586 end_time = time.time(); delta_time = end_time - current_transfer_started 587 speed = current_resource.total_size/delta_time; dt_str = RNS.prettytime(delta_time) 588 ss = size_str(speed, "b") 589 stat_str = f"{percent}% - {ps} of {ts} in {dt_str} - {ss}ps{phy_str}" 590 print(f"{erase_str}Transfer complete {stat_str}", end=es) 591 else: 592 print(f"{erase_str}Waiting for transfer to start {syms[i]} ", end=es) 593 sys.stdout.flush() 594 i = (i+1)%len(syms) 595 596 if not current_resource or current_resource.status != RNS.Resource.COMPLETE: 597 if silent: 598 print("The transfer failed") 599 else: 600 print(f"{erase_str}The transfer failed") 601 RNS.exit(1) 602 else: 603 if silent: 604 print(str(file)+" fetched from "+RNS.prettyhexrep(destination_hash)) 605 else: 606 print("\n"+str(file)+" fetched from "+RNS.prettyhexrep(destination_hash)) 607 link.teardown() 608 time.sleep(0.1) 609 RNS.exit(0) 610 611 link.teardown() 612 time.sleep(0.1) 613 RNS.exit(0) 614 615 616 def send(configdir, identitypath = None, verbosity = 0, quietness = 0, destination = None, file = None, timeout = RNS.Transport.PATH_REQUEST_TIMEOUT, silent=False, phy_rates=False, no_compress=False): 617 global current_resource, resource_done, link, speed, show_phy_rates, phy_got_total, phy_speed, identity 618 targetloglevel = 3+verbosity-quietness 619 show_phy_rates = phy_rates 620 621 try: 622 dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2 623 if len(destination) != dest_len: 624 raise ValueError("Allowed destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2)) 625 try: 626 destination_hash = bytes.fromhex(destination) 627 except Exception as e: 628 raise ValueError("Invalid destination entered. Check your input.") 629 except Exception as e: 630 print(str(e)) 631 RNS.exit(1) 632 633 634 file_path = os.path.expanduser(file) 635 if not os.path.isfile(file_path): 636 print("File not found") 637 sys.exit(1) 638 639 metadata = {"name": os.path.basename(file_path).encode("utf-8") } 640 641 print(f"{erase_str}", end="") 642 643 reticulum = RNS.Reticulum(configdir=configdir, loglevel=targetloglevel) 644 645 if identity == None: 646 prepare_identity(identitypath) 647 648 if not RNS.Transport.has_path(destination_hash): 649 RNS.Transport.request_path(destination_hash) 650 if silent: 651 print("Path to "+RNS.prettyhexrep(destination_hash)+" requested") 652 else: 653 print("Path to "+RNS.prettyhexrep(destination_hash)+" requested ", end=es) 654 sys.stdout.flush() 655 656 i = 0 657 syms = "⢄⢂⢁⡁⡈⡐⡠" 658 estab_timeout = time.time()+timeout 659 while not RNS.Transport.has_path(destination_hash) and time.time() < estab_timeout: 660 if not silent: 661 time.sleep(0.1) 662 print(("\b\b"+syms[i]+" "), end="") 663 sys.stdout.flush() 664 i = (i+1)%len(syms) 665 666 if not RNS.Transport.has_path(destination_hash): 667 if silent: 668 print("Path not found") 669 else: 670 print(f"{erase_str}Path not found") 671 RNS.exit(1) 672 else: 673 if silent: 674 print("Establishing link with "+RNS.prettyhexrep(destination_hash)) 675 else: 676 print(f"{erase_str}Establishing link with "+RNS.prettyhexrep(destination_hash)+" ", end=es) 677 678 receiver_identity = RNS.Identity.recall(destination_hash) 679 receiver_destination = RNS.Destination( 680 receiver_identity, 681 RNS.Destination.OUT, 682 RNS.Destination.SINGLE, 683 APP_NAME, 684 "receive" 685 ) 686 687 link = RNS.Link(receiver_destination) 688 while link.status != RNS.Link.ACTIVE and time.time() < estab_timeout: 689 if not silent: 690 time.sleep(0.1) 691 print(("\b\b"+syms[i]+" "), end="") 692 sys.stdout.flush() 693 i = (i+1)%len(syms) 694 695 if time.time() > estab_timeout: 696 if silent: 697 print("Link establishment with "+RNS.prettyhexrep(destination_hash)+" timed out") 698 else: 699 print(f"{erase_str}Link establishment with "+RNS.prettyhexrep(destination_hash)+" timed out") 700 RNS.exit(1) 701 elif not RNS.Transport.has_path(destination_hash): 702 if silent: 703 print("No path found to "+RNS.prettyhexrep(destination_hash)) 704 else: 705 print(f"{erase_str}No path found to "+RNS.prettyhexrep(destination_hash)) 706 RNS.exit(1) 707 else: 708 if silent: 709 print("Advertising file resource...") 710 else: 711 print(f"{erase_str}Advertising file resource ", end=es) 712 713 link.identify(identity) 714 auto_compress = True 715 if no_compress: auto_compress = False 716 try: resource = RNS.Resource(open(file_path, "rb"), link, metadata=metadata, callback = sender_progress, progress_callback = sender_progress, auto_compress = auto_compress) 717 except Exception as e: 718 print(f"Could not start transfer: {e}") 719 RNS.exit(1) 720 721 current_resource = resource 722 723 while resource.status < RNS.Resource.TRANSFERRING: 724 if not silent: 725 time.sleep(0.1) 726 print(("\b\b"+syms[i]+" "), end="") 727 sys.stdout.flush() 728 i = (i+1)%len(syms) 729 730 resource_started_at = time.time() 731 732 if resource.status > RNS.Resource.COMPLETE: 733 if silent: 734 print("File was not accepted by "+RNS.prettyhexrep(destination_hash)) 735 else: 736 print(f"{erase_str}File was not accepted by "+RNS.prettyhexrep(destination_hash)) 737 RNS.exit(1) 738 else: 739 if silent: 740 print("Transferring file...") 741 else: 742 print(f"{erase_str}Transferring file ", end=es) 743 744 def progress_update(i, done=False): 745 time.sleep(0.1) 746 prg = current_resource.get_progress() 747 percent = round(prg * 100.0, 1) 748 if show_phy_rates and not resource_done: 749 pss = size_str(phy_speed, "b") 750 phy_str = f" ({pss}ps at physical layer)" 751 else: 752 phy_str = "" 753 es = " " 754 cs = size_str(int(prg*current_resource.total_size)) 755 ts = size_str(current_resource.total_size) 756 ss = size_str(speed, "b") 757 stat_str = f"{percent}% - {cs} of {ts} - {ss}ps{phy_str}" 758 if not done: 759 print(f"{erase_str}Transferring file "+syms[i]+" "+stat_str, end=es) 760 else: 761 print(f"{erase_str}Transfer complete "+stat_str, end=es) 762 sys.stdout.flush() 763 i = (i+1)%len(syms) 764 return i 765 766 while not resource_done: 767 if not silent: 768 i = progress_update(i) 769 770 resource_concluded_at = time.time() 771 transfer_time = resource_concluded_at - resource_started_at 772 speed = current_resource.total_size/transfer_time 773 # phy_speed = phy_got_total/transfer_time 774 775 if not silent: 776 i = progress_update(i, done=True) 777 778 if current_resource.status != RNS.Resource.COMPLETE: 779 if silent: 780 print("The transfer failed") 781 else: 782 print(f"{erase_str}The transfer failed") 783 RNS.exit(1) 784 else: 785 if silent: 786 print(str(file_path)+" copied to "+RNS.prettyhexrep(destination_hash)) 787 else: 788 print("\n"+str(file_path)+" copied to "+RNS.prettyhexrep(destination_hash)) 789 link.teardown() 790 time.sleep(0.25) 791 RNS.exit(0) 792 793 def main(): 794 try: 795 parser = argparse.ArgumentParser(description="Reticulum File Transfer Utility") 796 parser.add_argument("file", nargs="?", default=None, help="file to be transferred", type=str) 797 parser.add_argument("destination", nargs="?", default=None, help="hexadecimal hash of the receiver", type=str) 798 parser.add_argument("--config", metavar="path", action="store", default=None, help="path to alternative Reticulum config directory", type=str) 799 parser.add_argument('-v', '--verbose', action='count', default=0, help="increase verbosity") 800 parser.add_argument('-q', '--quiet', action='count', default=0, help="decrease verbosity") 801 parser.add_argument("-S", '--silent', action='store_true', default=False, help="disable transfer progress output") 802 parser.add_argument("-l", '--listen', action='store_true', default=False, help="listen for incoming transfer requests") 803 parser.add_argument("-C", '--no-compress', action='store_true', default=False, help="disable automatic compression") 804 parser.add_argument("-F", '--allow-fetch', action='store_true', default=False, help="allow authenticated clients to fetch files") 805 parser.add_argument("-f", '--fetch', action='store_true', default=False, help="fetch file from remote listener instead of sending") 806 parser.add_argument("-j", "--jail", metavar="path", action="store", default=None, help="restrict fetch requests to specified path", type=str) 807 parser.add_argument("-s", "--save", metavar="path", action="store", default=None, help="save received files in specified path", type=str) 808 parser.add_argument('-O', '--overwrite', action='store_true', default=False, help="Allow overwriting received files, instead of adding postfix") 809 parser.add_argument("-b", action='store', metavar="seconds", default=-1, help="announce interval, 0 to only announce at startup", type=int) 810 parser.add_argument('-a', metavar="allowed_hash", dest="allowed", action='append', help="allow this identity (or add in ~/.rncp/allowed_identities)", type=str) 811 parser.add_argument('-n', '--no-auth', action='store_true', default=False, help="accept requests from anyone") 812 parser.add_argument('-p', '--print-identity', action='store_true', default=False, help="print identity and destination info and exit") 813 parser.add_argument('-i', metavar="identity", action='store', dest="identity", default=None, help="path to identity to use", type=str) 814 parser.add_argument("-w", action="store", metavar="seconds", type=float, help="sender timeout before giving up", default=RNS.Transport.PATH_REQUEST_TIMEOUT) 815 parser.add_argument('-P', '--phy-rates', action='store_true', default=False, help="display physical layer transfer rates") 816 # parser.add_argument("--limit", action="store", metavar="files", type=float, help="maximum number of files to accept", default=None) 817 parser.add_argument("--version", action="version", version="rncp {version}".format(version=__version__)) 818 819 args = parser.parse_args() 820 821 if args.listen or args.print_identity: 822 listen( 823 configdir = args.config, 824 identitypath = args.identity, 825 verbosity=args.verbose, 826 quietness=args.quiet, 827 allowed = args.allowed, 828 fetch_allowed = args.allow_fetch, 829 no_compress = args.no_compress, 830 jail = args.jail, 831 save = args.save, 832 display_identity=args.print_identity, 833 # limit=args.limit, 834 disable_auth=args.no_auth, 835 announce=args.b, 836 allow_overwrite=args.overwrite, 837 ) 838 839 elif args.fetch: 840 if args.destination != None and args.file != None: 841 fetch( 842 configdir = args.config, 843 identitypath = args.identity, 844 verbosity = args.verbose, 845 quietness = args.quiet, 846 destination = args.destination, 847 file = args.file, 848 timeout = args.w, 849 silent = args.silent, 850 phy_rates = args.phy_rates, 851 save = args.save, 852 allow_overwrite=args.overwrite, 853 ) 854 else: 855 print("") 856 parser.print_help() 857 print("") 858 859 elif args.destination != None and args.file != None: 860 send( 861 configdir = args.config, 862 identitypath = args.identity, 863 verbosity = args.verbose, 864 quietness = args.quiet, 865 destination = args.destination, 866 file = args.file, 867 timeout = args.w, 868 silent = args.silent, 869 phy_rates = args.phy_rates, 870 no_compress = args.no_compress, 871 ) 872 873 else: 874 print("") 875 parser.print_help() 876 print("") 877 878 except KeyboardInterrupt: 879 print("") 880 if resource != None: 881 resource.cancel() 882 if link != None: 883 link.teardown() 884 RNS.exit() 885 886 def size_str(num, suffix='B'): 887 units = ['','K','M','G','T','P','E','Z'] 888 last_unit = 'Y' 889 890 if suffix == 'b': 891 num *= 8 892 units = ['','K','M','G','T','P','E','Z'] 893 last_unit = 'Y' 894 895 for unit in units: 896 if abs(num) < 1000.0: 897 if unit == "": 898 return "%.0f %s%s" % (num, unit, suffix) 899 else: 900 return "%.2f %s%s" % (num, unit, suffix) 901 num /= 1000.0 902 903 return "%.2f%s%s" % (num, last_unit, suffix) 904 905 if __name__ == "__main__": 906 main()