From dadc5a2e71924e735cfb7d8b8087485bbd7959e3 Mon Sep 17 00:00:00 2001 From: ametama Date: Sat, 27 Dec 2025 03:37:28 +0100 Subject: [PATCH 1/1] initial commit --- .gitignore | 2 ++ requirements.txt | 4 ++++ src/engine.py | 47 ++++++++++++++++++++++++++++++++++++++++++ src/entity.py | 53 ++++++++++++++++++++++++++++++++++++++++++++++++ src/main.py | 35 ++++++++++++++++++++++++++++++++ src/postgres.py | 48 +++++++++++++++++++++++++++++++++++++++++++ tag | 10 +++++++++ test/key.pem | 1 + test/key.pub | 1 + test/test.py | 17 ++++++++++++++++ version | 1 + 11 files changed, 219 insertions(+) create mode 100644 .gitignore create mode 100644 requirements.txt create mode 100644 src/engine.py create mode 100644 src/entity.py create mode 100644 src/main.py create mode 100644 src/postgres.py create mode 100644 tag create mode 100644 test/key.pem create mode 100644 test/key.pub create mode 100644 test/test.py create mode 100644 version diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d2152a2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +src/__pycache__/ +.venv/ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..04fb830 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +websockets +cryptography +aiopg +pyyaml diff --git a/src/engine.py b/src/engine.py new file mode 100644 index 0000000..406d8bc --- /dev/null +++ b/src/engine.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +from abc import ABC, abstractmethod +import secrets +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey +import websockets +from entity import Entity, User, Client + +class Engine(ABC): + users: dict[int, User] + entities: dict[int, Entity] + + def __init__(self): + self.users = {} + + def challenge(self): + return secrets.token_bytes(256) + + async def on_connection(self, socket: websockets.ServerConnection): + try: + session_id = await socket.recv(decode=True) + session_id = int(session_id) + challenge = self.challenge() + await socket.send(challenge) + signature = await socket.recv(decode=False) + client = await self.authorize(socket, session_id, challenge, signature) + await socket.send(str(client.user.id)) + async for data in socket: + if type(data) != str: continue + await self.on_message(client, data) + except: pass + finally: + await socket.close() + + async def on_message(self, client: Client, message: str): + pass + + async def authorize(self, socket: websockets.ServerConnection, session_id: int, challenge: bytes, signature: bytes): + user, pkey = await self.fetch_session(session_id) + pkey.verify(signature, challenge) + if user.id not in self.users: self.users[user.id] = user + client = Client(socket, session_id, user) + user.clients.add(client) + return client + + @abstractmethod + async def fetch_session(self, session_id: int) -> tuple[User, Ed25519PublicKey]: ... diff --git a/src/entity.py b/src/entity.py new file mode 100644 index 0000000..9ba25f1 --- /dev/null +++ b/src/entity.py @@ -0,0 +1,53 @@ +from __future__ import annotations + +import asyncio +import json +import websockets + +Primitive = str | int | float | bool + +class User: + id: int + clients: set[Client] + + def __init__(self, id: int): + self.id = id + self.clients = set() + +class Client: + socket: websockets.ServerConnection + session_id: int + user: User + + def __init__(self, socket: websockets.ServerConnection, session_id: int, user: User): + self.socket = socket + self.session_id = session_id + self.user = user + +class EntityException(BaseException): pass +class Entity(dict): + id: int + __t_subscribers: set[Client] + + def __init__(self, id: int): + self.id = id + self.__t_subscribers = set() + + def subscribe(self, client: Client): + self.__t_subscribers.add(client) + + def unsubscribe(self, client: Client): + self.__t_subscribers.remove(client) + + async def set(self, key, value: Primitive): + await self.merge({ key: value }) + + async def merge(self, obj: dict[str, Primitive]): + for key in obj: dict.__setitem__(self, key, obj[key]) + await asyncio.gather(*[client.socket.send(json.dumps({ self.id: obj })) for client in self.__t_subscribers]) + + def __setitem__(self, key, value): + raise EntityException("Entity: set() must be used in place of __setitem__().") + + def __delitem__(self, key): + raise EntityException("Entity: __delitem__() may not be called on Entity objects.") diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..707aef7 --- /dev/null +++ b/src/main.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +from typing import TypedDict +import websockets +import asyncio +import sys +import yaml +from engine import Engine +from postgres import PostgresConfig, PostgresEngine + +class Config(TypedDict): + postgres: None | PostgresConfig + +async def main(): + if (len(sys.argv) > 1 and sys.argv[1] in ["-v", "--version"]): + with open("version", "r") as f: + print("tachyon " + "v" + f.read()) + exit() + with open("tag", "r") as f: + print(f.read()) + with open("version", "r") as f: + print(" " * 64 + "v" + f.read()) + cfg: None | Config = None + with open("/etc/tachyon.yml", "r") as f: + cfg = yaml.safe_load(f.read()) + if not cfg or not cfg["postgres"]: + print("tachyon: config file or psql not defined. Exiting.") + exit() + psql: PostgresEngine = PostgresEngine() + await psql.start(cfg["postgres"]) + engine: Engine = psql + async with websockets.serve(engine.on_connection, "localhost", 8088): await asyncio.Future() + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/src/postgres.py b/src/postgres.py new file mode 100644 index 0000000..7a0dfbd --- /dev/null +++ b/src/postgres.py @@ -0,0 +1,48 @@ +from __future__ import annotations + +import aiopg +from typing import TypedDict +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey +from engine import Engine +from entity import User + +class PostgresConfig(TypedDict): + database: str + user: str + password: str + host: str + port: int + +class PostgresEngine(Engine): + _pool: aiopg.Pool + _connection: aiopg.Connection + + async def start(self, config: PostgresConfig): + self._pool = await aiopg.create_pool(**config) + self._connection = await self._pool.acquire() + async with self.cursor() as cur: + await cur.execute(""" + CREATE TABLE IF NOT EXISTS users ( + id integer primary key + ); + CREATE TABLE IF NOT EXISTS sessions ( + id integer primary key, + user_id integer references users(id), + key bytea + ); + """) + + def cursor(self): + return self._connection.cursor() + + async def close(self): + await self._connection.close() + self._pool.close() + + async def fetch_session(self, session_id: int): + async with self.cursor() as cur: + await cur.execute("SELECT u.id, s.key FROM sessions s JOIN users u ON u.id = s.user_id WHERE s.id = %s", (session_id,)) + row: tuple[int, memoryview] | None = await cur.fetchone() + assert row + user_id, memview = row + return User(user_id) if user_id not in self.users else self.users[user_id], Ed25519PublicKey.from_public_bytes(memview.tobytes()) diff --git a/tag b/tag new file mode 100644 index 0000000..eb221ea --- /dev/null +++ b/tag @@ -0,0 +1,10 @@ + + mm + ## ## + ####### m#####m m#####m ##m####m "## ### m####m ##m####m + ## " mmm## ##" " ##" ## ##m ## ##" "## ##" ## + ## m##"""## ## ## ## ####" ## ## ## ## + ##mmm ##mmm### "##mmmm# ## ## ### "##mm##" ## ## + """" """" "" """"" "" "" ## """" "" "" + ### + \ No newline at end of file diff --git a/test/key.pem b/test/key.pem new file mode 100644 index 0000000..69682f9 --- /dev/null +++ b/test/key.pem @@ -0,0 +1 @@ +±QWSÁ²›ç@IÞI z•3 †¢:-4¶ü \ No newline at end of file diff --git a/test/key.pub b/test/key.pub new file mode 100644 index 0000000..a746bcc --- /dev/null +++ b/test/key.pub @@ -0,0 +1 @@ +_Ér´‡û*ëK%í´xmç'1X‘Êž™®ÓIIp \ No newline at end of file diff --git a/test/test.py b/test/test.py new file mode 100644 index 0000000..803535f --- /dev/null +++ b/test/test.py @@ -0,0 +1,17 @@ +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey +from websockets.asyncio.client import connect +import asyncio + +async def main(): + prf = open("test/key.pem", "rb") + prkey = Ed25519PrivateKey.from_private_bytes(prf.read()) + async with connect("ws://localhost:8088") as ws: + await ws.send("1") + challenge = await ws.recv(decode=False) + signature = prkey.sign(challenge) + await ws.send(signature) + prf.close() + print(f"Logged in as: {await ws.recv(decode=True)}") + await ws.close() + +asyncio.run(main()) diff --git a/version b/version new file mode 100644 index 0000000..bd52db8 --- /dev/null +++ b/version @@ -0,0 +1 @@ +0.0.0 \ No newline at end of file -- 2.34.1