Samuel Sloniker
1 year ago
6 changed files with 391 additions and 0 deletions
@ -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()) |
@ -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") |
@ -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 |
||||
) |
@ -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") |
@ -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") |
@ -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 |
Loading…
Reference in new issue