WIP: authentication rework

This commit is contained in:
Francesco Tolomei 2022-04-07 00:05:31 +02:00
parent 8f04b34092
commit a2c2b78f89
No known key found for this signature in database
GPG key ID: 413C3451D6179D06
6 changed files with 50 additions and 101 deletions

View file

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

View file

@ -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,7 +220,6 @@ 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)
elif isinstance(packet, PacketKickDisconnect):

View file

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

View file

@ -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':

View file

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

View file

@ -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"],
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