diff --git a/auth/app/auth.py b/auth/app/auth.py index 895fbe0..b0a9483 100644 --- a/auth/app/auth.py +++ b/auth/app/auth.py @@ -1,7 +1,8 @@ import base64 import json -from fastapi import HTTPException -from .models import UserLogin, UserInDB, Session, UserCreate +from fastapi import HTTPException, UploadFile +from fastapi.responses import JSONResponse +from .models import UserLogin, UserInDB, Session, UserCreate, SkinUpdate, CapeUpdate from .utils import ( verify_password, get_password_hash, @@ -11,6 +12,14 @@ from .utils import ( from .database import users_collection, sessions_collection import uuid from datetime import datetime, timedelta +from cryptography.hazmat.primitives import serialization, hashes +from cryptography.hazmat.primitives.asymmetric import padding +from dotenv import load_dotenv +import os + +load_dotenv() + +FILES_URL = os.getenv("FILES_URL") class AuthService: async def register(self, user: UserCreate): @@ -86,41 +95,61 @@ class AuthService: return {"accessToken": new_access_token, "clientToken": client_token} async def get_minecraft_profile(self, uuid: str): - formatted_uuid = f"{uuid[:8]}-{uuid[8:12]}-{uuid[12:16]}-{uuid[16:20]}-{uuid[20:]}" - user = await users_collection.find_one({"uuid": formatted_uuid}) + # Преобразуем UUID без дефисов в формат с дефисами (если нужно) + if '-' not in uuid: + formatted_uuid = f"{uuid[:8]}-{uuid[8:12]}-{uuid[12:16]}-{uuid[16:20]}-{uuid[20:]}" + else: + formatted_uuid = uuid + + user = await users_collection.find_one({"uuid": formatted_uuid}) # Ищем по UUID с дефисами if not user: - raise HTTPException( - status_code=404, - detail=f"User not found (searched for UUID: {formatted_uuid})" - ) - + raise HTTPException(status_code=404, detail="User not found") + textures = { - "timestamp": int(datetime.now().timestamp()), - "profileId": formatted_uuid, + "timestamp": int(datetime.now().timestamp() * 1000), + "profileId": user["uuid"], # UUID с дефисами "profileName": user["username"], - "textures": { - "SKIN": {"url": user.get("skin_url", "")}, - "CAPE": {"url": user.get("cloak_url", "")} - } if user.get("skin_url") or user.get("cloak_url") else {} + "textures": {} } - + + if user.get("skin_url"): + textures["textures"]["SKIN"] = { + "url": user["skin_url"], + "metadata": {"model": user.get("skin_model", "classic")} + } + + if user.get("cloak_url"): + textures["textures"]["CAPE"] = {"url": user["cloak_url"]} + textures_json = json.dumps(textures).encode() base64_textures = base64.b64encode(textures_json).decode() - return { - "id": formatted_uuid, - "name": user["username"], - "properties": [ - { - "name": "textures", - "value": base64_textures - } - ] - } + # Подписываем текстуры + with open("private_key.pem", "rb") as key_file: + private_key = serialization.load_pem_private_key( + key_file.read(), + password=None + ) + + signature = private_key.sign( + textures_json, + padding.PKCS1v15(), + hashes.SHA1() + ) + return JSONResponse({ + "id": user["uuid"].replace("-", ""), # Уберите дефисы + "name": user["username"], + "properties": [{ + "name": "textures", + "value": base64_textures, + "signature": base64.b64encode(signature).decode() + }] + }) + async def join_server(self, request_data: dict): access_token = request_data.get("accessToken") - selected_profile = request_data.get("selectedProfile") + selected_profile = request_data.get("selectedProfile") # UUID без дефисов server_id = request_data.get("serverId") if not all([access_token, selected_profile, server_id]): @@ -134,33 +163,165 @@ class AuthService: if token_uuid != selected_profile: raise HTTPException(status_code=403, detail="Token doesn't match selected profile") + # Сохраняем server_id в сессию + await sessions_collection.update_one( + {"user_uuid": decoded_token["uuid"]}, # UUID с дефисами + {"$set": {"server_id": server_id}}, + upsert=True + ) + return True async def has_joined(self, username: str, server_id: str): user = await users_collection.find_one({"username": username}) if not user: raise HTTPException(status_code=404, detail="User not found") - - response_uuid = user["uuid"].replace("-", "") - + + # Ищем сессию с этим server_id + session = await sessions_collection.find_one({ + "user_uuid": user["uuid"], # UUID с дефисами + "server_id": server_id + }) + if not session: + raise HTTPException(status_code=403, detail="Not joined this server") + textures = {} if user.get("skin_url"): textures["SKIN"] = {"url": user["skin_url"]} if user.get("cloak_url"): textures["CAPE"] = {"url": user["cloak_url"]} - + textures_value = base64.b64encode(json.dumps({ "timestamp": int(datetime.now().timestamp()), - "profileId": response_uuid, + "profileId": user["uuid"].replace("-", ""), # UUID без дефисов "profileName": username, "textures": textures }).encode()).decode() return { - "id": response_uuid, + "id": user["uuid"].replace("-", ""), # UUID без дефисов "name": username, "properties": [{ "name": "textures", "value": textures_value }] if textures else [] } + + async def set_skin(self, username: str, skin_file: UploadFile, skin_model: str = "classic"): + """Установка или замена скина через загрузку файла""" + # Проверяем тип файла + if not skin_file.content_type.startswith('image/'): + raise HTTPException(status_code=400, detail="File must be an image") + + # Проверяем размер файла (максимум 2MB) + max_size = 2 * 1024 * 1024 # 2MB + contents = await skin_file.read() + if len(contents) > max_size: + raise HTTPException(status_code=400, detail="File too large (max 2MB)") + + # Создаем папку для скинов, если ее нет + import os + from pathlib import Path + skin_dir = Path("skins") + skin_dir.mkdir(exist_ok=True) + + # Генерируем имя файла + skin_filename = f"{username}_{int(datetime.now().timestamp())}.png" + skin_path = skin_dir / skin_filename + + # Сохраняем файл + with open(skin_path, "wb") as f: + f.write(contents) + + # Обновляем запись пользователя + result = await users_collection.update_one( + {"username": username}, + {"$set": { + "skin_url": f"{FILES_URL}/skins/{skin_filename}", + "skin_model": skin_model + }} + ) + + if result.modified_count == 0: + raise HTTPException(status_code=404, detail="User not found") + return {"status": "success"} + + async def remove_skin(self, username: str): + """Удаление скина""" + user = await users_collection.find_one({"username": username}) + if not user: + raise HTTPException(status_code=404, detail="User not found") + + # Удаляем файл скина, если он существует + if user.get("skin_url") and user["skin_url"].startswith("/skins/"): + import os + try: + os.remove(f"skins/{user['skin_url'].split('/')[-1]}") + except: + pass + + result = await users_collection.update_one( + {"username": username}, + {"$unset": { + "skin_url": "", + "skin_model": "" + }} + ) + return {"status": "success"} + + async def set_cape(self, username: str, cape_file: UploadFile): + """Установка или замена плаща через загрузку файла (PNG или GIF)""" + # Проверяем тип файла + if not cape_file.content_type.startswith('image/'): + raise HTTPException(status_code=400, detail="File must be an image") + + # Определяем расширение + ext = None + if cape_file.content_type == "image/png": + ext = "png" + elif cape_file.content_type == "image/gif": + ext = "gif" + else: + raise HTTPException(status_code=400, detail="Only PNG and GIF capes are supported") + + # Проверяем размер файла (максимум 2MB) + max_size = 2 * 1024 * 1024 # 2MB + contents = await cape_file.read() + if len(contents) > max_size: + raise HTTPException(status_code=400, detail="File too large (max 2MB)") + + # Создаем папку для плащей, если ее нет + import os + from pathlib import Path + cape_dir = Path("capes") + cape_dir.mkdir(exist_ok=True) + + # Генерируем имя файла с правильным расширением + cape_filename = f"{username}_{int(datetime.now().timestamp())}.{ext}" + cape_path = cape_dir / cape_filename + + # Сохраняем файл + with open(cape_path, "wb") as f: + f.write(contents) + + # Обновляем запись пользователя + result = await users_collection.update_one( + {"username": username}, + {"$set": { + "cloak_url": f"{FILES_URL}/capes/{cape_filename}" + }} + ) + + if result.modified_count == 0: + raise HTTPException(status_code=404, detail="User not found") + return {"status": "success"} + + async def remove_cape(self, username: str): + """Удаление плаща""" + result = await users_collection.update_one( + {"username": username}, + {"$unset": {"cloak_url": ""}} + ) + if result.modified_count == 0: + raise HTTPException(status_code=404, detail="User not found") + return {"status": "success"} diff --git a/auth/app/generate_key.py b/auth/app/generate_key.py new file mode 100644 index 0000000..42ef134 --- /dev/null +++ b/auth/app/generate_key.py @@ -0,0 +1,19 @@ +from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.hazmat.primitives import serialization + +# Генерация ключа +private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) + +# Сохранение в PEM-формат +with open("private_key.pem", "wb") as f: + f.write(private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption() + )) + +with open("public_key.pem", "wb") as f: + f.write(private_key.public_key().public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo + )) \ No newline at end of file diff --git a/auth/app/main.py b/auth/app/main.py index b358aef..c1dc637 100644 --- a/auth/app/main.py +++ b/auth/app/main.py @@ -1,13 +1,15 @@ import base64 from datetime import datetime import json -from fastapi import FastAPI, Depends, HTTPException, Body, Request, Response +from fastapi import FastAPI, Depends, File, Form, HTTPException, Body, Request, Response, UploadFile from fastapi.security import OAuth2PasswordBearer -from .models import UserCreate, UserLogin, ValidateRequest +from fastapi.staticfiles import StaticFiles +from .models import UserCreate, UserLogin, ValidateRequest, SkinUpdate, CapeUpdate from .auth import AuthService from .database import users_collection from .utils import decode_token import os +from pathlib import Path from typing import Union from fastapi.middleware.cors import CORSMiddleware import logging @@ -16,6 +18,14 @@ import logging app = FastAPI() auth_service = AuthService() +skin_dir = Path("skins") +skin_dir.mkdir(exist_ok=True) +app.mount("/skins", StaticFiles(directory="skins"), name="skins") + +cape_dir = Path("capes") +cape_dir.mkdir(exist_ok=True) +app.mount("/capes", StaticFiles(directory="capes"), name="capes") + app.add_middleware( CORSMiddleware, allow_origins=["*"], # Разрешить все домены @@ -32,8 +42,10 @@ def api_root(): "implementationVersion": "1.0.0", "links": { "homepage": "https://your-server.com" - } - } + }, + }, + "skinDomains": ["147.78.65.214"], + "capeDomains": ["147.78.65.214"] } # Эндпоинты Mojang-like API @@ -74,6 +86,39 @@ async def join_server(request_data: dict = Body(...)): async def has_joined(username: str, serverId: str): return await auth_service.has_joined(username, serverId) + +@app.post("/user/{username}/skin") +async def set_skin( + username: str, + skin_file: UploadFile = File(...), + skin_model: str = Form("classic") +): + return await auth_service.set_skin(username, skin_file, skin_model) + +@app.delete("/user/{username}/skin") +async def remove_skin(username: str): + return await auth_service.remove_skin(username) + +@app.post("/user/{username}/cape") +async def set_cape( + username: str, + cape_file: UploadFile = File(...) +): + return await auth_service.set_cape(username, cape_file) + +@app.delete("/user/{username}/cape") +async def remove_cape(username: str): + return await auth_service.remove_cape(username) + +@app.get("/debug/profile/{uuid}") +async def debug_profile(uuid: str): + profile = await auth_service.get_minecraft_profile(uuid) + textures = base64.b64decode(profile['properties'][0]['value']).decode() + return { + "profile": profile, + "textures_decoded": json.loads(textures) + } + if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/auth/app/models.py b/auth/app/models.py index ff1dfec..a5dcdfe 100644 --- a/auth/app/models.py +++ b/auth/app/models.py @@ -19,6 +19,7 @@ class UserInDB(BaseModel): hashed_password: str uuid: str skin_url: Optional[str] = None + skin_model: Optional[str] = "classic" # "classic" или "slim" cloak_url: Optional[str] = None is_active: bool = True created_at: datetime = datetime.utcnow() @@ -32,3 +33,10 @@ class Session(BaseModel): class ValidateRequest(BaseModel): accessToken: str # camelCase clientToken: str + +class SkinUpdate(BaseModel): + skin_model: Optional[str] = "classic" # "classic" или "slim" + # Удаляем skin_url и skin_file, так как будем принимать файл напрямую + +class CapeUpdate(BaseModel): + cape_url: str \ No newline at end of file diff --git a/capes/DIKER_1752698970.png b/capes/DIKER_1752698970.png new file mode 100644 index 0000000..f005e17 Binary files /dev/null and b/capes/DIKER_1752698970.png differ diff --git a/capes/DIKER_1752699250.png b/capes/DIKER_1752699250.png new file mode 100644 index 0000000..83c97d5 Binary files /dev/null and b/capes/DIKER_1752699250.png differ diff --git a/capes/DIKER_1752699326.png b/capes/DIKER_1752699326.png new file mode 100644 index 0000000..9cef367 Binary files /dev/null and b/capes/DIKER_1752699326.png differ diff --git a/capes/DIKER_1752699356.png b/capes/DIKER_1752699356.png new file mode 100644 index 0000000..f005e17 Binary files /dev/null and b/capes/DIKER_1752699356.png differ diff --git a/capes/DIKER_1752699438.gif b/capes/DIKER_1752699438.gif new file mode 100644 index 0000000..f005e17 Binary files /dev/null and b/capes/DIKER_1752699438.gif differ diff --git a/capes/DIKER_1752699669.png b/capes/DIKER_1752699669.png new file mode 100644 index 0000000..83c97d5 Binary files /dev/null and b/capes/DIKER_1752699669.png differ diff --git a/private_key.pem b/private_key.pem new file mode 100644 index 0000000..11304a5 --- /dev/null +++ b/private_key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC1olpnwfijhTT9 +vRR/AzZc5t4DYM3AOMRf0rdBGn5YWzvzk6LQaWLV7PAycUehzvZmSD5Ko5leg4mm +pCVod/bm+U99fofaW+MpQ/T+UEg2q3FAkVTmyTts6lQaDXgkOckLj4SQcKemn/9l +HDL5czDfpUnxBA5ONuTiBiQpxYAQHLk9r2cxIzyDk6veBzAvg6W9eBg1AAs8WJXo +zo9EwM0JustDgsLgK+x+g43U9pHSF9DoWBCoICYeShDl2DMQQFO4DW6v/E4qGgI9 +BGii3ef4glXJ9OwJv0JlE5Cg3FaHgyFm0I+NroDurIED7JiL4k7UXw+Bb22KrL9Z +tgGggzspAgMBAAECggEAOTj/8GZc1e92hWYXWfiCHPyi/z91MtTvkRzKnRkiquV7 +Wr6tcalx+OGfvtSPc7vHRuwFq/AktnEMYdKe8m2w/I2Y7Hl7hWCjjXGacrCKP6b9 +lBD1RYwqS6L7ggWyTv9hhmHdqr/DIayQgqNCr/IJeLwTMnpLo3qJ22eB5yMQuII6 +inhTbL/uCvoU2R2gfMQRjtC2GIXApjWJwz20Ydv6B9X+jdw431n5ZNresyR6Chfx +tVCku+LZ7gpbX/t7B6glYZ/B+CGO8RglQwW67YEYfgxaEHqoYy7yKEaOYRv2BXKA +T6Aj7+0z0P2kV067eVCu8tf4WK/ji44K1uNEVmbxnQKBgQD8nFhFz+AXULNnjJuf +RwE2WuxmXKkL3fCYp/PlN9+WznoNfRLyMxYI5tJe7yqoBj1S7caBTS9JkPTpEIhQ +qsJaDdvEzNm62TxeArUs9KO/IPI5P8P616W4z2YWYFGHrKpJz3KXfOP/DMYcyCG3 +cmzPjKaQhGyi3TXPh1sbgmCbSwKBgQC4Eji3mqCpDLxJlctQs67uQ6gdNEePDDL/ +Kl6BoqQ8RaSSh5PAHdFzRefeKxRSEF63ZRVJBaqZadS/Wx9qu6ZbpMx7NO+6B4/7 +mcf2OBPr3/ioVuaAOAwOPNsCtMPnQDbETAnGBddRj8Bo2+YF75xaIGR0N8qI+Gic +dvx9rGvm2wKBgQD0JHb8IgjG/+wkrDTMH+gADKhl1jBbk8kxAUIry3CBZFV6K+Pf +yZgGSnAP6L8lXcJvH/e2iE6nnz3U83GL5T2po7M/5WyZtdMuWReZt2d7FfCFfCeB +jGJS18Am6DhkFHEQnTp3RvFkU4g10QclMaYQgjOJgTMtxPZ4+K0JTVzpOQKBgCor +Un8Nl5zi5Affn1J/t6WyLkNyhKpK2ywF4tzEC+ga9Fb1ZG3w5tkHvNTy/ZbHVUui +hrvR5oF681hbYdkr4DLCkG3xdLIjpWK4mkzYEAhLqUW3ktrw/CIO4wW9r9u8pE9Y +NCz/jZKL4kKjjhDyEdm77geJ+IZkkmK2B6Yq6BVdAoGAPlSRQFCo3ZKk78wJm19j +IjSacufzhQyG9G6US0Ql0HEqsjo+T2ZOPhge2zkfxs+sr6EQrbEBOoTV3IOtet78 +x6u/IxD1QfJQtIAS8n4s6+HEFV7Gu+zvkz9dIIN6nMKZb1tfZlMmEtdm0Ms9kFma +1eyyqBR6aOhWwYopsHhdCOc= +-----END PRIVATE KEY----- diff --git a/public_key.pem b/public_key.pem new file mode 100644 index 0000000..46ae52d --- /dev/null +++ b/public_key.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtaJaZ8H4o4U0/b0UfwM2 +XObeA2DNwDjEX9K3QRp+WFs785Oi0Gli1ezwMnFHoc72Zkg+SqOZXoOJpqQlaHf2 +5vlPfX6H2lvjKUP0/lBINqtxQJFU5sk7bOpUGg14JDnJC4+EkHCnpp//ZRwy+XMw +36VJ8QQOTjbk4gYkKcWAEBy5Pa9nMSM8g5Or3gcwL4OlvXgYNQALPFiV6M6PRMDN +CbrLQ4LC4CvsfoON1PaR0hfQ6FgQqCAmHkoQ5dgzEEBTuA1ur/xOKhoCPQRoot3n ++IJVyfTsCb9CZROQoNxWh4MhZtCPja6A7qyBA+yYi+JO1F8PgW9tiqy/WbYBoIM7 +KQIDAQAB +-----END PUBLIC KEY----- diff --git a/skins/DIKER_1752693632.png b/skins/DIKER_1752693632.png new file mode 100644 index 0000000..b4ea403 Binary files /dev/null and b/skins/DIKER_1752693632.png differ diff --git a/skins/DIKER_1752698381.png b/skins/DIKER_1752698381.png new file mode 100644 index 0000000..b72171d Binary files /dev/null and b/skins/DIKER_1752698381.png differ