From 1a4d3c023935317cfe8d82469743456c8e729988 Mon Sep 17 00:00:00 2001 From: Sh-wayz Date: Mon, 21 Feb 2022 08:11:25 -0500 Subject: [PATCH 01/34] fix explosion.py --- pymine_net/packets/757/play/explosion.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/pymine_net/packets/757/play/explosion.py b/pymine_net/packets/757/play/explosion.py index 9d265f7..5c5ac4c 100644 --- a/pymine_net/packets/757/play/explosion.py +++ b/pymine_net/packets/757/play/explosion.py @@ -36,14 +36,11 @@ def __init__( self.pmz = pmz def pack(self) -> Buffer: - return ( - Buffer.write("f", self.x) - + Buffer.write("f", self.y) - + Buffer.write("f", self.z) - + Buffer.write("f", self.strength) - + Buffer.write("i", self.record_count) - + b"".join([Buffer.write("b", r) for r in self.records]) - + Buffer.write("f", self.pmx) - + Buffer.write("f", self.pmy) - + Buffer.write("f", self.pmz) - ) + + buf = Buffer().write("f", self.x).write("f", self.y).write("f", self.z).write("f", self.strength).write("i", self.record_count) + + for r in self.records: + buf.write("b", r) + buf.write("f", self.pmx).write("f", self.pmy).write("f", self.pmz) + return buf + From 76640708a3db85e6d2023d9f3ddf822aedcd58d9 Mon Sep 17 00:00:00 2001 From: Sh-wayz Date: Mon, 21 Feb 2022 08:14:53 -0500 Subject: [PATCH 02/34] formatting --- pymine_net/packets/757/play/explosion.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/pymine_net/packets/757/play/explosion.py b/pymine_net/packets/757/play/explosion.py index 5c5ac4c..634c1b5 100644 --- a/pymine_net/packets/757/play/explosion.py +++ b/pymine_net/packets/757/play/explosion.py @@ -37,10 +37,17 @@ def __init__( def pack(self) -> Buffer: - buf = Buffer().write("f", self.x).write("f", self.y).write("f", self.z).write("f", self.strength).write("i", self.record_count) + buf = ( + Buffer() + .write("f", self.x) + .write("f", self.y) + .write("f", self.z) + .write("f", self.strength) + .write("i", self.record_count) + ) for r in self.records: buf.write("b", r) + buf.write("f", self.pmx).write("f", self.pmy).write("f", self.pmz) return buf - From fe643dbf43baff86cd14e46db2bdeceec79d7f3e Mon Sep 17 00:00:00 2001 From: Sh-wayz Date: Mon, 21 Feb 2022 08:27:54 -0500 Subject: [PATCH 03/34] 0x21 --- pymine_net/packets/757/play/keep_alive.py | 52 +++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 pymine_net/packets/757/play/keep_alive.py diff --git a/pymine_net/packets/757/play/keep_alive.py b/pymine_net/packets/757/play/keep_alive.py new file mode 100644 index 0000000..712c255 --- /dev/null +++ b/pymine_net/packets/757/play/keep_alive.py @@ -0,0 +1,52 @@ +"""Contains packets for maintaining the connection between client and server.""" + +from __future__ import annotations + +from pymine_net.types.packet import ServerBoundPacket, ClientBoundPacket +from pymine_net.types.buffer import Buffer + +__all__ = ( + "PlayKeepAliveClientBound", + "PlayKeepAliveServerBound", +) + + +class PlayKeepAliveClientBound(ClientBoundPacket): + """Sent by the server in order to maintain connection with the client. (Server -> Client) + + :param int keep_alive_id: A randomly generated (by the server) integer/long. + :ivar int id: Unique packet ID. + :ivar keep_alive_id: + """ + + id = 0x21 + + def __init__(self, keep_alive_id: int) -> None: + super().__init__() + + self.keep_alive_id = keep_alive_id + + def pack(self) -> Buffer: + return Buffer().write("q", self.keep_alive_id) + + +class PlayKeepAliveServerBound(Packet): + """Sent by client in order to maintain connection with server. (Client -> Server) + + :param int keep_alive_id: A randomly generated (by the server) integer/long. + :ivar int id: Unique packet ID. + :ivar int to: Packet direction. + :ivar keep_alive_id: + """ + + id = 0x10 + to = 0 + + def __init__(self, keep_alive_id: int) -> None: + super().__init__() + + self.keep_alive_id = keep_alive_id + + @classmethod + def decode(cls, buf: Buffer) -> PlayKeepAliveServerBound: + return cls(buf.unpack("q")) From bb27d67a9dbd6cdb47a67d23af6a0e6e9d614cb3 Mon Sep 17 00:00:00 2001 From: Sh-wayz Date: Mon, 21 Feb 2022 08:35:47 -0500 Subject: [PATCH 04/34] 0x0F --- pymine_net/packets/757/play/keep_alive.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/pymine_net/packets/757/play/keep_alive.py b/pymine_net/packets/757/play/keep_alive.py index 712c255..56f7b53 100644 --- a/pymine_net/packets/757/play/keep_alive.py +++ b/pymine_net/packets/757/play/keep_alive.py @@ -30,17 +30,15 @@ def pack(self) -> Buffer: return Buffer().write("q", self.keep_alive_id) -class PlayKeepAliveServerBound(Packet): +class PlayKeepAliveServerBound(ServerBoundPacket): """Sent by client in order to maintain connection with server. (Client -> Server) :param int keep_alive_id: A randomly generated (by the server) integer/long. :ivar int id: Unique packet ID. - :ivar int to: Packet direction. :ivar keep_alive_id: """ - id = 0x10 - to = 0 + id = 0x0F def __init__(self, keep_alive_id: int) -> None: super().__init__() @@ -48,5 +46,5 @@ def __init__(self, keep_alive_id: int) -> None: self.keep_alive_id = keep_alive_id @classmethod - def decode(cls, buf: Buffer) -> PlayKeepAliveServerBound: - return cls(buf.unpack("q")) + def unpack(cls, buf: Buffer) -> PlayKeepAliveServerBound: + return cls(buf.read("q")) From a19890c2117ffc7de57ef12580fb5921d8e35845 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 21 Feb 2022 13:36:53 +0000 Subject: [PATCH 05/34] apply import sorting --- pymine_net/packets/757/play/keep_alive.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pymine_net/packets/757/play/keep_alive.py b/pymine_net/packets/757/play/keep_alive.py index 56f7b53..af51c50 100644 --- a/pymine_net/packets/757/play/keep_alive.py +++ b/pymine_net/packets/757/play/keep_alive.py @@ -2,8 +2,8 @@ from __future__ import annotations -from pymine_net.types.packet import ServerBoundPacket, ClientBoundPacket from pymine_net.types.buffer import Buffer +from pymine_net.types.packet import ClientBoundPacket, ServerBoundPacket __all__ = ( "PlayKeepAliveClientBound", From 4499f8cf72056d4ee584f0d0db5a0b7f347c7330 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 21 Feb 2022 14:07:06 +0000 Subject: [PATCH 06/34] apply black codestyle --- pymine_net/errors.py | 20 +++++++++++++++++--- pymine_net/types/packet_map.py | 15 ++++++++++----- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/pymine_net/errors.py b/pymine_net/errors.py index 3fb0c6a..12894e5 100644 --- a/pymine_net/errors.py +++ b/pymine_net/errors.py @@ -8,8 +8,16 @@ class PyMineNetError(Exception): class UnknownPacketIdError(Exception): - def __init__(self, protocol: Union[str, int], state: GameState, packet_id: int, direction: PacketDirection): - super().__init__(f"Unknown packet ID 0x{packet_id:02X} (protocol={protocol}, state={state.name}, {direction.value})") + def __init__( + self, + protocol: Union[str, int], + state: GameState, + packet_id: int, + direction: PacketDirection, + ): + super().__init__( + f"Unknown packet ID 0x{packet_id:02X} (protocol={protocol}, state={state.name}, {direction.value})" + ) self.protocol = protocol self.state = state @@ -18,7 +26,13 @@ def __init__(self, protocol: Union[str, int], state: GameState, packet_id: int, class DuplicatePacketIdError(Exception): - def __init__(self, protocol: Union[str, int], state: GameState, packet_id: int, direction: PacketDirection): + def __init__( + self, + protocol: Union[str, int], + state: GameState, + packet_id: int, + direction: PacketDirection, + ): super().__init__( f"Duplicate packet ID found (protocol={protocol}, state={state.name}, {direction}): 0x{packet_id:02X}" ) diff --git a/pymine_net/types/packet_map.py b/pymine_net/types/packet_map.py index c4ebfca..5f189fd 100644 --- a/pymine_net/types/packet_map.py +++ b/pymine_net/types/packet_map.py @@ -39,7 +39,9 @@ def from_list( ] if len(found) > 1: - raise DuplicatePacketIdError("unknown", state, packet_id, PacketDirection.SERVERBOUND) + raise DuplicatePacketIdError( + "unknown", state, packet_id, PacketDirection.SERVERBOUND + ) for packet_id in self.client_bound.keys(): found = [ @@ -47,7 +49,9 @@ def from_list( ] if len(found) > 1: - raise DuplicatePacketIdError("unknown", state, packet_id, PacketDirection.CLIENTBOUND) + raise DuplicatePacketIdError( + "unknown", state, packet_id, PacketDirection.CLIENTBOUND + ) return self @@ -72,7 +76,9 @@ def encode_packet(self, packet: ClientBoundPacket, compression_threshold: int = return Buffer().write_varint(len(buf)).extend(buf) - def decode_packet(self, buf: Buffer, state: GameState, compression_threshold: int = -1) -> ServerBoundPacket: + def decode_packet( + self, buf: Buffer, state: GameState, compression_threshold: int = -1 + ) -> ServerBoundPacket: """Decodes and (if necessary) decompresses a ServerBoundPacket.""" # decompress packet if necessary @@ -89,6 +95,5 @@ def decode_packet(self, buf: Buffer, state: GameState, compression_threshold: in packet_class = self.packets[state].server_bound[packet_id] except KeyError: raise UnknownPacketIdError(self.protocol, state, packet_id, PacketDirection.SERVERBOUND) - + return packet_class.unpack(buf) - From ccd59980800164ae8b8a55db4b54ef6e6780080e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 21 Feb 2022 15:56:10 +0000 Subject: [PATCH 07/34] apply black codestyle --- pymine_net/strict_abc.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pymine_net/strict_abc.py b/pymine_net/strict_abc.py index 6472803..522035b 100644 --- a/pymine_net/strict_abc.py +++ b/pymine_net/strict_abc.py @@ -23,10 +23,14 @@ def abstract(obj): obj.__abstract__ = AbstractObjData(annotations=getattr(obj, "__annotations__", None)) return obj + def optionalabstract(obj): - obj.__abstract__ = AbstractObjData(optional=True, annotations=getattr(obj, "__annotations__", None)) + obj.__abstract__ = AbstractObjData( + optional=True, annotations=getattr(obj, "__annotations__", None) + ) return obj + def check_annotations(a: dict, b: dict) -> bool: for k, v in a.items(): if k not in b: From ecaf23dd9b8fbdadf0106a07ba3996b18cd5d2ec Mon Sep 17 00:00:00 2001 From: Milo Weinberg Date: Mon, 21 Feb 2022 11:10:38 -0500 Subject: [PATCH 08/34] add missing pack/unpack method to all non play packets --- .../packets/757/handshaking/handshake.py | 3 ++ pymine_net/packets/757/login/compression.py | 4 ++ pymine_net/packets/757/login/login.py | 49 ++++++++++++++----- pymine_net/packets/757/status/status.py | 17 +++++-- 4 files changed, 57 insertions(+), 16 deletions(-) diff --git a/pymine_net/packets/757/handshaking/handshake.py b/pymine_net/packets/757/handshaking/handshake.py index a049d51..96ecc6c 100644 --- a/pymine_net/packets/757/handshaking/handshake.py +++ b/pymine_net/packets/757/handshaking/handshake.py @@ -30,6 +30,9 @@ def __init__(self, protocol: int, address: str, port: int, next_state: int): self.port = port self.next_state = next_state + def pack(self) -> Buffer: + return Buffer().write_varint(self.protocol).write_string(self.address).write("H", self.port).write_varint(self.next_state) + @classmethod def unpack(cls, buf: Buffer) -> HandshakeHandshake: return cls(buf.read_varint(), buf.read_string(), buf.read("H"), buf.read_varint()) diff --git a/pymine_net/packets/757/login/compression.py b/pymine_net/packets/757/login/compression.py index 62dfb26..2f90bc6 100644 --- a/pymine_net/packets/757/login/compression.py +++ b/pymine_net/packets/757/login/compression.py @@ -25,3 +25,7 @@ def __init__(self, compression_threshold: int = -1): def pack(self) -> Buffer: return Buffer().write_varint(self.compression_threshold) + + @classmethod + def unpack(cls, buf: Buffer) -> LoginSetCompression: + return cls(buf.read_varint()) diff --git a/pymine_net/packets/757/login/login.py b/pymine_net/packets/757/login/login.py index 512f03b..5612a9c 100644 --- a/pymine_net/packets/757/login/login.py +++ b/pymine_net/packets/757/login/login.py @@ -32,6 +32,9 @@ def __init__(self, username: str): self.username = username + def pack(self) -> Buffer: + return Buffer().write_string(self.username) + @classmethod def unpack(cls, buf: Buffer) -> LoginStart: return cls(buf.read_string()) @@ -64,6 +67,15 @@ def pack(self) -> Buffer: .write_bytes(self.verify_token) ) + @classmethod + def unpack(cls, buf: Buffer) -> LoginEncryptionRequest: + buf.read_string() + + return cls( + buf.read_bytes(buf.read_varint()), + buf.read_bytes(buf.read_varint()) + ) + class LoginEncryptionResponse(ServerBoundPacket): """Response from the client to a LoginEncryptionRequest. (Client -> Server) @@ -83,6 +95,9 @@ def __init__(self, shared_key: bytes, verify_token: bytes): self.shared_key = shared_key self.verify_token = verify_token + def pack(self) -> Buffer: + return Buffer().write_varint(len(self.shared_key)).write_bytes(self.shared_key).write_varint(len(self.verify_token)).write_bytes(self.verify_token) + @classmethod def unpack(cls, buf: Buffer) -> LoginEncryptionResponse: return cls(buf.read_bytes(buf.read_varint()), buf.read_bytes(buf.read_varint())) @@ -109,6 +124,10 @@ def __init__(self, uuid: UUID, username: str): def pack(self) -> Buffer: return Buffer().write_uuid(self.uuid).write_string(self.username) + @classmethod + def unpack(cls, buf: Buffer) -> LoginSuccess: + return cls(buf.read_uuid(), buf.read_string()) + class LoginDisconnect(ClientBoundPacket): """Sent by the server to kick a player while in the login state. (Server -> Client) @@ -127,6 +146,10 @@ def __init__(self, reason: Chat): def pack(self) -> Buffer: return Buffer().write_chat(self.reason) + @classmethod + def unpack(cls, buf: Buffer) -> LoginDisconnect: + return cls(buf.read_chat()) + class LoginPluginRequest(ClientBoundPacket): """Sent by server to implement a custom handshaking flow. @@ -149,29 +172,33 @@ def pack(self) -> Buffer: Buffer().write_varint(self.message_id).write_string(self.channel).write_bytes(self.data) ) + @classmethod + def unpack(cls, buf: Buffer) -> LoginPluginRequest: + return cls( + buf.read_varint(), + buf.read_string(), + buf.read_bytes() + ) + class LoginPluginResponse(ServerBoundPacket): """Response to LoginPluginRequest from client. :param int message_id: Message id, generated by the server, should be unique to the connection. - :param bool successful: True if the client understands the request, False otherwise. When false, no data follows. - :param Optional[bytes] data: Optional response data. + :param Optional[bytes] data: Optional response data, present if client understood request. :ivar int id: Unique packet ID. """ id = 0x02 - def __init__(self, message_id: int, successful: bool, data: bytes = None): + def __init__(self, message_id: int, data: bytes = None): self.message_id = message_id - self.successful = successful self.data = data + def pack(self) -> Buffer: + buf = Buffer().write_varint(self.message_id) + return buf.write_optional(buf.write_bytes, self.data) + @classmethod def unpack(cls, buf: Buffer) -> LoginPluginResponse: - instance = cls(buf.read_varint(), buf.read("?")) - - # assumes that buf can be .read() to completion because data must be inferred from the packet length. - if instance.successful: - instance.data = buf.read() - - return instance + return cls(buf.read_varint(), buf.read_optional(buf.read_bytes)) diff --git a/pymine_net/packets/757/status/status.py b/pymine_net/packets/757/status/status.py index 4aab519..d675e21 100644 --- a/pymine_net/packets/757/status/status.py +++ b/pymine_net/packets/757/status/status.py @@ -17,6 +17,9 @@ class StatusStatusRequest(ServerBoundPacket): def __init__(self): super().__init__() + def pack(self) -> Buffer: + return Buffer() + @classmethod def unpack(cls, buf: Buffer) -> StatusStatusRequest: return cls() @@ -25,20 +28,24 @@ def unpack(cls, buf: Buffer) -> StatusStatusRequest: class StatusStatusResponse(ClientBoundPacket): """Returns server status data back to the requesting client. (Server -> Client) - :param dict response_data: JSON response data sent back to the client. + :param dict data: JSON response data sent back to the client. :ivar int id: Unique packet ID. - :ivar response_data: + :ivar data: """ id = 0x00 - def __init__(self, response_data: dict): + def __init__(self, data: dict): super().__init__() - self.response_data = response_data + self.data = data def pack(self) -> Buffer: - return Buffer().write_json(self.response_data) + return Buffer().write_json(self.data) + + @classmethod + def unpack(cls, buf: Buffer) -> StatusStatusResponse: + return cls(buf.read_json()) class StatusStatusPingPong(ServerBoundPacket, ClientBoundPacket): From 16f556364db3b22e26f6caf83d7f96b6780947c9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 21 Feb 2022 16:11:05 +0000 Subject: [PATCH 09/34] apply black codestyle --- .../packets/757/handshaking/handshake.py | 8 +++++++- pymine_net/packets/757/login/login.py | 19 +++++++++---------- pymine_net/packets/757/status/status.py | 2 +- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/pymine_net/packets/757/handshaking/handshake.py b/pymine_net/packets/757/handshaking/handshake.py index 96ecc6c..6156eb0 100644 --- a/pymine_net/packets/757/handshaking/handshake.py +++ b/pymine_net/packets/757/handshaking/handshake.py @@ -31,7 +31,13 @@ def __init__(self, protocol: int, address: str, port: int, next_state: int): self.next_state = next_state def pack(self) -> Buffer: - return Buffer().write_varint(self.protocol).write_string(self.address).write("H", self.port).write_varint(self.next_state) + return ( + Buffer() + .write_varint(self.protocol) + .write_string(self.address) + .write("H", self.port) + .write_varint(self.next_state) + ) @classmethod def unpack(cls, buf: Buffer) -> HandshakeHandshake: diff --git a/pymine_net/packets/757/login/login.py b/pymine_net/packets/757/login/login.py index 5612a9c..8ebc983 100644 --- a/pymine_net/packets/757/login/login.py +++ b/pymine_net/packets/757/login/login.py @@ -71,10 +71,7 @@ def pack(self) -> Buffer: def unpack(cls, buf: Buffer) -> LoginEncryptionRequest: buf.read_string() - return cls( - buf.read_bytes(buf.read_varint()), - buf.read_bytes(buf.read_varint()) - ) + return cls(buf.read_bytes(buf.read_varint()), buf.read_bytes(buf.read_varint())) class LoginEncryptionResponse(ServerBoundPacket): @@ -96,7 +93,13 @@ def __init__(self, shared_key: bytes, verify_token: bytes): self.verify_token = verify_token def pack(self) -> Buffer: - return Buffer().write_varint(len(self.shared_key)).write_bytes(self.shared_key).write_varint(len(self.verify_token)).write_bytes(self.verify_token) + return ( + Buffer() + .write_varint(len(self.shared_key)) + .write_bytes(self.shared_key) + .write_varint(len(self.verify_token)) + .write_bytes(self.verify_token) + ) @classmethod def unpack(cls, buf: Buffer) -> LoginEncryptionResponse: @@ -174,11 +177,7 @@ def pack(self) -> Buffer: @classmethod def unpack(cls, buf: Buffer) -> LoginPluginRequest: - return cls( - buf.read_varint(), - buf.read_string(), - buf.read_bytes() - ) + return cls(buf.read_varint(), buf.read_string(), buf.read_bytes()) class LoginPluginResponse(ServerBoundPacket): diff --git a/pymine_net/packets/757/status/status.py b/pymine_net/packets/757/status/status.py index d675e21..fa3808b 100644 --- a/pymine_net/packets/757/status/status.py +++ b/pymine_net/packets/757/status/status.py @@ -42,7 +42,7 @@ def __init__(self, data: dict): def pack(self) -> Buffer: return Buffer().write_json(self.data) - + @classmethod def unpack(cls, buf: Buffer) -> StatusStatusResponse: return cls(buf.read_json()) From 56a857e4921997195efe2358fe3526eb4502aac6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 21 Feb 2022 16:34:00 +0000 Subject: [PATCH 10/34] apply import sorting --- tests/test_packets.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_packets.py b/tests/test_packets.py index f5bffb1..9a280da 100644 --- a/tests/test_packets.py +++ b/tests/test_packets.py @@ -4,12 +4,12 @@ from typing import Dict, Optional, Tuple, Union import colorama -from pymine_net.strict_abc import is_abstract -from pymine_net.types.packet import ServerBoundPacket import pytest +from pymine_net.strict_abc import is_abstract from pymine_net.types.buffer import Buffer from pymine_net.types.chat import Chat +from pymine_net.types.packet import ServerBoundPacket colorama.init(autoreset=True) From 91dd2f0dd114648d47f50e1360d401f85eea215f Mon Sep 17 00:00:00 2001 From: Milo Weinberg Date: Mon, 21 Feb 2022 12:59:40 -0500 Subject: [PATCH 11/34] minor docstring + typing + other fixes --- pymine_net/packets/757/play/advancement.py | 2 +- pymine_net/packets/757/play/block.py | 2 + pymine_net/packets/757/play/chat.py | 2 +- pymine_net/packets/757/play/command.py | 4 +- pymine_net/packets/757/play/cooldown.py | 2 + pymine_net/packets/757/play/difficulty.py | 4 +- pymine_net/packets/757/play/effect.py | 10 ++-- pymine_net/packets/757/play/explosion.py | 58 ++++++++++++++-------- pymine_net/packets/757/play/keep_alive.py | 6 +-- 9 files changed, 58 insertions(+), 32 deletions(-) diff --git a/pymine_net/packets/757/play/advancement.py b/pymine_net/packets/757/play/advancement.py index d416213..03a1352 100644 --- a/pymine_net/packets/757/play/advancement.py +++ b/pymine_net/packets/757/play/advancement.py @@ -7,7 +7,7 @@ __all__ = ( "PlayAdvancementTab", - "PlaySelectAdvancementTab", + "PlaySelectAdvancementTab" ) diff --git a/pymine_net/packets/757/play/block.py b/pymine_net/packets/757/play/block.py index a493068..d7c149c 100644 --- a/pymine_net/packets/757/play/block.py +++ b/pymine_net/packets/757/play/block.py @@ -176,6 +176,8 @@ class PlayNBTQueryResponse(ClientBoundPacket): :param int transaction_id: :param nbt.TAG nbt_tag: :ivar int id: Unique packet ID. + :ivar transaction_id: + :ivar nbt_tag: """ id = 0x60 diff --git a/pymine_net/packets/757/play/chat.py b/pymine_net/packets/757/play/chat.py index 1b9a244..9c36293 100644 --- a/pymine_net/packets/757/play/chat.py +++ b/pymine_net/packets/757/play/chat.py @@ -74,7 +74,7 @@ class PlayTabCompleteServerBound(ServerBoundPacket): id = 0x06 - def __init__(self, transaction_id: int, text: str) -> None: + def __init__(self, transaction_id: int, text: str): super().__init__() self.transaction_id = transaction_id diff --git a/pymine_net/packets/757/play/command.py b/pymine_net/packets/757/play/command.py index ba0a635..b737f6d 100644 --- a/pymine_net/packets/757/play/command.py +++ b/pymine_net/packets/757/play/command.py @@ -14,8 +14,8 @@ class PlayDeclareCommands(ClientBoundPacket): """Tells the clients what commands there are. (Server -> Client) :param List[dict] nodes: The command nodes, a list of dictionaries. The first item is assumed to be the root node. - :attr int id: Unique packet ID. - :attr nodes: + :ivar int id: Unique packet ID. + :ivar nodes: """ id = 0x12 diff --git a/pymine_net/packets/757/play/cooldown.py b/pymine_net/packets/757/play/cooldown.py index 5e62acf..f7e31a5 100644 --- a/pymine_net/packets/757/play/cooldown.py +++ b/pymine_net/packets/757/play/cooldown.py @@ -15,6 +15,8 @@ class PlaySetCooldown(ClientBoundPacket): :param int item_id: The unique id of the type of affected items. :param int cooldown_ticks: The length of the cooldown in in-game ticks. :ivar int id: The unique ID of the packet. + :ivar item_id: + :ivar cooldown_ticks: """ id = 0x17 diff --git a/pymine_net/packets/757/play/difficulty.py b/pymine_net/packets/757/play/difficulty.py index 1f0f0ef..378893b 100644 --- a/pymine_net/packets/757/play/difficulty.py +++ b/pymine_net/packets/757/play/difficulty.py @@ -51,7 +51,7 @@ def __init__(self, new_difficulty: int) -> None: @classmethod def unpack(cls, buf: Buffer) -> PlaySetDifficulty: - return cls(buf.read("b")) + return cls(buf.read_byte()) class PlayLockDifficulty(ServerBoundPacket): @@ -64,7 +64,7 @@ class PlayLockDifficulty(ServerBoundPacket): id = 0x10 - def __init__(self, locked: bool) -> None: + def __init__(self, locked: bool): super().__init__() self.locked = locked diff --git a/pymine_net/packets/757/play/effect.py b/pymine_net/packets/757/play/effect.py index 32320dd..519b90d 100644 --- a/pymine_net/packets/757/play/effect.py +++ b/pymine_net/packets/757/play/effect.py @@ -8,7 +8,7 @@ __all__ = ( "PlayEffect", "PlayEntityEffect", - "PlaySoundEffect", + "PlaySoundEffect" ) @@ -38,7 +38,9 @@ def __init__( super().__init__() self.effect_id = effect_id - self.x, self.y, self.z = x, y, z + self.x = x + self.y = y + self.z = z self.data = data self.disable_relative_volume = disable_relative_volume @@ -117,7 +119,9 @@ def __init__( self.sound_id = sound_id self.category = category - self.x, self.y, self.z = x, y, z + self.x = x + self.y = y + self.z = z self.volume = volume self.pitch = pitch diff --git a/pymine_net/packets/757/play/explosion.py b/pymine_net/packets/757/play/explosion.py index 928cc76..20697ab 100644 --- a/pymine_net/packets/757/play/explosion.py +++ b/pymine_net/packets/757/play/explosion.py @@ -1,6 +1,7 @@ """Contains packets related to entities.""" from __future__ import annotations +from typing import List, Tuple from pymine_net.types.buffer import Buffer from pymine_net.types.packet import ClientBoundPacket @@ -9,45 +10,62 @@ class PlayExplosion(ClientBoundPacket): - """Sent when an explosion occurs (creepers, TNT, and ghast fireballs). Each block in Records is set to air. Coordinates for each axis in record is int(X) + record. (Server -> Client)""" + """Sent when an explosion occurs (creepers, TNT, and ghast fireballs). (Server -> Client) + + :param float x: The x coordinate of the explosion. + :param float y: The y coordinate of the explosion. + :param float z: The z coordinate of the explosion. + :param float strength: Strength of the explosion, will summon a minecraft:explosion_emitter particle if >=2.0 else a minecraft:explosion particle. + :param List[Tuple[int, int, int]] records: Array of bytes containing the coordinates of the blocks to destroy relative to the explosion's coordinates. + :param float pmx: Velocity to add to the player's motion in the x axis due to the explosion. + :param float pmy: Velocity to add to the player's motion in the y axis due to the explosion. + :param float pmz: Velocity to add to the player's motion in the z axis due to the explosion. + :ivar int id: + :ivar x: + :ivar y: + :ivar z: + :ivar strength: + :ivar records: + :ivar pmx: + :ivar pmy: + :ivar pmz: + """ id = 0x1C def __init__( self, - x: int, - y: int, - z: int, - strength: int, - record_count: int, - records: list, - pmx: int, - pmy: int, - pmz: int, - ) -> None: + x: float, + y: float, + z: float, + strength: float, + records: List[Tuple[int, int, int]], + pmx: float, + pmy: float, + pmz: float, + ): super().__init__() - self.x, self.y, self.z = x, y, z + self.x = x + self.y = y + self.z = z self.strength = strength - self.record_count = record_count self.records = records self.pmx = pmx self.pmy = pmy self.pmz = pmz def pack(self) -> Buffer: - buf = ( Buffer() .write("f", self.x) .write("f", self.y) .write("f", self.z) .write("f", self.strength) - .write("i", self.record_count) + .write("i", len(self.records)) ) - for r in self.records: - buf.write("b", r) - - buf.write("f", self.pmx).write("f", self.pmy).write("f", self.pmz) - return buf + for rx, ry, rz in self.records: + buf.write_byte(rx).write_byte(ry).write_byte(rz) + + return buf.write("f", self.pmx).write("f", self.pmy).write("f", self.pmz) diff --git a/pymine_net/packets/757/play/keep_alive.py b/pymine_net/packets/757/play/keep_alive.py index af51c50..53ffef4 100644 --- a/pymine_net/packets/757/play/keep_alive.py +++ b/pymine_net/packets/757/play/keep_alive.py @@ -7,7 +7,7 @@ __all__ = ( "PlayKeepAliveClientBound", - "PlayKeepAliveServerBound", + "PlayKeepAliveServerBound" ) @@ -21,7 +21,7 @@ class PlayKeepAliveClientBound(ClientBoundPacket): id = 0x21 - def __init__(self, keep_alive_id: int) -> None: + def __init__(self, keep_alive_id: int): super().__init__() self.keep_alive_id = keep_alive_id @@ -40,7 +40,7 @@ class PlayKeepAliveServerBound(ServerBoundPacket): id = 0x0F - def __init__(self, keep_alive_id: int) -> None: + def __init__(self, keep_alive_id: int): super().__init__() self.keep_alive_id = keep_alive_id From 005ae8d741d3df83430bdb7d56ff8cd5ad2af5ac Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 21 Feb 2022 18:00:02 +0000 Subject: [PATCH 12/34] apply import sorting --- pymine_net/packets/757/play/explosion.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pymine_net/packets/757/play/explosion.py b/pymine_net/packets/757/play/explosion.py index 20697ab..daf53d8 100644 --- a/pymine_net/packets/757/play/explosion.py +++ b/pymine_net/packets/757/play/explosion.py @@ -1,6 +1,7 @@ """Contains packets related to entities.""" from __future__ import annotations + from typing import List, Tuple from pymine_net.types.buffer import Buffer From 817dbdda66a9f4443d2e84c92278226f284ddaca Mon Sep 17 00:00:00 2001 From: Milo Weinberg Date: Mon, 21 Feb 2022 16:40:08 -0500 Subject: [PATCH 13/34] add PlayMapData in map.py --- pymine_net/packets/757/play/map.py | 92 ++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 pymine_net/packets/757/play/map.py diff --git a/pymine_net/packets/757/play/map.py b/pymine_net/packets/757/play/map.py new file mode 100644 index 0000000..6452377 --- /dev/null +++ b/pymine_net/packets/757/play/map.py @@ -0,0 +1,92 @@ +"""Contains packets related to the in-game map item.""" + +from __future__ import annotations +from typing import List, Optional, Tuple + +from pymine_net.types.packet import ClientBoundPacket, ServerBoundPacket +from pymine_net.types.buffer import Buffer +from pymine_net.types.chat import Chat + +__all__ = ("PlayMapData",) + + +class PlayMapData(ClientBoundPacket): + """Updates a rectangular area on a map item. (Server -> Client) + + :param int map_id: ID of the map to be modified. + :param int scale: Zoom of the map (0 fully zoomed in - 4 fully zoomed out). + :param bool locked: Whether the map has been locked in a cartography table or not. + :param bool tracking_pos: Whether the player and other icons are shown on the map. + :param List[Tuple[int, int, int, int, bool, Optional[Chat]]] icons: List of icons shown on the map. + :param int columns: Number of columns being updated. + :param Optional[int] rows: Number of rows being updated, only if columns > 0. + :param Optional[int] x: X offset of the westernmost column, only if columns > 0. + :param Optional[int] z: Z offset of the northernmost row, only if columns > 0. + :param bytes data: The map data, see https://minecraft.fandom.com/wiki/Map_item_format. + :ivar int id: + :ivar map_id: + :ivar scale: + :ivar locked: + :ivar tracking_pos: + :ivar icons: + :ivar columns: + :ivar rows: + :ivar x: + :ivar z: + :ivar data: + """ + + id = 0x27 + + def __init__( + self, + map_id: int, + scale: int, + locked: bool, + tracking_pos: bool, + icons: List[Tuple[int, int, int, int, bool, Optional[Chat]]], + columns: int, + rows: int = None, + x: int = None, + z: int = None, + data: bytes = None, + ): + super().__init__() + + self.map_id = map_id + self.scale = scale + self.tracking_pos = tracking_pos + self.locked = locked + self.icons = icons + self.columns = columns + self.rows = rows + self.x = x + self.z = z + self.data = data + + def pack(self) -> Buffer: + buf = ( + Buffer() + .write_varint(self.map_id) + .write_byte(self.scale) + .write("?", self.locked) + .write("?", self.tracking_pos) + .write_varint(len(self.icons)) + ) + + for (icon_type, x, z, direction, display_name) in self.icons: + ( + buf + .write_varint(icon_type) + .write_byte(x) + .write_byte(z) + .write_byte(direction) + .write_optional(buf.write_chat, display_name) + ) + + buf.write("B", self.columns) + + if len(self.columns) < 1: + return buf + + return buf.write("B", self.rows).write_byte(self.x).write_byte(self.y).write_varint(len(self.data)).write_bytes(self.data) From beb039f321896883f0de56193729e9651665a560 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 21 Feb 2022 21:40:32 +0000 Subject: [PATCH 14/34] apply import sorting --- pymine_net/packets/757/play/map.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pymine_net/packets/757/play/map.py b/pymine_net/packets/757/play/map.py index 6452377..b5a546d 100644 --- a/pymine_net/packets/757/play/map.py +++ b/pymine_net/packets/757/play/map.py @@ -1,11 +1,12 @@ """Contains packets related to the in-game map item.""" from __future__ import annotations + from typing import List, Optional, Tuple -from pymine_net.types.packet import ClientBoundPacket, ServerBoundPacket from pymine_net.types.buffer import Buffer from pymine_net.types.chat import Chat +from pymine_net.types.packet import ClientBoundPacket, ServerBoundPacket __all__ = ("PlayMapData",) From 37b92d206f50390d98a41b56fc0e70e87a5555dd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 21 Feb 2022 22:18:56 +0000 Subject: [PATCH 15/34] apply black codestyle --- pymine_net/packets/757/play/advancement.py | 5 +---- pymine_net/packets/757/play/effect.py | 6 +----- pymine_net/packets/757/play/explosion.py | 4 ++-- pymine_net/packets/757/play/keep_alive.py | 5 +---- pymine_net/packets/757/play/map.py | 13 +++++++++---- 5 files changed, 14 insertions(+), 19 deletions(-) diff --git a/pymine_net/packets/757/play/advancement.py b/pymine_net/packets/757/play/advancement.py index 03a1352..aaa0db8 100644 --- a/pymine_net/packets/757/play/advancement.py +++ b/pymine_net/packets/757/play/advancement.py @@ -5,10 +5,7 @@ from pymine_net.types.buffer import Buffer from pymine_net.types.packet import ClientBoundPacket, ServerBoundPacket -__all__ = ( - "PlayAdvancementTab", - "PlaySelectAdvancementTab" -) +__all__ = ("PlayAdvancementTab", "PlaySelectAdvancementTab") class PlayAdvancementTab(ServerBoundPacket): diff --git a/pymine_net/packets/757/play/effect.py b/pymine_net/packets/757/play/effect.py index 519b90d..a5530ef 100644 --- a/pymine_net/packets/757/play/effect.py +++ b/pymine_net/packets/757/play/effect.py @@ -5,11 +5,7 @@ from pymine_net.types.buffer import Buffer from pymine_net.types.packet import ClientBoundPacket -__all__ = ( - "PlayEffect", - "PlayEntityEffect", - "PlaySoundEffect" -) +__all__ = ("PlayEffect", "PlayEntityEffect", "PlaySoundEffect") class PlayEffect(ClientBoundPacket): diff --git a/pymine_net/packets/757/play/explosion.py b/pymine_net/packets/757/play/explosion.py index daf53d8..b550c79 100644 --- a/pymine_net/packets/757/play/explosion.py +++ b/pymine_net/packets/757/play/explosion.py @@ -12,7 +12,7 @@ class PlayExplosion(ClientBoundPacket): """Sent when an explosion occurs (creepers, TNT, and ghast fireballs). (Server -> Client) - + :param float x: The x coordinate of the explosion. :param float y: The y coordinate of the explosion. :param float z: The z coordinate of the explosion. @@ -68,5 +68,5 @@ def pack(self) -> Buffer: for rx, ry, rz in self.records: buf.write_byte(rx).write_byte(ry).write_byte(rz) - + return buf.write("f", self.pmx).write("f", self.pmy).write("f", self.pmz) diff --git a/pymine_net/packets/757/play/keep_alive.py b/pymine_net/packets/757/play/keep_alive.py index 53ffef4..47ddd96 100644 --- a/pymine_net/packets/757/play/keep_alive.py +++ b/pymine_net/packets/757/play/keep_alive.py @@ -5,10 +5,7 @@ from pymine_net.types.buffer import Buffer from pymine_net.types.packet import ClientBoundPacket, ServerBoundPacket -__all__ = ( - "PlayKeepAliveClientBound", - "PlayKeepAliveServerBound" -) +__all__ = ("PlayKeepAliveClientBound", "PlayKeepAliveServerBound") class PlayKeepAliveClientBound(ClientBoundPacket): diff --git a/pymine_net/packets/757/play/map.py b/pymine_net/packets/757/play/map.py index b5a546d..20a836e 100644 --- a/pymine_net/packets/757/play/map.py +++ b/pymine_net/packets/757/play/map.py @@ -13,7 +13,7 @@ class PlayMapData(ClientBoundPacket): """Updates a rectangular area on a map item. (Server -> Client) - + :param int map_id: ID of the map to be modified. :param int scale: Zoom of the map (0 fully zoomed in - 4 fully zoomed out). :param bool locked: Whether the map has been locked in a cartography table or not. @@ -77,8 +77,7 @@ def pack(self) -> Buffer: for (icon_type, x, z, direction, display_name) in self.icons: ( - buf - .write_varint(icon_type) + buf.write_varint(icon_type) .write_byte(x) .write_byte(z) .write_byte(direction) @@ -90,4 +89,10 @@ def pack(self) -> Buffer: if len(self.columns) < 1: return buf - return buf.write("B", self.rows).write_byte(self.x).write_byte(self.y).write_varint(len(self.data)).write_bytes(self.data) + return ( + buf.write("B", self.rows) + .write_byte(self.x) + .write_byte(self.y) + .write_varint(len(self.data)) + .write_bytes(self.data) + ) From 31c7de7a36251c34073ee891b6ca0f51532a92b3 Mon Sep 17 00:00:00 2001 From: Milo Weinberg Date: Mon, 21 Feb 2022 21:06:37 -0500 Subject: [PATCH 16/34] add sync + async implementations of TCPStream + EncryptedTCPStream + clients --- poetry.lock | 118 ++++++++++++++++++++++++++- pymine_net/__init__.py | 1 + pymine_net/net/asyncio/tcp/client.py | 54 ++++++++++++ pymine_net/net/asyncio/tcp/stream.py | 66 +++++++++++++++ pymine_net/net/client.py | 72 ++++++++++++++++ pymine_net/net/socket/tcp/client.py | 55 +++++++++++++ pymine_net/net/socket/tcp/stream.py | 61 ++++++++++++++ pymine_net/strict_abc.py | 8 +- pymine_net/types/packet.py | 2 +- pymine_net/types/packet_map.py | 39 ++------- pyproject.toml | 1 + 11 files changed, 440 insertions(+), 37 deletions(-) create mode 100644 pymine_net/net/asyncio/tcp/client.py create mode 100644 pymine_net/net/asyncio/tcp/stream.py create mode 100644 pymine_net/net/client.py create mode 100644 pymine_net/net/socket/tcp/client.py create mode 100644 pymine_net/net/socket/tcp/stream.py diff --git a/poetry.lock b/poetry.lock index c462fbc..80f2540 100644 --- a/poetry.lock +++ b/poetry.lock @@ -43,6 +43,17 @@ d = ["aiohttp (>=3.7.4)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] +[[package]] +name = "cffi" +version = "1.15.0" +description = "Foreign Function Interface for Python calling C code." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +pycparser = "*" + [[package]] name = "click" version = "8.0.4" @@ -63,6 +74,25 @@ category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +[[package]] +name = "cryptography" +version = "36.0.1" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +cffi = ">=1.12" + +[package.extras] +docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] +docstest = ["pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"] +pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] +sdist = ["setuptools_rust (>=0.11.4)"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["pytest (>=6.2.0)", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] + [[package]] name = "flake8" version = "4.0.1" @@ -204,6 +234,14 @@ category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +[[package]] +name = "pycparser" +version = "2.21" +description = "C parser in Python" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + [[package]] name = "pyflakes" version = "2.4.0" @@ -284,7 +322,7 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest- [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "2f15fbf5c7ab8e26677fc4278d0a9a230416e2b6b1fb618dc069250dbbcf326d" +content-hash = "010e898a38171e3b7d1e6dfbf52af3dd938d1481ac9327b49a3cd3394d0e7f7c" [metadata.files] atomicwrites = [ @@ -320,6 +358,58 @@ black = [ {file = "black-22.1.0-py3-none-any.whl", hash = "sha256:3524739d76b6b3ed1132422bf9d82123cd1705086723bc3e235ca39fd21c667d"}, {file = "black-22.1.0.tar.gz", hash = "sha256:a7c0192d35635f6fc1174be575cb7915e92e5dd629ee79fdaf0dcfa41a80afb5"}, ] +cffi = [ + {file = "cffi-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962"}, + {file = "cffi-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:23cfe892bd5dd8941608f93348c0737e369e51c100d03718f108bf1add7bd6d0"}, + {file = "cffi-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:41d45de54cd277a7878919867c0f08b0cf817605e4eb94093e7516505d3c8d14"}, + {file = "cffi-1.15.0-cp27-cp27m-win32.whl", hash = "sha256:4a306fa632e8f0928956a41fa8e1d6243c71e7eb59ffbd165fc0b41e316b2474"}, + {file = "cffi-1.15.0-cp27-cp27m-win_amd64.whl", hash = "sha256:e7022a66d9b55e93e1a845d8c9eba2a1bebd4966cd8bfc25d9cd07d515b33fa6"}, + {file = "cffi-1.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:14cd121ea63ecdae71efa69c15c5543a4b5fbcd0bbe2aad864baca0063cecf27"}, + {file = "cffi-1.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d4d692a89c5cf08a8557fdeb329b82e7bf609aadfaed6c0d79f5a449a3c7c023"}, + {file = "cffi-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0104fb5ae2391d46a4cb082abdd5c69ea4eab79d8d44eaaf79f1b1fd806ee4c2"}, + {file = "cffi-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:91ec59c33514b7c7559a6acda53bbfe1b283949c34fe7440bcf917f96ac0723e"}, + {file = "cffi-1.15.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f5c7150ad32ba43a07c4479f40241756145a1f03b43480e058cfd862bf5041c7"}, + {file = "cffi-1.15.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:00c878c90cb53ccfaae6b8bc18ad05d2036553e6d9d1d9dbcf323bbe83854ca3"}, + {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abb9a20a72ac4e0fdb50dae135ba5e77880518e742077ced47eb1499e29a443c"}, + {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a5263e363c27b653a90078143adb3d076c1a748ec9ecc78ea2fb916f9b861962"}, + {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f54a64f8b0c8ff0b64d18aa76675262e1700f3995182267998c31ae974fbc382"}, + {file = "cffi-1.15.0-cp310-cp310-win32.whl", hash = "sha256:c21c9e3896c23007803a875460fb786118f0cdd4434359577ea25eb556e34c55"}, + {file = "cffi-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:5e069f72d497312b24fcc02073d70cb989045d1c91cbd53979366077959933e0"}, + {file = "cffi-1.15.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:64d4ec9f448dfe041705426000cc13e34e6e5bb13736e9fd62e34a0b0c41566e"}, + {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2756c88cbb94231c7a147402476be2c4df2f6078099a6f4a480d239a8817ae39"}, + {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b96a311ac60a3f6be21d2572e46ce67f09abcf4d09344c49274eb9e0bf345fc"}, + {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75e4024375654472cc27e91cbe9eaa08567f7fbdf822638be2814ce059f58032"}, + {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:59888172256cac5629e60e72e86598027aca6bf01fa2465bdb676d37636573e8"}, + {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:27c219baf94952ae9d50ec19651a687b826792055353d07648a5695413e0c605"}, + {file = "cffi-1.15.0-cp36-cp36m-win32.whl", hash = "sha256:4958391dbd6249d7ad855b9ca88fae690783a6be9e86df65865058ed81fc860e"}, + {file = "cffi-1.15.0-cp36-cp36m-win_amd64.whl", hash = "sha256:f6f824dc3bce0edab5f427efcfb1d63ee75b6fcb7282900ccaf925be84efb0fc"}, + {file = "cffi-1.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:06c48159c1abed75c2e721b1715c379fa3200c7784271b3c46df01383b593636"}, + {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c2051981a968d7de9dd2d7b87bcb9c939c74a34626a6e2f8181455dd49ed69e4"}, + {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fd8a250edc26254fe5b33be00402e6d287f562b6a5b2152dec302fa15bb3e997"}, + {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91d77d2a782be4274da750752bb1650a97bfd8f291022b379bb8e01c66b4e96b"}, + {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:45db3a33139e9c8f7c09234b5784a5e33d31fd6907800b316decad50af323ff2"}, + {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:263cc3d821c4ab2213cbe8cd8b355a7f72a8324577dc865ef98487c1aeee2bc7"}, + {file = "cffi-1.15.0-cp37-cp37m-win32.whl", hash = "sha256:17771976e82e9f94976180f76468546834d22a7cc404b17c22df2a2c81db0c66"}, + {file = "cffi-1.15.0-cp37-cp37m-win_amd64.whl", hash = "sha256:3415c89f9204ee60cd09b235810be700e993e343a408693e80ce7f6a40108029"}, + {file = "cffi-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4238e6dab5d6a8ba812de994bbb0a79bddbdf80994e4ce802b6f6f3142fcc880"}, + {file = "cffi-1.15.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0808014eb713677ec1292301ea4c81ad277b6cdf2fdd90fd540af98c0b101d20"}, + {file = "cffi-1.15.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:57e9ac9ccc3101fac9d6014fba037473e4358ef4e89f8e181f8951a2c0162024"}, + {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b6c2ea03845c9f501ed1313e78de148cd3f6cad741a75d43a29b43da27f2e1e"}, + {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:10dffb601ccfb65262a27233ac273d552ddc4d8ae1bf93b21c94b8511bffe728"}, + {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:786902fb9ba7433aae840e0ed609f45c7bcd4e225ebb9c753aa39725bb3e6ad6"}, + {file = "cffi-1.15.0-cp38-cp38-win32.whl", hash = "sha256:da5db4e883f1ce37f55c667e5c0de439df76ac4cb55964655906306918e7363c"}, + {file = "cffi-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:181dee03b1170ff1969489acf1c26533710231c58f95534e3edac87fff06c443"}, + {file = "cffi-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:45e8636704eacc432a206ac7345a5d3d2c62d95a507ec70d62f23cd91770482a"}, + {file = "cffi-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:31fb708d9d7c3f49a60f04cf5b119aeefe5644daba1cd2a0fe389b674fd1de37"}, + {file = "cffi-1.15.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6dc2737a3674b3e344847c8686cf29e500584ccad76204efea14f451d4cc669a"}, + {file = "cffi-1.15.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:74fdfdbfdc48d3f47148976f49fab3251e550a8720bebc99bf1483f5bfb5db3e"}, + {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffaa5c925128e29efbde7301d8ecaf35c8c60ffbcd6a1ffd3a552177c8e5e796"}, + {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f7d084648d77af029acb79a0ff49a0ad7e9d09057a9bf46596dac9514dc07df"}, + {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef1f279350da2c586a69d32fc8733092fd32cc8ac95139a00377841f59a3f8d8"}, + {file = "cffi-1.15.0-cp39-cp39-win32.whl", hash = "sha256:2a23af14f408d53d5e6cd4e3d9a24ff9e05906ad574822a10563efcef137979a"}, + {file = "cffi-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139"}, + {file = "cffi-1.15.0.tar.gz", hash = "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954"}, +] click = [ {file = "click-8.0.4-py3-none-any.whl", hash = "sha256:6a7a62563bbfabfda3a38f3023a1db4a35978c0abd76f6c9605ecd6554d6d9b1"}, {file = "click-8.0.4.tar.gz", hash = "sha256:8458d7b1287c5fb128c90e23381cf99dcde74beaf6c7ff6384ce84d6fe090adb"}, @@ -328,6 +418,28 @@ colorama = [ {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, ] +cryptography = [ + {file = "cryptography-36.0.1-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:73bc2d3f2444bcfeac67dd130ff2ea598ea5f20b40e36d19821b4df8c9c5037b"}, + {file = "cryptography-36.0.1-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:2d87cdcb378d3cfed944dac30596da1968f88fb96d7fc34fdae30a99054b2e31"}, + {file = "cryptography-36.0.1-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:74d6c7e80609c0f4c2434b97b80c7f8fdfaa072ca4baab7e239a15d6d70ed73a"}, + {file = "cryptography-36.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:6c0c021f35b421ebf5976abf2daacc47e235f8b6082d3396a2fe3ccd537ab173"}, + {file = "cryptography-36.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d59a9d55027a8b88fd9fd2826c4392bd487d74bf628bb9d39beecc62a644c12"}, + {file = "cryptography-36.0.1-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a817b961b46894c5ca8a66b599c745b9a3d9f822725221f0e0fe49dc043a3a3"}, + {file = "cryptography-36.0.1-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:94ae132f0e40fe48f310bba63f477f14a43116f05ddb69d6fa31e93f05848ae2"}, + {file = "cryptography-36.0.1-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:7be0eec337359c155df191d6ae00a5e8bbb63933883f4f5dffc439dac5348c3f"}, + {file = "cryptography-36.0.1-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:e0344c14c9cb89e76eb6a060e67980c9e35b3f36691e15e1b7a9e58a0a6c6dc3"}, + {file = "cryptography-36.0.1-cp36-abi3-win32.whl", hash = "sha256:4caa4b893d8fad33cf1964d3e51842cd78ba87401ab1d2e44556826df849a8ca"}, + {file = "cryptography-36.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:391432971a66cfaf94b21c24ab465a4cc3e8bf4a939c1ca5c3e3a6e0abebdbcf"}, + {file = "cryptography-36.0.1-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:bb5829d027ff82aa872d76158919045a7c1e91fbf241aec32cb07956e9ebd3c9"}, + {file = "cryptography-36.0.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ebc15b1c22e55c4d5566e3ca4db8689470a0ca2babef8e3a9ee057a8b82ce4b1"}, + {file = "cryptography-36.0.1-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:596f3cd67e1b950bc372c33f1a28a0692080625592ea6392987dba7f09f17a94"}, + {file = "cryptography-36.0.1-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:30ee1eb3ebe1644d1c3f183d115a8c04e4e603ed6ce8e394ed39eea4a98469ac"}, + {file = "cryptography-36.0.1-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ec63da4e7e4a5f924b90af42eddf20b698a70e58d86a72d943857c4c6045b3ee"}, + {file = "cryptography-36.0.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca238ceb7ba0bdf6ce88c1b74a87bffcee5afbfa1e41e173b1ceb095b39add46"}, + {file = "cryptography-36.0.1-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:ca28641954f767f9822c24e927ad894d45d5a1e501767599647259cbf030b903"}, + {file = "cryptography-36.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:39bdf8e70eee6b1c7b289ec6e5d84d49a6bfa11f8b8646b5b3dfe41219153316"}, + {file = "cryptography-36.0.1.tar.gz", hash = "sha256:53e5c1dc3d7a953de055d77bef2ff607ceef7a2aac0353b5d630ab67f7423638"}, +] flake8 = [ {file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"}, {file = "flake8-4.0.1.tar.gz", hash = "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"}, @@ -391,6 +503,10 @@ pycodestyle = [ {file = "pycodestyle-2.8.0-py2.py3-none-any.whl", hash = "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20"}, {file = "pycodestyle-2.8.0.tar.gz", hash = "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"}, ] +pycparser = [ + {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, + {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, +] pyflakes = [ {file = "pyflakes-2.4.0-py2.py3-none-any.whl", hash = "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e"}, {file = "pyflakes-2.4.0.tar.gz", hash = "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c"}, diff --git a/pymine_net/__init__.py b/pymine_net/__init__.py index 50dd981..ff0f015 100644 --- a/pymine_net/__init__.py +++ b/pymine_net/__init__.py @@ -2,3 +2,4 @@ from pymine_net.packets import load_packet_map # noqa: F401 from pymine_net.types.buffer import Buffer # noqa: F401 from pymine_net.types.packet import ClientBoundPacket, Packet, ServerBoundPacket # noqa: F401 +from pymine_net.net.asyncio.tcp.client import AsyncTCPClient diff --git a/pymine_net/net/asyncio/tcp/client.py b/pymine_net/net/asyncio/tcp/client.py new file mode 100644 index 0000000..6b2b362 --- /dev/null +++ b/pymine_net/net/asyncio/tcp/client.py @@ -0,0 +1,54 @@ +import asyncio +import struct +from typing import Union + +from pymine_net.net.asyncio.tcp.stream import AsyncTCPStream +from pymine_net.net.client import AbstractTCPClient +from pymine_net.types.packet import ClientBoundPacket, ServerBoundPacket + + +class AsyncTCPClient(AbstractTCPClient): + """An async connection over a TCP socket for reading + writing Minecraft packets.""" + + def __init__(self, host: str, port: int, protocol: Union[int, str]): + super().__init__(host, port, protocol) + + self.stream: AsyncTCPStream = None + + async def connect(self) -> None: + _, writer = await asyncio.open_connection(self.host, self.port) + self.stream = AsyncTCPStream(writer) + + async def close(self) -> None: + self.stream.close() + await self.stream.wait_closed() + + async def read_packet_length(self) -> int: + value = 0 + + for i in range(10): + byte = struct.unpack(">B", await self.stream.readexactly(1)) + value |= (byte & 0x7F) << 7 * i + + if not byte & 0x80: + break + + if value & (1 << 31): + value -= 1 << 32 + + value_max = (1 << (32 - 1)) - 1 + value_min = -1 << (32 - 1) + + if not (value_min <= value <= value_max): + raise ValueError( + f"Value doesn't fit in given range: {value_min} <= {value} < {value_max}" + ) + + return value + + async def read_packet(self) -> ClientBoundPacket: + packet_length = await self.read_packet_length() + return self.decode_packet(await self.stream.readexactly(packet_length)) + + async def write_packet(self, packet: ServerBoundPacket) -> None: + await self.stream.write(self.encode_packet(packet)) diff --git a/pymine_net/net/asyncio/tcp/stream.py b/pymine_net/net/asyncio/tcp/stream.py new file mode 100644 index 0000000..b9cd8d8 --- /dev/null +++ b/pymine_net/net/asyncio/tcp/stream.py @@ -0,0 +1,66 @@ +from ctypes import Union +from typing import Tuple +from cryptography.hazmat.primitives.ciphers import Cipher +from asyncio import StreamWriter + +from pymine_net.types.buffer import Buffer + + +class AsyncTCPStream(StreamWriter): + """Used for reading and writing from/to a connected client, merges functions of a StreamReader and StreamWriter. + + :param StreamReader reader: An asyncio.StreamReader instance. + :param StreamWriter writer: An asyncio.StreamWriter instance. + :ivar Tuple[str, int] remote: A tuple which stores the remote client's address and port. + """ + + def __init__(self, writer: StreamWriter): + super().__init__(writer._transport, writer._protocol, writer._reader, writer._loop) + + self.remote: Tuple[str, int] = self.get_extra_info("peername") + + async def read(self, length: int = -1) -> Buffer: + return Buffer(await self._reader.read(length)) + + async def readline(self) -> Buffer: + return Buffer(await self._reader.readline()) + + async def readexactly(self, length: int) -> Buffer: + return Buffer(await self._reader.readexactly(length)) + + async def readuntil(self, separator: bytes = b"\n") -> Buffer: + return Buffer(await self._reader.readuntil(separator)) + + +class AsyncEncryptedTCPStream(AsyncTCPStream): + """An encrypted version of an AsyncTCPStream, automatically encrypts and decrypts outgoing and incoming data. + + :param AsyncTCPStream stream: The original, stream-compatible object. + :param Cipher cipher: The cipher instance, used to encrypt + decrypt data. + :ivar _CipherContext decryptor: Description of parameter `_CipherContext`. + :ivar _CipherContext encryptor: Description of parameter `_CipherContext`. + """ + + def __init__(self, stream: AsyncTCPStream, cipher: Cipher): + super().__init__(stream) + + self.decryptor = cipher.decryptor() + self.encryptor = cipher.encryptor() + + async def read(self, length: int = -1) -> Buffer: + return Buffer(self.decryptor.update(await super().read(length))) + + async def readline(self) -> Buffer: + return Buffer(self.decryptor.update(await super().readline())) + + async def readexactly(self, length: int) -> Buffer: + return Buffer(self.decryptor.update(await super().readexactly(length))) + + async def readuntil(self, separator: bytes = b"\n") -> Buffer: + return Buffer(self.decryptor.update(await super().readuntil(separator))) + + def write(self, data: Union[Buffer, bytes, bytearray]) -> None: + super().write(self.encryptor.update(data)) + + def writelines(self, data: Union[Buffer, bytes, bytearray]) -> None: + super().writelines(self.encryptor.update(data)) diff --git a/pymine_net/net/client.py b/pymine_net/net/client.py new file mode 100644 index 0000000..39b646b --- /dev/null +++ b/pymine_net/net/client.py @@ -0,0 +1,72 @@ +from typing import Type, Union +import zlib +from pymine_net.enums import GameState, PacketDirection +from pymine_net.errors import UnknownPacketIdError +from pymine_net.packets import load_packet_map +from pymine_net.types.buffer import Buffer +from pymine_net.types.packet import ClientBoundPacket, ServerBoundPacket +from pymine_net.strict_abc import StrictABC, abstract + +class AbstractTCPClient(StrictABC): + """Abstract class for a connection over a TCP socket for reading + writing Minecraft packets.""" + + def __init__(self, host: str, port: int, protocol: Union[int, str]): + self.host = host + self.port = port + + self.packet_map = load_packet_map(protocol) + + self.state = GameState.HANDSHAKING + self.compression_threshold = -1 + + @abstract + def connect(self) -> None: + pass + + @abstract + def close(self) -> None: + pass + + @staticmethod + def encode_packet(packet: ClientBoundPacket, compression_threshold: int = -1) -> Buffer: + """Encodes and (if necessary) compresses a ClientBoundPacket.""" + + buf = Buffer().write_varint(packet.id).extend(packet.pack()) + + if compression_threshold >= 1: + if len(buf) >= compression_threshold: + buf = Buffer().write_varint(len(buf)).extend(zlib.compress(buf)) + else: + buf = Buffer().write_varint(0).extend(buf) + + return Buffer().write_varint(len(buf)).extend(buf) + + def decode_packet(self, buf: Buffer) -> ClientBoundPacket: + # decompress packet if necessary + if self.compression_threshold >= 0: + uncompressed_length = buf.read_varint() + + if uncompressed_length > 0: + buf = Buffer(zlib.decompress(buf.read_bytes())) + + packet_id = buf.read_varint() + + # attempt to get packet class from given state and packet id + try: + packet_class: Type[ClientBoundPacket] = self.packet_map[PacketDirection.CLIENTBOUND, self.state, packet_id] + except KeyError: + raise UnknownPacketIdError(None, self.state, packet_id, PacketDirection.CLIENTBOUND) + + return packet_class.unpack(buf) + + @abstract + def read_packet_length(self) -> int: + pass + + @abstract + def read_packet(self) -> ClientBoundPacket: + pass + + @abstract + def write_packet(self, packet: ServerBoundPacket) -> None: + pass diff --git a/pymine_net/net/socket/tcp/client.py b/pymine_net/net/socket/tcp/client.py new file mode 100644 index 0000000..b140921 --- /dev/null +++ b/pymine_net/net/socket/tcp/client.py @@ -0,0 +1,55 @@ +import socket +import struct +from typing import Union + +from pymine_net.net.socket.tcp.stream import SocketTCPStream +from pymine_net.net.client import AbstractTCPClient +from pymine_net.types.packet import ClientBoundPacket, ServerBoundPacket + + +class SocketTCPClient(AbstractTCPClient): + """A connection over a TCP socket for reading + writing Minecraft packets.""" + + def __init__(self, host: str, port: int, protocol: Union[int, str]): + super().__init__(host, port, protocol) + + self.stream: SocketTCPStream = None + + def connect(self) -> None: + sock = socket.create_connection((self.host, self.port)) + sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + + self.stream = SocketTCPStream(sock) + + def close(self) -> None: + self.stream.close() + + def read_packet_length(self) -> int: + value = 0 + + for i in range(10): + byte = struct.unpack(">B", self.stream.read(1)) + value |= (byte & 0x7F) << 7 * i + + if not byte & 0x80: + break + + if value & (1 << 31): + value -= 1 << 32 + + value_max = (1 << (32 - 1)) - 1 + value_min = -1 << (32 - 1) + + if not (value_min <= value <= value_max): + raise ValueError( + f"Value doesn't fit in given range: {value_min} <= {value} < {value_max}" + ) + + return value + + def read_packet(self) -> ClientBoundPacket: + packet_length = self.read_packet_length() + return self.decode_packet(self.stream.read(packet_length)) + + def write_packet(self, packet: ServerBoundPacket) -> None: + self.stream.write(self.encode_packet(packet)) diff --git a/pymine_net/net/socket/tcp/stream.py b/pymine_net/net/socket/tcp/stream.py new file mode 100644 index 0000000..f5e655a --- /dev/null +++ b/pymine_net/net/socket/tcp/stream.py @@ -0,0 +1,61 @@ +import socket +from typing import Tuple, Union +from cryptography.hazmat.primitives.ciphers import Cipher + +from pymine_net.types.buffer import Buffer + + +class SocketTCPStream(socket.socket): + """Used for reading and writing from/to a connected client, wraps a socket.socket. + + :param socket.socket sock: A socket.socket instance. + :ivar Tuple[str, int] remote: A tuple which stores the remote client's address and port. + :ivar sock: + """ + + def __init__(self, sock: socket.socket): + self.sock = sock + self.remote: Tuple[str, int] = sock.getsockname() + + def read(self, length: int) -> bytearray: + result = bytearray() + + while len(result) < length: + read_bytes = self.sock.recv(length - len(result)) + + if len(read_bytes) == 0: + raise IOError("Server didn't respond with information!") + + result.extend(read_bytes) + + return result + + def write(self, data: bytes) -> None: + self.sock.send(data) + + def close(self) -> None: + self.sock.close() + + +class EncryptedSocketTCPStream(SocketTCPStream): + """Used for reading and writing from/to a connected client, wraps a socket.socket. + + :param socket.socket sock: A socket.socket instance. + :param Cipher cipher: The cipher instance, used to encrypt + decrypt data. + :ivar Tuple[str, int] remote: A tuple which stores the remote client's address and port. + :ivar sock: + :ivar _CipherContext decryptor: Description of parameter `_CipherContext`. + :ivar _CipherContext encryptor: Description of parameter `_CipherContext`. + """ + + def __init__(self, sock: socket.socket, cipher: Cipher): + super().__init__(sock) + + self.decryptor = cipher.decryptor() + self.encryptor = cipher.encryptor() + + def read(self, length: int) -> Buffer: + return Buffer(self.decryptor.update(super().read(length))) + + def write(self, data: Union[Buffer, bytes, bytearray]) -> None: + super().write(self.encryptor.update(data)) diff --git a/pymine_net/strict_abc.py b/pymine_net/strict_abc.py index 20d4186..422ce47 100644 --- a/pymine_net/strict_abc.py +++ b/pymine_net/strict_abc.py @@ -43,8 +43,12 @@ def check_annotations(a: dict, b: dict) -> bool: if type(v) is str: return type(b[k]) is str and v == b[k] - if not issubclass(b[k], v): - return False + try: + if not issubclass(b[k], v): + return False + except TypeError: + if b[k] is not v: + return False return True diff --git a/pymine_net/types/packet.py b/pymine_net/types/packet.py index e06f7e5..594882c 100644 --- a/pymine_net/types/packet.py +++ b/pymine_net/types/packet.py @@ -47,5 +47,5 @@ def pack(self) -> Buffer: @classmethod @optionalabstract - def unpack(cls, buf: Buffer) -> ServerBoundPacket: + def unpack(cls, buf: Buffer) -> ClientBoundPacket: raise NotImplementedError diff --git a/pymine_net/types/packet_map.py b/pymine_net/types/packet_map.py index 7ca6531..1913416 100644 --- a/pymine_net/types/packet_map.py +++ b/pymine_net/types/packet_map.py @@ -1,7 +1,7 @@ from __future__ import annotations import zlib -from typing import Dict, List, Type, Union +from typing import Dict, List, Tuple, Type, Union from pymine_net.enums import GameState, PacketDirection from pymine_net.errors import DuplicatePacketIdError, UnknownPacketIdError @@ -63,37 +63,10 @@ def __init__(self, protocol: Union[str, int], packets: Dict[GameState, StatePack self.protocol = protocol self.packets = packets - def encode_packet(self, packet: ClientBoundPacket, compression_threshold: int = -1) -> Buffer: - """Encodes and (if necessary) compresses a ClientBoundPacket.""" + def __getitem__(self, key: Tuple[PacketDirection, int, int]) -> Packet: + direction, state, packet_id = key - buf = Buffer().write_varint(packet.id).extend(packet.pack()) + if direction == PacketDirection.CLIENTBOUND: + return self.packets[state].client_bound[packet_id] - if compression_threshold >= 1: - if len(buf) >= compression_threshold: - buf = Buffer().write_varint(len(buf)).extend(zlib.compress(buf)) - else: - buf = Buffer().write_varint(0).extend(buf) - - return Buffer().write_varint(len(buf)).extend(buf) - - def decode_packet( - self, buf: Buffer, state: GameState, compression_threshold: int = -1 - ) -> ServerBoundPacket: - """Decodes and (if necessary) decompresses a ServerBoundPacket.""" - - # decompress packet if necessary - if compression_threshold >= 0: - uncompressed_length = buf.read_varint() - - if uncompressed_length > 0: - buf = Buffer(zlib.decompress(buf.read_bytes())) - - packet_id = buf.read_varint() - - # attempt to get packet class from given state and packet id - try: - packet_class = self.packets[state].server_bound[packet_id] - except KeyError: - raise UnknownPacketIdError(self.protocol, state, packet_id, PacketDirection.SERVERBOUND) - - return packet_class.unpack(buf) + return self.packets[state].server_bound[packet_id] diff --git a/pyproject.toml b/pyproject.toml index 0b2fd67..ce00158 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,7 @@ keywords = ["Minecraft", "protocol", "networking"] python = "^3.7" mutf8 = "^1.0.6" pytest = "^7.0.1" +cryptography = "^36.0.1" [tool.poetry.dev-dependencies] flake8 = "^4.0.1" From a055f2b007947ccb3c855c2ec54513803910c833 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 22 Feb 2022 02:07:01 +0000 Subject: [PATCH 17/34] apply import sorting --- pymine_net/__init__.py | 2 +- pymine_net/net/asyncio/tcp/stream.py | 3 ++- pymine_net/net/client.py | 6 ++++-- pymine_net/net/socket/tcp/client.py | 2 +- pymine_net/net/socket/tcp/stream.py | 1 + 5 files changed, 9 insertions(+), 5 deletions(-) diff --git a/pymine_net/__init__.py b/pymine_net/__init__.py index ff0f015..83e65eb 100644 --- a/pymine_net/__init__.py +++ b/pymine_net/__init__.py @@ -1,5 +1,5 @@ import pymine_net.types.nbt as nbt # noqa: F401 +from pymine_net.net.asyncio.tcp.client import AsyncTCPClient from pymine_net.packets import load_packet_map # noqa: F401 from pymine_net.types.buffer import Buffer # noqa: F401 from pymine_net.types.packet import ClientBoundPacket, Packet, ServerBoundPacket # noqa: F401 -from pymine_net.net.asyncio.tcp.client import AsyncTCPClient diff --git a/pymine_net/net/asyncio/tcp/stream.py b/pymine_net/net/asyncio/tcp/stream.py index b9cd8d8..b906c79 100644 --- a/pymine_net/net/asyncio/tcp/stream.py +++ b/pymine_net/net/asyncio/tcp/stream.py @@ -1,7 +1,8 @@ +from asyncio import StreamWriter from ctypes import Union from typing import Tuple + from cryptography.hazmat.primitives.ciphers import Cipher -from asyncio import StreamWriter from pymine_net.types.buffer import Buffer diff --git a/pymine_net/net/client.py b/pymine_net/net/client.py index 39b646b..2b9eae7 100644 --- a/pymine_net/net/client.py +++ b/pymine_net/net/client.py @@ -1,11 +1,13 @@ -from typing import Type, Union import zlib +from typing import Type, Union + from pymine_net.enums import GameState, PacketDirection from pymine_net.errors import UnknownPacketIdError from pymine_net.packets import load_packet_map +from pymine_net.strict_abc import StrictABC, abstract from pymine_net.types.buffer import Buffer from pymine_net.types.packet import ClientBoundPacket, ServerBoundPacket -from pymine_net.strict_abc import StrictABC, abstract + class AbstractTCPClient(StrictABC): """Abstract class for a connection over a TCP socket for reading + writing Minecraft packets.""" diff --git a/pymine_net/net/socket/tcp/client.py b/pymine_net/net/socket/tcp/client.py index b140921..dd54cb1 100644 --- a/pymine_net/net/socket/tcp/client.py +++ b/pymine_net/net/socket/tcp/client.py @@ -2,8 +2,8 @@ import struct from typing import Union -from pymine_net.net.socket.tcp.stream import SocketTCPStream from pymine_net.net.client import AbstractTCPClient +from pymine_net.net.socket.tcp.stream import SocketTCPStream from pymine_net.types.packet import ClientBoundPacket, ServerBoundPacket diff --git a/pymine_net/net/socket/tcp/stream.py b/pymine_net/net/socket/tcp/stream.py index f5e655a..3cc8783 100644 --- a/pymine_net/net/socket/tcp/stream.py +++ b/pymine_net/net/socket/tcp/stream.py @@ -1,5 +1,6 @@ import socket from typing import Tuple, Union + from cryptography.hazmat.primitives.ciphers import Cipher from pymine_net.types.buffer import Buffer From 4f677698092a245630559eff53ff4bb47c265e7d Mon Sep 17 00:00:00 2001 From: Milo Weinberg Date: Tue, 22 Feb 2022 11:44:05 -0500 Subject: [PATCH 18/34] more progress on networking code --- pymine_net/net/asyncio/tcp/client.py | 30 ++---------- pymine_net/net/asyncio/tcp/server.py | 51 ++++++++++++++++++++ pymine_net/net/asyncio/tcp/stream.py | 30 ++++++++++-- pymine_net/net/client.py | 10 ++-- pymine_net/net/server.py | 71 ++++++++++++++++++++++++++++ pymine_net/net/socket/tcp/client.py | 29 ++---------- pymine_net/net/socket/tcp/stream.py | 30 +++++++++++- pymine_net/net/stream.py | 16 +++++++ 8 files changed, 204 insertions(+), 63 deletions(-) create mode 100644 pymine_net/net/asyncio/tcp/server.py create mode 100644 pymine_net/net/server.py create mode 100644 pymine_net/net/stream.py diff --git a/pymine_net/net/asyncio/tcp/client.py b/pymine_net/net/asyncio/tcp/client.py index 6b2b362..9791160 100644 --- a/pymine_net/net/asyncio/tcp/client.py +++ b/pymine_net/net/asyncio/tcp/client.py @@ -23,32 +23,10 @@ async def close(self) -> None: self.stream.close() await self.stream.wait_closed() - async def read_packet_length(self) -> int: - value = 0 - - for i in range(10): - byte = struct.unpack(">B", await self.stream.readexactly(1)) - value |= (byte & 0x7F) << 7 * i - - if not byte & 0x80: - break - - if value & (1 << 31): - value -= 1 << 32 - - value_max = (1 << (32 - 1)) - 1 - value_min = -1 << (32 - 1) - - if not (value_min <= value <= value_max): - raise ValueError( - f"Value doesn't fit in given range: {value_min} <= {value} < {value_max}" - ) - - return value - async def read_packet(self) -> ClientBoundPacket: - packet_length = await self.read_packet_length() - return self.decode_packet(await self.stream.readexactly(packet_length)) + packet_length = await self.stream.read_varint() + return self._decode_packet(await self.stream.readexactly(packet_length)) async def write_packet(self, packet: ServerBoundPacket) -> None: - await self.stream.write(self.encode_packet(packet)) + self.stream.write(self._encode_packet(packet)) + await self.stream.drain() diff --git a/pymine_net/net/asyncio/tcp/server.py b/pymine_net/net/asyncio/tcp/server.py new file mode 100644 index 0000000..e740b8e --- /dev/null +++ b/pymine_net/net/asyncio/tcp/server.py @@ -0,0 +1,51 @@ + +import asyncio +from typing import Dict, Tuple, Union +from pymine_net.net.asyncio.tcp.stream import AsyncTCPStream +from pymine_net.net.server import AbstractTCPServer, AbstractTCPServerClient +from pymine_net.strict_abc import abstract +from pymine_net.types.packet import ClientBoundPacket, ServerBoundPacket +from pymine_net.types.packet_map import PacketMap + + +class AsyncTCPServerClient(AbstractTCPServerClient): + __slots__ = ("stream", "state", "compression_threshold") + + def __init__(self, stream: AsyncTCPStream): + super().__init__(stream) + self.stream = stream + + +class AsyncTCPServer(AbstractTCPServer): + def __init__(self, host: str, port: int, protocol: Union[int, str], packet_map: PacketMap): + super().__init__(host, port, protocol, packet_map) + + self.connected_clients: Dict[Tuple[str, int], AsyncTCPServerClient] = {} + + self.server: asyncio.AbstractServer = None + + async def run(self) -> None: + self.server = await asyncio.start_server() + + async def stop(self) -> None: + self.server.close() + await self.server.wait_closed() + + async def read_packet(self, client: AsyncTCPServerClient) -> ServerBoundPacket: + length = await client.stream.read_varint() + return self._decode_packet(client, await client.stream.readexactly(length)) + + async def write_packet(self, client: AsyncTCPServerClient, packet: ClientBoundPacket) -> None: + client.stream.write(self._encode_packet(packet, client.compression_threshold)) + await client.stream.drain() + + async def _client_connected_cb(self, _: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None: + client = AsyncTCPServerClient(AsyncTCPStream(writer)) + + self.connected_clients[client.stream.remote] = client + + await self.new_client_connected(client) + + @abstract + async def new_client_connected(self, client: AsyncTCPServerClient) -> None: + pass diff --git a/pymine_net/net/asyncio/tcp/stream.py b/pymine_net/net/asyncio/tcp/stream.py index b906c79..b88d72c 100644 --- a/pymine_net/net/asyncio/tcp/stream.py +++ b/pymine_net/net/asyncio/tcp/stream.py @@ -1,13 +1,14 @@ from asyncio import StreamWriter -from ctypes import Union -from typing import Tuple +import struct +from typing import Tuple, Union from cryptography.hazmat.primitives.ciphers import Cipher +from pymine_net.net.stream import AbstractTCPStream from pymine_net.types.buffer import Buffer -class AsyncTCPStream(StreamWriter): +class AsyncTCPStream(AbstractTCPStream, StreamWriter): """Used for reading and writing from/to a connected client, merges functions of a StreamReader and StreamWriter. :param StreamReader reader: An asyncio.StreamReader instance. @@ -32,6 +33,29 @@ async def readexactly(self, length: int) -> Buffer: async def readuntil(self, separator: bytes = b"\n") -> Buffer: return Buffer(await self._reader.readuntil(separator)) + async def read_varint(self) -> int: + value = 0 + + for i in range(10): + byte = struct.unpack(">B", await self.readexactly(1)) + value |= (byte & 0x7F) << 7 * i + + if not byte & 0x80: + break + + if value & (1 << 31): + value -= 1 << 32 + + value_max = (1 << (32 - 1)) - 1 + value_min = -1 << (32 - 1) + + if not (value_min <= value <= value_max): + raise ValueError( + f"Value doesn't fit in given range: {value_min} <= {value} < {value_max}" + ) + + return value + class AsyncEncryptedTCPStream(AsyncTCPStream): """An encrypted version of an AsyncTCPStream, automatically encrypts and decrypts outgoing and incoming data. diff --git a/pymine_net/net/client.py b/pymine_net/net/client.py index 2b9eae7..a826a42 100644 --- a/pymine_net/net/client.py +++ b/pymine_net/net/client.py @@ -30,8 +30,8 @@ def close(self) -> None: pass @staticmethod - def encode_packet(packet: ClientBoundPacket, compression_threshold: int = -1) -> Buffer: - """Encodes and (if necessary) compresses a ClientBoundPacket.""" + def _encode_packet(packet: ServerBoundPacket, compression_threshold: int = -1) -> Buffer: + """Encodes and (if necessary) compresses a ServerBoundPacket.""" buf = Buffer().write_varint(packet.id).extend(packet.pack()) @@ -43,7 +43,7 @@ def encode_packet(packet: ClientBoundPacket, compression_threshold: int = -1) -> return Buffer().write_varint(len(buf)).extend(buf) - def decode_packet(self, buf: Buffer) -> ClientBoundPacket: + def _decode_packet(self, buf: Buffer) -> ClientBoundPacket: # decompress packet if necessary if self.compression_threshold >= 0: uncompressed_length = buf.read_varint() @@ -61,10 +61,6 @@ def decode_packet(self, buf: Buffer) -> ClientBoundPacket: return packet_class.unpack(buf) - @abstract - def read_packet_length(self) -> int: - pass - @abstract def read_packet(self) -> ClientBoundPacket: pass diff --git a/pymine_net/net/server.py b/pymine_net/net/server.py new file mode 100644 index 0000000..b369602 --- /dev/null +++ b/pymine_net/net/server.py @@ -0,0 +1,71 @@ +from typing import Dict, Tuple, Type, Union +import zlib +from pymine_net.enums import GameState, PacketDirection +from pymine_net.errors import UnknownPacketIdError +from pymine_net.net.stream import AbstractTCPStream + +from pymine_net.strict_abc import StrictABC, abstract +from pymine_net.types.buffer import Buffer +from pymine_net.types.packet import ClientBoundPacket, ServerBoundPacket +from pymine_net.types.packet_map import PacketMap + + +class AbstractTCPServerClient(StrictABC): + __slots__ = ("stream", "state", "compression_threshold") + + def __init__(self, stream: AbstractTCPStream): + self.stream = stream + self.state = GameState.HANDSHAKING + self.compression_threshold = -1 + + +class AbstractTCPServer: + """Abstract class for a TCP server that handles Minecraft packets.""" + + def __init__(self, host: str, port: int, protocol: Union[int, str], packet_map: PacketMap): + self.host = host + self.port = port + self.protocol = protocol + self.packet_map = packet_map + + self.connected_clients: Dict[Tuple[str, int], AbstractTCPServerClient] = {} + + @abstract + def run(self) -> None: + pass + + @abstract + def close(self) -> None: + pass + + @staticmethod + def _encode_packet(packet: ClientBoundPacket, compression_threshold: int = -1) -> Buffer: + """Encodes and (if necessary) compresses a ClientBoundPacket.""" + + buf = Buffer().write_varint(packet.id).extend(packet.pack()) + + if compression_threshold >= 1: + if len(buf) >= compression_threshold: + buf = Buffer().write_varint(len(buf)).extend(zlib.compress(buf)) + else: + buf = Buffer().write_varint(0).extend(buf) + + return Buffer().write_varint(len(buf)).extend(buf) + + def _decode_packet(self, client: AbstractTCPServerClient, buf: Buffer) -> ServerBoundPacket: + # decompress packet if necessary + if client.compression_threshold >= 0: + uncompressed_length = buf.read_varint() + + if uncompressed_length > 0: + buf = Buffer(zlib.decompress(buf.read_bytes())) + + packet_id = buf.read_varint() + + # attempt to get packet class from given state and packet id + try: + packet_class: Type[ClientBoundPacket] = self.packet_map[PacketDirection.SERVERBOUND, client.state, packet_id] + except KeyError: + raise UnknownPacketIdError(None, client.state, packet_id, PacketDirection.SERVERBOUND) + + return packet_class.unpack(buf) diff --git a/pymine_net/net/socket/tcp/client.py b/pymine_net/net/socket/tcp/client.py index dd54cb1..6d2a23e 100644 --- a/pymine_net/net/socket/tcp/client.py +++ b/pymine_net/net/socket/tcp/client.py @@ -24,32 +24,9 @@ def connect(self) -> None: def close(self) -> None: self.stream.close() - def read_packet_length(self) -> int: - value = 0 - - for i in range(10): - byte = struct.unpack(">B", self.stream.read(1)) - value |= (byte & 0x7F) << 7 * i - - if not byte & 0x80: - break - - if value & (1 << 31): - value -= 1 << 32 - - value_max = (1 << (32 - 1)) - 1 - value_min = -1 << (32 - 1) - - if not (value_min <= value <= value_max): - raise ValueError( - f"Value doesn't fit in given range: {value_min} <= {value} < {value_max}" - ) - - return value - def read_packet(self) -> ClientBoundPacket: - packet_length = self.read_packet_length() - return self.decode_packet(self.stream.read(packet_length)) + packet_length = self.stream.read_varint() + return self._decode_packet(self.stream.read(packet_length)) def write_packet(self, packet: ServerBoundPacket) -> None: - self.stream.write(self.encode_packet(packet)) + self.stream.write(self._encode_packet(packet)) diff --git a/pymine_net/net/socket/tcp/stream.py b/pymine_net/net/socket/tcp/stream.py index 3cc8783..26799a4 100644 --- a/pymine_net/net/socket/tcp/stream.py +++ b/pymine_net/net/socket/tcp/stream.py @@ -1,12 +1,14 @@ import socket +import struct from typing import Tuple, Union from cryptography.hazmat.primitives.ciphers import Cipher +from pymine_net.net.stream import AbstractTCPStream from pymine_net.types.buffer import Buffer -class SocketTCPStream(socket.socket): +class SocketTCPStream(AbstractTCPStream, socket.socket): """Used for reading and writing from/to a connected client, wraps a socket.socket. :param socket.socket sock: A socket.socket instance. @@ -14,8 +16,11 @@ class SocketTCPStream(socket.socket): :ivar sock: """ + __slots__ = ("sock",) + def __init__(self, sock: socket.socket): self.sock = sock + self.remote: Tuple[str, int] = sock.getsockname() def read(self, length: int) -> bytearray: @@ -37,6 +42,29 @@ def write(self, data: bytes) -> None: def close(self) -> None: self.sock.close() + def read_varint(self) -> int: + value = 0 + + for i in range(10): + byte = struct.unpack(">B", self.read(1)) + value |= (byte & 0x7F) << 7 * i + + if not byte & 0x80: + break + + if value & (1 << 31): + value -= 1 << 32 + + value_max = (1 << (32 - 1)) - 1 + value_min = -1 << (32 - 1) + + if not (value_min <= value <= value_max): + raise ValueError( + f"Value doesn't fit in given range: {value_min} <= {value} < {value_max}" + ) + + return value + class EncryptedSocketTCPStream(SocketTCPStream): """Used for reading and writing from/to a connected client, wraps a socket.socket. diff --git a/pymine_net/net/stream.py b/pymine_net/net/stream.py new file mode 100644 index 0000000..c00bf65 --- /dev/null +++ b/pymine_net/net/stream.py @@ -0,0 +1,16 @@ + +from re import L +from typing import Tuple +from pymine_net.strict_abc import abstract + + +class AbstractTCPStream: + """Abstract class for a TCP stream.""" + + @property + def remote(self) -> Tuple[str, int]: + raise NotImplementedError + + @abstract + def read_varint(self) -> int: + pass From 91623c0e5bda6bab099165277ac311017204ed7f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 22 Feb 2022 16:44:27 +0000 Subject: [PATCH 19/34] apply import sorting --- pymine_net/net/asyncio/tcp/server.py | 1 + pymine_net/net/asyncio/tcp/stream.py | 4 ++-- pymine_net/net/server.py | 4 ++-- pymine_net/net/socket/tcp/stream.py | 2 +- pymine_net/net/stream.py | 1 + 5 files changed, 7 insertions(+), 5 deletions(-) diff --git a/pymine_net/net/asyncio/tcp/server.py b/pymine_net/net/asyncio/tcp/server.py index e740b8e..03f95fa 100644 --- a/pymine_net/net/asyncio/tcp/server.py +++ b/pymine_net/net/asyncio/tcp/server.py @@ -1,6 +1,7 @@ import asyncio from typing import Dict, Tuple, Union + from pymine_net.net.asyncio.tcp.stream import AsyncTCPStream from pymine_net.net.server import AbstractTCPServer, AbstractTCPServerClient from pymine_net.strict_abc import abstract diff --git a/pymine_net/net/asyncio/tcp/stream.py b/pymine_net/net/asyncio/tcp/stream.py index b88d72c..461e9a0 100644 --- a/pymine_net/net/asyncio/tcp/stream.py +++ b/pymine_net/net/asyncio/tcp/stream.py @@ -1,10 +1,10 @@ -from asyncio import StreamWriter import struct +from asyncio import StreamWriter from typing import Tuple, Union from cryptography.hazmat.primitives.ciphers import Cipher -from pymine_net.net.stream import AbstractTCPStream +from pymine_net.net.stream import AbstractTCPStream from pymine_net.types.buffer import Buffer diff --git a/pymine_net/net/server.py b/pymine_net/net/server.py index b369602..511112c 100644 --- a/pymine_net/net/server.py +++ b/pymine_net/net/server.py @@ -1,9 +1,9 @@ -from typing import Dict, Tuple, Type, Union import zlib +from typing import Dict, Tuple, Type, Union + from pymine_net.enums import GameState, PacketDirection from pymine_net.errors import UnknownPacketIdError from pymine_net.net.stream import AbstractTCPStream - from pymine_net.strict_abc import StrictABC, abstract from pymine_net.types.buffer import Buffer from pymine_net.types.packet import ClientBoundPacket, ServerBoundPacket diff --git a/pymine_net/net/socket/tcp/stream.py b/pymine_net/net/socket/tcp/stream.py index 26799a4..d9a12d6 100644 --- a/pymine_net/net/socket/tcp/stream.py +++ b/pymine_net/net/socket/tcp/stream.py @@ -3,8 +3,8 @@ from typing import Tuple, Union from cryptography.hazmat.primitives.ciphers import Cipher -from pymine_net.net.stream import AbstractTCPStream +from pymine_net.net.stream import AbstractTCPStream from pymine_net.types.buffer import Buffer diff --git a/pymine_net/net/stream.py b/pymine_net/net/stream.py index c00bf65..32d18dd 100644 --- a/pymine_net/net/stream.py +++ b/pymine_net/net/stream.py @@ -1,6 +1,7 @@ from re import L from typing import Tuple + from pymine_net.strict_abc import abstract From 0aeec03dad8c843fea968eacbbcc0c2499d7084c Mon Sep 17 00:00:00 2001 From: Milo Weinberg Date: Tue, 22 Feb 2022 12:52:18 -0500 Subject: [PATCH 20/34] minor changes + add __all__ to some files --- pymine_net/net/asyncio/tcp/client.py | 4 +++- pymine_net/net/asyncio/tcp/server.py | 3 ++- pymine_net/net/asyncio/tcp/stream.py | 4 +++- pymine_net/net/socket/tcp/client.py | 3 ++- pymine_net/net/socket/tcp/stream.py | 2 ++ 5 files changed, 12 insertions(+), 4 deletions(-) diff --git a/pymine_net/net/asyncio/tcp/client.py b/pymine_net/net/asyncio/tcp/client.py index 9791160..929e188 100644 --- a/pymine_net/net/asyncio/tcp/client.py +++ b/pymine_net/net/asyncio/tcp/client.py @@ -1,5 +1,4 @@ import asyncio -import struct from typing import Union from pymine_net.net.asyncio.tcp.stream import AsyncTCPStream @@ -7,6 +6,9 @@ from pymine_net.types.packet import ClientBoundPacket, ServerBoundPacket +__all__ = ("AsyncTCPClient",) + + class AsyncTCPClient(AbstractTCPClient): """An async connection over a TCP socket for reading + writing Minecraft packets.""" diff --git a/pymine_net/net/asyncio/tcp/server.py b/pymine_net/net/asyncio/tcp/server.py index 03f95fa..86e85fa 100644 --- a/pymine_net/net/asyncio/tcp/server.py +++ b/pymine_net/net/asyncio/tcp/server.py @@ -1,4 +1,3 @@ - import asyncio from typing import Dict, Tuple, Union @@ -8,6 +7,8 @@ from pymine_net.types.packet import ClientBoundPacket, ServerBoundPacket from pymine_net.types.packet_map import PacketMap +__all__ = ("AsyncTCPServerClient", "AsyncTCPServer") + class AsyncTCPServerClient(AbstractTCPServerClient): __slots__ = ("stream", "state", "compression_threshold") diff --git a/pymine_net/net/asyncio/tcp/stream.py b/pymine_net/net/asyncio/tcp/stream.py index 461e9a0..3b7ab40 100644 --- a/pymine_net/net/asyncio/tcp/stream.py +++ b/pymine_net/net/asyncio/tcp/stream.py @@ -7,6 +7,8 @@ from pymine_net.net.stream import AbstractTCPStream from pymine_net.types.buffer import Buffer +__all__ = ("AsyncTCPStream", "EncryptedAsyncTCPStream") + class AsyncTCPStream(AbstractTCPStream, StreamWriter): """Used for reading and writing from/to a connected client, merges functions of a StreamReader and StreamWriter. @@ -57,7 +59,7 @@ async def read_varint(self) -> int: return value -class AsyncEncryptedTCPStream(AsyncTCPStream): +class EncryptedAsyncTCPStream(AsyncTCPStream): """An encrypted version of an AsyncTCPStream, automatically encrypts and decrypts outgoing and incoming data. :param AsyncTCPStream stream: The original, stream-compatible object. diff --git a/pymine_net/net/socket/tcp/client.py b/pymine_net/net/socket/tcp/client.py index 6d2a23e..d9a4fd9 100644 --- a/pymine_net/net/socket/tcp/client.py +++ b/pymine_net/net/socket/tcp/client.py @@ -1,11 +1,12 @@ import socket -import struct from typing import Union from pymine_net.net.client import AbstractTCPClient from pymine_net.net.socket.tcp.stream import SocketTCPStream from pymine_net.types.packet import ClientBoundPacket, ServerBoundPacket +__all__ = ("SocketTCPClient",) + class SocketTCPClient(AbstractTCPClient): """A connection over a TCP socket for reading + writing Minecraft packets.""" diff --git a/pymine_net/net/socket/tcp/stream.py b/pymine_net/net/socket/tcp/stream.py index d9a12d6..f15dd5e 100644 --- a/pymine_net/net/socket/tcp/stream.py +++ b/pymine_net/net/socket/tcp/stream.py @@ -7,6 +7,8 @@ from pymine_net.net.stream import AbstractTCPStream from pymine_net.types.buffer import Buffer +__all__ = ("SocketTCPStream", "EncryptedSocketTCPStream") + class SocketTCPStream(AbstractTCPStream, socket.socket): """Used for reading and writing from/to a connected client, wraps a socket.socket. From 162e0ce95b3350e47eaf4488830682e5dbd94534 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 22 Feb 2022 17:52:53 +0000 Subject: [PATCH 21/34] apply import sorting --- pymine_net/net/asyncio/tcp/client.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pymine_net/net/asyncio/tcp/client.py b/pymine_net/net/asyncio/tcp/client.py index 929e188..ec4be5e 100644 --- a/pymine_net/net/asyncio/tcp/client.py +++ b/pymine_net/net/asyncio/tcp/client.py @@ -5,7 +5,6 @@ from pymine_net.net.client import AbstractTCPClient from pymine_net.types.packet import ClientBoundPacket, ServerBoundPacket - __all__ = ("AsyncTCPClient",) From 1e5cc8752e05198f28bdff665b325d2df3d8b28f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 22 Feb 2022 18:45:13 +0000 Subject: [PATCH 22/34] apply black & isort --- pymine_net/net/asyncio/tcp/server.py | 6 ++++-- pymine_net/net/client.py | 8 +++++--- pymine_net/net/server.py | 8 +++++--- pymine_net/net/socket/tcp/stream.py | 2 +- pymine_net/net/stream.py | 1 - 5 files changed, 15 insertions(+), 10 deletions(-) diff --git a/pymine_net/net/asyncio/tcp/server.py b/pymine_net/net/asyncio/tcp/server.py index 86e85fa..5cab2fb 100644 --- a/pymine_net/net/asyncio/tcp/server.py +++ b/pymine_net/net/asyncio/tcp/server.py @@ -36,12 +36,14 @@ async def stop(self) -> None: async def read_packet(self, client: AsyncTCPServerClient) -> ServerBoundPacket: length = await client.stream.read_varint() return self._decode_packet(client, await client.stream.readexactly(length)) - + async def write_packet(self, client: AsyncTCPServerClient, packet: ClientBoundPacket) -> None: client.stream.write(self._encode_packet(packet, client.compression_threshold)) await client.stream.drain() - async def _client_connected_cb(self, _: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None: + async def _client_connected_cb( + self, _: asyncio.StreamReader, writer: asyncio.StreamWriter + ) -> None: client = AsyncTCPServerClient(AsyncTCPStream(writer)) self.connected_clients[client.stream.remote] = client diff --git a/pymine_net/net/client.py b/pymine_net/net/client.py index a826a42..13d0bfd 100644 --- a/pymine_net/net/client.py +++ b/pymine_net/net/client.py @@ -17,7 +17,7 @@ def __init__(self, host: str, port: int, protocol: Union[int, str]): self.port = port self.packet_map = load_packet_map(protocol) - + self.state = GameState.HANDSHAKING self.compression_threshold = -1 @@ -28,7 +28,7 @@ def connect(self) -> None: @abstract def close(self) -> None: pass - + @staticmethod def _encode_packet(packet: ServerBoundPacket, compression_threshold: int = -1) -> Buffer: """Encodes and (if necessary) compresses a ServerBoundPacket.""" @@ -55,7 +55,9 @@ def _decode_packet(self, buf: Buffer) -> ClientBoundPacket: # attempt to get packet class from given state and packet id try: - packet_class: Type[ClientBoundPacket] = self.packet_map[PacketDirection.CLIENTBOUND, self.state, packet_id] + packet_class: Type[ClientBoundPacket] = self.packet_map[ + PacketDirection.CLIENTBOUND, self.state, packet_id + ] except KeyError: raise UnknownPacketIdError(None, self.state, packet_id, PacketDirection.CLIENTBOUND) diff --git a/pymine_net/net/server.py b/pymine_net/net/server.py index 511112c..870e849 100644 --- a/pymine_net/net/server.py +++ b/pymine_net/net/server.py @@ -29,7 +29,7 @@ def __init__(self, host: str, port: int, protocol: Union[int, str], packet_map: self.packet_map = packet_map self.connected_clients: Dict[Tuple[str, int], AbstractTCPServerClient] = {} - + @abstract def run(self) -> None: pass @@ -37,7 +37,7 @@ def run(self) -> None: @abstract def close(self) -> None: pass - + @staticmethod def _encode_packet(packet: ClientBoundPacket, compression_threshold: int = -1) -> Buffer: """Encodes and (if necessary) compresses a ClientBoundPacket.""" @@ -64,7 +64,9 @@ def _decode_packet(self, client: AbstractTCPServerClient, buf: Buffer) -> Server # attempt to get packet class from given state and packet id try: - packet_class: Type[ClientBoundPacket] = self.packet_map[PacketDirection.SERVERBOUND, client.state, packet_id] + packet_class: Type[ClientBoundPacket] = self.packet_map[ + PacketDirection.SERVERBOUND, client.state, packet_id + ] except KeyError: raise UnknownPacketIdError(None, client.state, packet_id, PacketDirection.SERVERBOUND) diff --git a/pymine_net/net/socket/tcp/stream.py b/pymine_net/net/socket/tcp/stream.py index f15dd5e..33f0bc4 100644 --- a/pymine_net/net/socket/tcp/stream.py +++ b/pymine_net/net/socket/tcp/stream.py @@ -22,7 +22,7 @@ class SocketTCPStream(AbstractTCPStream, socket.socket): def __init__(self, sock: socket.socket): self.sock = sock - + self.remote: Tuple[str, int] = sock.getsockname() def read(self, length: int) -> bytearray: diff --git a/pymine_net/net/stream.py b/pymine_net/net/stream.py index 32d18dd..41a1409 100644 --- a/pymine_net/net/stream.py +++ b/pymine_net/net/stream.py @@ -1,4 +1,3 @@ - from re import L from typing import Tuple From d334fbd831543c4ae1a4ffe67f0177e48dda1611 Mon Sep 17 00:00:00 2001 From: Milo Weinberg Date: Tue, 22 Feb 2022 14:25:18 -0500 Subject: [PATCH 23/34] add packets in player_list.py and particle.py + cleanup some imports --- pymine_net/packets/757/play/map.py | 2 +- pymine_net/packets/757/play/particle.py | 77 ++++++++++++++++++++++ pymine_net/packets/757/play/player_list.py | 29 ++++++++ tests/test_buffer.py | 1 - 4 files changed, 107 insertions(+), 2 deletions(-) create mode 100644 pymine_net/packets/757/play/particle.py create mode 100644 pymine_net/packets/757/play/player_list.py diff --git a/pymine_net/packets/757/play/map.py b/pymine_net/packets/757/play/map.py index 20a836e..89d5565 100644 --- a/pymine_net/packets/757/play/map.py +++ b/pymine_net/packets/757/play/map.py @@ -6,7 +6,7 @@ from pymine_net.types.buffer import Buffer from pymine_net.types.chat import Chat -from pymine_net.types.packet import ClientBoundPacket, ServerBoundPacket +from pymine_net.types.packet import ClientBoundPacket __all__ = ("PlayMapData",) diff --git a/pymine_net/packets/757/play/particle.py b/pymine_net/packets/757/play/particle.py new file mode 100644 index 0000000..0e7d592 --- /dev/null +++ b/pymine_net/packets/757/play/particle.py @@ -0,0 +1,77 @@ +"""Contains packets that are related to particles.""" + +from __future__ import annotations + +from pymine_net.types.buffer import Buffer +from pymine_net.types.packet import ClientBoundPacket + +__all__ = ("PlayParticle",) + + +class PlayParticle(ClientBoundPacket): + """Sent by server to make the client display particles. + + :param int particle_id: ID of the particle. + :param bool long_distance: If true, particle distance increases to 65536 from 256. + :param int x: X coordinate of the particle. + :param int y: Y coordinate of the particle. + :param int z: Z coordinate of the particle. + :param float particle_data: Particle data. + :param int particle_count: How many particles to display. + :param dict data: More particle data. + :ivar int id: Unique packet ID. + :ivar particle_id: + :ivar long_distance: + :ivar x: + :ivar y: + :ivar z: + :ivar particle_data: + :ivar particle_count: + :ivar data: + """ + + id = 0x24 + + def __init__( + self, + particle_id: int, + long_distance: bool, + x: float, + y: float, + z: float, + offset_x: float, + offset_y: float, + offset_z: float, + particle_data: float, + particle_count: int, + data: dict, + ): + super().__init__() + + self.part_id = particle_id + self.long_dist = long_distance + self.x = x + self.y = y + self.z = z + self.offset_x = offset_x + self.offset_y = offset_y + self.offset_z = offset_z + self.particle_data = particle_data + self.particle_count = particle_count + self.data = data + + def pack(self) -> Buffer: + return ( + Buffer() + .write("i", self.part_id) + .write("?", self.long_dist) + .write("d", self.x) + .write("d", self.y) + .write("d", self.z) + .write("f", self.offset_x) + .write("f", self.offset_y) + .write("f", self.offset_z) + .write("f", self.particle_data) + .write("i", self.particle_count) + .write_particle(self.data) + ) diff --git a/pymine_net/packets/757/play/player_list.py b/pymine_net/packets/757/play/player_list.py new file mode 100644 index 0000000..b11a16d --- /dev/null +++ b/pymine_net/packets/757/play/player_list.py @@ -0,0 +1,29 @@ +from __future__ import annotations + +from pymine_net.types.packet import ClientBoundPacket +from pymine_net.types.buffer import Buffer +from pymine_net.types.chat import Chat + +__all__ = ("PlayPlayerListHeaderAndFooter",) + + +class PlayPlayerListHeaderAndFooter(ClientBoundPacket): + """Sent to display additional information above/below the client's player list. + + :param Chat header: Content to display above player list. + :param Chat footer: Content to display below player list. + :ivar int id: Unique packet ID. + :ivar header: + :ivar footer: + """ + + id = 0x5F + + def __init__(self, header: Chat, footer: Chat): + super().__init__() + + self.header = header + self.footer = footer + + def pack(self) -> Buffer: + return Buffer().write_chat(self.header).write_chat(self.footer) diff --git a/tests/test_buffer.py b/tests/test_buffer.py index 95b4d19..fdfd9ef 100644 --- a/tests/test_buffer.py +++ b/tests/test_buffer.py @@ -7,7 +7,6 @@ # fix path sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) -import pymine_net.types.nbt as nbt from pymine_net.types.buffer import Buffer VAR_INT_ERR_MSG = "Value doesn't fit in given range" From a5c3bf6a2c8c7c4ae452b9f4d8894657dceec7de Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 22 Feb 2022 19:26:11 +0000 Subject: [PATCH 24/34] apply black & isort --- pymine_net/packets/757/play/particle.py | 2 +- pymine_net/packets/757/play/player_list.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pymine_net/packets/757/play/particle.py b/pymine_net/packets/757/play/particle.py index 0e7d592..6fdaab9 100644 --- a/pymine_net/packets/757/play/particle.py +++ b/pymine_net/packets/757/play/particle.py @@ -10,7 +10,7 @@ class PlayParticle(ClientBoundPacket): """Sent by server to make the client display particles. - + :param int particle_id: ID of the particle. :param bool long_distance: If true, particle distance increases to 65536 from 256. :param int x: X coordinate of the particle. diff --git a/pymine_net/packets/757/play/player_list.py b/pymine_net/packets/757/play/player_list.py index b11a16d..861fea6 100644 --- a/pymine_net/packets/757/play/player_list.py +++ b/pymine_net/packets/757/play/player_list.py @@ -1,15 +1,15 @@ from __future__ import annotations -from pymine_net.types.packet import ClientBoundPacket from pymine_net.types.buffer import Buffer from pymine_net.types.chat import Chat +from pymine_net.types.packet import ClientBoundPacket __all__ = ("PlayPlayerListHeaderAndFooter",) class PlayPlayerListHeaderAndFooter(ClientBoundPacket): """Sent to display additional information above/below the client's player list. - + :param Chat header: Content to display above player list. :param Chat footer: Content to display below player list. :ivar int id: Unique packet ID. From 3660b70e1902aefdd9d58dd0f54e56be4798999a Mon Sep 17 00:00:00 2001 From: Milo Weinberg Date: Tue, 22 Feb 2022 14:32:03 -0500 Subject: [PATCH 25/34] add missing direction in docstrings --- pymine_net/packets/757/play/particle.py | 2 +- pymine_net/packets/757/play/player_list.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pymine_net/packets/757/play/particle.py b/pymine_net/packets/757/play/particle.py index 6fdaab9..e5aa490 100644 --- a/pymine_net/packets/757/play/particle.py +++ b/pymine_net/packets/757/play/particle.py @@ -9,7 +9,7 @@ class PlayParticle(ClientBoundPacket): - """Sent by server to make the client display particles. + """Sent by server to make the client display particles. (Server -> Client) :param int particle_id: ID of the particle. :param bool long_distance: If true, particle distance increases to 65536 from 256. diff --git a/pymine_net/packets/757/play/player_list.py b/pymine_net/packets/757/play/player_list.py index 861fea6..e926ecf 100644 --- a/pymine_net/packets/757/play/player_list.py +++ b/pymine_net/packets/757/play/player_list.py @@ -8,7 +8,7 @@ class PlayPlayerListHeaderAndFooter(ClientBoundPacket): - """Sent to display additional information above/below the client's player list. + """Sent to display additional information above/below the client's player list. (Server -> Client) :param Chat header: Content to display above player list. :param Chat footer: Content to display below player list. From e5170006a457bfac58c78ed23985be1eacfe54c4 Mon Sep 17 00:00:00 2001 From: Milo Weinberg Date: Tue, 22 Feb 2022 15:43:25 -0500 Subject: [PATCH 26/34] start on SocketTCPServer --- pymine_net/net/server.py | 8 +++++++ pymine_net/net/socket/tcp/server.py | 37 +++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 pymine_net/net/socket/tcp/server.py diff --git a/pymine_net/net/server.py b/pymine_net/net/server.py index 511112c..e23040a 100644 --- a/pymine_net/net/server.py +++ b/pymine_net/net/server.py @@ -69,3 +69,11 @@ def _decode_packet(self, client: AbstractTCPServerClient, buf: Buffer) -> Server raise UnknownPacketIdError(None, client.state, packet_id, PacketDirection.SERVERBOUND) return packet_class.unpack(buf) + + @abstract + def read_packet(self, client: AbstractTCPServerClient) -> ServerBoundPacket: + pass + + @abstract + def write_packet(self, client: AbstractTCPServerClient, packet: ClientBoundPacket) -> None: + pass diff --git a/pymine_net/net/socket/tcp/server.py b/pymine_net/net/socket/tcp/server.py new file mode 100644 index 0000000..7b5e5e6 --- /dev/null +++ b/pymine_net/net/socket/tcp/server.py @@ -0,0 +1,37 @@ +from typing import Dict, Tuple, Union +import socket + +from pymine_net.net.server import AbstractTCPServer, AbstractTCPServerClient +from pymine_net.net.socket.tcp.stream import SocketTCPStream +from pymine_net.types.packet import ClientBoundPacket, ServerBoundPacket +from pymine_net.types.packet_map import PacketMap + + +class SocketTCPServerClient(AbstractTCPServerClient): + __slots__ = ("stream", "state", "compression_threshold") + + def __init__(self, stream: SocketTCPStream): + super().__init__(stream) + self.stream = stream + + +class SocketTCPServer(AbstractTCPServer): + def __init__(self, host: str, port: int, protocol: Union[int, str], packet_map: PacketMap): + super().__init__(host, port, protocol, packet_map) + + self.connected_clients: Dict[Tuple[str, int], SocketTCPServerClient] = {} + + self.sock: socket.socket = None + + async def run(self) -> None: + self.server = socket.create_server((self.host, self.port)) + + async def stop(self) -> None: + self.server.close() + + def read_packet(self, client: SocketTCPServerClient) -> ServerBoundPacket: + length = client.stream.read_varint() + return self._decode_packet(client, client.stream.read(length)) + + async def write_packet(self, client: SocketTCPServerClient, packet: ClientBoundPacket) -> None: + client.stream.write(self._encode_packet(packet, client.compression_threshold)) From 8dcc92968fa705556b8eb1bd1638ebd57e75a033 Mon Sep 17 00:00:00 2001 From: Milo Weinberg Date: Tue, 22 Feb 2022 16:16:23 -0500 Subject: [PATCH 27/34] finish SocketTCPServer --- pymine_net/net/socket/tcp/server.py | 35 +++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/pymine_net/net/socket/tcp/server.py b/pymine_net/net/socket/tcp/server.py index 7b5e5e6..e3b900b 100644 --- a/pymine_net/net/socket/tcp/server.py +++ b/pymine_net/net/socket/tcp/server.py @@ -1,8 +1,10 @@ from typing import Dict, Tuple, Union +import selectors import socket from pymine_net.net.server import AbstractTCPServer, AbstractTCPServerClient from pymine_net.net.socket.tcp.stream import SocketTCPStream +from pymine_net.strict_abc import abstract from pymine_net.types.packet import ClientBoundPacket, ServerBoundPacket from pymine_net.types.packet_map import PacketMap @@ -22,9 +24,24 @@ def __init__(self, host: str, port: int, protocol: Union[int, str], packet_map: self.connected_clients: Dict[Tuple[str, int], SocketTCPServerClient] = {} self.sock: socket.socket = None + self.selector = selectors.DefaultSelector() + self.running = False async def run(self) -> None: - self.server = socket.create_server((self.host, self.port)) + self.sock = socket.socket() + self.sock.bind((self.host, self.port)) + self.sock.listen(100) + self.sock.setblocking(False) + + self.selector.register(self.sock, selectors.EVENT_READ, self._client_connected_cb) + + while self.running: + for key, mask in self.selector.select(): + if not self.running: + break + + key.data(key.fileobj, mask) + async def stop(self) -> None: self.server.close() @@ -33,5 +50,19 @@ def read_packet(self, client: SocketTCPServerClient) -> ServerBoundPacket: length = client.stream.read_varint() return self._decode_packet(client, client.stream.read(length)) - async def write_packet(self, client: SocketTCPServerClient, packet: ClientBoundPacket) -> None: + def write_packet(self, client: SocketTCPServerClient, packet: ClientBoundPacket) -> None: client.stream.write(self._encode_packet(packet, client.compression_threshold)) + + def _client_connected_cb(self, sock: socket.socket, mask: selectors._EventMask) -> None: + connection, address = sock.accept() + connection.setblocking(False) + self.selector.register(connection, selectors.EVENT_READ, self._connection_update_cb) + self.connected_clients[address] = SocketTCPServerClient(SocketTCPStream(connection)) + + def _connection_update_cb(self, connection: socket.socket, mask: selectors._EventMask) -> None: + client = self.connected_clients[connection.getpeername()] + self.connection_update(client, self.read_packet(client)) + + @abstract + def incoming_packet(self, client: SocketTCPServerClient, packet: ServerBoundPacket) -> None: + pass From 6e61c41f604ed7c0e4ef71605bec3389094fc852 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 22 Feb 2022 21:16:51 +0000 Subject: [PATCH 28/34] apply black & isort --- pymine_net/net/server.py | 2 +- pymine_net/net/socket/tcp/server.py | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/pymine_net/net/server.py b/pymine_net/net/server.py index 7747800..1b7028b 100644 --- a/pymine_net/net/server.py +++ b/pymine_net/net/server.py @@ -71,7 +71,7 @@ def _decode_packet(self, client: AbstractTCPServerClient, buf: Buffer) -> Server raise UnknownPacketIdError(None, client.state, packet_id, PacketDirection.SERVERBOUND) return packet_class.unpack(buf) - + @abstract def read_packet(self, client: AbstractTCPServerClient) -> ServerBoundPacket: pass diff --git a/pymine_net/net/socket/tcp/server.py b/pymine_net/net/socket/tcp/server.py index e3b900b..5c64a70 100644 --- a/pymine_net/net/socket/tcp/server.py +++ b/pymine_net/net/socket/tcp/server.py @@ -1,6 +1,6 @@ -from typing import Dict, Tuple, Union import selectors import socket +from typing import Dict, Tuple, Union from pymine_net.net.server import AbstractTCPServer, AbstractTCPServerClient from pymine_net.net.socket.tcp.stream import SocketTCPStream @@ -42,14 +42,13 @@ async def run(self) -> None: key.data(key.fileobj, mask) - async def stop(self) -> None: self.server.close() def read_packet(self, client: SocketTCPServerClient) -> ServerBoundPacket: length = client.stream.read_varint() return self._decode_packet(client, client.stream.read(length)) - + def write_packet(self, client: SocketTCPServerClient, packet: ClientBoundPacket) -> None: client.stream.write(self._encode_packet(packet, client.compression_threshold)) From 5bb926089d445c9f96a5079e39bde0c12a8d12ae Mon Sep 17 00:00:00 2001 From: Milo Weinberg Date: Tue, 22 Feb 2022 16:24:02 -0500 Subject: [PATCH 29/34] rename folder 757 to v_1_18_1 --- pymine_net/packets/__init__.py | 5 +++++ .../packets/{757 => v_1_18_1}/handshaking/handshake.py | 0 pymine_net/packets/{757 => v_1_18_1}/login/compression.py | 0 pymine_net/packets/{757 => v_1_18_1}/login/login.py | 0 pymine_net/packets/{757 => v_1_18_1}/play/advancement.py | 0 pymine_net/packets/{757 => v_1_18_1}/play/animations.py | 0 pymine_net/packets/{757 => v_1_18_1}/play/beacon.py | 0 pymine_net/packets/{757 => v_1_18_1}/play/block.py | 0 pymine_net/packets/{757 => v_1_18_1}/play/boss.py | 0 pymine_net/packets/{757 => v_1_18_1}/play/chat.py | 0 pymine_net/packets/{757 => v_1_18_1}/play/command.py | 0 pymine_net/packets/{757 => v_1_18_1}/play/command_block.py | 0 pymine_net/packets/{757 => v_1_18_1}/play/cooldown.py | 0 pymine_net/packets/{757 => v_1_18_1}/play/crafting.py | 0 pymine_net/packets/{757 => v_1_18_1}/play/difficulty.py | 0 pymine_net/packets/{757 => v_1_18_1}/play/effect.py | 0 pymine_net/packets/{757 => v_1_18_1}/play/entity.py | 0 pymine_net/packets/{757 => v_1_18_1}/play/explosion.py | 0 pymine_net/packets/{757 => v_1_18_1}/play/keep_alive.py | 0 pymine_net/packets/{757 => v_1_18_1}/play/map.py | 0 pymine_net/packets/{757 => v_1_18_1}/play/particle.py | 0 pymine_net/packets/{757 => v_1_18_1}/play/player_list.py | 0 pymine_net/packets/{757 => v_1_18_1}/status/status.py | 0 23 files changed, 5 insertions(+) rename pymine_net/packets/{757 => v_1_18_1}/handshaking/handshake.py (100%) rename pymine_net/packets/{757 => v_1_18_1}/login/compression.py (100%) rename pymine_net/packets/{757 => v_1_18_1}/login/login.py (100%) rename pymine_net/packets/{757 => v_1_18_1}/play/advancement.py (100%) rename pymine_net/packets/{757 => v_1_18_1}/play/animations.py (100%) rename pymine_net/packets/{757 => v_1_18_1}/play/beacon.py (100%) rename pymine_net/packets/{757 => v_1_18_1}/play/block.py (100%) rename pymine_net/packets/{757 => v_1_18_1}/play/boss.py (100%) rename pymine_net/packets/{757 => v_1_18_1}/play/chat.py (100%) rename pymine_net/packets/{757 => v_1_18_1}/play/command.py (100%) rename pymine_net/packets/{757 => v_1_18_1}/play/command_block.py (100%) rename pymine_net/packets/{757 => v_1_18_1}/play/cooldown.py (100%) rename pymine_net/packets/{757 => v_1_18_1}/play/crafting.py (100%) rename pymine_net/packets/{757 => v_1_18_1}/play/difficulty.py (100%) rename pymine_net/packets/{757 => v_1_18_1}/play/effect.py (100%) rename pymine_net/packets/{757 => v_1_18_1}/play/entity.py (100%) rename pymine_net/packets/{757 => v_1_18_1}/play/explosion.py (100%) rename pymine_net/packets/{757 => v_1_18_1}/play/keep_alive.py (100%) rename pymine_net/packets/{757 => v_1_18_1}/play/map.py (100%) rename pymine_net/packets/{757 => v_1_18_1}/play/particle.py (100%) rename pymine_net/packets/{757 => v_1_18_1}/play/player_list.py (100%) rename pymine_net/packets/{757 => v_1_18_1}/status/status.py (100%) diff --git a/pymine_net/packets/__init__.py b/pymine_net/packets/__init__.py index a1a160e..265ae9b 100644 --- a/pymine_net/packets/__init__.py +++ b/pymine_net/packets/__init__.py @@ -19,8 +19,13 @@ # the directory this file is contained in FILE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) +PROTOCOL_MAP = { + 757: "v_1_18_1" +} + def load_packet_map(protocol: Union[int, str], *, debug: bool = False) -> PacketMap: + protocol = PROTOCOL_MAP.get(protocol, protocol) packets: Dict[GameState, StatePacketMap] = {} for state, state_name in GAME_STATES.items(): diff --git a/pymine_net/packets/757/handshaking/handshake.py b/pymine_net/packets/v_1_18_1/handshaking/handshake.py similarity index 100% rename from pymine_net/packets/757/handshaking/handshake.py rename to pymine_net/packets/v_1_18_1/handshaking/handshake.py diff --git a/pymine_net/packets/757/login/compression.py b/pymine_net/packets/v_1_18_1/login/compression.py similarity index 100% rename from pymine_net/packets/757/login/compression.py rename to pymine_net/packets/v_1_18_1/login/compression.py diff --git a/pymine_net/packets/757/login/login.py b/pymine_net/packets/v_1_18_1/login/login.py similarity index 100% rename from pymine_net/packets/757/login/login.py rename to pymine_net/packets/v_1_18_1/login/login.py diff --git a/pymine_net/packets/757/play/advancement.py b/pymine_net/packets/v_1_18_1/play/advancement.py similarity index 100% rename from pymine_net/packets/757/play/advancement.py rename to pymine_net/packets/v_1_18_1/play/advancement.py diff --git a/pymine_net/packets/757/play/animations.py b/pymine_net/packets/v_1_18_1/play/animations.py similarity index 100% rename from pymine_net/packets/757/play/animations.py rename to pymine_net/packets/v_1_18_1/play/animations.py diff --git a/pymine_net/packets/757/play/beacon.py b/pymine_net/packets/v_1_18_1/play/beacon.py similarity index 100% rename from pymine_net/packets/757/play/beacon.py rename to pymine_net/packets/v_1_18_1/play/beacon.py diff --git a/pymine_net/packets/757/play/block.py b/pymine_net/packets/v_1_18_1/play/block.py similarity index 100% rename from pymine_net/packets/757/play/block.py rename to pymine_net/packets/v_1_18_1/play/block.py diff --git a/pymine_net/packets/757/play/boss.py b/pymine_net/packets/v_1_18_1/play/boss.py similarity index 100% rename from pymine_net/packets/757/play/boss.py rename to pymine_net/packets/v_1_18_1/play/boss.py diff --git a/pymine_net/packets/757/play/chat.py b/pymine_net/packets/v_1_18_1/play/chat.py similarity index 100% rename from pymine_net/packets/757/play/chat.py rename to pymine_net/packets/v_1_18_1/play/chat.py diff --git a/pymine_net/packets/757/play/command.py b/pymine_net/packets/v_1_18_1/play/command.py similarity index 100% rename from pymine_net/packets/757/play/command.py rename to pymine_net/packets/v_1_18_1/play/command.py diff --git a/pymine_net/packets/757/play/command_block.py b/pymine_net/packets/v_1_18_1/play/command_block.py similarity index 100% rename from pymine_net/packets/757/play/command_block.py rename to pymine_net/packets/v_1_18_1/play/command_block.py diff --git a/pymine_net/packets/757/play/cooldown.py b/pymine_net/packets/v_1_18_1/play/cooldown.py similarity index 100% rename from pymine_net/packets/757/play/cooldown.py rename to pymine_net/packets/v_1_18_1/play/cooldown.py diff --git a/pymine_net/packets/757/play/crafting.py b/pymine_net/packets/v_1_18_1/play/crafting.py similarity index 100% rename from pymine_net/packets/757/play/crafting.py rename to pymine_net/packets/v_1_18_1/play/crafting.py diff --git a/pymine_net/packets/757/play/difficulty.py b/pymine_net/packets/v_1_18_1/play/difficulty.py similarity index 100% rename from pymine_net/packets/757/play/difficulty.py rename to pymine_net/packets/v_1_18_1/play/difficulty.py diff --git a/pymine_net/packets/757/play/effect.py b/pymine_net/packets/v_1_18_1/play/effect.py similarity index 100% rename from pymine_net/packets/757/play/effect.py rename to pymine_net/packets/v_1_18_1/play/effect.py diff --git a/pymine_net/packets/757/play/entity.py b/pymine_net/packets/v_1_18_1/play/entity.py similarity index 100% rename from pymine_net/packets/757/play/entity.py rename to pymine_net/packets/v_1_18_1/play/entity.py diff --git a/pymine_net/packets/757/play/explosion.py b/pymine_net/packets/v_1_18_1/play/explosion.py similarity index 100% rename from pymine_net/packets/757/play/explosion.py rename to pymine_net/packets/v_1_18_1/play/explosion.py diff --git a/pymine_net/packets/757/play/keep_alive.py b/pymine_net/packets/v_1_18_1/play/keep_alive.py similarity index 100% rename from pymine_net/packets/757/play/keep_alive.py rename to pymine_net/packets/v_1_18_1/play/keep_alive.py diff --git a/pymine_net/packets/757/play/map.py b/pymine_net/packets/v_1_18_1/play/map.py similarity index 100% rename from pymine_net/packets/757/play/map.py rename to pymine_net/packets/v_1_18_1/play/map.py diff --git a/pymine_net/packets/757/play/particle.py b/pymine_net/packets/v_1_18_1/play/particle.py similarity index 100% rename from pymine_net/packets/757/play/particle.py rename to pymine_net/packets/v_1_18_1/play/particle.py diff --git a/pymine_net/packets/757/play/player_list.py b/pymine_net/packets/v_1_18_1/play/player_list.py similarity index 100% rename from pymine_net/packets/757/play/player_list.py rename to pymine_net/packets/v_1_18_1/play/player_list.py diff --git a/pymine_net/packets/757/status/status.py b/pymine_net/packets/v_1_18_1/status/status.py similarity index 100% rename from pymine_net/packets/757/status/status.py rename to pymine_net/packets/v_1_18_1/status/status.py From 0c914b2e0689c028aba7c323317b5476b793ca93 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 22 Feb 2022 21:24:30 +0000 Subject: [PATCH 30/34] apply black & isort --- pymine_net/packets/__init__.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pymine_net/packets/__init__.py b/pymine_net/packets/__init__.py index 265ae9b..6905d3d 100644 --- a/pymine_net/packets/__init__.py +++ b/pymine_net/packets/__init__.py @@ -19,9 +19,7 @@ # the directory this file is contained in FILE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) -PROTOCOL_MAP = { - 757: "v_1_18_1" -} +PROTOCOL_MAP = {757: "v_1_18_1"} def load_packet_map(protocol: Union[int, str], *, debug: bool = False) -> PacketMap: From 89326eda7e5c1f2781a3f61467dfea3eb2abf98b Mon Sep 17 00:00:00 2001 From: Milo Weinberg Date: Tue, 22 Feb 2022 21:43:57 -0500 Subject: [PATCH 31/34] restructuring + organization changes + add tests for networking functionality --- poetry.lock | 21 ++++- pymine_net/__init__.py | 2 +- pymine_net/net/asyncio/__init__.py | 3 + pymine_net/net/asyncio/{tcp => }/client.py | 7 +- pymine_net/net/asyncio/server.py | 53 +++++++++++++ pymine_net/net/asyncio/{tcp => }/stream.py | 7 +- pymine_net/net/asyncio/tcp/server.py | 55 ------------- pymine_net/net/client.py | 7 +- pymine_net/net/server.py | 69 ++++++++-------- pymine_net/net/socket/__init__.py | 3 + pymine_net/net/socket/{tcp => }/client.py | 7 +- pymine_net/net/socket/server.py | 65 +++++++++++++++ pymine_net/net/socket/{tcp => }/stream.py | 8 +- pymine_net/net/socket/tcp/server.py | 67 ---------------- pymine_net/net/stream.py | 4 - pymine_net/types/buffer.py | 20 +++-- pyproject.toml | 3 +- tests/test_net_asyncio.py | 92 ++++++++++++++++++++++ tests/test_net_socket.py | 88 +++++++++++++++++++++ 19 files changed, 397 insertions(+), 184 deletions(-) create mode 100644 pymine_net/net/asyncio/__init__.py rename pymine_net/net/asyncio/{tcp => }/client.py (84%) create mode 100644 pymine_net/net/asyncio/server.py rename pymine_net/net/asyncio/{tcp => }/stream.py (96%) delete mode 100644 pymine_net/net/asyncio/tcp/server.py create mode 100644 pymine_net/net/socket/__init__.py rename pymine_net/net/socket/{tcp => }/client.py (83%) create mode 100644 pymine_net/net/socket/server.py rename pymine_net/net/socket/{tcp => }/stream.py (94%) delete mode 100644 pymine_net/net/socket/tcp/server.py create mode 100644 tests/test_net_asyncio.py create mode 100644 tests/test_net_socket.py diff --git a/poetry.lock b/poetry.lock index 01bad45..60989c7 100644 --- a/poetry.lock +++ b/poetry.lock @@ -283,6 +283,21 @@ tomli = ">=1.0.0" [package.extras] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] +[[package]] +name = "pytest-asyncio" +version = "0.18.1" +description = "Pytest support for asyncio" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +pytest = ">=6.1.0" +typing-extensions = {version = ">=3.7.2", markers = "python_version < \"3.8\""} + +[package.extras] +testing = ["coverage (==6.2)", "hypothesis (>=5.7.1)", "flaky (>=3.5.0)", "mypy (==0.931)"] + [[package]] name = "tomli" version = "2.0.1" @@ -322,7 +337,7 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest- [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "82276c7b7e02a9ad60378a512547bbcb2cb213906e17e24a9211e8af5d64ceef" +content-hash = "3750687b24a352b625441ebaf1d20726351e3ebfc592fb386e4491455582ae49" [metadata.files] atomicwrites = [ @@ -519,6 +534,10 @@ pytest = [ {file = "pytest-7.0.1-py3-none-any.whl", hash = "sha256:9ce3ff477af913ecf6321fe337b93a2c0dcf2a0a1439c43f5452112c1e4280db"}, {file = "pytest-7.0.1.tar.gz", hash = "sha256:e30905a0c131d3d94b89624a1cc5afec3e0ba2fbdb151867d8e0ebd49850f171"}, ] +pytest-asyncio = [ + {file = "pytest-asyncio-0.18.1.tar.gz", hash = "sha256:c43fcdfea2335dd82ffe0f2774e40285ddfea78a8e81e56118d47b6a90fbb09e"}, + {file = "pytest_asyncio-0.18.1-py3-none-any.whl", hash = "sha256:c9ec48e8bbf5cc62755e18c4d8bc6907843ec9c5f4ac8f61464093baeba24a7e"}, +] tomli = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, diff --git a/pymine_net/__init__.py b/pymine_net/__init__.py index 83e65eb..8bb9256 100644 --- a/pymine_net/__init__.py +++ b/pymine_net/__init__.py @@ -1,5 +1,5 @@ import pymine_net.types.nbt as nbt # noqa: F401 -from pymine_net.net.asyncio.tcp.client import AsyncTCPClient +from pymine_net.net.asyncio.client import AsyncTCPClient from pymine_net.packets import load_packet_map # noqa: F401 from pymine_net.types.buffer import Buffer # noqa: F401 from pymine_net.types.packet import ClientBoundPacket, Packet, ServerBoundPacket # noqa: F401 diff --git a/pymine_net/net/asyncio/__init__.py b/pymine_net/net/asyncio/__init__.py new file mode 100644 index 0000000..18c8378 --- /dev/null +++ b/pymine_net/net/asyncio/__init__.py @@ -0,0 +1,3 @@ +from .stream import AsyncTCPStream, EncryptedAsyncTCPStream +from .client import AsyncTCPClient +from .server import AsyncProtocolServer, AsyncProtocolServerClient diff --git a/pymine_net/net/asyncio/tcp/client.py b/pymine_net/net/asyncio/client.py similarity index 84% rename from pymine_net/net/asyncio/tcp/client.py rename to pymine_net/net/asyncio/client.py index ec4be5e..78defee 100644 --- a/pymine_net/net/asyncio/tcp/client.py +++ b/pymine_net/net/asyncio/client.py @@ -1,9 +1,10 @@ import asyncio from typing import Union -from pymine_net.net.asyncio.tcp.stream import AsyncTCPStream +from pymine_net.net.asyncio.stream import AsyncTCPStream from pymine_net.net.client import AbstractTCPClient from pymine_net.types.packet import ClientBoundPacket, ServerBoundPacket +from pymine_net.types.packet_map import PacketMap __all__ = ("AsyncTCPClient",) @@ -11,8 +12,8 @@ class AsyncTCPClient(AbstractTCPClient): """An async connection over a TCP socket for reading + writing Minecraft packets.""" - def __init__(self, host: str, port: int, protocol: Union[int, str]): - super().__init__(host, port, protocol) + def __init__(self, host: str, port: int, protocol: Union[int, str], packet_map: PacketMap): + super().__init__(host, port, protocol, packet_map) self.stream: AsyncTCPStream = None diff --git a/pymine_net/net/asyncio/server.py b/pymine_net/net/asyncio/server.py new file mode 100644 index 0000000..e9f6e29 --- /dev/null +++ b/pymine_net/net/asyncio/server.py @@ -0,0 +1,53 @@ +import asyncio +from typing import Dict, Tuple, Union + +from pymine_net.net.asyncio.stream import AsyncTCPStream +from pymine_net.net.server import AbstractProtocolServer, AbstractProtocolServerClient +from pymine_net.strict_abc import abstract +from pymine_net.types.packet import ClientBoundPacket, ServerBoundPacket +from pymine_net.types.packet_map import PacketMap + +__all__ = ("AsyncProtocolServerClient", "AsyncProtocolServer") + + +class AsyncProtocolServerClient(AbstractProtocolServerClient): + def __init__(self, stream: AsyncTCPStream, packet_map: PacketMap): + super().__init__(stream, packet_map) + self.stream = stream # redefine this cause typehints + + async def read_packet(self) -> ServerBoundPacket: + length = await self.stream.read_varint() + return self._decode_packet(await self.stream.readexactly(length)) + + async def write_packet(self, packet: ClientBoundPacket) -> None: + self.stream.write(self._encode_packet(packet)) + await self.stream.drain() + + +class AsyncProtocolServer(AbstractProtocolServer): + def __init__(self, host: str, port: int, protocol: Union[int, str], packet_map: PacketMap): + super().__init__(host, port, protocol, packet_map) + + self.connected_clients: Dict[Tuple[str, int], AsyncProtocolServerClient] = {} + + self.server: asyncio.AbstractServer = None + + async def run(self) -> None: + self.server = await asyncio.start_server(self._client_connected_cb, self.host, self.port) + + async def close(self) -> None: + self.server.close() + await self.server.wait_closed() + + async def _client_connected_cb( + self, _: asyncio.StreamReader, writer: asyncio.StreamWriter + ) -> None: + client = AsyncProtocolServerClient(AsyncTCPStream(writer), self.packet_map) + + self.connected_clients[client.stream.remote] = client + + await self.new_client_connected(client) + + @abstract + async def new_client_connected(self, client: AsyncProtocolServerClient) -> None: + pass diff --git a/pymine_net/net/asyncio/tcp/stream.py b/pymine_net/net/asyncio/stream.py similarity index 96% rename from pymine_net/net/asyncio/tcp/stream.py rename to pymine_net/net/asyncio/stream.py index 3b7ab40..6b3b124 100644 --- a/pymine_net/net/asyncio/tcp/stream.py +++ b/pymine_net/net/asyncio/stream.py @@ -20,7 +20,7 @@ class AsyncTCPStream(AbstractTCPStream, StreamWriter): def __init__(self, writer: StreamWriter): super().__init__(writer._transport, writer._protocol, writer._reader, writer._loop) - + self.remote: Tuple[str, int] = self.get_extra_info("peername") async def read(self, length: int = -1) -> Buffer: @@ -35,11 +35,14 @@ async def readexactly(self, length: int) -> Buffer: async def readuntil(self, separator: bytes = b"\n") -> Buffer: return Buffer(await self._reader.readuntil(separator)) + def write(self, data): + super().write(data) + async def read_varint(self) -> int: value = 0 for i in range(10): - byte = struct.unpack(">B", await self.readexactly(1)) + byte, = struct.unpack(">B", await self.readexactly(1)) value |= (byte & 0x7F) << 7 * i if not byte & 0x80: diff --git a/pymine_net/net/asyncio/tcp/server.py b/pymine_net/net/asyncio/tcp/server.py deleted file mode 100644 index 5cab2fb..0000000 --- a/pymine_net/net/asyncio/tcp/server.py +++ /dev/null @@ -1,55 +0,0 @@ -import asyncio -from typing import Dict, Tuple, Union - -from pymine_net.net.asyncio.tcp.stream import AsyncTCPStream -from pymine_net.net.server import AbstractTCPServer, AbstractTCPServerClient -from pymine_net.strict_abc import abstract -from pymine_net.types.packet import ClientBoundPacket, ServerBoundPacket -from pymine_net.types.packet_map import PacketMap - -__all__ = ("AsyncTCPServerClient", "AsyncTCPServer") - - -class AsyncTCPServerClient(AbstractTCPServerClient): - __slots__ = ("stream", "state", "compression_threshold") - - def __init__(self, stream: AsyncTCPStream): - super().__init__(stream) - self.stream = stream - - -class AsyncTCPServer(AbstractTCPServer): - def __init__(self, host: str, port: int, protocol: Union[int, str], packet_map: PacketMap): - super().__init__(host, port, protocol, packet_map) - - self.connected_clients: Dict[Tuple[str, int], AsyncTCPServerClient] = {} - - self.server: asyncio.AbstractServer = None - - async def run(self) -> None: - self.server = await asyncio.start_server() - - async def stop(self) -> None: - self.server.close() - await self.server.wait_closed() - - async def read_packet(self, client: AsyncTCPServerClient) -> ServerBoundPacket: - length = await client.stream.read_varint() - return self._decode_packet(client, await client.stream.readexactly(length)) - - async def write_packet(self, client: AsyncTCPServerClient, packet: ClientBoundPacket) -> None: - client.stream.write(self._encode_packet(packet, client.compression_threshold)) - await client.stream.drain() - - async def _client_connected_cb( - self, _: asyncio.StreamReader, writer: asyncio.StreamWriter - ) -> None: - client = AsyncTCPServerClient(AsyncTCPStream(writer)) - - self.connected_clients[client.stream.remote] = client - - await self.new_client_connected(client) - - @abstract - async def new_client_connected(self, client: AsyncTCPServerClient) -> None: - pass diff --git a/pymine_net/net/client.py b/pymine_net/net/client.py index 13d0bfd..43022fc 100644 --- a/pymine_net/net/client.py +++ b/pymine_net/net/client.py @@ -7,16 +7,17 @@ from pymine_net.strict_abc import StrictABC, abstract from pymine_net.types.buffer import Buffer from pymine_net.types.packet import ClientBoundPacket, ServerBoundPacket +from pymine_net.types.packet_map import PacketMap class AbstractTCPClient(StrictABC): """Abstract class for a connection over a TCP socket for reading + writing Minecraft packets.""" - def __init__(self, host: str, port: int, protocol: Union[int, str]): + def __init__(self, host: str, port: int, protocol: Union[int, str], packet_map: PacketMap): self.host = host self.port = port - - self.packet_map = load_packet_map(protocol) + self.protocol = protocol + self.packet_map = packet_map self.state = GameState.HANDSHAKING self.compression_threshold = -1 diff --git a/pymine_net/net/server.py b/pymine_net/net/server.py index 1b7028b..88bf202 100644 --- a/pymine_net/net/server.py +++ b/pymine_net/net/server.py @@ -10,51 +10,32 @@ from pymine_net.types.packet_map import PacketMap -class AbstractTCPServerClient(StrictABC): - __slots__ = ("stream", "state", "compression_threshold") +class AbstractProtocolServerClient(StrictABC): + __slots__ = ("stream", "packet_map", "state", "compression_threshold") - def __init__(self, stream: AbstractTCPStream): + def __init__(self, stream: AbstractTCPStream, packet_map: PacketMap): self.stream = stream + self.packet_map = packet_map self.state = GameState.HANDSHAKING self.compression_threshold = -1 - -class AbstractTCPServer: - """Abstract class for a TCP server that handles Minecraft packets.""" - - def __init__(self, host: str, port: int, protocol: Union[int, str], packet_map: PacketMap): - self.host = host - self.port = port - self.protocol = protocol - self.packet_map = packet_map - - self.connected_clients: Dict[Tuple[str, int], AbstractTCPServerClient] = {} - - @abstract - def run(self) -> None: - pass - - @abstract - def close(self) -> None: - pass - - @staticmethod - def _encode_packet(packet: ClientBoundPacket, compression_threshold: int = -1) -> Buffer: + def _encode_packet(self, packet: ClientBoundPacket) -> Buffer: """Encodes and (if necessary) compresses a ClientBoundPacket.""" buf = Buffer().write_varint(packet.id).extend(packet.pack()) - if compression_threshold >= 1: - if len(buf) >= compression_threshold: + if self.compression_threshold >= 1: + if len(buf) >= self.compression_threshold: buf = Buffer().write_varint(len(buf)).extend(zlib.compress(buf)) else: buf = Buffer().write_varint(0).extend(buf) - return Buffer().write_varint(len(buf)).extend(buf) + buf = Buffer().write_varint(len(buf)).extend(buf) + return buf - def _decode_packet(self, client: AbstractTCPServerClient, buf: Buffer) -> ServerBoundPacket: + def _decode_packet(self, buf: Buffer) -> ServerBoundPacket: # decompress packet if necessary - if client.compression_threshold >= 0: + if self.compression_threshold >= 0: uncompressed_length = buf.read_varint() if uncompressed_length > 0: @@ -65,17 +46,37 @@ def _decode_packet(self, client: AbstractTCPServerClient, buf: Buffer) -> Server # attempt to get packet class from given state and packet id try: packet_class: Type[ClientBoundPacket] = self.packet_map[ - PacketDirection.SERVERBOUND, client.state, packet_id + PacketDirection.SERVERBOUND, self.state, packet_id ] except KeyError: - raise UnknownPacketIdError(None, client.state, packet_id, PacketDirection.SERVERBOUND) + raise UnknownPacketIdError(self.packet_map.protocol, self.state, packet_id, PacketDirection.SERVERBOUND) return packet_class.unpack(buf) @abstract - def read_packet(self, client: AbstractTCPServerClient) -> ServerBoundPacket: + def read_packet(self) -> ServerBoundPacket: pass @abstract - def write_packet(self, client: AbstractTCPServerClient, packet: ClientBoundPacket) -> None: + def write_packet(self, packet: ClientBoundPacket) -> None: + pass + + +class AbstractProtocolServer(StrictABC): + """Abstract class for a TCP server that handles Minecraft packets.""" + + def __init__(self, host: str, port: int, protocol: Union[int, str], packet_map: PacketMap): + self.host = host + self.port = port + self.protocol = protocol + self.packet_map = packet_map + + self.connected_clients: Dict[Tuple[str, int], AbstractProtocolServerClient] = {} + + @abstract + def run(self) -> None: + pass + + @abstract + def close(self) -> None: pass diff --git a/pymine_net/net/socket/__init__.py b/pymine_net/net/socket/__init__.py new file mode 100644 index 0000000..d1ce156 --- /dev/null +++ b/pymine_net/net/socket/__init__.py @@ -0,0 +1,3 @@ +from .stream import SocketTCPStream, EncryptedSocketTCPStream +from .client import SocketTCPClient +from .server import SocketProtocolServer, SocketProtocolServerClient diff --git a/pymine_net/net/socket/tcp/client.py b/pymine_net/net/socket/client.py similarity index 83% rename from pymine_net/net/socket/tcp/client.py rename to pymine_net/net/socket/client.py index d9a4fd9..35e52f6 100644 --- a/pymine_net/net/socket/tcp/client.py +++ b/pymine_net/net/socket/client.py @@ -2,8 +2,9 @@ from typing import Union from pymine_net.net.client import AbstractTCPClient -from pymine_net.net.socket.tcp.stream import SocketTCPStream +from pymine_net.net.socket.stream import SocketTCPStream from pymine_net.types.packet import ClientBoundPacket, ServerBoundPacket +from pymine_net.types.packet_map import PacketMap __all__ = ("SocketTCPClient",) @@ -11,8 +12,8 @@ class SocketTCPClient(AbstractTCPClient): """A connection over a TCP socket for reading + writing Minecraft packets.""" - def __init__(self, host: str, port: int, protocol: Union[int, str]): - super().__init__(host, port, protocol) + def __init__(self, host: str, port: int, protocol: Union[int, str], packet_map: PacketMap): + super().__init__(host, port, protocol, packet_map) self.stream: SocketTCPStream = None diff --git a/pymine_net/net/socket/server.py b/pymine_net/net/socket/server.py new file mode 100644 index 0000000..68673c7 --- /dev/null +++ b/pymine_net/net/socket/server.py @@ -0,0 +1,65 @@ +import threading +import socket +from typing import Dict, List, Tuple, Union + +from pymine_net.net.server import AbstractProtocolServer, AbstractProtocolServerClient +from pymine_net.net.socket.stream import SocketTCPStream +from pymine_net.strict_abc import abstract +from pymine_net.types.packet import ClientBoundPacket, ServerBoundPacket +from pymine_net.types.packet_map import PacketMap + + +class SocketProtocolServerClient(AbstractProtocolServerClient): + def __init__(self, stream: SocketTCPStream, packet_map: PacketMap): + super().__init__(stream, packet_map) + self.stream = stream # redefine this cause tyephints + + def read_packet(self) -> ServerBoundPacket: + length = self.stream.read_varint() + return self._decode_packet(self.stream.read(length)) + + def write_packet(self, packet: ClientBoundPacket) -> None: + self.stream.write(self._encode_packet(packet)) + + +class SocketProtocolServer(AbstractProtocolServer): + def __init__(self, host: str, port: int, protocol: Union[int, str], packet_map: PacketMap): + super().__init__(host, port, protocol, packet_map) + + self.connected_clients: Dict[Tuple[str, int], SocketProtocolServerClient] = {} + + self.sock: socket.socket = None + self.threads: List[threading.Thread] = [] + self.running = False + + def run(self) -> None: + self.running = True + + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as self.sock: + self.sock.bind((self.host, self.port)) + self.sock.listen(20) + + while self.running: + connection, _ = self.sock.accept() + self._client_connected_cb(connection) + + def close(self) -> None: + self.sock.close() + + for thread in self.threads: + thread.join(0.1) + + def _client_connected_cb(self, sock: socket.socket) -> None: + client = SocketProtocolServerClient(SocketTCPStream(sock), self.packet_map) + self.connected_clients[client.stream.remote] = client + thread = threading.Thread(target=self._new_client_connected, args=(client,)) + self.threads.append(thread) + thread.start() + + def _new_client_connected(self, client: SocketProtocolServerClient) -> None: + with client.stream.sock: + self.new_client_connected(client) + + @abstract + def new_client_connected(self, client: SocketProtocolServerClient) -> None: + pass diff --git a/pymine_net/net/socket/tcp/stream.py b/pymine_net/net/socket/stream.py similarity index 94% rename from pymine_net/net/socket/tcp/stream.py rename to pymine_net/net/socket/stream.py index 33f0bc4..3e5c1e3 100644 --- a/pymine_net/net/socket/tcp/stream.py +++ b/pymine_net/net/socket/stream.py @@ -25,8 +25,8 @@ def __init__(self, sock: socket.socket): self.remote: Tuple[str, int] = sock.getsockname() - def read(self, length: int) -> bytearray: - result = bytearray() + def read(self, length: int) -> Buffer: + result = Buffer() while len(result) < length: read_bytes = self.sock.recv(length - len(result)) @@ -39,7 +39,7 @@ def read(self, length: int) -> bytearray: return result def write(self, data: bytes) -> None: - self.sock.send(data) + self.sock.sendall(data) def close(self) -> None: self.sock.close() @@ -48,7 +48,7 @@ def read_varint(self) -> int: value = 0 for i in range(10): - byte = struct.unpack(">B", self.read(1)) + byte, = struct.unpack(">B", self.read(1)) value |= (byte & 0x7F) << 7 * i if not byte & 0x80: diff --git a/pymine_net/net/socket/tcp/server.py b/pymine_net/net/socket/tcp/server.py deleted file mode 100644 index 5c64a70..0000000 --- a/pymine_net/net/socket/tcp/server.py +++ /dev/null @@ -1,67 +0,0 @@ -import selectors -import socket -from typing import Dict, Tuple, Union - -from pymine_net.net.server import AbstractTCPServer, AbstractTCPServerClient -from pymine_net.net.socket.tcp.stream import SocketTCPStream -from pymine_net.strict_abc import abstract -from pymine_net.types.packet import ClientBoundPacket, ServerBoundPacket -from pymine_net.types.packet_map import PacketMap - - -class SocketTCPServerClient(AbstractTCPServerClient): - __slots__ = ("stream", "state", "compression_threshold") - - def __init__(self, stream: SocketTCPStream): - super().__init__(stream) - self.stream = stream - - -class SocketTCPServer(AbstractTCPServer): - def __init__(self, host: str, port: int, protocol: Union[int, str], packet_map: PacketMap): - super().__init__(host, port, protocol, packet_map) - - self.connected_clients: Dict[Tuple[str, int], SocketTCPServerClient] = {} - - self.sock: socket.socket = None - self.selector = selectors.DefaultSelector() - self.running = False - - async def run(self) -> None: - self.sock = socket.socket() - self.sock.bind((self.host, self.port)) - self.sock.listen(100) - self.sock.setblocking(False) - - self.selector.register(self.sock, selectors.EVENT_READ, self._client_connected_cb) - - while self.running: - for key, mask in self.selector.select(): - if not self.running: - break - - key.data(key.fileobj, mask) - - async def stop(self) -> None: - self.server.close() - - def read_packet(self, client: SocketTCPServerClient) -> ServerBoundPacket: - length = client.stream.read_varint() - return self._decode_packet(client, client.stream.read(length)) - - def write_packet(self, client: SocketTCPServerClient, packet: ClientBoundPacket) -> None: - client.stream.write(self._encode_packet(packet, client.compression_threshold)) - - def _client_connected_cb(self, sock: socket.socket, mask: selectors._EventMask) -> None: - connection, address = sock.accept() - connection.setblocking(False) - self.selector.register(connection, selectors.EVENT_READ, self._connection_update_cb) - self.connected_clients[address] = SocketTCPServerClient(SocketTCPStream(connection)) - - def _connection_update_cb(self, connection: socket.socket, mask: selectors._EventMask) -> None: - client = self.connected_clients[connection.getpeername()] - self.connection_update(client, self.read_packet(client)) - - @abstract - def incoming_packet(self, client: SocketTCPServerClient, packet: ServerBoundPacket) -> None: - pass diff --git a/pymine_net/net/stream.py b/pymine_net/net/stream.py index 41a1409..423873d 100644 --- a/pymine_net/net/stream.py +++ b/pymine_net/net/stream.py @@ -7,10 +7,6 @@ class AbstractTCPStream: """Abstract class for a TCP stream.""" - @property - def remote(self) -> Tuple[str, int]: - raise NotImplementedError - @abstract def read_varint(self) -> int: pass diff --git a/pymine_net/types/buffer.py b/pymine_net/types/buffer.py index c08a049..5e51133 100644 --- a/pymine_net/types/buffer.py +++ b/pymine_net/types/buffer.py @@ -21,9 +21,7 @@ def __init__(self, *args, **kwargs): def write_bytes(self, data: Union[bytes, bytearray]) -> Buffer: """Writes bytes to the buffer.""" - self.extend(data) - - return self + return self.extend(data) def read_bytes(self, length: int = None) -> bytearray: """Reads bytes from the buffer, if length is None then all bytes are read.""" @@ -47,6 +45,10 @@ def reset(self) -> None: self.pos = 0 + def extend(self, data: Union[Buffer, bytes, bytearray]) -> Buffer: + super().extend(data) + return self + def read_byte(self) -> int: """Reads a singular byte as an integer from the buffer.""" @@ -57,13 +59,19 @@ def read_byte(self) -> int: def write_byte(self, value: int) -> Buffer: """Writes a singular byte to the buffer.""" - self.extend(struct.pack(">b", value)) - return self + return self.extend(struct.pack(">b", value)) def read(self, fmt: str) -> Union[object, Tuple[object]]: """Using the given format, reads from the buffer and returns the unpacked value.""" - unpacked = struct.unpack(">" + fmt, self.read_bytes(struct.calcsize(fmt))) + try: + unpacked = struct.unpack(">" + fmt, self.read_bytes(struct.calcsize(fmt))) + except Exception as e: + # print(f"Read from internal buffer ({size}): {data} (caused error {e})") + raise + else: + # print(f"Read from internal buffer ({size}): {data} (else)") + pass if len(unpacked) == 1: return unpacked[0] diff --git a/pyproject.toml b/pyproject.toml index b3380e6..34eace0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,12 +6,12 @@ include = '\.pyi?$' [tool.pytest.ini_options] addopts = "--full-trace -rxP" testpaths = ["tests"] +asyncio_mode = "auto" [tool.isort] profile="black" line_length=100 - [tool.poetry] name = "pymine-net" version = "0.1.1" @@ -33,6 +33,7 @@ black = "^22.1.0" pytest = "^7.0.1" colorama = "^0.4.4" isort = "^5.10.1" +pytest-asyncio = "^0.18.1" [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/tests/test_net_asyncio.py b/tests/test_net_asyncio.py new file mode 100644 index 0000000..88b92e2 --- /dev/null +++ b/tests/test_net_asyncio.py @@ -0,0 +1,92 @@ +import asyncio +import pytest + +from pymine_net.packets import load_packet_map +from pymine_net.net.asyncio import AsyncProtocolServer, AsyncTCPClient, AsyncProtocolServerClient +from pymine_net.packets.v_1_18_1.handshaking.handshake import HandshakeHandshake +from pymine_net.enums import GameState +from pymine_net.packets.v_1_18_1.status.status import StatusStatusPingPong, StatusStatusRequest, StatusStatusResponse + + +TESTING_PROTOCOL = 757 +TESTING_HOST = "localhost" +TESTING_PORT = 12345 +TESTING_RANDOM_LONG = 1234567890 +TESTING_STATUS_JSON = { + "version": { + "name": "1.18.1", + "protocol": TESTING_PROTOCOL + }, + "players": { + "max": 20, + "online": 0, + "sample": [ + { + "name": "Iapetus11", + "id": "cbcfa252-867d-4bda-a214-776c881cf370" + } + ] + }, + "description": { + "text": "Hello world" + }, + "favicon": None +} + + +# proactor event loop vomits an error on exit on windows due to a Python bug +@pytest.fixture +def event_loop(): + asyncio.set_event_loop(asyncio.SelectorEventLoop()) + yield asyncio.get_event_loop() + + +@pytest.mark.asyncio +async def test_asyncio_net_status(): + class TestAsyncTCPServer(AsyncProtocolServer): + async def new_client_connected(self, client: AsyncProtocolServerClient) -> None: + packet = await client.read_packet() + assert isinstance(packet, HandshakeHandshake) + assert packet.protocol == TESTING_PROTOCOL + assert packet.address == "localhost" + assert packet.port == TESTING_PORT + assert packet.next_state == GameState.STATUS + + client.state = packet.next_state + + packet = await client.read_packet() + assert isinstance(packet, StatusStatusRequest) + + await client.write_packet(StatusStatusResponse(TESTING_STATUS_JSON)) + + packet = await client.read_packet() + assert isinstance(packet, StatusStatusPingPong) + assert packet.payload == TESTING_RANDOM_LONG + await client.write_packet(packet) + + packet_map = load_packet_map(TESTING_PROTOCOL) + + server = TestAsyncTCPServer(TESTING_HOST, TESTING_PORT, TESTING_PROTOCOL, packet_map) + server_task = asyncio.create_task(server.run()) + + client = AsyncTCPClient(TESTING_HOST, TESTING_PORT, TESTING_PROTOCOL, packet_map) + await client.connect() + + await client.write_packet(HandshakeHandshake(TESTING_PROTOCOL, TESTING_HOST, TESTING_PORT, GameState.STATUS)) + client.state = GameState.STATUS + + await client.write_packet(StatusStatusRequest()) + + packet = await client.read_packet() + assert isinstance(packet, StatusStatusResponse) + assert packet.data == TESTING_STATUS_JSON + + await client.write_packet(StatusStatusPingPong(TESTING_RANDOM_LONG)) + packet = await client.read_packet() + assert isinstance(packet, StatusStatusPingPong) + assert packet.payload == TESTING_RANDOM_LONG + + await client.close() + + server_task.cancel() + await server.close() diff --git a/tests/test_net_socket.py b/tests/test_net_socket.py new file mode 100644 index 0000000..bc388b6 --- /dev/null +++ b/tests/test_net_socket.py @@ -0,0 +1,88 @@ +from concurrent.futures import ThreadPoolExecutor +from pymine_net.types.packet import ServerBoundPacket +import pytest +import time + +from pymine_net.packets import load_packet_map +from pymine_net.net.socket import SocketProtocolServer, SocketTCPClient, SocketProtocolServerClient +from pymine_net.packets.v_1_18_1.handshaking.handshake import HandshakeHandshake +from pymine_net.enums import GameState +from pymine_net.packets.v_1_18_1.status.status import StatusStatusPingPong, StatusStatusRequest, StatusStatusResponse + + + +TESTING_PROTOCOL = 757 +TESTING_HOST = "localhost" +TESTING_PORT = 12345 +TESTING_RANDOM_LONG = 1234567890 +TESTING_STATUS_JSON = { + "version": { + "name": "1.18.1", + "protocol": TESTING_PROTOCOL + }, + "players": { + "max": 20, + "online": 0, + "sample": [ + { + "name": "Iapetus11", + "id": "cbcfa252-867d-4bda-a214-776c881cf370" + } + ] + }, + "description": { + "text": "Hello world" + }, + "favicon": None +} + + +def test_socket_net_status(): + class TestSocketTCPServer(SocketProtocolServer): + def new_client_connected(self, client: SocketProtocolServerClient) -> None: + packet = client.read_packet() + assert isinstance(packet, HandshakeHandshake) + assert packet.protocol == TESTING_PROTOCOL + assert packet.address == "localhost" + assert packet.port == TESTING_PORT + assert packet.next_state == GameState.STATUS + + client.state = packet.next_state + + packet = client.read_packet() + assert isinstance(packet, StatusStatusRequest) + + client.write_packet(StatusStatusResponse(TESTING_STATUS_JSON)) + + packet = client.read_packet() + assert isinstance(packet, StatusStatusPingPong) + assert packet.payload == TESTING_RANDOM_LONG + client.write_packet(packet) + + packet_map = load_packet_map(TESTING_PROTOCOL) + + server = TestSocketTCPServer(TESTING_HOST, TESTING_PORT, TESTING_PROTOCOL, packet_map) + threadpool = ThreadPoolExecutor() + threadpool.submit(server.run) + + client = SocketTCPClient(TESTING_HOST, TESTING_PORT, TESTING_PROTOCOL, packet_map) + client.connect() + + client.write_packet(HandshakeHandshake(TESTING_PROTOCOL, TESTING_HOST, TESTING_PORT, GameState.STATUS)) + client.state = GameState.STATUS + + client.write_packet(StatusStatusRequest()) + + packet = client.read_packet() + assert isinstance(packet, StatusStatusResponse) + assert packet.data == TESTING_STATUS_JSON + + client.write_packet(StatusStatusPingPong(TESTING_RANDOM_LONG)) + packet = client.read_packet() + assert isinstance(packet, StatusStatusPingPong) + assert packet.payload == TESTING_RANDOM_LONG + + client.close() + + threadpool.shutdown(wait=False, cancel_futures=True) + server.close() From 762a3662da64519d06209b55eb60ee591e4fbb32 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 23 Feb 2022 02:44:18 +0000 Subject: [PATCH 32/34] apply black & isort --- pymine_net/net/asyncio/__init__.py | 2 +- pymine_net/net/asyncio/stream.py | 4 +-- pymine_net/net/server.py | 4 ++- pymine_net/net/socket/__init__.py | 2 +- pymine_net/net/socket/server.py | 4 +-- pymine_net/net/socket/stream.py | 2 +- tests/test_net_asyncio.py | 38 ++++++++++++-------------- tests/test_net_socket.py | 43 +++++++++++++----------------- 8 files changed, 46 insertions(+), 53 deletions(-) diff --git a/pymine_net/net/asyncio/__init__.py b/pymine_net/net/asyncio/__init__.py index 18c8378..e2f9428 100644 --- a/pymine_net/net/asyncio/__init__.py +++ b/pymine_net/net/asyncio/__init__.py @@ -1,3 +1,3 @@ -from .stream import AsyncTCPStream, EncryptedAsyncTCPStream from .client import AsyncTCPClient from .server import AsyncProtocolServer, AsyncProtocolServerClient +from .stream import AsyncTCPStream, EncryptedAsyncTCPStream diff --git a/pymine_net/net/asyncio/stream.py b/pymine_net/net/asyncio/stream.py index 6b3b124..7502aaa 100644 --- a/pymine_net/net/asyncio/stream.py +++ b/pymine_net/net/asyncio/stream.py @@ -20,7 +20,7 @@ class AsyncTCPStream(AbstractTCPStream, StreamWriter): def __init__(self, writer: StreamWriter): super().__init__(writer._transport, writer._protocol, writer._reader, writer._loop) - + self.remote: Tuple[str, int] = self.get_extra_info("peername") async def read(self, length: int = -1) -> Buffer: @@ -42,7 +42,7 @@ async def read_varint(self) -> int: value = 0 for i in range(10): - byte, = struct.unpack(">B", await self.readexactly(1)) + (byte,) = struct.unpack(">B", await self.readexactly(1)) value |= (byte & 0x7F) << 7 * i if not byte & 0x80: diff --git a/pymine_net/net/server.py b/pymine_net/net/server.py index 88bf202..3429e8e 100644 --- a/pymine_net/net/server.py +++ b/pymine_net/net/server.py @@ -49,7 +49,9 @@ def _decode_packet(self, buf: Buffer) -> ServerBoundPacket: PacketDirection.SERVERBOUND, self.state, packet_id ] except KeyError: - raise UnknownPacketIdError(self.packet_map.protocol, self.state, packet_id, PacketDirection.SERVERBOUND) + raise UnknownPacketIdError( + self.packet_map.protocol, self.state, packet_id, PacketDirection.SERVERBOUND + ) return packet_class.unpack(buf) diff --git a/pymine_net/net/socket/__init__.py b/pymine_net/net/socket/__init__.py index d1ce156..d929d7d 100644 --- a/pymine_net/net/socket/__init__.py +++ b/pymine_net/net/socket/__init__.py @@ -1,3 +1,3 @@ -from .stream import SocketTCPStream, EncryptedSocketTCPStream from .client import SocketTCPClient from .server import SocketProtocolServer, SocketProtocolServerClient +from .stream import EncryptedSocketTCPStream, SocketTCPStream diff --git a/pymine_net/net/socket/server.py b/pymine_net/net/socket/server.py index 68673c7..d4f323a 100644 --- a/pymine_net/net/socket/server.py +++ b/pymine_net/net/socket/server.py @@ -1,5 +1,5 @@ -import threading import socket +import threading from typing import Dict, List, Tuple, Union from pymine_net.net.server import AbstractProtocolServer, AbstractProtocolServerClient @@ -45,7 +45,7 @@ def run(self) -> None: def close(self) -> None: self.sock.close() - + for thread in self.threads: thread.join(0.1) diff --git a/pymine_net/net/socket/stream.py b/pymine_net/net/socket/stream.py index 3e5c1e3..502fc83 100644 --- a/pymine_net/net/socket/stream.py +++ b/pymine_net/net/socket/stream.py @@ -48,7 +48,7 @@ def read_varint(self) -> int: value = 0 for i in range(10): - byte, = struct.unpack(">B", self.read(1)) + (byte,) = struct.unpack(">B", self.read(1)) value |= (byte & 0x7F) << 7 * i if not byte & 0x80: diff --git a/tests/test_net_asyncio.py b/tests/test_net_asyncio.py index 88b92e2..bb89219 100644 --- a/tests/test_net_asyncio.py +++ b/tests/test_net_asyncio.py @@ -1,36 +1,30 @@ import asyncio + import pytest +from pymine_net.enums import GameState +from pymine_net.net.asyncio import AsyncProtocolServer, AsyncProtocolServerClient, AsyncTCPClient from pymine_net.packets import load_packet_map -from pymine_net.net.asyncio import AsyncProtocolServer, AsyncTCPClient, AsyncProtocolServerClient from pymine_net.packets.v_1_18_1.handshaking.handshake import HandshakeHandshake -from pymine_net.enums import GameState -from pymine_net.packets.v_1_18_1.status.status import StatusStatusPingPong, StatusStatusRequest, StatusStatusResponse - +from pymine_net.packets.v_1_18_1.status.status import ( + StatusStatusPingPong, + StatusStatusRequest, + StatusStatusResponse, +) TESTING_PROTOCOL = 757 TESTING_HOST = "localhost" TESTING_PORT = 12345 TESTING_RANDOM_LONG = 1234567890 TESTING_STATUS_JSON = { - "version": { - "name": "1.18.1", - "protocol": TESTING_PROTOCOL - }, + "version": {"name": "1.18.1", "protocol": TESTING_PROTOCOL}, "players": { "max": 20, "online": 0, - "sample": [ - { - "name": "Iapetus11", - "id": "cbcfa252-867d-4bda-a214-776c881cf370" - } - ] - }, - "description": { - "text": "Hello world" + "sample": [{"name": "Iapetus11", "id": "cbcfa252-867d-4bda-a214-776c881cf370"}], }, - "favicon": None + "description": {"text": "Hello world"}, + "favicon": None, } @@ -71,12 +65,14 @@ async def new_client_connected(self, client: AsyncProtocolServerClient) -> None: client = AsyncTCPClient(TESTING_HOST, TESTING_PORT, TESTING_PROTOCOL, packet_map) await client.connect() - - await client.write_packet(HandshakeHandshake(TESTING_PROTOCOL, TESTING_HOST, TESTING_PORT, GameState.STATUS)) + + await client.write_packet( + HandshakeHandshake(TESTING_PROTOCOL, TESTING_HOST, TESTING_PORT, GameState.STATUS) + ) client.state = GameState.STATUS await client.write_packet(StatusStatusRequest()) - + packet = await client.read_packet() assert isinstance(packet, StatusStatusResponse) assert packet.data == TESTING_STATUS_JSON diff --git a/tests/test_net_socket.py b/tests/test_net_socket.py index bc388b6..cf66d56 100644 --- a/tests/test_net_socket.py +++ b/tests/test_net_socket.py @@ -1,39 +1,32 @@ +import time from concurrent.futures import ThreadPoolExecutor -from pymine_net.types.packet import ServerBoundPacket + import pytest -import time +from pymine_net.enums import GameState +from pymine_net.net.socket import SocketProtocolServer, SocketProtocolServerClient, SocketTCPClient from pymine_net.packets import load_packet_map -from pymine_net.net.socket import SocketProtocolServer, SocketTCPClient, SocketProtocolServerClient from pymine_net.packets.v_1_18_1.handshaking.handshake import HandshakeHandshake -from pymine_net.enums import GameState -from pymine_net.packets.v_1_18_1.status.status import StatusStatusPingPong, StatusStatusRequest, StatusStatusResponse - - +from pymine_net.packets.v_1_18_1.status.status import ( + StatusStatusPingPong, + StatusStatusRequest, + StatusStatusResponse, +) +from pymine_net.types.packet import ServerBoundPacket TESTING_PROTOCOL = 757 TESTING_HOST = "localhost" TESTING_PORT = 12345 TESTING_RANDOM_LONG = 1234567890 TESTING_STATUS_JSON = { - "version": { - "name": "1.18.1", - "protocol": TESTING_PROTOCOL - }, + "version": {"name": "1.18.1", "protocol": TESTING_PROTOCOL}, "players": { "max": 20, "online": 0, - "sample": [ - { - "name": "Iapetus11", - "id": "cbcfa252-867d-4bda-a214-776c881cf370" - } - ] + "sample": [{"name": "Iapetus11", "id": "cbcfa252-867d-4bda-a214-776c881cf370"}], }, - "description": { - "text": "Hello world" - }, - "favicon": None + "description": {"text": "Hello world"}, + "favicon": None, } @@ -48,7 +41,7 @@ def new_client_connected(self, client: SocketProtocolServerClient) -> None: assert packet.next_state == GameState.STATUS client.state = packet.next_state - + packet = client.read_packet() assert isinstance(packet, StatusStatusRequest) @@ -67,8 +60,10 @@ def new_client_connected(self, client: SocketProtocolServerClient) -> None: client = SocketTCPClient(TESTING_HOST, TESTING_PORT, TESTING_PROTOCOL, packet_map) client.connect() - - client.write_packet(HandshakeHandshake(TESTING_PROTOCOL, TESTING_HOST, TESTING_PORT, GameState.STATUS)) + + client.write_packet( + HandshakeHandshake(TESTING_PROTOCOL, TESTING_HOST, TESTING_PORT, GameState.STATUS) + ) client.state = GameState.STATUS client.write_packet(StatusStatusRequest()) From 0dfb48b4b28b27678451b4cd8023c7fb05834680 Mon Sep 17 00:00:00 2001 From: Milo Weinberg Date: Tue, 22 Feb 2022 21:53:47 -0500 Subject: [PATCH 33/34] minor cleanup --- pymine_net/__init__.py | 2 +- pymine_net/net/asyncio/__init__.py | 6 +++--- pymine_net/net/client.py | 1 - pymine_net/net/socket/__init__.py | 6 +++--- pymine_net/net/stream.py | 3 --- pymine_net/types/buffer.py | 9 +-------- pymine_net/types/packet_map.py | 4 +--- tests/test_net_socket.py | 4 ---- 8 files changed, 9 insertions(+), 26 deletions(-) diff --git a/pymine_net/__init__.py b/pymine_net/__init__.py index 8bb9256..ad07e28 100644 --- a/pymine_net/__init__.py +++ b/pymine_net/__init__.py @@ -1,5 +1,5 @@ import pymine_net.types.nbt as nbt # noqa: F401 -from pymine_net.net.asyncio.client import AsyncTCPClient +from pymine_net.net.asyncio.client import AsyncTCPClient # noqa: F401 from pymine_net.packets import load_packet_map # noqa: F401 from pymine_net.types.buffer import Buffer # noqa: F401 from pymine_net.types.packet import ClientBoundPacket, Packet, ServerBoundPacket # noqa: F401 diff --git a/pymine_net/net/asyncio/__init__.py b/pymine_net/net/asyncio/__init__.py index e2f9428..9a0fe40 100644 --- a/pymine_net/net/asyncio/__init__.py +++ b/pymine_net/net/asyncio/__init__.py @@ -1,3 +1,3 @@ -from .client import AsyncTCPClient -from .server import AsyncProtocolServer, AsyncProtocolServerClient -from .stream import AsyncTCPStream, EncryptedAsyncTCPStream +from .client import AsyncTCPClient # noqa: F401 +from .server import AsyncProtocolServer, AsyncProtocolServerClient # noqa: F401 +from .stream import AsyncTCPStream, EncryptedAsyncTCPStream # noqa: F401 diff --git a/pymine_net/net/client.py b/pymine_net/net/client.py index 43022fc..195204a 100644 --- a/pymine_net/net/client.py +++ b/pymine_net/net/client.py @@ -3,7 +3,6 @@ from pymine_net.enums import GameState, PacketDirection from pymine_net.errors import UnknownPacketIdError -from pymine_net.packets import load_packet_map from pymine_net.strict_abc import StrictABC, abstract from pymine_net.types.buffer import Buffer from pymine_net.types.packet import ClientBoundPacket, ServerBoundPacket diff --git a/pymine_net/net/socket/__init__.py b/pymine_net/net/socket/__init__.py index d929d7d..275e020 100644 --- a/pymine_net/net/socket/__init__.py +++ b/pymine_net/net/socket/__init__.py @@ -1,3 +1,3 @@ -from .client import SocketTCPClient -from .server import SocketProtocolServer, SocketProtocolServerClient -from .stream import EncryptedSocketTCPStream, SocketTCPStream +from .client import SocketTCPClient # noqa: F401 +from .server import SocketProtocolServer, SocketProtocolServerClient # noqa: F401 +from .stream import EncryptedSocketTCPStream, SocketTCPStream # noqa: F401 diff --git a/pymine_net/net/stream.py b/pymine_net/net/stream.py index 423873d..217401b 100644 --- a/pymine_net/net/stream.py +++ b/pymine_net/net/stream.py @@ -1,6 +1,3 @@ -from re import L -from typing import Tuple - from pymine_net.strict_abc import abstract diff --git a/pymine_net/types/buffer.py b/pymine_net/types/buffer.py index 5e51133..f5bc6d0 100644 --- a/pymine_net/types/buffer.py +++ b/pymine_net/types/buffer.py @@ -64,14 +64,7 @@ def write_byte(self, value: int) -> Buffer: def read(self, fmt: str) -> Union[object, Tuple[object]]: """Using the given format, reads from the buffer and returns the unpacked value.""" - try: - unpacked = struct.unpack(">" + fmt, self.read_bytes(struct.calcsize(fmt))) - except Exception as e: - # print(f"Read from internal buffer ({size}): {data} (caused error {e})") - raise - else: - # print(f"Read from internal buffer ({size}): {data} (else)") - pass + unpacked = struct.unpack(">" + fmt, self.read_bytes(struct.calcsize(fmt))) if len(unpacked) == 1: return unpacked[0] diff --git a/pymine_net/types/packet_map.py b/pymine_net/types/packet_map.py index 1913416..8856d08 100644 --- a/pymine_net/types/packet_map.py +++ b/pymine_net/types/packet_map.py @@ -1,11 +1,9 @@ from __future__ import annotations -import zlib from typing import Dict, List, Tuple, Type, Union from pymine_net.enums import GameState, PacketDirection -from pymine_net.errors import DuplicatePacketIdError, UnknownPacketIdError -from pymine_net.types.buffer import Buffer +from pymine_net.errors import DuplicatePacketIdError from pymine_net.types.packet import ClientBoundPacket, Packet, ServerBoundPacket diff --git a/tests/test_net_socket.py b/tests/test_net_socket.py index cf66d56..7c973ba 100644 --- a/tests/test_net_socket.py +++ b/tests/test_net_socket.py @@ -1,8 +1,5 @@ -import time from concurrent.futures import ThreadPoolExecutor -import pytest - from pymine_net.enums import GameState from pymine_net.net.socket import SocketProtocolServer, SocketProtocolServerClient, SocketTCPClient from pymine_net.packets import load_packet_map @@ -12,7 +9,6 @@ StatusStatusRequest, StatusStatusResponse, ) -from pymine_net.types.packet import ServerBoundPacket TESTING_PROTOCOL = 757 TESTING_HOST = "localhost" From fcde5444dd511b4d028b67f805c55d3554d0ff09 Mon Sep 17 00:00:00 2001 From: Milo Weinberg Date: Tue, 22 Feb 2022 22:02:55 -0500 Subject: [PATCH 34/34] remove unnecessary write() overriding + fix typo + fix typehint --- pymine_net/net/asyncio/stream.py | 3 --- pymine_net/net/socket/server.py | 2 +- pymine_net/net/socket/stream.py | 2 +- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/pymine_net/net/asyncio/stream.py b/pymine_net/net/asyncio/stream.py index 7502aaa..b136968 100644 --- a/pymine_net/net/asyncio/stream.py +++ b/pymine_net/net/asyncio/stream.py @@ -35,9 +35,6 @@ async def readexactly(self, length: int) -> Buffer: async def readuntil(self, separator: bytes = b"\n") -> Buffer: return Buffer(await self._reader.readuntil(separator)) - def write(self, data): - super().write(data) - async def read_varint(self) -> int: value = 0 diff --git a/pymine_net/net/socket/server.py b/pymine_net/net/socket/server.py index d4f323a..a87be35 100644 --- a/pymine_net/net/socket/server.py +++ b/pymine_net/net/socket/server.py @@ -12,7 +12,7 @@ class SocketProtocolServerClient(AbstractProtocolServerClient): def __init__(self, stream: SocketTCPStream, packet_map: PacketMap): super().__init__(stream, packet_map) - self.stream = stream # redefine this cause tyephints + self.stream = stream # redefine this cause typehints def read_packet(self) -> ServerBoundPacket: length = self.stream.read_varint() diff --git a/pymine_net/net/socket/stream.py b/pymine_net/net/socket/stream.py index 502fc83..19e75ac 100644 --- a/pymine_net/net/socket/stream.py +++ b/pymine_net/net/socket/stream.py @@ -38,7 +38,7 @@ def read(self, length: int) -> Buffer: return result - def write(self, data: bytes) -> None: + def write(self, data: Union[Buffer, bytes, bytearray]) -> None: self.sock.sendall(data) def close(self) -> None: