Skip to content
This repository has been archived by the owner on Dec 12, 2022. It is now read-only.

Add networking functionality #7

Merged
merged 47 commits into from
Feb 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
1a4d3c0
fix explosion.py
Sh-wayz Feb 21, 2022
7664070
formatting
Sh-wayz Feb 21, 2022
fe643db
0x21
Sh-wayz Feb 21, 2022
bb27d67
0x0F
Sh-wayz Feb 21, 2022
a19890c
apply import sorting
github-actions[bot] Feb 21, 2022
3e3575b
Merge branch 'main' into 3-packets
Iapetus-11 Feb 21, 2022
4499f8c
apply black codestyle
github-actions[bot] Feb 21, 2022
42f5735
Merge branch 'main' into 3-packets
Iapetus-11 Feb 21, 2022
cf0a94d
Merge branch 'main' into 3-packets
Iapetus-11 Feb 21, 2022
ccd5998
apply black codestyle
github-actions[bot] Feb 21, 2022
ecaf23d
add missing pack/unpack method to all non play packets
Iapetus-11 Feb 21, 2022
119c90e
Merge branch '3-packets' of https://github.com/py-mine/PyMine-Net int…
Iapetus-11 Feb 21, 2022
16f5563
apply black codestyle
github-actions[bot] Feb 21, 2022
7cff855
Merge branch 'main' into 3-packets
Iapetus-11 Feb 21, 2022
a38d0ef
Merge branch 'main' into 3-packets
Iapetus-11 Feb 21, 2022
56a857e
apply import sorting
github-actions[bot] Feb 21, 2022
866b3d1
Merge branch 'main' into 3-packets
Iapetus-11 Feb 21, 2022
91dd2f0
minor docstring + typing + other fixes
Iapetus-11 Feb 21, 2022
005ae8d
apply import sorting
github-actions[bot] Feb 21, 2022
817dbdd
add PlayMapData in map.py
Iapetus-11 Feb 21, 2022
beb039f
apply import sorting
github-actions[bot] Feb 21, 2022
88c8020
Merge branch 'main' into 3-packets
Iapetus-11 Feb 21, 2022
37b92d2
apply black codestyle
github-actions[bot] Feb 21, 2022
31833cc
Merge branch 'main' into 3-packets
Iapetus-11 Feb 21, 2022
31c7de7
add sync + async implementations of TCPStream + EncryptedTCPStream + …
Iapetus-11 Feb 22, 2022
a055f2b
apply import sorting
github-actions[bot] Feb 22, 2022
4f67769
more progress on networking code
Iapetus-11 Feb 22, 2022
91623c0
apply import sorting
github-actions[bot] Feb 22, 2022
0aeec03
minor changes + add __all__ to some files
Iapetus-11 Feb 22, 2022
162e0ce
apply import sorting
github-actions[bot] Feb 22, 2022
d68c75d
Merge branch 'main' into 6-networking
Iapetus-11 Feb 22, 2022
1e5cc87
apply black & isort
github-actions[bot] Feb 22, 2022
bd3bcd7
Merge branch 'main' into 3-packets
Iapetus-11 Feb 22, 2022
d334fbd
add packets in player_list.py and particle.py + cleanup some imports
Iapetus-11 Feb 22, 2022
a5c3bf6
apply black & isort
github-actions[bot] Feb 22, 2022
3660b70
add missing direction in docstrings
Iapetus-11 Feb 22, 2022
e517000
start on SocketTCPServer
Iapetus-11 Feb 22, 2022
04aedbb
Merge branch '6-networking' of https://github.com/py-mine/PyMine-Net …
Iapetus-11 Feb 22, 2022
8dcc929
finish SocketTCPServer
Iapetus-11 Feb 22, 2022
6e61c41
apply black & isort
github-actions[bot] Feb 22, 2022
5bb9260
rename folder 757 to v_1_18_1
Iapetus-11 Feb 22, 2022
0c914b2
apply black & isort
github-actions[bot] Feb 22, 2022
39a89d0
Merge branch '3-packets' into 6-networking
Iapetus-11 Feb 22, 2022
89326ed
restructuring + organization changes + add tests for networking funct…
Iapetus-11 Feb 23, 2022
762a366
apply black & isort
github-actions[bot] Feb 23, 2022
0dfb48b
minor cleanup
Iapetus-11 Feb 23, 2022
fcde544
remove unnecessary write() overriding + fix typo + fix typehint
Iapetus-11 Feb 23, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 136 additions & 1 deletion poetry.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions pymine_net/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import pymine_net.types.nbt as nbt # noqa: F401
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
3 changes: 3 additions & 0 deletions pymine_net/net/asyncio/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .client import AsyncTCPClient # noqa: F401
from .server import AsyncProtocolServer, AsyncProtocolServerClient # noqa: F401
from .stream import AsyncTCPStream, EncryptedAsyncTCPStream # noqa: F401
34 changes: 34 additions & 0 deletions pymine_net/net/asyncio/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import asyncio
from typing import Union

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",)


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], packet_map: PacketMap):
super().__init__(host, port, protocol, packet_map)

self.stream: AsyncTCPStream = None

async def connect(self) -> None:
_, writer = await asyncio.open_connection(self.host, self.port)
Iapetus-11 marked this conversation as resolved.
Show resolved Hide resolved
self.stream = AsyncTCPStream(writer)

async def close(self) -> None:
self.stream.close()
await self.stream.wait_closed()

async def read_packet(self) -> ClientBoundPacket:
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:
self.stream.write(self._encode_packet(packet))
await self.stream.drain()
53 changes: 53 additions & 0 deletions pymine_net/net/asyncio/server.py
Original file line number Diff line number Diff line change
@@ -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
93 changes: 93 additions & 0 deletions pymine_net/net/asyncio/stream.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
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.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.

: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))

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 EncryptedAsyncTCPStream(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))
72 changes: 72 additions & 0 deletions pymine_net/net/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import zlib
from typing import Type, Union

from pymine_net.enums import GameState, PacketDirection
from pymine_net.errors import UnknownPacketIdError
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], packet_map: PacketMap):
self.host = host
self.port = port
self.protocol = protocol
self.packet_map = packet_map

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: ServerBoundPacket, compression_threshold: int = -1) -> Buffer:
"""Encodes and (if necessary) compresses a ServerBoundPacket."""

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(self) -> ClientBoundPacket:
pass

@abstract
def write_packet(self, packet: ServerBoundPacket) -> None:
pass
84 changes: 84 additions & 0 deletions pymine_net/net/server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
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
from pymine_net.types.packet_map import PacketMap


class AbstractProtocolServerClient(StrictABC):
__slots__ = ("stream", "packet_map", "state", "compression_threshold")

def __init__(self, stream: AbstractTCPStream, packet_map: PacketMap):
self.stream = stream
self.packet_map = packet_map
self.state = GameState.HANDSHAKING
self.compression_threshold = -1

def _encode_packet(self, packet: ClientBoundPacket) -> Buffer:
"""Encodes and (if necessary) compresses a ClientBoundPacket."""

buf = Buffer().write_varint(packet.id).extend(packet.pack())

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)

buf = Buffer().write_varint(len(buf)).extend(buf)
return buf

def _decode_packet(self, buf: Buffer) -> ServerBoundPacket:
# 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.SERVERBOUND, self.state, packet_id
]
except KeyError:
raise UnknownPacketIdError(
self.packet_map.protocol, self.state, packet_id, PacketDirection.SERVERBOUND
)

return packet_class.unpack(buf)

@abstract
def read_packet(self) -> ServerBoundPacket:
pass

@abstract
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
3 changes: 3 additions & 0 deletions pymine_net/net/socket/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .client import SocketTCPClient # noqa: F401
from .server import SocketProtocolServer, SocketProtocolServerClient # noqa: F401
from .stream import EncryptedSocketTCPStream, SocketTCPStream # noqa: F401
34 changes: 34 additions & 0 deletions pymine_net/net/socket/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import socket
from typing import Union

from pymine_net.net.client import AbstractTCPClient
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",)


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], packet_map: PacketMap):
super().__init__(host, port, protocol, packet_map)

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(self) -> ClientBoundPacket:
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))
Loading