Link.py
1 ########################################################## 2 # This RNS example demonstrates how to set up a link to # 3 # a destination, and pass data back and forth over it. # 4 ########################################################## 5 6 import os 7 import sys 8 import time 9 import argparse 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 #### Server Part ######################################### 20 ########################################################## 21 22 # A reference to the latest client link that connected 23 latest_client_link = None 24 25 # This initialisation is executed when the users chooses 26 # to run as a server 27 def server(configpath): 28 # We must first initialise Reticulum 29 reticulum = RNS.Reticulum(configpath) 30 31 # Randomly create a new identity for our link example 32 server_identity = RNS.Identity() 33 34 # We create a destination that clients can connect to. We 35 # want clients to create links to this destination, so we 36 # need to create a "single" destination type. 37 server_destination = RNS.Destination( 38 server_identity, 39 RNS.Destination.IN, 40 RNS.Destination.SINGLE, 41 APP_NAME, 42 "linkexample" 43 ) 44 45 # We configure a function that will get called every time 46 # a new client creates a link to this destination. 47 server_destination.set_link_established_callback(client_connected) 48 49 # Everything's ready! 50 # Let's Wait for client requests or user input 51 server_loop(server_destination) 52 53 def server_loop(destination): 54 # Let the user know that everything is ready 55 RNS.log( 56 "Link example "+ 57 RNS.prettyhexrep(destination.hash)+ 58 " running, waiting for a connection." 59 ) 60 61 RNS.log("Hit enter to manually send an announce (Ctrl-C to quit)") 62 63 # We enter a loop that runs until the users exits. 64 # If the user hits enter, we will announce our server 65 # destination on the network, which will let clients 66 # know how to create messages directed towards it. 67 while True: 68 entered = input() 69 destination.announce() 70 RNS.log("Sent announce from "+RNS.prettyhexrep(destination.hash)) 71 72 # When a client establishes a link to our server 73 # destination, this function will be called with 74 # a reference to the link. 75 def client_connected(link): 76 global latest_client_link 77 78 RNS.log("Client connected") 79 link.set_link_closed_callback(client_disconnected) 80 link.set_packet_callback(server_packet_received) 81 latest_client_link = link 82 83 def client_disconnected(link): 84 RNS.log("Client disconnected") 85 86 def server_packet_received(message, packet): 87 global latest_client_link 88 89 # When data is received over any active link, 90 # it will all be directed to the last client 91 # that connected. 92 text = message.decode("utf-8") 93 RNS.log("Received data on the link: "+text) 94 95 reply_text = "I received \""+text+"\" over the link" 96 reply_data = reply_text.encode("utf-8") 97 RNS.Packet(latest_client_link, reply_data).send() 98 99 100 ########################################################## 101 #### Client Part ######################################### 102 ########################################################## 103 104 # A reference to the server link 105 server_link = None 106 107 # This initialisation is executed when the users chooses 108 # to run as a client 109 def client(destination_hexhash, configpath): 110 # We need a binary representation of the destination 111 # hash that was entered on the command line 112 try: 113 dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2 114 if len(destination_hexhash) != dest_len: 115 raise ValueError( 116 "Destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2) 117 ) 118 119 destination_hash = bytes.fromhex(destination_hexhash) 120 except: 121 RNS.log("Invalid destination entered. Check your input!\n") 122 sys.exit(0) 123 124 # We must first initialise Reticulum 125 reticulum = RNS.Reticulum(configpath) 126 127 # Check if we know a path to the destination 128 if not RNS.Transport.has_path(destination_hash): 129 RNS.log("Destination is not yet known. Requesting path and waiting for announce to arrive...") 130 RNS.Transport.request_path(destination_hash) 131 while not RNS.Transport.has_path(destination_hash): 132 time.sleep(0.1) 133 134 # Recall the server identity 135 server_identity = RNS.Identity.recall(destination_hash) 136 137 # Inform the user that we'll begin connecting 138 RNS.log("Establishing link with server...") 139 140 # When the server identity is known, we set 141 # up a destination 142 server_destination = RNS.Destination( 143 server_identity, 144 RNS.Destination.OUT, 145 RNS.Destination.SINGLE, 146 APP_NAME, 147 "linkexample" 148 ) 149 150 # And create a link 151 link = RNS.Link(server_destination) 152 153 # We set a callback that will get executed 154 # every time a packet is received over the 155 # link 156 link.set_packet_callback(client_packet_received) 157 158 # We'll also set up functions to inform the 159 # user when the link is established or closed 160 link.set_link_established_callback(link_established) 161 link.set_link_closed_callback(link_closed) 162 163 # Everything is set up, so let's enter a loop 164 # for the user to interact with the example 165 client_loop() 166 167 def client_loop(): 168 global server_link 169 170 # Wait for the link to become active 171 while not server_link: 172 time.sleep(0.1) 173 174 should_quit = False 175 while not should_quit: 176 try: 177 print("> ", end=" ") 178 text = input() 179 180 # Check if we should quit the example 181 if text == "quit" or text == "q" or text == "exit": 182 should_quit = True 183 server_link.teardown() 184 185 # If not, send the entered text over the link 186 if text != "": 187 data = text.encode("utf-8") 188 if len(data) <= RNS.Link.MDU: 189 RNS.Packet(server_link, data).send() 190 else: 191 RNS.log( 192 "Cannot send this packet, the data size of "+ 193 str(len(data))+" bytes exceeds the link packet MDU of "+ 194 str(RNS.Link.MDU)+" bytes", 195 RNS.LOG_ERROR 196 ) 197 198 except Exception as e: 199 RNS.log("Error while sending data over the link: "+str(e)) 200 should_quit = True 201 server_link.teardown() 202 203 # This function is called when a link 204 # has been established with the server 205 def link_established(link): 206 # We store a reference to the link 207 # instance for later use 208 global server_link 209 server_link = link 210 211 # Inform the user that the server is 212 # connected 213 RNS.log("Link established with server, enter some text to send, or \"quit\" to quit") 214 215 # When a link is closed, we'll inform the 216 # user, and exit the program 217 def link_closed(link): 218 if link.teardown_reason == RNS.Link.TIMEOUT: 219 RNS.log("The link timed out, exiting now") 220 elif link.teardown_reason == RNS.Link.DESTINATION_CLOSED: 221 RNS.log("The link was closed by the server, exiting now") 222 else: 223 RNS.log("Link closed, exiting now") 224 225 time.sleep(1.5) 226 sys.exit(0) 227 228 # When a packet is received over the link, we 229 # simply print out the data. 230 def client_packet_received(message, packet): 231 text = message.decode("utf-8") 232 RNS.log("Received data on the link: "+text) 233 print("> ", end=" ") 234 sys.stdout.flush() 235 236 237 ########################################################## 238 #### Program Startup ##################################### 239 ########################################################## 240 241 # This part of the program runs at startup, 242 # and parses input of from the user, and then 243 # starts up the desired program mode. 244 if __name__ == "__main__": 245 try: 246 parser = argparse.ArgumentParser(description="Simple link example") 247 248 parser.add_argument( 249 "-s", 250 "--server", 251 action="store_true", 252 help="wait for incoming link requests from clients" 253 ) 254 255 parser.add_argument( 256 "--config", 257 action="store", 258 default=None, 259 help="path to alternative Reticulum config directory", 260 type=str 261 ) 262 263 parser.add_argument( 264 "destination", 265 nargs="?", 266 default=None, 267 help="hexadecimal hash of the server destination", 268 type=str 269 ) 270 271 args = parser.parse_args() 272 273 if args.config: 274 configarg = args.config 275 else: 276 configarg = None 277 278 if args.server: 279 server(configarg) 280 else: 281 if (args.destination == None): 282 print("") 283 parser.print_help() 284 print("") 285 else: 286 client(args.destination, configarg) 287 288 except KeyboardInterrupt: 289 print("") 290 sys.exit(0)