From 177f2247603f48d6270a7d44ae861fd89f65e332 Mon Sep 17 00:00:00 2001 From: Xarkam Date: Tue, 24 Mar 2026 10:30:40 +0100 Subject: [PATCH] =?UTF-8?q?Creation=20d'un=20module=20http=5Fclient=20pour?= =?UTF-8?q?=20=C3=A9viter=20la=20redondance=20et=20faciliter=20le=20loggin?= =?UTF-8?q?g.=20R=C3=A9-=C3=A9criture=20en=20utilisant=20ce=20module?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/fivemserver/get_server_token.py | 80 ++++++++++++++++++++--------- src/fivemserver/whitelistmanager.py | 27 +++++----- src/tools/http_client.py | 73 ++++++++++++++++++++++++++ src/ui/main_window.py | 2 +- 4 files changed, 145 insertions(+), 37 deletions(-) create mode 100644 src/tools/http_client.py diff --git a/src/fivemserver/get_server_token.py b/src/fivemserver/get_server_token.py index 70ce9f3..81988d9 100644 --- a/src/fivemserver/get_server_token.py +++ b/src/fivemserver/get_server_token.py @@ -1,20 +1,21 @@ import base64 -import requests from cryptography.hazmat.primitives import serialization, hashes from cryptography.hazmat.primitives.asymmetric import ec from cryptography.hazmat.primitives.kdf.hkdf import HKDF from cryptography.hazmat.primitives.ciphers.aead import AESGCM from config.constants import Urls +from tools.http_client import http_post, ApiError + class GetServerTokenForDiscord: derived_key: bytes | None = None @staticmethod - def authenticate(server = Urls.API_URL.value): - + def authenticate(server: str | None = Urls.API_URL.value) -> str: if server is None: server = Urls.API_URL.value + # ========================== # Génération clé ECDH client # ========================== @@ -29,14 +30,22 @@ class GetServerTokenForDiscord: # ========================== # AUTH # ========================== - auth = requests.post(server + "/api_v2/auth", verify=False, json={ - "client_pub": base64.b64encode(client_pub_pem).decode() - }).json() + try: + auth = http_post( + f"{server}/api_v2/auth", + json={"client_pub": base64.b64encode(client_pub_pem).decode()}, + ).json() + except ApiError: + raise + except ValueError as exc: + raise ApiError("Réponse JSON invalide lors de l'authentification.", url=f"{server}/api_v2/auth") from exc - server_pub = serialization.load_pem_public_key( - base64.b64decode(auth["server_pub"]) - ) + server_pub_b64 = auth.get("server_pub") + session_id = auth.get("session_id") + if not server_pub_b64 or not session_id: + raise ApiError("Réponse d'authentification invalide : données manquantes.", url=f"{server}/api_v2/auth") + server_pub = serialization.load_pem_public_key(base64.b64decode(server_pub_b64)) shared_key = client_private.exchange(ec.ECDH(), server_pub) GetServerTokenForDiscord.derived_key = HKDF( @@ -49,31 +58,56 @@ class GetServerTokenForDiscord: return auth["session_id"] @staticmethod - def get_token(session_id: bytes, server = Urls.API_URL.value): + def get_token(session_id: bytes, server: str | None = Urls.API_URL.value) -> bytes: # ========================== # DISCORD TOKEN # ========================== if server is None: server = Urls.API_URL.value - download = requests.post(server + "/api_v2/tkn_auth", verify=False, headers={ - "x-session-id": session_id - }).json() - nonce = base64.b64decode(download["nonce"]) - encrypted_data = base64.b64decode(download["data"]) + if GetServerTokenForDiscord.derived_key is None: + raise ApiError("Clé dérivée manquante : appelez authenticate() avant get_token().") + + try: + download = http_post( + f"{server}/api_v2/tkn_auth", + headers={"x-session-id": session_id}, + ).json() + except ApiError: + raise + except ValueError as exc: + raise ApiError("Réponse JSON invalide lors de la récupération du token.", + url=f"{server}/api_v2/tkn_auth") from exc + + nonce_b64 = download.get("nonce") + encrypted_data_b64 = download.get("data") + if not nonce_b64 or not encrypted_data_b64: + raise ApiError("Réponse de token invalide : nonce ou data manquant.", url=f"{server}/api_v2/tkn_auth") + + nonce = base64.b64decode(nonce_b64) + encrypted_data = base64.b64decode(encrypted_data_b64) aesgcm = AESGCM(GetServerTokenForDiscord.derived_key) # type: ignore[arg-type] return aesgcm.decrypt(nonce, encrypted_data, None) @staticmethod - def register_discord_user(discord_user_id: str, session_id: str, server = Urls.API_URL.value) -> bool: + def register_discord_user( + discord_user_id: str, session_id: str, server: str | None = Urls.API_URL.value) -> bool: if server is None: server = Urls.API_URL.value - registeredId = requests.post(server + "/api_v2/connection/register", verify=False, headers={ - "x-session-id": session_id, - }, - json={ - "discord_id": discord_user_id - }).json() - return registeredId["success"] + try: + registration_data = http_post( + f"{server}/api_v2/connection/register", + headers={"x-session-id": session_id}, + json={"discord_id": discord_user_id}, + ).json() + except ApiError: + raise + except ValueError as exc: + raise ApiError( + "Réponse JSON invalide lors de l'enregistrement Discord.", + url=f"{server}/api_v2/connection/register", + ) from exc + + return bool(registration_data.get("success", False)) diff --git a/src/fivemserver/whitelistmanager.py b/src/fivemserver/whitelistmanager.py index f0f5647..a0cd2ad 100644 --- a/src/fivemserver/whitelistmanager.py +++ b/src/fivemserver/whitelistmanager.py @@ -1,21 +1,22 @@ -import requests -from urllib3 import disable_warnings -from urllib3.exceptions import InsecureRequestWarning +from tools.http_client import ApiError, http_get from config.constants import PlayerServerInfo -# Supress only InsecureRequestWarning -disable_warnings(InsecureRequestWarning) +WHITELIST_URL_ENDPOINT = "iswhitelist/" -WHITELIST_URL_ENDPOINT = f'iswhitelist/' class WhiteList: @staticmethod - def checkwhitelist(url, discord_user_id: str) -> None: + def check_whitelist(url: str, discord_user_id: str) -> None: + try: + api_data = http_get(f"{url}{WHITELIST_URL_ENDPOINT}{discord_user_id}").json() + except ApiError: + raise + except ValueError as exc: + raise ApiError( + "Réponse JSON invalide lors de la vérification whitelist.", + url=f"{url}{WHITELIST_URL_ENDPOINT}{discord_user_id}", + ) from exc - response = requests.get(url + WHITELIST_URL_ENDPOINT + discord_user_id, verify=False) - api_data = response.json() - - PlayerServerInfo.is_whitelist = api_data.get('whitelisted', False) - PlayerServerInfo.is_staff = api_data.get('isStaff', False) - #PlayerServerInfo.is_staff = True + PlayerServerInfo.is_whitelist = api_data.get("whitelisted", False) + PlayerServerInfo.is_staff = api_data.get("isStaff", False) diff --git a/src/tools/http_client.py b/src/tools/http_client.py new file mode 100644 index 0000000..0f4e501 --- /dev/null +++ b/src/tools/http_client.py @@ -0,0 +1,73 @@ +from __future__ import annotations + +import logging +from typing import Any, Literal + +import requests + + +DEFAULT_TIMEOUT = 15 +HttpMethod = Literal["GET", "POST", "PUT", "PATCH", "DELETE"] + +logger = logging.getLogger(__name__) + +class ApiError(RuntimeError): + def __init__(self, message: str, *, url: str | None = None, status_code: int | None = None) -> None: + super().__init__(message) + self.url = url + self.status_code = status_code + + +def http_request( + method: HttpMethod, + url: str, + *, + headers: dict[str, str] | None = None, + params: dict[str, Any] | None = None, + data: dict[str, Any] | None = None, + json: dict[str, Any] | None = None, + timeout: int = DEFAULT_TIMEOUT, +) -> requests.Response: + try: + response = requests.request( + method=method, + url=url, + headers=headers, + params=params, + data=data, + json=json, + verify=False, + timeout=timeout, + ) + response.raise_for_status() + return response + + except requests.RequestException as exc: + status_code = getattr(getattr(exc, "response", None), "status_code", None) + logger.warning("HTTP %s failed for %s (%s)", method, url, status_code or "no-status") + raise ApiError( + f"Erreur HTTP sur {method} {url}", + url=url, + status_code=status_code, + ) from exc + + +def http_get( + url: str, + *, + headers: dict[str, str] | None = None, + params: dict[str, Any] | None = None, + timeout: int = DEFAULT_TIMEOUT, +) -> requests.Response: + return http_request("GET", url, headers=headers, params=params, timeout=timeout) + + +def http_post( + url: str, + *, + headers: dict[str, str] | None = None, + data: dict[str, Any] | None = None, + json: dict[str, Any] | None = None, + timeout: int = DEFAULT_TIMEOUT, +) -> requests.Response: + return http_request("POST", url, headers=headers, data=data, json=json, timeout=timeout) diff --git a/src/ui/main_window.py b/src/ui/main_window.py index b77d735..d64c48c 100644 --- a/src/ui/main_window.py +++ b/src/ui/main_window.py @@ -49,7 +49,7 @@ class MainWindow(QMainWindow): self.ui.queue_position.hide() self.ui.stackedWidget.setCurrentIndex(1) - WhiteList.checkwhitelist(Urls.API_URL.value, self.config.get_discord_user()) + WhiteList.check_whitelist(Urls.API_URL.value, self.config.get_discord_user()) if PlayerServerInfo.is_whitelist: self.start_queue()