This commit is contained in:
Samuel Sloniker 2022-11-03 18:40:33 -07:00
parent 3b24127cf6
commit 905e407b10
6 changed files with 391 additions and 0 deletions

79
leonet/__main__.py Normal file
View File

@ -0,0 +1,79 @@
import os
import time
import threading
import argparse
import modem
import frames
sent_messages = {}
received_messages = {}
def sends():
while True:
try:
with open(input_file) as f:
recipient, message = f.read().split(" ", 1)
os.unlink(input_file)
except FileNotFoundError:
continue
message = message.strip()
if message.startswith("QRY "):
message_id = message.split(" ")[1]
modem.send(
modem.wrap_frame(
frames.QryFrame(
recipient, my_identifier, message_id
).encode(),
callsign,
)
)
else:
frame = frames.MsgFrame(
recipient,
my_identifier,
message,
)
sent_messages[frame.message_id] = [
frame,
False,
]
modem.send(modem.wrap_frame(frame.encode(), callsign))
parser = argparse.ArgumentParser(prog="leonet")
parser.add_argument("identifier", help="my identifier (e.g. CALLSIGN-1)")
parser.add_argument(
"callsign", help="my call sign (does not need to match identifier)"
)
parser.add_argument("input_file", help="file to watch for messages")
args = parser.parse_args()
my_identifier = args.identifier
callsign = args.callsign
input_file = args.input_file
threading.Thread(target=sends).start()
transceiver = modem.Transceiver(my_identifier, callsign)
while True:
frame = transceiver.receive()
print()
print(frame)
if isinstance(frame, frames.MsgFrame):
print(frame.timestamp, frame.originator, frame.message)
time.sleep(5)
received_messages[frame.message_id] = frame
transceiver.transmit(frames.gen_ack().encode())
elif isinstance(frame, frames.QryFrame):
time.sleep(5)
if frame.message_id in received_messages:
transceiver.transmit(frames.gen_ack().encode())
else:
transceiver.transmit(frames.gen_nak().encode())

22
leonet/crypto.py Normal file
View File

@ -0,0 +1,22 @@
import hmac
import hashlib
import base64
def find_auth_code(password, message):
if password:
return base64.b64encode(
hmac.digest(
password.encode("utf-8"),
message.encode("ascii"),
"sha256",
)
).decode("ascii")
else:
return ""
def find_checksum(data):
return base64.b64encode(
hashlib.sha256(data.encode("ascii")).digest()[:8]
).decode("ascii")

122
leonet/frames.py Normal file
View File

@ -0,0 +1,122 @@
import typing
import datetime
import dataclasses
import crypto
import message_codec
def create_frame(frame_type, recipient, originator, content, password):
main_frame = f"{frame_type}#{recipient}#{originator}#{content}"
checksum = crypto.find_checksum(main_frame)
auth_code = crypto.find_auth_code(password, main_frame)
return f"\nLEONET {main_frame} {checksum} {auth_code} TENOEL\n"
@dataclasses.dataclass
class MsgFrame:
recipient: str
originator: str
message: str
timestamp: dataclasses.InitVar[typing.Optional[str]] = None
message_id: dataclasses.InitVar[typing.Optional[str]] = None
def __post_init__(self, message_id, timestamp):
if timestamp is not None:
self.timestamp = timestamp
else:
self.timestamp = datetime.datetime.utcnow().isoformat()
if message_id is not None:
self.message_id = message_id
else:
safe_message = message_codec.encode(self.message)
body = f"{self.timestamp}&{safe_message}"
self.message_id = crypto.find_checksum(body)
self.frame_recipient = self.recipient
def encode(self, password=""):
safe_message = message_codec.encode(self.message)
body = f"{self.timestamp}&{safe_message}"
message_id = crypto.find_checksum(body)
full_body = f"{body};{message_id}"
return create_frame(
"MSG", self.recipient, self.originator, full_body, password
)
def gen_ack(self):
return frames.AckFrame(
self.recipient,
self.originator,
self.message_id,
)
def gen_nak(self):
return frames.NakFrame(
self.recipient,
self.originator,
self.message_id,
)
@dataclasses.dataclass
class QryFrame:
recipient: str
originator: str
message_id: str
def __post_init__(self):
self.frame_recipient = self.recipient
def encode(self, password=""):
return create_frame(
"QRY", self.recipient, self.originator, self.message_id, password
)
def gen_ack(self):
return frames.AckFrame(
self.recipient,
self.originator,
self.message_id,
)
def gen_nak(self):
return frames.NakFrame(
self.recipient,
self.originator,
self.message_id,
)
@dataclasses.dataclass
class AckFrame:
recipient: str
originator: str
message_id: str
def __post_init__(self):
self.frame_recipient = self.originator
def encode(self, password=""):
return create_frame(
"ACK", self.recipient, self.originator, self.message_id, password
)
@dataclasses.dataclass
class NakFrame:
recipient: str
originator: str
message_id: str
def __post_init__(self):
self.frame_recipient = self.originator
def encode(self, password=""):
return create_frame(
"NAK", self.recipient, self.originator, self.message_id, password
)

22
leonet/hashers.py Normal file
View File

@ -0,0 +1,22 @@
import hmac
import hashlib
import base64
def find_auth_code(password, message):
if password:
return base64.b64encode(
hmac.digest(
password.encode("utf-8"),
message.encode("ascii"),
"sha256",
)
).decode("ascii")
else:
return ""
def find_checksum(data):
return base64.b64encode(
hashlib.sha256(data.encode("ascii")).digest()[:8]
).decode("ascii")

11
leonet/message_codec.py Normal file
View File

@ -0,0 +1,11 @@
import base64
def encode(message):
return "B64:" + base64.b64encode(message.encode("utf-8")).decode("ascii")
def decode(message):
code, content = message.split(":", 1)
if code == "B64":
return base64.b64decode(content).decode("utf-8")

135
leonet/modem.py Normal file
View File

@ -0,0 +1,135 @@
import queue
import threading
import subprocess
import frames
import crypto
def wrap_frame(message, callsign):
return f"Syncronizing... Synchronizing... Synchronizing...{message}https://kj7rrv.com/leonet DE {callsign}\n\n"
class Transmitter:
def __init__(self, callsign):
self.callsign = callsign
self.queue = queue.Queue()
self.transmit = self.queue.put
threading.Thread(target=self.tx_loop).start()
def tx_loop(self):
while True:
send(wrap_frame(self.queue.get(), callsign))
def send(data):
print(data)
subprocess.run(
["minimodem", "--tx", "300"],
input=data,
encoding="ascii",
)
class InvalidFrameError(BaseException):
pass
class FrameDecodeError(InvalidFrameError):
pass
class FrameAuthenticationError(InvalidFrameError):
pass
def decode(frame_bytes, passwords):
try:
frame = frame_bytes.decode("ascii")
main, checksum, auth_code = frame.split(" ")[1:-1]
if checksum != crypto.find_checksum(main):
raise FrameDecodeError()
frame_type, recipient, originator, payload = main.split("#")
if passwords is not None:
password = passwords.get(
originator if directions[frame_type] == "OR" else recipient, ""
)
if auth_code != crypto.find_auth_code(password, main):
raise FrameAuthenticationError
if frame_type == "MSG":
body, message_id = payload.split(";")
timestamp, safe_message = body.split("&")
message = make_message_unsafe(safe_message)
return frame_objects.MsgFrame(
recipient,
originator,
message_id,
timestamp,
message,
)
elif frame_type == "QRY":
return frame_objects.QryFrame(recipient, originator, payload)
elif frame_type == "ACK":
return frame_objects.AckFrame(recipient, originator, payload)
elif frame_type == "Nak":
return frame_objects.NakFrame(recipient, originator, payload)
else:
raise FrameDecodeError()
except BaseException:
raise FrameDecodeError()
class Receiver:
def __init__(self, identifier):
self.modem = subprocess.Popen(
["minimodem", "--quiet", "--rx", "300"], stdout=subprocess.PIPE
)
self.running = True
self.queue = queue.Queue()
self.receive = self.queue.get
self.identifier = identifier
threading.Thread(target=self.rx_loop).start()
def rx_loop(self):
rx_buffer = b""
while True:
if not self.running:
break
byte = self.modem.stdout.read(1)
if byte == b"\n":
if rx_buffer.startswith(b"LEONET") and rx_buffer.endswith(
b"TENOEL"
):
try:
frame = decode(rx_buffer, None) # passwords dict
if (
self.identifier is None
or frame.frame_recipient == self.identifier
):
self.queue.put(frame)
except InvalidFrameError as e:
pass
rx_buffer = b""
else:
rx_buffer += byte
class Transceiver:
def __init__(self, identifier, callsign):
self.transmitter = Transmitter(callsign)
self.receiver = Receiver(identifier)
self.tx_queue = self.transmitter.queue
self.rx_queue = self.receiver.queue
self.transmit = self.transmitter.transmit
self.receive = self.receiver.receive