Creation d'un module http_client pour éviter la redondance et faciliter le logging. Ré-écriture en utilisant ce module

This commit is contained in:
2026-03-24 10:30:40 +01:00
parent bad0cb43bf
commit 177f224760
4 changed files with 145 additions and 37 deletions

View File

@@ -1,20 +1,21 @@
import base64 import base64
import requests
from cryptography.hazmat.primitives import serialization, hashes from cryptography.hazmat.primitives import serialization, hashes
from cryptography.hazmat.primitives.asymmetric import ec from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.kdf.hkdf import HKDF from cryptography.hazmat.primitives.kdf.hkdf import HKDF
from cryptography.hazmat.primitives.ciphers.aead import AESGCM from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from config.constants import Urls from config.constants import Urls
from tools.http_client import http_post, ApiError
class GetServerTokenForDiscord: class GetServerTokenForDiscord:
derived_key: bytes | None = None derived_key: bytes | None = None
@staticmethod @staticmethod
def authenticate(server = Urls.API_URL.value): def authenticate(server: str | None = Urls.API_URL.value) -> str:
if server is None: if server is None:
server = Urls.API_URL.value server = Urls.API_URL.value
# ========================== # ==========================
# Génération clé ECDH client # Génération clé ECDH client
# ========================== # ==========================
@@ -29,14 +30,22 @@ class GetServerTokenForDiscord:
# ========================== # ==========================
# AUTH # AUTH
# ========================== # ==========================
auth = requests.post(server + "/api_v2/auth", verify=False, json={ try:
"client_pub": base64.b64encode(client_pub_pem).decode() auth = http_post(
}).json() 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( server_pub_b64 = auth.get("server_pub")
base64.b64decode(auth["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) shared_key = client_private.exchange(ec.ECDH(), server_pub)
GetServerTokenForDiscord.derived_key = HKDF( GetServerTokenForDiscord.derived_key = HKDF(
@@ -49,31 +58,56 @@ class GetServerTokenForDiscord:
return auth["session_id"] return auth["session_id"]
@staticmethod @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 # DISCORD TOKEN
# ========================== # ==========================
if server is None: if server is None:
server = Urls.API_URL.value 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"]) if GetServerTokenForDiscord.derived_key is None:
encrypted_data = base64.b64decode(download["data"]) 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] aesgcm = AESGCM(GetServerTokenForDiscord.derived_key) # type: ignore[arg-type]
return aesgcm.decrypt(nonce, encrypted_data, None) return aesgcm.decrypt(nonce, encrypted_data, None)
@staticmethod @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: if server is None:
server = Urls.API_URL.value 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))

View File

@@ -1,21 +1,22 @@
import requests from tools.http_client import ApiError, http_get
from urllib3 import disable_warnings
from urllib3.exceptions import InsecureRequestWarning
from config.constants import PlayerServerInfo from config.constants import PlayerServerInfo
# Supress only InsecureRequestWarning WHITELIST_URL_ENDPOINT = "iswhitelist/"
disable_warnings(InsecureRequestWarning)
WHITELIST_URL_ENDPOINT = f'iswhitelist/'
class WhiteList: class WhiteList:
@staticmethod @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) PlayerServerInfo.is_whitelist = api_data.get("whitelisted", False)
api_data = response.json() PlayerServerInfo.is_staff = api_data.get("isStaff", False)
PlayerServerInfo.is_whitelist = api_data.get('whitelisted', False)
PlayerServerInfo.is_staff = api_data.get('isStaff', False)
#PlayerServerInfo.is_staff = True

73
src/tools/http_client.py Normal file
View File

@@ -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)

View File

@@ -49,7 +49,7 @@ class MainWindow(QMainWindow):
self.ui.queue_position.hide() self.ui.queue_position.hide()
self.ui.stackedWidget.setCurrentIndex(1) 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: if PlayerServerInfo.is_whitelist:
self.start_queue() self.start_queue()