Channel.py
1 ########################################################## 2 # This RNS example demonstrates how to set up a link to # 3 # a destination, and pass structured messages over it # 4 # using a channel. # 5 ########################################################## 6 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 #### Shared Objects ###################################### 24 ########################################################## 25 26 # Channel data must be structured in a subclass of 27 # MessageBase. This ensures that the channel will be able 28 # to serialize and deserialize the object and multiplex it 29 # with other objects. Both ends of a link will need the 30 # same object definitions to be able to communicate over 31 # a channel. 32 # 33 # Note: The objects we wish to use over the channel must 34 # be registered with the channel, and each link has a 35 # different channel instance. See the client_connected 36 # and link_established functions in this example to see 37 # how message types are registered. 38 39 # Let's make a simple message class called StringMessage 40 # that will convey a string with a timestamp. 41 42 class StringMessage(RNS.MessageBase): 43 # The MSGTYPE class variable needs to be assigned a 44 # 2 byte integer value. This identifier allows the 45 # channel to look up your message's constructor when a 46 # message arrives over the channel. 47 # 48 # MSGTYPE must be unique across all message types we 49 # register with the channel. MSGTYPEs >= 0xf000 are 50 # reserved for the system. 51 MSGTYPE = 0x0101 52 53 # The constructor of our object must be callable with 54 # no arguments. We can have parameters, but they must 55 # have a default assignment. 56 # 57 # This is needed so the channel can create an empty 58 # version of our message into which the incoming 59 # message can be unpacked. 60 def __init__(self, data=None): 61 self.data = data 62 self.timestamp = datetime.now() 63 64 # Finally, our message needs to implement functions 65 # the channel can call to pack and unpack our message 66 # to/from the raw packet payload. We'll use the 67 # umsgpack package bundled with RNS. We could also use 68 # the struct package bundled with Python if we wanted 69 # more control over the structure of the packed bytes. 70 # 71 # Also note that packed message objects must fit 72 # entirely in one packet. The number of bytes 73 # available for message payloads can be queried from 74 # the channel using the Channel.MDU property. The 75 # channel MDU is slightly less than the link MDU due 76 # to encoding the message header. 77 78 # The pack function encodes the message contents into 79 # a byte stream. 80 def pack(self) -> bytes: 81 return umsgpack.packb((self.data, self.timestamp)) 82 83 # And the unpack function decodes a byte stream into 84 # the message contents. 85 def unpack(self, raw): 86 self.data, self.timestamp = umsgpack.unpackb(raw) 87 88 89 ########################################################## 90 #### Server Part ######################################### 91 ########################################################## 92 93 # A reference to the latest client link that connected 94 latest_client_link = None 95 96 # This initialisation is executed when the users chooses 97 # to run as a server 98 def server(configpath): 99 # We must first initialise Reticulum 100 reticulum = RNS.Reticulum(configpath) 101 102 # Randomly create a new identity for our link example 103 server_identity = RNS.Identity() 104 105 # We create a destination that clients can connect to. We 106 # want clients to create links to this destination, so we 107 # need to create a "single" destination type. 108 server_destination = RNS.Destination( 109 server_identity, 110 RNS.Destination.IN, 111 RNS.Destination.SINGLE, 112 APP_NAME, 113 "channelexample" 114 ) 115 116 # We configure a function that will get called every time 117 # a new client creates a link to this destination. 118 server_destination.set_link_established_callback(client_connected) 119 120 # Everything's ready! 121 # Let's Wait for client requests or user input 122 server_loop(server_destination) 123 124 def server_loop(destination): 125 # Let the user know that everything is ready 126 RNS.log( 127 "Channel example "+ 128 RNS.prettyhexrep(destination.hash)+ 129 " running, waiting for a connection." 130 ) 131 132 RNS.log("Hit enter to manually send an announce (Ctrl-C to quit)") 133 134 # We enter a loop that runs until the users exits. 135 # If the user hits enter, we will announce our server 136 # destination on the network, which will let clients 137 # know how to create messages directed towards it. 138 while True: 139 entered = input() 140 destination.announce() 141 RNS.log("Sent announce from "+RNS.prettyhexrep(destination.hash)) 142 143 # When a client establishes a link to our server 144 # destination, this function will be called with 145 # a reference to the link. 146 def client_connected(link): 147 global latest_client_link 148 latest_client_link = link 149 150 RNS.log("Client connected") 151 link.set_link_closed_callback(client_disconnected) 152 153 # Register message types and add callback to channel 154 channel = link.get_channel() 155 channel.register_message_type(StringMessage) 156 channel.add_message_handler(server_message_received) 157 158 def client_disconnected(link): 159 RNS.log("Client disconnected") 160 161 def server_message_received(message): 162 """ 163 A message handler 164 @param message: An instance of a subclass of MessageBase 165 @return: True if message was handled 166 """ 167 global latest_client_link 168 # When a message is received over any active link, 169 # the replies will all be directed to the last client 170 # that connected. 171 172 # In a message handler, any deserializable message 173 # that arrives over the link's channel will be passed 174 # to all message handlers, unless a preceding handler indicates it 175 # has handled the message. 176 # 177 # 178 if isinstance(message, StringMessage): 179 RNS.log("Received data on the link: " + message.data + " (message created at " + str(message.timestamp) + ")") 180 181 reply_message = StringMessage("I received \""+message.data+"\" over the link") 182 latest_client_link.get_channel().send(reply_message) 183 184 # Incoming messages are sent to each message 185 # handler added to the channel, in the order they 186 # were added. 187 # If any message handler returns True, the message 188 # is considered handled and any subsequent 189 # handlers are skipped. 190 return True 191 192 193 ########################################################## 194 #### Client Part ######################################### 195 ########################################################## 196 197 # A reference to the server link 198 server_link = None 199 200 # This initialisation is executed when the users chooses 201 # to run as a client 202 def client(destination_hexhash, configpath): 203 # We need a binary representation of the destination 204 # hash that was entered on the command line 205 try: 206 dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2 207 if len(destination_hexhash) != dest_len: 208 raise ValueError( 209 "Destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2) 210 ) 211 212 destination_hash = bytes.fromhex(destination_hexhash) 213 except: 214 RNS.log("Invalid destination entered. Check your input!\n") 215 sys.exit(0) 216 217 # We must first initialise Reticulum 218 reticulum = RNS.Reticulum(configpath) 219 220 # Check if we know a path to the destination 221 if not RNS.Transport.has_path(destination_hash): 222 RNS.log("Destination is not yet known. Requesting path and waiting for announce to arrive...") 223 RNS.Transport.request_path(destination_hash) 224 while not RNS.Transport.has_path(destination_hash): 225 time.sleep(0.1) 226 227 # Recall the server identity 228 server_identity = RNS.Identity.recall(destination_hash) 229 230 # Inform the user that we'll begin connecting 231 RNS.log("Establishing link with server...") 232 233 # When the server identity is known, we set 234 # up a destination 235 server_destination = RNS.Destination( 236 server_identity, 237 RNS.Destination.OUT, 238 RNS.Destination.SINGLE, 239 APP_NAME, 240 "channelexample" 241 ) 242 243 # And create a link 244 link = RNS.Link(server_destination) 245 246 # We'll also set up functions to inform the 247 # user when the link is established or closed 248 link.set_link_established_callback(link_established) 249 link.set_link_closed_callback(link_closed) 250 251 # Everything is set up, so let's enter a loop 252 # for the user to interact with the example 253 client_loop() 254 255 def client_loop(): 256 global server_link 257 258 # Wait for the link to become active 259 while not server_link: 260 time.sleep(0.1) 261 262 should_quit = False 263 while not should_quit: 264 try: 265 print("> ", end=" ") 266 text = input() 267 268 # Check if we should quit the example 269 if text == "quit" or text == "q" or text == "exit": 270 should_quit = True 271 server_link.teardown() 272 273 # If not, send the entered text over the link 274 if text != "": 275 message = StringMessage(text) 276 packed_size = len(message.pack()) 277 channel = server_link.get_channel() 278 if channel.is_ready_to_send(): 279 if packed_size <= channel.mdu: 280 channel.send(message) 281 else: 282 RNS.log( 283 "Cannot send this packet, the data size of "+ 284 str(packed_size)+" bytes exceeds the link packet MDU of "+ 285 str(channel.MDU)+" bytes", 286 RNS.LOG_ERROR 287 ) 288 else: 289 RNS.log("Channel is not ready to send, please wait for " + 290 "pending messages to complete.", RNS.LOG_ERROR) 291 292 except Exception as e: 293 RNS.log("Error while sending data over the link: "+str(e)) 294 should_quit = True 295 server_link.teardown() 296 297 # This function is called when a link 298 # has been established with the server 299 def link_established(link): 300 # We store a reference to the link 301 # instance for later use 302 global server_link 303 server_link = link 304 305 # Register messages and add handler to channel 306 channel = link.get_channel() 307 channel.register_message_type(StringMessage) 308 channel.add_message_handler(client_message_received) 309 310 # Inform the user that the server is 311 # connected 312 RNS.log("Link established with server, enter some text to send, or \"quit\" to quit") 313 314 # When a link is closed, we'll inform the 315 # user, and exit the program 316 def link_closed(link): 317 if link.teardown_reason == RNS.Link.TIMEOUT: 318 RNS.log("The link timed out, exiting now") 319 elif link.teardown_reason == RNS.Link.DESTINATION_CLOSED: 320 RNS.log("The link was closed by the server, exiting now") 321 else: 322 RNS.log("Link closed, exiting now") 323 324 time.sleep(1.5) 325 sys.exit(0) 326 327 # When a packet is received over the channel, we 328 # simply print out the data. 329 def client_message_received(message): 330 if isinstance(message, StringMessage): 331 RNS.log("Received data on the link: " + message.data + " (message created at " + str(message.timestamp) + ")") 332 print("> ", end=" ") 333 sys.stdout.flush() 334 335 336 ########################################################## 337 #### Program Startup ##################################### 338 ########################################################## 339 340 # This part of the program runs at startup, 341 # and parses input of from the user, and then 342 # starts up the desired program mode. 343 if __name__ == "__main__": 344 try: 345 parser = argparse.ArgumentParser(description="Simple channel example") 346 347 parser.add_argument( 348 "-s", 349 "--server", 350 action="store_true", 351 help="wait for incoming link requests from clients" 352 ) 353 354 parser.add_argument( 355 "--config", 356 action="store", 357 default=None, 358 help="path to alternative Reticulum config directory", 359 type=str 360 ) 361 362 parser.add_argument( 363 "destination", 364 nargs="?", 365 default=None, 366 help="hexadecimal hash of the server destination", 367 type=str 368 ) 369 370 args = parser.parse_args() 371 372 if args.config: 373 configarg = args.config 374 else: 375 configarg = None 376 377 if args.server: 378 server(configarg) 379 else: 380 if (args.destination == None): 381 print("") 382 parser.print_help() 383 print("") 384 else: 385 client(args.destination, configarg) 386 387 except KeyboardInterrupt: 388 print("") 389 sys.exit(0)