/ Examples / Buffer.py
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)