better starting, auto reconnect, respawn
This commit is contained in:
parent
a697ca21dd
commit
33396a0704
3 changed files with 126 additions and 72 deletions
|
@ -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):
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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))
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue