2021-08-19 12:10:37 +00:00
|
|
|
##########################################################
|
|
|
|
# This RNS example demonstrates how to set up a link to #
|
|
|
|
# a destination, and identify the initiator to it's peer #
|
|
|
|
##########################################################
|
|
|
|
|
|
|
|
import os
|
|
|
|
import sys
|
|
|
|
import time
|
|
|
|
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
|
|
|
|
|
|
|
|
# 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,
|
|
|
|
"identifyexample"
|
|
|
|
)
|
|
|
|
|
|
|
|
# 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)
|
|
|
|
|
|
|
|
# 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(
|
2024-10-07 08:44:18 +00:00
|
|
|
f"Link identification example {RNS.prettyhexrep(destination.hash)} running, waiting for a connection."
|
2021-08-19 12:10:37 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
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()
|
2024-10-07 08:44:18 +00:00
|
|
|
RNS.log(f"Sent announce from {RNS.prettyhexrep(destination.hash)}")
|
2021-08-19 12:10:37 +00:00
|
|
|
|
|
|
|
# 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)
|
|
|
|
link.set_packet_callback(server_packet_received)
|
|
|
|
link.set_remote_identified_callback(remote_identified)
|
|
|
|
latest_client_link = link
|
|
|
|
|
|
|
|
def client_disconnected(link):
|
|
|
|
RNS.log("Client disconnected")
|
|
|
|
|
2022-06-30 18:07:48 +00:00
|
|
|
def remote_identified(link, identity):
|
2024-10-07 08:44:18 +00:00
|
|
|
RNS.log(f"Remote identified as: {identity}")
|
2021-08-19 12:10:37 +00:00
|
|
|
|
|
|
|
def server_packet_received(message, packet):
|
|
|
|
global latest_client_link
|
|
|
|
|
|
|
|
# Get the originating identity for display
|
|
|
|
remote_peer = "unidentified peer"
|
|
|
|
if packet.link.get_remote_identity() != None:
|
|
|
|
remote_peer = str(packet.link.get_remote_identity())
|
|
|
|
|
|
|
|
# When data is received over any active link,
|
|
|
|
# it will all be directed to the last client
|
|
|
|
# that connected.
|
|
|
|
text = message.decode("utf-8")
|
|
|
|
|
2024-10-07 08:44:18 +00:00
|
|
|
RNS.log(f"Received data from {remote_peer}: {text}")
|
2021-08-19 12:10:37 +00:00
|
|
|
|
2024-10-07 08:44:18 +00:00
|
|
|
reply_text = f"I received \"{text}\" over the link from {remote_peer}"
|
2021-08-19 12:10:37 +00:00
|
|
|
reply_data = reply_text.encode("utf-8")
|
|
|
|
RNS.Packet(latest_client_link, reply_data).send()
|
|
|
|
|
|
|
|
|
|
|
|
##########################################################
|
|
|
|
#### Client Part #########################################
|
|
|
|
##########################################################
|
|
|
|
|
|
|
|
# A reference to the server link
|
|
|
|
server_link = None
|
|
|
|
|
|
|
|
# A reference to the client identity
|
|
|
|
client_identity = None
|
|
|
|
|
|
|
|
# This initialisation is executed when the users chooses
|
|
|
|
# to run as a client
|
|
|
|
def client(destination_hexhash, configpath):
|
|
|
|
global client_identity
|
|
|
|
# We need a binary representation of the destination
|
|
|
|
# hash that was entered on the command line
|
|
|
|
try:
|
2022-06-30 18:07:48 +00:00
|
|
|
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
|
|
|
|
if len(destination_hexhash) != dest_len:
|
|
|
|
raise ValueError(
|
2024-10-07 08:44:18 +00:00
|
|
|
f"Destination length is invalid, must be {dest_len} hexadecimal characters ({dest_len // 2} bytes)."
|
2022-06-30 18:07:48 +00:00
|
|
|
)
|
|
|
|
|
2021-08-19 12:10:37 +00:00
|
|
|
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)
|
|
|
|
|
|
|
|
# Create a new client identity
|
|
|
|
client_identity = RNS.Identity()
|
|
|
|
RNS.log(
|
2024-10-07 08:44:18 +00:00
|
|
|
f"Client created new identity {client_identity}"
|
2021-08-19 12:10:37 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
# 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,
|
|
|
|
"identifyexample"
|
|
|
|
)
|
|
|
|
|
|
|
|
# And create a link
|
|
|
|
link = RNS.Link(server_destination)
|
|
|
|
|
|
|
|
# We set a callback that will get executed
|
|
|
|
# every time a packet is received over the
|
|
|
|
# link
|
|
|
|
link.set_packet_callback(client_packet_received)
|
|
|
|
|
|
|
|
# We'll also 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()
|
|
|
|
|
|
|
|
# If not, send the entered text over the link
|
|
|
|
if text != "":
|
|
|
|
data = text.encode("utf-8")
|
|
|
|
if len(data) <= RNS.Link.MDU:
|
|
|
|
RNS.Packet(server_link, data).send()
|
|
|
|
else:
|
|
|
|
RNS.log(
|
2024-10-07 08:44:18 +00:00
|
|
|
f"Cannot send this packet, the data size of {len(data)} bytes exceeds the link packet MDU of {RNS.Link.MDU} bytes",
|
2021-08-19 12:10:37 +00:00
|
|
|
RNS.LOG_ERROR
|
|
|
|
)
|
|
|
|
|
|
|
|
except Exception as e:
|
2024-10-07 08:44:18 +00:00
|
|
|
RNS.log(f"Error while sending data over the link: {e}")
|
2021-08-19 12:10:37 +00:00
|
|
|
should_quit = True
|
|
|
|
server_link.teardown()
|
|
|
|
|
|
|
|
# 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, client_identity
|
|
|
|
server_link = link
|
|
|
|
|
|
|
|
# Inform the user that the server is
|
|
|
|
# connected
|
|
|
|
RNS.log("Link established with server, identifying to remote peer...")
|
|
|
|
|
|
|
|
link.identify(client_identity)
|
|
|
|
|
|
|
|
# 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)
|
|
|
|
|
|
|
|
# When a packet is received over the link, we
|
|
|
|
# simply print out the data.
|
|
|
|
def client_packet_received(message, packet):
|
|
|
|
text = message.decode("utf-8")
|
2024-10-07 08:44:18 +00:00
|
|
|
RNS.log(f"Received data on the link: {text}")
|
2021-08-19 12:10:37 +00:00
|
|
|
print("> ", end=" ")
|
|
|
|
sys.stdout.flush()
|
|
|
|
|
|
|
|
|
|
|
|
##########################################################
|
|
|
|
#### 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 link example")
|
|
|
|
|
|
|
|
parser.add_argument(
|
|
|
|
"-s",
|
|
|
|
"--server",
|
|
|
|
action="store_true",
|
|
|
|
help="wait for incoming link 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()
|