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? # TODO rework how this is started! Maybe implement client context manager?
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
token = loop.run_until_complete(Token.authenticate(username, pwd)) client = Client(host, port, username=username, password=pwd)
client = Client(token, host, port)
@client.on_packet(PacketChat, ConnectionState.PLAY) @client.on_packet(PacketChat, ConnectionState.PLAY)
async def print_chat(packet: PacketChat): async def print_chat(packet: PacketChat):

View file

@ -3,7 +3,7 @@ import logging
from asyncio import Task from asyncio import Task
from enum import Enum 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 .dispatcher import Dispatcher, ConnectionState
from .mc.mctypes import VarInt from .mc.mctypes import VarInt
@ -34,7 +34,9 @@ _STATE_REGS = {
class Client: class Client:
host:str host:str
port:int port:int
token:Token username:Optional[str]
password:Optional[str]
token:Optional[Token]
dispatcher : Dispatcher dispatcher : Dispatcher
_processing : bool _processing : bool
@ -44,13 +46,18 @@ class Client:
def __init__( def __init__(
self, self,
token:Token,
host:str, host:str,
port:int = 25565, port:int = 25565,
username:Optional[str] = None,
password:Optional[str] = None,
token:Optional[Token] = None,
): ):
self.host = host self.host = host
self.port = port self.port = port
self.token = token self.token = token
self.username = username
self.password = password
self.dispatcher = Dispatcher(host, port) self.dispatcher = Dispatcher(host, port)
self._processing = False self._processing = False
@ -72,42 +79,80 @@ class Client:
return fun return fun
return wrapper return wrapper
async def start(self): async def authenticate(self) -> bool:
await self.dispatcher.start() if not self.token:
self._processing = True 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( async def run(self):
proto.handshaking.serverbound.PacketSetProtocol( 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!!!! 340, # TODO!!!!
protocolVersion=340, protocolVersion=340,
serverHost=self.host, serverHost=self.host,
serverPort=self.port, serverPort=self.port,
nextState=2, # play 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!!!! async def _process_packets(self):
proto.login.serverbound.PacketLoginStart(340, username=self.token.profile.name) while self.dispatcher.connected:
)
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:
try: try:
# logger.info("Awaiting packet")
packet = await asyncio.wait_for(self.dispatcher.incoming.get(), timeout=5) 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: if self.dispatcher.state == ConnectionState.LOGIN:
await self.login_logic(packet) await self.login_logic(packet)
elif self.dispatcher.state == ConnectionState.PLAY: elif self.dispatcher.state == ConnectionState.PLAY:
@ -126,9 +171,10 @@ class Client:
except Exception: except Exception:
logger.exception("Exception while processing packet %s", packet) logger.exception("Exception while processing packet %s", packet)
# TODO move these in separate module
async def login_logic(self, packet:Packet): async def login_logic(self, packet:Packet):
if isinstance(packet, proto.login.clientbound.PacketEncryptionBegin): if isinstance(packet, proto.login.clientbound.PacketEncryptionBegin):
logger.info("Encryption request")
secret = encryption.generate_shared_secret() secret = encryption.generate_shared_secret()
token, encrypted_secret = encryption.encrypt_token_and_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.write(encryption_response, wait=True)
await self.dispatcher.encrypt(secret) self.dispatcher.encrypt(secret)
elif isinstance(packet, proto.login.clientbound.PacketDisconnect): elif isinstance(packet, proto.login.clientbound.PacketDisconnect):
logger.error("Disconnected while logging in") 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 # raise Exception("Disconnected while logging in") # TODO make a more specific one, do some shit
elif isinstance(packet, proto.login.clientbound.PacketCompress): elif isinstance(packet, proto.login.clientbound.PacketCompress):
logger.info("Set compression") logger.info("Compression enabled")
self.dispatcher.compression = packet.threshold self.dispatcher.compression = packet.threshold
elif isinstance(packet, proto.login.clientbound.PacketSuccess): elif isinstance(packet, proto.login.clientbound.PacketSuccess):
logger.info("Login success") logger.info("Login success, joining world...")
self.dispatcher.state = ConnectionState.PLAY self.dispatcher.state = ConnectionState.PLAY
elif isinstance(packet, proto.login.clientbound.PacketLoginPluginRequest): elif isinstance(packet, proto.login.clientbound.PacketLoginPluginRequest):
@ -174,33 +220,30 @@ class Client:
async def play_logic(self, packet:Packet): async def play_logic(self, packet:Packet):
if isinstance(packet, proto.play.clientbound.PacketSetCompression): if isinstance(packet, proto.play.clientbound.PacketSetCompression):
logger.info("Set compression") logger.info("Compression updated")
self.dispatcher.compression = packet.threshold self.dispatcher.compression = packet.threshold
elif isinstance(packet, proto.play.clientbound.PacketKeepAlive): 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) keep_alive_packet = proto.play.serverbound.packet_keep_alive.PacketKeepAlive(340, keepAliveId=packet.keepAliveId)
await self.dispatcher.write(keep_alive_packet) await self.dispatcher.write(keep_alive_packet)
elif isinstance(packet, proto.play.clientbound.PacketPosition): elif isinstance(packet, proto.play.clientbound.PacketPosition):
logger.info("PlayerPosLook") logger.info("Position synchronized")
# if self.connection.context.protocol_later_eq(107): await self.dispatcher.write(
# teleport_confirm = serverbound.play.TeleportConfirmPacket() proto.play.serverbound.PacketTeleportConfirm(
# teleport_confirm.teleport_id = packet.teleport_id 340,
# self.connection.write_packet(teleport_confirm) teleportId=packet.teleportId
# else: )
# position_response = serverbound.play.PositionAndLookPacket() )
# position_response.x = packet.x
# position_response.feet_y = packet.y elif isinstance(packet, proto.play.clientbound.PacketUpdateHealth):
# position_response.z = packet.z if packet.health <= 0:
# position_response.yaw = packet.yaw logger.info("Dead, respawning...")
# position_response.pitch = packet.pitch await self.dispatcher.write(
# position_response.on_ground = True proto.play.serverbound.PacketClientCommand(self.dispatcher.proto, actionId=0) # respawn
# self.connection.write_packet(position_response) )
# self.connection.spawned = True
pass
elif isinstance(packet, proto.play.clientbound.PacketKickDisconnect): elif isinstance(packet, proto.play.clientbound.PacketKickDisconnect):
logger.info("Play Disconnect") logger.error("Disconnected")
raise Exception("Disconnected while playing") # TODO make a more specific one, do some shit await self.dispatcher.disconnect(block=False)

View file

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