Add code
This commit is contained in:
parent
3b24127cf6
commit
905e407b10
79
leonet/__main__.py
Normal file
79
leonet/__main__.py
Normal 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
22
leonet/crypto.py
Normal 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
122
leonet/frames.py
Normal 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
22
leonet/hashers.py
Normal 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
11
leonet/message_codec.py
Normal 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
135
leonet/modem.py
Normal 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
|
Loading…
Reference in New Issue
Block a user