Skip to content

Commit 13c5a1f

Browse files
committed
Added module server
1 parent 5eed3c9 commit 13c5a1f

File tree

6 files changed

+280
-0
lines changed

6 files changed

+280
-0
lines changed

src/server/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .websock import init

src/server/commands.py

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import asyncio
2+
import sys
3+
import uuid
4+
5+
from .users import get_users, save_users, save_user
6+
from .crypto import generate_key_pair
7+
8+
9+
async def start_command_reader():
10+
while True:
11+
# This creates a future that will be set when stdin has data
12+
line = await asyncio.to_thread(sys.stdin.readline)
13+
line = line.strip()
14+
15+
if line:
16+
await process_command(line)
17+
18+
19+
async def process_command(command: str) -> None:
20+
cmd_parts = command.split()
21+
if not cmd_parts:
22+
return
23+
24+
cmd = cmd_parts[0].lower()
25+
26+
if cmd == "help":
27+
print("Available commands:")
28+
print(" new - Create new user")
29+
print(" list - List all registered users")
30+
print(" del <uuid> - Delete a user by UUID")
31+
print(" exit - Exit the server")
32+
print(" help - Show this help message")
33+
34+
elif cmd == "new":
35+
if len(cmd_parts) != 1:
36+
print("Usage: new")
37+
return
38+
39+
await generate_user_keys()
40+
41+
elif cmd == "list":
42+
users = get_users()
43+
if not users:
44+
print("No users registered.")
45+
return
46+
47+
print("Registered users:")
48+
for user_id, user_data in users.items():
49+
print(f" UUID: {user_id}")
50+
51+
elif cmd == "del":
52+
if len(cmd_parts) < 2:
53+
print("Usage: del <uuid>")
54+
return
55+
56+
user_id = cmd_parts[1]
57+
users = get_users()
58+
if user_id in users:
59+
del users[user_id]
60+
save_users(users)
61+
print(f"User {user_id} deleted.")
62+
else:
63+
print(f"User {user_id} not found.")
64+
65+
elif cmd == "exit":
66+
print("Shutting down server...")
67+
# Signal to stop the server
68+
asyncio.get_event_loop().stop()
69+
sys.exit()
70+
71+
else:
72+
print(f"Unknown command: {cmd}")
73+
print("Type 'help' for available commands.")
74+
75+
76+
async def generate_user_keys() -> None:
77+
import get_from_env
78+
79+
# Generate server keys
80+
server_secret, server_public = generate_key_pair()
81+
82+
# Generate client keys
83+
client_secret, client_public = generate_key_pair()
84+
85+
# Generate UUID for the user
86+
user_id = str(uuid.uuid4())
87+
server_conf = {
88+
"secret": server_secret,
89+
"public_key": client_public
90+
}
91+
user_conf = {
92+
"url": f"ws://{get_from_env.get_addr()}:{get_from_env.get_port()}",
93+
"uuid": user_id,
94+
"secret": client_secret,
95+
"public": server_public
96+
}
97+
98+
# Save to users.json
99+
users = get_users()
100+
users[user_id] = server_conf
101+
save_users(users)
102+
103+
save_user(user_id, user_conf)
104+
105+
print(f"New user UUID: {user_id}")

src/server/crypto.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import base64
2+
import ecies
3+
4+
5+
def generate_key_pair():
6+
key = ecies.utils.generate_key()
7+
secret = key.secret
8+
public = key.public_key.format(True)
9+
10+
# Convert to bytes and then base64 for storage
11+
secret_b64 = base64.b64encode(secret).decode("utf-8")
12+
public_b64 = base64.b64encode(public).decode("utf-8")
13+
14+
return secret_b64, public_b64
15+
16+
17+
def decrypt_data(private_key, encrypted_data):
18+
return ecies.decrypt(private_key, encrypted_data).decode("utf-8")
19+
20+
21+
def encrypt_data(public_key, data):
22+
return ecies.encrypt(public_key, data)

src/server/get_from_env.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import os
2+
import logging
3+
4+
def get_addr() -> str:
5+
addr = os.getenv("SWUC_SERVER_ADDR")
6+
if not addr:
7+
return "localhost"
8+
else:
9+
return addr
10+
11+
12+
def get_port() -> int:
13+
port = os.getenv("SWUC_SERVER_PORT")
14+
try:
15+
port = int(port) if port is not None else 8765
16+
except ValueError:
17+
logging.warning(f"Incorrect port value: '{port}', using default 8765")
18+
port = 8765
19+
20+
return port

src/server/users.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import json
2+
import os
3+
from logging import warning
4+
5+
def get_users() -> dict:
6+
try:
7+
if not os.path.exists("users.json"):
8+
with open("users.json", "w") as f:
9+
json.dump({}, f)
10+
11+
with open("users.json", "r") as users_file:
12+
return json.load(users_file)
13+
except json.JSONDecodeError:
14+
return {}
15+
except Exception as e:
16+
warning(f"Error loading users: {repr(e)}")
17+
return {}
18+
19+
def save_users(users: dict) -> None:
20+
try:
21+
with open("users.json", "w") as users_file:
22+
json.dump(users, users_file, indent=2)
23+
except Exception as e:
24+
warning(f"Error saving users: {repr(e)}")
25+
26+
def save_user(uuid: str, user: dict) -> None:
27+
if not os.path.exists("users"):
28+
os.makedirs("users")
29+
try:
30+
with open(f"users/{uuid}.json", "w") as users_file:
31+
json.dump(user, users_file, indent=2)
32+
except Exception as e:
33+
warning(f"Error saving user: {repr(e)}")

src/server/websock.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
from websockets.asyncio.server import serve
2+
from logging import info, warning
3+
import base64
4+
import json
5+
import asyncio
6+
7+
from .crypto import decrypt_data, encrypt_data
8+
from .users import get_users
9+
from . import get_from_env
10+
from . import commands
11+
12+
async def init() -> None:
13+
# Getting info from environment
14+
addr = get_from_env.get_addr()
15+
info(f"Using address: {addr}")
16+
17+
port = get_from_env.get_port()
18+
info(f"Using port: {port}")
19+
20+
# Start command reader task
21+
asyncio.create_task(commands.start_command_reader())
22+
23+
print("Server starting! Type 'help' for available commands.")
24+
25+
# Start serving
26+
await start_websocket_server(addr, port)
27+
28+
async def handler(websocket) -> None:
29+
try:
30+
async for message in websocket:
31+
info(f"Received msg: {message}")
32+
response = await process(message)
33+
await websocket.send(response)
34+
info(f"Sent msg: {response}")
35+
except Exception as e:
36+
warning(f"Handler error: {repr(e)}")
37+
38+
39+
async def start_websocket_server(addr: str, port: int) -> None:
40+
async with serve(handler, addr, port) as server:
41+
await server.serve_forever()
42+
43+
44+
async def process(message: str) -> str:
45+
users = get_users()
46+
47+
try:
48+
# Decode base64 message and parse JSON
49+
json_request = base64.b64decode(message).decode("utf-8")
50+
request = json.loads(json_request)
51+
info(f"Request: {request}")
52+
53+
# Validate UUID
54+
if "uuid" not in request or request["uuid"] not in users:
55+
return "Invalid UUID"
56+
57+
# Decrypt incoming data with server's private key
58+
encrypted_names = base64.b64decode(request["raw"])
59+
server_private_key = base64.b64decode(users[request["uuid"]]["secret"])
60+
61+
decrypted_names = decrypt_data(server_private_key, encrypted_names)
62+
63+
# Process decrypted names
64+
result = []
65+
for encoded_name in decrypted_names.split("|"):
66+
if encoded_name: # Check for empty strings
67+
import sys
68+
from os import path
69+
sys.path.append(path.join(path.dirname(__file__), '..'))
70+
from services import VersionFinder
71+
72+
finder = VersionFinder()
73+
74+
name = base64.b64decode(encoded_name).decode("utf-8")
75+
76+
res = finder.find_version(name)
77+
78+
result.append(res)
79+
80+
# Prepare JSON response
81+
response_json = json.dumps({"status": "success", "software": result})
82+
83+
# Encrypt response with client's public key
84+
client_public_key = base64.b64decode(users[request["uuid"]]["public_key"])
85+
encrypted_response = encrypt_data(client_public_key, response_json.encode("utf-8"))
86+
87+
# Return base64-encoded encrypted response
88+
return base64.b64encode(encrypted_response).decode("utf-8")
89+
90+
except json.JSONDecodeError:
91+
return "Bad JSON in request"
92+
except KeyError:
93+
return "Bad Key in JSON"
94+
except ValueError as e:
95+
if "padding" in str(e):
96+
return "Bad Base64 in request"
97+
return f"Processing error: {str(e)}"
98+
except Exception as e:
99+
return f"Error: {repr(e)}"

0 commit comments

Comments
 (0)