From 1dc6655017704db9b15129e2ef0b48ca4fe9df4e Mon Sep 17 00:00:00 2001
From: Mark Qvist
Date: Fri, 20 Aug 2021 23:29:06 +0200
Subject: [PATCH] Implemented request and response API
---
Examples/Request.py | 283 +++++++++++++++++
RNS/Destination.py | 45 +++
RNS/Link.py | 141 ++++++++-
RNS/Packet.py | 22 +-
docs/manual/.buildinfo | 2 +-
docs/manual/_sources/examples.rst.txt | 11 +
docs/manual/_static/documentation_options.js | 2 +-
docs/manual/examples.html | 303 ++++++++++++++++++-
docs/manual/genindex.html | 12 +-
docs/manual/gettingstartedfast.html | 6 +-
docs/manual/index.html | 7 +-
docs/manual/objects.inv | Bin 1517 -> 1548 bytes
docs/manual/reference.html | 56 +++-
docs/manual/search.html | 6 +-
docs/manual/searchindex.js | 2 +-
docs/manual/understanding.html | 6 +-
docs/manual/whatis.html | 6 +-
docs/source/conf.py | 2 +-
docs/source/examples.rst | 11 +
19 files changed, 881 insertions(+), 42 deletions(-)
create mode 100644 Examples/Request.py
diff --git a/Examples/Request.py b/Examples/Request.py
new file mode 100644
index 0000000..228dfae
--- /dev/null
+++ b/Examples/Request.py
@@ -0,0 +1,283 @@
+##########################################################
+# This RNS example demonstrates how to set perform #
+# requests and receive responses over a link. #
+##########################################################
+
+import os
+import sys
+import time
+import random
+import argparse
+import RNS
+
+# Let's define an app name. We'll use this for all
+# destinations we create. Since this echo example
+# is part of a range of example utilities, we'll put
+# them all within the app namespace "example_utilities"
+APP_NAME = "example_utilities"
+
+##########################################################
+#### Server Part #########################################
+##########################################################
+
+# A reference to the latest client link that connected
+latest_client_link = None
+
+def random_text_generator(path, data, request_id, remote_identity_hash, requested_at):
+ RNS.log("Generating response to request "+RNS.prettyhexrep(request_id))
+ texts = ["They looked up", "On each full moon", "Becky was upset", "I’ll stay away from it", "The pet shop stocks everything"]
+ return texts[random.randint(0, len(texts)-1)]
+
+# This initialisation is executed when the users chooses
+# to run as a server
+def server(configpath):
+ # We must first initialise Reticulum
+ reticulum = RNS.Reticulum(configpath)
+
+ # Randomly create a new identity for our link example
+ server_identity = RNS.Identity()
+
+ # We create a destination that clients can connect to. We
+ # want clients to create links to this destination, so we
+ # need to create a "single" destination type.
+ server_destination = RNS.Destination(
+ server_identity,
+ RNS.Destination.IN,
+ RNS.Destination.SINGLE,
+ APP_NAME,
+ "requestexample"
+ )
+
+ # We configure a function that will get called every time
+ # a new client creates a link to this destination.
+ server_destination.set_link_established_callback(client_connected)
+
+ # We register a request handler for handling incoming
+ # requests over any established links.
+ server_destination.register_request_handler(
+ "/random/text",
+ response_generator = random_text_generator,
+ allow = RNS.Destination.ALLOW_ALL
+ )
+
+ # Everything's ready!
+ # Let's Wait for client requests or user input
+ server_loop(server_destination)
+
+def server_loop(destination):
+ # Let the user know that everything is ready
+ RNS.log(
+ "Request example "+
+ RNS.prettyhexrep(destination.hash)+
+ " running, waiting for a connection."
+ )
+
+ RNS.log("Hit enter to manually send an announce (Ctrl-C to quit)")
+
+ # We enter a loop that runs until the users exits.
+ # If the user hits enter, we will announce our server
+ # destination on the network, which will let clients
+ # know how to create messages directed towards it.
+ while True:
+ entered = input()
+ destination.announce()
+ RNS.log("Sent announce from "+RNS.prettyhexrep(destination.hash))
+
+# When a client establishes a link to our server
+# destination, this function will be called with
+# a reference to the link.
+def client_connected(link):
+ global latest_client_link
+
+ RNS.log("Client connected")
+ link.set_link_closed_callback(client_disconnected)
+ latest_client_link = link
+
+def client_disconnected(link):
+ RNS.log("Client disconnected")
+
+
+##########################################################
+#### Client Part #########################################
+##########################################################
+
+# A reference to the server link
+server_link = None
+
+# This initialisation is executed when the users chooses
+# to run as a client
+def client(destination_hexhash, configpath):
+ # We need a binary representation of the destination
+ # hash that was entered on the command line
+ try:
+ if len(destination_hexhash) != 20:
+ raise ValueError("Destination length is invalid, must be 20 hexadecimal characters (10 bytes)")
+ destination_hash = bytes.fromhex(destination_hexhash)
+ except:
+ RNS.log("Invalid destination entered. Check your input!\n")
+ exit()
+
+ # We must first initialise Reticulum
+ reticulum = RNS.Reticulum(configpath)
+
+ # Check if we know a path to the destination
+ if not RNS.Transport.has_path(destination_hash):
+ RNS.log("Destination is not yet known. Requesting path and waiting for announce to arrive...")
+ RNS.Transport.request_path(destination_hash)
+ while not RNS.Transport.has_path(destination_hash):
+ time.sleep(0.1)
+
+ # Recall the server identity
+ server_identity = RNS.Identity.recall(destination_hash)
+
+ # Inform the user that we'll begin connecting
+ RNS.log("Establishing link with server...")
+
+ # When the server identity is known, we set
+ # up a destination
+ server_destination = RNS.Destination(
+ server_identity,
+ RNS.Destination.OUT,
+ RNS.Destination.SINGLE,
+ APP_NAME,
+ "requestexample"
+ )
+
+ # And create a link
+ link = RNS.Link(server_destination)
+
+ # We'll set up functions to inform the
+ # user when the link is established or closed
+ link.set_link_established_callback(link_established)
+ link.set_link_closed_callback(link_closed)
+
+ # Everything is set up, so let's enter a loop
+ # for the user to interact with the example
+ client_loop()
+
+def client_loop():
+ global server_link
+
+ # Wait for the link to become active
+ while not server_link:
+ time.sleep(0.1)
+
+ should_quit = False
+ while not should_quit:
+ try:
+ print("> ", end=" ")
+ text = input()
+
+ # Check if we should quit the example
+ if text == "quit" or text == "q" or text == "exit":
+ should_quit = True
+ server_link.teardown()
+
+ else:
+ server_link.request(
+ "/random/text",
+ data = None,
+ response_callback = got_response,
+ failed_callback = request_failed
+ )
+
+
+ except Exception as e:
+ RNS.log("Error while sending request over the link: "+str(e))
+ should_quit = True
+ server_link.teardown()
+
+def got_response(request_receipt):
+ request_id = request_receipt.request_id
+ response = request_receipt.response
+
+ RNS.log("Got response for request "+RNS.prettyhexrep(request_id)+": "+str(response))
+
+def request_received(request_receipt):
+ RNS.log("The request "+RNS.prettyhexrep(request_receipt.request_id)+" was received by the remote peer.")
+
+def request_failed(request_receipt):
+ RNS.log("The request "+RNS.prettyhexrep(request_receipt.request_id)+" failed.")
+
+
+# This function is called when a link
+# has been established with the server
+def link_established(link):
+ # We store a reference to the link
+ # instance for later use
+ global server_link
+ server_link = link
+
+ # Inform the user that the server is
+ # connected
+ RNS.log("Link established with server, hit enter to perform a request, or type in \"quit\" to quit")
+
+# When a link is closed, we'll inform the
+# user, and exit the program
+def link_closed(link):
+ if link.teardown_reason == RNS.Link.TIMEOUT:
+ RNS.log("The link timed out, exiting now")
+ elif link.teardown_reason == RNS.Link.DESTINATION_CLOSED:
+ RNS.log("The link was closed by the server, exiting now")
+ else:
+ RNS.log("Link closed, exiting now")
+
+ RNS.Reticulum.exit_handler()
+ time.sleep(1.5)
+ os._exit(0)
+
+
+##########################################################
+#### Program Startup #####################################
+##########################################################
+
+# This part of the program runs at startup,
+# and parses input of from the user, and then
+# starts up the desired program mode.
+if __name__ == "__main__":
+ try:
+ parser = argparse.ArgumentParser(description="Simple request/response example")
+
+ parser.add_argument(
+ "-s",
+ "--server",
+ action="store_true",
+ help="wait for incoming requests from clients"
+ )
+
+ parser.add_argument(
+ "--config",
+ action="store",
+ default=None,
+ help="path to alternative Reticulum config directory",
+ type=str
+ )
+
+ parser.add_argument(
+ "destination",
+ nargs="?",
+ default=None,
+ help="hexadecimal hash of the server destination",
+ type=str
+ )
+
+ args = parser.parse_args()
+
+ if args.config:
+ configarg = args.config
+ else:
+ configarg = None
+
+ if args.server:
+ server(configarg)
+ else:
+ if (args.destination == None):
+ print("")
+ parser.print_help()
+ print("")
+ else:
+ client(args.destination, configarg)
+
+ except KeyboardInterrupt:
+ print("")
+ exit()
\ No newline at end of file
diff --git a/RNS/Destination.py b/RNS/Destination.py
index f8b24b3..cdf4499 100755
--- a/RNS/Destination.py
+++ b/RNS/Destination.py
@@ -40,6 +40,11 @@ class Destination:
PROVE_ALL = 0x23
proof_strategies = [PROVE_NONE, PROVE_APP, PROVE_ALL]
+ ALLOW_NONE = 0x00
+ ALLOW_ALL = 0x01
+ ALLOW_LIST = 0x02
+ request_policies = [ALLOW_NONE, ALLOW_ALL, ALLOW_LIST]
+
IN = 0x11;
OUT = 0x12;
directions = [IN, OUT]
@@ -97,6 +102,7 @@ class Destination:
if not type in Destination.types: raise ValueError("Unknown destination type")
if not direction in Destination.directions: raise ValueError("Unknown destination direction")
self.callbacks = Callbacks()
+ self.request_handlers = {}
self.type = type
self.direction = direction
self.proof_strategy = Destination.PROVE_NONE
@@ -208,6 +214,45 @@ class Destination:
else:
self.proof_strategy = proof_strategy
+
+ def register_request_handler(self, path, response_generator = None, allow = ALLOW_NONE, allowed_list = None):
+ """
+ Registers a request handler.
+
+ :param path: The path for the request handler to be registered.
+ :param response_generator: A function or method with the signature *response_generator(path, data, request_id, remote_identity_hash, requested_at)* to be called. Whatever this funcion returns will be sent as a response to the requester. If the function returns ``None``, no response will be sent.
+ :param allow: One of ``RNS.Destination.ALLOW_NONE``, ``RNS.Destination.ALLOW_ALL`` or ``RNS.Destination.ALLOW_LIST``. If ``RNS.Destination.ALLOW_LIST`` is set, the request handler will only respond to requests for identified peers in the supplied list.
+ :param allowed_list: A list of *bytes-like* :ref:`RNS.Identity` hashes.
+ :raises: ``ValueError`` if any of the supplied arguments are invalid.
+ """
+ if path == None or path == "":
+ raise ValueError("Invalid path specified")
+ elif not callable(response_generator):
+ raise ValueError("Invalid response generator specified")
+ elif not allow in Destination.request_policies:
+ raise ValueError("Invalid request policy")
+ else:
+ path_hash = RNS.Identity.truncated_hash(path.encode("utf-8"))
+ request_handler = [path, response_generator, allow, allowed_list]
+ self.request_handlers[path_hash] = request_handler
+
+
+ def deregister_request_handler(self, path):
+ """
+ Deregisters a request handler.
+
+ :param path: The path for the request handler to be deregistered.
+ :returns: True if the handler was deregistered, otherwise False.
+ """
+ path_hash = RNS.Identity.truncated_hash(path.encode("utf-8"))
+ if path_hash in self.request_handlers:
+ self.request_handlers.pop(path_hash)
+ return True
+ else:
+ return False
+
+
+
def receive(self, packet):
if packet.packet_type == RNS.Packet.LINKREQUEST:
plaintext = packet.data
diff --git a/RNS/Link.py b/RNS/Link.py
index 5d40854..995dc6e 100644
--- a/RNS/Link.py
+++ b/RNS/Link.py
@@ -110,6 +110,7 @@ class Link:
self.resource_strategy = Link.ACCEPT_NONE
self.outgoing_resources = []
self.incoming_resources = []
+ self.pending_requests = []
self.last_inbound = 0
self.last_outbound = 0
self.tx = 0
@@ -265,6 +266,26 @@ class Link:
self.had_outbound()
+ def request(self, path, data = None, response_callback = None, failed_callback = None):
+ """
+ Sends a request to the remote peer.
+
+ :param path: The request path.
+ :param response_callback: A function or method with the signature *response_callback(request_receipt)* to be called when a response is received. See the :ref:`Request Example` for more info.
+ :param failed_callback: A function or method with the signature *failed_callback(request_receipt)* to be called when a request fails. See the :ref:`Request Example` for more info.
+ """
+ request_path_hash = RNS.Identity.truncated_hash(path.encode("utf-8"))
+ unpacked_request = [time.time(), request_path_hash, data]
+ packed_request = umsgpack.packb(unpacked_request)
+
+ if len(packed_request) <= Link.MDU:
+ request_packet = RNS.Packet(self, packed_request, RNS.Packet.DATA, context = RNS.Packet.REQUEST)
+ return RequestReceipt(self, request_packet.send(), response_callback, failed_callback)
+ else:
+ # TODO: Implement sending requests as Resources
+ raise IOError("Request size of "+str(len(packed_request))+" exceeds MDU of "+str(Link.MDU)+" bytes")
+
+
def rtt_packet(self, packet):
try:
# TODO: This is crude, we should use the delta
@@ -467,6 +488,70 @@ class Link:
if self.callbacks.remote_identified != None:
self.callbacks.remote_identified(self.__remote_identity)
+ elif packet.context == RNS.Packet.REQUEST:
+ try:
+ request_id = packet.getTruncatedHash()
+ packed_request = self.decrypt(packet.data)
+ unpacked_request = umsgpack.unpackb(packed_request)
+ requested_at = unpacked_request[0]
+ path_hash = unpacked_request[1]
+ request_data = unpacked_request[2]
+
+ if path_hash in self.destination.request_handlers:
+ request_handler = self.destination.request_handlers[path_hash]
+ path = request_handler[0]
+ response_generator = request_handler[1]
+ allow = request_handler[2]
+ allowed_list = request_handler[3]
+
+ allowed = False
+ if not allow == RNS.Destination.ALLOW_NONE:
+ if allow == RNS.Destination.ALLOW_LIST:
+ if self.__remote_identity in allowed_list:
+ allowed = True
+ elif allow == RNS.Destination.ALLOW_ALL:
+ allowed = True
+
+ if allowed:
+ response = response_generator(path, request_data, request_id, self.__remote_identity, requested_at)
+ if response != None:
+ packed_response = umsgpack.packb([request_id, True, response])
+
+ if len(packed_response) <= Link.MDU:
+ RNS.Packet(self, packed_response, RNS.Packet.DATA, context = RNS.Packet.RESPONSE).send()
+ else:
+ # TODO: Implement transfer as resource
+ packed_response = umsgpack.packb([request_id, False, None])
+ raise Exception("Response transfer as resource not implemented")
+
+ except Exception as e:
+ RNS.log("Error occurred while handling request. The contained exception was: "+str(e), RNS.LOG_ERROR)
+
+ elif packet.context == RNS.Packet.RESPONSE:
+ packed_response = self.decrypt(packet.data)
+ unpacked_response = umsgpack.unpackb(packed_response)
+ request_id = unpacked_response[0]
+
+ if unpacked_response[1] == True:
+ remove = None
+ for pending_request in self.pending_requests:
+ if pending_request.request_id == request_id:
+ response_data = unpacked_response[2]
+ remove = pending_request
+ try:
+ pending_request.response_received(response_data)
+ except Exception as e:
+ RNS.log("Error occurred while handling response. The contained exception was: "+str(e), RNS.LOG_ERROR)
+
+ break
+
+ if remove != None:
+ self.pending_requests.remove(remove)
+
+ else:
+ # TODO: Implement receiving responses as Resources
+ raise Exception("Response transfer as resource not implemented")
+
elif packet.context == RNS.Packet.LRRTT:
if not self.initiator:
self.rtt_packet(packet)
@@ -691,4 +776,58 @@ class Link:
return self.__encryption_disabled
def __str__(self):
- return RNS.prettyhexrep(self.link_id)
\ No newline at end of file
+ return RNS.prettyhexrep(self.link_id)
+
+
+class RequestReceipt():
+ FAILED = 0x00
+ SENT = 0x01
+ DELIVERED = 0x02
+ READY = 0x03
+
+ def __init__(self, link, packet_receipt, response_callback = None, failed_callback = None):
+ self.hash = packet_receipt.truncated_hash
+ self.link = link
+ self.request_id = self.hash
+
+ self.response = None
+ self.status = RequestReceipt.SENT
+ self.sent_at = time.time()
+ self.timeout = RNS.Packet.TIMEOUT
+ self.concluded_at = None
+
+ self.callbacks = RequestReceiptCallbacks()
+ self.callbacks.response = response_callback
+ self.callbacks.failed = failed_callback
+
+ self.packet_receipt = packet_receipt
+ self.packet_receipt.set_timeout_callback(self.request_timed_out)
+
+ self.link.pending_requests.append(self)
+
+
+ def request_timed_out(self, packet_receipt):
+ self.status = RequestReceipt.FAILED
+ self.concluded_at = time.time()
+ self.link.pending_requests.remove(self)
+
+ if self.callbacks.failed != None:
+ self.callbacks.failed(self)
+
+ def response_received(self, response):
+ self.response = response
+
+ self.packet_receipt.status = RNS.PacketReceipt.DELIVERED
+ self.packet_receipt.proved = True
+ self.packet_receipt.concluded_at = time.time()
+ if self.packet_receipt.callbacks.delivery != None:
+ self.packet_receipt.callbacks.delivery(self)
+
+ if self.callbacks.response != None:
+ self.callbacks.response(self)
+
+
+class RequestReceiptCallbacks:
+ def __init__(self):
+ self.response = None
+ self.failed = None
\ No newline at end of file
diff --git a/RNS/Packet.py b/RNS/Packet.py
index 4e9f70d..15cfd0f 100755
--- a/RNS/Packet.py
+++ b/RNS/Packet.py
@@ -41,7 +41,7 @@ class Packet:
HEADER_4 = 0x03 # Reserved
header_types = [HEADER_1, HEADER_2, HEADER_3, HEADER_4]
- # Data packet context types
+ # Packet context types
NONE = 0x00 # Generic data packet
RESOURCE = 0x01 # Packet is part of a resource
RESOURCE_ADV = 0x02 # Packet is a resource advertisement
@@ -68,7 +68,6 @@ class Packet:
HEADER_MAXSIZE = RNS.Reticulum.HEADER_MAXSIZE
MDU = RNS.Reticulum.MDU
- # TODO: Update this
# With an MTU of 500, the maximum of data we can
# send in a single encrypted packet is given by
# the below calculation; 383 bytes.
@@ -326,15 +325,16 @@ class PacketReceipt:
# Creates a new packet receipt from a sent packet
def __init__(self, packet):
- self.hash = packet.get_hash()
- self.sent = True
- self.sent_at = time.time()
- self.timeout = Packet.TIMEOUT
- self.proved = False
- self.status = PacketReceipt.SENT
- self.destination = packet.destination
- self.callbacks = PacketReceiptCallbacks()
- self.concluded_at = None
+ self.hash = packet.get_hash()
+ self.truncated_hash = packet.getTruncatedHash()
+ self.sent = True
+ self.sent_at = time.time()
+ self.timeout = Packet.TIMEOUT
+ self.proved = False
+ self.status = PacketReceipt.SENT
+ self.destination = packet.destination
+ self.callbacks = PacketReceiptCallbacks()
+ self.concluded_at = None
def get_status(self):
"""
diff --git a/docs/manual/.buildinfo b/docs/manual/.buildinfo
index e75ab37..17e7f72 100644
--- a/docs/manual/.buildinfo
+++ b/docs/manual/.buildinfo
@@ -1,4 +1,4 @@
# Sphinx build info version 1
# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done.
-config: 205a0b937612ce08d1a58b1cbb471256
+config: 966ae7177c1d48c9ee15971994c623b5
tags: 645f666f9bcd5a90fca523b33c5a78b7
diff --git a/docs/manual/_sources/examples.rst.txt b/docs/manual/_sources/examples.rst.txt
index 84018b9..ee50df9 100644
--- a/docs/manual/_sources/examples.rst.txt
+++ b/docs/manual/_sources/examples.rst.txt
@@ -80,6 +80,17 @@ the link has been established.
This example can also be found at ``_.
+.. _example-request:
+
+Requests & Responses
+====================
+
+The *Request* example explores sendig requests and receiving responses.
+
+.. literalinclude:: ../../Examples/Request.py
+
+This example can also be found at ``_.
+
.. _example-filetransfer:
Filetransfer
diff --git a/docs/manual/_static/documentation_options.js b/docs/manual/_static/documentation_options.js
index 5eb22a9..65c3a7b 100644
--- a/docs/manual/_static/documentation_options.js
+++ b/docs/manual/_static/documentation_options.js
@@ -1,6 +1,6 @@
var DOCUMENTATION_OPTIONS = {
URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'),
- VERSION: '0.2.1 beta',
+ VERSION: '0.2.2 beta',
LANGUAGE: 'None',
COLLAPSE_INDEX: false,
BUILDER: 'html',
diff --git a/docs/manual/examples.html b/docs/manual/examples.html
index a83ff5c..6ebac6f 100644
--- a/docs/manual/examples.html
+++ b/docs/manual/examples.html
@@ -5,7 +5,7 @@
- Examples — Reticulum Network Stack 0.2.1 beta documentation
+ Examples — Reticulum Network Stack 0.2.2 beta documentation
@@ -27,7 +27,7 @@
The Request example explores sendig requests and receiving responses.
+
##########################################################
+# This RNS example demonstrates how to set perform #
+# requests and receive responses over a link. #
+##########################################################
+
+importos
+importsys
+importtime
+importrandom
+importargparse
+importRNS
+
+# Let's define an app name. We'll use this for all
+# destinations we create. Since this echo example
+# is part of a range of example utilities, we'll put
+# them all within the app namespace "example_utilities"
+APP_NAME="example_utilities"
+
+##########################################################
+#### Server Part #########################################
+##########################################################
+
+# A reference to the latest client link that connected
+latest_client_link=None
+
+defrandom_text_generator(path,data,remote_identity_hash,requested_at):
+ texts=["They looked up","On each full moon","Becky was upset","I’ll stay away from it","The pet shop stocks everything"]
+ returntexts[random.randint(0,len(texts)-1)]
+
+# This initialisation is executed when the users chooses
+# to run as a server
+defserver(configpath):
+ # We must first initialise Reticulum
+ reticulum=RNS.Reticulum(configpath)
+
+ # Randomly create a new identity for our link example
+ server_identity=RNS.Identity()
+
+ # We create a destination that clients can connect to. We
+ # want clients to create links to this destination, so we
+ # need to create a "single" destination type.
+ server_destination=RNS.Destination(
+ server_identity,
+ RNS.Destination.IN,
+ RNS.Destination.SINGLE,
+ APP_NAME,
+ "requestexample"
+ )
+
+ # We configure a function that will get called every time
+ # a new client creates a link to this destination.
+ server_destination.set_link_established_callback(client_connected)
+
+ # We register a request handler for handling incoming
+ # requests over any established links.
+ server_destination.register_request_handler(
+ "/random/text",
+ response_generator=random_text_generator,
+ allow=RNS.Destination.ALLOW_ALL
+ )
+
+ # Everything's ready!
+ # Let's Wait for client requests or user input
+ server_loop(server_destination)
+
+defserver_loop(destination):
+ # Let the user know that everything is ready
+ RNS.log(
+ "Request example "+
+ RNS.prettyhexrep(destination.hash)+
+ " running, waiting for a connection."
+ )
+
+ RNS.log("Hit enter to manually send an announce (Ctrl-C to quit)")
+
+ # We enter a loop that runs until the users exits.
+ # If the user hits enter, we will announce our server
+ # destination on the network, which will let clients
+ # know how to create messages directed towards it.
+ whileTrue:
+ entered=input()
+ destination.announce()
+ RNS.log("Sent announce from "+RNS.prettyhexrep(destination.hash))
+
+# When a client establishes a link to our server
+# destination, this function will be called with
+# a reference to the link.
+defclient_connected(link):
+ globallatest_client_link
+
+ RNS.log("Client connected")
+ link.set_link_closed_callback(client_disconnected)
+ latest_client_link=link
+
+defclient_disconnected(link):
+ RNS.log("Client disconnected")
+
+
+##########################################################
+#### Client Part #########################################
+##########################################################
+
+# A reference to the server link
+server_link=None
+
+# This initialisation is executed when the users chooses
+# to run as a client
+defclient(destination_hexhash,configpath):
+ # We need a binary representation of the destination
+ # hash that was entered on the command line
+ try:
+ iflen(destination_hexhash)!=20:
+ raiseValueError("Destination length is invalid, must be 20 hexadecimal characters (10 bytes)")
+ destination_hash=bytes.fromhex(destination_hexhash)
+ except:
+ RNS.log("Invalid destination entered. Check your input!\n")
+ exit()
+
+ # We must first initialise Reticulum
+ reticulum=RNS.Reticulum(configpath)
+
+ # Check if we know a path to the destination
+ ifnotRNS.Transport.has_path(destination_hash):
+ RNS.log("Destination is not yet known. Requesting path and waiting for announce to arrive...")
+ RNS.Transport.request_path(destination_hash)
+ whilenotRNS.Transport.has_path(destination_hash):
+ time.sleep(0.1)
+
+ # Recall the server identity
+ server_identity=RNS.Identity.recall(destination_hash)
+
+ # Inform the user that we'll begin connecting
+ RNS.log("Establishing link with server...")
+
+ # When the server identity is known, we set
+ # up a destination
+ server_destination=RNS.Destination(
+ server_identity,
+ RNS.Destination.OUT,
+ RNS.Destination.SINGLE,
+ APP_NAME,
+ "requestexample"
+ )
+
+ # And create a link
+ link=RNS.Link(server_destination)
+
+ # We'll set up functions to inform the
+ # user when the link is established or closed
+ link.set_link_established_callback(link_established)
+ link.set_link_closed_callback(link_closed)
+
+ # Everything is set up, so let's enter a loop
+ # for the user to interact with the example
+ client_loop()
+
+defclient_loop():
+ globalserver_link
+
+ # Wait for the link to become active
+ whilenotserver_link:
+ time.sleep(0.1)
+
+ should_quit=False
+ whilenotshould_quit:
+ try:
+ print("> ",end=" ")
+ text=input()
+
+ # Check if we should quit the example
+ iftext=="quit"ortext=="q"ortext=="exit":
+ should_quit=True
+ server_link.teardown()
+
+ else:
+ server_link.request(
+ "/random/text",
+ data=None,
+ response_callback=got_response,
+ failed_callback=request_failed
+ )
+
+
+ exceptExceptionase:
+ RNS.log("Error while sending request over the link: "+str(e))
+ should_quit=True
+ server_link.teardown()
+
+defgot_response(request_receipt):
+ request_id=request_receipt.request_id
+ response=request_receipt.response
+
+ RNS.log("Got response for request "+RNS.prettyhexrep(request_id)+": "+str(response))
+
+defrequest_received(request_receipt):
+ RNS.log("The request "+RNS.prettyhexrep(request_receipt.request_id)+" was received by the remote peer.")
+
+defrequest_failed(request_receipt):
+ RNS.log("The request "+RNS.prettyhexrep(request_receipt.request_id)+" failed.")
+
+
+# This function is called when a link
+# has been established with the server
+deflink_established(link):
+ # We store a reference to the link
+ # instance for later use
+ globalserver_link
+ server_link=link
+
+ # Inform the user that the server is
+ # connected
+ RNS.log("Link established with server, hit enter to perform a request, or type in \"quit\" to quit")
+
+# When a link is closed, we'll inform the
+# user, and exit the program
+deflink_closed(link):
+ iflink.teardown_reason==RNS.Link.TIMEOUT:
+ RNS.log("The link timed out, exiting now")
+ eliflink.teardown_reason==RNS.Link.DESTINATION_CLOSED:
+ RNS.log("The link was closed by the server, exiting now")
+ else:
+ RNS.log("Link closed, exiting now")
+
+ RNS.Reticulum.exit_handler()
+ time.sleep(1.5)
+ os._exit(0)
+
+
+##########################################################
+#### Program Startup #####################################
+##########################################################
+
+# This part of the program runs at startup,
+# and parses input of from the user, and then
+# starts up the desired program mode.
+if__name__=="__main__":
+ try:
+ parser=argparse.ArgumentParser(description="Simple request/response example")
+
+ parser.add_argument(
+ "-s",
+ "--server",
+ action="store_true",
+ help="wait for incoming requests from clients"
+ )
+
+ parser.add_argument(
+ "--config",
+ action="store",
+ default=None,
+ help="path to alternative Reticulum config directory",
+ type=str
+ )
+
+ parser.add_argument(
+ "destination",
+ nargs="?",
+ default=None,
+ help="hexadecimal hash of the server destination",
+ type=str
+ )
+
+ args=parser.parse_args()
+
+ ifargs.config:
+ configarg=args.config
+ else:
+ configarg=None
+
+ ifargs.server:
+ server(configarg)
+ else:
+ if(args.destination==None):
+ print("")
+ parser.print_help()
+ print("")
+ else:
+ client(args.destination,configarg)
+
+ exceptKeyboardInterrupt:
+ print("")
+ exit()
+
path – The path for the request handler to be registered.
+
response_generator – A function or method with the signature response_generator(path, data, remote_identity_hash, requested_at) to be called. Whatever this funcion returns will be sent as a response to the requester. If the function returns None, no response will be sent.
+
allow – One of RNS.Destination.ALLOW_NONE, RNS.Destination.ALLOW_ALL or RNS.Destination.ALLOW_LIST. If RNS.Destination.ALLOW_LIST is set, the request handler will only respond to requests for identified peers in the supplied list.
+
allowed_list – A list of bytes-likeRNS.Identity hashes.
+
+
+
Raises
+
ValueError if any of the supplied arguments are invalid.
response_callback – A function or method with the signature response_callback(request_receipt) to be called when a response is received. See the Request Example for more info.
+
failed_callback – A function or method with the signature failed_callback(request_receipt) to be called when a request fails. See the Request Example for more info.
diff --git a/docs/source/conf.py b/docs/source/conf.py
index f74593e..cf462a0 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -22,7 +22,7 @@ copyright = '2021, Mark Qvist'
author = 'Mark Qvist'
# The full version, including alpha/beta/rc tags
-release = '0.2.1 beta'
+release = '0.2.2 beta'
# -- General configuration ---------------------------------------------------
diff --git a/docs/source/examples.rst b/docs/source/examples.rst
index 84018b9..ee50df9 100644
--- a/docs/source/examples.rst
+++ b/docs/source/examples.rst
@@ -80,6 +80,17 @@ the link has been established.
This example can also be found at ``_.
+.. _example-request:
+
+Requests & Responses
+====================
+
+The *Request* example explores sendig requests and receiving responses.
+
+.. literalinclude:: ../../Examples/Request.py
+
+This example can also be found at ``_.
+
.. _example-filetransfer:
Filetransfer