--- /dev/null
+src/__pycache__/
+.venv/
--- /dev/null
+websockets
+cryptography
+aiopg
+pyyaml
--- /dev/null
+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]: ...
--- /dev/null
+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.")
--- /dev/null
+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())
--- /dev/null
+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())
--- /dev/null
+
+ mm
+ ## ##
+ ####### m#####m m#####m ##m####m "## ### m####m ##m####m
+ ## " mmm## ##" " ##" ## ##m ## ##" "## ##" ##
+ ## m##"""## ## ## ## ####" ## ## ## ##
+ ##mmm ##mmm### "##mmmm# ## ## ### "##mm##" ## ##
+ """" """" "" """"" "" "" ## """" "" ""
+ ###
+
\ No newline at end of file
--- /dev/null
+±QWSÁ²\19\9bç\e@IÞ\13\14I\r\1cz\953 \86¢:\18\a-\e4¶ü
\ No newline at end of file
--- /dev/null
+_Ér´\87û\16*ëK%í´xm\11ç'1X\91Ê\9e\99\13\81\7f®ÓIIp
\ No newline at end of file
--- /dev/null
+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())
--- /dev/null
+0.0.0
\ No newline at end of file