2022-05-22 15:44:32 -06:00
#!/usr/bin/env python3
# MIT License
#
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import RNS
import argparse
2023-09-18 08:36:58 -06:00
import threading
2022-05-22 15:44:32 -06:00
import time
import sys
import os
from RNS . _version import __version__
APP_NAME = " rncp "
allow_all = False
2024-05-25 14:37:50 -06:00
allow_fetch = False
2024-05-28 12:58:20 -06:00
fetch_jail = None
2024-10-09 16:41:57 -06:00
save_path = None
2024-10-09 11:39:39 -06:00
show_phy_rates = False
2022-05-22 15:44:32 -06:00
allowed_identity_hashes = [ ]
2024-05-25 14:37:50 -06:00
REQ_FETCH_NOT_ALLOWED = 0xF0
2024-10-09 11:39:39 -06:00
es = " "
erase_str = " \33 [2K \r "
2024-05-25 14:37:50 -06:00
def listen ( configdir , verbosity = 0 , quietness = 0 , allowed = [ ] , display_identity = False ,
2024-10-09 16:41:57 -06:00
limit = None , disable_auth = None , fetch_allowed = False , jail = None , save = None , announce = False ) :
global allow_all , allow_fetch , allowed_identity_hashes , fetch_jail , save_path
2023-09-18 14:22:44 -06:00
from tempfile import TemporaryFile
2024-05-25 14:37:50 -06:00
allow_fetch = fetch_allowed
2022-05-22 15:44:32 -06:00
identity = None
2023-09-18 08:36:58 -06:00
if announce < 0 :
announce = False
2022-05-22 15:44:32 -06:00
targetloglevel = 3 + verbosity - quietness
reticulum = RNS . Reticulum ( configdir = configdir , loglevel = targetloglevel )
2024-05-28 12:58:20 -06:00
if jail != None :
fetch_jail = os . path . abspath ( os . path . expanduser ( jail ) )
RNS . log ( " Restricting fetch requests to paths under \" " + fetch_jail + " \" " , RNS . LOG_VERBOSE )
2024-10-09 16:41:57 -06:00
if save != None :
sp = os . path . abspath ( os . path . expanduser ( save ) )
if os . path . isdir ( sp ) :
if os . access ( sp , os . W_OK ) :
save_path = sp
else :
RNS . log ( " Output directory not writable " , RNS . LOG_ERROR )
exit ( 4 )
else :
RNS . log ( " Output directory not found " , RNS . LOG_ERROR )
exit ( 3 )
RNS . log ( " Saving received files in \" " + save_path + " \" " , RNS . LOG_VERBOSE )
2022-05-22 15:44:32 -06:00
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 ( " No valid saved identity found, creating new... " , RNS . LOG_INFO )
identity = RNS . Identity ( )
identity . to_file ( identity_path )
destination = RNS . Destination ( identity , RNS . Destination . IN , RNS . Destination . SINGLE , APP_NAME , " receive " )
if display_identity :
print ( " Identity : " + str ( identity ) )
2023-09-19 02:14:02 -06:00
print ( " Listening on : " + RNS . prettyhexrep ( destination . hash ) )
2022-05-22 15:44:32 -06:00
exit ( 0 )
if disable_auth :
allow_all = True
else :
2023-09-18 08:12:45 -06:00
dest_len = ( RNS . Reticulum . TRUNCATED_HASHLENGTH / / 8 ) * 2
try :
allowed_file_name = " allowed_identities "
allowed_file = None
if os . path . isfile ( os . path . expanduser ( " /etc/rncp/ " + allowed_file_name ) ) :
allowed_file = os . path . expanduser ( " /etc/rncp/ " + allowed_file_name )
elif os . path . isfile ( os . path . expanduser ( " ~/.config/rncp/ " + allowed_file_name ) ) :
allowed_file = os . path . expanduser ( " ~/.config/rncp/ " + allowed_file_name )
elif os . path . isfile ( os . path . expanduser ( " ~/.rncp/ " + allowed_file_name ) ) :
allowed_file = os . path . expanduser ( " ~/.rncp/ " + allowed_file_name )
if allowed_file != None :
af = open ( allowed_file , " r " )
al = af . read ( ) . replace ( " \r " , " " ) . split ( " \n " )
ali = [ ]
for a in al :
if len ( a ) == dest_len :
ali . append ( a )
if len ( ali ) > 0 :
if not allowed :
allowed = ali
else :
allowed . extend ( ali )
2023-09-18 14:22:44 -06:00
if len ( ali ) == 1 :
2023-09-18 08:12:45 -06:00
ms = " y "
else :
ms = " ies "
RNS . log ( " Loaded " + str ( len ( ali ) ) + " allowed identit " + ms + " from " + str ( allowed_file ) , RNS . LOG_VERBOSE )
except Exception as e :
RNS . log ( " Error while parsing allowed_identities file. The contained exception was: " + str ( e ) , RNS . LOG_ERROR )
2022-05-22 15:44:32 -06:00
if allowed != None :
for a in allowed :
try :
if len ( a ) != 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 ( a )
allowed_identity_hashes . append ( destination_hash )
except Exception as e :
raise ValueError ( " Invalid destination entered. Check your input. " )
except Exception as e :
print ( str ( e ) )
exit ( 1 )
if len ( allowed_identity_hashes ) < 1 and not disable_auth :
print ( " Warning: No allowed identities configured, rncp will not accept any files! " )
2023-09-18 14:22:44 -06:00
def fetch_request ( path , data , request_id , link_id , remote_identity , requested_at ) :
2024-05-28 12:58:20 -06:00
global allow_fetch , fetch_jail
2024-05-25 14:37:50 -06:00
if not allow_fetch :
return REQ_FETCH_NOT_ALLOWED
2024-05-28 12:58:20 -06:00
if fetch_jail :
2024-10-09 16:41:57 -06:00
if data . startswith ( fetch_jail + " / " ) :
data = data . replace ( fetch_jail + " / " , " " )
2024-10-09 11:39:39 -06:00
file_path = os . path . abspath ( os . path . expanduser ( f " { fetch_jail } / { data } " ) )
2024-10-09 16:41:57 -06:00
if not file_path . startswith ( fetch_jail + " / " ) :
RNS . log ( f " Disallowing fetch request for { file_path } outside of fetch jail { fetch_jail } " , RNS . LOG_WARNING )
2024-05-28 12:58:20 -06:00
return REQ_FETCH_NOT_ALLOWED
2024-10-09 11:39:39 -06:00
else :
file_path = os . path . abspath ( os . path . expanduser ( f " { data } " ) )
2024-05-28 12:58:20 -06:00
2023-09-18 14:22:44 -06:00
target_link = None
for link in RNS . Transport . active_links :
if link . link_id == link_id :
target_link = link
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 )
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 )
2024-10-09 11:39:39 -06:00
if allow_fetch :
if allow_all :
RNS . log ( " Allowing unauthenticated fetch requests " , RNS . LOG_WARNING )
destination . register_request_handler ( " fetch_file " , response_generator = fetch_request , allow = RNS . Destination . ALLOW_ALL )
else :
destination . register_request_handler ( " fetch_file " , response_generator = fetch_request , allow = RNS . Destination . ALLOW_LIST , allowed_list = allowed_identity_hashes )
2023-09-18 08:36:58 -06:00
print ( " rncp listening on " + RNS . prettyhexrep ( destination . hash ) )
2022-05-22 15:44:32 -06:00
2023-09-18 08:36:58 -06:00
if announce > = 0 :
def job ( ) :
destination . announce ( )
if announce > 0 :
while True :
time . sleep ( announce )
destination . announce ( )
threading . Thread ( target = job , daemon = True ) . start ( )
2022-05-22 15:44:32 -06:00
while True :
time . sleep ( 1 )
2023-09-18 14:22:44 -06:00
def client_link_established ( link ) :
2022-05-22 15:44:32 -06:00
RNS . log ( " Incoming link established " , RNS . LOG_VERBOSE )
link . set_remote_identified_callback ( receive_sender_identified )
link . set_resource_strategy ( RNS . Link . ACCEPT_APP )
link . set_resource_callback ( receive_resource_callback )
link . set_resource_started_callback ( receive_resource_started )
link . set_resource_concluded_callback ( receive_resource_concluded )
def receive_sender_identified ( link , identity ) :
2022-07-02 01:48:15 -06:00
global allow_all
2022-05-22 15:44:32 -06:00
if identity . hash in allowed_identity_hashes :
RNS . log ( " Authenticated sender " , RNS . LOG_VERBOSE )
else :
2022-07-02 01:33:05 -06:00
if not allow_all :
RNS . log ( " Sender not allowed, tearing down link " , RNS . LOG_VERBOSE )
link . teardown ( )
else :
pass
2022-05-22 15:44:32 -06:00
def receive_resource_callback ( resource ) :
2022-07-02 01:48:15 -06:00
global allow_all
2022-05-22 15:44:32 -06:00
sender_identity = resource . link . get_remote_identity ( )
if sender_identity != None :
if sender_identity . hash in allowed_identity_hashes :
return True
2022-07-02 01:48:15 -06:00
if allow_all :
return True
2022-05-22 15:44:32 -06:00
return False
def receive_resource_started ( resource ) :
if resource . link . get_remote_identity ( ) :
id_str = " from " + RNS . prettyhexrep ( resource . link . get_remote_identity ( ) . hash )
else :
id_str = " "
print ( " Starting resource transfer " + RNS . prettyhexrep ( resource . hash ) + id_str )
def receive_resource_concluded ( resource ) :
2024-10-09 16:41:57 -06:00
global save_path
2022-05-22 15:44:32 -06:00
if resource . status == RNS . Resource . COMPLETE :
print ( str ( resource ) + " completed " )
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
2024-10-09 16:41:57 -06:00
if save_path :
saved_filename = os . path . abspath ( os . path . expanduser ( save_path + " / " + filename ) )
if not saved_filename . startswith ( save_path + " / " ) :
2024-10-09 11:39:39 -06:00
RNS . log ( f " Invalid save path { saved_filename } , ignoring " , RNS . LOG_ERROR )
return
else :
saved_filename = filename
full_save_path = saved_filename
while os . path . isfile ( full_save_path ) :
2022-05-22 15:44:32 -06:00
counter + = 1
2024-10-09 11:39:39 -06:00
full_save_path = saved_filename + " . " + str ( counter )
2022-05-22 15:44:32 -06:00
2024-10-09 11:39:39 -06:00
file = open ( full_save_path , " wb " )
2022-05-22 15:44:32 -06:00
file . write ( resource . data . read ( ) )
file . close ( )
else :
print ( " Invalid data received, ignoring resource " )
else :
print ( " Resource failed " )
resource_done = False
current_resource = None
stats = [ ]
speed = 0.0
2024-10-09 11:39:39 -06:00
phy_speed = 0.0
2022-05-22 15:44:32 -06:00
def sender_progress ( resource ) :
stats_max = 32
2024-10-09 11:39:39 -06:00
global current_resource , stats , speed , phy_speed , resource_done
2022-05-22 15:44:32 -06:00
current_resource = resource
2024-10-09 11:39:39 -06:00
2022-05-22 15:44:32 -06:00
now = time . time ( )
2024-10-09 11:39:39 -06:00
got = current_resource . get_progress ( ) * current_resource . get_data_size ( )
phy_got = current_resource . get_segment_progress ( ) * current_resource . get_transfer_size ( )
entry = [ now , got , phy_got ]
2022-05-22 15:44:32 -06:00
stats . append ( entry )
2024-10-09 11:39:39 -06:00
2022-05-22 15:44:32 -06:00
while len ( stats ) > stats_max :
stats . pop ( 0 )
span = now - stats [ 0 ] [ 0 ]
if span == 0 :
speed = 0
2024-10-09 11:39:39 -06:00
phy_speed = 0
2022-05-22 15:44:32 -06:00
else :
diff = got - stats [ 0 ] [ 1 ]
speed = diff / span
2024-10-09 11:39:39 -06:00
phy_diff = phy_got - stats [ 0 ] [ 2 ]
if phy_diff > 0 :
phy_speed = phy_diff / span
2022-05-22 15:44:32 -06:00
if resource . status < RNS . Resource . COMPLETE :
resource_done = False
else :
resource_done = True
link = None
2024-10-09 16:41:57 -06:00
def fetch ( configdir , verbosity = 0 , quietness = 0 , destination = None , file = None , timeout = RNS . Transport . PATH_REQUEST_TIMEOUT , silent = False , phy_rates = False , save = None ) :
global current_resource , resource_done , link , speed , show_phy_rates , save_path
2023-09-18 14:22:44 -06:00
targetloglevel = 3 + verbosity - quietness
2024-10-09 11:39:39 -06:00
show_phy_rates = phy_rates
2023-09-18 14:22:44 -06:00
2024-10-09 16:41:57 -06:00
if save :
sp = os . path . abspath ( os . path . expanduser ( save ) )
if os . path . isdir ( sp ) :
if os . access ( sp , os . W_OK ) :
save_path = sp
else :
RNS . log ( " Output directory not writable " , RNS . LOG_ERROR )
exit ( 4 )
else :
RNS . log ( " Output directory not found " , RNS . LOG_ERROR )
exit ( 3 )
2023-09-18 14:22:44 -06:00
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 :
2024-10-09 11:39:39 -06:00
print ( " Path to " + RNS . prettyhexrep ( destination_hash ) + " requested " , end = es )
2023-09-18 14:22:44 -06:00
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 :
2024-10-09 11:39:39 -06:00
print ( f " { erase_str } Path not found " )
2023-09-18 14:22:44 -06:00
exit ( 1 )
else :
if silent :
print ( " Establishing link with " + RNS . prettyhexrep ( destination_hash ) )
else :
2024-10-09 11:39:39 -06:00
print ( f " { erase_str } Establishing link with " + RNS . prettyhexrep ( destination_hash ) + " " , end = es )
2023-09-18 14:22:44 -06:00
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 :
2024-10-09 11:39:39 -06:00
print ( f " { erase_str } Could not establish link with " + RNS . prettyhexrep ( destination_hash ) )
2023-09-18 14:22:44 -06:00
exit ( 1 )
else :
if silent :
print ( " Requesting file from remote... " )
else :
2024-10-09 11:39:39 -06:00
print ( f " { erase_str } Requesting file from remote " , end = es )
2023-09-18 14:22:44 -06:00
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 "
2024-05-25 14:37:50 -06:00
elif request_receipt . response == REQ_FETCH_NOT_ALLOWED :
request_status = " fetch_not_allowed "
2023-09-18 14:22:44 -06:00
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
2024-10-09 16:41:57 -06:00
global save_path
2023-09-18 14:22:44 -06:00
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
2024-10-09 16:41:57 -06:00
if save_path :
saved_filename = os . path . abspath ( os . path . expanduser ( save_path + " / " + filename ) )
else :
saved_filename = filename
full_save_path = saved_filename
while os . path . isfile ( full_save_path ) :
2023-09-18 14:22:44 -06:00
counter + = 1
2024-10-09 16:41:57 -06:00
full_save_path = saved_filename + " . " + str ( counter )
file = open ( full_save_path , " wb " )
2023-09-18 14:22:44 -06:00
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 )
2024-05-25 14:37:50 -06:00
if request_status == " fetch_not_allowed " :
2024-10-09 11:39:39 -06:00
if not silent : print ( f " { erase_str } " , end = " " )
2024-05-25 14:37:50 -06:00
print ( " Fetch request failed, fetching the file " + str ( file ) + " was not allowed by the remote " )
link . teardown ( )
2024-05-28 12:58:20 -06:00
time . sleep ( 0.15 )
2024-05-25 14:37:50 -06:00
exit ( 0 )
elif request_status == " not_found " :
2024-10-09 11:39:39 -06:00
if not silent : print ( f " { erase_str } " , end = " " )
2023-09-18 14:22:44 -06:00
print ( " Fetch request failed, the file " + str ( file ) + " was not found on the remote " )
link . teardown ( )
2024-05-28 12:58:20 -06:00
time . sleep ( 0.15 )
2023-09-18 14:22:44 -06:00
exit ( 0 )
elif request_status == " remote_error " :
2024-10-09 11:39:39 -06:00
if not silent : print ( f " { erase_str } " , end = " " )
2023-09-18 14:22:44 -06:00
print ( " Fetch request failed due to an error on the remote system " )
link . teardown ( )
2024-05-28 12:58:20 -06:00
time . sleep ( 0.15 )
2023-09-18 14:22:44 -06:00
exit ( 0 )
elif request_status == " unknown " :
2024-10-09 11:39:39 -06:00
if not silent : print ( f " { erase_str } " , end = " " )
2023-09-18 14:40:29 -06:00
print ( " Fetch request failed due to an unknown error (probably not authorised) " )
2023-09-18 14:22:44 -06:00
link . teardown ( )
2024-05-28 12:58:20 -06:00
time . sleep ( 0.15 )
2023-09-18 14:22:44 -06:00
exit ( 0 )
elif request_status == " found " :
2024-10-09 11:39:39 -06:00
if not silent : print ( f " { erase_str } " , end = " " )
2023-09-18 14:22:44 -06:00
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 )
2024-10-09 11:39:39 -06:00
if show_phy_rates :
2024-10-09 11:53:07 -06:00
pss = size_str ( phy_speed , " b " )
phy_str = f " ( { pss } ps at physical layer) "
2024-10-09 11:39:39 -06:00
else :
phy_str = " "
2024-10-09 11:53:07 -06:00
ps = size_str ( int ( prg * current_resource . total_size ) )
ts = size_str ( current_resource . total_size )
ss = size_str ( speed , " b " )
stat_str = f " { percent } % - { ps } of { ts } - { ss } ps { phy_str } "
2024-08-28 13:34:16 -06:00
if prg != 1.0 :
2024-10-09 11:39:39 -06:00
print ( f " { erase_str } Transferring file { syms [ i ] } { stat_str } " , end = es )
2024-08-28 13:34:16 -06:00
else :
2024-10-09 11:39:39 -06:00
print ( f " { erase_str } Transfer complete { stat_str } " , end = es )
2023-09-18 14:22:44 -06:00
else :
2024-10-09 11:39:39 -06:00
print ( f " { erase_str } Waiting for transfer to start { syms [ i ] } " , end = es )
2023-09-18 14:22:44 -06:00
sys . stdout . flush ( )
i = ( i + 1 ) % len ( syms )
if current_resource . status != RNS . Resource . COMPLETE :
if silent :
print ( " The transfer failed " )
else :
2024-10-09 11:39:39 -06:00
print ( f " { erase_str } The transfer failed " )
2023-09-18 14:22:44 -06:00
exit ( 1 )
else :
if silent :
2024-08-28 13:34:16 -06:00
print ( str ( file ) + " fetched from " + RNS . prettyhexrep ( destination_hash ) )
2023-09-18 14:22:44 -06:00
else :
2024-08-28 13:21:38 -06:00
print ( " \n " + str ( file ) + " fetched from " + RNS . prettyhexrep ( destination_hash ) )
2023-09-18 14:22:44 -06:00
link . teardown ( )
2024-05-28 12:58:20 -06:00
time . sleep ( 0.15 )
2023-09-18 14:22:44 -06:00
exit ( 0 )
link . teardown ( )
exit ( 0 )
2024-10-09 11:39:39 -06:00
def send ( configdir , verbosity = 0 , quietness = 0 , destination = None , file = None , timeout = RNS . Transport . PATH_REQUEST_TIMEOUT , silent = False , phy_rates = False ) :
global current_resource , resource_done , link , speed , show_phy_rates
2022-05-22 15:44:32 -06:00
from tempfile import TemporaryFile
targetloglevel = 3 + verbosity - quietness
2024-10-09 11:39:39 -06:00
show_phy_rates = phy_rates
2022-05-22 15:44:32 -06:00
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 )
file_path = os . path . expanduser ( file )
if not os . path . isfile ( file_path ) :
print ( " File not found " )
exit ( 1 )
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 :
2024-10-09 11:39:39 -06:00
print ( " Preparing file... " , end = es )
2022-05-22 15:44:32 -06:00
temp_file . write ( filename_len . to_bytes ( 2 , " big " ) )
temp_file . write ( filename_bytes )
temp_file . write ( real_file . read ( ) )
temp_file . seek ( 0 )
2024-10-09 11:39:39 -06:00
print ( f " { erase_str } " , end = " " )
2022-05-22 15:44:32 -06:00
reticulum = RNS . Reticulum ( configdir = configdir , loglevel = targetloglevel )
identity_path = RNS . Reticulum . identitypath + " / " + APP_NAME
if os . path . isfile ( identity_path ) :
2023-05-31 07:39:55 -06:00
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
2022-05-22 15:44:32 -06:00
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 )
2023-09-18 08:36:58 -06:00
if silent :
print ( " Path to " + RNS . prettyhexrep ( destination_hash ) + " requested " )
else :
2024-10-09 11:39:39 -06:00
print ( " Path to " + RNS . prettyhexrep ( destination_hash ) + " requested " , end = es )
2022-05-22 15:44:32 -06:00
sys . stdout . flush ( )
i = 0
syms = " ⢄⢂⢁⡁⡈⡐⡠ "
estab_timeout = time . time ( ) + timeout
while not RNS . Transport . has_path ( destination_hash ) and time . time ( ) < estab_timeout :
2023-09-18 08:36:58 -06:00
if not silent :
time . sleep ( 0.1 )
print ( ( " \b \b " + syms [ i ] + " " ) , end = " " )
sys . stdout . flush ( )
i = ( i + 1 ) % len ( syms )
2022-05-22 15:44:32 -06:00
if not RNS . Transport . has_path ( destination_hash ) :
2023-09-18 08:36:58 -06:00
if silent :
print ( " Path not found " )
else :
2024-10-09 11:39:39 -06:00
print ( f " { erase_str } Path not found " )
2022-05-22 15:44:32 -06:00
exit ( 1 )
else :
2023-09-18 08:36:58 -06:00
if silent :
print ( " Establishing link with " + RNS . prettyhexrep ( destination_hash ) )
else :
2024-10-09 11:39:39 -06:00
print ( f " { erase_str } Establishing link with " + RNS . prettyhexrep ( destination_hash ) + " " , end = es )
2022-05-22 15:44:32 -06:00
receiver_identity = RNS . Identity . recall ( destination_hash )
receiver_destination = RNS . Destination (
receiver_identity ,
RNS . Destination . OUT ,
RNS . Destination . SINGLE ,
APP_NAME ,
" receive "
)
link = RNS . Link ( receiver_destination )
while link . status != RNS . Link . ACTIVE and time . time ( ) < estab_timeout :
2023-09-18 08:36:58 -06:00
if not silent :
time . sleep ( 0.1 )
print ( ( " \b \b " + syms [ i ] + " " ) , end = " " )
sys . stdout . flush ( )
i = ( i + 1 ) % len ( syms )
2022-05-22 15:44:32 -06:00
2023-09-21 06:12:14 -06:00
if time . time ( ) > estab_timeout :
2023-09-18 08:36:58 -06:00
if silent :
2023-09-21 06:12:14 -06:00
print ( " Link establishment with " + RNS . prettyhexrep ( destination_hash ) + " timed out " )
2023-09-18 08:36:58 -06:00
else :
2024-10-09 11:39:39 -06:00
print ( f " { erase_str } Link establishment with " + RNS . prettyhexrep ( destination_hash ) + " timed out " )
2023-09-21 06:12:14 -06:00
exit ( 1 )
elif not RNS . Transport . has_path ( destination_hash ) :
if silent :
print ( " No path found to " + RNS . prettyhexrep ( destination_hash ) )
else :
2024-10-09 11:39:39 -06:00
print ( f " { erase_str } No path found to " + RNS . prettyhexrep ( destination_hash ) )
2022-05-22 15:44:32 -06:00
exit ( 1 )
else :
2023-09-18 08:36:58 -06:00
if silent :
print ( " Advertising file resource... " )
else :
2024-10-09 11:39:39 -06:00
print ( f " { erase_str } Advertising file resource " , end = es )
2022-05-22 15:44:32 -06:00
link . identify ( identity )
resource = RNS . Resource ( temp_file , link , callback = sender_progress , progress_callback = sender_progress )
current_resource = resource
while resource . status < RNS . Resource . TRANSFERRING :
2023-09-18 08:36:58 -06:00
if not silent :
time . sleep ( 0.1 )
print ( ( " \b \b " + syms [ i ] + " " ) , end = " " )
sys . stdout . flush ( )
i = ( i + 1 ) % len ( syms )
2022-05-22 15:44:32 -06:00
if resource . status > RNS . Resource . COMPLETE :
2023-09-18 08:36:58 -06:00
if silent :
print ( " File was not accepted by " + RNS . prettyhexrep ( destination_hash ) )
else :
2024-10-09 11:39:39 -06:00
print ( f " { erase_str } File was not accepted by " + RNS . prettyhexrep ( destination_hash ) )
2022-05-22 15:44:32 -06:00
exit ( 1 )
else :
2023-09-18 08:36:58 -06:00
if silent :
print ( " Transferring file... " )
else :
2024-10-09 11:39:39 -06:00
print ( f " { erase_str } Transferring file " , end = es )
2022-05-22 15:44:32 -06:00
2024-08-28 13:34:16 -06:00
def progress_update ( i , done = False ) :
2024-08-28 13:21:38 -06:00
time . sleep ( 0.1 )
prg = current_resource . get_progress ( )
percent = round ( prg * 100.0 , 1 )
2024-10-09 11:39:39 -06:00
if show_phy_rates :
2024-10-09 11:53:07 -06:00
pss = size_str ( phy_speed , " b " )
phy_str = f " ( { pss } ps at physical layer) "
2024-10-09 11:39:39 -06:00
else :
phy_str = " "
es = " "
2024-10-09 11:53:07 -06:00
cs = size_str ( int ( prg * current_resource . total_size ) )
ts = size_str ( current_resource . total_size )
ss = size_str ( speed , " b " )
stat_str = f " { percent } % - { cs } of { ts } - { ss } ps { phy_str } "
2024-08-28 13:34:16 -06:00
if not done :
2024-10-09 11:39:39 -06:00
print ( f " { erase_str } Transferring file " + syms [ i ] + " " + stat_str , end = es )
2024-08-28 13:34:16 -06:00
else :
2024-10-09 11:39:39 -06:00
print ( f " { erase_str } Transfer complete " + stat_str , end = es )
2024-08-28 13:21:38 -06:00
sys . stdout . flush ( )
i = ( i + 1 ) % len ( syms )
return i
2022-05-22 15:44:32 -06:00
while not resource_done :
2023-09-18 08:36:58 -06:00
if not silent :
2024-08-28 13:21:38 -06:00
i = progress_update ( i )
2022-05-22 15:44:32 -06:00
2024-08-28 13:34:16 -06:00
if not silent :
i = progress_update ( i , done = True )
2022-05-22 15:44:32 -06:00
if current_resource . status != RNS . Resource . COMPLETE :
2023-09-18 08:36:58 -06:00
if silent :
print ( " The transfer failed " )
else :
2024-10-09 11:39:39 -06:00
print ( f " { erase_str } The transfer failed " )
2022-05-22 15:44:32 -06:00
exit ( 1 )
else :
2023-09-18 08:36:58 -06:00
if silent :
print ( str ( file_path ) + " copied to " + RNS . prettyhexrep ( destination_hash ) )
else :
2024-08-28 13:21:38 -06:00
print ( " \n " + str ( file_path ) + " copied to " + RNS . prettyhexrep ( destination_hash ) )
2022-05-22 15:44:32 -06:00
link . teardown ( )
time . sleep ( 0.25 )
real_file . close ( )
temp_file . close ( )
exit ( 0 )
def main ( ) :
try :
parser = argparse . ArgumentParser ( description = " Reticulum File Transfer Utility " )
parser . add_argument ( " file " , nargs = " ? " , default = None , help = " file to be transferred " , type = str )
parser . add_argument ( " destination " , nargs = " ? " , default = None , help = " hexadecimal hash of the receiver " , type = str )
parser . add_argument ( " --config " , metavar = " path " , action = " store " , default = None , help = " path to alternative Reticulum config directory " , type = str )
parser . add_argument ( ' -v ' , ' --verbose ' , action = ' count ' , default = 0 , help = " increase verbosity " )
parser . add_argument ( ' -q ' , ' --quiet ' , action = ' count ' , default = 0 , help = " decrease verbosity " )
2023-09-18 08:36:58 -06:00
parser . add_argument ( " -S " , ' --silent ' , action = ' store_true ' , default = False , help = " disable transfer progress output " )
parser . add_argument ( " -l " , ' --listen ' , action = ' store_true ' , default = False , help = " listen for incoming transfer requests " )
2024-05-25 14:37:50 -06:00
parser . add_argument ( " -F " , ' --allow-fetch ' , action = ' store_true ' , default = False , help = " allow authenticated clients to fetch files " )
2023-09-18 14:40:29 -06:00
parser . add_argument ( " -f " , ' --fetch ' , action = ' store_true ' , default = False , help = " fetch file from remote listener instead of sending " )
2024-05-28 12:58:20 -06:00
parser . add_argument ( " -j " , " --jail " , metavar = " path " , action = " store " , default = None , help = " restrict fetch requests to specified path " , type = str )
2024-10-09 16:41:57 -06:00
parser . add_argument ( " -s " , " --save " , metavar = " path " , action = " store " , default = None , help = " save received files in specified path " , type = str )
2023-09-18 08:36:58 -06:00
parser . add_argument ( " -b " , action = ' store ' , metavar = " seconds " , default = - 1 , help = " announce interval, 0 to only announce at startup " , type = int )
2024-10-09 16:41:57 -06:00
parser . add_argument ( ' -a ' , metavar = " allowed_hash " , dest = " allowed " , action = ' append ' , help = " allow this identity (or add in ~/.rncp/allowed_identities) " , type = str )
2024-05-25 14:39:50 -06:00
parser . add_argument ( ' -n ' , ' --no-auth ' , action = ' store_true ' , default = False , help = " accept requests from anyone " )
2023-09-18 08:36:58 -06:00
parser . add_argument ( ' -p ' , ' --print-identity ' , action = ' store_true ' , default = False , help = " print identity and destination info and exit " )
2022-05-22 15:44:32 -06:00
parser . add_argument ( " -w " , action = " store " , metavar = " seconds " , type = float , help = " sender timeout before giving up " , default = RNS . Transport . PATH_REQUEST_TIMEOUT )
2024-10-09 11:39:39 -06:00
parser . add_argument ( ' -P ' , ' --phy-rates ' , action = ' store_true ' , default = False , help = " display physical layer transfer rates " )
2022-05-22 15:44:32 -06:00
# parser.add_argument("--limit", action="store", metavar="files", type=float, help="maximum number of files to accept", default=None)
parser . add_argument ( " --version " , action = " version " , version = " rncp {version} " . format ( version = __version__ ) )
args = parser . parse_args ( )
2023-09-18 08:36:58 -06:00
if args . listen or args . print_identity :
2023-09-18 14:22:44 -06:00
listen (
2022-05-22 15:44:32 -06:00
configdir = args . config ,
verbosity = args . verbose ,
quietness = args . quiet ,
2022-05-24 12:13:54 -06:00
allowed = args . allowed ,
2024-05-25 14:37:50 -06:00
fetch_allowed = args . allow_fetch ,
2024-05-28 12:58:20 -06:00
jail = args . jail ,
2024-10-09 16:41:57 -06:00
save = args . save ,
2022-05-24 12:13:54 -06:00
display_identity = args . print_identity ,
2022-05-22 15:44:32 -06:00
# limit=args.limit,
disable_auth = args . no_auth ,
2023-09-18 08:36:58 -06:00
announce = args . b ,
2022-05-22 15:44:32 -06:00
)
2023-09-18 14:22:44 -06:00
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 ,
2024-10-09 11:39:39 -06:00
phy_rates = args . phy_rates ,
2024-10-09 16:41:57 -06:00
save = args . save ,
2023-09-18 14:22:44 -06:00
)
else :
print ( " " )
parser . print_help ( )
print ( " " )
2022-05-22 15:44:32 -06:00
elif args . destination != None and args . file != None :
send (
configdir = args . config ,
verbosity = args . verbose ,
quietness = args . quiet ,
destination = args . destination ,
file = args . file ,
timeout = args . w ,
2023-09-18 08:36:58 -06:00
silent = args . silent ,
2024-10-09 11:39:39 -06:00
phy_rates = args . phy_rates ,
2022-05-22 15:44:32 -06:00
)
else :
print ( " " )
parser . print_help ( )
print ( " " )
except KeyboardInterrupt :
print ( " " )
if resource != None :
resource . cancel ( )
if link != None :
link . teardown ( )
exit ( )
def size_str ( num , suffix = ' B ' ) :
units = [ ' ' , ' K ' , ' M ' , ' G ' , ' T ' , ' P ' , ' E ' , ' Z ' ]
last_unit = ' Y '
if suffix == ' b ' :
num * = 8
units = [ ' ' , ' K ' , ' M ' , ' G ' , ' T ' , ' P ' , ' E ' , ' Z ' ]
last_unit = ' Y '
for unit in units :
if abs ( num ) < 1000.0 :
if unit == " " :
return " %.0f %s %s " % ( num , unit , suffix )
else :
return " %.2f %s %s " % ( num , unit , suffix )
num / = 1000.0
return " %.2f %s %s " % ( num , last_unit , suffix )
if __name__ == " __main__ " :
main ( )