WIP: authentication rework
This commit is contained in:
parent
8f04b34092
commit
a2c2b78f89
6 changed files with 50 additions and 101 deletions
|
@ -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")
|
||||
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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':
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue