From 558be67f61a52d755188f748b5bbbddb0c96a6ec Mon Sep 17 00:00:00 2001 From: Sunpy Date: Sat, 4 Apr 2020 16:04:40 +0200 Subject: [PATCH] Research notes --- notes/packet_writeup.md | 25 ++++++++++++++ notes/packetlist.txt | 13 +++++++ notes/reliabilityLayer.txt | 3 ++ sampy/__init__.py | 5 +-- sampy/client/player.py | 58 ++++++++++++++++++++++++++++++++ sampy/raknet/bitstream.py | 67 +++++++++++++++++++++++++++++++++++++ sampy/server/__init__.py | 2 +- sampy/server/compression.py | 19 ++++++++--- sampy/struct/packet.py | 33 ++++++++++++++++++ sampy/struct/server.py | 2 +- test.py | 30 +++++++++++++++++ 11 files changed, 248 insertions(+), 9 deletions(-) create mode 100644 notes/packet_writeup.md create mode 100644 notes/packetlist.txt create mode 100644 notes/reliabilityLayer.txt create mode 100644 sampy/raknet/bitstream.py create mode 100644 sampy/struct/packet.py create mode 100644 test.py diff --git a/notes/packet_writeup.md b/notes/packet_writeup.md new file mode 100644 index 0000000..611b4ba --- /dev/null +++ b/notes/packet_writeup.md @@ -0,0 +1,25 @@ +# General packet +## Client to server spesific +* Encode (encrypt) data + * use first byte as checksum + * every byte ^ 0xAA added together + * xor + * Server port ^ 0xCCCC + * Every other byte + * Lookup table (use byte as indexing value) + + +## Shared +* Can be internal packet + +# Internal packet +|condition|name|bit length|type|notes| +|---|---|---|---|---| +||messageNumber|0x10|ushort|| +||reliability|0x04|uchar|Has to be over 5 (value > 5)| +|reliability in (7, 10, 9)|orderingChannel|0x05||| +|reliability in (7, 10, 9)|orderingIndex|0x10|ushort|| +||isSplitPacket|0x01|bool|Drop packet as we no longer support split packet?| +|isSplitPacket == 0|dataBitLength|0x10|ushort|| +|isSplitPacket == 0|data|dataBitLength|uchar*|| + diff --git a/notes/packetlist.txt b/notes/packetlist.txt new file mode 100644 index 0000000..f675037 --- /dev/null +++ b/notes/packetlist.txt @@ -0,0 +1,13 @@ +0x07 ID_PING +0x08 ID_PING_OPEN_CONNECTIONS +0x0B ID_CONNECTION_REQUEST +0x18 ID_OPEN_CONNECTION_REQUEST +0x19 ID_OPEN_CONNECTION_REPLY +0x1D ID_CONNECTION_ATTEMPT_FAILED +0x1F ID_NO_FREE_INCOMING_CONNECTIONS +0x24 ID_CONNECTION_BANNED +0x25 ID_INVALID_PASSWORD +0x27 ID_PONG +0x37 ID_ADVERTISE_SYSTEM + +0x26 ID_MODIFIED_PACKET # default behavior (did not find the packet||could not handle because not found) diff --git a/notes/reliabilityLayer.txt b/notes/reliabilityLayer.txt new file mode 100644 index 0000000..2c68f21 --- /dev/null +++ b/notes/reliabilityLayer.txt @@ -0,0 +1,3 @@ +0x07 UNRELIABLE_SEQUENCED +0x0A RELIABLE_SEQUENCED +0x09 RELIABLE_ORDERED diff --git a/sampy/__init__.py b/sampy/__init__.py index e21f02d..4787b85 100644 --- a/sampy/__init__.py +++ b/sampy/__init__.py @@ -1,7 +1,8 @@ -from . import env from . import server from . import client from . import shared from . import struct -from . import client \ No newline at end of file + +from . import env +#from . import logging \ No newline at end of file diff --git a/sampy/client/player.py b/sampy/client/player.py index 1b26df8..04b6013 100644 --- a/sampy/client/player.py +++ b/sampy/client/player.py @@ -1,14 +1,72 @@ import socket +import struct from .base import BaseClient import logging logger = logging.getLogger(__name__) +STATE_CONNECTING = 0 +STATE_AUTH = 1 + class PlayerClient(BaseClient): def __init__(self, server: "__ServerInstance__", ip: str, port: int): super().__init__(server, ip, port) logger.debug("Client resolved to PlayerClient") + + self.state = STATE_CONNECTING + + self.handlers = { + 0x18: self.on_connection_request, + #0x00: self.on_state_dependent_handle + } async def on_packet(self, packet: bytes): logger.debug("on_packet(%s)" % packet) + + try: + packet = self.server.compressor.decompress(packet) + + + + await self.handlers.get(packet[0], self.on_unimplemented)(packet) + except Exception as err: + logger.error(err) + + async def on_unimplemented(self, packet: bytes): + pass + + async def on_connection_request(self, packet: bytes): + logger.debug("on_connection_request(%s)" % packet) + + if self.state != STATE_CONNECTING: # Client should not send this after it has passed + return + + challenge = (self.ip_uint ^ self.server.config.challenge_short) & 0xFFFF + challenge_solution = challenge ^ 0x6969 # 0x6969 being client_version? + + challenge_answer, = struct.unpack_from(b" 8: + + + if offset == 0: + self.buffer.append(0) + + v = self.buffer[buffer_index] + + mask = (1 << ((self.offset % 8) + 1)) + """ + + + def read(self, bit_length: int) -> int: + byte_read_from = self.offset // 8 + byte_read_to = (self.offset + bit_length - 1) // 8 + bytes_to_read = byte_read_to - byte_read_from + 1 + + if byte_read_to >= len(self.buffer): + raise Exception("Reading beyond the buffer") + + value = 0 + shifts = bytes_to_read - 1 + while shifts >= 0: + value += self.buffer[byte_read_to - shifts] << (shifts * 8) + shifts -= 1 + + value <<= self.offset % 8 + value &= (1 << (8 * bytes_to_read)) - 1 + value >>= (bytes_to_read * 8) - bit_length + + self.offset += bit_length + return value + + def __repr__(self) -> str: + b = bytearray(" " + " ".join(" ".join(format(c, "08b")) for c in self.buffer) + " ", "ascii") + m = self.offset * 2 + (self.offset // 8) - (self.offset // (max(1, len(self.buffer)) * 8)) + #b[m] = 91 # [ + #b[m + 2] = 93 # ] + b[m] = 124 # | + return b.decode() diff --git a/sampy/server/__init__.py b/sampy/server/__init__.py index 474da09..db205d5 100644 --- a/sampy/server/__init__.py +++ b/sampy/server/__init__.py @@ -52,7 +52,7 @@ class Server: if addr not in self.clients: ip, port = addr self.clients[addr] = Client(self, ip, port) - await self.clients[addr].on_packet(data) + await (self.clients[addr].onpacket(data)) #await self.clients[addr].on_packet(data) disconnected = [c for c in self.clients.values() if c.connected == False] for c in disconnected: # Remove dead connections diff --git a/sampy/server/compression.py b/sampy/server/compression.py index db3c507..cbea019 100644 --- a/sampy/server/compression.py +++ b/sampy/server/compression.py @@ -19,7 +19,7 @@ class Compressor: Arguments: bytestream {bytes} -- Bytes sent by client """ - checksum, data = bytestream[0], bytestream[1:] + checksum, data = bytestream[0], bytearray(bytestream[1:]) data = self.xor_every_other_byte(self.get_port_xor_key(), data) data = self.run_though_lookup_table(data) @@ -28,17 +28,19 @@ class Compressor: logger.error("Checksum failed!") raise Exception("Checksum fail") - return data + logger.debug("decompress -> %s" % data) - def xor_every_other_byte(self, xor: int, bytestream: bytes) -> bytes: + return bytes(data) + + def xor_every_other_byte(self, xor: int, bytestream: bytearray) -> bytearray: for i in range(1, len(bytestream), 2): bytestream[i] ^= xor return bytestream - def run_though_lookup_table(self, bytestream: bytes) -> bytes: + def run_though_lookup_table(self, bytestream: bytearray) -> bytearray: return bytes(LOOKUP_TABLE[b] for b in bytestream) - def calc_checksum(self, bytestream: bytes) -> int: + def calc_checksum(self, bytestream: bytearray) -> int: checksum = 0 for byte in bytestream: checksum ^= byte & 0xAA @@ -46,3 +48,10 @@ class Compressor: def get_port_xor_key(self) -> int: return (self.config.port ^ 0xCCCC) & 0xFF + +class StringCompressor: + def __init__(self): + pass + + def decode_string(self, data: bytes) -> (str, int): + pass \ No newline at end of file diff --git a/sampy/struct/packet.py b/sampy/struct/packet.py new file mode 100644 index 0000000..5cd0edb --- /dev/null +++ b/sampy/struct/packet.py @@ -0,0 +1,33 @@ +class InternalPacket: + def __init__(self): + self.message_number = message_number # ushort + self.pack_number = pack_number # uint + self.priority = priority # int? this could be a byte (actually 2 bits is enough) + self.reliability = reliability # int? this could be a byte (actually 3 bits is enough) + self.ordering_channel = ordering_channel # uchar + self.ordering_index = ordering_index # ushort + self.split_packet_id = split_packet_id # ushort + self.split_packet_index = split_packet_index # uint + self.split_packet_count = split_packet_count # uint + self.creation_time = creation_time # long long + self.next_action_time = next_action_time # long long + self.data_bit_length = data_bit_length # uint + self.data = data # char* + self.histogram_marker = histogram_marker # uint + + def get_bitstream_header_length(self) -> int: + bit_length = 32 # 2 * 2 * 8 + bit_length += 3 # TODO: merge ^ + + if self.reliability in [1, 3, 4]: + bit_length += 5 + bit_length += 16 # 2 * 8 TODO: merge ^ + + bit_length += 1 # TODO: merge ^ + + if self.split_packet_count > 0: + bit_length += 80 # (2 + 4 * 2) * 8 + + bit_length += 16 # TODO: merge ^ + + return bit_length \ No newline at end of file diff --git a/sampy/struct/server.py b/sampy/struct/server.py index ae0c28e..dfac9f7 100644 --- a/sampy/struct/server.py +++ b/sampy/struct/server.py @@ -16,7 +16,7 @@ class ServerConfig: self.rcon_password = rcon_password self.max_players = max_players - + self.mode = mode self.language = language diff --git a/test.py b/test.py new file mode 100644 index 0000000..0cc3c1e --- /dev/null +++ b/test.py @@ -0,0 +1,30 @@ +from sampy.raknet.bitstream import Bitstream + +a = Bitstream() +""" +a.buffer.append(0x42) +a.buffer.append(0x13) +a.buffer.append(0xFF) +a.buffer.append(0xAC) +a.buffer.append(0x00) +a.buffer.append(0x69) +""" +""" +a.write(1) +a.write(0) +a.write(1) +a.write(1) +a.write(1) +a.write(1) +a.write(0) +a.write(1) + +a.write(1) +""" + +data = [int(x, 16) for x in "00 00 42 90 0b 61 61 61 61 62 62 62 62 63 63 63 63 66 66 66 66 00".split(" ")] + +for b in data: + a.buffer.append(b) + +print(a) \ No newline at end of file