From a2c2b78f89312aed7f057dc35e788149b935f2c7 Mon Sep 17 00:00:00 2001 From: Francesco Tolomei <80046572+f-tlm@users.noreply.github.com> Date: Thu, 7 Apr 2022 00:05:31 +0200 Subject: [PATCH] WIP: authentication rework --- src/aiocraft/__main__.py | 44 ------------------------ src/aiocraft/client.py | 31 ++++------------- src/aiocraft/dispatcher.py | 6 ++-- src/aiocraft/mc/auth/interface.py | 2 +- src/aiocraft/mc/auth/microsoft.py | 11 ++++-- src/aiocraft/mc/auth/mojang.py | 57 +++++++++++++++++-------------- 6 files changed, 50 insertions(+), 101 deletions(-) delete mode 100644 src/aiocraft/__main__.py diff --git a/src/aiocraft/__main__.py b/src/aiocraft/__main__.py deleted file mode 100644 index e295c58..0000000 --- a/src/aiocraft/__main__.py +++ /dev/null @@ -1,44 +0,0 @@ -import sys -import json -import logging - -from .mc.proto.play.clientbound import PacketChat -from .client import MinecraftClient -from .server import MinecraftServer -from .util.helpers import parse_chat - -if __name__ == "__main__": - logging.basicConfig(level=logging.DEBUG) - - if sys.argv[1] == "--server": - host = sys.argv[2] if len(sys.argv) > 2 else "localhost" - port = int(sys.argv[3]) if len(sys.argv) > 3 else 25565 - - serv = MinecraftServer(host, port=port) - - serv.run() # will block and start asyncio event loop - else: - username = sys.argv[1] - pwd = sys.argv[2] - server = sys.argv[3] - color = not (len(sys.argv) > 4 and sys.argv[4] == "--no-color" ) - - if ":" in server: - _host, _port = server.split(":") - host = _host.strip() - port = int(_port) - else: - host = server.strip() - port = 25565 - - client = MinecraftClient(host, port, username=username, password=pwd) - - @client.on_packet(PacketChat) - async def print_chat(packet: PacketChat): - msg = parse_chat(packet.message, ansi_color=color) - print(f"[{packet.position}] {msg}") - - client.run() # will block and start asyncio event loop - - logging.info("Terminated") - diff --git a/src/aiocraft/client.py b/src/aiocraft/client.py index b436090..acddf52 100644 --- a/src/aiocraft/client.py +++ b/src/aiocraft/client.py @@ -27,18 +27,9 @@ from .util import encryption, helpers LOGGER = logging.getLogger(__name__) -@dataclass -class ClientOptions: - reconnect : bool = True - reconnect_delay : float = 10.0 - keep_alive : bool = True - poll_interval : float = 1.0 - use_packet_whitelist : bool = True - class MinecraftClient: host:str port:int - options:ClientOptions username:str online_mode:bool authenticator:Optional[AuthInterface] @@ -54,7 +45,6 @@ class MinecraftClient: online_mode:bool = True, authenticator:AuthInterface=None, username:str = "", - **kwargs ): super().__init__() if ":" in server: @@ -65,8 +55,6 @@ class MinecraftClient: self.host = server.strip() self.port = 25565 - self.options = ClientOptions(**kwargs) - self.username = username self.online_mode = online_mode self.authenticator = authenticator @@ -90,15 +78,12 @@ class MinecraftClient: try: await self.authenticator.validate() # will raise an exc if token is invalid except AuthException: - if self.code: - await self.authenticator.login(self.code) - self.code = None - self.logger.info("Logged in with OAuth code") - elif self.authenticator.refreshable: + if self.authenticator.refreshable: await self._authenticator.refresh() self.logger.warning("Refreshed Token") else: - raise ValueError("No refreshable auth or code to login") + await self.authenticator.login() + self.logger.info("Logged in") self._authenticated = True async def info(self, host:str="", port:int=0, proto:int=0, ping:bool=False) -> Dict[str, Any]: @@ -122,7 +107,6 @@ class MinecraftClient: host=self.host, port=self.port, proto=proto, - queue_timeout=self.options.poll_interval, packet_whitelist=packet_whitelist ) await self._handshake(ConnectionState.LOGIN) @@ -176,7 +160,7 @@ class MinecraftClient: await self.dispatcher.write( PacketLoginStart( self.dispatcher.proto, - username=self.authenticator.selectedProfile.name if self.online_mode else self._username + username=self.authenticator.selectedProfile.name if self.online_mode and self.authenticator else self.username ) ) async for packet in self.dispatcher.packets(): @@ -195,7 +179,7 @@ class MinecraftClient: ) if packet.serverId != '-': try: - await self._authenticator.join( + await self.authenticator.join( encryption.generate_verification_hash( packet.serverId, secret, @@ -236,9 +220,8 @@ class MinecraftClient: self.logger.info("Compression updated") self.dispatcher.compression = packet.threshold elif isinstance(packet, PacketKeepAlive): - if self.options.keep_alive: - keep_alive_packet = PacketKeepAliveResponse(340, keepAliveId=packet.keepAliveId) - await self.dispatcher.write(keep_alive_packet) + keep_alive_packet = PacketKeepAliveResponse(340, keepAliveId=packet.keepAliveId) + await self.dispatcher.write(keep_alive_packet) elif isinstance(packet, PacketKickDisconnect): self.logger.error("Kicked while in game : %s", helpers.parse_chat(packet.reason)) break diff --git a/src/aiocraft/dispatcher.py b/src/aiocraft/dispatcher.py index b864296..1700793 100644 --- a/src/aiocraft/dispatcher.py +++ b/src/aiocraft/dispatcher.py @@ -95,7 +95,6 @@ class Dispatcher: host:Optional[str] = None, port:Optional[int] = None, proto:Optional[int] = None, - queue_timeout:float = 1, queue_size:int = 100, packet_whitelist : Set[Type[Packet]] = None ): @@ -128,14 +127,13 @@ class Dispatcher: proto : Optional[int] = None, reader : Optional[StreamReader] = None, writer : Optional[StreamWriter] = None, - queue_timeout : float = 1, queue_size : int = 100, packet_whitelist : Set[Type[Packet]] = None, ): if self.connected: raise InvalidState("Dispatcher already connected") - self._prepare(host, port, proto, queue_timeout, queue_size, packet_whitelist) + self._prepare(host, port, proto, queue_size, packet_whitelist) if reader and writer: self._down, self._up = reader, writer @@ -147,7 +145,7 @@ class Dispatcher: self._dispatching = True self._reader = asyncio.get_event_loop().create_task(self._down_worker()) - self._writer = asyncio.get_event_loop().create_task(self._up_worker(timeout=queue_timeout)) + self._writer = asyncio.get_event_loop().create_task(self._up_worker()) self._logger.info("Connected") async def disconnect(self, block:bool=True): diff --git a/src/aiocraft/mc/auth/interface.py b/src/aiocraft/mc/auth/interface.py index f1c2cff..b68a364 100644 --- a/src/aiocraft/mc/auth/interface.py +++ b/src/aiocraft/mc/auth/interface.py @@ -28,7 +28,7 @@ class AuthInterface: SESSION_SERVER = "https://sessionserver.mojang.com/session/minecraft" TIMEOUT = aiohttp.ClientTimeout(total=3) - async def login(self, *args) -> 'AuthInterface': + async def login(self) -> 'AuthInterface': raise NotImplementedError async def refresh(self) -> 'AuthInterface': diff --git a/src/aiocraft/mc/auth/microsoft.py b/src/aiocraft/mc/auth/microsoft.py index 2f23d78..97b08f8 100644 --- a/src/aiocraft/mc/auth/microsoft.py +++ b/src/aiocraft/mc/auth/microsoft.py @@ -18,6 +18,7 @@ class MicrosoftAuthenticator(AuthInterface): client_id : str client_secret : str redirect_uri : str + code : Optional[str] accessToken : str selectedProfile : GameProfile @@ -32,11 +33,13 @@ class MicrosoftAuthenticator(AuthInterface): def __init__(self, client_id:str, client_secret:str, - redirect_uri:str="http://localhost" + redirect_uri:str="http://localhost", + code:Optional[str]=None ): self.client_id = client_id self.client_secret = client_secret self.redirect_uri = redirect_uri + self.code = code self.refreshToken = None self.accessToken = '' self.selectedProfile = GameProfile(id='', name='') @@ -68,8 +71,8 @@ class MicrosoftAuthenticator(AuthInterface): f"&state={state}" ) - async def login(self, code:str): # TODO nicer way to get code? - self.accessToken = await self.authenticate(code) + async def login(self): # TODO nicer way to get code? + self.accessToken = await self.authenticate(self.code) prof = await self.fetch_profile() self.selectedProfile = GameProfile(id=prof['id'], name=prof['name']) logging.info("Successfully logged into Microsoft account") @@ -83,6 +86,8 @@ class MicrosoftAuthenticator(AuthInterface): logging.info("Successfully refreshed Microsoft token") async def validate(self): + if not self.accessToken: + raise AuthException("No access token") prof = await self.fetch_profile() self.selectedProfile = GameProfile(id=prof['id'], name=prof['name']) logging.info("Session validated : %s", repr(self.selectedProfile)) diff --git a/src/aiocraft/mc/auth/mojang.py b/src/aiocraft/mc/auth/mojang.py index b1d3317..c80865a 100644 --- a/src/aiocraft/mc/auth/mojang.py +++ b/src/aiocraft/mc/auth/mojang.py @@ -4,16 +4,17 @@ import uuid import logging from dataclasses import dataclass -from typing import Optional +from typing import Optional, Dict, Any import aiohttp -from .interface import AuthInterface +from .interface import AuthInterface, AuthException from ..definitions import GameProfile @dataclass class MojangAuthenticator(AuthInterface): username : str + password : Optional[str] accessToken : str clientToken : str selectedProfile : GameProfile @@ -24,8 +25,9 @@ class MojangAuthenticator(AuthInterface): CONTENT_TYPE = "application/json" HEADERS = {"content-type": CONTENT_TYPE} - def __init__(self): - pass + def __init__(self, username:str="", password:Optional[str]=None): + self.username = username + self.password = password def __equals__(self, other) -> bool: if not isinstance(other, self.__class__): @@ -38,12 +40,12 @@ class MojangAuthenticator(AuthInterface): return False def __repr__(self) -> str: - return json.dumps(self.as_dict()) + return json.dumps(self.serialize()) def __str__(self) -> str: return repr(self) - def serialize(self): + def serialize(self) -> Dict[str, Any]: return { "username":self.username, "accessToken":self.accessToken, @@ -51,30 +53,29 @@ class MojangAuthenticator(AuthInterface): "selectedProfile": self.selectedProfile.as_dict(), } - def deserialize(self, data:dict): - self.username=data["username"] if "username" in data else data["selectedProfile"]["name"], - self.accessToken=data["accessToken"], - self.clientToken=data["clientToken"], - self.selectedProfile=GameProfile(**data["selectedProfile"]) + def deserialize(self, data:Dict[str, Any]) -> AuthInterface: + self.username = data["username"] if "username" in data else data["selectedProfile"]["name"] + self.accessToken = data["accessToken"] + self.clientToken = data["clientToken"] + self.selectedProfile = GameProfile(**data["selectedProfile"]) + return self - async def login(self, username:str, password:str, invalidate=False) -> AuthInterface: + async def login(self) -> AuthInterface: payload = { "agent": { "name": self.AGENT_NAME, "version": self.AGENT_VERSION }, - "username": username, - "password": password + "username": self.username, + "password": self.password } - if not invalidate: - payload["clientToken"] = uuid.uuid4().hex + payload["clientToken"] = uuid.uuid4().hex # don't include this to invalidate all other sessions - res = await self._post(self.AUTH_SERVER + "/authenticate", payload) + res = await self._post(self.AUTH_SERVER + "/authenticate", json=payload) - self.username=username, - self.accessToken=res["accessToken"], - self.clientToken=res["clientToken"], + self.accessToken=res["accessToken"] + self.clientToken=res["clientToken"] self.selectedProfile=GameProfile(**res["selectedProfile"]) return self @@ -90,7 +91,7 @@ class MojangAuthenticator(AuthInterface): } ) - async def refresh(self, requestUser:bool = False) -> dict: + async def refresh(self, requestUser:bool = False) -> AuthInterface: res = await self._post( self.AUTH_SERVER + "/refresh", headers=self.HEADERS, @@ -107,18 +108,23 @@ class MojangAuthenticator(AuthInterface): if "user" in res: self.username = res["user"]["username"] - async def validate(self, clientToken:bool = True) -> dict: + return self + + async def validate(self, clientToken:bool = True) -> AuthInterface: + if not self.accessToken: + raise AuthException("/validate", 404, {"message":"No access token"}, {}) payload = { "accessToken": self.accessToken } if clientToken: payload["clientToken"] = self.clientToken - return await self._post( + await self._post( self.AUTH_SERVER + "/validate", headers=self.HEADERS, json=payload, ) + return self - async def invalidate(self) -> dict: - return await self._post( + async def invalidate(self) -> AuthInterface: + await self._post( self.AUTH_SERVER + "/invalidate", headers=self.HEADERS, json= { @@ -126,3 +132,4 @@ class MojangAuthenticator(AuthInterface): "clientToken": self.clientToken } ) + return self