Echo.py
1 ########################################################## 2 # This RNS example demonstrates a simple client/server # 3 # echo utility. A client can send an echo request to the # 4 # server, and the server will respond by proving receipt # 5 # of the packet. # 6 ########################################################## 7 8 import argparse 9 import sys 10 import RNS 11 12 # Let's define an app name. We'll use this for all 13 # destinations we create. Since this echo example 14 # is part of a range of example utilities, we'll put 15 # them all within the app namespace "example_utilities" 16 APP_NAME = "example_utilities" 17 18 19 ########################################################## 20 #### Server Part ######################################### 21 ########################################################## 22 23 # This initialisation is executed when the users chooses 24 # to run as a server 25 def server(configpath): 26 global reticulum 27 28 # We must first initialise Reticulum 29 reticulum = RNS.Reticulum(configpath) 30 31 # Randomly create a new identity for our echo server 32 server_identity = RNS.Identity() 33 34 # We create a destination that clients can query. We want 35 # to be able to verify echo replies to our clients, so we 36 # create a "single" destination that can receive encrypted 37 # messages. This way the client can send a request and be 38 # certain that no-one else than this destination was able 39 # to read it. 40 echo_destination = RNS.Destination( 41 server_identity, 42 RNS.Destination.IN, 43 RNS.Destination.SINGLE, 44 APP_NAME, 45 "echo", 46 "request" 47 ) 48 49 # We configure the destination to automatically prove all 50 # packets addressed to it. By doing this, RNS will automatically 51 # generate a proof for each incoming packet and transmit it 52 # back to the sender of that packet. 53 echo_destination.set_proof_strategy(RNS.Destination.PROVE_ALL) 54 55 # Tell the destination which function in our program to 56 # run when a packet is received. We do this so we can 57 # print a log message when the server receives a request 58 echo_destination.set_packet_callback(server_callback) 59 60 # Everything's ready! 61 # Let's Wait for client requests or user input 62 announceLoop(echo_destination) 63 64 65 def announceLoop(destination): 66 # Let the user know that everything is ready 67 RNS.log( 68 "Echo server "+ 69 RNS.prettyhexrep(destination.hash)+ 70 " running, hit enter to manually send an announce (Ctrl-C to quit)" 71 ) 72 73 # We enter a loop that runs until the users exits. 74 # If the user hits enter, we will announce our server 75 # destination on the network, which will let clients 76 # know how to create messages directed towards it. 77 while True: 78 entered = input() 79 destination.announce() 80 RNS.log("Sent announce from "+RNS.prettyhexrep(destination.hash)) 81 82 83 def server_callback(message, packet): 84 global reticulum 85 86 # Tell the user that we received an echo request, and 87 # that we are going to send a reply to the requester. 88 # Sending the proof is handled automatically, since we 89 # set up the destination to prove all incoming packets. 90 91 reception_stats = "" 92 if reticulum.is_connected_to_shared_instance: 93 reception_rssi = reticulum.get_packet_rssi(packet.packet_hash) 94 reception_snr = reticulum.get_packet_snr(packet.packet_hash) 95 96 if reception_rssi != None: 97 reception_stats += " [RSSI "+str(reception_rssi)+" dBm]" 98 99 if reception_snr != None: 100 reception_stats += " [SNR "+str(reception_snr)+" dBm]" 101 102 else: 103 if packet.rssi != None: 104 reception_stats += " [RSSI "+str(packet.rssi)+" dBm]" 105 106 if packet.snr != None: 107 reception_stats += " [SNR "+str(packet.snr)+" dB]" 108 109 RNS.log("Received packet from echo client, proof sent"+reception_stats) 110 111 112 ########################################################## 113 #### Client Part ######################################### 114 ########################################################## 115 116 # This initialisation is executed when the users chooses 117 # to run as a client 118 def client(destination_hexhash, configpath, timeout=None): 119 global reticulum 120 121 # We need a binary representation of the destination 122 # hash that was entered on the command line 123 try: 124 dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2 125 if len(destination_hexhash) != dest_len: 126 raise ValueError( 127 "Destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2) 128 ) 129 130 destination_hash = bytes.fromhex(destination_hexhash) 131 except Exception as e: 132 RNS.log("Invalid destination entered. Check your input!") 133 RNS.log(str(e)+"\n") 134 sys.exit(0) 135 136 # We must first initialise Reticulum 137 reticulum = RNS.Reticulum(configpath) 138 139 # We override the loglevel to provide feedback when 140 # an announce is received 141 if RNS.loglevel < RNS.LOG_INFO: 142 RNS.loglevel = RNS.LOG_INFO 143 144 # Tell the user that the client is ready! 145 RNS.log( 146 "Echo client ready, hit enter to send echo request to "+ 147 destination_hexhash+ 148 " (Ctrl-C to quit)" 149 ) 150 151 # We enter a loop that runs until the user exits. 152 # If the user hits enter, we will try to send an 153 # echo request to the destination specified on the 154 # command line. 155 while True: 156 input() 157 158 # Let's first check if RNS knows a path to the destination. 159 # If it does, we'll load the server identity and create a packet 160 if RNS.Transport.has_path(destination_hash): 161 162 # To address the server, we need to know it's public 163 # key, so we check if Reticulum knows this destination. 164 # This is done by calling the "recall" method of the 165 # Identity module. If the destination is known, it will 166 # return an Identity instance that can be used in 167 # outgoing destinations. 168 server_identity = RNS.Identity.recall(destination_hash) 169 170 # We got the correct identity instance from the 171 # recall method, so let's create an outgoing 172 # destination. We use the naming convention: 173 # example_utilities.echo.request 174 # This matches the naming we specified in the 175 # server part of the code. 176 request_destination = RNS.Destination( 177 server_identity, 178 RNS.Destination.OUT, 179 RNS.Destination.SINGLE, 180 APP_NAME, 181 "echo", 182 "request" 183 ) 184 185 # The destination is ready, so let's create a packet. 186 # We set the destination to the request_destination 187 # that was just created, and the only data we add 188 # is a random hash. 189 echo_request = RNS.Packet(request_destination, RNS.Identity.get_random_hash()) 190 191 # Send the packet! If the packet is successfully 192 # sent, it will return a PacketReceipt instance. 193 packet_receipt = echo_request.send() 194 195 # If the user specified a timeout, we set this 196 # timeout on the packet receipt, and configure 197 # a callback function, that will get called if 198 # the packet times out. 199 if timeout != None: 200 packet_receipt.set_timeout(timeout) 201 packet_receipt.set_timeout_callback(packet_timed_out) 202 203 # We can then set a delivery callback on the receipt. 204 # This will get automatically called when a proof for 205 # this specific packet is received from the destination. 206 packet_receipt.set_delivery_callback(packet_delivered) 207 208 # Tell the user that the echo request was sent 209 RNS.log("Sent echo request to "+RNS.prettyhexrep(request_destination.hash)) 210 else: 211 # If we do not know this destination, tell the 212 # user to wait for an announce to arrive. 213 RNS.log("Destination is not yet known. Requesting path...") 214 RNS.log("Hit enter to manually retry once an announce is received.") 215 RNS.Transport.request_path(destination_hash) 216 217 # This function is called when our reply destination 218 # receives a proof packet. 219 def packet_delivered(receipt): 220 global reticulum 221 222 if receipt.status == RNS.PacketReceipt.DELIVERED: 223 rtt = receipt.get_rtt() 224 if (rtt >= 1): 225 rtt = round(rtt, 3) 226 rttstring = str(rtt)+" seconds" 227 else: 228 rtt = round(rtt*1000, 3) 229 rttstring = str(rtt)+" milliseconds" 230 231 reception_stats = "" 232 if reticulum.is_connected_to_shared_instance: 233 reception_rssi = reticulum.get_packet_rssi(receipt.proof_packet.packet_hash) 234 reception_snr = reticulum.get_packet_snr(receipt.proof_packet.packet_hash) 235 236 if reception_rssi != None: 237 reception_stats += " [RSSI "+str(reception_rssi)+" dBm]" 238 239 if reception_snr != None: 240 reception_stats += " [SNR "+str(reception_snr)+" dB]" 241 242 else: 243 if receipt.proof_packet != None: 244 if receipt.proof_packet.rssi != None: 245 reception_stats += " [RSSI "+str(receipt.proof_packet.rssi)+" dBm]" 246 247 if receipt.proof_packet.snr != None: 248 reception_stats += " [SNR "+str(receipt.proof_packet.snr)+" dB]" 249 250 RNS.log( 251 "Valid reply received from "+ 252 RNS.prettyhexrep(receipt.destination.hash)+ 253 ", round-trip time is "+rttstring+ 254 reception_stats 255 ) 256 257 # This function is called if a packet times out. 258 def packet_timed_out(receipt): 259 if receipt.status == RNS.PacketReceipt.FAILED: 260 RNS.log("Packet "+RNS.prettyhexrep(receipt.hash)+" timed out") 261 262 263 ########################################################## 264 #### Program Startup ##################################### 265 ########################################################## 266 267 # This part of the program gets run at startup, 268 # and parses input from the user, and then starts 269 # the desired program mode. 270 if __name__ == "__main__": 271 try: 272 parser = argparse.ArgumentParser(description="Simple echo server and client utility") 273 274 parser.add_argument( 275 "-s", 276 "--server", 277 action="store_true", 278 help="wait for incoming packets from clients" 279 ) 280 281 parser.add_argument( 282 "-t", 283 "--timeout", 284 action="store", 285 metavar="s", 286 default=None, 287 help="set a reply timeout in seconds", 288 type=float 289 ) 290 291 parser.add_argument("--config", 292 action="store", 293 default=None, 294 help="path to alternative Reticulum config directory", 295 type=str 296 ) 297 298 parser.add_argument( 299 "destination", 300 nargs="?", 301 default=None, 302 help="hexadecimal hash of the server destination", 303 type=str 304 ) 305 306 args = parser.parse_args() 307 308 if args.server: 309 configarg=None 310 if args.config: 311 configarg = args.config 312 server(configarg) 313 else: 314 if args.config: 315 configarg = args.config 316 else: 317 configarg = None 318 319 if args.timeout: 320 timeoutarg = float(args.timeout) 321 else: 322 timeoutarg = None 323 324 if (args.destination == None): 325 print("") 326 parser.print_help() 327 print("") 328 else: 329 client(args.destination, configarg, timeout=timeoutarg) 330 except KeyboardInterrupt: 331 print("") 332 sys.exit(0)