Password update
This commit is contained in:
parent
28cbbf2cad
commit
b0ff612023
39
README.MD
39
README.MD
|
|
@ -32,36 +32,28 @@ Everything happens in memory only. Nothing is written to disk.
|
||||||
|
|
||||||
## 🚀 Installation & Run
|
## 🚀 Installation & Run
|
||||||
|
|
||||||
### Option 1: Python
|
### Python
|
||||||
|
|
||||||
Install with pip:
|
1. Clone the repository:
|
||||||
```bash
|
`git clone https://github.com/emilycodestar/cmd-chat.git`
|
||||||
pip install secured_console_chat
|
`cd cmd-chat`
|
||||||
````
|
|
||||||
|
|
||||||
Run directly from Python:
|
2. Create a virtual environment and install dependencies:
|
||||||
|
|
||||||
```python
|
Linux / macOS:
|
||||||
import asyncio
|
`python -m venv venv && source venv/bin/activate && pip install -r requirements.txt`
|
||||||
import cmd_chat
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
Windows (PowerShell):
|
||||||
asyncio.run(cmd_chat.run())
|
`python -m venv venv ; .\venv\Scripts\activate ; pip install -r requirements.txt`
|
||||||
```
|
|
||||||
|
|
||||||
### Option 2: CLI
|
3. Start the server (set a password for client connections):
|
||||||
|
`python cmd_chat.py serve 0.0.0.0 1000 --password YOUR_PASSWORD`
|
||||||
|
|
||||||
Start the server:
|
4. Connect a client:
|
||||||
|
`python cmd_chat.py connect SERVER_IP 1000 USERNAME YOUR_PASSWORD`
|
||||||
|
|
||||||
```bash
|
Example (local run):
|
||||||
cmd_chat serve localhost 5000
|
`python cmd_chat.py connect localhost 1000 tyler YOUR_PASSWORD`
|
||||||
```
|
|
||||||
|
|
||||||
Connect to the server:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cmd_chat connect localhost 5000 tyler
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -70,4 +62,3 @@ cmd_chat connect localhost 5000 tyler
|
||||||
Here’s how it looks in action:
|
Here’s how it looks in action:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|
|
||||||
47
ROADMAP.md
Normal file
47
ROADMAP.md
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
## 🗺️ Roadmap
|
||||||
|
|
||||||
|
### 1. Core stability
|
||||||
|
|
||||||
|
- [ ] Switch from `ast.literal_eval` to `json` for messages.
|
||||||
|
- [ ] Add proper reconnect with heartbeat (ping/pong, timeouts).
|
||||||
|
- [ ] Clean error handling (no leaking stack traces to clients).
|
||||||
|
- [ ] Limit message size and frequency (basic anti-spam).
|
||||||
|
|
||||||
|
### 2. Security improvements
|
||||||
|
|
||||||
|
- [ ] Per-client symmetric keys instead of one global key.
|
||||||
|
- [ ] Upgrade RSA 512 → 2048 (or curve25519 ECDH + HKDF).
|
||||||
|
- [ ] Replace shared password with invite tokens or session-based bearer tokens.
|
||||||
|
- [ ] Force WSS (TLS) for production.
|
||||||
|
|
||||||
|
### 3. Chat features
|
||||||
|
|
||||||
|
- [ ] Multiple rooms (room_id support).
|
||||||
|
- [ ] Commands (`/nick`, `/clear`, `/help`, `/quit`).
|
||||||
|
- [ ] Message timestamps + sequence numbers.
|
||||||
|
- [ ] Delta updates (only send new messages instead of full history).
|
||||||
|
|
||||||
|
### 4. UX / Client
|
||||||
|
|
||||||
|
- [ ] Local encrypted history (optional).
|
||||||
|
- [ ] Customizable renderers (rich, minimal, json mode).
|
||||||
|
- [ ] Quiet reconnection status indicator.
|
||||||
|
- [ ] Configurable message buffer length.
|
||||||
|
|
||||||
|
### 5. File & media
|
||||||
|
|
||||||
|
- [ ] File transfer via encrypted chunks.
|
||||||
|
- [ ] Inline images (optional, in rich renderer).
|
||||||
|
|
||||||
|
### 6. Deployment & Ops
|
||||||
|
|
||||||
|
- [ ] Dockerfile + docker-compose (server + client).
|
||||||
|
- [ ] Add uvloop + multiple Sanic workers.
|
||||||
|
- [ ] Graceful shutdown & restart.
|
||||||
|
- [ ] Systemd service unit for server.
|
||||||
|
|
||||||
|
### 7. Privacy & audit
|
||||||
|
|
||||||
|
- [ ] Disable sensitive logs (no passwords/tokens in logs).
|
||||||
|
- [ ] Minimal server metrics: connected users, msg/sec.
|
||||||
|
- [ ] Configurable retention (in-memory only vs file-based).
|
||||||
|
|
@ -1,64 +1,38 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
import argparse
|
import argparse
|
||||||
|
|
||||||
from cmd_chat.server.server import run_server
|
from cmd_chat.server.server import run_server
|
||||||
from cmd_chat.client.client import Client
|
from cmd_chat.client.client import Client
|
||||||
|
|
||||||
|
def run_http_server(ip: str, port: int, password: str | None) -> None:
|
||||||
|
run_server(ip, int(port), False, password)
|
||||||
|
|
||||||
def run_http_server(
|
async def run_client(username: str, server: str, port: int, password: str | None) -> None:
|
||||||
ip: str,
|
Client(server=server, port=port, username=username, password=password).run()
|
||||||
port: int
|
|
||||||
) -> None:
|
|
||||||
run_server(ip, port, False)
|
|
||||||
|
|
||||||
|
|
||||||
async def run_client(
|
|
||||||
username: str,
|
|
||||||
server: str,
|
|
||||||
port: int
|
|
||||||
) -> None:
|
|
||||||
Client(
|
|
||||||
server = server,
|
|
||||||
port = port,
|
|
||||||
username = username
|
|
||||||
).run()
|
|
||||||
|
|
||||||
|
|
||||||
async def run() -> None:
|
async def run() -> None:
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(description='Command-line chat application')
|
||||||
description='Command-line chat application'
|
subparsers = parser.add_subparsers(dest='command', required=True)
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
'command',
|
|
||||||
choices=['serve', 'connect'],
|
|
||||||
help='Command to execute'
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
'ip_address',
|
|
||||||
help='IP address to serve or connect'
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
'port',
|
|
||||||
help='PORT of server'
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
'username',
|
|
||||||
nargs='?',
|
|
||||||
default='',
|
|
||||||
help='Username for connection (required for connect command)'
|
|
||||||
)
|
|
||||||
args = parser.parse_args()
|
|
||||||
if args.command == 'serve':
|
|
||||||
run_http_server(args.ip_address, int(args.port))
|
|
||||||
elif args.command == 'connect':
|
|
||||||
if not args.username:
|
|
||||||
parser.error("Username is required for 'connect' command")
|
|
||||||
await run_client(args.username, args.ip_address, int(args.port))
|
|
||||||
|
|
||||||
|
serve_p = subparsers.add_parser('serve', help='Run server')
|
||||||
|
serve_p.add_argument('ip_address')
|
||||||
|
serve_p.add_argument('port')
|
||||||
|
serve_p.add_argument('--password', '-p', required=True, help='Admin password required for clients')
|
||||||
|
|
||||||
|
connect_p = subparsers.add_parser('connect', help='Connect to server')
|
||||||
|
connect_p.add_argument('ip_address')
|
||||||
|
connect_p.add_argument('port')
|
||||||
|
connect_p.add_argument('username')
|
||||||
|
connect_p.add_argument('password', help='Password to auth on server')
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.command == 'serve':
|
||||||
|
run_http_server(args.ip_address, args.port, args.password)
|
||||||
|
elif args.command == 'connect':
|
||||||
|
await run_client(args.username, args.ip_address, int(args.port), args.password)
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
asyncio.run(run())
|
asyncio.run(run())
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
import ast
|
import ast
|
||||||
import time
|
import time
|
||||||
import threading
|
import threading
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from websocket import create_connection
|
from websocket import create_connection, WebSocketConnectionClosedException
|
||||||
|
|
||||||
from cmd_chat.client.core.crypto import RSAService
|
from cmd_chat.client.core.crypto import RSAService
|
||||||
from cmd_chat.client.core.default_renderer import DefaultClientRenderer
|
from cmd_chat.client.core.default_renderer import DefaultClientRenderer
|
||||||
|
|
@ -17,104 +18,154 @@ class Client(RSAService, RichClientRenderer):
|
||||||
self,
|
self,
|
||||||
server: str,
|
server: str,
|
||||||
port: int,
|
port: int,
|
||||||
username: str
|
username: str,
|
||||||
|
password: Optional[str] = None
|
||||||
):
|
):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
# Server info
|
|
||||||
self.server = server
|
self.server = server
|
||||||
self.port = port
|
self.port = port
|
||||||
self.username = username
|
self.username = username
|
||||||
# Urls
|
self.password = password or ""
|
||||||
self.base_url = f"http://{self.server}:{self.port}"
|
self.base_url = f"http://{self.server}:{self.port}"
|
||||||
self.talk_url = f"{self.base_url}/talk"
|
|
||||||
self.info_url = f"{self.base_url}/update"
|
|
||||||
self.key_url = f"{self.base_url}/get_key"
|
|
||||||
self.ws_url = f"ws://{self.server}:{self.port}"
|
self.ws_url = f"ws://{self.server}:{self.port}"
|
||||||
self.close_response = str({
|
self.close_response = str({
|
||||||
"action": "close",
|
"action": "close",
|
||||||
"username": self.username
|
"username": self.username
|
||||||
})
|
})
|
||||||
# Threads
|
|
||||||
self.__stop_threads = False
|
self.__stop_threads = False
|
||||||
|
|
||||||
|
def _ws_full(self, path: str) -> str:
|
||||||
|
if self.password:
|
||||||
|
return f"{self.ws_url}{path}?password={self.password}"
|
||||||
|
return f"{self.ws_url}{path}"
|
||||||
|
|
||||||
|
def _connect_ws(self, path: str, retries: int = 5, backoff: float = 0.5):
|
||||||
|
last_exc = None
|
||||||
|
for attempt in range(retries):
|
||||||
|
try:
|
||||||
|
return create_connection(self._ws_full(path))
|
||||||
|
except Exception as exc:
|
||||||
|
last_exc = exc
|
||||||
|
time.sleep(backoff * (2 ** attempt))
|
||||||
|
print(f"Can't connect to {path}: {last_exc}")
|
||||||
|
raise last_exc
|
||||||
|
|
||||||
def send_info(self):
|
def send_info(self):
|
||||||
""" sending message to websocket
|
ws = self._connect_ws("/talk")
|
||||||
"""
|
try:
|
||||||
ws = create_connection(f"{self.ws_url}/talk")
|
|
||||||
while not self.__stop_threads:
|
while not self.__stop_threads:
|
||||||
try:
|
try:
|
||||||
user_input = input("You're message: ")
|
user_input = input("You're message: ")
|
||||||
if user_input == "q":
|
if user_input == "q":
|
||||||
self.__stop_threads = True
|
self.__stop_threads = True
|
||||||
|
try:
|
||||||
|
if ws:
|
||||||
|
ws.send(self.close_response)
|
||||||
|
ws.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
break
|
||||||
message = f'{self.username}: {user_input}'
|
message = f'{self.username}: {user_input}'
|
||||||
socket_message = str({
|
socket_message = str({
|
||||||
"text": self._encrypt(message),
|
"text": self._encrypt(message),
|
||||||
"username": self.username
|
"username": self.username
|
||||||
})
|
})
|
||||||
ws.send(payload=socket_message.encode())
|
ws.send(socket_message)
|
||||||
|
except (WebSocketConnectionClosedException, ConnectionResetError, ConnectionAbortedError, OSError):
|
||||||
|
try:
|
||||||
|
if ws:
|
||||||
|
try:
|
||||||
|
ws.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
ws = self._connect_ws("/talk")
|
||||||
|
continue
|
||||||
|
except Exception:
|
||||||
|
print("Can't establish channel")
|
||||||
|
self.__stop_threads = True
|
||||||
|
break
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
|
self.__stop_threads = True
|
||||||
|
try:
|
||||||
ws.send(self.close_response)
|
ws.send(self.close_response)
|
||||||
ws.close()
|
ws.close()
|
||||||
self.__stop_threads = True
|
except Exception:
|
||||||
except Exception as exc:
|
pass
|
||||||
ws.send(self.close_response)
|
break
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
ws.close()
|
ws.close()
|
||||||
print("Something went wrong! ", exc)
|
except Exception:
|
||||||
self.__stop_threads = True
|
pass
|
||||||
raise exc
|
|
||||||
|
|
||||||
def update_info(self):
|
def update_info(self):
|
||||||
""" connecting to websocket,
|
ws = self._connect_ws("/update")
|
||||||
wating for updates,
|
|
||||||
updating every RENDER_TIME seconds
|
|
||||||
"""
|
|
||||||
ws = create_connection(f"{self.ws_url}/update")
|
|
||||||
last_try = None
|
last_try = None
|
||||||
|
try:
|
||||||
while not self.__stop_threads:
|
while not self.__stop_threads:
|
||||||
try:
|
try:
|
||||||
time.sleep(RENDER_TIME)
|
time.sleep(RENDER_TIME)
|
||||||
response = ast.literal_eval(ws.recv().decode('utf-8'))
|
raw = ws.recv()
|
||||||
|
if isinstance(raw, bytes):
|
||||||
|
raw = raw.decode("utf-8")
|
||||||
|
response = ast.literal_eval(raw)
|
||||||
if last_try == response:
|
if last_try == response:
|
||||||
continue
|
continue
|
||||||
last_try = response
|
last_try = response
|
||||||
self.clear_console()
|
self.clear_console()
|
||||||
if len(last_try["messages"]) > 0:
|
if len(last_try["messages"]) > 0:
|
||||||
self.print_chat(response = last_try)
|
self.print_chat(response=last_try)
|
||||||
except KeyboardInterrupt:
|
except (WebSocketConnectionClosedException, ConnectionResetError, ConnectionAbortedError, OSError):
|
||||||
ws.send(self.close_response)
|
try:
|
||||||
|
if ws:
|
||||||
|
try:
|
||||||
ws.close()
|
ws.close()
|
||||||
self.__stop_threads = True
|
except Exception:
|
||||||
except ConnectionAbortedError:
|
pass
|
||||||
# Reconnect if somehow client was disconnected
|
ws = self._connect_ws("/update")
|
||||||
ws = create_connection(f"{self.ws_url}/update")
|
|
||||||
continue
|
continue
|
||||||
except Exception as exc:
|
except Exception:
|
||||||
|
print("Connection lost: can't establish update channel")
|
||||||
|
self.__stop_threads = True
|
||||||
|
break
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
self.__stop_threads = True
|
||||||
|
try:
|
||||||
ws.send(self.close_response)
|
ws.send(self.close_response)
|
||||||
ws.close()
|
ws.close()
|
||||||
print("Something went wrong! ", exc)
|
except Exception:
|
||||||
self.__stop_threads = True
|
pass
|
||||||
raise exc
|
break
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
ws.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
def _validate_keys(self) -> None:
|
def _validate_keys(self) -> None:
|
||||||
self._request_key(self.key_url, self.username)
|
self._request_key(
|
||||||
|
url=f"{self.base_url}/get_key",
|
||||||
|
username=self.username,
|
||||||
|
password=self.password
|
||||||
|
)
|
||||||
self._remove_keys()
|
self._remove_keys()
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
# Running two threads,
|
|
||||||
# One for sending info
|
|
||||||
# Second one for updating info
|
|
||||||
self._validate_keys()
|
self._validate_keys()
|
||||||
threads = [
|
threads = [
|
||||||
threading.Thread(target=self.send_info),
|
threading.Thread(target=self.send_info, daemon=True),
|
||||||
threading.Thread(target=self.update_info)
|
threading.Thread(target=self.update_info, daemon=True)
|
||||||
]
|
]
|
||||||
for th in threads:
|
for th in threads:
|
||||||
th.start()
|
th.start()
|
||||||
|
for th in threads:
|
||||||
|
th.join()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
Client(
|
Client(
|
||||||
server=input("server ip:\n"),
|
server=input("server ip:\n"),
|
||||||
port=int(input("server port: \n")),
|
port=int(input("server port: \n")),
|
||||||
username=input("username:\n").replace(" ", "").lower()
|
username=input("username:\n").replace(" ", "").lower(),
|
||||||
|
password=input("password:\n")
|
||||||
).run()
|
).run()
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
|
|
||||||
class CryptoService(ABC):
|
class CryptoService(ABC):
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
|
|
@ -12,7 +11,7 @@ class CryptoService(ABC):
|
||||||
raise NotImplementedError("Need to implement decrypt method")
|
raise NotImplementedError("Need to implement decrypt method")
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def _request_key(self, url: str, username: str):
|
def _request_key(self, url: str, username: str, password: str | None = None):
|
||||||
raise NotImplementedError("Need to implement request key method")
|
raise NotImplementedError("Need to implement request key method")
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
|
|
|
||||||
|
|
@ -19,17 +19,20 @@ class RSAService(CryptoService):
|
||||||
self._generate_keys()
|
self._generate_keys()
|
||||||
|
|
||||||
def _encrypt(self, message: str) -> str:
|
def _encrypt(self, message: str) -> str:
|
||||||
return self.fernet.encrypt(message.encode())
|
return self.fernet.encrypt(message.encode()).decode("utf-8")
|
||||||
|
|
||||||
def _decrypt(self, message: str) -> str:
|
def _decrypt(self, message: str) -> str:
|
||||||
return self.fernet.decrypt(message.encode()).decode("utf-8")
|
return self.fernet.decrypt(message.encode()).decode("utf-8")
|
||||||
|
|
||||||
def _request_key(self, url: str, username: str):
|
def _request_key(self, url: str, username: str, password: str | None = None):
|
||||||
data = {
|
data = {
|
||||||
"pubkey": self._open_generated_file(self.public_key_name),
|
"pubkey": self._open_generated_file(self.public_key_name),
|
||||||
"username": username
|
"username": username
|
||||||
}
|
}
|
||||||
r = requests.get(url, data=data, stream=True)
|
if password:
|
||||||
|
data["password"] = password
|
||||||
|
r = requests.post(url, files={"pubkey": ("public.pem", data["pubkey"])}, data={"username": username, "password": password or ""}, stream=True)
|
||||||
|
r.raise_for_status()
|
||||||
message = r.raw.read(999)
|
message = r.raw.read(999)
|
||||||
self.symmetric_key = rsa.decrypt(message, self.private_key)
|
self.symmetric_key = rsa.decrypt(message, self.private_key)
|
||||||
self.fernet = Fernet(self.symmetric_key)
|
self.fernet = Fernet(self.symmetric_key)
|
||||||
|
|
@ -63,4 +66,7 @@ class RSAService(CryptoService):
|
||||||
|
|
||||||
def _remove_keys(self):
|
def _remove_keys(self):
|
||||||
for key in self.keys_path:
|
for key in self.keys_path:
|
||||||
|
try:
|
||||||
os.remove(key)
|
os.remove(key)
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,10 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
import rsa
|
import rsa
|
||||||
from cryptography.fernet import Fernet
|
from cryptography.fernet import Fernet
|
||||||
|
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
from sanic.worker.loader import AppLoader
|
from sanic.worker.loader import AppLoader
|
||||||
|
|
||||||
from sanic.response import HTTPResponse
|
from sanic.response import HTTPResponse
|
||||||
from sanic import Sanic, Request, response, Websocket
|
from sanic import Sanic, Request, response, Websocket
|
||||||
|
|
||||||
from cmd_chat.server.models import Message
|
from cmd_chat.server.models import Message
|
||||||
from cmd_chat.server.services import (
|
from cmd_chat.server.services import (
|
||||||
_get_bytes_and_serialize,
|
_get_bytes_and_serialize,
|
||||||
|
|
@ -18,70 +13,98 @@ from cmd_chat.server.services import (
|
||||||
_generate_update_payload
|
_generate_update_payload
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
app = Sanic("app")
|
app = Sanic("app")
|
||||||
app.config.OAS = False
|
app.config.OAS = False
|
||||||
|
|
||||||
|
|
||||||
# Message structure is:
|
|
||||||
# [username: message, ...]
|
|
||||||
MESSAGES_MEMORY_DB: list[Message] = []
|
MESSAGES_MEMORY_DB: list[Message] = []
|
||||||
|
|
||||||
|
|
||||||
# Users structure is
|
|
||||||
# {Ip, Username: Public key}
|
|
||||||
USERS: dict[str, str] = {}
|
USERS: dict[str, str] = {}
|
||||||
PUBLIC_KEY = Fernet.generate_key()
|
PUBLIC_KEY = Fernet.generate_key()
|
||||||
|
|
||||||
|
|
||||||
def attach_endpoints(app: Sanic):
|
def _check_password(request: Request, expected: str | None) -> bool:
|
||||||
|
if not expected:
|
||||||
|
return True
|
||||||
|
q = request.args.get("password")
|
||||||
|
f = request.form.get("password") if hasattr(request, "form") else None
|
||||||
|
return (q or f) == expected
|
||||||
|
|
||||||
|
def _get_str_arg(request: Request, name: str) -> str | None:
|
||||||
|
return request.form.get(name) or request.args.get(name)
|
||||||
|
|
||||||
|
def attach_endpoints(app: Sanic):
|
||||||
@app.websocket("/talk")
|
@app.websocket("/talk")
|
||||||
async def talk_ws_view(request: Request, ws: Websocket) -> HTTPResponse:
|
async def talk_ws_view(request: Request, ws: Websocket) -> HTTPResponse:
|
||||||
|
if not _check_password(request, app.ctx.ADMIN_PASSWORD):
|
||||||
|
await ws.close(code=4001, reason="unauthorized")
|
||||||
|
return
|
||||||
while True:
|
while True:
|
||||||
serialized_message: dict = await _get_bytes_and_serialize(ws)
|
serialized_message: dict = await _get_bytes_and_serialize(ws)
|
||||||
await _check_ws_for_close_status(
|
await _check_ws_for_close_status(serialized_message, ws)
|
||||||
serialized_message,
|
new_message = await _generate_new_message(serialized_message.get("text"))
|
||||||
ws
|
|
||||||
)
|
|
||||||
new_message = await _generate_new_message(
|
|
||||||
serialized_message.get("text")
|
|
||||||
)
|
|
||||||
MESSAGES_MEMORY_DB.append(new_message)
|
MESSAGES_MEMORY_DB.append(new_message)
|
||||||
await ws.send(
|
await ws.send(str({"status": "ok"}))
|
||||||
str({"status": "ok"})
|
|
||||||
)
|
|
||||||
await asyncio.sleep(0.2)
|
await asyncio.sleep(0.2)
|
||||||
|
|
||||||
|
|
||||||
@app.websocket("/update")
|
@app.websocket("/update")
|
||||||
async def update_ws_view(request: Request, ws: Websocket) -> HTTPResponse:
|
async def update_ws_view(request: Request, ws: Websocket) -> HTTPResponse:
|
||||||
|
if not _check_password(request, app.ctx.ADMIN_PASSWORD):
|
||||||
|
await ws.close(code=4001, reason="unauthorized")
|
||||||
|
return
|
||||||
while True:
|
while True:
|
||||||
payload = await _generate_update_payload(
|
payload = await _generate_update_payload(MESSAGES_MEMORY_DB, USERS)
|
||||||
MESSAGES_MEMORY_DB,
|
|
||||||
USERS
|
|
||||||
)
|
|
||||||
await ws.send(payload.encode())
|
await ws.send(payload.encode())
|
||||||
await asyncio.sleep(0.2)
|
await asyncio.sleep(0.2)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/get_key', methods=['GET', 'POST'])
|
@app.route('/get_key', methods=['GET', 'POST'])
|
||||||
async def get_key_view(request: Request) -> HTTPResponse:
|
async def get_key_view(request: Request) -> HTTPResponse:
|
||||||
public_key = rsa.PublicKey.load_pkcs1(request.form.get('pubkey'))
|
if not _check_password(request, app.ctx.ADMIN_PASSWORD):
|
||||||
|
return response.text("unauthorized", status=401)
|
||||||
|
|
||||||
|
pubkey_bytes: bytes | None = None
|
||||||
|
|
||||||
|
if "pubkey" in request.files and request.files.get("pubkey"):
|
||||||
|
f = request.files.get("pubkey")
|
||||||
|
if isinstance(f, list):
|
||||||
|
f = f[0]
|
||||||
|
pubkey_bytes = f.body
|
||||||
|
|
||||||
|
if pubkey_bytes is None:
|
||||||
|
raw = request.form.get("pubkey")
|
||||||
|
if raw:
|
||||||
|
pubkey_bytes = raw if isinstance(raw, bytes) else str(raw).encode()
|
||||||
|
|
||||||
|
if pubkey_bytes is None:
|
||||||
|
raw = request.args.get("pubkey")
|
||||||
|
if raw:
|
||||||
|
pubkey_bytes = raw.encode()
|
||||||
|
|
||||||
|
if not pubkey_bytes:
|
||||||
|
return response.text("bad request: pubkey is required", status=400)
|
||||||
|
|
||||||
|
try:
|
||||||
|
public_key = rsa.PublicKey.load_pkcs1(pubkey_bytes)
|
||||||
|
except Exception as e:
|
||||||
|
return response.text(f"bad pubkey: {e}", status=400)
|
||||||
|
|
||||||
encrypted_data = rsa.encrypt(PUBLIC_KEY, public_key)
|
encrypted_data = rsa.encrypt(PUBLIC_KEY, public_key)
|
||||||
if request.ip not in USERS:
|
|
||||||
USERS[f"{request.ip}, {request.form.get('username')}"] = PUBLIC_KEY
|
username = _get_str_arg(request, "username") or "unknown"
|
||||||
|
user_key = f"{request.ip}, {username}"
|
||||||
|
if user_key not in USERS:
|
||||||
|
USERS[user_key] = PUBLIC_KEY
|
||||||
|
|
||||||
return response.raw(encrypted_data)
|
return response.raw(encrypted_data)
|
||||||
|
|
||||||
|
|
||||||
def create_app(app_name: str) -> Sanic:
|
def create_app(app_name: str, admin_password: str | None) -> Sanic:
|
||||||
app = Sanic(app_name)
|
app = Sanic(app_name)
|
||||||
|
app.ctx.ADMIN_PASSWORD = admin_password
|
||||||
attach_endpoints(app)
|
attach_endpoints(app)
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|
||||||
def run_server(host: str, port: int, dev: bool=False) -> None:
|
def run_server(host: str, port: int, dev: bool = False, admin_password: str | None = None) -> None:
|
||||||
loader = AppLoader(factory=partial(create_app, "CMD_SERVER"))
|
loader = AppLoader(factory=partial(create_app, "CMD_SERVER", admin_password))
|
||||||
app = loader.load()
|
app = loader.load()
|
||||||
app.prepare(host=host, port=port, dev=dev)
|
app.prepare(host=host, port=port, dev=dev)
|
||||||
Sanic.serve(primary=app, app_loader=loader)
|
Sanic.serve(primary=app, app_loader=loader)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user