initial commit
authorametama <ametama@wafflesoft.org>
Sat, 27 Dec 2025 02:37:28 +0000 (03:37 +0100)
committerametama <ametama@wafflesoft.org>
Sat, 27 Dec 2025 02:37:28 +0000 (03:37 +0100)
.gitignore [new file with mode: 0644]
requirements.txt [new file with mode: 0644]
src/engine.py [new file with mode: 0644]
src/entity.py [new file with mode: 0644]
src/main.py [new file with mode: 0644]
src/postgres.py [new file with mode: 0644]
tag [new file with mode: 0644]
test/key.pem [new file with mode: 0644]
test/key.pub [new file with mode: 0644]
test/test.py [new file with mode: 0644]
version [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..d2152a2
--- /dev/null
@@ -0,0 +1,2 @@
+src/__pycache__/
+.venv/
diff --git a/requirements.txt b/requirements.txt
new file mode 100644 (file)
index 0000000..04fb830
--- /dev/null
@@ -0,0 +1,4 @@
+websockets
+cryptography
+aiopg
+pyyaml
diff --git a/src/engine.py b/src/engine.py
new file mode 100644 (file)
index 0000000..406d8bc
--- /dev/null
@@ -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 (file)
index 0000000..9ba25f1
--- /dev/null
@@ -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 (file)
index 0000000..707aef7
--- /dev/null
@@ -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 (file)
index 0000000..7a0dfbd
--- /dev/null
@@ -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 (file)
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 (file)
index 0000000..69682f9
--- /dev/null
@@ -0,0 +1 @@
+±QWSÁ²\19\9bç\e@IÞ\13\14I\r\1cz\95\86¢:\18\a-\e4¶ü
\ No newline at end of file
diff --git a/test/key.pub b/test/key.pub
new file mode 100644 (file)
index 0000000..a746bcc
--- /dev/null
@@ -0,0 +1 @@
+_Ér´\87û\16*ëK%í´xm\11ç'1X\91Ê\9e\99\13\81\7f®ÓIIp
\ No newline at end of file
diff --git a/test/test.py b/test/test.py
new file mode 100644 (file)
index 0000000..803535f
--- /dev/null
@@ -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 (file)
index 0000000..bd52db8
--- /dev/null
+++ b/version
@@ -0,0 +1 @@
+0.0.0
\ No newline at end of file