Buffer.py
1 ########################################################## 2 # This RNS example demonstrates how to set up a link to # 3 # a destination, and pass binary data over it using a # 4 # channel buffer. # 5 ########################################################## 6 from __future__ import annotations 7 import os 8 import sys 9 import time 10 import argparse 11 from datetime import datetime 12 13 import RNS 14 from RNS.vendor import umsgpack 15 16 # Let's define an app name. We'll use this for all 17 # destinations we create. Since this echo example 18 # is part of a range of example utilities, we'll put 19 # them all within the app namespace "example_utilities" 20 APP_NAME = "example_utilities" 21 22 23 ########################################################## 24 #### Server Part ######################################### 25 ########################################################## 26 27 # A reference to the latest client link that connected 28 latest_client_link = None 29 30 # A reference to the latest buffer object 31 latest_buffer = None 32 33 # This initialisation is executed when the users chooses 34 # to run as a server 35 def server(configpath): 36 # We must first initialise Reticulum 37 reticulum = RNS.Reticulum(configpath) 38 39 # Randomly create a new identity for our example 40 server_identity = RNS.Identity() 41 42 # We create a destination that clients can connect to. We 43 # want clients to create links to this destination, so we 44 # need to create a "single" destination type. 45 server_destination = RNS.Destination( 46 server_identity, 47 RNS.Destination.IN, 48 RNS.Destination.SINGLE, 49 APP_NAME, 50 "bufferexample" 51 ) 52 53 # We configure a function that will get called every time 54 # a new client creates a link to this destination. 55 server_destination.set_link_established_callback(client_connected) 56 57 # Everything's ready! 58 # Let's Wait for client requests or user input 59 server_loop(server_destination) 60 61 def server_loop(destination): 62 # Let the user know that everything is ready 63 RNS.log( 64 "Link buffer example "+ 65 RNS.prettyhexrep(destination.hash)+ 66 " running, waiting for a connection." 67 ) 68 69 RNS.log("Hit enter to manually send an announce (Ctrl-C to quit)") 70 71 # We enter a loop that runs until the users exits. 72 # If the user hits enter, we will announce our server 73 # destination on the network, which will let clients 74 # know how to create messages directed towards it. 75 while True: 76 entered = input() 77 destination.announce() 78 RNS.log("Sent announce from "+RNS.prettyhexrep(destination.hash)) 79 80 # When a client establishes a link to our server 81 # destination, this function will be called with 82 # a reference to the link. 83 def client_connected(link): 84 global latest_client_link, latest_buffer 85 latest_client_link = link 86 87 RNS.log("Client connected") 88 link.set_link_closed_callback(client_disconnected) 89 90 # If a new connection is received, the old reader 91 # needs to be disconnected. 92 if latest_buffer: 93 latest_buffer.close() 94 95 96 # Create buffer objects. 97 # The stream_id parameter to these functions is 98 # a bit like a file descriptor, except that it 99 # is unique to the *receiver*. 100 # 101 # In this example, both the reader and the writer 102 # use stream_id = 0, but there are actually two 103 # separate unidirectional streams flowing in 104 # opposite directions. 105 # 106 channel = link.get_channel() 107 latest_buffer = RNS.Buffer.create_bidirectional_buffer(0, 0, channel, server_buffer_ready) 108 109 def client_disconnected(link): 110 RNS.log("Client disconnected") 111 112 def server_buffer_ready(ready_bytes: int): 113 """ 114 Callback from buffer when buffer has data available 115 116 :param ready_bytes: The number of bytes ready to read 117 """ 118 global latest_buffer 119 120 data = latest_buffer.read(ready_bytes) 121 data = data.decode("utf-8") 122 123 RNS.log("Received data over the buffer: " + data) 124 125 reply_message = "I received \""+data+"\" over the buffer" 126 reply_message = reply_message.encode("utf-8") 127 latest_buffer.write(reply_message) 128 latest_buffer.flush() 129 130 131 132 133 ########################################################## 134 #### Client Part ######################################### 135 ########################################################## 136 137 # A reference to the server link 138 server_link = None 139 140 # A reference to the buffer object, needed to share the 141 # object from the link connected callback to the client 142 # loop. 143 buffer = None 144 145 # This initialisation is executed when the users chooses 146 # to run as a client 147 def client(destination_hexhash, configpath): 148 # We need a binary representation of the destination 149 # hash that was entered on the command line 150 try: 151 dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2 152 if len(destination_hexhash) != dest_len: 153 raise ValueError( 154 "Destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2) 155 ) 156 157 destination_hash = bytes.fromhex(destination_hexhash) 158 except: 159 RNS.log("Invalid destination entered. Check your input!\n") 160 sys.exit(0) 161 162 # We must first initialise Reticulum 163 reticulum = RNS.Reticulum(configpath) 164 165 # Check if we know a path to the destination 166 if not RNS.Transport.has_path(destination_hash): 167 RNS.log("Destination is not yet known. Requesting path and waiting for announce to arrive...") 168 RNS.Transport.request_path(destination_hash) 169 while not RNS.Transport.has_path(destination_hash): 170 time.sleep(0.1) 171 172 # Recall the server identity 173 server_identity = RNS.Identity.recall(destination_hash) 174 175 # Inform the user that we'll begin connecting 176 RNS.log("Establishing link with server...") 177 178 # When the server identity is known, we set 179 # up a destination 180 server_destination = RNS.Destination( 181 server_identity, 182 RNS.Destination.OUT, 183 RNS.Destination.SINGLE, 184 APP_NAME, 185 "bufferexample" 186 ) 187 188 # And create a link 189 link = RNS.Link(server_destination) 190 191 # We'll also set up functions to inform the 192 # user when the link is established or closed 193 link.set_link_established_callback(link_established) 194 link.set_link_closed_callback(link_closed) 195 196 # Everything is set up, so let's enter a loop 197 # for the user to interact with the example 198 client_loop() 199 200 def client_loop(): 201 global server_link 202 203 # Wait for the link to become active 204 while not server_link: 205 time.sleep(0.1) 206 207 should_quit = False 208 while not should_quit: 209 try: 210 print("> ", end=" ") 211 text = input() 212 213 # Check if we should quit the example 214 if text == "quit" or text == "q" or text == "exit": 215 should_quit = True 216 server_link.teardown() 217 else: 218 # Otherwise, encode the text and write it to the buffer. 219 text = text.encode("utf-8") 220 buffer.write(text) 221 # Flush the buffer to force the data to be sent. 222 buffer.flush() 223 224 225 except Exception as e: 226 RNS.log("Error while sending data over the link buffer: "+str(e)) 227 should_quit = True 228 server_link.teardown() 229 230 # This function is called when a link 231 # has been established with the server 232 def link_established(link): 233 # We store a reference to the link 234 # instance for later use 235 global server_link, buffer 236 server_link = link 237 238 # Create buffer, see server_client_connected() for 239 # more detail about setting up the buffer. 240 channel = link.get_channel() 241 buffer = RNS.Buffer.create_bidirectional_buffer(0, 0, channel, client_buffer_ready) 242 243 # Inform the user that the server is 244 # connected 245 RNS.log("Link established with server, enter some text to send, or \"quit\" to quit") 246 247 # When a link is closed, we'll inform the 248 # user, and exit the program 249 def link_closed(link): 250 if link.teardown_reason == RNS.Link.TIMEOUT: 251 RNS.log("The link timed out, exiting now") 252 elif link.teardown_reason == RNS.Link.DESTINATION_CLOSED: 253 RNS.log("The link was closed by the server, exiting now") 254 else: 255 RNS.log("Link closed, exiting now") 256 257 time.sleep(1.5) 258 sys.exit(0) 259 260 # When the buffer has new data, read it and write it to the terminal. 261 def client_buffer_ready(ready_bytes: int): 262 global buffer 263 data = buffer.read(ready_bytes) 264 RNS.log("Received data over the link buffer: " + data.decode("utf-8")) 265 print("> ", end=" ") 266 sys.stdout.flush() 267 268 269 ########################################################## 270 #### Program Startup ##################################### 271 ########################################################## 272 273 # This part of the program runs at startup, 274 # and parses input of from the user, and then 275 # starts up the desired program mode. 276 if __name__ == "__main__": 277 try: 278 parser = argparse.ArgumentParser(description="Simple buffer example") 279 280 parser.add_argument( 281 "-s", 282 "--server", 283 action="store_true", 284 help="wait for incoming link requests from clients" 285 ) 286 287 parser.add_argument( 288 "--config", 289 action="store", 290 default=None, 291 help="path to alternative Reticulum config directory", 292 type=str 293 ) 294 295 parser.add_argument( 296 "destination", 297 nargs="?", 298 default=None, 299 help="hexadecimal hash of the server destination", 300 type=str 301 ) 302 303 args = parser.parse_args() 304 305 if args.config: 306 configarg = args.config 307 else: 308 configarg = None 309 310 if args.server: 311 server(configarg) 312 else: 313 if (args.destination == None): 314 print("") 315 parser.print_help() 316 print("") 317 else: 318 client(args.destination, configarg) 319 320 except KeyboardInterrupt: 321 print("") 322 sys.exit(0)