Compare commits

...

16 Commits

Author SHA1 Message Date
6e46af4c13 Prepare for Upload mods 2023-07-13 02:35:38 +03:00
d21798aaf1 Minor fix 2023-07-13 02:34:56 +03:00
22105b2030 Minor fix 2023-07-13 02:34:25 +03:00
19c121f208 Refactor Client ID 2023-07-13 02:33:45 +03:00
85c379bd9e Minor fixes 2023-07-13 01:17:01 +03:00
a15eb316bb Update TODOs 2023-07-13 01:16:10 +03:00
cecd6f13d6 Handle FastApi Log in file 2023-07-13 00:44:51 +03:00
df171aaa70 Minor fix 2023-07-13 00:31:02 +03:00
5a1cb8a133 Handle web logs 2023-07-13 00:23:57 +03:00
d44cff1116 Update version 2023-07-12 23:58:35 +03:00
bc6cf60099 Update TODOs 2023-07-12 23:58:23 +03:00
fc886ef415 Create log history 2023-07-12 23:58:14 +03:00
bd7b988b01 logs 2023-07-12 23:39:52 +03:00
4f5a6edc48 Update TODOs 2023-07-12 20:56:24 +03:00
541849642c BEAMP -> BeamMP 2023-07-12 20:56:14 +03:00
f364f29d79 Update TODOs 2023-07-12 20:49:39 +03:00
16 changed files with 184 additions and 118 deletions

1
.gitignore vendored
View File

@ -137,3 +137,4 @@ dmypy.json
/src/plugins
/test/
*test.py
logs/

View File

@ -7,43 +7,48 @@ BeamingDrive Multiplayer (BeamMP) server compatible with BeamMP clients.
## TODOs
- [ ] Server core
- [x] BEAMP System
- [x] Private access without key (Direct connect)
- [x] Server authentication (For public access)
- [x] BeamMP System
- [x] Private access (Without key, Direct connect)
- [x] Public access (With key, listing in Launcher)
- [X] Player authentication
- [ ] TCP Server part:
- [x] Handle code
- [x] Understanding beamp header
- [x] Understanding BeamMP header
- [ ] Upload mods
- [x] Connecting to the world
- [x] Chat
- [ ] Player counter _(Code: Ss)_
- [ ] Car state synchronizations _(Codes: We, Vi)_
- [ ] "ABG:" (compressed data)
- [x] Decompress data
- [ ] Vehicle data
- [ ] Players synchronizations
- [ ] UDP Server part:
- [ ] Players synchronizations
- [ ] Ping
- [ ] Player counter
- [ ] Players synchronizations _(Code: Zp)_
- [ ] Ping _(Code: p)_
- [x] Additional:
- [x] Logger
- [x] Just logging
- [x] Log in file
- [x] Log history (.1.log, .2.log, ...)
- [x] Console:
- [x] Tabulation
- [ ] _(Deferred)_ Static text
- [ ] _(Deferred)_ Static text (bug)
- [x] Events System
- [x] Call events
- [x] Create custom events
- [ ] Return from events
- [x] Plugins support
- [x] Load Python plugins
- [ ] Load Lua plugins (Original BEAMP compatibility)
- [ ] Load Lua plugins (Original BeamMP compatibility)
- [x] MultiLanguage (i18n support)
- [x] Core
- [x] Console
- [x] WebAPI
- [x] HTTP API Server (fastapi)
- [x] Stop and Start with core
- [x] Custom logger
- [x] Configure FastAPI logger
- [ ] Sync with event system
- [ ] Add methods...
- [ ] [Documentation](docs/en/readme.md)
## Installation

View File

@ -7,8 +7,8 @@
"stop": "Сервер остановлен!",
"": "Server auth",
"auth_need_key": "Нужен BEAMP ключ для запуска!",
"auth_empty_key": "BEAMP ключ пустой!",
"auth_need_key": "Нужен BeamMP ключ для запуска!",
"auth_empty_key": "BeamMP ключ пустой!",
"auth_cannot_open_browser": "Не получилось открыть браузер: {}",
"auth_use_link": "Используй эту ссылку: {}",
@ -17,7 +17,7 @@
"GUI_no": "Нет",
"GUI_ok": "Окей",
"GUI_cancel": "Отмена",
"GUI_need_key_message": "Нужен BEAMP ключ для запуска!\nХотите открыть ссылку в браузере для получения ключа?",
"GUI_need_key_message": "Нужен BeamMP ключ для запуска!\nХотите открыть ссылку в браузере для получения ключа?",
"GUI_enter_key_message": "Пожалуйста введите ключ:",
"GUI_cannot_open_browser": "Не получилось открыть браузер.\nИспользуй эту ссылку: {}",

View File

@ -43,7 +43,7 @@ Server:
server_ip: 0.0.0.0
server_port: 30814
```
* Если поставить `private: false` и не установить `key`, то сервер запросит BEAMP ключ, без него не запустится.
* Введя BEAMP ключ сервер появится в списке лаунчера.
* Если поставить `private: false` и не установить `key`, то сервер запросит BeamMP ключ, без него не запустится.
* Введя BeamMP ключ сервер появится в списке лаунчера.
* Взять ключ можно тут: [https://beammp.com/k/keys](https://beammp.com/k/keys)

View File

@ -67,13 +67,13 @@ ev.builtins_hook()
log.info(i18n.hello)
log.info(i18n.config_path.format(config_path))
log.debug("Initializing BEAMP Server system...")
log.debug("Initializing BeamMP Server system...")
# Key handler..
if not config.Auth['private'] and not config.Auth['key']:
log.warn(i18n.auth_need_key)
url = "https://beammp.com/k/keys"
if shortcuts.yes_no_dialog(
title='BEAMP Server Key',
title='BeamMP Server Key',
text=i18n.GUI_need_key_message,
yes_text=i18n.GUI_yes,
no_text=i18n.GUI_no).run():
@ -84,12 +84,12 @@ if not config.Auth['private'] and not config.Auth['key']:
log.error(i18n.auth_cannot_open_browser.format(e))
log.info(i18n.auth_use_link.format(url))
shortcuts.message_dialog(
title='BEAMP Server Key',
title='BeamMP Server Key',
text=i18n.GUI_cannot_open_browser.format(url),
ok_text=i18n.GUI_ok).run()
config.Auth['key'] = shortcuts.input_dialog(
title='BEAMP Server Key',
title='BeamMP Server Key',
text=i18n.GUI_enter_key_message,
ok_text=i18n.GUI_ok,
cancel_text=i18n.GUI_cancel).run()

View File

@ -23,6 +23,7 @@ class Client:
def __init__(self, reader, writer, core):
self.reader = reader
self.writer = writer
self.down_rw = (None, None)
self.log = utils.get_logger("client(None:0)")
self.addr = writer.get_extra_info("sockname")
self.loop = asyncio.get_event_loop()
@ -61,7 +62,7 @@ class Client:
async def tcp_send(self, data):
# TNetwork.cpp; Line: 383
# BEAMP TCP protocol sends a header of 4 bytes, followed by the data.
# BeamMP TCP protocol sends a header of 4 bytes, followed by the data.
# [][][][][][]...[]
# ^------^^---...-^
# size data
@ -108,18 +109,31 @@ class Client:
return data
async def sync_resources(self):
await self.tcp_send(b"P" + bytes(f"{self.cid}", "utf-8"))
while True:
data = await self.recv()
if data.startswith(b"SR"):
await self.tcp_send(b"-") # Cannot handle mods for now.
if data.startswith(b"f"):
# TODO: SendFile
pass
elif data.startswith(b"SR"):
# TODO: Create mods list
self.log.debug("Sending Mod Info")
mods = []
mod_list = b''
# * code *
if len(mods) == 0:
await self.tcp_send(b"-")
else:
await self.tcp_send(mod_list)
data = await self.recv()
if data == b"Done":
await self.tcp_send(b"M/levels/" + bytes(config.Game['map'], 'utf-8') + b"/info.json")
await self.last_handle()
break
async def last_handle(self):
async def looper(self):
# self.is_disconnected()
self.log.debug(f"Alive: {self.alive}")
await self.tcp_send(b"P" + bytes(f"{self.cid}", "utf-8"))
await self.sync_resources()
while self.alive:
data = await self.recv()
if data == b"":
@ -147,8 +161,9 @@ class Core:
self.loop = asyncio.get_event_loop()
self.run = False
self.direct = False
self.clients = {}
self.clients_counter = 0
self.clients = []
self.clients_by_id = {}
self.clients_by_nick = {}
self.mods_dir = "./mods"
self.mods_list = [0, ]
self.server_ip = config.Server["server_ip"]
@ -160,33 +175,42 @@ class Core:
self.web_stop = None
self.client_major_version = "2.0"
self.BEAMP_version = "3.2.0"
self.BeamMP_version = "3.2.0"
def get_client(self, sock=None, cid=None):
def get_client(self, sock=None, cid=None, nick=None):
if cid:
return self.clients.get(cid)
return self.clients_by_id.get(cid)
if nick:
return self.clients_by_nick.get(nick)
if sock:
return self.clients.get(sock.getsockname())
return self.clients_by_nick.get(sock.getsockname())
def insert_client(self, client):
self.log.debug(f"Inserting client: {client.cid}")
self.clients.update({client.cid: client, client.nick: client})
self.clients_by_nick.update({client.nick: client})
self.clients_by_id.update({client.cid: client})
self.clients[client.cid] = client
def create_client(self, *args, **kwargs):
client = Client(*args, **kwargs)
self.clients_counter += 1
client.id = self.clients_counter
cid = 1
for client in self.clients:
if client.cid == cid:
cid += 1
else:
break
client.cid = cid
client._update_logger()
self.log.debug(f"Create client: {client.cid}; clients_counter: {self.clients_counter}")
self.log.debug(f"Create client; client.cid: {client.cid};")
return client
async def check_alive(self):
await asyncio.sleep(5)
self.log.debug(f"Checking if clients is alive")
for cl in self.clients.values():
d = await cl.is_disconnected()
for client in self.clients:
d = client.is_disconnected()
if d:
self.log.debug(f"Client ID: {cl.id} died...")
self.log.debug(f"Client ID: {client.cid} died...")
@staticmethod
def start_web():
@ -224,7 +248,7 @@ class Core:
while self.run:
data = {"uuid": config.Auth["key"], "players": len(self.clients), "maxplayers": config.Game["players"],
"port": config.Server["server_port"], "map": f"/levels/{config.Game['map']}/info.json",
"private": config.Auth['private'], "version": self.BEAMP_version,
"private": config.Auth['private'], "version": self.BeamMP_version,
"clientversion": self.client_major_version,
"name": config.Server["name"], "modlist": modlist, "modstotalsize": modstotalsize,
"modstotal": modstotal, "playerslist": "", "desc": config.Server['description'], "pass": False}
@ -337,5 +361,6 @@ class Core:
def stop(self):
self.run = False
self.log.info(i18n.stop)
if config.WebAPI["enabled"]:
asyncio.run(self.web_stop())
exit(0)

View File

@ -7,7 +7,7 @@
import asyncio
from asyncio import StreamWriter, StreamReader
from threading import Thread
from typing import Callable
from typing import Callable, List, Dict, Tuple
from core import utils
from .tcp_server import TCPServer
@ -19,6 +19,7 @@ class Client:
def __init__(self, reader: StreamReader, writer: StreamWriter, core: Core) -> "Client":
self.reader = reader
self.writer = writer
self.down_rw: Tuple[StreamReader, StreamWriter] | Tuple[None, None] = (None, None)
self.log = utils.get_logger("client(id: )")
self.addr = writer.get_extra_info("sockname")
self.loop = asyncio.get_event_loop()
@ -34,7 +35,7 @@ class Client:
async def tcp_send(self, data: bytes) -> None: ...
async def sync_resources(self) -> None: ...
async def recv(self) -> bytes: ...
async def last_handle(self) -> bytes: ...
async def looper(self) -> None: ...
def _update_logger(self) -> None: ...
@ -44,7 +45,9 @@ class Core:
self.loop = asyncio.get_event_loop()
self.run = False
self.direct = False
self.clients = dict()
self.clients: List[Client]= []
self.clients_by_id: Dict[{int: Client}]= {}
self.clients_by_nick: Dict[{str: Client}] = {}
self.clients_counter: int = 0
self.mods_dir: str = "mods"
self.mods_list: list = []
@ -55,7 +58,7 @@ class Core:
self.web_thread: Thread = None
self.web_stop: Callable = lambda: None
self.client_major_version = "2.0"
self.BEAMP_version = "3.2.0"
self.BeamMP_version = "3.2.0"
def insert_client(self, client: Client) -> None: ...
def create_client(self, *args, **kwargs) -> Client: ...
async def check_alive(self) -> None: ...

View File

@ -25,10 +25,7 @@ class TCPServer:
self.log.info(f"Identifying new ClientConnection...")
data = await client.recv()
self.log.debug(f"recv1 data: {data}")
if len(data) > 50:
await client.kick("Too long data")
return False, None
if "VC2.0" not in data.decode("utf-8"):
if data.decode("utf-8") != f"VC{self.Core.client_major_version}":
await client.kick("Outdated Version.")
return False, None
else:
@ -57,8 +54,9 @@ class TCPServer:
self.log.error(f"Auth error: {e}")
await client.kick('Invalid authentication data! Try to connect in 5 minutes.')
# TODO: Password party
# await client.tcp_send(b"S") # Ask client key (How?)
for _client in self.Core.clients:
if _client.nick == client.nick and _client.guest == client.guest:
await client.kick('Stale Client (replaced by new client)')
ev.call_event("on_auth", client)
@ -70,9 +68,17 @@ class TCPServer:
return True, client
async def handle_download(self, writer):
# TODO: HandleDownload
self.log.debug(f"Client: \"IP: {0!r}; ID: {0}\" - HandleDownload!")
async def set_down_rw(self, reader, writer):
try:
cid = (await reader.read(1)).decode() # FIXME: wtf? 1 byte?
self.log.debug(f"Client: \"ID: {cid}\" - HandleDownload!")
if not cid.isdigit():
return False
for _client in self.Core.clients:
if _client.cid == cid:
_client.down_rw = (reader, writer)
return True
finally:
return False
async def handle_code(self, code, reader, writer):
@ -80,12 +86,11 @@ class TCPServer:
case "C":
result, client = await self.auth_client(reader, writer)
if result:
await client.sync_resources()
# await client.kick("Authentication success! Server not ready.")
await client.looper()
return True
return False
case "D":
return await self.handle_download(writer)
return await self.set_down_rw(reader, writer)
case "P":
writer.write(b"P")
await writer.drain()
@ -115,7 +120,7 @@ class TCPServer:
self.log.debug("Starting TCP server.")
try:
server = await asyncio.start_server(self.handle_client, self.host, self.port,
backlog=config.Game["players"] + 1)
backlog=int(config.Game["players"] * 1.3))
except OSError as e:
self.log.error(f"Error: {e}")
self.Core.run = False

View File

@ -20,7 +20,7 @@ class TCPServer:
self.port = port
self.loop = asyncio.get_event_loop()
async def auth_client(self, reader: StreamReader, writer: StreamWriter) -> Tuple[bool, Client]: ...
async def handle_download(self, writer: StreamWriter) -> bool: ...
async def set_down_rw(self, reader: StreamReader, writer: StreamWriter) -> bool: ...
async def handle_code(self, code: str, reader: StreamReader, writer: StreamWriter) -> bool: ...
async def handle_client(self, reader: StreamReader, writer: StreamWriter) -> None: ...
async def start(self) -> None: ...

View File

@ -1,21 +1,37 @@
# Developed by KuiToi Dev
# File core.utils.py
# Written by: SantaSpeen
# Version 1.0
# Version 1.1
# Licence: FPA
# (c) kuitoi.su 2023
import datetime
import logging
import os
import tarfile
log_format = "[%(asctime)s | %(name)-14s | %(levelname)-5s] %(message)s"
log_format_access = '[%(asctime)s | %(name)-14s | %(levelname)-5s] %(client_addr)s - "%(request_line)s" %(status_code)s'
log_file = "server.log"
log_dir = "./logs/"
log_file = log_dir + "server.log"
log_level = logging.INFO
# Инициализируем логирование
logging.basicConfig(level=log_level, format=log_format)
# Настройка логирование в файл.
# if os.path.exists(log_file):
# os.remove(log_file)
if not os.path.exists(log_dir):
os.mkdir(log_dir)
if os.path.exists(log_file):
mtime = os.path.getmtime(log_file)
gz_path = log_dir + datetime.datetime.fromtimestamp(mtime).strftime('%d.%m.%Y') + "-%s.tar.gz"
index = 1
while True:
if not os.path.exists(gz_path % index):
break
index += 1
with tarfile.open(gz_path % index, "w:gz") as tar:
logs_files = [log_file, "./logs/web.log", "./logs/web_access.log"]
for file in logs_files:
if os.path.exists(file):
tar.add(file, os.path.basename(file))
os.remove(file)
fh = logging.FileHandler(log_file, encoding='utf-8')
fh.setFormatter(logging.Formatter(log_format))

View File

@ -14,6 +14,7 @@ from prompt_toolkit import PromptSession, print_formatted_text, HTML
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
from prompt_toolkit.completion import NestedCompleter
from prompt_toolkit.history import FileHistory
from prompt_toolkit.patch_stdout import patch_stdout
from core import get_logger
@ -186,8 +187,12 @@ class Console:
session = PromptSession(history=FileHistory('./.cmdhistory'))
while True:
try:
cmd_in = await session.prompt_async(self.__prompt_in,
completer=self.completer, auto_suggest=AutoSuggestFromHistory())
with patch_stdout():
cmd_in = await session.prompt_async(
self.__prompt_in,
completer=self.completer,
auto_suggest=AutoSuggestFromHistory()
)
cmd_s = cmd_in.split(" ")
cmd = cmd_s[0]
if cmd == "":
@ -210,13 +215,3 @@ class Console:
def stop(self, *args, **kwargs):
self.__is_run = False
raise KeyboardInterrupt
# if __name__ == '__main__':
# c = Console()
# c.logger_hook()
# c.builtins_hook()
# log = logging.getLogger(name="name")
# log.info("Starting console")
# print("Starting console")
# asyncio.run(c.start())

View File

@ -21,7 +21,7 @@ class EventsSystem:
self.log.debug(f"register_event({event_name}, {event_func}):")
if not callable(event_func):
self.log.error(f"Cannot add event '{event_name}'. "
f"Use `BEAMP.add_event({event_name}', function)` instead. Skipping it...")
f"Use `KuiToi.add_event({event_name}', function)` instead. Skipping it...")
return
if event_name not in self.__events:
self.__events.update({str(event_name): [event_func]})

View File

@ -8,7 +8,7 @@ class KuiToi:
def __init__(self, name=None):
if name is None:
raise Exception("BEAMP: Name is required")
raise Exception("BeamMP: Name is required")
self.log = get_logger(f"PluginsLoader | {name}")
self.name = name
@ -43,7 +43,7 @@ class PluginsLoader:
plugin.print = print
file = os.path.join(self.__plugins_dir, file)
with open(f'{file}', 'r') as f:
code = f.read().replace("import BEAMP\n", "")
code = f.read().replace("import KuiToi\n", "")
exec(code, plugin.__dict__)
plugin.load()
self.__plugins.update({file[:-3]: plugin})

View File

@ -7,7 +7,6 @@ from fastapi.exceptions import RequestValidationError
from starlette import status
from starlette.exceptions import HTTPException as StarletteHTTPException
from starlette.responses import JSONResponse
from uvicorn.config import LOGGING_CONFIG
import core.utils
from . import utils
@ -21,30 +20,6 @@ uvserver = None
data_pool = []
data_run = [True]
LOGGING_CONFIG["formatters"]["default"]['fmt'] = core.utils.log_format
LOGGING_CONFIG["formatters"]["access"]["fmt"] = core.utils.log_format_access
LOGGING_CONFIG["formatters"].update({
"file_default": {
"fmt": core.utils.log_format
},
"file_access": {
"fmt": core.utils.log_format_access
}
})
LOGGING_CONFIG["handlers"]["default"]['stream'] = "ext://sys.stdout"
LOGGING_CONFIG["handlers"].update({
"file_default": {
"class": "logging.handlers.RotatingFileHandler",
"filename": "webserver.log"
},
"file_access": {
"class": "logging.handlers.RotatingFileHandler",
"filename": "webserver.log"
}
})
LOGGING_CONFIG["loggers"]["uvicorn"]["handlers"].append("file_default")
LOGGING_CONFIG["loggers"]["uvicorn.access"]["handlers"].append("file_access")
def response(data=None, code=status.HTTP_200_OK, error_code=0, error_message=None):
if 200 >= code <= 300:
@ -78,6 +53,7 @@ async def _method(method, secret_key: str = None):
async def _stop():
await asyncio.sleep(1)
if uvserver is not None:
uvserver.should_exit = True
data_run[0] = False

View File

@ -2,10 +2,17 @@ import asyncio
import sys
import click
from uvicorn.server import Server, logger
import uvicorn.server as uvs
from uvicorn.config import LOGGING_CONFIG
from uvicorn.lifespan import on
import core.utils
# logger = core.utils.get_logger("uvicorn")
# uvs.logger = logger
logger = uvs.logger
def ev_log_started_message(self, listeners) -> None:
cfg = self.config
@ -42,7 +49,7 @@ async def ev_shutdown(self, sockets=None) -> None:
try:
await asyncio.wait_for(self._wait_tasks_to_complete(), timeout=self.config.timeout_graceful_shutdown)
except asyncio.TimeoutError:
logger.error("Cancel %s running task(s), timeout graceful shutdown exceeded",len(self.server_state.tasks))
logger.error("Cancel %s running task(s), timeout graceful shutdown exceeded", len(self.server_state.tasks))
for t in self.server_state.tasks:
if sys.version_info < (3, 9):
t.cancel()
@ -81,7 +88,40 @@ async def on_shutdown(self) -> None:
def hack_fastapi():
Server.shutdown = ev_shutdown
Server._log_started_message = ev_log_started_message
uvs.Server.shutdown = ev_shutdown
uvs.Server._log_started_message = ev_log_started_message
on.LifespanOn.startup = on_startup
on.LifespanOn.shutdown = on_shutdown
LOGGING_CONFIG["formatters"]["default"]['fmt'] = core.utils.log_format
LOGGING_CONFIG["formatters"]["access"]["fmt"] = core.utils.log_format
LOGGING_CONFIG["formatters"].update({
"file_default": {
"()": "logging.Formatter",
"fmt": core.utils.log_format
},
"file_access": {
"()": "logging.Formatter",
"fmt": core.utils.log_format
}
})
LOGGING_CONFIG["handlers"]["default"]['stream'] = "ext://sys.stdout"
LOGGING_CONFIG["handlers"].update({
"file_default": {
"class": "logging.handlers.RotatingFileHandler",
"filename": "./logs/web.log",
"encoding": "utf-8",
"formatter": "file_default"
},
"file_access": {
"class": "logging.handlers.RotatingFileHandler",
"filename": "./logs/web_access.log",
"encoding": "utf-8",
"formatter": "file_access"
}
})
LOGGING_CONFIG["loggers"]["uvicorn"]["handlers"].append("file_default")
LOGGING_CONFIG["loggers"]["uvicorn.access"]["handlers"].append("file_access")
print(LOGGING_CONFIG)

View File

@ -7,8 +7,8 @@
"stop": "Сервер остановлен!",
"": "Server auth",
"auth_need_key": "Нужен BEAMP ключ для запуска!",
"auth_empty_key": "BEAMP ключ пустой!",
"auth_need_key": "Нужен BeamMP ключ для запуска!",
"auth_empty_key": "BeamMP ключ пустой!",
"auth_cannot_open_browser": "Не получилось открыть браузер: {}",
"auth_use_link": "Используй эту ссылку: {}",
@ -17,7 +17,7 @@
"GUI_no": "Нет",
"GUI_ok": "Окей",
"GUI_cancel": "Отмена",
"GUI_need_key_message": "Нужен BEAMP ключ для запуска!\nХотите открыть ссылку в браузере для получения ключа?",
"GUI_need_key_message": "Нужен BeamMP ключ для запуска!\nХотите открыть ссылку в браузере для получения ключа?",
"GUI_enter_key_message": "Пожалуйста введите ключ:",
"GUI_cannot_open_browser": "Не получилось открыть браузер.\nИспользуй эту ссылку: {}",