better starting, auto reconnect, respawn

This commit is contained in:
əlemi 2021-11-10 22:44:47 +01:00
parent a697ca21dd
commit 33396a0704
3 changed files with 126 additions and 72 deletions

View file

@ -32,9 +32,7 @@ if __name__ == "__main__":
# TODO rework how this is started! Maybe implement client context manager?
loop = asyncio.get_event_loop()
token = loop.run_until_complete(Token.authenticate(username, pwd))
client = Client(token, host, port)
client = Client(host, port, username=username, password=pwd)
@client.on_packet(PacketChat, ConnectionState.PLAY)
async def print_chat(packet: PacketChat):

View file

@ -3,7 +3,7 @@ import logging
from asyncio import Task
from enum import Enum
from typing import Dict, List, Callable, Type
from typing import Dict, List, Callable, Type, Optional, Tuple
from .dispatcher import Dispatcher, ConnectionState
from .mc.mctypes import VarInt
@ -34,7 +34,9 @@ _STATE_REGS = {
class Client:
host:str
port:int
token:Token
username:Optional[str]
password:Optional[str]
token:Optional[Token]
dispatcher : Dispatcher
_processing : bool
@ -44,13 +46,18 @@ class Client:
def __init__(
self,
token:Token,
host:str,
port:int = 25565,
username:Optional[str] = None,
password:Optional[str] = None,
token:Optional[Token] = None,
):
self.host = host
self.port = port
self.token = token
self.username = username
self.password = password
self.dispatcher = Dispatcher(host, port)
self._processing = False
@ -72,42 +79,80 @@ class Client:
return fun
return wrapper
async def start(self):
await self.dispatcher.start()
self._processing = True
async def authenticate(self) -> bool:
if not self.token:
if self.username and self.password:
self.token = await Token.authenticate(self.username, self.password)
logger.info("Authenticated from credentials")
return True
return False
try:
await self.token.validate() # will raise an exc if token is invalid
except Exception: # idk TODO
try:
await self.token.refresh()
logger.info("Refreshed Token")
except Exception:
return False
return True
await self.dispatcher.write(
proto.handshaking.serverbound.PacketSetProtocol(
async def run(self):
await self.start()
try:
while True: # TODO don't busywait even if it doesn't matter much
await asyncio.sleep(5)
except KeyboardInterrupt:
logger.info("Received SIGINT, stopping...")
await self.stop()
async def start(self):
self._processing = True
self._worker = asyncio.get_event_loop().create_task(self._client_worker())
logger.info("Client started")
async def stop(self, block=True):
await self.dispatcher.disconnect()
self._processing = False
if block:
await self._worker
logger.info("Client stopped")
async def _client_worker(self):
while self._processing:
if not await self.authenticate():
raise Exception("Token not refreshable or credentials invalid") # TODO!
try:
await self.dispatcher.connect()
for packet in self._handshake():
await self.dispatcher.write(packet)
self.dispatcher.state = ConnectionState.LOGIN
await self._process_packets()
except Exception:
logger.exception("Connection terminated")
await asyncio.sleep(2)
def _handshake(self, force:bool=False) -> Tuple[Packet, Packet]: # TODO make this fancier! poll for version and status first
return ( proto.handshaking.serverbound.PacketSetProtocol(
340, # TODO!!!!
protocolVersion=340,
serverHost=self.host,
serverPort=self.port,
nextState=2, # play
),
proto.login.serverbound.PacketLoginStart(
340,
username=self.token.profile.name if self.token else self.username
)
)
await self.dispatcher.write( # TODO PROTO!!!!
proto.login.serverbound.PacketLoginStart(340, username=self.token.profile.name)
)
self.dispatcher.state = ConnectionState.LOGIN
self._processing = True
self._worker = asyncio.get_event_loop().create_task(self._client_worker())
async def stop(self, block=True):
await self.dispatcher.stop()
self._processing = False
if block:
await self._worker
async def _client_worker(self):
while self._processing:
async def _process_packets(self):
while self.dispatcher.connected:
try:
# logger.info("Awaiting packet")
packet = await asyncio.wait_for(self.dispatcher.incoming.get(), timeout=5)
# logger.info("Client processing packet %s [state %s]", str(packet), str(self.dispatcher.state))
logger.debug("[ * ] Processing | %s", str(packet))
# Process packets? switch state, invoke callbacks? Maybe implement Reactors?
if self.dispatcher.state == ConnectionState.LOGIN:
await self.login_logic(packet)
elif self.dispatcher.state == ConnectionState.PLAY:
@ -126,9 +171,10 @@ class Client:
except Exception:
logger.exception("Exception while processing packet %s", packet)
# TODO move these in separate module
async def login_logic(self, packet:Packet):
if isinstance(packet, proto.login.clientbound.PacketEncryptionBegin):
logger.info("Encryption request")
secret = encryption.generate_shared_secret()
token, encrypted_secret = encryption.encrypt_token_and_secret(
@ -154,19 +200,19 @@ class Client:
await self.dispatcher.write(encryption_response, wait=True)
await self.dispatcher.encrypt(secret)
self.dispatcher.encrypt(secret)
elif isinstance(packet, proto.login.clientbound.PacketDisconnect):
logger.error("Disconnected while logging in")
await self.stop(False)
await self.dispatcher.disconnect(block=False)
# raise Exception("Disconnected while logging in") # TODO make a more specific one, do some shit
elif isinstance(packet, proto.login.clientbound.PacketCompress):
logger.info("Set compression")
logger.info("Compression enabled")
self.dispatcher.compression = packet.threshold
elif isinstance(packet, proto.login.clientbound.PacketSuccess):
logger.info("Login success")
logger.info("Login success, joining world...")
self.dispatcher.state = ConnectionState.PLAY
elif isinstance(packet, proto.login.clientbound.PacketLoginPluginRequest):
@ -174,33 +220,30 @@ class Client:
async def play_logic(self, packet:Packet):
if isinstance(packet, proto.play.clientbound.PacketSetCompression):
logger.info("Set compression")
logger.info("Compression updated")
self.dispatcher.compression = packet.threshold
elif isinstance(packet, proto.play.clientbound.PacketKeepAlive):
logger.info("Keep Alive")
keep_alive_packet = proto.play.serverbound.packet_keep_alive.PacketKeepAlive(340, keepAliveId=packet.keepAliveId)
await self.dispatcher.write(keep_alive_packet)
elif isinstance(packet, proto.play.clientbound.PacketPosition):
logger.info("PlayerPosLook")
# if self.connection.context.protocol_later_eq(107):
# teleport_confirm = serverbound.play.TeleportConfirmPacket()
# teleport_confirm.teleport_id = packet.teleport_id
# self.connection.write_packet(teleport_confirm)
# else:
# position_response = serverbound.play.PositionAndLookPacket()
# position_response.x = packet.x
# position_response.feet_y = packet.y
# position_response.z = packet.z
# position_response.yaw = packet.yaw
# position_response.pitch = packet.pitch
# position_response.on_ground = True
# self.connection.write_packet(position_response)
# self.connection.spawned = True
pass
logger.info("Position synchronized")
await self.dispatcher.write(
proto.play.serverbound.PacketTeleportConfirm(
340,
teleportId=packet.teleportId
)
)
elif isinstance(packet, proto.play.clientbound.PacketUpdateHealth):
if packet.health <= 0:
logger.info("Dead, respawning...")
await self.dispatcher.write(
proto.play.serverbound.PacketClientCommand(self.dispatcher.proto, actionId=0) # respawn
)
elif isinstance(packet, proto.play.clientbound.PacketKickDisconnect):
logger.info("Play Disconnect")
raise Exception("Disconnected while playing") # TODO make a more specific one, do some shit
logger.error("Disconnected")
await self.dispatcher.disconnect(block=False)

View file

@ -53,7 +53,6 @@ class Dispatcher:
port : int
proto : int
connected : bool
state : ConnectionState
encryption : bool
compression : Optional[int]
@ -63,13 +62,17 @@ class Dispatcher:
self.port = port
self.proto = 340
self.connected = False
self._dispatching = False
self.compression = None
self.encryption = False
self.incoming = Queue()
self.outgoing = Queue()
self._reader = None
self._writer = None
@property
def connected(self) -> bool:
return self._dispatching
async def write(self, packet:Packet, wait:bool=False) -> int:
await self.outgoing.put(packet)
@ -77,36 +80,44 @@ class Dispatcher:
await packet.sent.wait()
return self.outgoing.qsize()
async def start(self):
if self.connected:
raise InvalidState("Dispatcher already connected")
await self.connect()
async def stop(self, block:bool=True):
async def disconnect(self, block:bool=True):
self._dispatching = False
if block:
if block and self._writer and self._reader:
await asyncio.gather(self._writer, self._reader)
if self._up.can_write_eof():
self._up.write_eof()
self._up.close()
if block:
await self._up.wait_closed()
logger.info("Disconnected")
async def connect(self):
if self.connected:
raise InvalidState("Dispatcher already connected")
self.encryption = False
self.compression = None
self.state = ConnectionState.HANDSHAKING
# self.proto = 340 # TODO
self.incoming = Queue()
self.outgoing = Queue()
self._down, self._up = await asyncio.open_connection(
host=self.host,
port=self.port,
)
self.encryption = False
self.compression = None
self.connected = True
self.state = ConnectionState.HANDSHAKING
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())
logger.info("Connected")
async def encrypt(self, secret:bytes):
def encrypt(self, secret:bytes):
cipher = encryption.create_AES_cipher(secret)
self._encryptor = cipher.encryptor()
self._decryptor = cipher.decryptor()
self.encryption = True
logger.info("Encryption enabled")
async def _read_varint(self) -> int:
numRead = 0
@ -154,6 +165,7 @@ class Dispatcher:
# logger.info("Parsing packet '%d' [%s] | %s", packet_id, str(self.state), buffer.getvalue())
cls = _STATE_REGS[self.state][self.proto][packet_id]
packet = cls.deserialize(self.proto, buffer)
logger.debug("[<--] Received | %s", str(packet))
await self.incoming.put(packet)
except AttributeError:
logger.debug("Received unimplemented packet %d", packet_id)
@ -193,8 +205,9 @@ class Dispatcher:
await self._up.drain()
packet.sent.set() # Notify
logger.debug("[-->] Sent | %s", str(packet))
except asyncio.TimeoutError:
pass # need this to recheck self._dispatching periodically
except Exception:
logger.exception("Error while sending packet %s", str(packet))
logger.exception("Exception dispatching packet %s", str(packet))