Indentation and formatting cleanup

This commit is contained in:
Mark Qvist 2020-08-13 12:15:56 +02:00
parent e4dfd052e6
commit bd098c338a
17 changed files with 4477 additions and 4477 deletions

View File

@ -15,50 +15,50 @@ APP_NAME = "example_utilitites"
# This initialisation is executed when the program is started # This initialisation is executed when the program is started
def program_setup(configpath, channel=None): def program_setup(configpath, channel=None):
# We must first initialise Reticulum # We must first initialise Reticulum
reticulum = RNS.Reticulum(configpath) reticulum = RNS.Reticulum(configpath)
# If the user did not select a "channel" we use # If the user did not select a "channel" we use
# a default one called "public_information". # a default one called "public_information".
# This "channel" is added to the destination name- # This "channel" is added to the destination name-
# space, so the user can select different broadcast # space, so the user can select different broadcast
# channels. # channels.
if channel == None: if channel == None:
channel = "public_information" channel = "public_information"
# We create a PLAIN destination. This is an uncencrypted endpoint # We create a PLAIN destination. This is an uncencrypted endpoint
# that anyone can listen to and send information to. # that anyone can listen to and send information to.
broadcast_destination = RNS.Destination(None, RNS.Destination.IN, RNS.Destination.PLAIN, APP_NAME, "broadcast", channel) broadcast_destination = RNS.Destination(None, RNS.Destination.IN, RNS.Destination.PLAIN, APP_NAME, "broadcast", channel)
# We specify a callback that will get called every time # We specify a callback that will get called every time
# the destination receives data. # the destination receives data.
broadcast_destination.packet_callback(packet_callback) broadcast_destination.packet_callback(packet_callback)
# Everything's ready! # Everything's ready!
# Let's hand over control to the main loop # Let's hand over control to the main loop
broadcastLoop(broadcast_destination) broadcastLoop(broadcast_destination)
def packet_callback(data, packet): def packet_callback(data, packet):
# Simply print out the received data # Simply print out the received data
print("") print("")
print("Received data: "+data.decode("utf-8")+"\r\n> ", end="") print("Received data: "+data.decode("utf-8")+"\r\n> ", end="")
sys.stdout.flush() sys.stdout.flush()
def broadcastLoop(destination): def broadcastLoop(destination):
# Let the user know that everything is ready # Let the user know that everything is ready
RNS.log("Broadcast example "+RNS.prettyhexrep(destination.hash)+" running, enter text and hit enter to broadcast (Ctrl-C to quit)") RNS.log("Broadcast example "+RNS.prettyhexrep(destination.hash)+" running, enter text and hit enter to broadcast (Ctrl-C to quit)")
# We enter a loop that runs until the users exits. # We enter a loop that runs until the users exits.
# If the user hits enter, we will send the information # If the user hits enter, we will send the information
# that the user entered into the prompt. # that the user entered into the prompt.
while True: while True:
print("> ", end="") print("> ", end="")
entered = input() entered = input()
if entered != "": if entered != "":
data = entered.encode("utf-8") data = entered.encode("utf-8")
packet = RNS.Packet(destination, data) packet = RNS.Packet(destination, data)
packet.send() packet.send()
@ -70,24 +70,24 @@ def broadcastLoop(destination):
# and parses input from the user, and then starts # and parses input from the user, and then starts
# the program. # the program.
if __name__ == "__main__": if __name__ == "__main__":
try: try:
parser = argparse.ArgumentParser(description="Reticulum example that demonstrates sending and receiving unencrypted broadcasts") parser = argparse.ArgumentParser(description="Reticulum example that demonstrates sending and receiving unencrypted broadcasts")
parser.add_argument("--config", action="store", default=None, help="path to alternative Reticulum config directory", type=str) parser.add_argument("--config", action="store", default=None, help="path to alternative Reticulum config directory", type=str)
parser.add_argument("--channel", action="store", default=None, help="path to alternative Reticulum config directory", type=str) parser.add_argument("--channel", action="store", default=None, help="path to alternative Reticulum config directory", type=str)
args = parser.parse_args() args = parser.parse_args()
if args.config: if args.config:
configarg = args.config configarg = args.config
else: else:
configarg = None configarg = None
if args.channel: if args.channel:
channelarg = args.channel channelarg = args.channel
else: else:
channelarg = None channelarg = None
program_setup(configarg, channelarg) program_setup(configarg, channelarg)
except KeyboardInterrupt: except KeyboardInterrupt:
print("") print("")
exit() exit()

View File

@ -22,56 +22,56 @@ APP_NAME = "example_utilitites"
# This initialisation is executed when the users chooses # This initialisation is executed when the users chooses
# to run as a server # to run as a server
def server(configpath): def server(configpath):
# We must first initialise Reticulum # We must first initialise Reticulum
reticulum = RNS.Reticulum(configpath) reticulum = RNS.Reticulum(configpath)
# Randomly create a new identity for our echo server # Randomly create a new identity for our echo server
server_identity = RNS.Identity() server_identity = RNS.Identity()
# We create a destination that clients can query. We want # We create a destination that clients can query. We want
# to be able to verify echo replies to our clients, so we # to be able to verify echo replies to our clients, so we
# create a "single" destination that can receive encrypted # create a "single" destination that can receive encrypted
# messages. This way the client can send a request and be # messages. This way the client can send a request and be
# certain that no-one else than this destination was able # certain that no-one else than this destination was able
# to read it. # to read it.
echo_destination = RNS.Destination(server_identity, RNS.Destination.IN, RNS.Destination.SINGLE, APP_NAME, "echo", "request") echo_destination = RNS.Destination(server_identity, RNS.Destination.IN, RNS.Destination.SINGLE, APP_NAME, "echo", "request")
# We configure the destination to automatically prove all # We configure the destination to automatically prove all
# packets adressed to it. By doing this, RNS will automatically # packets adressed to it. By doing this, RNS will automatically
# generate a proof for each incoming packet and transmit it # generate a proof for each incoming packet and transmit it
# back to the sender of that packet. # back to the sender of that packet.
echo_destination.set_proof_strategy(RNS.Destination.PROVE_ALL) echo_destination.set_proof_strategy(RNS.Destination.PROVE_ALL)
# Tell the destination which function in our program to # Tell the destination which function in our program to
# run when a packet is received. We do this so we can # run when a packet is received. We do this so we can
# print a log message when the server receives a request # print a log message when the server receives a request
echo_destination.packet_callback(server_callback) echo_destination.packet_callback(server_callback)
# Everything's ready! # Everything's ready!
# Let's Wait for client requests or user input # Let's Wait for client requests or user input
announceLoop(echo_destination) announceLoop(echo_destination)
def announceLoop(destination): def announceLoop(destination):
# Let the user know that everything is ready # Let the user know that everything is ready
RNS.log("Echo server "+RNS.prettyhexrep(destination.hash)+" running, hit enter to manually send an announce (Ctrl-C to quit)") RNS.log("Echo server "+RNS.prettyhexrep(destination.hash)+" running, hit enter to manually send an announce (Ctrl-C to quit)")
# We enter a loop that runs until the users exits. # We enter a loop that runs until the users exits.
# If the user hits enter, we will announce our server # If the user hits enter, we will announce our server
# destination on the network, which will let clients # destination on the network, which will let clients
# know how to create messages directed towards it. # know how to create messages directed towards it.
while True: while True:
entered = input() entered = input()
destination.announce() destination.announce()
RNS.log("Sent announce from "+RNS.prettyhexrep(destination.hash)) RNS.log("Sent announce from "+RNS.prettyhexrep(destination.hash))
def server_callback(message, packet): def server_callback(message, packet):
# Tell the user that we received an echo request, and # Tell the user that we received an echo request, and
# that we are going to send a reply to the requester. # that we are going to send a reply to the requester.
# Sending the proof is handled automatically, since we # Sending the proof is handled automatically, since we
# set up the destination to prove all incoming packets. # set up the destination to prove all incoming packets.
RNS.log("Received packet from echo client, proof sent") RNS.log("Received packet from echo client, proof sent")
########################################################## ##########################################################
@ -81,103 +81,103 @@ def server_callback(message, packet):
# This initialisation is executed when the users chooses # This initialisation is executed when the users chooses
# to run as a client # to run as a client
def client(destination_hexhash, configpath, timeout=None): def client(destination_hexhash, configpath, timeout=None):
# We need a binary representation of the destination # We need a binary representation of the destination
# hash that was entered on the command line # hash that was entered on the command line
try: try:
if len(destination_hexhash) != 20: if len(destination_hexhash) != 20:
raise ValueError("Destination length is invalid, must be 20 hexadecimal characters (10 bytes)") raise ValueError("Destination length is invalid, must be 20 hexadecimal characters (10 bytes)")
destination_hash = bytes.fromhex(destination_hexhash) destination_hash = bytes.fromhex(destination_hexhash)
except: except:
RNS.log("Invalid destination entered. Check your input!\n") RNS.log("Invalid destination entered. Check your input!\n")
exit() exit()
# We must first initialise Reticulum # We must first initialise Reticulum
reticulum = RNS.Reticulum(configpath) reticulum = RNS.Reticulum(configpath)
# We override the loglevel to provide feedback when # We override the loglevel to provide feedback when
# an announce is received # an announce is received
if RNS.loglevel < RNS.LOG_INFO: if RNS.loglevel < RNS.LOG_INFO:
RNS.loglevel = RNS.LOG_INFO RNS.loglevel = RNS.LOG_INFO
# Tell the user that the client is ready! # Tell the user that the client is ready!
RNS.log("Echo client ready, hit enter to send echo request to "+destination_hexhash+" (Ctrl-C to quit)") RNS.log("Echo client ready, hit enter to send echo request to "+destination_hexhash+" (Ctrl-C to quit)")
# We enter a loop that runs until the user exits. # We enter a loop that runs until the user exits.
# If the user hits enter, we will try to send an # If the user hits enter, we will try to send an
# echo request to the destination specified on the # echo request to the destination specified on the
# command line. # command line.
while True: while True:
input() input()
# Let's first check if RNS knows a path to the destination. # Let's first check if RNS knows a path to the destination.
# If it does, we'll load the server identity and create a packet # If it does, we'll load the server identity and create a packet
if RNS.Transport.hasPath(destination_hash): if RNS.Transport.hasPath(destination_hash):
# To address the server, we need to know it's public # To address the server, we need to know it's public
# key, so we check if Reticulum knows this destination. # key, so we check if Reticulum knows this destination.
# This is done by calling the "recall" method of the # This is done by calling the "recall" method of the
# Identity module. If the destination is known, it will # Identity module. If the destination is known, it will
# return an Identity instance that can be used in # return an Identity instance that can be used in
# outgoing destinations. # outgoing destinations.
server_identity = RNS.Identity.recall(destination_hash) server_identity = RNS.Identity.recall(destination_hash)
# We got the correct identity instance from the # We got the correct identity instance from the
# recall method, so let's create an outgoing # recall method, so let's create an outgoing
# destination. We use the naming convention: # destination. We use the naming convention:
# example_utilities.echo.request # example_utilities.echo.request
# This matches the naming we specified in the # This matches the naming we specified in the
# server part of the code. # server part of the code.
request_destination = RNS.Destination(server_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, APP_NAME, "echo", "request") request_destination = RNS.Destination(server_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, APP_NAME, "echo", "request")
# The destination is ready, so let's create a packet. # The destination is ready, so let's create a packet.
# We set the destination to the request_destination # We set the destination to the request_destination
# that was just created, and the only data we add # that was just created, and the only data we add
# is a random hash. # is a random hash.
echo_request = RNS.Packet(request_destination, RNS.Identity.getRandomHash()) echo_request = RNS.Packet(request_destination, RNS.Identity.getRandomHash())
# Send the packet! If the packet is successfully # Send the packet! If the packet is successfully
# sent, it will return a PacketReceipt instance. # sent, it will return a PacketReceipt instance.
packet_receipt = echo_request.send() packet_receipt = echo_request.send()
# If the user specified a timeout, we set this # If the user specified a timeout, we set this
# timeout on the packet receipt, and configure # timeout on the packet receipt, and configure
# a callback function, that will get called if # a callback function, that will get called if
# the packet times out. # the packet times out.
if timeout != None: if timeout != None:
packet_receipt.set_timeout(timeout) packet_receipt.set_timeout(timeout)
packet_receipt.timeout_callback(packet_timed_out) packet_receipt.timeout_callback(packet_timed_out)
# We can then set a delivery callback on the receipt. # We can then set a delivery callback on the receipt.
# This will get automatically called when a proof for # This will get automatically called when a proof for
# this specific packet is received from the destination. # this specific packet is received from the destination.
packet_receipt.delivery_callback(packet_delivered) packet_receipt.delivery_callback(packet_delivered)
# Tell the user that the echo request was sent # Tell the user that the echo request was sent
RNS.log("Sent echo request to "+RNS.prettyhexrep(request_destination.hash)) RNS.log("Sent echo request to "+RNS.prettyhexrep(request_destination.hash))
else: else:
# If we do not know this destination, tell the # If we do not know this destination, tell the
# user to wait for an announce to arrive. # user to wait for an announce to arrive.
RNS.log("Destination is not yet known. Requesting path...") RNS.log("Destination is not yet known. Requesting path...")
RNS.Transport.requestPath(destination_hash) RNS.Transport.requestPath(destination_hash)
# This function is called when our reply destination # This function is called when our reply destination
# receives a proof packet. # receives a proof packet.
def packet_delivered(receipt): def packet_delivered(receipt):
if receipt.status == RNS.PacketReceipt.DELIVERED: if receipt.status == RNS.PacketReceipt.DELIVERED:
rtt = receipt.rtt() rtt = receipt.rtt()
if (rtt >= 1): if (rtt >= 1):
rtt = round(rtt, 3) rtt = round(rtt, 3)
rttstring = str(rtt)+" seconds" rttstring = str(rtt)+" seconds"
else: else:
rtt = round(rtt*1000, 3) rtt = round(rtt*1000, 3)
rttstring = str(rtt)+" milliseconds" rttstring = str(rtt)+" milliseconds"
RNS.log("Valid reply received from "+RNS.prettyhexrep(receipt.destination.hash)+", round-trip time is "+rttstring) RNS.log("Valid reply received from "+RNS.prettyhexrep(receipt.destination.hash)+", round-trip time is "+rttstring)
# This function is called if a packet times out. # This function is called if a packet times out.
def packet_timed_out(receipt): def packet_timed_out(receipt):
if receipt.status == RNS.PacketReceipt.FAILED: if receipt.status == RNS.PacketReceipt.FAILED:
RNS.log("Packet "+RNS.prettyhexrep(receipt.hash)+" timed out") RNS.log("Packet "+RNS.prettyhexrep(receipt.hash)+" timed out")
########################################################## ##########################################################
@ -188,36 +188,36 @@ def packet_timed_out(receipt):
# and parses input from the user, and then starts # and parses input from the user, and then starts
# the desired program mode. # the desired program mode.
if __name__ == "__main__": if __name__ == "__main__":
try: try:
parser = argparse.ArgumentParser(description="Simple echo server and client utility") parser = argparse.ArgumentParser(description="Simple echo server and client utility")
parser.add_argument("-s", "--server", action="store_true", help="wait for incoming packets from clients") parser.add_argument("-s", "--server", action="store_true", help="wait for incoming packets from clients")
parser.add_argument("-t", "--timeout", action="store", metavar="s", default=None, help="set a reply timeout in seconds", type=float) parser.add_argument("-t", "--timeout", action="store", metavar="s", default=None, help="set a reply timeout in seconds", type=float)
parser.add_argument("--config", action="store", default=None, help="path to alternative Reticulum config directory", type=str) 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) parser.add_argument("destination", nargs="?", default=None, help="hexadecimal hash of the server destination", type=str)
args = parser.parse_args() args = parser.parse_args()
if args.server: if args.server:
configarg=None configarg=None
if args.config: if args.config:
configarg = args.config configarg = args.config
server(configarg) server(configarg)
else: else:
if args.config: if args.config:
configarg = args.config configarg = args.config
else: else:
configarg = None configarg = None
if args.timeout: if args.timeout:
timeoutarg = float(args.timeout) timeoutarg = float(args.timeout)
else: else:
timeoutarg = None timeoutarg = None
if (args.destination == None): if (args.destination == None):
print("") print("")
parser.print_help() parser.print_help()
print("") print("")
else: else:
client(args.destination, configarg, timeout=timeoutarg) client(args.destination, configarg, timeout=timeoutarg)
except KeyboardInterrupt: except KeyboardInterrupt:
print("") print("")
exit() exit()

View File

@ -42,132 +42,132 @@ serve_path = None
# This initialisation is executed when the users chooses # This initialisation is executed when the users chooses
# to run as a server # to run as a server
def server(configpath, path): def server(configpath, path):
# We must first initialise Reticulum # We must first initialise Reticulum
reticulum = RNS.Reticulum(configpath) reticulum = RNS.Reticulum(configpath)
# Randomly create a new identity for our file server # Randomly create a new identity for our file server
server_identity = RNS.Identity() server_identity = RNS.Identity()
global serve_path global serve_path
serve_path = path serve_path = path
# We create a destination that clients can connect to. We # We create a destination that clients can connect to. We
# want clients to create links to this destination, so we # want clients to create links to this destination, so we
# need to create a "single" destination type. # need to create a "single" destination type.
server_destination = RNS.Destination(server_identity, RNS.Destination.IN, RNS.Destination.SINGLE, APP_NAME, "filetransfer", "server") server_destination = RNS.Destination(server_identity, RNS.Destination.IN, RNS.Destination.SINGLE, APP_NAME, "filetransfer", "server")
# We configure a function that will get called every time # We configure a function that will get called every time
# a new client creates a link to this destination. # a new client creates a link to this destination.
server_destination.link_established_callback(client_connected) server_destination.link_established_callback(client_connected)
# Everything's ready! # Everything's ready!
# Let's Wait for client requests or user input # Let's Wait for client requests or user input
announceLoop(server_destination) announceLoop(server_destination)
def announceLoop(destination): def announceLoop(destination):
# Let the user know that everything is ready # Let the user know that everything is ready
RNS.log("File server "+RNS.prettyhexrep(destination.hash)+" running") RNS.log("File server "+RNS.prettyhexrep(destination.hash)+" running")
RNS.log("Hit enter to manually send an announce (Ctrl-C to quit)") RNS.log("Hit enter to manually send an announce (Ctrl-C to quit)")
# We enter a loop that runs until the users exits. # We enter a loop that runs until the users exits.
# If the user hits enter, we will announce our server # If the user hits enter, we will announce our server
# destination on the network, which will let clients # destination on the network, which will let clients
# know how to create messages directed towards it. # know how to create messages directed towards it.
while True: while True:
entered = input() entered = input()
destination.announce() destination.announce()
RNS.log("Sent announce from "+RNS.prettyhexrep(destination.hash)) RNS.log("Sent announce from "+RNS.prettyhexrep(destination.hash))
# Here's a convenience function for listing all files # Here's a convenience function for listing all files
# in our served directory # in our served directory
def list_files(): def list_files():
# We add all entries from the directory that are # We add all entries from the directory that are
# actual files, and does not start with "." # actual files, and does not start with "."
global serve_path global serve_path
return [file for file in os.listdir(serve_path) if os.path.isfile(os.path.join(serve_path, file)) and file[:1] != "."] return [file for file in os.listdir(serve_path) if os.path.isfile(os.path.join(serve_path, file)) and file[:1] != "."]
# When a client establishes a link to our server # When a client establishes a link to our server
# destination, this function will be called with # destination, this function will be called with
# a reference to the link. We then send the client # a reference to the link. We then send the client
# a list of files hosted on the server. # a list of files hosted on the server.
def client_connected(link): def client_connected(link):
# Check if the served directory still exists # Check if the served directory still exists
if os.path.isdir(serve_path): if os.path.isdir(serve_path):
RNS.log("Client connected, sending file list...") RNS.log("Client connected, sending file list...")
link.link_closed_callback(client_disconnected) link.link_closed_callback(client_disconnected)
# We pack a list of files for sending in a packet # We pack a list of files for sending in a packet
data = umsgpack.packb(list_files()) data = umsgpack.packb(list_files())
# Check the size of the packed data # Check the size of the packed data
if len(data) <= RNS.Link.MDU: if len(data) <= RNS.Link.MDU:
# If it fits in one packet, we will just # If it fits in one packet, we will just
# send it as a single packet over the link. # send it as a single packet over the link.
list_packet = RNS.Packet(link, data) list_packet = RNS.Packet(link, data)
list_receipt = list_packet.send() list_receipt = list_packet.send()
list_receipt.set_timeout(APP_TIMEOUT) list_receipt.set_timeout(APP_TIMEOUT)
list_receipt.delivery_callback(list_delivered) list_receipt.delivery_callback(list_delivered)
list_receipt.timeout_callback(list_timeout) list_receipt.timeout_callback(list_timeout)
else: else:
RNS.log("Too many files in served directory!", RNS.LOG_ERROR) RNS.log("Too many files in served directory!", RNS.LOG_ERROR)
RNS.log("You should implement a function to split the filelist over multiple packets.", RNS.LOG_ERROR) RNS.log("You should implement a function to split the filelist over multiple packets.", RNS.LOG_ERROR)
RNS.log("Hint: The client already supports it :)", RNS.LOG_ERROR) RNS.log("Hint: The client already supports it :)", RNS.LOG_ERROR)
# After this, we're just going to keep the link # After this, we're just going to keep the link
# open until the client requests a file. We'll # open until the client requests a file. We'll
# configure a function that get's called when # configure a function that get's called when
# the client sends a packet with a file request. # the client sends a packet with a file request.
link.packet_callback(client_request) link.packet_callback(client_request)
else: else:
RNS.log("Client connected, but served path no longer exists!", RNS.LOG_ERROR) RNS.log("Client connected, but served path no longer exists!", RNS.LOG_ERROR)
link.teardown() link.teardown()
def client_disconnected(link): def client_disconnected(link):
RNS.log("Client disconnected") RNS.log("Client disconnected")
def client_request(message, packet): def client_request(message, packet):
global serve_path global serve_path
filename = message.decode("utf-8") filename = message.decode("utf-8")
if filename in list_files(): if filename in list_files():
try: try:
# If we have the requested file, we'll # If we have the requested file, we'll
# read it and pack it as a resource # read it and pack it as a resource
RNS.log("Client requested \""+filename+"\"") RNS.log("Client requested \""+filename+"\"")
file = open(os.path.join(serve_path, filename), "rb") file = open(os.path.join(serve_path, filename), "rb")
file_resource = RNS.Resource(file, packet.link, callback=resource_sending_concluded) file_resource = RNS.Resource(file, packet.link, callback=resource_sending_concluded)
file_resource.filename = filename file_resource.filename = filename
except Exception as e: except Exception as e:
# If somethign went wrong, we close # If somethign went wrong, we close
# the link # the link
RNS.log("Error while reading file \""+filename+"\"", RNS.LOG_ERROR) RNS.log("Error while reading file \""+filename+"\"", RNS.LOG_ERROR)
packet.link.teardown() packet.link.teardown()
raise e raise e
else: else:
# If we don't have it, we close the link # If we don't have it, we close the link
RNS.log("Client requested an unknown file") RNS.log("Client requested an unknown file")
packet.link.teardown() packet.link.teardown()
# This function is called on the server when a # This function is called on the server when a
# resource transfer concludes. # resource transfer concludes.
def resource_sending_concluded(resource): def resource_sending_concluded(resource):
if hasattr(resource, "filename"): if hasattr(resource, "filename"):
name = resource.filename name = resource.filename
else: else:
name = "resource" name = "resource"
if resource.status == RNS.Resource.COMPLETE: if resource.status == RNS.Resource.COMPLETE:
RNS.log("Done sending \""+name+"\" to client") RNS.log("Done sending \""+name+"\" to client")
elif resource.status == RNS.Resource.FAILED: elif resource.status == RNS.Resource.FAILED:
RNS.log("Sending \""+name+"\" to client failed") RNS.log("Sending \""+name+"\" to client failed")
def list_delivered(receipt): def list_delivered(receipt):
RNS.log("The file list was received by the client") RNS.log("The file list was received by the client")
def list_timeout(receipt): def list_timeout(receipt):
RNS.log("Sending list to client timed out, closing this link") RNS.log("Sending list to client timed out, closing this link")
link = receipt.destination link = receipt.destination
link.teardown() link.teardown()
########################################################## ##########################################################
#### Client Part ######################################### #### Client Part #########################################
@ -194,116 +194,116 @@ file_size = 0
# This initialisation is executed when the users chooses # This initialisation is executed when the users chooses
# to run as a client # to run as a client
def client(destination_hexhash, configpath): def client(destination_hexhash, configpath):
# We need a binary representation of the destination # We need a binary representation of the destination
# hash that was entered on the command line # hash that was entered on the command line
try: try:
if len(destination_hexhash) != 20: if len(destination_hexhash) != 20:
raise ValueError("Destination length is invalid, must be 20 hexadecimal characters (10 bytes)") raise ValueError("Destination length is invalid, must be 20 hexadecimal characters (10 bytes)")
destination_hash = bytes.fromhex(destination_hexhash) destination_hash = bytes.fromhex(destination_hexhash)
except: except:
RNS.log("Invalid destination entered. Check your input!\n") RNS.log("Invalid destination entered. Check your input!\n")
exit() exit()
# We must first initialise Reticulum # We must first initialise Reticulum
reticulum = RNS.Reticulum(configpath) reticulum = RNS.Reticulum(configpath)
# Check if we know a path to the destination # Check if we know a path to the destination
if not RNS.Transport.hasPath(destination_hash): if not RNS.Transport.hasPath(destination_hash):
RNS.log("Destination is not yet known. Requesting path and waiting for announce to arrive...") RNS.log("Destination is not yet known. Requesting path and waiting for announce to arrive...")
RNS.Transport.requestPath(destination_hash) RNS.Transport.requestPath(destination_hash)
while not RNS.Transport.hasPath(destination_hash): while not RNS.Transport.hasPath(destination_hash):
time.sleep(0.1) time.sleep(0.1)
# Recall the server identity # Recall the server identity
server_identity = RNS.Identity.recall(destination_hash) server_identity = RNS.Identity.recall(destination_hash)
# Inform the user that we'll begin connecting # Inform the user that we'll begin connecting
RNS.log("Establishing link with server...") RNS.log("Establishing link with server...")
# When the server identity is known, we set # When the server identity is known, we set
# up a destination # up a destination
server_destination = RNS.Destination(server_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, APP_NAME, "filetransfer", "server") server_destination = RNS.Destination(server_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, APP_NAME, "filetransfer", "server")
# We also want to automatically prove incoming packets # We also want to automatically prove incoming packets
server_destination.set_proof_strategy(RNS.Destination.PROVE_ALL) server_destination.set_proof_strategy(RNS.Destination.PROVE_ALL)
# And create a link # And create a link
link = RNS.Link(server_destination) link = RNS.Link(server_destination)
# We expect any normal data packets on the link # We expect any normal data packets on the link
# to contain a list of served files, so we set # to contain a list of served files, so we set
# a callback accordingly # a callback accordingly
link.packet_callback(filelist_received) link.packet_callback(filelist_received)
# We'll also set up functions to inform the # We'll also set up functions to inform the
# user when the link is established or closed # user when the link is established or closed
link.link_established_callback(link_established) link.link_established_callback(link_established)
link.link_closed_callback(link_closed) link.link_closed_callback(link_closed)
# And set the link to automatically begin # And set the link to automatically begin
# downloading advertised resources # downloading advertised resources
link.set_resource_strategy(RNS.Link.ACCEPT_ALL) link.set_resource_strategy(RNS.Link.ACCEPT_ALL)
link.resource_started_callback(download_began) link.resource_started_callback(download_began)
link.resource_concluded_callback(download_concluded) link.resource_concluded_callback(download_concluded)
menu() menu()
# Requests the specified file from the server # Requests the specified file from the server
def download(filename): def download(filename):
global server_link, menu_mode, current_filename, transfer_size, download_started global server_link, menu_mode, current_filename, transfer_size, download_started
current_filename = filename current_filename = filename
download_started = 0 download_started = 0
transfer_size = 0 transfer_size = 0
# We just create a packet containing the # We just create a packet containing the
# requested filename, and send it down the # requested filename, and send it down the
# link. We also specify we don't need a # link. We also specify we don't need a
# packet receipt. # packet receipt.
request_packet = RNS.Packet(server_link, filename.encode("utf-8"), create_receipt=False) request_packet = RNS.Packet(server_link, filename.encode("utf-8"), create_receipt=False)
request_packet.send() request_packet.send()
print("") print("")
print(("Requested \""+filename+"\" from server, waiting for download to begin...")) print(("Requested \""+filename+"\" from server, waiting for download to begin..."))
menu_mode = "download_started" menu_mode = "download_started"
# This function runs a simple menu for the user # This function runs a simple menu for the user
# to select which files to download, or quit # to select which files to download, or quit
menu_mode = None menu_mode = None
def menu(): def menu():
global server_files, server_link global server_files, server_link
# Wait until we have a filelist # Wait until we have a filelist
while len(server_files) == 0: while len(server_files) == 0:
time.sleep(0.1) time.sleep(0.1)
RNS.log("Ready!") RNS.log("Ready!")
time.sleep(0.5) time.sleep(0.5)
global menu_mode global menu_mode
menu_mode = "main" menu_mode = "main"
should_quit = False should_quit = False
while (not should_quit): while (not should_quit):
print_menu() print_menu()
while not menu_mode == "main": while not menu_mode == "main":
# Wait # Wait
time.sleep(0.25) time.sleep(0.25)
user_input = input() user_input = input()
if user_input == "q" or user_input == "quit" or user_input == "exit": if user_input == "q" or user_input == "quit" or user_input == "exit":
should_quit = True should_quit = True
print("") print("")
else: else:
if user_input in server_files: if user_input in server_files:
download(user_input) download(user_input)
else: else:
try: try:
if 0 <= int(user_input) < len(server_files): if 0 <= int(user_input) < len(server_files):
download(server_files[int(user_input)]) download(server_files[int(user_input)])
except: except:
pass pass
if should_quit: if should_quit:
server_link.teardown() server_link.teardown()
# Prints out menus or screens for the # Prints out menus or screens for the
# various states of the client program. # various states of the client program.
@ -311,145 +311,145 @@ def menu():
# I won't go into detail here. Just # I won't go into detail here. Just
# strings basically. # strings basically.
def print_menu(): def print_menu():
global menu_mode, download_time, download_started, download_finished, transfer_size, file_size global menu_mode, download_time, download_started, download_finished, transfer_size, file_size
if menu_mode == "main": if menu_mode == "main":
clear_screen() clear_screen()
print_filelist() print_filelist()
print("") print("")
print("Select a file to download by entering name or number, or q to quit") print("Select a file to download by entering name or number, or q to quit")
print(("> "), end=' ') print(("> "), end=' ')
elif menu_mode == "download_started": elif menu_mode == "download_started":
download_began = time.time() download_began = time.time()
while menu_mode == "download_started": while menu_mode == "download_started":
time.sleep(0.1) time.sleep(0.1)
if time.time() > download_began+APP_TIMEOUT: if time.time() > download_began+APP_TIMEOUT:
print("The download timed out") print("The download timed out")
time.sleep(1) time.sleep(1)
server_link.teardown() server_link.teardown()
if menu_mode == "downloading": if menu_mode == "downloading":
print("Download started") print("Download started")
print("") print("")
while menu_mode == "downloading": while menu_mode == "downloading":
global current_download global current_download
percent = round(current_download.progress() * 100.0, 1) percent = round(current_download.progress() * 100.0, 1)
print(("\rProgress: "+str(percent)+" % "), end=' ') print(("\rProgress: "+str(percent)+" % "), end=' ')
sys.stdout.flush() sys.stdout.flush()
time.sleep(0.1) time.sleep(0.1)
if menu_mode == "save_error": if menu_mode == "save_error":
print(("\rProgress: 100.0 %"), end=' ') print(("\rProgress: 100.0 %"), end=' ')
sys.stdout.flush() sys.stdout.flush()
print("") print("")
print("Could not write downloaded file to disk") print("Could not write downloaded file to disk")
current_download.status = RNS.Resource.FAILED current_download.status = RNS.Resource.FAILED
menu_mode = "download_concluded" menu_mode = "download_concluded"
if menu_mode == "download_concluded": if menu_mode == "download_concluded":
if current_download.status == RNS.Resource.COMPLETE: if current_download.status == RNS.Resource.COMPLETE:
print(("\rProgress: 100.0 %"), end=' ') print(("\rProgress: 100.0 %"), end=' ')
sys.stdout.flush() sys.stdout.flush()
# Print statistics # Print statistics
hours, rem = divmod(download_time, 3600) hours, rem = divmod(download_time, 3600)
minutes, seconds = divmod(rem, 60) minutes, seconds = divmod(rem, 60)
timestring = "{:0>2}:{:0>2}:{:05.2f}".format(int(hours),int(minutes),seconds) timestring = "{:0>2}:{:0>2}:{:05.2f}".format(int(hours),int(minutes),seconds)
print("") print("")
print("") print("")
print("--- Statistics -----") print("--- Statistics -----")
print("\tTime taken : "+timestring) print("\tTime taken : "+timestring)
print("\tFile size : "+size_str(file_size)) print("\tFile size : "+size_str(file_size))
print("\tData transferred : "+size_str(transfer_size)) print("\tData transferred : "+size_str(transfer_size))
print("\tEffective rate : "+size_str(file_size/download_time, suffix='b')+"/s") print("\tEffective rate : "+size_str(file_size/download_time, suffix='b')+"/s")
print("\tTransfer rate : "+size_str(transfer_size/download_time, suffix='b')+"/s") print("\tTransfer rate : "+size_str(transfer_size/download_time, suffix='b')+"/s")
print("") print("")
print("The download completed! Press enter to return to the menu.") print("The download completed! Press enter to return to the menu.")
print("") print("")
input() input()
else: else:
print("") print("")
print("The download failed! Press enter to return to the menu.") print("The download failed! Press enter to return to the menu.")
input() input()
current_download = None current_download = None
menu_mode = "main" menu_mode = "main"
print_menu() print_menu()
# This function prints out a list of files # This function prints out a list of files
# on the connected server. # on the connected server.
def print_filelist(): def print_filelist():
global server_files global server_files
print("Files on server:") print("Files on server:")
for index,file in enumerate(server_files): for index,file in enumerate(server_files):
print("\t("+str(index)+")\t"+file) print("\t("+str(index)+")\t"+file)
def filelist_received(filelist_data, packet): def filelist_received(filelist_data, packet):
global server_files, menu_mode global server_files, menu_mode
try: try:
# Unpack the list and extend our # Unpack the list and extend our
# local list of available files # local list of available files
filelist = umsgpack.unpackb(filelist_data) filelist = umsgpack.unpackb(filelist_data)
for file in filelist: for file in filelist:
if not file in server_files: if not file in server_files:
server_files.append(file) server_files.append(file)
# If the menu is already visible, # If the menu is already visible,
# we'll update it with what was # we'll update it with what was
# just received # just received
if menu_mode == "main": if menu_mode == "main":
print_menu() print_menu()
except: except:
RNS.log("Invalid file list data received, closing link") RNS.log("Invalid file list data received, closing link")
packet.link.teardown() packet.link.teardown()
# This function is called when a link # This function is called when a link
# has been established with the server # has been established with the server
def link_established(link): def link_established(link):
# We store a reference to the link # We store a reference to the link
# instance for later use # instance for later use
global server_link global server_link
server_link = link server_link = link
# Inform the user that the server is # Inform the user that the server is
# connected # connected
RNS.log("Link established with server") RNS.log("Link established with server")
RNS.log("Waiting for filelist...") RNS.log("Waiting for filelist...")
# And set up a small job to check for # And set up a small job to check for
# a potential timeout in receiving the # a potential timeout in receiving the
# file list # file list
thread = threading.Thread(target=filelist_timeout_job) thread = threading.Thread(target=filelist_timeout_job)
thread.setDaemon(True) thread.setDaemon(True)
thread.start() thread.start()
# This job just sleeps for the specified # This job just sleeps for the specified
# time, and then checks if the file list # time, and then checks if the file list
# was received. If not, the program will # was received. If not, the program will
# exit. # exit.
def filelist_timeout_job(): def filelist_timeout_job():
time.sleep(APP_TIMEOUT) time.sleep(APP_TIMEOUT)
global server_files global server_files
if len(server_files) == 0: if len(server_files) == 0:
RNS.log("Timed out waiting for filelist, exiting") RNS.log("Timed out waiting for filelist, exiting")
os._exit(0) os._exit(0)
# When a link is closed, we'll inform the # When a link is closed, we'll inform the
# user, and exit the program # user, and exit the program
def link_closed(link): def link_closed(link):
if link.teardown_reason == RNS.Link.TIMEOUT: if link.teardown_reason == RNS.Link.TIMEOUT:
RNS.log("The link timed out, exiting now") RNS.log("The link timed out, exiting now")
elif link.teardown_reason == RNS.Link.DESTINATION_CLOSED: elif link.teardown_reason == RNS.Link.DESTINATION_CLOSED:
RNS.log("The link was closed by the server, exiting now") RNS.log("The link was closed by the server, exiting now")
else: else:
RNS.log("Link closed, exiting now") RNS.log("Link closed, exiting now")
RNS.Reticulum.exit_handler() RNS.Reticulum.exit_handler()
time.sleep(1.5) time.sleep(1.5)
os._exit(0) os._exit(0)
# When RNS detects that the download has # When RNS detects that the download has
# started, we'll update our menu state # started, we'll update our menu state
@ -471,45 +471,45 @@ def download_began(resource):
# or not, we'll update our menu state and # or not, we'll update our menu state and
# inform the user about how it all went. # inform the user about how it all went.
def download_concluded(resource): def download_concluded(resource):
global menu_mode, current_filename, download_started, download_finished, download_time global menu_mode, current_filename, download_started, download_finished, download_time
download_finished = time.time() download_finished = time.time()
download_time = download_finished - download_started download_time = download_finished - download_started
saved_filename = current_filename saved_filename = current_filename
if resource.status == RNS.Resource.COMPLETE: if resource.status == RNS.Resource.COMPLETE:
counter = 0 counter = 0
while os.path.isfile(saved_filename): while os.path.isfile(saved_filename):
counter += 1 counter += 1
saved_filename = current_filename+"."+str(counter) saved_filename = current_filename+"."+str(counter)
try: try:
file = open(saved_filename, "wb") file = open(saved_filename, "wb")
file.write(resource.data.read()) file.write(resource.data.read())
file.close() file.close()
menu_mode = "download_concluded" menu_mode = "download_concluded"
except: except:
menu_mode = "save_error" menu_mode = "save_error"
else: else:
menu_mode = "download_concluded" menu_mode = "download_concluded"
# A convenience function for printing a human- # A convenience function for printing a human-
# readable file size # readable file size
def size_str(num, suffix='B'): def size_str(num, suffix='B'):
units = ['','Ki','Mi','Gi','Ti','Pi','Ei','Zi'] units = ['','Ki','Mi','Gi','Ti','Pi','Ei','Zi']
last_unit = 'Yi' last_unit = 'Yi'
if suffix == 'b': if suffix == 'b':
num *= 8 num *= 8
units = ['','K','M','G','T','P','E','Z'] units = ['','K','M','G','T','P','E','Z']
last_unit = 'Y' last_unit = 'Y'
for unit in units: for unit in units:
if abs(num) < 1024.0: if abs(num) < 1024.0:
return "%3.2f %s%s" % (num, unit, suffix) return "%3.2f %s%s" % (num, unit, suffix)
num /= 1024.0 num /= 1024.0
return "%.2f %s%s" % (num, last_unit, suffix) return "%.2f %s%s" % (num, last_unit, suffix)
# A convenience function for clearing the screen # A convenience function for clearing the screen
def clear_screen(): def clear_screen():
@ -523,31 +523,31 @@ def clear_screen():
# and parses input of from the user, and then # and parses input of from the user, and then
# starts up the desired program mode. # starts up the desired program mode.
if __name__ == "__main__": if __name__ == "__main__":
try: try:
parser = argparse.ArgumentParser(description="Simple file transfer server and client utility") parser = argparse.ArgumentParser(description="Simple file transfer server and client utility")
parser.add_argument("-s", "--serve", action="store", metavar="dir", help="serve a directory of files to clients") parser.add_argument("-s", "--serve", action="store", metavar="dir", help="serve a directory of files to clients")
parser.add_argument("--config", action="store", default=None, help="path to alternative Reticulum config directory", type=str) 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) parser.add_argument("destination", nargs="?", default=None, help="hexadecimal hash of the server destination", type=str)
args = parser.parse_args() args = parser.parse_args()
if args.config: if args.config:
configarg = args.config configarg = args.config
else: else:
configarg = None configarg = None
if args.serve: if args.serve:
if os.path.isdir(args.serve): if os.path.isdir(args.serve):
server(configarg, args.serve) server(configarg, args.serve)
else: else:
RNS.log("The specified directory does not exist") RNS.log("The specified directory does not exist")
else: else:
if (args.destination == None): if (args.destination == None):
print("") print("")
parser.print_help() parser.print_help()
print("") print("")
else: else:
client(args.destination, configarg) client(args.destination, configarg)
except KeyboardInterrupt: except KeyboardInterrupt:
print("") print("")
exit() exit()

View File

@ -25,65 +25,65 @@ latest_client_link = None
# This initialisation is executed when the users chooses # This initialisation is executed when the users chooses
# to run as a server # to run as a server
def server(configpath): def server(configpath):
# We must first initialise Reticulum # We must first initialise Reticulum
reticulum = RNS.Reticulum(configpath) reticulum = RNS.Reticulum(configpath)
# Randomly create a new identity for our link example # Randomly create a new identity for our link example
server_identity = RNS.Identity() server_identity = RNS.Identity()
# We create a destination that clients can connect to. We # We create a destination that clients can connect to. We
# want clients to create links to this destination, so we # want clients to create links to this destination, so we
# need to create a "single" destination type. # need to create a "single" destination type.
server_destination = RNS.Destination(server_identity, RNS.Destination.IN, RNS.Destination.SINGLE, APP_NAME, "linkexample") server_destination = RNS.Destination(server_identity, RNS.Destination.IN, RNS.Destination.SINGLE, APP_NAME, "linkexample")
# We configure a function that will get called every time # We configure a function that will get called every time
# a new client creates a link to this destination. # a new client creates a link to this destination.
server_destination.link_established_callback(client_connected) server_destination.link_established_callback(client_connected)
# Everything's ready! # Everything's ready!
# Let's Wait for client requests or user input # Let's Wait for client requests or user input
server_loop(server_destination) server_loop(server_destination)
def server_loop(destination): def server_loop(destination):
# Let the user know that everything is ready # Let the user know that everything is ready
RNS.log("Link example "+RNS.prettyhexrep(destination.hash)+" running, waiting for a connection.") RNS.log("Link example "+RNS.prettyhexrep(destination.hash)+" running, waiting for a connection.")
RNS.log("Hit enter to manually send an announce (Ctrl-C to quit)") RNS.log("Hit enter to manually send an announce (Ctrl-C to quit)")
# We enter a loop that runs until the users exits. # We enter a loop that runs until the users exits.
# If the user hits enter, we will announce our server # If the user hits enter, we will announce our server
# destination on the network, which will let clients # destination on the network, which will let clients
# know how to create messages directed towards it. # know how to create messages directed towards it.
while True: while True:
entered = input() entered = input()
destination.announce() destination.announce()
RNS.log("Sent announce from "+RNS.prettyhexrep(destination.hash)) RNS.log("Sent announce from "+RNS.prettyhexrep(destination.hash))
# When a client establishes a link to our server # When a client establishes a link to our server
# destination, this function will be called with # destination, this function will be called with
# a reference to the link. # a reference to the link.
def client_connected(link): def client_connected(link):
global latest_client_link global latest_client_link
RNS.log("Client connected") RNS.log("Client connected")
link.link_closed_callback(client_disconnected) link.link_closed_callback(client_disconnected)
link.packet_callback(server_packet_received) link.packet_callback(server_packet_received)
latest_client_link = link latest_client_link = link
def client_disconnected(link): def client_disconnected(link):
RNS.log("Client disconnected") RNS.log("Client disconnected")
def server_packet_received(message, packet): def server_packet_received(message, packet):
global latest_client_link global latest_client_link
# When data is received over any active link, # When data is received over any active link,
# it will all be directed to the last client # it will all be directed to the last client
# that connected. # that connected.
text = message.decode("utf-8") text = message.decode("utf-8")
RNS.log("Received data on the link: "+text) RNS.log("Received data on the link: "+text)
reply_text = "I received \""+text+"\" over the link" reply_text = "I received \""+text+"\" over the link"
reply_data = reply_text.encode("utf-8") reply_data = reply_text.encode("utf-8")
RNS.Packet(latest_client_link, reply_data).send() RNS.Packet(latest_client_link, reply_data).send()
########################################################## ##########################################################
@ -96,112 +96,112 @@ server_link = None
# This initialisation is executed when the users chooses # This initialisation is executed when the users chooses
# to run as a client # to run as a client
def client(destination_hexhash, configpath): def client(destination_hexhash, configpath):
# We need a binary representation of the destination # We need a binary representation of the destination
# hash that was entered on the command line # hash that was entered on the command line
try: try:
if len(destination_hexhash) != 20: if len(destination_hexhash) != 20:
raise ValueError("Destination length is invalid, must be 20 hexadecimal characters (10 bytes)") raise ValueError("Destination length is invalid, must be 20 hexadecimal characters (10 bytes)")
destination_hash = bytes.fromhex(destination_hexhash) destination_hash = bytes.fromhex(destination_hexhash)
except: except:
RNS.log("Invalid destination entered. Check your input!\n") RNS.log("Invalid destination entered. Check your input!\n")
exit() exit()
# We must first initialise Reticulum # We must first initialise Reticulum
reticulum = RNS.Reticulum(configpath) reticulum = RNS.Reticulum(configpath)
# Check if we know a path to the destination # Check if we know a path to the destination
if not RNS.Transport.hasPath(destination_hash): if not RNS.Transport.hasPath(destination_hash):
RNS.log("Destination is not yet known. Requesting path and waiting for announce to arrive...") RNS.log("Destination is not yet known. Requesting path and waiting for announce to arrive...")
RNS.Transport.requestPath(destination_hash) RNS.Transport.requestPath(destination_hash)
while not RNS.Transport.hasPath(destination_hash): while not RNS.Transport.hasPath(destination_hash):
time.sleep(0.1) time.sleep(0.1)
# Recall the server identity # Recall the server identity
server_identity = RNS.Identity.recall(destination_hash) server_identity = RNS.Identity.recall(destination_hash)
# Inform the user that we'll begin connecting # Inform the user that we'll begin connecting
RNS.log("Establishing link with server...") RNS.log("Establishing link with server...")
# When the server identity is known, we set # When the server identity is known, we set
# up a destination # up a destination
server_destination = RNS.Destination(server_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, APP_NAME, "linkexample") server_destination = RNS.Destination(server_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, APP_NAME, "linkexample")
# And create a link # And create a link
link = RNS.Link(server_destination) link = RNS.Link(server_destination)
# We set a callback that will get executed # We set a callback that will get executed
# every time a packet is received over the # every time a packet is received over the
# link # link
link.packet_callback(client_packet_received) link.packet_callback(client_packet_received)
# We'll also set up functions to inform the # We'll also set up functions to inform the
# user when the link is established or closed # user when the link is established or closed
link.link_established_callback(link_established) link.link_established_callback(link_established)
link.link_closed_callback(link_closed) link.link_closed_callback(link_closed)
# Everything is set up, so let's enter a loop # Everything is set up, so let's enter a loop
# for the user to interact with the example # for the user to interact with the example
client_loop() client_loop()
def client_loop(): def client_loop():
global server_link global server_link
# Wait for the link to become active # Wait for the link to become active
while not server_link: while not server_link:
time.sleep(0.1) time.sleep(0.1)
should_quit = False should_quit = False
while not should_quit: while not should_quit:
try: try:
print("> ", end=" ") print("> ", end=" ")
text = input() text = input()
# Check if we should quit the example # Check if we should quit the example
if text == "quit" or text == "q" or text == "exit": if text == "quit" or text == "q" or text == "exit":
should_quit = True should_quit = True
server_link.teardown() server_link.teardown()
# If not, send the entered text over the link # If not, send the entered text over the link
if text != "": if text != "":
data = text.encode("utf-8") data = text.encode("utf-8")
RNS.Packet(server_link, data).send() RNS.Packet(server_link, data).send()
except Exception as e: except Exception as e:
should_quit = True should_quit = True
server_link.teardown() server_link.teardown()
# This function is called when a link # This function is called when a link
# has been established with the server # has been established with the server
def link_established(link): def link_established(link):
# We store a reference to the link # We store a reference to the link
# instance for later use # instance for later use
global server_link global server_link
server_link = link server_link = link
# Inform the user that the server is # Inform the user that the server is
# connected # connected
RNS.log("Link established with server, enter some text to send, or \"quit\" to quit") RNS.log("Link established with server, enter some text to send, or \"quit\" to quit")
# When a link is closed, we'll inform the # When a link is closed, we'll inform the
# user, and exit the program # user, and exit the program
def link_closed(link): def link_closed(link):
if link.teardown_reason == RNS.Link.TIMEOUT: if link.teardown_reason == RNS.Link.TIMEOUT:
RNS.log("The link timed out, exiting now") RNS.log("The link timed out, exiting now")
elif link.teardown_reason == RNS.Link.DESTINATION_CLOSED: elif link.teardown_reason == RNS.Link.DESTINATION_CLOSED:
RNS.log("The link was closed by the server, exiting now") RNS.log("The link was closed by the server, exiting now")
else: else:
RNS.log("Link closed, exiting now") RNS.log("Link closed, exiting now")
RNS.Reticulum.exit_handler() RNS.Reticulum.exit_handler()
time.sleep(1.5) time.sleep(1.5)
os._exit(0) os._exit(0)
# When a packet is received over the link, we # When a packet is received over the link, we
# simply print out the data. # simply print out the data.
def client_packet_received(message, packet): def client_packet_received(message, packet):
text = message.decode("utf-8") text = message.decode("utf-8")
RNS.log("Received data on the link: "+text) RNS.log("Received data on the link: "+text)
print("> ", end=" ") print("> ", end=" ")
sys.stdout.flush() sys.stdout.flush()
########################################################## ##########################################################
@ -212,28 +212,28 @@ def client_packet_received(message, packet):
# and parses input of from the user, and then # and parses input of from the user, and then
# starts up the desired program mode. # starts up the desired program mode.
if __name__ == "__main__": if __name__ == "__main__":
try: try:
parser = argparse.ArgumentParser(description="Simple link example") 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("-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("--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) parser.add_argument("destination", nargs="?", default=None, help="hexadecimal hash of the server destination", type=str)
args = parser.parse_args() args = parser.parse_args()
if args.config: if args.config:
configarg = args.config configarg = args.config
else: else:
configarg = None configarg = None
if args.server: if args.server:
server(configarg) server(configarg)
else: else:
if (args.destination == None): if (args.destination == None):
print("") print("")
parser.print_help() parser.print_help()
print("") print("")
else: else:
client(args.destination, configarg) client(args.destination, configarg)
except KeyboardInterrupt: except KeyboardInterrupt:
print("") print("")
exit() exit()

View File

@ -15,45 +15,45 @@ APP_NAME = "example_utilitites"
# This initialisation is executed when the program is started # This initialisation is executed when the program is started
def program_setup(configpath): def program_setup(configpath):
# We must first initialise Reticulum # We must first initialise Reticulum
reticulum = RNS.Reticulum(configpath) reticulum = RNS.Reticulum(configpath)
# Randomly create a new identity for our example # Randomly create a new identity for our example
identity = RNS.Identity() identity = RNS.Identity()
# Using the identity we just created, we create a destination. # Using the identity we just created, we create a destination.
# Destinations are endpoints in Reticulum, that can be addressed # Destinations are endpoints in Reticulum, that can be addressed
# and communicated with. Destinations can also announce their # and communicated with. Destinations can also announce their
# existence, which will let the network know they are reachable # existence, which will let the network know they are reachable
# and autoomatically create paths to them, from anywhere else # and autoomatically create paths to them, from anywhere else
# in the network. # in the network.
destination = RNS.Destination(identity, RNS.Destination.IN, RNS.Destination.SINGLE, APP_NAME, "minimalsample") destination = RNS.Destination(identity, RNS.Destination.IN, RNS.Destination.SINGLE, APP_NAME, "minimalsample")
# We configure the destination to automatically prove all # We configure the destination to automatically prove all
# packets adressed to it. By doing this, RNS will automatically # packets adressed to it. By doing this, RNS will automatically
# generate a proof for each incoming packet and transmit it # generate a proof for each incoming packet and transmit it
# back to the sender of that packet. This will let anyone that # back to the sender of that packet. This will let anyone that
# tries to communicate with the destination know whether their # tries to communicate with the destination know whether their
# communication was received correctly. # communication was received correctly.
destination.set_proof_strategy(RNS.Destination.PROVE_ALL) destination.set_proof_strategy(RNS.Destination.PROVE_ALL)
# Everything's ready! # Everything's ready!
# Let's hand over control to the announce loop # Let's hand over control to the announce loop
announceLoop(destination) announceLoop(destination)
def announceLoop(destination): def announceLoop(destination):
# Let the user know that everything is ready # Let the user know that everything is ready
RNS.log("Minimal example "+RNS.prettyhexrep(destination.hash)+" running, hit enter to manually send an announce (Ctrl-C to quit)") RNS.log("Minimal example "+RNS.prettyhexrep(destination.hash)+" running, hit enter to manually send an announce (Ctrl-C to quit)")
# We enter a loop that runs until the users exits. # We enter a loop that runs until the users exits.
# If the user hits enter, we will announce our server # If the user hits enter, we will announce our server
# destination on the network, which will let clients # destination on the network, which will let clients
# know how to create messages directed towards it. # know how to create messages directed towards it.
while True: while True:
entered = input() entered = input()
destination.announce() destination.announce()
RNS.log("Sent announce from "+RNS.prettyhexrep(destination.hash)) RNS.log("Sent announce from "+RNS.prettyhexrep(destination.hash))
########################################################## ##########################################################
@ -64,18 +64,18 @@ def announceLoop(destination):
# and parses input from the user, and then starts # and parses input from the user, and then starts
# the desired program mode. # the desired program mode.
if __name__ == "__main__": if __name__ == "__main__":
try: try:
parser = argparse.ArgumentParser(description="Bare minimum example to start Reticulum and create a destination") parser = argparse.ArgumentParser(description="Bare minimum example to start Reticulum and create a destination")
parser.add_argument("--config", action="store", default=None, help="path to alternative Reticulum config directory", type=str) parser.add_argument("--config", action="store", default=None, help="path to alternative Reticulum config directory", type=str)
args = parser.parse_args() args = parser.parse_args()
if args.config: if args.config:
configarg = args.config configarg = args.config
else: else:
configarg = None configarg = None
program_setup(configarg) program_setup(configarg)
except KeyboardInterrupt: except KeyboardInterrupt:
print("") print("")
exit() exit()

View File

@ -10,228 +10,228 @@ from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives.asymmetric import padding from cryptography.hazmat.primitives.asymmetric import padding
class Callbacks: class Callbacks:
def __init__(self): def __init__(self):
self.link_established = None self.link_established = None
self.packet = None self.packet = None
self.proof_requested = None self.proof_requested = None
class Destination: class Destination:
KEYSIZE = RNS.Identity.KEYSIZE; KEYSIZE = RNS.Identity.KEYSIZE;
PADDINGSIZE= RNS.Identity.PADDINGSIZE; PADDINGSIZE= RNS.Identity.PADDINGSIZE;
# Constants # Constants
SINGLE = 0x00 SINGLE = 0x00
GROUP = 0x01 GROUP = 0x01
PLAIN = 0x02 PLAIN = 0x02
LINK = 0x03 LINK = 0x03
types = [SINGLE, GROUP, PLAIN, LINK] types = [SINGLE, GROUP, PLAIN, LINK]
PROVE_NONE = 0x21 PROVE_NONE = 0x21
PROVE_APP = 0x22 PROVE_APP = 0x22
PROVE_ALL = 0x23 PROVE_ALL = 0x23
proof_strategies = [PROVE_NONE, PROVE_APP, PROVE_ALL] proof_strategies = [PROVE_NONE, PROVE_APP, PROVE_ALL]
IN = 0x11; IN = 0x11;
OUT = 0x12; OUT = 0x12;
directions = [IN, OUT] directions = [IN, OUT]
@staticmethod @staticmethod
def getDestinationName(app_name, *aspects): def getDestinationName(app_name, *aspects):
# Check input values and build name string # Check input values and build name string
if "." in app_name: raise ValueError("Dots can't be used in app names") if "." in app_name: raise ValueError("Dots can't be used in app names")
name = app_name name = app_name
for aspect in aspects: for aspect in aspects:
if "." in aspect: raise ValueError("Dots can't be used in aspects") if "." in aspect: raise ValueError("Dots can't be used in aspects")
name = name + "." + aspect name = name + "." + aspect
return name return name
@staticmethod @staticmethod
def getDestinationHash(app_name, *aspects): def getDestinationHash(app_name, *aspects):
name = Destination.getDestinationName(app_name, *aspects) name = Destination.getDestinationName(app_name, *aspects)
# Create a digest for the destination # Create a digest for the destination
digest = hashes.Hash(hashes.SHA256(), backend=default_backend()) digest = hashes.Hash(hashes.SHA256(), backend=default_backend())
digest.update(name.encode("UTF-8")) digest.update(name.encode("UTF-8"))
return digest.finalize()[:10] return digest.finalize()[:10]
def __init__(self, identity, direction, type, app_name, *aspects): def __init__(self, identity, direction, type, app_name, *aspects):
# Check input values and build name string # Check input values and build name string
if "." in app_name: raise ValueError("Dots can't be used in app names") if "." in app_name: raise ValueError("Dots can't be used in app names")
if not type in Destination.types: raise ValueError("Unknown destination type") if not type in Destination.types: raise ValueError("Unknown destination type")
if not direction in Destination.directions: raise ValueError("Unknown destination direction") if not direction in Destination.directions: raise ValueError("Unknown destination direction")
self.callbacks = Callbacks() self.callbacks = Callbacks()
self.type = type self.type = type
self.direction = direction self.direction = direction
self.proof_strategy = Destination.PROVE_NONE self.proof_strategy = Destination.PROVE_NONE
self.mtu = 0 self.mtu = 0
self.links = [] self.links = []
if identity != None and type == Destination.SINGLE: if identity != None and type == Destination.SINGLE:
aspects = aspects+(identity.hexhash,) aspects = aspects+(identity.hexhash,)
if identity == None and direction == Destination.IN and self.type != Destination.PLAIN: if identity == None and direction == Destination.IN and self.type != Destination.PLAIN:
identity = RNS.Identity() identity = RNS.Identity()
aspects = aspects+(identity.hexhash,) aspects = aspects+(identity.hexhash,)
self.identity = identity self.identity = identity
self.name = Destination.getDestinationName(app_name, *aspects) self.name = Destination.getDestinationName(app_name, *aspects)
self.hash = Destination.getDestinationHash(app_name, *aspects) self.hash = Destination.getDestinationHash(app_name, *aspects)
self.hexhash = self.hash.hex() self.hexhash = self.hash.hex()
self.callback = None self.callback = None
self.proofcallback = None self.proofcallback = None
RNS.Transport.registerDestination(self) RNS.Transport.registerDestination(self)
def __str__(self): def __str__(self):
return "<"+self.name+"/"+self.hexhash+">" return "<"+self.name+"/"+self.hexhash+">"
def link_established_callback(self, callback): def link_established_callback(self, callback):
self.callbacks.link_established = callback self.callbacks.link_established = callback
def packet_callback(self, callback): def packet_callback(self, callback):
self.callbacks.packet = callback self.callbacks.packet = callback
def proof_requested_callback(self, callback): def proof_requested_callback(self, callback):
self.callbacks.proof_requested = callback self.callbacks.proof_requested = callback
def set_proof_strategy(self, proof_strategy): def set_proof_strategy(self, proof_strategy):
if not proof_strategy in Destination.proof_strategies: if not proof_strategy in Destination.proof_strategies:
raise TypeError("Unsupported proof strategy") raise TypeError("Unsupported proof strategy")
else: else:
self.proof_strategy = proof_strategy self.proof_strategy = proof_strategy
def receive(self, packet): def receive(self, packet):
plaintext = self.decrypt(packet.data) plaintext = self.decrypt(packet.data)
if plaintext != None: if plaintext != None:
if packet.packet_type == RNS.Packet.LINKREQUEST: if packet.packet_type == RNS.Packet.LINKREQUEST:
self.incomingLinkRequest(plaintext, packet) self.incomingLinkRequest(plaintext, packet)
if packet.packet_type == RNS.Packet.DATA: if packet.packet_type == RNS.Packet.DATA:
if self.callbacks.packet != None: if self.callbacks.packet != None:
self.callbacks.packet(plaintext, packet) self.callbacks.packet(plaintext, packet)
def incomingLinkRequest(self, data, packet): def incomingLinkRequest(self, data, packet):
link = RNS.Link.validateRequest(self, data, packet) link = RNS.Link.validateRequest(self, data, packet)
if link != None: if link != None:
self.links.append(link) self.links.append(link)
def createKeys(self): def createKeys(self):
if self.type == Destination.PLAIN: if self.type == Destination.PLAIN:
raise TypeError("A plain destination does not hold any keys") raise TypeError("A plain destination does not hold any keys")
if self.type == Destination.SINGLE: if self.type == Destination.SINGLE:
raise TypeError("A single destination holds keys through an Identity instance") raise TypeError("A single destination holds keys through an Identity instance")
if self.type == Destination.GROUP: if self.type == Destination.GROUP:
self.prv_bytes = Fernet.generate_key() self.prv_bytes = Fernet.generate_key()
self.prv = Fernet(self.prv_bytes) self.prv = Fernet(self.prv_bytes)
def getPrivateKey(self): def getPrivateKey(self):
if self.type == Destination.PLAIN: if self.type == Destination.PLAIN:
raise TypeError("A plain destination does not hold any keys") raise TypeError("A plain destination does not hold any keys")
elif self.type == Destination.SINGLE: elif self.type == Destination.SINGLE:
raise TypeError("A single destination holds keys through an Identity instance") raise TypeError("A single destination holds keys through an Identity instance")
else: else:
return self.prv_bytes return self.prv_bytes
def loadPrivateKey(self, key): def loadPrivateKey(self, key):
if self.type == Destination.PLAIN: if self.type == Destination.PLAIN:
raise TypeError("A plain destination does not hold any keys") raise TypeError("A plain destination does not hold any keys")
if self.type == Destination.SINGLE: if self.type == Destination.SINGLE:
raise TypeError("A single destination holds keys through an Identity instance") raise TypeError("A single destination holds keys through an Identity instance")
if self.type == Destination.GROUP: if self.type == Destination.GROUP:
self.prv_bytes = key self.prv_bytes = key
self.prv = Fernet(self.prv_bytes) self.prv = Fernet(self.prv_bytes)
def loadPublicKey(self, key): def loadPublicKey(self, key):
if self.type != Destination.SINGLE: if self.type != Destination.SINGLE:
raise TypeError("Only the \"single\" destination type can hold a public key") raise TypeError("Only the \"single\" destination type can hold a public key")
else: else:
raise TypeError("A single destination holds keys through an Identity instance") raise TypeError("A single destination holds keys through an Identity instance")
def encrypt(self, plaintext): def encrypt(self, plaintext):
if self.type == Destination.PLAIN: if self.type == Destination.PLAIN:
return plaintext return plaintext
if self.type == Destination.SINGLE and self.identity != None: if self.type == Destination.SINGLE and self.identity != None:
return self.identity.encrypt(plaintext) return self.identity.encrypt(plaintext)
if self.type == Destination.GROUP: if self.type == Destination.GROUP:
if hasattr(self, "prv") and self.prv != None: if hasattr(self, "prv") and self.prv != None:
try: try:
return base64.urlsafe_b64decode(self.prv.encrypt(plaintext)) return base64.urlsafe_b64decode(self.prv.encrypt(plaintext))
except Exception as e: except Exception as e:
RNS.log("The GROUP destination could not encrypt data", RNS.LOG_ERROR) RNS.log("The GROUP destination could not encrypt data", RNS.LOG_ERROR)
RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR) RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR)
else: else:
raise ValueError("No private key held by GROUP destination. Did you create or load one?") raise ValueError("No private key held by GROUP destination. Did you create or load one?")
def decrypt(self, ciphertext): def decrypt(self, ciphertext):
if self.type == Destination.PLAIN: if self.type == Destination.PLAIN:
return ciphertext return ciphertext
if self.type == Destination.SINGLE and self.identity != None: if self.type == Destination.SINGLE and self.identity != None:
return self.identity.decrypt(ciphertext) return self.identity.decrypt(ciphertext)
if self.type == Destination.GROUP: if self.type == Destination.GROUP:
if hasattr(self, "prv") and self.prv != None: if hasattr(self, "prv") and self.prv != None:
try: try:
return self.prv.decrypt(base64.urlsafe_b64encode(ciphertext)) return self.prv.decrypt(base64.urlsafe_b64encode(ciphertext))
except Exception as e: except Exception as e:
RNS.log("The GROUP destination could not decrypt data", RNS.LOG_ERROR) RNS.log("The GROUP destination could not decrypt data", RNS.LOG_ERROR)
RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR) RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR)
else: else:
raise ValueError("No private key held by GROUP destination. Did you create or load one?") raise ValueError("No private key held by GROUP destination. Did you create or load one?")
def sign(self, message): def sign(self, message):
if self.type == Destination.SINGLE and self.identity != None: if self.type == Destination.SINGLE and self.identity != None:
return self.identity.sign(message) return self.identity.sign(message)
else: else:
return None return None
# Creates an announce packet for this destination. # Creates an announce packet for this destination.
# Application specific data can be added to the announce. # Application specific data can be added to the announce.
def announce(self, app_data=None, path_response=False): def announce(self, app_data=None, path_response=False):
destination_hash = self.hash destination_hash = self.hash
random_hash = RNS.Identity.getRandomHash() random_hash = RNS.Identity.getRandomHash()
signed_data = self.hash+self.identity.getPublicKey()+random_hash signed_data = self.hash+self.identity.getPublicKey()+random_hash
if app_data != None: if app_data != None:
signed_data += app_data signed_data += app_data
signature = self.identity.sign(signed_data) signature = self.identity.sign(signed_data)
# TODO: Check if this could be optimised by only # TODO: Check if this could be optimised by only
# carrying the hash in the destination field, not # carrying the hash in the destination field, not
# also redundantly inside the signed blob as here # also redundantly inside the signed blob as here
announce_data = self.hash+self.identity.getPublicKey()+random_hash+signature announce_data = self.hash+self.identity.getPublicKey()+random_hash+signature
if app_data != None: if app_data != None:
announce_data += app_data announce_data += app_data
if path_response: if path_response:
announce_context = RNS.Packet.PATH_RESPONSE announce_context = RNS.Packet.PATH_RESPONSE
else: else:
announce_context = RNS.Packet.NONE announce_context = RNS.Packet.NONE
RNS.Packet(self, announce_data, RNS.Packet.ANNOUNCE, context = announce_context).send() RNS.Packet(self, announce_data, RNS.Packet.ANNOUNCE, context = announce_context).send()

View File

@ -15,313 +15,313 @@ from cryptography.hazmat.primitives.asymmetric import padding
class Identity: class Identity:
#KEYSIZE = 1536 #KEYSIZE = 1536
KEYSIZE = 1024 KEYSIZE = 1024
DERKEYSIZE = KEYSIZE+272 DERKEYSIZE = KEYSIZE+272
# Non-configurable constants # Non-configurable constants
PADDINGSIZE = 336 # In bits PADDINGSIZE = 336 # In bits
HASHLENGTH = 256 # In bits HASHLENGTH = 256 # In bits
SIGLENGTH = KEYSIZE SIGLENGTH = KEYSIZE
ENCRYPT_CHUNKSIZE = (KEYSIZE-PADDINGSIZE)//8 ENCRYPT_CHUNKSIZE = (KEYSIZE-PADDINGSIZE)//8
DECRYPT_CHUNKSIZE = KEYSIZE//8 DECRYPT_CHUNKSIZE = KEYSIZE//8
TRUNCATED_HASHLENGTH = 80 # In bits TRUNCATED_HASHLENGTH = 80 # In bits
# Storage # Storage
known_destinations = {} known_destinations = {}
@staticmethod @staticmethod
def remember(packet_hash, destination_hash, public_key, app_data = None): def remember(packet_hash, destination_hash, public_key, app_data = None):
Identity.known_destinations[destination_hash] = [time.time(), packet_hash, public_key, app_data] Identity.known_destinations[destination_hash] = [time.time(), packet_hash, public_key, app_data]
@staticmethod @staticmethod
def recall(destination_hash): def recall(destination_hash):
RNS.log("Searching for "+RNS.prettyhexrep(destination_hash)+"...", RNS.LOG_EXTREME) RNS.log("Searching for "+RNS.prettyhexrep(destination_hash)+"...", RNS.LOG_EXTREME)
if destination_hash in Identity.known_destinations: if destination_hash in Identity.known_destinations:
identity_data = Identity.known_destinations[destination_hash] identity_data = Identity.known_destinations[destination_hash]
identity = Identity(public_only=True) identity = Identity(public_only=True)
identity.loadPublicKey(identity_data[2]) identity.loadPublicKey(identity_data[2])
RNS.log("Found "+RNS.prettyhexrep(destination_hash)+" in known destinations", RNS.LOG_EXTREME) RNS.log("Found "+RNS.prettyhexrep(destination_hash)+" in known destinations", RNS.LOG_EXTREME)
return identity return identity
else: else:
RNS.log("Could not find "+RNS.prettyhexrep(destination_hash)+" in known destinations", RNS.LOG_EXTREME) RNS.log("Could not find "+RNS.prettyhexrep(destination_hash)+" in known destinations", RNS.LOG_EXTREME)
return None return None
@staticmethod @staticmethod
def saveKnownDestinations(): def saveKnownDestinations():
RNS.log("Saving known destinations to storage...", RNS.LOG_VERBOSE) RNS.log("Saving known destinations to storage...", RNS.LOG_VERBOSE)
file = open(RNS.Reticulum.storagepath+"/known_destinations","wb") file = open(RNS.Reticulum.storagepath+"/known_destinations","wb")
umsgpack.dump(Identity.known_destinations, file) umsgpack.dump(Identity.known_destinations, file)
file.close() file.close()
RNS.log("Done saving known destinations to storage", RNS.LOG_VERBOSE) RNS.log("Done saving known destinations to storage", RNS.LOG_VERBOSE)
@staticmethod @staticmethod
def loadKnownDestinations(): def loadKnownDestinations():
if os.path.isfile(RNS.Reticulum.storagepath+"/known_destinations"): if os.path.isfile(RNS.Reticulum.storagepath+"/known_destinations"):
try: try:
file = open(RNS.Reticulum.storagepath+"/known_destinations","rb") file = open(RNS.Reticulum.storagepath+"/known_destinations","rb")
Identity.known_destinations = umsgpack.load(file) Identity.known_destinations = umsgpack.load(file)
file.close() file.close()
RNS.log("Loaded "+str(len(Identity.known_destinations))+" known destination from storage", RNS.LOG_VERBOSE) RNS.log("Loaded "+str(len(Identity.known_destinations))+" known destination from storage", RNS.LOG_VERBOSE)
except: except:
RNS.log("Error loading known destinations from disk, file will be recreated on exit", RNS.LOG_ERROR) RNS.log("Error loading known destinations from disk, file will be recreated on exit", RNS.LOG_ERROR)
else: else:
RNS.log("Destinations file does not exist, so no known destinations loaded", RNS.LOG_VERBOSE) RNS.log("Destinations file does not exist, so no known destinations loaded", RNS.LOG_VERBOSE)
@staticmethod @staticmethod
def fullHash(data): def fullHash(data):
digest = hashes.Hash(hashes.SHA256(), backend=default_backend()) digest = hashes.Hash(hashes.SHA256(), backend=default_backend())
digest.update(data) digest.update(data)
return digest.finalize() return digest.finalize()
@staticmethod @staticmethod
def truncatedHash(data): def truncatedHash(data):
# TODO: Remove # TODO: Remove
# digest = hashes.Hash(hashes.SHA256(), backend=default_backend()) # digest = hashes.Hash(hashes.SHA256(), backend=default_backend())
# digest.update(data) # digest.update(data)
return Identity.fullHash(data)[:(Identity.TRUNCATED_HASHLENGTH//8)] return Identity.fullHash(data)[:(Identity.TRUNCATED_HASHLENGTH//8)]
@staticmethod @staticmethod
def getRandomHash(): def getRandomHash():
return Identity.truncatedHash(os.urandom(10)) return Identity.truncatedHash(os.urandom(10))
@staticmethod @staticmethod
def validateAnnounce(packet): def validateAnnounce(packet):
if packet.packet_type == RNS.Packet.ANNOUNCE: if packet.packet_type == RNS.Packet.ANNOUNCE:
RNS.log("Validating announce from "+RNS.prettyhexrep(packet.destination_hash), RNS.LOG_DEBUG) RNS.log("Validating announce from "+RNS.prettyhexrep(packet.destination_hash), RNS.LOG_DEBUG)
destination_hash = packet.destination_hash destination_hash = packet.destination_hash
public_key = packet.data[10:Identity.DERKEYSIZE//8+10] public_key = packet.data[10:Identity.DERKEYSIZE//8+10]
random_hash = packet.data[Identity.DERKEYSIZE//8+10:Identity.DERKEYSIZE//8+20] random_hash = packet.data[Identity.DERKEYSIZE//8+10:Identity.DERKEYSIZE//8+20]
signature = packet.data[Identity.DERKEYSIZE//8+20:Identity.DERKEYSIZE//8+20+Identity.KEYSIZE//8] signature = packet.data[Identity.DERKEYSIZE//8+20:Identity.DERKEYSIZE//8+20+Identity.KEYSIZE//8]
app_data = b"" app_data = b""
if len(packet.data) > Identity.DERKEYSIZE//8+20+Identity.KEYSIZE//8: if len(packet.data) > Identity.DERKEYSIZE//8+20+Identity.KEYSIZE//8:
app_data = packet.data[Identity.DERKEYSIZE//8+20+Identity.KEYSIZE//8:] app_data = packet.data[Identity.DERKEYSIZE//8+20+Identity.KEYSIZE//8:]
signed_data = destination_hash+public_key+random_hash+app_data signed_data = destination_hash+public_key+random_hash+app_data
announced_identity = Identity(public_only=True) announced_identity = Identity(public_only=True)
announced_identity.loadPublicKey(public_key) announced_identity.loadPublicKey(public_key)
if announced_identity.pub != None and announced_identity.validate(signature, signed_data): if announced_identity.pub != None and announced_identity.validate(signature, signed_data):
RNS.Identity.remember(packet.getHash(), destination_hash, public_key) RNS.Identity.remember(packet.getHash(), destination_hash, public_key)
RNS.log("Stored valid announce from "+RNS.prettyhexrep(destination_hash), RNS.LOG_DEBUG) RNS.log("Stored valid announce from "+RNS.prettyhexrep(destination_hash), RNS.LOG_DEBUG)
del announced_identity del announced_identity
return True return True
else: else:
RNS.log("Received invalid announce", RNS.LOG_DEBUG) RNS.log("Received invalid announce", RNS.LOG_DEBUG)
del announced_identity del announced_identity
return False return False
@staticmethod @staticmethod
def exitHandler(): def exitHandler():
Identity.saveKnownDestinations() Identity.saveKnownDestinations()
@staticmethod @staticmethod
def from_file(path): def from_file(path):
identity = Identity(public_only=True) identity = Identity(public_only=True)
if identity.load(path): if identity.load(path):
return identity return identity
else: else:
return None return None
def __init__(self,public_only=False): def __init__(self,public_only=False):
# Initialize keys to none # Initialize keys to none
self.prv = None self.prv = None
self.pub = None self.pub = None
self.prv_bytes = None self.prv_bytes = None
self.pub_bytes = None self.pub_bytes = None
self.hash = None self.hash = None
self.hexhash = None self.hexhash = None
if not public_only: if not public_only:
self.createKeys() self.createKeys()
def createKeys(self): def createKeys(self):
self.prv = rsa.generate_private_key( self.prv = rsa.generate_private_key(
public_exponent=65337, public_exponent=65337,
key_size=Identity.KEYSIZE, key_size=Identity.KEYSIZE,
backend=default_backend() backend=default_backend()
) )
self.prv_bytes = self.prv.private_bytes( self.prv_bytes = self.prv.private_bytes(
encoding=serialization.Encoding.DER, encoding=serialization.Encoding.DER,
format=serialization.PrivateFormat.PKCS8, format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=serialization.NoEncryption() encryption_algorithm=serialization.NoEncryption()
) )
self.pub = self.prv.public_key() self.pub = self.prv.public_key()
self.pub_bytes = self.pub.public_bytes( self.pub_bytes = self.pub.public_bytes(
encoding=serialization.Encoding.DER, encoding=serialization.Encoding.DER,
format=serialization.PublicFormat.SubjectPublicKeyInfo format=serialization.PublicFormat.SubjectPublicKeyInfo
) )
self.updateHashes() self.updateHashes()
RNS.log("Identity keys created for "+RNS.prettyhexrep(self.hash), RNS.LOG_VERBOSE) RNS.log("Identity keys created for "+RNS.prettyhexrep(self.hash), RNS.LOG_VERBOSE)
def getPrivateKey(self): def getPrivateKey(self):
return self.prv_bytes return self.prv_bytes
def getPublicKey(self): def getPublicKey(self):
return self.pub_bytes return self.pub_bytes
def loadPrivateKey(self, prv_bytes): def loadPrivateKey(self, prv_bytes):
try: try:
self.prv_bytes = prv_bytes self.prv_bytes = prv_bytes
self.prv = serialization.load_der_private_key( self.prv = serialization.load_der_private_key(
self.prv_bytes, self.prv_bytes,
password=None, password=None,
backend=default_backend() backend=default_backend()
) )
self.pub = self.prv.public_key() self.pub = self.prv.public_key()
self.pub_bytes = self.pub.public_bytes( self.pub_bytes = self.pub.public_bytes(
encoding=serialization.Encoding.DER, encoding=serialization.Encoding.DER,
format=serialization.PublicFormat.SubjectPublicKeyInfo format=serialization.PublicFormat.SubjectPublicKeyInfo
) )
self.updateHashes() self.updateHashes()
return True return True
except Exception as e: except Exception as e:
RNS.log("Failed to load identity key", RNS.LOG_ERROR) RNS.log("Failed to load identity key", RNS.LOG_ERROR)
RNS.log("The contained exception was: "+str(e)) RNS.log("The contained exception was: "+str(e))
return False return False
def loadPublicKey(self, key): def loadPublicKey(self, key):
try: try:
self.pub_bytes = key self.pub_bytes = key
self.pub = load_der_public_key(self.pub_bytes, backend=default_backend()) self.pub = load_der_public_key(self.pub_bytes, backend=default_backend())
self.updateHashes() self.updateHashes()
except Exception as e: except Exception as e:
RNS.log("Error while loading public key, the contained exception was: "+str(e), RNS.LOG_ERROR) RNS.log("Error while loading public key, the contained exception was: "+str(e), RNS.LOG_ERROR)
def updateHashes(self): def updateHashes(self):
self.hash = Identity.truncatedHash(self.pub_bytes) self.hash = Identity.truncatedHash(self.pub_bytes)
self.hexhash = self.hash.hex() self.hexhash = self.hash.hex()
def save(self, path): def save(self, path):
try: try:
with open(path, "wb") as key_file: with open(path, "wb") as key_file:
key_file.write(self.prv_bytes) key_file.write(self.prv_bytes)
return True return True
return False return False
except Exception as e: except Exception as e:
RNS.log("Error while saving identity to "+str(path), RNS.LOG_ERROR) RNS.log("Error while saving identity to "+str(path), RNS.LOG_ERROR)
RNS.log("The contained exception was: "+str(e)) RNS.log("The contained exception was: "+str(e))
def load(self, path): def load(self, path):
try: try:
with open(path, "rb") as key_file: with open(path, "rb") as key_file:
prv_bytes = key_file.read() prv_bytes = key_file.read()
return self.loadPrivateKey(prv_bytes) return self.loadPrivateKey(prv_bytes)
return False return False
except Exception as e: except Exception as e:
RNS.log("Error while loading identity from "+str(path), RNS.LOG_ERROR) RNS.log("Error while loading identity from "+str(path), RNS.LOG_ERROR)
RNS.log("The contained exception was: "+str(e)) RNS.log("The contained exception was: "+str(e))
def encrypt(self, plaintext): def encrypt(self, plaintext):
if self.pub != None: if self.pub != None:
chunksize = Identity.ENCRYPT_CHUNKSIZE chunksize = Identity.ENCRYPT_CHUNKSIZE
chunks = int(math.ceil(len(plaintext)/(float(chunksize)))) chunks = int(math.ceil(len(plaintext)/(float(chunksize))))
ciphertext = b""; ciphertext = b"";
for chunk in range(chunks): for chunk in range(chunks):
start = chunk*chunksize start = chunk*chunksize
end = (chunk+1)*chunksize end = (chunk+1)*chunksize
if (chunk+1)*chunksize > len(plaintext): if (chunk+1)*chunksize > len(plaintext):
end = len(plaintext) end = len(plaintext)
ciphertext += self.pub.encrypt( ciphertext += self.pub.encrypt(
plaintext[start:end], plaintext[start:end],
padding.OAEP( padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA1()), mgf=padding.MGF1(algorithm=hashes.SHA1()),
algorithm=hashes.SHA1(), algorithm=hashes.SHA1(),
label=None label=None
) )
) )
return ciphertext return ciphertext
else: else:
raise KeyError("Encryption failed because identity does not hold a public key") raise KeyError("Encryption failed because identity does not hold a public key")
def decrypt(self, ciphertext): def decrypt(self, ciphertext):
if self.prv != None: if self.prv != None:
plaintext = None plaintext = None
try: try:
chunksize = Identity.DECRYPT_CHUNKSIZE chunksize = Identity.DECRYPT_CHUNKSIZE
chunks = int(math.ceil(len(ciphertext)/(float(chunksize)))) chunks = int(math.ceil(len(ciphertext)/(float(chunksize))))
plaintext = b""; plaintext = b"";
for chunk in range(chunks): for chunk in range(chunks):
start = chunk*chunksize start = chunk*chunksize
end = (chunk+1)*chunksize end = (chunk+1)*chunksize
if (chunk+1)*chunksize > len(ciphertext): if (chunk+1)*chunksize > len(ciphertext):
end = len(ciphertext) end = len(ciphertext)
plaintext += self.prv.decrypt( plaintext += self.prv.decrypt(
ciphertext[start:end], ciphertext[start:end],
padding.OAEP( padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA1()), mgf=padding.MGF1(algorithm=hashes.SHA1()),
algorithm=hashes.SHA1(), algorithm=hashes.SHA1(),
label=None label=None
) )
) )
except: except:
RNS.log("Decryption by "+RNS.prettyhexrep(self.hash)+" failed", RNS.LOG_VERBOSE) RNS.log("Decryption by "+RNS.prettyhexrep(self.hash)+" failed", RNS.LOG_VERBOSE)
return plaintext; return plaintext;
else: else:
raise KeyError("Decryption failed because identity does not hold a private key") raise KeyError("Decryption failed because identity does not hold a private key")
def sign(self, message): def sign(self, message):
if self.prv != None: if self.prv != None:
signature = self.prv.sign( signature = self.prv.sign(
message, message,
padding.PSS( padding.PSS(
mgf=padding.MGF1(hashes.SHA256()), mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH salt_length=padding.PSS.MAX_LENGTH
), ),
hashes.SHA256() hashes.SHA256()
) )
return signature return signature
else: else:
raise KeyError("Signing failed because identity does not hold a private key") raise KeyError("Signing failed because identity does not hold a private key")
def validate(self, signature, message): def validate(self, signature, message):
if self.pub != None: if self.pub != None:
try: try:
self.pub.verify( self.pub.verify(
signature, signature,
message, message,
padding.PSS( padding.PSS(
mgf=padding.MGF1(hashes.SHA256()), mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH salt_length=padding.PSS.MAX_LENGTH
), ),
hashes.SHA256() hashes.SHA256()
) )
return True return True
except Exception as e: except Exception as e:
return False return False
else: else:
raise KeyError("Signature validation failed because identity does not hold a public key") raise KeyError("Signature validation failed because identity does not hold a public key")
def prove(self, packet, destination=None): def prove(self, packet, destination=None):
signature = self.sign(packet.packet_hash) signature = self.sign(packet.packet_hash)
if RNS.Reticulum.should_use_implicit_proof(): if RNS.Reticulum.should_use_implicit_proof():
proof_data = signature proof_data = signature
else: else:
proof_data = packet.packet_hash + signature proof_data = packet.packet_hash + signature
if destination == None: if destination == None:
destination = packet.generateProofDestination() destination = packet.generateProofDestination()
proof = RNS.Packet(destination, proof_data, RNS.Packet.PROOF, attached_interface = packet.receiving_interface) proof = RNS.Packet(destination, proof_data, RNS.Packet.PROOF, attached_interface = packet.receiving_interface)
proof.send() proof.send()
def __str__(self): def __str__(self):
return RNS.prettyhexrep(self.hash) return RNS.prettyhexrep(self.hash)

View File

@ -8,298 +8,298 @@ import time
import RNS import RNS
class KISS(): class KISS():
FEND = 0xC0 FEND = 0xC0
FESC = 0xDB FESC = 0xDB
TFEND = 0xDC TFEND = 0xDC
TFESC = 0xDD TFESC = 0xDD
CMD_UNKNOWN = 0xFE CMD_UNKNOWN = 0xFE
CMD_DATA = 0x00 CMD_DATA = 0x00
CMD_TXDELAY = 0x01 CMD_TXDELAY = 0x01
CMD_P = 0x02 CMD_P = 0x02
CMD_SLOTTIME = 0x03 CMD_SLOTTIME = 0x03
CMD_TXTAIL = 0x04 CMD_TXTAIL = 0x04
CMD_FULLDUPLEX = 0x05 CMD_FULLDUPLEX = 0x05
CMD_SETHARDWARE = 0x06 CMD_SETHARDWARE = 0x06
CMD_READY = 0x0F CMD_READY = 0x0F
CMD_RETURN = 0xFF CMD_RETURN = 0xFF
@staticmethod @staticmethod
def escape(data): def escape(data):
data = data.replace(bytes([0xdb]), bytes([0xdb, 0xdd])) data = data.replace(bytes([0xdb]), bytes([0xdb, 0xdd]))
data = data.replace(bytes([0xc0]), bytes([0xdb, 0xdc])) data = data.replace(bytes([0xc0]), bytes([0xdb, 0xdc]))
return data return data
class AX25(): class AX25():
PID_NOLAYER3 = 0xF0 PID_NOLAYER3 = 0xF0
CTRL_UI = 0x03 CTRL_UI = 0x03
CRC_CORRECT = bytes([0xF0])+bytes([0xB8]) CRC_CORRECT = bytes([0xF0])+bytes([0xB8])
HEADER_SIZE = 16 HEADER_SIZE = 16
class AX25KISSInterface(Interface): class AX25KISSInterface(Interface):
MAX_CHUNK = 32768 MAX_CHUNK = 32768
owner = None owner = None
port = None port = None
speed = None speed = None
databits = None databits = None
parity = None parity = None
stopbits = None stopbits = None
serial = None serial = None
def __init__(self, owner, name, callsign, ssid, port, speed, databits, parity, stopbits, preamble, txtail, persistence, slottime, flow_control): def __init__(self, owner, name, callsign, ssid, port, speed, databits, parity, stopbits, preamble, txtail, persistence, slottime, flow_control):
self.serial = None self.serial = None
self.owner = owner self.owner = owner
self.name = name self.name = name
self.src_call = callsign.upper().encode("ascii") self.src_call = callsign.upper().encode("ascii")
self.src_ssid = ssid self.src_ssid = ssid
self.dst_call = "APZRNS".encode("ascii") self.dst_call = "APZRNS".encode("ascii")
self.dst_ssid = 0 self.dst_ssid = 0
self.port = port self.port = port
self.speed = speed self.speed = speed
self.databits = databits self.databits = databits
self.parity = serial.PARITY_NONE self.parity = serial.PARITY_NONE
self.stopbits = stopbits self.stopbits = stopbits
self.timeout = 100 self.timeout = 100
self.online = False self.online = False
# TODO: Sane default and make this configurable # TODO: Sane default and make this configurable
# TODO: Changed to 25ms instead of 100ms, check it # TODO: Changed to 25ms instead of 100ms, check it
self.txdelay = 0.025 self.txdelay = 0.025
self.packet_queue = [] self.packet_queue = []
self.flow_control = flow_control self.flow_control = flow_control
self.interface_ready = False self.interface_ready = False
if (len(self.src_call) < 3 or len(self.src_call) > 6): if (len(self.src_call) < 3 or len(self.src_call) > 6):
raise ValueError("Invalid callsign for "+str(self)) raise ValueError("Invalid callsign for "+str(self))
if (self.src_ssid < 0 or self.src_ssid > 15): if (self.src_ssid < 0 or self.src_ssid > 15):
raise ValueError("Invalid SSID for "+str(self)) raise ValueError("Invalid SSID for "+str(self))
self.preamble = preamble if preamble != None else 350; self.preamble = preamble if preamble != None else 350;
self.txtail = txtail if txtail != None else 20; self.txtail = txtail if txtail != None else 20;
self.persistence = persistence if persistence != None else 64; self.persistence = persistence if persistence != None else 64;
self.slottime = slottime if slottime != None else 20; self.slottime = slottime if slottime != None else 20;
if parity.lower() == "e" or parity.lower() == "even": if parity.lower() == "e" or parity.lower() == "even":
self.parity = serial.PARITY_EVEN self.parity = serial.PARITY_EVEN
if parity.lower() == "o" or parity.lower() == "odd": if parity.lower() == "o" or parity.lower() == "odd":
self.parity = serial.PARITY_ODD self.parity = serial.PARITY_ODD
try: try:
RNS.log("Opening serial port "+self.port+"...") RNS.log("Opening serial port "+self.port+"...")
self.serial = serial.Serial( self.serial = serial.Serial(
port = self.port, port = self.port,
baudrate = self.speed, baudrate = self.speed,
bytesize = self.databits, bytesize = self.databits,
parity = self.parity, parity = self.parity,
stopbits = self.stopbits, stopbits = self.stopbits,
xonxoff = False, xonxoff = False,
rtscts = False, rtscts = False,
timeout = 0, timeout = 0,
inter_byte_timeout = None, inter_byte_timeout = None,
write_timeout = None, write_timeout = None,
dsrdtr = False, dsrdtr = False,
) )
except Exception as e: except Exception as e:
RNS.log("Could not open serial port for interface "+str(self), RNS.LOG_ERROR) RNS.log("Could not open serial port for interface "+str(self), RNS.LOG_ERROR)
raise e raise e
if self.serial.is_open: if self.serial.is_open:
# Allow time for interface to initialise before config # Allow time for interface to initialise before config
sleep(2.0) sleep(2.0)
thread = threading.Thread(target=self.readLoop) thread = threading.Thread(target=self.readLoop)
thread.setDaemon(True) thread.setDaemon(True)
thread.start() thread.start()
self.online = True self.online = True
RNS.log("Serial port "+self.port+" is now open") RNS.log("Serial port "+self.port+" is now open")
RNS.log("Configuring AX.25 KISS interface parameters...") RNS.log("Configuring AX.25 KISS interface parameters...")
self.setPreamble(self.preamble) self.setPreamble(self.preamble)
self.setTxTail(self.txtail) self.setTxTail(self.txtail)
self.setPersistence(self.persistence) self.setPersistence(self.persistence)
self.setSlotTime(self.slottime) self.setSlotTime(self.slottime)
self.setFlowControl(self.flow_control) self.setFlowControl(self.flow_control)
self.interface_ready = True self.interface_ready = True
RNS.log("AX.25 KISS interface configured") RNS.log("AX.25 KISS interface configured")
sleep(2) sleep(2)
else: else:
raise IOError("Could not open serial port") raise IOError("Could not open serial port")
def setPreamble(self, preamble): def setPreamble(self, preamble):
preamble_ms = preamble preamble_ms = preamble
preamble = int(preamble_ms / 10) preamble = int(preamble_ms / 10)
if preamble < 0: if preamble < 0:
preamble = 0 preamble = 0
if preamble > 255: if preamble > 255:
preamble = 255 preamble = 255
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_TXDELAY])+bytes([preamble])+bytes([KISS.FEND]) kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_TXDELAY])+bytes([preamble])+bytes([KISS.FEND])
written = self.serial.write(kiss_command) written = self.serial.write(kiss_command)
if written != len(kiss_command): if written != len(kiss_command):
raise IOError("Could not configure AX.25 KISS interface preamble to "+str(preamble_ms)+" (command value "+str(preamble)+")") raise IOError("Could not configure AX.25 KISS interface preamble to "+str(preamble_ms)+" (command value "+str(preamble)+")")
def setTxTail(self, txtail): def setTxTail(self, txtail):
txtail_ms = txtail txtail_ms = txtail
txtail = int(txtail_ms / 10) txtail = int(txtail_ms / 10)
if txtail < 0: if txtail < 0:
txtail = 0 txtail = 0
if txtail > 255: if txtail > 255:
txtail = 255 txtail = 255
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_TXTAIL])+bytes([txtail])+bytes([KISS.FEND]) kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_TXTAIL])+bytes([txtail])+bytes([KISS.FEND])
written = self.serial.write(kiss_command) written = self.serial.write(kiss_command)
if written != len(kiss_command): if written != len(kiss_command):
raise IOError("Could not configure AX.25 KISS interface TX tail to "+str(txtail_ms)+" (command value "+str(txtail)+")") raise IOError("Could not configure AX.25 KISS interface TX tail to "+str(txtail_ms)+" (command value "+str(txtail)+")")
def setPersistence(self, persistence): def setPersistence(self, persistence):
if persistence < 0: if persistence < 0:
persistence = 0 persistence = 0
if persistence > 255: if persistence > 255:
persistence = 255 persistence = 255
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_P])+bytes([persistence])+bytes([KISS.FEND]) kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_P])+bytes([persistence])+bytes([KISS.FEND])
written = self.serial.write(kiss_command) written = self.serial.write(kiss_command)
if written != len(kiss_command): if written != len(kiss_command):
raise IOError("Could not configure AX.25 KISS interface persistence to "+str(persistence)) raise IOError("Could not configure AX.25 KISS interface persistence to "+str(persistence))
def setSlotTime(self, slottime): def setSlotTime(self, slottime):
slottime_ms = slottime slottime_ms = slottime
slottime = int(slottime_ms / 10) slottime = int(slottime_ms / 10)
if slottime < 0: if slottime < 0:
slottime = 0 slottime = 0
if slottime > 255: if slottime > 255:
slottime = 255 slottime = 255
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_SLOTTIME])+bytes([slottime])+bytes([KISS.FEND]) kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_SLOTTIME])+bytes([slottime])+bytes([KISS.FEND])
written = self.serial.write(kiss_command) written = self.serial.write(kiss_command)
if written != len(kiss_command): if written != len(kiss_command):
raise IOError("Could not configure AX.25 KISS interface slot time to "+str(slottime_ms)+" (command value "+str(slottime)+")") raise IOError("Could not configure AX.25 KISS interface slot time to "+str(slottime_ms)+" (command value "+str(slottime)+")")
def setFlowControl(self, flow_control): def setFlowControl(self, flow_control):
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_READY])+bytes([0x01])+bytes([KISS.FEND]) kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_READY])+bytes([0x01])+bytes([KISS.FEND])
written = self.serial.write(kiss_command) written = self.serial.write(kiss_command)
if written != len(kiss_command): if written != len(kiss_command):
if (flow_control): if (flow_control):
raise IOError("Could not enable AX.25 KISS interface flow control") raise IOError("Could not enable AX.25 KISS interface flow control")
else: else:
raise IOError("Could not enable AX.25 KISS interface flow control") raise IOError("Could not enable AX.25 KISS interface flow control")
def processIncoming(self, data): def processIncoming(self, data):
if (len(data) > AX25.HEADER_SIZE): if (len(data) > AX25.HEADER_SIZE):
self.owner.inbound(data[AX25.HEADER_SIZE:], self) self.owner.inbound(data[AX25.HEADER_SIZE:], self)
def processOutgoing(self,data): def processOutgoing(self,data):
if self.online: if self.online:
if self.interface_ready: if self.interface_ready:
if self.flow_control: if self.flow_control:
self.interface_ready = False self.interface_ready = False
encoded_dst_ssid = bytes([0x60 | (self.dst_ssid << 1)]) encoded_dst_ssid = bytes([0x60 | (self.dst_ssid << 1)])
encoded_src_ssid = bytes([0x60 | (self.src_ssid << 1) | 0x01]) encoded_src_ssid = bytes([0x60 | (self.src_ssid << 1) | 0x01])
addr = b"" addr = b""
for i in range(0,6): for i in range(0,6):
if (i < len(self.dst_call)): if (i < len(self.dst_call)):
addr += bytes([self.dst_call[i]<<1]) addr += bytes([self.dst_call[i]<<1])
else: else:
addr += bytes([0x20]) addr += bytes([0x20])
addr += encoded_dst_ssid addr += encoded_dst_ssid
for i in range(0,6): for i in range(0,6):
if (i < len(self.src_call)): if (i < len(self.src_call)):
addr += bytes([self.src_call[i]<<1]) addr += bytes([self.src_call[i]<<1])
else: else:
addr += bytes([0x20]) addr += bytes([0x20])
addr += encoded_src_ssid addr += encoded_src_ssid
data = addr+bytes([AX25.CTRL_UI])+bytes([AX25.PID_NOLAYER3])+data data = addr+bytes([AX25.CTRL_UI])+bytes([AX25.PID_NOLAYER3])+data
data = data.replace(bytes([0xdb]), bytes([0xdb])+bytes([0xdd])) data = data.replace(bytes([0xdb]), bytes([0xdb])+bytes([0xdd]))
data = data.replace(bytes([0xc0]), bytes([0xdb])+bytes([0xdc])) data = data.replace(bytes([0xc0]), bytes([0xdb])+bytes([0xdc]))
kiss_frame = bytes([KISS.FEND])+bytes([0x00])+data+bytes([KISS.FEND]) kiss_frame = bytes([KISS.FEND])+bytes([0x00])+data+bytes([KISS.FEND])
if (self.txdelay > 0): if (self.txdelay > 0):
RNS.log(str(self.name)+" delaying TX for "+str(self.txdelay)+" seconds", RNS.LOG_EXTREME) RNS.log(str(self.name)+" delaying TX for "+str(self.txdelay)+" seconds", RNS.LOG_EXTREME)
sleep(self.txdelay) sleep(self.txdelay)
written = self.serial.write(kiss_frame) written = self.serial.write(kiss_frame)
if written != len(kiss_frame): if written != len(kiss_frame):
if self.flow_control: if self.flow_control:
self.interface_ready = True self.interface_ready = True
raise IOError("AX.25 interface only wrote "+str(written)+" bytes of "+str(len(kiss_frame))) raise IOError("AX.25 interface only wrote "+str(written)+" bytes of "+str(len(kiss_frame)))
else: else:
self.queue(data) self.queue(data)
def queue(self, data): def queue(self, data):
self.packet_queue.append(data) self.packet_queue.append(data)
def process_queue(self): def process_queue(self):
if len(self.packet_queue) > 0: if len(self.packet_queue) > 0:
data = self.packet_queue.pop(0) data = self.packet_queue.pop(0)
self.interface_ready = True self.interface_ready = True
self.processOutgoing(data) self.processOutgoing(data)
elif len(self.packet_queue) == 0: elif len(self.packet_queue) == 0:
self.interface_ready = True self.interface_ready = True
def readLoop(self): def readLoop(self):
try: try:
in_frame = False in_frame = False
escape = False escape = False
command = KISS.CMD_UNKNOWN command = KISS.CMD_UNKNOWN
data_buffer = b"" data_buffer = b""
last_read_ms = int(time.time()*1000) last_read_ms = int(time.time()*1000)
while self.serial.is_open: while self.serial.is_open:
if self.serial.in_waiting: if self.serial.in_waiting:
byte = ord(self.serial.read(1)) byte = ord(self.serial.read(1))
last_read_ms = int(time.time()*1000) last_read_ms = int(time.time()*1000)
if (in_frame and byte == KISS.FEND and command == KISS.CMD_DATA): if (in_frame and byte == KISS.FEND and command == KISS.CMD_DATA):
in_frame = False in_frame = False
self.processIncoming(data_buffer) self.processIncoming(data_buffer)
elif (byte == KISS.FEND): elif (byte == KISS.FEND):
in_frame = True in_frame = True
command = KISS.CMD_UNKNOWN command = KISS.CMD_UNKNOWN
data_buffer = b"" data_buffer = b""
elif (in_frame and len(data_buffer) < RNS.Reticulum.MTU+AX25.HEADER_SIZE): elif (in_frame and len(data_buffer) < RNS.Reticulum.MTU+AX25.HEADER_SIZE):
if (len(data_buffer) == 0 and command == KISS.CMD_UNKNOWN): if (len(data_buffer) == 0 and command == KISS.CMD_UNKNOWN):
# We only support one HDLC port for now, so # We only support one HDLC port for now, so
# strip off the port nibble # strip off the port nibble
byte = byte & 0x0F byte = byte & 0x0F
command = byte command = byte
elif (command == KISS.CMD_DATA): elif (command == KISS.CMD_DATA):
if (byte == KISS.FESC): if (byte == KISS.FESC):
escape = True escape = True
else: else:
if (escape): if (escape):
if (byte == KISS.TFEND): if (byte == KISS.TFEND):
byte = KISS.FEND byte = KISS.FEND
if (byte == KISS.TFESC): if (byte == KISS.TFESC):
byte = KISS.FESC byte = KISS.FESC
escape = False escape = False
data_buffer = data_buffer+bytes([byte]) data_buffer = data_buffer+bytes([byte])
elif (command == KISS.CMD_READY): elif (command == KISS.CMD_READY):
# TODO: add timeout and reset if ready # TODO: add timeout and reset if ready
# command never arrives # command never arrives
self.process_queue() self.process_queue()
else: else:
time_since_last = int(time.time()*1000) - last_read_ms time_since_last = int(time.time()*1000) - last_read_ms
if len(data_buffer) > 0 and time_since_last > self.timeout: if len(data_buffer) > 0 and time_since_last > self.timeout:
data_buffer = b"" data_buffer = b""
in_frame = False in_frame = False
command = KISS.CMD_UNKNOWN command = KISS.CMD_UNKNOWN
escape = False escape = False
sleep(0.08) sleep(0.08)
except Exception as e: except Exception as e:
self.online = False self.online = False
RNS.log("A serial port error occurred, the contained exception was: "+str(e), RNS.LOG_ERROR) RNS.log("A serial port error occurred, the contained exception was: "+str(e), RNS.LOG_ERROR)
RNS.log("The interface "+str(self.name)+" is now offline. Restart Reticulum to attempt reconnection.", RNS.LOG_ERROR) RNS.log("The interface "+str(self.name)+" is now offline. Restart Reticulum to attempt reconnection.", RNS.LOG_ERROR)
def __str__(self): def __str__(self):
return "AX25KISSInterface["+self.name+"]" return "AX25KISSInterface["+self.name+"]"

View File

@ -11,5 +11,5 @@ class Interface:
pass pass
def get_hash(self): def get_hash(self):
# TODO: Maybe expand this to something more unique # TODO: Maybe expand this to something more unique
return RNS.Identity.fullHash(str(self).encode("utf-8")) return RNS.Identity.fullHash(str(self).encode("utf-8"))

View File

@ -7,250 +7,250 @@ import time
import RNS import RNS
class KISS(): class KISS():
FEND = 0xC0 FEND = 0xC0
FESC = 0xDB FESC = 0xDB
TFEND = 0xDC TFEND = 0xDC
TFESC = 0xDD TFESC = 0xDD
CMD_UNKNOWN = 0xFE CMD_UNKNOWN = 0xFE
CMD_DATA = 0x00 CMD_DATA = 0x00
CMD_TXDELAY = 0x01 CMD_TXDELAY = 0x01
CMD_P = 0x02 CMD_P = 0x02
CMD_SLOTTIME = 0x03 CMD_SLOTTIME = 0x03
CMD_TXTAIL = 0x04 CMD_TXTAIL = 0x04
CMD_FULLDUPLEX = 0x05 CMD_FULLDUPLEX = 0x05
CMD_SETHARDWARE = 0x06 CMD_SETHARDWARE = 0x06
CMD_READY = 0x0F CMD_READY = 0x0F
CMD_RETURN = 0xFF CMD_RETURN = 0xFF
@staticmethod @staticmethod
def escape(data): def escape(data):
data = data.replace(bytes([0xdb]), bytes([0xdb, 0xdd])) data = data.replace(bytes([0xdb]), bytes([0xdb, 0xdd]))
data = data.replace(bytes([0xc0]), bytes([0xdb, 0xdc])) data = data.replace(bytes([0xc0]), bytes([0xdb, 0xdc]))
return data return data
class KISSInterface(Interface): class KISSInterface(Interface):
MAX_CHUNK = 32768 MAX_CHUNK = 32768
owner = None owner = None
port = None port = None
speed = None speed = None
databits = None databits = None
parity = None parity = None
stopbits = None stopbits = None
serial = None serial = None
def __init__(self, owner, name, port, speed, databits, parity, stopbits, preamble, txtail, persistence, slottime, flow_control): def __init__(self, owner, name, port, speed, databits, parity, stopbits, preamble, txtail, persistence, slottime, flow_control):
self.serial = None self.serial = None
self.owner = owner self.owner = owner
self.name = name self.name = name
self.port = port self.port = port
self.speed = speed self.speed = speed
self.databits = databits self.databits = databits
self.parity = serial.PARITY_NONE self.parity = serial.PARITY_NONE
self.stopbits = stopbits self.stopbits = stopbits
self.timeout = 100 self.timeout = 100
self.online = False self.online = False
self.packet_queue = [] self.packet_queue = []
self.flow_control = flow_control self.flow_control = flow_control
self.interface_ready = False self.interface_ready = False
self.preamble = preamble if preamble != None else 350; self.preamble = preamble if preamble != None else 350;
self.txtail = txtail if txtail != None else 20; self.txtail = txtail if txtail != None else 20;
self.persistence = persistence if persistence != None else 64; self.persistence = persistence if persistence != None else 64;
self.slottime = slottime if slottime != None else 20; self.slottime = slottime if slottime != None else 20;
if parity.lower() == "e" or parity.lower() == "even": if parity.lower() == "e" or parity.lower() == "even":
self.parity = serial.PARITY_EVEN self.parity = serial.PARITY_EVEN
if parity.lower() == "o" or parity.lower() == "odd": if parity.lower() == "o" or parity.lower() == "odd":
self.parity = serial.PARITY_ODD self.parity = serial.PARITY_ODD
try: try:
RNS.log("Opening serial port "+self.port+"...") RNS.log("Opening serial port "+self.port+"...")
self.serial = serial.Serial( self.serial = serial.Serial(
port = self.port, port = self.port,
baudrate = self.speed, baudrate = self.speed,
bytesize = self.databits, bytesize = self.databits,
parity = self.parity, parity = self.parity,
stopbits = self.stopbits, stopbits = self.stopbits,
xonxoff = False, xonxoff = False,
rtscts = False, rtscts = False,
timeout = 0, timeout = 0,
inter_byte_timeout = None, inter_byte_timeout = None,
write_timeout = None, write_timeout = None,
dsrdtr = False, dsrdtr = False,
) )
except Exception as e: except Exception as e:
RNS.log("Could not open serial port "+self.port, RNS.LOG_ERROR) RNS.log("Could not open serial port "+self.port, RNS.LOG_ERROR)
raise e raise e
if self.serial.is_open: if self.serial.is_open:
# Allow time for interface to initialise before config # Allow time for interface to initialise before config
sleep(2.0) sleep(2.0)
thread = threading.Thread(target=self.readLoop) thread = threading.Thread(target=self.readLoop)
thread.setDaemon(True) thread.setDaemon(True)
thread.start() thread.start()
self.online = True self.online = True
RNS.log("Serial port "+self.port+" is now open") RNS.log("Serial port "+self.port+" is now open")
RNS.log("Configuring KISS interface parameters...") RNS.log("Configuring KISS interface parameters...")
self.setPreamble(self.preamble) self.setPreamble(self.preamble)
self.setTxTail(self.txtail) self.setTxTail(self.txtail)
self.setPersistence(self.persistence) self.setPersistence(self.persistence)
self.setSlotTime(self.slottime) self.setSlotTime(self.slottime)
self.setFlowControl(self.flow_control) self.setFlowControl(self.flow_control)
self.interface_ready = True self.interface_ready = True
RNS.log("KISS interface configured") RNS.log("KISS interface configured")
else: else:
raise IOError("Could not open serial port") raise IOError("Could not open serial port")
def setPreamble(self, preamble): def setPreamble(self, preamble):
preamble_ms = preamble preamble_ms = preamble
preamble = int(preamble_ms / 10) preamble = int(preamble_ms / 10)
if preamble < 0: if preamble < 0:
preamble = 0 preamble = 0
if preamble > 255: if preamble > 255:
preamble = 255 preamble = 255
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_TXDELAY])+bytes([preamble])+bytes([KISS.FEND]) kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_TXDELAY])+bytes([preamble])+bytes([KISS.FEND])
written = self.serial.write(kiss_command) written = self.serial.write(kiss_command)
if written != len(kiss_command): if written != len(kiss_command):
raise IOError("Could not configure KISS interface preamble to "+str(preamble_ms)+" (command value "+str(preamble)+")") raise IOError("Could not configure KISS interface preamble to "+str(preamble_ms)+" (command value "+str(preamble)+")")
def setTxTail(self, txtail): def setTxTail(self, txtail):
txtail_ms = txtail txtail_ms = txtail
txtail = int(txtail_ms / 10) txtail = int(txtail_ms / 10)
if txtail < 0: if txtail < 0:
txtail = 0 txtail = 0
if txtail > 255: if txtail > 255:
txtail = 255 txtail = 255
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_TXTAIL])+bytes([txtail])+bytes([KISS.FEND]) kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_TXTAIL])+bytes([txtail])+bytes([KISS.FEND])
written = self.serial.write(kiss_command) written = self.serial.write(kiss_command)
if written != len(kiss_command): if written != len(kiss_command):
raise IOError("Could not configure KISS interface TX tail to "+str(txtail_ms)+" (command value "+str(txtail)+")") raise IOError("Could not configure KISS interface TX tail to "+str(txtail_ms)+" (command value "+str(txtail)+")")
def setPersistence(self, persistence): def setPersistence(self, persistence):
if persistence < 0: if persistence < 0:
persistence = 0 persistence = 0
if persistence > 255: if persistence > 255:
persistence = 255 persistence = 255
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_P])+bytes([persistence])+bytes([KISS.FEND]) kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_P])+bytes([persistence])+bytes([KISS.FEND])
written = self.serial.write(kiss_command) written = self.serial.write(kiss_command)
if written != len(kiss_command): if written != len(kiss_command):
raise IOError("Could not configure KISS interface persistence to "+str(persistence)) raise IOError("Could not configure KISS interface persistence to "+str(persistence))
def setSlotTime(self, slottime): def setSlotTime(self, slottime):
slottime_ms = slottime slottime_ms = slottime
slottime = int(slottime_ms / 10) slottime = int(slottime_ms / 10)
if slottime < 0: if slottime < 0:
slottime = 0 slottime = 0
if slottime > 255: if slottime > 255:
slottime = 255 slottime = 255
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_SLOTTIME])+bytes([slottime])+bytes([KISS.FEND]) kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_SLOTTIME])+bytes([slottime])+bytes([KISS.FEND])
written = self.serial.write(kiss_command) written = self.serial.write(kiss_command)
if written != len(kiss_command): if written != len(kiss_command):
raise IOError("Could not configure KISS interface slot time to "+str(slottime_ms)+" (command value "+str(slottime)+")") raise IOError("Could not configure KISS interface slot time to "+str(slottime_ms)+" (command value "+str(slottime)+")")
def setFlowControl(self, flow_control): def setFlowControl(self, flow_control):
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_READY])+bytes([0x01])+bytes([KISS.FEND]) kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_READY])+bytes([0x01])+bytes([KISS.FEND])
written = self.serial.write(kiss_command) written = self.serial.write(kiss_command)
if written != len(kiss_command): if written != len(kiss_command):
if (flow_control): if (flow_control):
raise IOError("Could not enable KISS interface flow control") raise IOError("Could not enable KISS interface flow control")
else: else:
raise IOError("Could not enable KISS interface flow control") raise IOError("Could not enable KISS interface flow control")
def processIncoming(self, data): def processIncoming(self, data):
self.owner.inbound(data, self) self.owner.inbound(data, self)
def processOutgoing(self,data): def processOutgoing(self,data):
if self.online: if self.online:
if self.interface_ready: if self.interface_ready:
if self.flow_control: if self.flow_control:
self.interface_ready = False self.interface_ready = False
data = data.replace(bytes([0xdb]), bytes([0xdb])+bytes([0xdd])) data = data.replace(bytes([0xdb]), bytes([0xdb])+bytes([0xdd]))
data = data.replace(bytes([0xc0]), bytes([0xdb])+bytes([0xdc])) data = data.replace(bytes([0xc0]), bytes([0xdb])+bytes([0xdc]))
frame = bytes([KISS.FEND])+bytes([0x00])+data+bytes([KISS.FEND]) frame = bytes([KISS.FEND])+bytes([0x00])+data+bytes([KISS.FEND])
written = self.serial.write(frame) written = self.serial.write(frame)
if written != len(frame): if written != len(frame):
raise IOError("Serial interface only wrote "+str(written)+" bytes of "+str(len(data))) raise IOError("Serial interface only wrote "+str(written)+" bytes of "+str(len(data)))
else: else:
self.queue(data) self.queue(data)
def queue(self, data): def queue(self, data):
self.packet_queue.append(data) self.packet_queue.append(data)
def process_queue(self): def process_queue(self):
if len(self.packet_queue) > 0: if len(self.packet_queue) > 0:
data = self.packet_queue.pop(0) data = self.packet_queue.pop(0)
self.interface_ready = True self.interface_ready = True
self.processOutgoing(data) self.processOutgoing(data)
elif len(self.packet_queue) == 0: elif len(self.packet_queue) == 0:
self.interface_ready = True self.interface_ready = True
def readLoop(self): def readLoop(self):
try: try:
in_frame = False in_frame = False
escape = False escape = False
command = KISS.CMD_UNKNOWN command = KISS.CMD_UNKNOWN
data_buffer = b"" data_buffer = b""
last_read_ms = int(time.time()*1000) last_read_ms = int(time.time()*1000)
while self.serial.is_open: while self.serial.is_open:
if self.serial.in_waiting: if self.serial.in_waiting:
byte = ord(self.serial.read(1)) byte = ord(self.serial.read(1))
last_read_ms = int(time.time()*1000) last_read_ms = int(time.time()*1000)
if (in_frame and byte == KISS.FEND and command == KISS.CMD_DATA): if (in_frame and byte == KISS.FEND and command == KISS.CMD_DATA):
in_frame = False in_frame = False
self.processIncoming(data_buffer) self.processIncoming(data_buffer)
elif (byte == KISS.FEND): elif (byte == KISS.FEND):
in_frame = True in_frame = True
command = KISS.CMD_UNKNOWN command = KISS.CMD_UNKNOWN
data_buffer = b"" data_buffer = b""
elif (in_frame and len(data_buffer) < RNS.Reticulum.MTU): elif (in_frame and len(data_buffer) < RNS.Reticulum.MTU):
if (len(data_buffer) == 0 and command == KISS.CMD_UNKNOWN): if (len(data_buffer) == 0 and command == KISS.CMD_UNKNOWN):
# We only support one HDLC port for now, so # We only support one HDLC port for now, so
# strip off the port nibble # strip off the port nibble
byte = byte & 0x0F byte = byte & 0x0F
command = byte command = byte
elif (command == KISS.CMD_DATA): elif (command == KISS.CMD_DATA):
if (byte == KISS.FESC): if (byte == KISS.FESC):
escape = True escape = True
else: else:
if (escape): if (escape):
if (byte == KISS.TFEND): if (byte == KISS.TFEND):
byte = KISS.FEND byte = KISS.FEND
if (byte == KISS.TFESC): if (byte == KISS.TFESC):
byte = KISS.FESC byte = KISS.FESC
escape = False escape = False
data_buffer = data_buffer+bytes([byte]) data_buffer = data_buffer+bytes([byte])
elif (command == KISS.CMD_READY): elif (command == KISS.CMD_READY):
# TODO: add timeout and reset if ready # TODO: add timeout and reset if ready
# command never arrives # command never arrives
self.process_queue() self.process_queue()
else: else:
time_since_last = int(time.time()*1000) - last_read_ms time_since_last = int(time.time()*1000) - last_read_ms
if len(data_buffer) > 0 and time_since_last > self.timeout: if len(data_buffer) > 0 and time_since_last > self.timeout:
data_buffer = b"" data_buffer = b""
in_frame = False in_frame = False
command = KISS.CMD_UNKNOWN command = KISS.CMD_UNKNOWN
escape = False escape = False
sleep(0.08) sleep(0.08)
except Exception as e: except Exception as e:
self.online = False self.online = False
RNS.log("A serial port error occurred, the contained exception was: "+str(e), RNS.LOG_ERROR) RNS.log("A serial port error occurred, the contained exception was: "+str(e), RNS.LOG_ERROR)
RNS.log("The interface "+str(self.name)+" is now offline. Restart Reticulum to attempt reconnection.", RNS.LOG_ERROR) RNS.log("The interface "+str(self.name)+" is now offline. Restart Reticulum to attempt reconnection.", RNS.LOG_ERROR)
def __str__(self): def __str__(self):
return "KISSInterface["+self.name+"]" return "KISSInterface["+self.name+"]"

View File

@ -9,454 +9,454 @@ import math
import RNS import RNS
class KISS(): class KISS():
FEND = 0xC0 FEND = 0xC0
FESC = 0xDB FESC = 0xDB
TFEND = 0xDC TFEND = 0xDC
TFESC = 0xDD TFESC = 0xDD
CMD_UNKNOWN = 0xFE CMD_UNKNOWN = 0xFE
CMD_DATA = 0x00 CMD_DATA = 0x00
CMD_FREQUENCY = 0x01 CMD_FREQUENCY = 0x01
CMD_BANDWIDTH = 0x02 CMD_BANDWIDTH = 0x02
CMD_TXPOWER = 0x03 CMD_TXPOWER = 0x03
CMD_SF = 0x04 CMD_SF = 0x04
CMD_CR = 0x05 CMD_CR = 0x05
CMD_RADIO_STATE = 0x06 CMD_RADIO_STATE = 0x06
CMD_RADIO_LOCK = 0x07 CMD_RADIO_LOCK = 0x07
CMD_DETECT = 0x08 CMD_DETECT = 0x08
CMD_READY = 0x0F CMD_READY = 0x0F
CMD_STAT_RX = 0x21 CMD_STAT_RX = 0x21
CMD_STAT_TX = 0x22 CMD_STAT_TX = 0x22
CMD_STAT_RSSI = 0x23 CMD_STAT_RSSI = 0x23
CMD_STAT_SNR = 0x24 CMD_STAT_SNR = 0x24
CMD_BLINK = 0x30 CMD_BLINK = 0x30
CMD_RANDOM = 0x40 CMD_RANDOM = 0x40
CMD_FW_VERSION = 0x50 CMD_FW_VERSION = 0x50
CMD_ROM_READ = 0x51 CMD_ROM_READ = 0x51
DETECT_REQ = 0x73 DETECT_REQ = 0x73
DETECT_RESP = 0x46 DETECT_RESP = 0x46
RADIO_STATE_OFF = 0x00 RADIO_STATE_OFF = 0x00
RADIO_STATE_ON = 0x01 RADIO_STATE_ON = 0x01
RADIO_STATE_ASK = 0xFF RADIO_STATE_ASK = 0xFF
CMD_ERROR = 0x90 CMD_ERROR = 0x90
ERROR_INITRADIO = 0x01 ERROR_INITRADIO = 0x01
ERROR_TXFAILED = 0x02 ERROR_TXFAILED = 0x02
ERROR_EEPROM_LOCKED = 0x03 ERROR_EEPROM_LOCKED = 0x03
@staticmethod @staticmethod
def escape(data): def escape(data):
data = data.replace(bytes([0xdb]), bytes([0xdb, 0xdd])) data = data.replace(bytes([0xdb]), bytes([0xdb, 0xdd]))
data = data.replace(bytes([0xc0]), bytes([0xdb, 0xdc])) data = data.replace(bytes([0xc0]), bytes([0xdb, 0xdc]))
return data return data
class RNodeInterface(Interface): class RNodeInterface(Interface):
MAX_CHUNK = 32768 MAX_CHUNK = 32768
owner = None owner = None
port = None port = None
speed = None speed = None
databits = None databits = None
parity = None parity = None
stopbits = None stopbits = None
serial = None serial = None
FREQ_MIN = 137000000 FREQ_MIN = 137000000
FREQ_MAX = 1020000000 FREQ_MAX = 1020000000
RSSI_OFFSET = 157 RSSI_OFFSET = 157
CALLSIGN_MAX_LEN = 32 CALLSIGN_MAX_LEN = 32
def __init__(self, owner, name, port, frequency = None, bandwidth = None, txpower = None, sf = None, cr = None, flow_control = False, id_interval = None, id_callsign = None): def __init__(self, owner, name, port, frequency = None, bandwidth = None, txpower = None, sf = None, cr = None, flow_control = False, id_interval = None, id_callsign = None):
self.serial = None self.serial = None
self.owner = owner self.owner = owner
self.name = name self.name = name
self.port = port self.port = port
self.speed = 115200 self.speed = 115200
self.databits = 8 self.databits = 8
self.parity = serial.PARITY_NONE self.parity = serial.PARITY_NONE
self.stopbits = 1 self.stopbits = 1
self.timeout = 100 self.timeout = 100
self.online = False self.online = False
self.frequency = frequency self.frequency = frequency
self.bandwidth = bandwidth self.bandwidth = bandwidth
self.txpower = txpower self.txpower = txpower
self.sf = sf self.sf = sf
self.cr = cr self.cr = cr
self.state = KISS.RADIO_STATE_OFF self.state = KISS.RADIO_STATE_OFF
self.bitrate = 0 self.bitrate = 0
self.last_id = 0 self.last_id = 0
self.r_frequency = None self.r_frequency = None
self.r_bandwidth = None self.r_bandwidth = None
self.r_txpower = None self.r_txpower = None
self.r_sf = None self.r_sf = None
self.r_cr = None self.r_cr = None
self.r_state = None self.r_state = None
self.r_lock = None self.r_lock = None
self.r_stat_rx = None self.r_stat_rx = None
self.r_stat_tx = None self.r_stat_tx = None
self.r_stat_rssi = None self.r_stat_rssi = None
self.r_random = None self.r_random = None
self.packet_queue = [] self.packet_queue = []
self.flow_control = flow_control self.flow_control = flow_control
self.interface_ready = False self.interface_ready = False
self.validcfg = True self.validcfg = True
if (self.frequency < RNodeInterface.FREQ_MIN or self.frequency > RNodeInterface.FREQ_MAX): if (self.frequency < RNodeInterface.FREQ_MIN or self.frequency > RNodeInterface.FREQ_MAX):
RNS.log("Invalid frequency configured for "+str(self), RNS.LOG_ERROR) RNS.log("Invalid frequency configured for "+str(self), RNS.LOG_ERROR)
self.validcfg = False self.validcfg = False
if (self.txpower < 0 or self.txpower > 17): if (self.txpower < 0 or self.txpower > 17):
RNS.log("Invalid TX power configured for "+str(self), RNS.LOG_ERROR) RNS.log("Invalid TX power configured for "+str(self), RNS.LOG_ERROR)
self.validcfg = False self.validcfg = False
if (self.bandwidth < 7800 or self.bandwidth > 500000): if (self.bandwidth < 7800 or self.bandwidth > 500000):
RNS.log("Invalid bandwidth configured for "+str(self), RNS.LOG_ERROR) RNS.log("Invalid bandwidth configured for "+str(self), RNS.LOG_ERROR)
self.validcfg = False self.validcfg = False
if (self.sf < 7 or self.sf > 12): if (self.sf < 7 or self.sf > 12):
RNS.log("Invalid spreading factor configured for "+str(self), RNS.LOG_ERROR) RNS.log("Invalid spreading factor configured for "+str(self), RNS.LOG_ERROR)
self.validcfg = False self.validcfg = False
if (self.cr < 5 or self.cr > 8): if (self.cr < 5 or self.cr > 8):
RNS.log("Invalid coding rate configured for "+str(self), RNS.LOG_ERROR) RNS.log("Invalid coding rate configured for "+str(self), RNS.LOG_ERROR)
self.validcfg = False self.validcfg = False
if id_interval != None and id_callsign != None: if id_interval != None and id_callsign != None:
if (len(id_callsign.encode("utf-8")) <= RNodeInterface.CALLSIGN_MAX_LEN): if (len(id_callsign.encode("utf-8")) <= RNodeInterface.CALLSIGN_MAX_LEN):
self.should_id = True self.should_id = True
self.id_callsign = id_callsign self.id_callsign = id_callsign
self.id_interval = id_interval self.id_interval = id_interval
else: else:
RNS.log("The encoded ID callsign for "+str(self)+" exceeds the max length of "+str(RNodeInterface.CALLSIGN_MAX_LEN)+" bytes.", RNS.LOG_ERROR) RNS.log("The encoded ID callsign for "+str(self)+" exceeds the max length of "+str(RNodeInterface.CALLSIGN_MAX_LEN)+" bytes.", RNS.LOG_ERROR)
self.validcfg = False self.validcfg = False
else: else:
self.id_interval = None self.id_interval = None
self.id_callsign = None self.id_callsign = None
if (not self.validcfg): if (not self.validcfg):
raise ValueError("The configuration for "+str(self)+" contains errors, interface is offline") raise ValueError("The configuration for "+str(self)+" contains errors, interface is offline")
try: try:
RNS.log("Opening serial port "+self.port+"...") RNS.log("Opening serial port "+self.port+"...")
self.serial = serial.Serial( self.serial = serial.Serial(
port = self.port, port = self.port,
baudrate = self.speed, baudrate = self.speed,
bytesize = self.databits, bytesize = self.databits,
parity = self.parity, parity = self.parity,
stopbits = self.stopbits, stopbits = self.stopbits,
xonxoff = False, xonxoff = False,
rtscts = False, rtscts = False,
timeout = 0, timeout = 0,
inter_byte_timeout = None, inter_byte_timeout = None,
write_timeout = None, write_timeout = None,
dsrdtr = False, dsrdtr = False,
) )
except Exception as e: except Exception as e:
RNS.log("Could not open serial port for interface "+str(self), RNS.LOG_ERROR) RNS.log("Could not open serial port for interface "+str(self), RNS.LOG_ERROR)
raise e raise e
if self.serial.is_open: if self.serial.is_open:
sleep(2.0) sleep(2.0)
thread = threading.Thread(target=self.readLoop) thread = threading.Thread(target=self.readLoop)
thread.setDaemon(True) thread.setDaemon(True)
thread.start() thread.start()
self.online = True self.online = True
RNS.log("Serial port "+self.port+" is now open") RNS.log("Serial port "+self.port+" is now open")
RNS.log("Configuring RNode interface...", RNS.LOG_VERBOSE) RNS.log("Configuring RNode interface...", RNS.LOG_VERBOSE)
self.initRadio() self.initRadio()
if (self.validateRadioState()): if (self.validateRadioState()):
self.interface_ready = True self.interface_ready = True
RNS.log(str(self)+" is configured and powered up") RNS.log(str(self)+" is configured and powered up")
sleep(1.0) sleep(1.0)
else: else:
RNS.log("After configuring "+str(self)+", the reported radio parameters did not match your configuration.", RNS.LOG_ERROR) RNS.log("After configuring "+str(self)+", the reported radio parameters did not match your configuration.", RNS.LOG_ERROR)
RNS.log("Make sure that your hardware actually supports the parameters specified in the configuration", RNS.LOG_ERROR) RNS.log("Make sure that your hardware actually supports the parameters specified in the configuration", RNS.LOG_ERROR)
RNS.log("Aborting RNode startup", RNS.LOG_ERROR) RNS.log("Aborting RNode startup", RNS.LOG_ERROR)
self.serial.close() self.serial.close()
raise IOError("RNode interface did not pass validation") raise IOError("RNode interface did not pass validation")
else: else:
raise IOError("Could not open serial port") raise IOError("Could not open serial port")
def initRadio(self): def initRadio(self):
self.setFrequency() self.setFrequency()
self.setBandwidth() self.setBandwidth()
self.setTXPower() self.setTXPower()
self.setSpreadingFactor() self.setSpreadingFactor()
self.setCodingRate() self.setCodingRate()
self.setRadioState(KISS.RADIO_STATE_ON) self.setRadioState(KISS.RADIO_STATE_ON)
def setFrequency(self): def setFrequency(self):
c1 = self.frequency >> 24 c1 = self.frequency >> 24
c2 = self.frequency >> 16 & 0xFF c2 = self.frequency >> 16 & 0xFF
c3 = self.frequency >> 8 & 0xFF c3 = self.frequency >> 8 & 0xFF
c4 = self.frequency & 0xFF c4 = self.frequency & 0xFF
data = KISS.escape(bytes([c1])+bytes([c2])+bytes([c3])+bytes([c4])) data = KISS.escape(bytes([c1])+bytes([c2])+bytes([c3])+bytes([c4]))
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_FREQUENCY])+data+bytes([KISS.FEND]) kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_FREQUENCY])+data+bytes([KISS.FEND])
written = self.serial.write(kiss_command) written = self.serial.write(kiss_command)
if written != len(kiss_command): if written != len(kiss_command):
raise IOError("An IO error occurred while configuring frequency for "+self(str)) raise IOError("An IO error occurred while configuring frequency for "+self(str))
def setBandwidth(self): def setBandwidth(self):
c1 = self.bandwidth >> 24 c1 = self.bandwidth >> 24
c2 = self.bandwidth >> 16 & 0xFF c2 = self.bandwidth >> 16 & 0xFF
c3 = self.bandwidth >> 8 & 0xFF c3 = self.bandwidth >> 8 & 0xFF
c4 = self.bandwidth & 0xFF c4 = self.bandwidth & 0xFF
data = KISS.escape(bytes([c1])+bytes([c2])+bytes([c3])+bytes([c4])) data = KISS.escape(bytes([c1])+bytes([c2])+bytes([c3])+bytes([c4]))
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_BANDWIDTH])+data+bytes([KISS.FEND]) kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_BANDWIDTH])+data+bytes([KISS.FEND])
written = self.serial.write(kiss_command) written = self.serial.write(kiss_command)
if written != len(kiss_command): if written != len(kiss_command):
raise IOError("An IO error occurred while configuring bandwidth for "+self(str)) raise IOError("An IO error occurred while configuring bandwidth for "+self(str))
def setTXPower(self): def setTXPower(self):
txp = bytes([self.txpower]) txp = bytes([self.txpower])
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_TXPOWER])+txp+bytes([KISS.FEND]) kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_TXPOWER])+txp+bytes([KISS.FEND])
written = self.serial.write(kiss_command) written = self.serial.write(kiss_command)
if written != len(kiss_command): if written != len(kiss_command):
raise IOError("An IO error occurred while configuring TX power for "+self(str)) raise IOError("An IO error occurred while configuring TX power for "+self(str))
def setSpreadingFactor(self): def setSpreadingFactor(self):
sf = bytes([self.sf]) sf = bytes([self.sf])
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_SF])+sf+bytes([KISS.FEND]) kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_SF])+sf+bytes([KISS.FEND])
written = self.serial.write(kiss_command) written = self.serial.write(kiss_command)
if written != len(kiss_command): if written != len(kiss_command):
raise IOError("An IO error occurred while configuring spreading factor for "+self(str)) raise IOError("An IO error occurred while configuring spreading factor for "+self(str))
def setCodingRate(self): def setCodingRate(self):
cr = bytes([self.cr]) cr = bytes([self.cr])
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_CR])+cr+bytes([KISS.FEND]) kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_CR])+cr+bytes([KISS.FEND])
written = self.serial.write(kiss_command) written = self.serial.write(kiss_command)
if written != len(kiss_command): if written != len(kiss_command):
raise IOError("An IO error occurred while configuring coding rate for "+self(str)) raise IOError("An IO error occurred while configuring coding rate for "+self(str))
def setRadioState(self, state): def setRadioState(self, state):
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_RADIO_STATE])+bytes([state])+bytes([KISS.FEND]) kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_RADIO_STATE])+bytes([state])+bytes([KISS.FEND])
written = self.serial.write(kiss_command) written = self.serial.write(kiss_command)
if written != len(kiss_command): if written != len(kiss_command):
raise IOError("An IO error occurred while configuring radio state for "+self(str)) raise IOError("An IO error occurred while configuring radio state for "+self(str))
def validateRadioState(self): def validateRadioState(self):
RNS.log("Validating radio configuration for "+str(self)+"...", RNS.LOG_VERBOSE) RNS.log("Validating radio configuration for "+str(self)+"...", RNS.LOG_VERBOSE)
sleep(0.25); sleep(0.25);
if (self.frequency != self.r_frequency): if (self.frequency != self.r_frequency):
RNS.log("Frequency mismatch", RNS.LOG_ERROR) RNS.log("Frequency mismatch", RNS.LOG_ERROR)
self.validcfg = False self.validcfg = False
if (self.bandwidth != self.r_bandwidth): if (self.bandwidth != self.r_bandwidth):
RNS.log("Bandwidth mismatch", RNS.LOG_ERROR) RNS.log("Bandwidth mismatch", RNS.LOG_ERROR)
self.validcfg = False self.validcfg = False
if (self.txpower != self.r_txpower): if (self.txpower != self.r_txpower):
RNS.log("TX power mismatch", RNS.LOG_ERROR) RNS.log("TX power mismatch", RNS.LOG_ERROR)
self.validcfg = False self.validcfg = False
if (self.sf != self.r_sf): if (self.sf != self.r_sf):
RNS.log("Spreading factor mismatch", RNS.LOG_ERROR) RNS.log("Spreading factor mismatch", RNS.LOG_ERROR)
self.validcfg = False self.validcfg = False
if (self.validcfg): if (self.validcfg):
return True return True
else: else:
return False return False
def updateBitrate(self): def updateBitrate(self):
try: try:
self.bitrate = self.r_sf * ( (4.0/self.r_cr) / (math.pow(2,self.r_sf)/(self.r_bandwidth/1000)) ) * 1000 self.bitrate = self.r_sf * ( (4.0/self.r_cr) / (math.pow(2,self.r_sf)/(self.r_bandwidth/1000)) ) * 1000
self.bitrate_kbps = round(self.bitrate/1000.0, 2) self.bitrate_kbps = round(self.bitrate/1000.0, 2)
RNS.log(str(self)+" On-air bitrate is now "+str(self.bitrate_kbps)+ " kbps", RNS.LOG_INFO) RNS.log(str(self)+" On-air bitrate is now "+str(self.bitrate_kbps)+ " kbps", RNS.LOG_INFO)
except: except:
self.bitrate = 0 self.bitrate = 0
def processIncoming(self, data): def processIncoming(self, data):
self.owner.inbound(data, self) self.owner.inbound(data, self)
def processOutgoing(self,data): def processOutgoing(self,data):
if self.online: if self.online:
if self.interface_ready: if self.interface_ready:
if self.flow_control: if self.flow_control:
self.interface_ready = False self.interface_ready = False
frame = b"" frame = b""
if self.id_interval != None and self.id_callsign != None: if self.id_interval != None and self.id_callsign != None:
if self.last_id + self.id_interval < time.time(): if self.last_id + self.id_interval < time.time():
self.last_id = time.time() self.last_id = time.time()
frame = bytes([0xc0])+bytes([0x00])+KISS.escape(self.id_callsign.encode("utf-8"))+bytes([0xc0]) frame = bytes([0xc0])+bytes([0x00])+KISS.escape(self.id_callsign.encode("utf-8"))+bytes([0xc0])
data = KISS.escape(data) data = KISS.escape(data)
frame += bytes([0xc0])+bytes([0x00])+data+bytes([0xc0]) frame += bytes([0xc0])+bytes([0x00])+data+bytes([0xc0])
written = self.serial.write(frame) written = self.serial.write(frame)
if written != len(frame): if written != len(frame):
raise IOError("Serial interface only wrote "+str(written)+" bytes of "+str(len(data))) raise IOError("Serial interface only wrote "+str(written)+" bytes of "+str(len(data)))
else: else:
self.queue(data) self.queue(data)
def queue(self, data): def queue(self, data):
self.packet_queue.append(data) self.packet_queue.append(data)
def process_queue(self): def process_queue(self):
if len(self.packet_queue) > 0: if len(self.packet_queue) > 0:
data = self.packet_queue.pop(0) data = self.packet_queue.pop(0)
self.interface_ready = True self.interface_ready = True
self.processOutgoing(data) self.processOutgoing(data)
elif len(self.packet_queue) == 0: elif len(self.packet_queue) == 0:
self.interface_ready = True self.interface_ready = True
def readLoop(self): def readLoop(self):
try: try:
in_frame = False in_frame = False
escape = False escape = False
command = KISS.CMD_UNKNOWN command = KISS.CMD_UNKNOWN
data_buffer = b"" data_buffer = b""
command_buffer = b"" command_buffer = b""
last_read_ms = int(time.time()*1000) last_read_ms = int(time.time()*1000)
while self.serial.is_open: while self.serial.is_open:
if self.serial.in_waiting: if self.serial.in_waiting:
byte = ord(self.serial.read(1)) byte = ord(self.serial.read(1))
last_read_ms = int(time.time()*1000) last_read_ms = int(time.time()*1000)
if (in_frame and byte == KISS.FEND and command == KISS.CMD_DATA): if (in_frame and byte == KISS.FEND and command == KISS.CMD_DATA):
in_frame = False in_frame = False
self.processIncoming(data_buffer) self.processIncoming(data_buffer)
data_buffer = b"" data_buffer = b""
command_buffer = b"" command_buffer = b""
elif (byte == KISS.FEND): elif (byte == KISS.FEND):
in_frame = True in_frame = True
command = KISS.CMD_UNKNOWN command = KISS.CMD_UNKNOWN
data_buffer = b"" data_buffer = b""
command_buffer = b"" command_buffer = b""
elif (in_frame and len(data_buffer) < RNS.Reticulum.MTU): elif (in_frame and len(data_buffer) < RNS.Reticulum.MTU):
if (len(data_buffer) == 0 and command == KISS.CMD_UNKNOWN): if (len(data_buffer) == 0 and command == KISS.CMD_UNKNOWN):
command = byte command = byte
elif (command == KISS.CMD_DATA): elif (command == KISS.CMD_DATA):
if (byte == KISS.FESC): if (byte == KISS.FESC):
escape = True escape = True
else: else:
if (escape): if (escape):
if (byte == KISS.TFEND): if (byte == KISS.TFEND):
byte = KISS.FEND byte = KISS.FEND
if (byte == KISS.TFESC): if (byte == KISS.TFESC):
byte = KISS.FESC byte = KISS.FESC
escape = False escape = False
data_buffer = data_buffer+bytes([byte]) data_buffer = data_buffer+bytes([byte])
elif (command == KISS.CMD_FREQUENCY): elif (command == KISS.CMD_FREQUENCY):
if (byte == KISS.FESC): if (byte == KISS.FESC):
escape = True escape = True
else: else:
if (escape): if (escape):
if (byte == KISS.TFEND): if (byte == KISS.TFEND):
byte = KISS.FEND byte = KISS.FEND
if (byte == KISS.TFESC): if (byte == KISS.TFESC):
byte = KISS.FESC byte = KISS.FESC
escape = False escape = False
command_buffer = command_buffer+bytes([byte]) command_buffer = command_buffer+bytes([byte])
if (len(command_buffer) == 4): if (len(command_buffer) == 4):
self.r_frequency = command_buffer[0] << 24 | command_buffer[1] << 16 | command_buffer[2] << 8 | command_buffer[3] self.r_frequency = command_buffer[0] << 24 | command_buffer[1] << 16 | command_buffer[2] << 8 | command_buffer[3]
RNS.log(str(self)+" Radio reporting frequency is "+str(self.r_frequency/1000000.0)+" MHz", RNS.LOG_DEBUG) RNS.log(str(self)+" Radio reporting frequency is "+str(self.r_frequency/1000000.0)+" MHz", RNS.LOG_DEBUG)
self.updateBitrate() self.updateBitrate()
elif (command == KISS.CMD_BANDWIDTH): elif (command == KISS.CMD_BANDWIDTH):
if (byte == KISS.FESC): if (byte == KISS.FESC):
escape = True escape = True
else: else:
if (escape): if (escape):
if (byte == KISS.TFEND): if (byte == KISS.TFEND):
byte = KISS.FEND byte = KISS.FEND
if (byte == KISS.TFESC): if (byte == KISS.TFESC):
byte = KISS.FESC byte = KISS.FESC
escape = False escape = False
command_buffer = command_buffer+bytes([byte]) command_buffer = command_buffer+bytes([byte])
if (len(command_buffer) == 4): if (len(command_buffer) == 4):
self.r_bandwidth = command_buffer[0] << 24 | command_buffer[1] << 16 | command_buffer[2] << 8 | command_buffer[3] self.r_bandwidth = command_buffer[0] << 24 | command_buffer[1] << 16 | command_buffer[2] << 8 | command_buffer[3]
RNS.log(str(self)+" Radio reporting bandwidth is "+str(self.r_bandwidth/1000.0)+" KHz", RNS.LOG_DEBUG) RNS.log(str(self)+" Radio reporting bandwidth is "+str(self.r_bandwidth/1000.0)+" KHz", RNS.LOG_DEBUG)
self.updateBitrate() self.updateBitrate()
elif (command == KISS.CMD_TXPOWER): elif (command == KISS.CMD_TXPOWER):
self.r_txpower = byte self.r_txpower = byte
RNS.log(str(self)+" Radio reporting TX power is "+str(self.r_txpower)+" dBm", RNS.LOG_DEBUG) RNS.log(str(self)+" Radio reporting TX power is "+str(self.r_txpower)+" dBm", RNS.LOG_DEBUG)
elif (command == KISS.CMD_SF): elif (command == KISS.CMD_SF):
self.r_sf = byte self.r_sf = byte
RNS.log(str(self)+" Radio reporting spreading factor is "+str(self.r_sf), RNS.LOG_DEBUG) RNS.log(str(self)+" Radio reporting spreading factor is "+str(self.r_sf), RNS.LOG_DEBUG)
self.updateBitrate() self.updateBitrate()
elif (command == KISS.CMD_CR): elif (command == KISS.CMD_CR):
self.r_cr = byte self.r_cr = byte
RNS.log(str(self)+" Radio reporting coding rate is "+str(self.r_cr), RNS.LOG_DEBUG) RNS.log(str(self)+" Radio reporting coding rate is "+str(self.r_cr), RNS.LOG_DEBUG)
self.updateBitrate() self.updateBitrate()
elif (command == KISS.CMD_RADIO_STATE): elif (command == KISS.CMD_RADIO_STATE):
self.r_state = byte self.r_state = byte
elif (command == KISS.CMD_RADIO_LOCK): elif (command == KISS.CMD_RADIO_LOCK):
self.r_lock = byte self.r_lock = byte
elif (command == KISS.CMD_STAT_RX): elif (command == KISS.CMD_STAT_RX):
if (byte == KISS.FESC): if (byte == KISS.FESC):
escape = True escape = True
else: else:
if (escape): if (escape):
if (byte == KISS.TFEND): if (byte == KISS.TFEND):
byte = KISS.FEND byte = KISS.FEND
if (byte == KISS.TFESC): if (byte == KISS.TFESC):
byte = KISS.FESC byte = KISS.FESC
escape = False escape = False
command_buffer = command_buffer+bytes([byte]) command_buffer = command_buffer+bytes([byte])
if (len(command_buffer) == 4): if (len(command_buffer) == 4):
self.r_stat_rx = ord(command_buffer[0]) << 24 | ord(command_buffer[1]) << 16 | ord(command_buffer[2]) << 8 | ord(command_buffer[3]) self.r_stat_rx = ord(command_buffer[0]) << 24 | ord(command_buffer[1]) << 16 | ord(command_buffer[2]) << 8 | ord(command_buffer[3])
elif (command == KISS.CMD_STAT_TX): elif (command == KISS.CMD_STAT_TX):
if (byte == KISS.FESC): if (byte == KISS.FESC):
escape = True escape = True
else: else:
if (escape): if (escape):
if (byte == KISS.TFEND): if (byte == KISS.TFEND):
byte = KISS.FEND byte = KISS.FEND
if (byte == KISS.TFESC): if (byte == KISS.TFESC):
byte = KISS.FESC byte = KISS.FESC
escape = False escape = False
command_buffer = command_buffer+bytes([byte]) command_buffer = command_buffer+bytes([byte])
if (len(command_buffer) == 4): if (len(command_buffer) == 4):
self.r_stat_tx = ord(command_buffer[0]) << 24 | ord(command_buffer[1]) << 16 | ord(command_buffer[2]) << 8 | ord(command_buffer[3]) self.r_stat_tx = ord(command_buffer[0]) << 24 | ord(command_buffer[1]) << 16 | ord(command_buffer[2]) << 8 | ord(command_buffer[3])
elif (command == KISS.CMD_STAT_RSSI): elif (command == KISS.CMD_STAT_RSSI):
self.r_stat_rssi = byte-RNodeInterface.RSSI_OFFSET self.r_stat_rssi = byte-RNodeInterface.RSSI_OFFSET
elif (command == KISS.CMD_STAT_SNR): elif (command == KISS.CMD_STAT_SNR):
self.r_stat_snr = int.from_bytes(bytes([byte]), byteorder="big", signed=True) * 0.25 self.r_stat_snr = int.from_bytes(bytes([byte]), byteorder="big", signed=True) * 0.25
elif (command == KISS.CMD_RANDOM): elif (command == KISS.CMD_RANDOM):
self.r_random = byte self.r_random = byte
elif (command == KISS.CMD_ERROR): elif (command == KISS.CMD_ERROR):
if (byte == KISS.ERROR_INITRADIO): if (byte == KISS.ERROR_INITRADIO):
RNS.log(str(self)+" hardware initialisation error (code "+RNS.hexrep(byte)+")", RNS.LOG_ERROR) RNS.log(str(self)+" hardware initialisation error (code "+RNS.hexrep(byte)+")", RNS.LOG_ERROR)
elif (byte == KISS.ERROR_INITRADIO): elif (byte == KISS.ERROR_INITRADIO):
RNS.log(str(self)+" hardware TX error (code "+RNS.hexrep(byte)+")", RNS.LOG_ERROR) RNS.log(str(self)+" hardware TX error (code "+RNS.hexrep(byte)+")", RNS.LOG_ERROR)
else: else:
RNS.log(str(self)+" hardware error (code "+RNS.hexrep(byte)+")", RNS.LOG_ERROR) RNS.log(str(self)+" hardware error (code "+RNS.hexrep(byte)+")", RNS.LOG_ERROR)
elif (command == KISS.CMD_READY): elif (command == KISS.CMD_READY):
self.process_queue() self.process_queue()
else: else:
time_since_last = int(time.time()*1000) - last_read_ms time_since_last = int(time.time()*1000) - last_read_ms
if len(data_buffer) > 0 and time_since_last > self.timeout: if len(data_buffer) > 0 and time_since_last > self.timeout:
RNS.log(str(self)+" serial read timeout", RNS.LOG_DEBUG) RNS.log(str(self)+" serial read timeout", RNS.LOG_DEBUG)
data_buffer = b"" data_buffer = b""
in_frame = False in_frame = False
command = KISS.CMD_UNKNOWN command = KISS.CMD_UNKNOWN
escape = False escape = False
sleep(0.08) sleep(0.08)
except Exception as e: except Exception as e:
self.online = False self.online = False
RNS.log("A serial port error occurred, the contained exception was: "+str(e), RNS.LOG_ERROR) RNS.log("A serial port error occurred, the contained exception was: "+str(e), RNS.LOG_ERROR)
RNS.log("The interface "+str(self.name)+" is now offline. Restart Reticulum to attempt reconnection.", RNS.LOG_ERROR) RNS.log("The interface "+str(self.name)+" is now offline. Restart Reticulum to attempt reconnection.", RNS.LOG_ERROR)
def __str__(self): def __str__(self):
return "RNodeInterface["+self.name+"]" return "RNodeInterface["+self.name+"]"

View File

@ -7,130 +7,130 @@ import time
import RNS import RNS
class HDLC(): class HDLC():
# The Serial Interface packetizes data using # The Serial Interface packetizes data using
# simplified HDLC framing, similar to PPP # simplified HDLC framing, similar to PPP
FLAG = 0x7E FLAG = 0x7E
ESC = 0x7D ESC = 0x7D
ESC_MASK = 0x20 ESC_MASK = 0x20
@staticmethod @staticmethod
def escape(data): def escape(data):
data = data.replace(bytes([HDLC.ESC]), bytes([HDLC.ESC, HDLC.ESC^HDLC.ESC_MASK])) data = data.replace(bytes([HDLC.ESC]), bytes([HDLC.ESC, HDLC.ESC^HDLC.ESC_MASK]))
data = data.replace(bytes([HDLC.FLAG]), bytes([HDLC.ESC, HDLC.FLAG^HDLC.ESC_MASK])) data = data.replace(bytes([HDLC.FLAG]), bytes([HDLC.ESC, HDLC.FLAG^HDLC.ESC_MASK]))
return data return data
class SerialInterface(Interface): class SerialInterface(Interface):
MAX_CHUNK = 32768 MAX_CHUNK = 32768
owner = None owner = None
port = None port = None
speed = None speed = None
databits = None databits = None
parity = None parity = None
stopbits = None stopbits = None
serial = None serial = None
def __init__(self, owner, name, port, speed, databits, parity, stopbits): def __init__(self, owner, name, port, speed, databits, parity, stopbits):
self.serial = None self.serial = None
self.owner = owner self.owner = owner
self.name = name self.name = name
self.port = port self.port = port
self.speed = speed self.speed = speed
self.databits = databits self.databits = databits
self.parity = serial.PARITY_NONE self.parity = serial.PARITY_NONE
self.stopbits = stopbits self.stopbits = stopbits
self.timeout = 100 self.timeout = 100
self.online = False self.online = False
if parity.lower() == "e" or parity.lower() == "even": if parity.lower() == "e" or parity.lower() == "even":
self.parity = serial.PARITY_EVEN self.parity = serial.PARITY_EVEN
if parity.lower() == "o" or parity.lower() == "odd": if parity.lower() == "o" or parity.lower() == "odd":
self.parity = serial.PARITY_ODD self.parity = serial.PARITY_ODD
try: try:
RNS.log("Opening serial port "+self.port+"...") RNS.log("Opening serial port "+self.port+"...")
self.serial = serial.Serial( self.serial = serial.Serial(
port = self.port, port = self.port,
baudrate = self.speed, baudrate = self.speed,
bytesize = self.databits, bytesize = self.databits,
parity = self.parity, parity = self.parity,
stopbits = self.stopbits, stopbits = self.stopbits,
xonxoff = False, xonxoff = False,
rtscts = False, rtscts = False,
timeout = 0, timeout = 0,
inter_byte_timeout = None, inter_byte_timeout = None,
write_timeout = None, write_timeout = None,
dsrdtr = False, dsrdtr = False,
) )
except Exception as e: except Exception as e:
RNS.log("Could not open serial port for interface "+str(self), RNS.LOG_ERROR) RNS.log("Could not open serial port for interface "+str(self), RNS.LOG_ERROR)
raise e raise e
if self.serial.is_open: if self.serial.is_open:
sleep(0.5) sleep(0.5)
thread = threading.Thread(target=self.readLoop) thread = threading.Thread(target=self.readLoop)
thread.setDaemon(True) thread.setDaemon(True)
thread.start() thread.start()
self.online = True self.online = True
RNS.log("Serial port "+self.port+" is now open") RNS.log("Serial port "+self.port+" is now open")
else: else:
raise IOError("Could not open serial port") raise IOError("Could not open serial port")
def processIncoming(self, data): def processIncoming(self, data):
self.owner.inbound(data, self) self.owner.inbound(data, self)
def processOutgoing(self,data): def processOutgoing(self,data):
if self.online: if self.online:
data = bytes([HDLC.FLAG])+HDLC.escape(data)+bytes([HDLC.FLAG]) data = bytes([HDLC.FLAG])+HDLC.escape(data)+bytes([HDLC.FLAG])
written = self.serial.write(data) written = self.serial.write(data)
if written != len(data): if written != len(data):
raise IOError("Serial interface only wrote "+str(written)+" bytes of "+str(len(data))) raise IOError("Serial interface only wrote "+str(written)+" bytes of "+str(len(data)))
def readLoop(self): def readLoop(self):
try: try:
in_frame = False in_frame = False
escape = False escape = False
data_buffer = b"" data_buffer = b""
last_read_ms = int(time.time()*1000) last_read_ms = int(time.time()*1000)
while self.serial.is_open: while self.serial.is_open:
if self.serial.in_waiting: if self.serial.in_waiting:
byte = ord(self.serial.read(1)) byte = ord(self.serial.read(1))
last_read_ms = int(time.time()*1000) last_read_ms = int(time.time()*1000)
if (in_frame and byte == HDLC.FLAG): if (in_frame and byte == HDLC.FLAG):
in_frame = False in_frame = False
self.processIncoming(data_buffer) self.processIncoming(data_buffer)
elif (byte == HDLC.FLAG): elif (byte == HDLC.FLAG):
in_frame = True in_frame = True
data_buffer = b"" data_buffer = b""
elif (in_frame and len(data_buffer) < RNS.Reticulum.MTU): elif (in_frame and len(data_buffer) < RNS.Reticulum.MTU):
if (byte == HDLC.ESC): if (byte == HDLC.ESC):
escape = True escape = True
else: else:
if (escape): if (escape):
if (byte == HDLC.FLAG ^ HDLC.ESC_MASK): if (byte == HDLC.FLAG ^ HDLC.ESC_MASK):
byte = HDLC.FLAG byte = HDLC.FLAG
if (byte == HDLC.ESC ^ HDLC.ESC_MASK): if (byte == HDLC.ESC ^ HDLC.ESC_MASK):
byte = HDLC.ESC byte = HDLC.ESC
escape = False escape = False
data_buffer = data_buffer+bytes([byte]) data_buffer = data_buffer+bytes([byte])
else: else:
time_since_last = int(time.time()*1000) - last_read_ms time_since_last = int(time.time()*1000) - last_read_ms
if len(data_buffer) > 0 and time_since_last > self.timeout: if len(data_buffer) > 0 and time_since_last > self.timeout:
data_buffer = b"" data_buffer = b""
in_frame = False in_frame = False
escape = False escape = False
sleep(0.08) sleep(0.08)
except Exception as e: except Exception as e:
self.online = False self.online = False
RNS.log("A serial port error occurred, the contained exception was: "+str(e), RNS.LOG_ERROR) RNS.log("A serial port error occurred, the contained exception was: "+str(e), RNS.LOG_ERROR)
RNS.log("The interface "+str(self.name)+" is now offline. Restart Reticulum to attempt reconnection.", RNS.LOG_ERROR) RNS.log("The interface "+str(self.name)+" is now offline. Restart Reticulum to attempt reconnection.", RNS.LOG_ERROR)
def __str__(self): def __str__(self):
return "SerialInterface["+self.name+"]" return "SerialInterface["+self.name+"]"

File diff suppressed because it is too large Load Diff

View File

@ -5,411 +5,411 @@ import time
import RNS import RNS
class Packet: class Packet:
# Packet types # Packet types
DATA = 0x00 # Data packets DATA = 0x00 # Data packets
ANNOUNCE = 0x01 # Announces ANNOUNCE = 0x01 # Announces
LINKREQUEST = 0x02 # Link requests LINKREQUEST = 0x02 # Link requests
PROOF = 0x03 # Proofs PROOF = 0x03 # Proofs
types = [DATA, ANNOUNCE, LINKREQUEST, PROOF] types = [DATA, ANNOUNCE, LINKREQUEST, PROOF]
# Header types # Header types
HEADER_1 = 0x00 # Normal header format HEADER_1 = 0x00 # Normal header format
HEADER_2 = 0x01 # Header format used for packets in transport HEADER_2 = 0x01 # Header format used for packets in transport
HEADER_3 = 0x02 # Reserved HEADER_3 = 0x02 # Reserved
HEADER_4 = 0x03 # Reserved HEADER_4 = 0x03 # Reserved
header_types = [HEADER_1, HEADER_2, HEADER_3, HEADER_4] header_types = [HEADER_1, HEADER_2, HEADER_3, HEADER_4]
# Data packet context types # Data packet context types
NONE = 0x00 # Generic data packet NONE = 0x00 # Generic data packet
RESOURCE = 0x01 # Packet is part of a resource RESOURCE = 0x01 # Packet is part of a resource
RESOURCE_ADV = 0x02 # Packet is a resource advertisement RESOURCE_ADV = 0x02 # Packet is a resource advertisement
RESOURCE_REQ = 0x03 # Packet is a resource part request RESOURCE_REQ = 0x03 # Packet is a resource part request
RESOURCE_HMU = 0x04 # Packet is a resource hashmap update RESOURCE_HMU = 0x04 # Packet is a resource hashmap update
RESOURCE_PRF = 0x05 # Packet is a resource proof RESOURCE_PRF = 0x05 # Packet is a resource proof
RESOURCE_ICL = 0x06 # Packet is a resource initiator cancel message RESOURCE_ICL = 0x06 # Packet is a resource initiator cancel message
RESOURCE_RCL = 0x07 # Packet is a resource receiver cancel message RESOURCE_RCL = 0x07 # Packet is a resource receiver cancel message
CACHE_REQUEST = 0x08 # Packet is a cache request CACHE_REQUEST = 0x08 # Packet is a cache request
REQUEST = 0x09 # Packet is a request REQUEST = 0x09 # Packet is a request
RESPONSE = 0x0A # Packet is a response to a request RESPONSE = 0x0A # Packet is a response to a request
PATH_RESPONSE = 0x0B # Packet is a response to a path request PATH_RESPONSE = 0x0B # Packet is a response to a path request
COMMAND = 0x0C # Packet is a command COMMAND = 0x0C # Packet is a command
COMMAND_STATUS = 0x0D # Packet is a status of an executed command COMMAND_STATUS = 0x0D # Packet is a status of an executed command
KEEPALIVE = 0xFB # Packet is a keepalive packet KEEPALIVE = 0xFB # Packet is a keepalive packet
LINKCLOSE = 0xFC # Packet is a link close message LINKCLOSE = 0xFC # Packet is a link close message
LINKPROOF = 0xFD # Packet is a link packet proof LINKPROOF = 0xFD # Packet is a link packet proof
LRRTT = 0xFE # Packet is a link request round-trip time measurement LRRTT = 0xFE # Packet is a link request round-trip time measurement
LRPROOF = 0xFF # Packet is a link request proof LRPROOF = 0xFF # Packet is a link request proof
# This is used to calculate allowable # This is used to calculate allowable
# payload sizes # payload sizes
HEADER_MAXSIZE = 23 HEADER_MAXSIZE = 23
MDU = RNS.Reticulum.MDU MDU = RNS.Reticulum.MDU
# With an MTU of 500, the maximum RSA-encrypted # With an MTU of 500, the maximum RSA-encrypted
# amount of data we can send in a single packet # amount of data we can send in a single packet
# is given by the below calculation; 258 bytes. # is given by the below calculation; 258 bytes.
RSA_MDU = math.floor(MDU/RNS.Identity.DECRYPT_CHUNKSIZE)*RNS.Identity.ENCRYPT_CHUNKSIZE RSA_MDU = math.floor(MDU/RNS.Identity.DECRYPT_CHUNKSIZE)*RNS.Identity.ENCRYPT_CHUNKSIZE
PLAIN_MDU = MDU PLAIN_MDU = MDU
# TODO: This should be calculated # TODO: This should be calculated
# more intelligently # more intelligently
# Default packet timeout # Default packet timeout
TIMEOUT = 60 TIMEOUT = 60
def __init__(self, destination, data, packet_type = DATA, context = NONE, transport_type = RNS.Transport.BROADCAST, header_type = HEADER_1, transport_id = None, attached_interface = None, create_receipt = True): def __init__(self, destination, data, packet_type = DATA, context = NONE, transport_type = RNS.Transport.BROADCAST, header_type = HEADER_1, transport_id = None, attached_interface = None, create_receipt = True):
if destination != None: if destination != None:
if transport_type == None: if transport_type == None:
transport_type = RNS.Transport.BROADCAST transport_type = RNS.Transport.BROADCAST
self.header_type = header_type self.header_type = header_type
self.packet_type = packet_type self.packet_type = packet_type
self.transport_type = transport_type self.transport_type = transport_type
self.context = context self.context = context
self.hops = 0; self.hops = 0;
self.destination = destination self.destination = destination
self.transport_id = transport_id self.transport_id = transport_id
self.data = data self.data = data
self.flags = self.getPackedFlags() self.flags = self.getPackedFlags()
self.raw = None self.raw = None
self.packed = False self.packed = False
self.sent = False self.sent = False
self.create_receipt = create_receipt self.create_receipt = create_receipt
self.receipt = None self.receipt = None
self.fromPacked = False self.fromPacked = False
else: else:
self.raw = data self.raw = data
self.packed = True self.packed = True
self.fromPacked = True self.fromPacked = True
self.create_receipt = False self.create_receipt = False
self.MTU = RNS.Reticulum.MTU self.MTU = RNS.Reticulum.MTU
self.sent_at = None self.sent_at = None
self.packet_hash = None self.packet_hash = None
self.attached_interface = attached_interface self.attached_interface = attached_interface
self.receiving_interface = None self.receiving_interface = None
def getPackedFlags(self): def getPackedFlags(self):
if self.context == Packet.LRPROOF: if self.context == Packet.LRPROOF:
packed_flags = (self.header_type << 6) | (self.transport_type << 4) | RNS.Destination.LINK | self.packet_type packed_flags = (self.header_type << 6) | (self.transport_type << 4) | RNS.Destination.LINK | self.packet_type
else: else:
packed_flags = (self.header_type << 6) | (self.transport_type << 4) | (self.destination.type << 2) | self.packet_type packed_flags = (self.header_type << 6) | (self.transport_type << 4) | (self.destination.type << 2) | self.packet_type
return packed_flags return packed_flags
def pack(self): def pack(self):
self.destination_hash = self.destination.hash self.destination_hash = self.destination.hash
self.header = b"" self.header = b""
self.header += struct.pack("!B", self.flags) self.header += struct.pack("!B", self.flags)
self.header += struct.pack("!B", self.hops) self.header += struct.pack("!B", self.hops)
if self.context == Packet.LRPROOF: if self.context == Packet.LRPROOF:
self.header += self.destination.link_id self.header += self.destination.link_id
self.ciphertext = self.data self.ciphertext = self.data
else: else:
if self.header_type == Packet.HEADER_1: if self.header_type == Packet.HEADER_1:
self.header += self.destination.hash self.header += self.destination.hash
if self.packet_type == Packet.ANNOUNCE: if self.packet_type == Packet.ANNOUNCE:
# Announce packets are not encrypted # Announce packets are not encrypted
self.ciphertext = self.data self.ciphertext = self.data
elif self.packet_type == Packet.PROOF and self.context == Packet.RESOURCE_PRF: elif self.packet_type == Packet.PROOF and self.context == Packet.RESOURCE_PRF:
# Resource proofs are not encrypted # Resource proofs are not encrypted
self.ciphertext = self.data self.ciphertext = self.data
elif self.packet_type == Packet.PROOF and self.destination.type == RNS.Destination.LINK: elif self.packet_type == Packet.PROOF and self.destination.type == RNS.Destination.LINK:
# Packet proofs over links are not encrypted # Packet proofs over links are not encrypted
self.ciphertext = self.data self.ciphertext = self.data
elif self.context == Packet.RESOURCE: elif self.context == Packet.RESOURCE:
# A resource takes care of symmetric # A resource takes care of symmetric
# encryption by itself # encryption by itself
self.ciphertext = self.data self.ciphertext = self.data
elif self.context == Packet.KEEPALIVE: elif self.context == Packet.KEEPALIVE:
# Keepalive packets contain no actual # Keepalive packets contain no actual
# data # data
self.ciphertext = self.data self.ciphertext = self.data
elif self.context == Packet.CACHE_REQUEST: elif self.context == Packet.CACHE_REQUEST:
# Cache-requests are not encrypted # Cache-requests are not encrypted
self.ciphertext = self.data self.ciphertext = self.data
else: else:
# In all other cases, we encrypt the packet # In all other cases, we encrypt the packet
# with the destination's encryption method # with the destination's encryption method
self.ciphertext = self.destination.encrypt(self.data) self.ciphertext = self.destination.encrypt(self.data)
if self.header_type == Packet.HEADER_2: if self.header_type == Packet.HEADER_2:
if self.transport_id != None: if self.transport_id != None:
self.header += self.transport_id self.header += self.transport_id
self.header += self.destination.hash self.header += self.destination.hash
if self.packet_type == Packet.ANNOUNCE: if self.packet_type == Packet.ANNOUNCE:
# Announce packets are not encrypted # Announce packets are not encrypted
self.ciphertext = self.data self.ciphertext = self.data
else: else:
raise IOError("Packet with header type 2 must have a transport ID") raise IOError("Packet with header type 2 must have a transport ID")
self.header += bytes([self.context]) self.header += bytes([self.context])
self.raw = self.header + self.ciphertext self.raw = self.header + self.ciphertext
if len(self.raw) > self.MTU: if len(self.raw) > self.MTU:
raise IOError("Packet size of "+str(len(self.raw))+" exceeds MTU of "+str(self.MTU)+" bytes") raise IOError("Packet size of "+str(len(self.raw))+" exceeds MTU of "+str(self.MTU)+" bytes")
self.packed = True self.packed = True
self.updateHash() self.updateHash()
def unpack(self): def unpack(self):
self.flags = self.raw[0] self.flags = self.raw[0]
self.hops = self.raw[1] self.hops = self.raw[1]
self.header_type = (self.flags & 0b11000000) >> 6 self.header_type = (self.flags & 0b11000000) >> 6
self.transport_type = (self.flags & 0b00110000) >> 4 self.transport_type = (self.flags & 0b00110000) >> 4
self.destination_type = (self.flags & 0b00001100) >> 2 self.destination_type = (self.flags & 0b00001100) >> 2
self.packet_type = (self.flags & 0b00000011) self.packet_type = (self.flags & 0b00000011)
if self.header_type == Packet.HEADER_2: if self.header_type == Packet.HEADER_2:
self.transport_id = self.raw[2:12] self.transport_id = self.raw[2:12]
self.destination_hash = self.raw[12:22] self.destination_hash = self.raw[12:22]
self.context = ord(self.raw[22:23]) self.context = ord(self.raw[22:23])
self.data = self.raw[23:] self.data = self.raw[23:]
else: else:
self.transport_id = None self.transport_id = None
self.destination_hash = self.raw[2:12] self.destination_hash = self.raw[2:12]
self.context = ord(self.raw[12:13]) self.context = ord(self.raw[12:13])
self.data = self.raw[13:] self.data = self.raw[13:]
self.packed = False self.packed = False
self.updateHash() self.updateHash()
# Sends the packet. Returns a receipt if one is generated, # Sends the packet. Returns a receipt if one is generated,
# or None if no receipt is available. Returns False if the # or None if no receipt is available. Returns False if the
# packet could not be sent. # packet could not be sent.
def send(self): def send(self):
if not self.sent: if not self.sent:
if self.destination.type == RNS.Destination.LINK: if self.destination.type == RNS.Destination.LINK:
if self.destination.status == RNS.Link.CLOSED: if self.destination.status == RNS.Link.CLOSED:
raise IOError("Attempt to transmit over a closed link") raise IOError("Attempt to transmit over a closed link")
else: else:
self.destination.last_outbound = time.time() self.destination.last_outbound = time.time()
self.destination.tx += 1 self.destination.tx += 1
self.destination.txbytes += len(self.data) self.destination.txbytes += len(self.data)
if not self.packed: if not self.packed:
self.pack() self.pack()
if RNS.Transport.outbound(self): if RNS.Transport.outbound(self):
return self.receipt return self.receipt
else: else:
RNS.log("No interfaces could process the outbound packet", RNS.LOG_ERROR) RNS.log("No interfaces could process the outbound packet", RNS.LOG_ERROR)
self.sent = False self.sent = False
self.receipt = None self.receipt = None
return False return False
else: else:
raise IOError("Packet was already sent") raise IOError("Packet was already sent")
def resend(self): def resend(self):
if self.sent: if self.sent:
if RNS.Transport.outbound(self): if RNS.Transport.outbound(self):
return self.receipt return self.receipt
else: else:
RNS.log("No interfaces could process the outbound packet", RNS.LOG_ERROR) RNS.log("No interfaces could process the outbound packet", RNS.LOG_ERROR)
self.sent = False self.sent = False
self.receipt = None self.receipt = None
return False return False
else: else:
raise IOError("Packet was not sent yet") raise IOError("Packet was not sent yet")
def prove(self, destination=None): def prove(self, destination=None):
if self.fromPacked and hasattr(self, "destination") and self.destination: if self.fromPacked and hasattr(self, "destination") and self.destination:
if self.destination.identity and self.destination.identity.prv: if self.destination.identity and self.destination.identity.prv:
self.destination.identity.prove(self, destination) self.destination.identity.prove(self, destination)
elif self.fromPacked and hasattr(self, "link") and self.link: elif self.fromPacked and hasattr(self, "link") and self.link:
self.link.prove_packet(self) self.link.prove_packet(self)
else: else:
RNS.log("Could not prove packet associated with neither a destination nor a link", RNS.LOG_ERROR) RNS.log("Could not prove packet associated with neither a destination nor a link", RNS.LOG_ERROR)
# Generates a special destination that allows Reticulum # Generates a special destination that allows Reticulum
# to direct the proof back to the proved packet's sender # to direct the proof back to the proved packet's sender
def generateProofDestination(self): def generateProofDestination(self):
return ProofDestination(self) return ProofDestination(self)
def validateProofPacket(self, proof_packet): def validateProofPacket(self, proof_packet):
return self.receipt.validateProofPacket(proof_packet) return self.receipt.validateProofPacket(proof_packet)
def validateProof(self, proof): def validateProof(self, proof):
return self.receipt.validateProof(proof) return self.receipt.validateProof(proof)
def updateHash(self): def updateHash(self):
self.packet_hash = self.getHash() self.packet_hash = self.getHash()
def getHash(self): def getHash(self):
return RNS.Identity.fullHash(self.getHashablePart()) return RNS.Identity.fullHash(self.getHashablePart())
def getTruncatedHash(self): def getTruncatedHash(self):
return RNS.Identity.truncatedHash(self.getHashablePart()) return RNS.Identity.truncatedHash(self.getHashablePart())
def getHashablePart(self): def getHashablePart(self):
hashable_part = bytes([self.raw[0] & 0b00001111]) hashable_part = bytes([self.raw[0] & 0b00001111])
if self.header_type == Packet.HEADER_2: if self.header_type == Packet.HEADER_2:
hashable_part += self.raw[12:] hashable_part += self.raw[12:]
else: else:
hashable_part += self.raw[2:] hashable_part += self.raw[2:]
return hashable_part return hashable_part
class ProofDestination: class ProofDestination:
def __init__(self, packet): def __init__(self, packet):
self.hash = packet.getHash()[:10]; self.hash = packet.getHash()[:10];
self.type = RNS.Destination.SINGLE self.type = RNS.Destination.SINGLE
def encrypt(self, plaintext): def encrypt(self, plaintext):
return plaintext return plaintext
class PacketReceipt: class PacketReceipt:
# Receipt status constants # Receipt status constants
FAILED = 0x00 FAILED = 0x00
SENT = 0x01 SENT = 0x01
DELIVERED = 0x02 DELIVERED = 0x02
CULLED = 0xFF CULLED = 0xFF
EXPL_LENGTH = RNS.Identity.HASHLENGTH//8+RNS.Identity.SIGLENGTH//8 EXPL_LENGTH = RNS.Identity.HASHLENGTH//8+RNS.Identity.SIGLENGTH//8
IMPL_LENGTH = RNS.Identity.SIGLENGTH//8 IMPL_LENGTH = RNS.Identity.SIGLENGTH//8
# Creates a new packet receipt from a sent packet # Creates a new packet receipt from a sent packet
def __init__(self, packet): def __init__(self, packet):
self.hash = packet.getHash() self.hash = packet.getHash()
self.sent = True self.sent = True
self.sent_at = time.time() self.sent_at = time.time()
self.timeout = Packet.TIMEOUT self.timeout = Packet.TIMEOUT
self.proved = False self.proved = False
self.status = PacketReceipt.SENT self.status = PacketReceipt.SENT
self.destination = packet.destination self.destination = packet.destination
self.callbacks = PacketReceiptCallbacks() self.callbacks = PacketReceiptCallbacks()
self.concluded_at = None self.concluded_at = None
# Validate a proof packet # Validate a proof packet
def validateProofPacket(self, proof_packet): def validateProofPacket(self, proof_packet):
if hasattr(proof_packet, "link") and proof_packet.link: if hasattr(proof_packet, "link") and proof_packet.link:
return self.validate_link_proof(proof_packet.data, proof_packet.link) return self.validate_link_proof(proof_packet.data, proof_packet.link)
else: else:
return self.validateProof(proof_packet.data) return self.validateProof(proof_packet.data)
# Validate a raw proof for a link # Validate a raw proof for a link
def validate_link_proof(self, proof, link): def validate_link_proof(self, proof, link):
# TODO: Hardcoded as explicit proofs for now # TODO: Hardcoded as explicit proofs for now
if True or len(proof) == PacketReceipt.EXPL_LENGTH: if True or len(proof) == PacketReceipt.EXPL_LENGTH:
# This is an explicit proof # This is an explicit proof
proof_hash = proof[:RNS.Identity.HASHLENGTH//8] proof_hash = proof[:RNS.Identity.HASHLENGTH//8]
signature = proof[RNS.Identity.HASHLENGTH//8:RNS.Identity.HASHLENGTH//8+RNS.Identity.SIGLENGTH//8] signature = proof[RNS.Identity.HASHLENGTH//8:RNS.Identity.HASHLENGTH//8+RNS.Identity.SIGLENGTH//8]
if proof_hash == self.hash: if proof_hash == self.hash:
proof_valid = link.validate(signature, self.hash) proof_valid = link.validate(signature, self.hash)
if proof_valid: if proof_valid:
self.status = PacketReceipt.DELIVERED self.status = PacketReceipt.DELIVERED
self.proved = True self.proved = True
self.concluded_at = time.time() self.concluded_at = time.time()
if self.callbacks.delivery != None: if self.callbacks.delivery != None:
self.callbacks.delivery(self) self.callbacks.delivery(self)
return True return True
else: else:
return False return False
else: else:
return False return False
elif len(proof) == PacketReceipt.IMPL_LENGTH: elif len(proof) == PacketReceipt.IMPL_LENGTH:
pass pass
# TODO: Why is this disabled? # TODO: Why is this disabled?
# signature = proof[:RNS.Identity.SIGLENGTH//8] # signature = proof[:RNS.Identity.SIGLENGTH//8]
# proof_valid = self.link.validate(signature, self.hash) # proof_valid = self.link.validate(signature, self.hash)
# if proof_valid: # if proof_valid:
# self.status = PacketReceipt.DELIVERED # self.status = PacketReceipt.DELIVERED
# self.proved = True # self.proved = True
# self.concluded_at = time.time() # self.concluded_at = time.time()
# if self.callbacks.delivery != None: # if self.callbacks.delivery != None:
# self.callbacks.delivery(self) # self.callbacks.delivery(self)
# RNS.log("valid") # RNS.log("valid")
# return True # return True
# else: # else:
# RNS.log("invalid") # RNS.log("invalid")
# return False # return False
else: else:
return False return False
# Validate a raw proof # Validate a raw proof
def validateProof(self, proof): def validateProof(self, proof):
if len(proof) == PacketReceipt.EXPL_LENGTH: if len(proof) == PacketReceipt.EXPL_LENGTH:
# This is an explicit proof # This is an explicit proof
proof_hash = proof[:RNS.Identity.HASHLENGTH//8] proof_hash = proof[:RNS.Identity.HASHLENGTH//8]
signature = proof[RNS.Identity.HASHLENGTH//8:RNS.Identity.HASHLENGTH//8+RNS.Identity.SIGLENGTH//8] signature = proof[RNS.Identity.HASHLENGTH//8:RNS.Identity.HASHLENGTH//8+RNS.Identity.SIGLENGTH//8]
if proof_hash == self.hash: if proof_hash == self.hash:
proof_valid = self.destination.identity.validate(signature, self.hash) proof_valid = self.destination.identity.validate(signature, self.hash)
if proof_valid: if proof_valid:
self.status = PacketReceipt.DELIVERED self.status = PacketReceipt.DELIVERED
self.proved = True self.proved = True
self.concluded_at = time.time() self.concluded_at = time.time()
if self.callbacks.delivery != None: if self.callbacks.delivery != None:
self.callbacks.delivery(self) self.callbacks.delivery(self)
return True return True
else: else:
return False return False
else: else:
return False return False
elif len(proof) == PacketReceipt.IMPL_LENGTH: elif len(proof) == PacketReceipt.IMPL_LENGTH:
# This is an implicit proof # This is an implicit proof
if self.destination.identity == None: if self.destination.identity == None:
return False return False
signature = proof[:RNS.Identity.SIGLENGTH//8] signature = proof[:RNS.Identity.SIGLENGTH//8]
proof_valid = self.destination.identity.validate(signature, self.hash) proof_valid = self.destination.identity.validate(signature, self.hash)
if proof_valid: if proof_valid:
self.status = PacketReceipt.DELIVERED self.status = PacketReceipt.DELIVERED
self.proved = True self.proved = True
self.concluded_at = time.time() self.concluded_at = time.time()
if self.callbacks.delivery != None: if self.callbacks.delivery != None:
self.callbacks.delivery(self) self.callbacks.delivery(self)
return True return True
else: else:
return False return False
else: else:
return False return False
def rtt(self): def rtt(self):
return self.concluded_at - self.sent_at return self.concluded_at - self.sent_at
def is_timed_out(self): def is_timed_out(self):
return (self.sent_at+self.timeout < time.time()) return (self.sent_at+self.timeout < time.time())
def check_timeout(self): def check_timeout(self):
if self.is_timed_out(): if self.is_timed_out():
if self.timeout == -1: if self.timeout == -1:
self.status = PacketReceipt.CULLED self.status = PacketReceipt.CULLED
else: else:
self.status = PacketReceipt.FAILED self.status = PacketReceipt.FAILED
self.concluded_at = time.time() self.concluded_at = time.time()
if self.callbacks.timeout: if self.callbacks.timeout:
thread = threading.Thread(target=self.callbacks.timeout, args=(self,)) thread = threading.Thread(target=self.callbacks.timeout, args=(self,))
thread.setDaemon(True) thread.setDaemon(True)
thread.start() thread.start()
#self.callbacks.timeout(self) #self.callbacks.timeout(self)
# Set the timeout in seconds # Set the timeout in seconds
def set_timeout(self, timeout): def set_timeout(self, timeout):
self.timeout = float(timeout) self.timeout = float(timeout)
# Set a function that gets called when # Set a function that gets called when
# a successfull delivery has been proved # a successfull delivery has been proved
def delivery_callback(self, callback): def delivery_callback(self, callback):
self.callbacks.delivery = callback self.callbacks.delivery = callback
# Set a function that gets called if the # Set a function that gets called if the
# delivery times out # delivery times out
def timeout_callback(self, callback): def timeout_callback(self, callback):
self.callbacks.timeout = callback self.callbacks.timeout = callback
class PacketReceiptCallbacks: class PacketReceiptCallbacks:
def __init__(self): def __init__(self):
self.delivery = None self.delivery = None
self.timeout = None self.timeout = None

View File

@ -10,387 +10,387 @@ import os
import RNS import RNS
class Reticulum: class Reticulum:
MTU = 500 MTU = 500
HEADER_MAXSIZE = 23 HEADER_MAXSIZE = 23
MDU = MTU - HEADER_MAXSIZE MDU = MTU - HEADER_MAXSIZE
router = None router = None
config = None config = None
configdir = os.path.expanduser("~")+"/.reticulum" configdir = os.path.expanduser("~")+"/.reticulum"
configpath = "" configpath = ""
storagepath = "" storagepath = ""
cachepath = "" cachepath = ""
@staticmethod @staticmethod
def exit_handler(): def exit_handler():
RNS.Transport.exitHandler() RNS.Transport.exitHandler()
RNS.Identity.exitHandler() RNS.Identity.exitHandler()
def __init__(self,configdir=None): def __init__(self,configdir=None):
if configdir != None: if configdir != None:
Reticulum.configdir = configdir Reticulum.configdir = configdir
Reticulum.configpath = Reticulum.configdir+"/config" Reticulum.configpath = Reticulum.configdir+"/config"
Reticulum.storagepath = Reticulum.configdir+"/storage" Reticulum.storagepath = Reticulum.configdir+"/storage"
Reticulum.cachepath = Reticulum.configdir+"/storage/cache" Reticulum.cachepath = Reticulum.configdir+"/storage/cache"
Reticulum.resourcepath = Reticulum.configdir+"/storage/resources" Reticulum.resourcepath = Reticulum.configdir+"/storage/resources"
Reticulum.__allow_unencrypted = False Reticulum.__allow_unencrypted = False
Reticulum.__transport_enabled = False Reticulum.__transport_enabled = False
Reticulum.__use_implicit_proof = True Reticulum.__use_implicit_proof = True
self.local_interface_port = 37428 self.local_interface_port = 37428
self.share_instance = True self.share_instance = True
self.is_shared_instance = False self.is_shared_instance = False
self.is_connected_to_shared_instance = False self.is_connected_to_shared_instance = False
self.is_standalone_instance = False self.is_standalone_instance = False
if not os.path.isdir(Reticulum.storagepath): if not os.path.isdir(Reticulum.storagepath):
os.makedirs(Reticulum.storagepath) os.makedirs(Reticulum.storagepath)
if not os.path.isdir(Reticulum.cachepath): if not os.path.isdir(Reticulum.cachepath):
os.makedirs(Reticulum.cachepath) os.makedirs(Reticulum.cachepath)
if not os.path.isdir(Reticulum.resourcepath): if not os.path.isdir(Reticulum.resourcepath):
os.makedirs(Reticulum.resourcepath) os.makedirs(Reticulum.resourcepath)
if os.path.isfile(self.configpath): if os.path.isfile(self.configpath):
try: try:
self.config = ConfigObj(self.configpath) self.config = ConfigObj(self.configpath)
RNS.log("Configuration loaded from "+self.configpath) RNS.log("Configuration loaded from "+self.configpath)
except Exception as e: except Exception as e:
RNS.log("Could not parse the configuration at "+self.configpath, RNS.LOG_ERROR) RNS.log("Could not parse the configuration at "+self.configpath, RNS.LOG_ERROR)
RNS.log("Check your configuration file for errors!", RNS.LOG_ERROR) RNS.log("Check your configuration file for errors!", RNS.LOG_ERROR)
RNS.panic() RNS.panic()
else: else:
RNS.log("Could not load config file, creating default configuration file...") RNS.log("Could not load config file, creating default configuration file...")
self.createDefaultConfig() self.createDefaultConfig()
RNS.log("Default config file created. Make any necessary changes in "+Reticulum.configdir+"/config and start Reticulum again.") RNS.log("Default config file created. Make any necessary changes in "+Reticulum.configdir+"/config and start Reticulum again.")
RNS.log("Exiting now!") RNS.log("Exiting now!")
exit(1) exit(1)
self.applyConfig() self.applyConfig()
RNS.Identity.loadKnownDestinations() RNS.Identity.loadKnownDestinations()
RNS.Transport.start(self) RNS.Transport.start(self)
atexit.register(Reticulum.exit_handler) atexit.register(Reticulum.exit_handler)
def start_local_interface(self): def start_local_interface(self):
if self.share_instance: if self.share_instance:
try: try:
interface = LocalInterface.LocalServerInterface( interface = LocalInterface.LocalServerInterface(
RNS.Transport, RNS.Transport,
self.local_interface_port self.local_interface_port
) )
interface.OUT = True interface.OUT = True
RNS.Transport.interfaces.append(interface) RNS.Transport.interfaces.append(interface)
self.is_shared_instance = True self.is_shared_instance = True
RNS.log("Started shared instance interface: "+str(interface), RNS.LOG_DEBUG) RNS.log("Started shared instance interface: "+str(interface), RNS.LOG_DEBUG)
except Exception as e: except Exception as e:
try: try:
interface = LocalInterface.LocalClientInterface( interface = LocalInterface.LocalClientInterface(
RNS.Transport, RNS.Transport,
"Local shared instance", "Local shared instance",
self.local_interface_port) self.local_interface_port)
interface.target_port = self.local_interface_port interface.target_port = self.local_interface_port
interface.OUT = True interface.OUT = True
RNS.Transport.interfaces.append(interface) RNS.Transport.interfaces.append(interface)
self.is_shared_instance = False self.is_shared_instance = False
self.is_standalone_instance = False self.is_standalone_instance = False
self.is_connected_to_shared_instance = True self.is_connected_to_shared_instance = True
RNS.log("Connected to local shared instance via: "+str(interface), RNS.LOG_DEBUG) RNS.log("Connected to local shared instance via: "+str(interface), RNS.LOG_DEBUG)
except Exception as e: except Exception as e:
RNS.log("Local shared instance appears to be running, but it could not be connected", RNS.LOG_ERROR) RNS.log("Local shared instance appears to be running, but it could not be connected", RNS.LOG_ERROR)
RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR) RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR)
self.is_shared_instance = False self.is_shared_instance = False
self.is_standalone_instance = True self.is_standalone_instance = True
self.is_connected_to_shared_instance = False self.is_connected_to_shared_instance = False
else: else:
self.is_shared_instance = False self.is_shared_instance = False
self.is_standalone_instance = True self.is_standalone_instance = True
self.is_connected_to_shared_instance = False self.is_connected_to_shared_instance = False
def applyConfig(self): def applyConfig(self):
if "logging" in self.config: if "logging" in self.config:
for option in self.config["logging"]: for option in self.config["logging"]:
value = self.config["logging"][option] value = self.config["logging"][option]
if option == "loglevel": if option == "loglevel":
RNS.loglevel = int(value) RNS.loglevel = int(value)
if RNS.loglevel < 0: if RNS.loglevel < 0:
RNS.loglevel = 0 RNS.loglevel = 0
if RNS.loglevel > 7: if RNS.loglevel > 7:
RNS.loglevel = 7 RNS.loglevel = 7
if "reticulum" in self.config: if "reticulum" in self.config:
for option in self.config["reticulum"]: for option in self.config["reticulum"]:
value = self.config["reticulum"][option] value = self.config["reticulum"][option]
if option == "share_instance": if option == "share_instance":
value = self.config["reticulum"].as_bool(option) value = self.config["reticulum"].as_bool(option)
self.share_instance = value self.share_instance = value
if option == "shared_instance_port": if option == "shared_instance_port":
value = int(self.config["reticulum"][option]) value = int(self.config["reticulum"][option])
self.local_interface_port = value self.local_interface_port = value
if option == "enable_transport": if option == "enable_transport":
v = self.config["reticulum"].as_bool(option) v = self.config["reticulum"].as_bool(option)
if v == True: if v == True:
Reticulum.__transport_enabled = True Reticulum.__transport_enabled = True
if option == "use_implicit_proof": if option == "use_implicit_proof":
v = self.config["reticulum"].as_bool(option) v = self.config["reticulum"].as_bool(option)
if v == True: if v == True:
Reticulum.__use_implicit_proof = True Reticulum.__use_implicit_proof = True
if v == False: if v == False:
Reticulum.__use_implicit_proof = False Reticulum.__use_implicit_proof = False
if option == "allow_unencrypted": if option == "allow_unencrypted":
v = self.config["reticulum"].as_bool(option) v = self.config["reticulum"].as_bool(option)
if v == True: if v == True:
RNS.log("", RNS.LOG_CRITICAL) RNS.log("", RNS.LOG_CRITICAL)
RNS.log("! ! ! ! ! ! ! ! !", RNS.LOG_CRITICAL) RNS.log("! ! ! ! ! ! ! ! !", RNS.LOG_CRITICAL)
RNS.log("", RNS.LOG_CRITICAL) RNS.log("", RNS.LOG_CRITICAL)
RNS.log("Danger! Encryptionless links have been allowed in the config file!", RNS.LOG_CRITICAL) RNS.log("Danger! Encryptionless links have been allowed in the config file!", RNS.LOG_CRITICAL)
RNS.log("Beware of the consequences! Any data sent over a link can potentially be intercepted,", RNS.LOG_CRITICAL) RNS.log("Beware of the consequences! Any data sent over a link can potentially be intercepted,", RNS.LOG_CRITICAL)
RNS.log("read and modified! If you are not absolutely sure that you want this,", RNS.LOG_CRITICAL) RNS.log("read and modified! If you are not absolutely sure that you want this,", RNS.LOG_CRITICAL)
RNS.log("you should exit Reticulum NOW and change your config file!", RNS.LOG_CRITICAL) RNS.log("you should exit Reticulum NOW and change your config file!", RNS.LOG_CRITICAL)
RNS.log("", RNS.LOG_CRITICAL) RNS.log("", RNS.LOG_CRITICAL)
RNS.log("! ! ! ! ! ! ! ! !", RNS.LOG_CRITICAL) RNS.log("! ! ! ! ! ! ! ! !", RNS.LOG_CRITICAL)
RNS.log("", RNS.LOG_CRITICAL) RNS.log("", RNS.LOG_CRITICAL)
Reticulum.__allow_unencrypted = True Reticulum.__allow_unencrypted = True
self.start_local_interface() self.start_local_interface()
if self.is_shared_instance or self.is_standalone_instance: if self.is_shared_instance or self.is_standalone_instance:
interface_names = [] interface_names = []
for name in self.config["interfaces"]: for name in self.config["interfaces"]:
if not name in interface_names: if not name in interface_names:
c = self.config["interfaces"][name] c = self.config["interfaces"][name]
try: try:
if ("interface_enabled" in c) and c.as_bool("interface_enabled") == True: if ("interface_enabled" in c) and c.as_bool("interface_enabled") == True:
if c["type"] == "UdpInterface": if c["type"] == "UdpInterface":
interface = UdpInterface.UdpInterface( interface = UdpInterface.UdpInterface(
RNS.Transport, RNS.Transport,
name, name,
c["listen_ip"], c["listen_ip"],
int(c["listen_port"]), int(c["listen_port"]),
c["forward_ip"], c["forward_ip"],
int(c["forward_port"]) int(c["forward_port"])
) )
if "outgoing" in c and c.as_bool("outgoing") == True: if "outgoing" in c and c.as_bool("outgoing") == True:
interface.OUT = True interface.OUT = True
else: else:
interface.OUT = False interface.OUT = False
RNS.Transport.interfaces.append(interface) RNS.Transport.interfaces.append(interface)
if c["type"] == "TCPServerInterface": if c["type"] == "TCPServerInterface":
interface = TCPInterface.TCPServerInterface( interface = TCPInterface.TCPServerInterface(
RNS.Transport, RNS.Transport,
name, name,
c["listen_ip"], c["listen_ip"],
int(c["listen_port"]) int(c["listen_port"])
) )
if "outgoing" in c and c.as_bool("outgoing") == True: if "outgoing" in c and c.as_bool("outgoing") == True:
interface.OUT = True interface.OUT = True
else: else:
interface.OUT = False interface.OUT = False
RNS.Transport.interfaces.append(interface) RNS.Transport.interfaces.append(interface)
if c["type"] == "TCPClientInterface": if c["type"] == "TCPClientInterface":
interface = TCPInterface.TCPClientInterface( interface = TCPInterface.TCPClientInterface(
RNS.Transport, RNS.Transport,
name, name,
c["target_host"], c["target_host"],
int(c["target_port"]) int(c["target_port"])
) )
if "outgoing" in c and c.as_bool("outgoing") == True: if "outgoing" in c and c.as_bool("outgoing") == True:
interface.OUT = True interface.OUT = True
else: else:
interface.OUT = False interface.OUT = False
RNS.Transport.interfaces.append(interface) RNS.Transport.interfaces.append(interface)
if c["type"] == "SerialInterface": if c["type"] == "SerialInterface":
port = c["port"] if "port" in c else None port = c["port"] if "port" in c else None
speed = int(c["speed"]) if "speed" in c else 9600 speed = int(c["speed"]) if "speed" in c else 9600
databits = int(c["databits"]) if "databits" in c else 8 databits = int(c["databits"]) if "databits" in c else 8
parity = c["parity"] if "parity" in c else "N" parity = c["parity"] if "parity" in c else "N"
stopbits = int(c["stopbits"]) if "stopbits" in c else 1 stopbits = int(c["stopbits"]) if "stopbits" in c else 1
if port == None: if port == None:
raise ValueError("No port specified for serial interface") raise ValueError("No port specified for serial interface")
interface = SerialInterface.SerialInterface( interface = SerialInterface.SerialInterface(
RNS.Transport, RNS.Transport,
name, name,
port, port,
speed, speed,
databits, databits,
parity, parity,
stopbits stopbits
) )
if "outgoing" in c and c["outgoing"].lower() == "true": if "outgoing" in c and c["outgoing"].lower() == "true":
interface.OUT = True interface.OUT = True
else: else:
interface.OUT = False interface.OUT = False
RNS.Transport.interfaces.append(interface) RNS.Transport.interfaces.append(interface)
if c["type"] == "KISSInterface": if c["type"] == "KISSInterface":
preamble = int(c["preamble"]) if "preamble" in c else None preamble = int(c["preamble"]) if "preamble" in c else None
txtail = int(c["txtail"]) if "txtail" in c else None txtail = int(c["txtail"]) if "txtail" in c else None
persistence = int(c["persistence"]) if "persistence" in c else None persistence = int(c["persistence"]) if "persistence" in c else None
slottime = int(c["slottime"]) if "slottime" in c else None slottime = int(c["slottime"]) if "slottime" in c else None
flow_control = c.as_bool("flow_control") if "flow_control" in c else False flow_control = c.as_bool("flow_control") if "flow_control" in c else False
port = c["port"] if "port" in c else None port = c["port"] if "port" in c else None
speed = int(c["speed"]) if "speed" in c else 9600 speed = int(c["speed"]) if "speed" in c else 9600
databits = int(c["databits"]) if "databits" in c else 8 databits = int(c["databits"]) if "databits" in c else 8
parity = c["parity"] if "parity" in c else "N" parity = c["parity"] if "parity" in c else "N"
stopbits = int(c["stopbits"]) if "stopbits" in c else 1 stopbits = int(c["stopbits"]) if "stopbits" in c else 1
if port == None: if port == None:
raise ValueError("No port specified for serial interface") raise ValueError("No port specified for serial interface")
interface = KISSInterface.KISSInterface( interface = KISSInterface.KISSInterface(
RNS.Transport, RNS.Transport,
name, name,
port, port,
speed, speed,
databits, databits,
parity, parity,
stopbits, stopbits,
preamble, preamble,
txtail, txtail,
persistence, persistence,
slottime, slottime,
flow_control flow_control
) )
if "outgoing" in c and c["outgoing"].lower() == "true": if "outgoing" in c and c["outgoing"].lower() == "true":
interface.OUT = True interface.OUT = True
else: else:
interface.OUT = False interface.OUT = False
RNS.Transport.interfaces.append(interface) RNS.Transport.interfaces.append(interface)
if c["type"] == "AX25KISSInterface": if c["type"] == "AX25KISSInterface":
preamble = int(c["preamble"]) if "preamble" in c else None preamble = int(c["preamble"]) if "preamble" in c else None
txtail = int(c["txtail"]) if "txtail" in c else None txtail = int(c["txtail"]) if "txtail" in c else None
persistence = int(c["persistence"]) if "persistence" in c else None persistence = int(c["persistence"]) if "persistence" in c else None
slottime = int(c["slottime"]) if "slottime" in c else None slottime = int(c["slottime"]) if "slottime" in c else None
flow_control = c.as_bool("flow_control") if "flow_control" in c else False flow_control = c.as_bool("flow_control") if "flow_control" in c else False
port = c["port"] if "port" in c else None port = c["port"] if "port" in c else None
speed = int(c["speed"]) if "speed" in c else 9600 speed = int(c["speed"]) if "speed" in c else 9600
databits = int(c["databits"]) if "databits" in c else 8 databits = int(c["databits"]) if "databits" in c else 8
parity = c["parity"] if "parity" in c else "N" parity = c["parity"] if "parity" in c else "N"
stopbits = int(c["stopbits"]) if "stopbits" in c else 1 stopbits = int(c["stopbits"]) if "stopbits" in c else 1
callsign = c["callsign"] if "callsign" in c else "" callsign = c["callsign"] if "callsign" in c else ""
ssid = int(c["ssid"]) if "ssid" in c else -1 ssid = int(c["ssid"]) if "ssid" in c else -1
if port == None: if port == None:
raise ValueError("No port specified for serial interface") raise ValueError("No port specified for serial interface")
interface = AX25KISSInterface.AX25KISSInterface( interface = AX25KISSInterface.AX25KISSInterface(
RNS.Transport, RNS.Transport,
name, name,
callsign, callsign,
ssid, ssid,
port, port,
speed, speed,
databits, databits,
parity, parity,
stopbits, stopbits,
preamble, preamble,
txtail, txtail,
persistence, persistence,
slottime, slottime,
flow_control flow_control
) )
if "outgoing" in c and c["outgoing"].lower() == "true": if "outgoing" in c and c["outgoing"].lower() == "true":
interface.OUT = True interface.OUT = True
else: else:
interface.OUT = False interface.OUT = False
RNS.Transport.interfaces.append(interface) RNS.Transport.interfaces.append(interface)
if c["type"] == "RNodeInterface": if c["type"] == "RNodeInterface":
frequency = int(c["frequency"]) if "frequency" in c else None frequency = int(c["frequency"]) if "frequency" in c else None
bandwidth = int(c["bandwidth"]) if "bandwidth" in c else None bandwidth = int(c["bandwidth"]) if "bandwidth" in c else None
txpower = int(c["txpower"]) if "txpower" in c else None txpower = int(c["txpower"]) if "txpower" in c else None
spreadingfactor = int(c["spreadingfactor"]) if "spreadingfactor" in c else None spreadingfactor = int(c["spreadingfactor"]) if "spreadingfactor" in c else None
codingrate = int(c["codingrate"]) if "codingrate" in c else None codingrate = int(c["codingrate"]) if "codingrate" in c else None
flow_control = c.as_bool("flow_control") if "flow_control" in c else False flow_control = c.as_bool("flow_control") if "flow_control" in c else False
id_interval = int(c["id_interval"]) if "id_interval" in c else None id_interval = int(c["id_interval"]) if "id_interval" in c else None
id_callsign = c["id_callsign"] if "id_callsign" in c else None id_callsign = c["id_callsign"] if "id_callsign" in c else None
port = c["port"] if "port" in c else None port = c["port"] if "port" in c else None
if port == None: if port == None:
raise ValueError("No port specified for RNode interface") raise ValueError("No port specified for RNode interface")
interface = RNodeInterface.RNodeInterface( interface = RNodeInterface.RNodeInterface(
RNS.Transport, RNS.Transport,
name, name,
port, port,
frequency = frequency, frequency = frequency,
bandwidth = bandwidth, bandwidth = bandwidth,
txpower = txpower, txpower = txpower,
sf = spreadingfactor, sf = spreadingfactor,
cr = codingrate, cr = codingrate,
flow_control = flow_control, flow_control = flow_control,
id_interval = id_interval, id_interval = id_interval,
id_callsign = id_callsign id_callsign = id_callsign
) )
if "outgoing" in c and c["outgoing"].lower() == "true": if "outgoing" in c and c["outgoing"].lower() == "true":
interface.OUT = True interface.OUT = True
else: else:
interface.OUT = False interface.OUT = False
RNS.Transport.interfaces.append(interface) RNS.Transport.interfaces.append(interface)
else: else:
RNS.log("Skipping disabled interface \""+name+"\"", RNS.LOG_NOTICE) RNS.log("Skipping disabled interface \""+name+"\"", RNS.LOG_NOTICE)
except Exception as e: except Exception as e:
RNS.log("The interface \""+name+"\" could not be created. Check your configuration file for errors!", RNS.LOG_ERROR) RNS.log("The interface \""+name+"\" could not be created. Check your configuration file for errors!", RNS.LOG_ERROR)
RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR) RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR)
RNS.panic() RNS.panic()
else: else:
RNS.log("The interface name \""+name+"\" was already used. Check your configuration file for errors!", RNS.LOG_ERROR) RNS.log("The interface name \""+name+"\" was already used. Check your configuration file for errors!", RNS.LOG_ERROR)
RNS.panic() RNS.panic()
def createDefaultConfig(self): def createDefaultConfig(self):
self.config = ConfigObj(__default_rns_config__) self.config = ConfigObj(__default_rns_config__)
self.config.filename = Reticulum.configpath self.config.filename = Reticulum.configpath
if not os.path.isdir(Reticulum.configdir): if not os.path.isdir(Reticulum.configdir):
os.makedirs(Reticulum.configdir) os.makedirs(Reticulum.configdir)
self.config.write() self.config.write()
self.applyConfig() self.applyConfig()
@staticmethod @staticmethod
def should_allow_unencrypted(): def should_allow_unencrypted():
return Reticulum.__allow_unencrypted return Reticulum.__allow_unencrypted
@staticmethod @staticmethod
def should_use_implicit_proof(): def should_use_implicit_proof():
return Reticulum.__use_implicit_proof return Reticulum.__use_implicit_proof
@staticmethod @staticmethod
def transport_enabled(): def transport_enabled():
return Reticulum.__transport_enabled return Reticulum.__transport_enabled
# Default configuration file: # Default configuration file:
__default_rns_config__ = '''# This is the default Reticulum config file. __default_rns_config__ = '''# This is the default Reticulum config file.
@ -467,7 +467,7 @@ loglevel = 4
# needs or turn it off completely. # needs or turn it off completely.
[[Default UDP Interface]] [[Default UDP Interface]]
type = UdpInterface type = UdpInterface
interface_enabled = True interface_enabled = True
outgoing = True outgoing = True
listen_ip = 0.0.0.0 listen_ip = 0.0.0.0
@ -564,7 +564,7 @@ loglevel = 4
# Allow transmit on interface. # Allow transmit on interface.
outgoing = true outgoing = true
# Serial port for the device # Serial port for the device
port = /dev/ttyUSB1 port = /dev/ttyUSB1
# Set the serial baud-rate and other # Set the serial baud-rate and other
@ -621,7 +621,7 @@ loglevel = 4
# Allow transmit on interface. # Allow transmit on interface.
outgoing = true outgoing = true
# Serial port for the device # Serial port for the device
port = /dev/ttyUSB2 port = /dev/ttyUSB2
# Set the serial baud-rate and other # Set the serial baud-rate and other

File diff suppressed because it is too large Load Diff

View File

@ -25,10 +25,10 @@ LOG_VERBOSE = 5
LOG_DEBUG = 6 LOG_DEBUG = 6
LOG_EXTREME = 7 LOG_EXTREME = 7
LOG_STDOUT = 0x91 LOG_STDOUT = 0x91
LOG_FILE = 0x92 LOG_FILE = 0x92
loglevel = LOG_NOTICE loglevel = LOG_NOTICE
logfile = None logfile = None
logdest = LOG_STDOUT logdest = LOG_STDOUT
logtimefmt = "%Y-%m-%d %H:%M:%S" logtimefmt = "%Y-%m-%d %H:%M:%S"
@ -36,54 +36,54 @@ logtimefmt = "%Y-%m-%d %H:%M:%S"
random.seed(os.urandom(10)) random.seed(os.urandom(10))
def loglevelname(level): def loglevelname(level):
if (level == LOG_CRITICAL): if (level == LOG_CRITICAL):
return "Critical" return "Critical"
if (level == LOG_ERROR): if (level == LOG_ERROR):
return "Error" return "Error"
if (level == LOG_WARNING): if (level == LOG_WARNING):
return "Warning" return "Warning"
if (level == LOG_NOTICE): if (level == LOG_NOTICE):
return "Notice" return "Notice"
if (level == LOG_INFO): if (level == LOG_INFO):
return "Info" return "Info"
if (level == LOG_VERBOSE): if (level == LOG_VERBOSE):
return "Verbose" return "Verbose"
if (level == LOG_DEBUG): if (level == LOG_DEBUG):
return "Debug" return "Debug"
if (level == LOG_EXTREME): if (level == LOG_EXTREME):
return "Extra" return "Extra"
return "Unknown" return "Unknown"
def log(msg, level=3): def log(msg, level=3):
# TODO: not thread safe # TODO: not thread safe
if loglevel >= level: if loglevel >= level:
timestamp = time.time() timestamp = time.time()
logstring = "["+time.strftime(logtimefmt)+"] ["+loglevelname(level)+"] "+msg logstring = "["+time.strftime(logtimefmt)+"] ["+loglevelname(level)+"] "+msg
if (logdest == LOG_STDOUT): if (logdest == LOG_STDOUT):
print(logstring) print(logstring)
if (logdest == LOG_FILE and logfile != None): if (logdest == LOG_FILE and logfile != None):
file = open(logfile, "a") file = open(logfile, "a")
file.write(logstring+"\n") file.write(logstring+"\n")
file.close() file.close()
def rand(): def rand():
result = random.random() result = random.random()
return result return result
def hexrep(data, delimit=True): def hexrep(data, delimit=True):
delimiter = ":" delimiter = ":"
if not delimit: if not delimit:
delimiter = "" delimiter = ""
hexrep = delimiter.join("{:02x}".format(c) for c in data) hexrep = delimiter.join("{:02x}".format(c) for c in data)
return hexrep return hexrep
def prettyhexrep(data): def prettyhexrep(data):
delimiter = "" delimiter = ""
hexrep = "<"+delimiter.join("{:02x}".format(c) for c in data)+">" hexrep = "<"+delimiter.join("{:02x}".format(c) for c in data)+">"
return hexrep return hexrep
def panic(): def panic():
os._exit(255) os._exit(255)