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