Samuel Sloniker
2 years 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