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