From 39832772f9a6a0a9a8e621ceeb30b990bfb4f51b Mon Sep 17 00:00:00 2001 From: Gabriel Huber Date: Sun, 22 Oct 2017 01:59:40 +0200 Subject: [PATCH 1/9] Restructure source module Implements #52 and flattens the structure a bit like suggested in #49 --- valve/source/__init__.py | 132 ++-------------------------------- valve/source/a2s.py | 9 +-- valve/source/basequerier.py | 128 +++++++++++++++++++++++++++++++++ valve/source/master_server.py | 6 +- 4 files changed, 139 insertions(+), 136 deletions(-) create mode 100644 valve/source/basequerier.py diff --git a/valve/source/__init__.py b/valve/source/__init__.py index 1b38712..9b248e8 100644 --- a/valve/source/__init__.py +++ b/valve/source/__init__.py @@ -1,128 +1,6 @@ -# -*- coding: utf-8 -*- -# Copyright (C) 2017 Oliver Ainsworth +from __future__ import absolute_import -from __future__ import (absolute_import, - unicode_literals, print_function, division) - -import functools -import select -import socket -import warnings - -import six - - -class NoResponseError(Exception): - """Raised when a server querier doesn't receive a response.""" - - -class QuerierClosedError(Exception): - """Raised when attempting to use a querier after it's closed.""" - - -class BaseQuerier(object): - """Base class for implementing source server queriers. - - When an instance of this class is initialised a socket is created. - It's important that, once a querier is to be discarded, the associated - socket be closed via :meth:`close`. For example: - - .. code-block:: python - - querier = valve.source.BaseQuerier(('...', 27015)) - try: - querier.request(...) - finally: - querier.close() - - When server queriers are used as context managers, the socket will - be cleaned up automatically. Hence it's preferably to use the `with` - statement over the `try`-`finally` pattern described above: - - .. code-block:: python - - with valve.source.BaseQuerier(('...', 27015)) as querier: - querier.request(...) - - Once a querier has been closed, any attempts to make additional requests - will result in a :exc:`QuerierClosedError` to be raised. - - :ivar host: Host requests will be sent to. - :ivar port: Port number requests will be sent to. - :ivar timeout: How long to wait for a response to a request. - """ - - def __init__(self, address, timeout=5.0): - self.host = address[0] - self.port = address[1] - self.timeout = timeout - self._contextual = False - self._socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - - def __enter__(self): - self._contextual = True - return self - - def __exit__(self, type_, exception, traceback): - self._contextual = False - self.close() - - def _check_open(function): - # Wrap methods to raise QuerierClosedError when called - # after the querier has been closed. - - @functools.wraps(function) - def wrapper(self, *args, **kwargs): - if self._socket is None: - raise QuerierClosedError - return function(self, *args, **kwargs) - - return wrapper - - def close(self): - """Close the querier's socket. - - It is safe to call this multiple times. - """ - if self._contextual: - warnings.warn("{0.__class__.__name__} used as context " - "manager but close called before exit".format(self)) - if self._socket is not None: - self._socket.close() - self._socket = None - - @_check_open - def request(self, *request): - """Issue a request. - - The given request segments will be encoded and combined to - form the final message that is sent to the configured address. - - :param request: Request message segments. - :type request: valve.source.messages.Message - - :raises QuerierClosedError: If the querier has been closed. - """ - request_final = b"".join(segment.encode() for segment in request) - self._socket.sendto(request_final, (self.host, self.port)) - - @_check_open - def get_response(self): - """Wait for a response to a request. - - :raises NoResponseError: If the configured :attr:`timeout` is - reached before a response is received. - :raises QuerierClosedError: If the querier has been closed. - - :returns: The raw response as a :class:`bytes`. - """ - ready = select.select([self._socket], [], [], self.timeout) - if not ready[0]: - raise NoResponseError("Timed out waiting for response") - try: - data = ready[0][0].recv(1400) - except socket.error as exc: - six.raise_from(NoResponseError(exc)) - return data - - del _check_open +from .basequerier import BaseQuerier, NoResponseError, QuerierClosedError +from .a2s import ServerQuerier +from .master_server import MasterServerQuerier, Duplicates +from .util import Platform, ServerType diff --git a/valve/source/a2s.py b/valve/source/a2s.py index c916b60..4914fdb 100644 --- a/valve/source/a2s.py +++ b/valve/source/a2s.py @@ -6,15 +6,12 @@ import monotonic -import valve.source +from .basequerier import BaseQuerier, NoResponseError from . import messages -# NOTE: backwards compatability; remove soon(tm) -NoResponseError = valve.source.NoResponseError - -class ServerQuerier(valve.source.BaseQuerier): +class ServerQuerier(BaseQuerier): """Implements the A2S Source server query protocol. https://developer.valvesoftware.com/wiki/Server_queries @@ -30,7 +27,7 @@ def request(self, request): def get_response(self): - data = valve.source.BaseQuerier.get_response(self) + data = BaseQuerier.get_response(self) # According to https://developer.valvesoftware.com/wiki/Server_queries # "TF2 currently does not split replies, expect A2S_PLAYER and diff --git a/valve/source/basequerier.py b/valve/source/basequerier.py new file mode 100644 index 0000000..1b38712 --- /dev/null +++ b/valve/source/basequerier.py @@ -0,0 +1,128 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2017 Oliver Ainsworth + +from __future__ import (absolute_import, + unicode_literals, print_function, division) + +import functools +import select +import socket +import warnings + +import six + + +class NoResponseError(Exception): + """Raised when a server querier doesn't receive a response.""" + + +class QuerierClosedError(Exception): + """Raised when attempting to use a querier after it's closed.""" + + +class BaseQuerier(object): + """Base class for implementing source server queriers. + + When an instance of this class is initialised a socket is created. + It's important that, once a querier is to be discarded, the associated + socket be closed via :meth:`close`. For example: + + .. code-block:: python + + querier = valve.source.BaseQuerier(('...', 27015)) + try: + querier.request(...) + finally: + querier.close() + + When server queriers are used as context managers, the socket will + be cleaned up automatically. Hence it's preferably to use the `with` + statement over the `try`-`finally` pattern described above: + + .. code-block:: python + + with valve.source.BaseQuerier(('...', 27015)) as querier: + querier.request(...) + + Once a querier has been closed, any attempts to make additional requests + will result in a :exc:`QuerierClosedError` to be raised. + + :ivar host: Host requests will be sent to. + :ivar port: Port number requests will be sent to. + :ivar timeout: How long to wait for a response to a request. + """ + + def __init__(self, address, timeout=5.0): + self.host = address[0] + self.port = address[1] + self.timeout = timeout + self._contextual = False + self._socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + + def __enter__(self): + self._contextual = True + return self + + def __exit__(self, type_, exception, traceback): + self._contextual = False + self.close() + + def _check_open(function): + # Wrap methods to raise QuerierClosedError when called + # after the querier has been closed. + + @functools.wraps(function) + def wrapper(self, *args, **kwargs): + if self._socket is None: + raise QuerierClosedError + return function(self, *args, **kwargs) + + return wrapper + + def close(self): + """Close the querier's socket. + + It is safe to call this multiple times. + """ + if self._contextual: + warnings.warn("{0.__class__.__name__} used as context " + "manager but close called before exit".format(self)) + if self._socket is not None: + self._socket.close() + self._socket = None + + @_check_open + def request(self, *request): + """Issue a request. + + The given request segments will be encoded and combined to + form the final message that is sent to the configured address. + + :param request: Request message segments. + :type request: valve.source.messages.Message + + :raises QuerierClosedError: If the querier has been closed. + """ + request_final = b"".join(segment.encode() for segment in request) + self._socket.sendto(request_final, (self.host, self.port)) + + @_check_open + def get_response(self): + """Wait for a response to a request. + + :raises NoResponseError: If the configured :attr:`timeout` is + reached before a response is received. + :raises QuerierClosedError: If the querier has been closed. + + :returns: The raw response as a :class:`bytes`. + """ + ready = select.select([self._socket], [], [], self.timeout) + if not ready[0]: + raise NoResponseError("Timed out waiting for response") + try: + data = ready[0][0].recv(1400) + except socket.error as exc: + six.raise_from(NoResponseError(exc)) + return data + + del _check_open diff --git a/valve/source/master_server.py b/valve/source/master_server.py index 8f1647b..0c8039b 100644 --- a/valve/source/master_server.py +++ b/valve/source/master_server.py @@ -9,7 +9,7 @@ import six -import valve.source +from .basequerier import BaseQuerier, NoResponseError from . import messages from . import util @@ -44,7 +44,7 @@ class Duplicates(enum.Enum): STOP = "stop" -class MasterServerQuerier(valve.source.BaseQuerier): +class MasterServerQuerier(BaseQuerier): """Implements the Source master server query protocol https://developer.valvesoftware.com/wiki/Master_Server_Query_Protocol @@ -103,7 +103,7 @@ def _query(self, region, filter_string): region=region, address=last_addr, filter=filter_string)) try: raw_response = self.get_response() - except valve.source.NoResponseError: + except NoResponseError: return else: response = messages.MasterServerResponse.decode(raw_response) From fba13abf3ab95905fc1f005be0f097dcc06a52dd Mon Sep 17 00:00:00 2001 From: Gabriel Huber Date: Sun, 22 Oct 2017 02:59:26 +0200 Subject: [PATCH 2/9] Implement extra data flag using ByteReader class --- valve/source/byteio.py | 154 +++++++++++++++++++++++++++++++++++++++ valve/source/messages.py | 68 ++++++++++++----- 2 files changed, 203 insertions(+), 19 deletions(-) create mode 100644 valve/source/byteio.py diff --git a/valve/source/byteio.py b/valve/source/byteio.py new file mode 100644 index 0000000..ac67407 --- /dev/null +++ b/valve/source/byteio.py @@ -0,0 +1,154 @@ +import struct +import io + +class ByteReader(object): + def __init__(self, stream, endian='='): + self.stream = stream + self.endian = endian + + def read(self, *args): + return self.stream.read(*args) + + def unpack(self, fmt): + fmt = self.endian + fmt + fmt_size = struct.calcsize(fmt) + return struct.unpack(fmt, self.stream.read(fmt_size)) + + def unpack_one(self, fmt): + values = self.unpack(fmt) + assert len(values) == 1 + return values[0] + + def read_char(self): + return self.unpack_one("c") + + def read_int8(self): + return self.unpack_one("b") + + def read_uint8(self): + return self.unpack_one("B") + + def read_int16(self): + return self.unpack_one("h") + + def read_uint16(self): + return self.unpack_one("H") + + def read_int32(self): + return self.unpack_one("l") + + def read_uint32(self): + return self.unpack_one("L") + + def read_int64(self): + return self.unpack_one("q") + + def read_uint64(self): + return self.unpack_one("Q") + + def read_float(self): + return self.unpack_one("f") + + def read_double(self): + return self.unpack_one("d") + + def read_cstring(self, charsize=1): + string = b"" + while True: + c = self.read(charsize) + if int.from_bytes(c, "little") == 0: + break + else: + string += c + return string + + def read_vec3(self): + return self.unpack("4f") + + def read_vec4(self): + return self.unpack("4f") + + def read_vec3x3(self): + return (self.read_vec3() for i in range(3)) + + def read_vec4x4(self): + return (self.read_vec4() for i in range(4)) + + def read_bool(self): + return bool(self.unpack("b")) + + def align(self, num): + current_pos = self.stream.tell() + align_bytes = num - (current_pos % num) + if align_bytes != num: + self.stream.seek(align_bytes, io.SEEK_CUR) + + +class ByteWriter(object): + def __init__(self, stream, endian='='): + self.stream = stream + self.endian = endian + + def write(self, *args): + return self.stream.write(*args) + + def pack(self, fmt, *values): + fmt = self.endian + fmt + fmt_size = struct.calcsize(fmt) + return self.stream.write(struct.pack(fmt, *values)) + + def write_char(self, val): + self.pack("c", val) + + def write_int8(self, val): + self.pack("b", val) + + def write_uint8(self, val): + self.pack("B", val) + + def write_int16(self, val): + self.pack("h", val) + + def write_uint16(self, val): + self.pack("H", val) + + def write_int32(self, val): + self.pack("l", val) + + def write_uint32(self, val): + self.pack("L", val) + + def write_int64(self, val): + self.pack("q", val) + + def write_uint64(self, val): + self.pack("Q", val) + + def write_float(self, val): + self.pack("f", val) + + def write_double(self, val): + self.pack("d", val) + + def write_vec3(self, val): + self.pack("4f", *val) + + def write_vec4(self, val): + self.pack("4f", *val) + + def write_vec3x3(self, val): + for vec in val: + self.write_vec3(vec) + + def write_vec4x4(self, val): + for vec in val: + self.write_vec4(vec) + + def write_bool(self, val): + self.pack("b", val) + + def align(self, num): + current_pos = self.stream.tell() + align_bytes = num - (current_pos % num) + if align_bytes != num: + self.stream.seek(align_bytes, io.SEEK_CUR) diff --git a/valve/source/messages.py b/valve/source/messages.py index d02c5e6..01abed1 100644 --- a/valve/source/messages.py +++ b/valve/source/messages.py @@ -6,10 +6,12 @@ import collections import struct +import io import six from . import util +from .byteio import ByteReader NO_SPLIT = -1 @@ -448,26 +450,54 @@ class InfoRequest(Message): ) -class InfoResponse(Message): +class InfoResponse(): - fields = ( - ByteField("response_type", validators=[lambda x: x == 0x49]), - ByteField("protocol"), - StringField("server_name"), - StringField("map"), - StringField("folder"), - StringField("game"), - ShortField("app_id"), - ByteField("player_count"), - ByteField("max_players"), - ByteField("bot_count"), - ServerTypeField("server_type"), - PlatformField("platform"), - ByteField("password_protected"), # BooleanField - ByteField("vac_enabled"), # BooleanField - StringField("version") - # TODO: EDF - ) + def __init__(self, packet=None): + if packet is not None: + self.read(packet) + + @staticmethod + def decode(packet): + return InfoResponse(packet) + + def read(self, packet): + stream = io.BytesIO(packet) + reader = ByteReader(stream) + + self.response_type = reader.read_uint8() + if self.response_type != 0x49: + raise BrokenMessageError( + "Invalid value ({}) for field 'response_type'" \ + .format(self.response_type)) + self.protocol = reader.read_uint8() + self.server_name = reader.read_cstring() + self.map = reader.read_cstring() + self.folder = reader.read_cstring() + self.game = reader.read_cstring() + self.app_id = reader.read_int16() + self.player_count = reader.read_uint8() + self.max_players = reader.read_uint8() + self.bot_count = reader.read_uint8() + self.server_type = util.ServerType(reader.read_uint8()) + self.platform = util.Platform(reader.read_uint8()) + self.password_protected = reader.read_bool() + self.vac_enabled = reader.read_bool() + self.version = reader.read_cstring() + try: + self.edf = reader.read_uint8() + except struct.error: + self.edf = 0 + if self.edf & 0x80: + self.port = reader.read_int16() + if self.edf & 0x10: + self.steam_id = reader.read_int64() + if self.edf & 0x40: + self.stv_port = reader.read_int16() + self.stv_name = reader.read_cstring() + if self.edf & 0x20: + self.keywords = reader.read_cstring() + if self.edf & 0x01: + self.game_id = reader.read_int64() class GetChallengeResponse(Message): From 37462ef04f40762c21e39c67c1fae7a9fccb7f36 Mon Sep 17 00:00:00 2001 From: Gabriel Huber Date: Sun, 5 Nov 2017 04:43:59 +0100 Subject: [PATCH 3/9] Rewrite message parser to use byteio streams --- valve/source/__init__.py | 6 +- valve/source/a2s.py | 29 +- valve/source/basequerier.py | 16 +- valve/source/byteio.py | 106 +++--- valve/source/master_server.py | 29 +- valve/source/messages.py | 673 +++++++++------------------------- valve/source/util.py | 50 +-- 7 files changed, 288 insertions(+), 621 deletions(-) diff --git a/valve/source/__init__.py b/valve/source/__init__.py index 9b248e8..7b54efd 100644 --- a/valve/source/__init__.py +++ b/valve/source/__init__.py @@ -1,6 +1,6 @@ -from __future__ import absolute_import - -from .basequerier import BaseQuerier, NoResponseError, QuerierClosedError +from .basequerier import BaseQuerier from .a2s import ServerQuerier from .master_server import MasterServerQuerier, Duplicates +from .util import (NoResponseError, QuerierClosedError, BrokenMessageError, + BufferExhaustedError) from .util import Platform, ServerType diff --git a/valve/source/a2s.py b/valve/source/a2s.py index 4914fdb..634e225 100644 --- a/valve/source/a2s.py +++ b/valve/source/a2s.py @@ -1,9 +1,6 @@ # -*- coding: utf-8 -*- # Copyright (C) 2013-2017 Oliver Ainsworth -from __future__ import (absolute_import, - unicode_literals, print_function, division) - import monotonic from .basequerier import BaseQuerier, NoResponseError @@ -39,20 +36,22 @@ def get_response(self): # or that the warning is no longer valid. response = messages.Header().decode(data) - if response["split"] == messages.SPLIT: - fragments = {} + if response.split == messages.SPLIT: fragment = messages.Fragment.decode(response.payload) if fragment.is_compressed: raise NotImplementedError("Fragments are compressed") - fragments[fragment["fragment_id"]] = fragment - while len(fragments) < fragment["fragment_count"]: + + fragments = [fragment] + while len(fragments) < fragment.fragment_count: data = BaseQuerier.get_response(self) fragment = messages.Fragment.decode( messages.Header.decode(data).payload) - fragments[fragment["fragment_id"]] = fragment - return b"".join([frag[1].payload for frag in - sorted(fragments.items(), key=lambda f: f[0])]) - return response.payload + fragments.append(fragment) + + fragments.sort(key=lambda f: f.fragment_id) + return b"".join(fragment.payload for fragment in fragments) + else: + return response.payload def ping(self): """Ping the server, returning the round-trip latency in milliseconds @@ -192,8 +191,8 @@ def players(self): # just use A2S_PLAYER to get challenge number which should work # fine for all servers self.request(messages.PlayersRequest(challenge=-1)) - challenge = messages.GetChallengeResponse.decode(self.get_response()) - self.request(messages.PlayersRequest(challenge=challenge["challenge"])) + challenge = messages.ChallengeResponse.decode(self.get_response()) + self.request(messages.PlayersRequest(challenge=challenge.challenge)) return messages.PlayersResponse.decode(self.get_response()) def rules(self): @@ -220,6 +219,6 @@ def rules(self): """ self.request(messages.RulesRequest(challenge=-1)) - challenge = messages.GetChallengeResponse.decode(self.get_response()) - self.request(messages.RulesRequest(challenge=challenge["challenge"])) + challenge = messages.ChallengeResponse.decode(self.get_response()) + self.request(messages.RulesRequest(challenge=challenge.challenge)) return messages.RulesResponse.decode(self.get_response()) diff --git a/valve/source/basequerier.py b/valve/source/basequerier.py index 1b38712..98fa1a7 100644 --- a/valve/source/basequerier.py +++ b/valve/source/basequerier.py @@ -1,26 +1,16 @@ # -*- coding: utf-8 -*- # Copyright (C) 2017 Oliver Ainsworth -from __future__ import (absolute_import, - unicode_literals, print_function, division) - import functools import select import socket import warnings -import six - - -class NoResponseError(Exception): - """Raised when a server querier doesn't receive a response.""" - +from .util import NoResponseError, QuerierClosedError -class QuerierClosedError(Exception): - """Raised when attempting to use a querier after it's closed.""" -class BaseQuerier(object): +class BaseQuerier(): """Base class for implementing source server queriers. When an instance of this class is initialised a socket is created. @@ -122,7 +112,7 @@ def get_response(self): try: data = ready[0][0].recv(1400) except socket.error as exc: - six.raise_from(NoResponseError(exc)) + raise NoResponseError(exc) return data del _check_open diff --git a/valve/source/byteio.py b/valve/source/byteio.py index ac67407..820927c 100644 --- a/valve/source/byteio.py +++ b/valve/source/byteio.py @@ -1,27 +1,42 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2017 Oliver Ainsworth + import struct import io -class ByteReader(object): - def __init__(self, stream, endian='='): +from .util import BufferExhaustedError + + + +class ByteReader(): + def __init__(self, stream, endian="=", encoding=None): self.stream = stream self.endian = endian + self.encoding = encoding - def read(self, *args): - return self.stream.read(*args) + def read(self, size=-1): + data = self.stream.read(size) + if size > -1 and len(data) != size: + raise BufferExhaustedError() + + return data + + def peek(self, size=-1): + cur_pos = self.stream.tell() + data = self.stream.read(size) + self.stream.seek(cur_pos, io.SEEK_SET) + return data def unpack(self, fmt): fmt = self.endian + fmt fmt_size = struct.calcsize(fmt) - return struct.unpack(fmt, self.stream.read(fmt_size)) + return struct.unpack(fmt, self.read(fmt_size)) def unpack_one(self, fmt): values = self.unpack(fmt) assert len(values) == 1 return values[0] - def read_char(self): - return self.unpack_one("c") - def read_int8(self): return self.unpack_one("b") @@ -52,6 +67,16 @@ def read_float(self): def read_double(self): return self.unpack_one("d") + def read_bool(self): + return bool(self.unpack("b")) + + def read_char(self): + char = self.unpack_one("c") + if self.encoding is not None: + return char.decode(self.encoding) + else: + return char + def read_cstring(self, charsize=1): string = b"" while True: @@ -60,34 +85,22 @@ def read_cstring(self, charsize=1): break else: string += c - return string - def read_vec3(self): - return self.unpack("4f") + if self.encoding is not None: + return string.decode(self.encoding) + else: + return string - def read_vec4(self): - return self.unpack("4f") + def read_ip(self): + octets = [self.read_uint8() for i in range(4)] + return ".".join(str(o) for o in octets) - def read_vec3x3(self): - return (self.read_vec3() for i in range(3)) - def read_vec4x4(self): - return (self.read_vec4() for i in range(4)) - - def read_bool(self): - return bool(self.unpack("b")) - - def align(self, num): - current_pos = self.stream.tell() - align_bytes = num - (current_pos % num) - if align_bytes != num: - self.stream.seek(align_bytes, io.SEEK_CUR) - - -class ByteWriter(object): - def __init__(self, stream, endian='='): +class ByteWriter(): + def __init__(self, stream, endian="=", encoding=None): self.stream = stream self.endian = endian + self.encoding = encoding def write(self, *args): return self.stream.write(*args) @@ -97,9 +110,6 @@ def pack(self, fmt, *values): fmt_size = struct.calcsize(fmt) return self.stream.write(struct.pack(fmt, *values)) - def write_char(self, val): - self.pack("c", val) - def write_int8(self, val): self.pack("b", val) @@ -130,25 +140,17 @@ def write_float(self, val): def write_double(self, val): self.pack("d", val) - def write_vec3(self, val): - self.pack("4f", *val) - - def write_vec4(self, val): - self.pack("4f", *val) - - def write_vec3x3(self, val): - for vec in val: - self.write_vec3(vec) - - def write_vec4x4(self, val): - for vec in val: - self.write_vec4(vec) - def write_bool(self, val): self.pack("b", val) - def align(self, num): - current_pos = self.stream.tell() - align_bytes = num - (current_pos % num) - if align_bytes != num: - self.stream.seek(align_bytes, io.SEEK_CUR) + def write_char(self, val): + if self.encoding is not None: + self.pack("c", val.encode(self.encoding)) + else: + self.pack("c", val) + + def write_cstring(self, val): + if self.encoding is not None: + self.write(val.encode(self.encoding) + b"\x00") + else: + self.write(val + b"\x00") diff --git a/valve/source/master_server.py b/valve/source/master_server.py index 0c8039b..dfe375f 100644 --- a/valve/source/master_server.py +++ b/valve/source/master_server.py @@ -1,19 +1,15 @@ # -*- coding: utf-8 -*- # Copyright (C) 2013-2017 Oliver Ainsworth -from __future__ import (absolute_import, - unicode_literals, print_function, division) - import enum import itertools -import six - from .basequerier import BaseQuerier, NoResponseError from . import messages from . import util + REGION_US_EAST_COAST = 0x00 REGION_US_WEST_COAST = 0x01 REGION_SOUTH_AMERICA = 0x02 @@ -27,6 +23,7 @@ MASTER_SERVER_ADDR = ("hl2master.steampowered.com", 27011) + class Duplicates(enum.Enum): """Behaviour for duplicate addresses. @@ -100,18 +97,19 @@ def _query(self, region, filter_string): while first_request or last_addr != "0.0.0.0:0": first_request = False self.request(messages.MasterServerRequest( - region=region, address=last_addr, filter=filter_string)) + region=region, address=last_addr, filter_=filter_string)) try: raw_response = self.get_response() except NoResponseError: return else: response = messages.MasterServerResponse.decode(raw_response) - for address in response["addresses"]: - last_addr = "{}:{}".format( - address["host"], address["port"]) + for address in response.addresses: if not address.is_null: - yield address["host"], address["port"] + yield (address.host, address.port) + + last_addr = "{}:{}".format( + response.addresses[-1].host, response.addresses[-1].port) def _deduplicate(self, method, query): """Deduplicate addresses in a :meth:`._query`. @@ -141,7 +139,7 @@ def _map_region(self, region): Returns a list of numeric region identifiers. """ - if isinstance(region, six.text_type): + if isinstance(region, str): try: regions = { "na-east": [REGION_US_EAST_COAST], @@ -288,20 +286,19 @@ def find(self, region="all", duplicates=Duplicates.SKIP, **filters): duplicates are excldued from the iterator returned by this method. See :class:`Duplicates` for controller this behaviour. """ - if isinstance(region, (int, six.text_type)): + if isinstance(region, (int, str)): regions = self._map_region(region) else: regions = [] for reg in region: regions.extend(self._map_region(reg)) filter_ = {} - for key, value in six.iteritems(filters): + for key, value in filters.items(): if key in {"secure", "linux", "empty", "full", "proxy", "noplayers", "white"}: value = int(bool(value)) elif key in {"gametype", "gamedata", "gamedataor"}: - value = [six.text_type(elt) - for elt in value if six.text_type(elt)] + value = [str(elt) for elt in value if str(elt)] if not value: continue value = ",".join(value) @@ -312,7 +309,7 @@ def find(self, region="all", duplicates=Duplicates.SKIP, **filters): value = util.ServerType(value).char else: value = value.char - filter_[key] = six.text_type(value) + filter_[key] = str(value) # Order doesn't actually matter, but it makes testing easier filter_ = sorted(filter_.items(), key=lambda pair: pair[0]) filter_string = "\\".join([part for pair in filter_ for part in pair]) diff --git a/valve/source/messages.py b/valve/source/messages.py index 01abed1..98a449a 100644 --- a/valve/source/messages.py +++ b/valve/source/messages.py @@ -1,474 +1,138 @@ # -*- coding: utf-8 -*- # Copyright (C) 2013 Oliver Ainsworth -from __future__ import (absolute_import, - unicode_literals, print_function, division) - -import collections import struct import io -import six - -from . import util -from .byteio import ByteReader +from .util import Platform, ServerType +from .util import BrokenMessageError, BufferExhaustedError +from .byteio import ByteReader, ByteWriter NO_SPLIT = -1 SPLIT = -2 +A2S_PLAYER_REQUEST = 0x55 +A2S_PLAYER_RESPONSE = 0x44 +A2S_INFO_REQUEST = 0x54 +A2S_INFO_RESPONSE = 0x49 +A2S_RULES_REQUEST = 0x56 +A2S_RULES_RESPONSE = 0x45 +A2S_CHALLENGE_RESPONSE = 0x41 -class BrokenMessageError(Exception): - pass - - -class BufferExhaustedError(BrokenMessageError): - - def __init__(self, message="Incomplete message"): - BrokenMessageError.__init__(self, message) - - -def use_default(func): - def use_default(self, value=None, values={}): - if value is None: - return func(self, self.default_value, values) - return func(self, value, values) - return use_default - - -def needs_buffer(func): - def needs_buffer(self, buffer, *args, **kwargs): - if len(buffer) == 0: - raise BufferExhaustedError - return func(self, buffer, *args, **kwargs) - return needs_buffer - +MASTER_SERVER_REQUEST = 0x31 -class MessageField(object): - fmt = None - validators = [] - def __init__(self, name, optional=False, - default_value=None, validators=[]): - """ - name -- used when decoding messages to set the key in the - returned dictionary +class Message(): - optional -- whether or not a field value must be provided - when encoding - - default_value -- if optional is False, the value that is - used if none is specified - - validators -- list of callables that return False if the - value they're passed is invalid - """ + def __getitem__(self, key): + return getattr(self, key) - if self.fmt is not None: - if self.fmt[0] not in "@=<>!": - self.format = "<" + self.fmt - else: - self.format = self.fmt - if six.PY2: - # Struct only accepts bytes - self.format = self.format.encode("ascii") - self.name = name - self.optional = optional - self._value = default_value - self.validators = self.__class__.validators + validators + def __setitem__(self, key, value): + setattr(self, key, value) - @property - def default_value(self): - if self.optional: - if self._value is not None: - return self._value - raise ValueError( - "Field '{fname}' is not optional".format(fname=self.name)) - - def validate(self, value): - for validator in self.validators: - try: - if not validator(value): - raise ValueError - except Exception: - raise BrokenMessageError( - "Invalid value ({}) for field '{}'".format( - value, self.name)) - return value - - @use_default - def encode(self, value, values={}): + @classmethod + def decode(cls, packet): + stream = io.BytesIO(packet) + instance = cls() try: - return struct.pack(self.format, self.validate(value)) + instance.read(stream) except struct.error as exc: raise BrokenMessageError(exc) + return instance - @needs_buffer - def decode(self, buffer, values={}): - """ - Accepts a string of raw bytes which it will attempt to - decode into some Python object which is returned. All - remaining data left in the buffer is also returned which - may be an empty string. - - Also acecpts a second argument which is a dictionary of the - fields that have been decoded so far (i.e. occurs before - this field in `fields` tuple). This allows the decoder to - adapt it's funtionality based on the value of other fields - if needs be. - - For example, in the case of A2S_PLAYER resposnes, the field - `player_count` needs to be accessed at decode-time to determine - how many player entries to attempt to decode. - """ - - field_size = struct.calcsize(self.format) - if len(buffer) < field_size: - raise BufferExhaustedError - field_data = buffer[:field_size] - left_overs = buffer[field_size:] + def encode(self): + stream = io.BytesIO() try: - return (self.validate( - struct.unpack(self.format, field_data)[0]), left_overs) + self.write(stream) except struct.error as exc: raise BrokenMessageError(exc) + return stream.getvalue() + def _raise_unexpected(self, values): + if values: + raise TypeError("{} got an unexpected value {!r}".format( + self.__class__.__name__, next(iter(values)))) -class ByteField(MessageField): - fmt = "B" - - -class StringField(MessageField): - fmt = "s" - - @use_default - def encode(self, value, values={}): - return value.encode("utf8") + b"\x00" - - @needs_buffer - def decode(self, buffer, values={}): - terminator = buffer.find(b"\x00") - if terminator == -1: - raise BufferExhaustedError("No string terminator") - field_size = terminator + 1 - field_data = buffer[:field_size-1] - left_overs = buffer[field_size:] - return field_data.decode("utf8", "ignore"), left_overs - + def _validate_response_type(self, response_type): + if self.response_type != response_type: + raise BrokenMessageError( + "Invalid value ({}) for field 'response_type'".format( + self.response_type)) -class ShortField(MessageField): - fmt = "h" +class Header(Message): -class LongField(MessageField): - fmt = "l" - - -class FloatField(MessageField): - fmt = "f" - - -class PlatformField(ByteField): - - @needs_buffer - def decode(self, buffer, values={}): - byte, remnant_buffer = super(PlatformField, - self).decode(buffer, values) - return util.Platform(byte), remnant_buffer - - -class ServerTypeField(ByteField): - - @needs_buffer - def decode(self, buffer, values={}): - byte, remnant_buffer = super(ServerTypeField, - self).decode(buffer, values) - return util.ServerType(byte), remnant_buffer - - -class MessageArrayField(MessageField): - """ - Represents a nested message within another message that is - repeated a given number of time (often defined within the - same message.) - """ - - def __init__(self, name, element, count=None): - """ - element -- the Message subclass that will attempt to be decoded - - count -- ideally a callable that returns the number of - 'elements' to attempt to decode; count must also present - a 'minimum' attribute which is minimum number of elements - that must be decoded or else raise BrokenMessageError - - If count isn't callable (e.g. a number) it will be - wrapped in a function with the minimum attribute set - equal to the given 'count' value - - Helper static methods all(), value_of() and at_least() - are provided which are intended to be used as the - 'count' argument, e.g. - - MessageArrayField("", SubMessage, MessageArrayField.all()) - - ... will decode all SubMessages within the buffer - """ - - MessageField.__init__(self, name) - if count is None: - count = self.all() - # Coerces the count argument to be a callable. For example, - # in most cases count would be a Message.value_of(), however - # if an integer is provided it will be wrapped in a lambda. - self.count = count - if not hasattr(count, "__call__"): - - def const_count(values={}): - return count - - const_count.minimum = count - self.count = const_count - self.element = element - - def encode(self, elements, values={}): - buf = [] - for i, element in enumerate(elements): - if not isinstance(element, self.element): - raise BrokenMessageError( - "Element {} ({}) is not instance of {}".format( - i, element, self.element.__name__)) - if i + 1 > self.count(values): - raise BrokenMessageError("Too many elements") - buf.append(element.encode()) - if len(buf) < self.count.minimum: - raise BrokenMessageError("Too few elements") - return b"".join(buf) - - def decode(self, buffer, values={}): - entries = [] - count = 0 - while count < self.count(values): - # Set start_buffer to the beginning of the buffer so that in - # the case of buffer exhaustion it can return from the - # start of the entry, not half-way through it. - # - # For example if you had the fields: - # - # ComplexField = - # LongField - # ShortField - # - # MessageArrayField(ComplexField, - # count=MessageArrayField.all()) - # ByteField() - # - # When attempting to decode the end of the buffer FF FF FF FF 00 - # the first four bytes will be consumed by LongField, - # however ShortField will fail with BufferExhaustedError as - # there's only one byte left. However, there is enough left - # for the trailing ByteField. So when ComplexField - # propagates ShortField's BufferExhaustedError the buffer will - # only have the 00 byte remaining. The exception if caught - # and buffer reverted to FF FF FF FF 00. This is passed - # to ByteField which consumes one byte and the reamining - # FF FF FF 00 bytes and stored as message payload. - # - # This is very much an edge case. :/ - start_buffer = buffer - try: - entry = self.element.decode(buffer) - buffer = entry.payload - entries.append(entry) - count += 1 - except (BufferExhaustedError, BrokenMessageError) as exc: - # Allow for returning 'at least something' if end of - # buffer is reached. - if count < self.count.minimum: - raise BrokenMessageError(exc) - buffer = start_buffer - break - return entries, buffer - - @staticmethod - def value_of(name): - """ - Reference another field's value as the argument 'count'. - """ - - def field(values={}, f=None): - f.minimum = values[name] - return values[name] - - if six.PY3: - field.__defaults__ = (field,) - else: - field.func_defaults = (field,) - return field - - @staticmethod - def all(): - """ - Decode as much as possible from the buffer. - - Note that if a full element field cannot be decoded it will - return all entries decoded up to that point, and reset the - buffer to the start of the entry which raised the - BufferExhaustedError. So it is possible to have addtional - fields follow a MessageArrayField and have - count=MessageArrayField.all() as long as the size of the - trailing fields < size of the MessageArrayField element. - """ - - i = [1] - - def all_(values={}): - i[0] = i[0] + 1 - return i[0] - - all_.minimum = -1 - return all_ - - @staticmethod - def at_least(minimum): - """ - Decode at least 'minimum' number of entries. - """ - - i = [1] - - def at_least(values={}): - i[0] = i[0] + 1 - return i[0] - - at_least.minimum = minimum - return at_least - - -class MessageDictField(MessageArrayField): - """ - Decodes a series of key-value pairs from a message. Functionally - identical to MessageArrayField except the results are returned as - a dictionary instead of a list. - """ - - def __init__(self, name, key_field, value_field, count=None): - """ - key_field and value_field are the respective components - of the name-value pair that are to be decoded. The fields - should have unique name strings. Tt is assumed that the - key-field comes first, followed by the value. - - count is the same as MessageArrayField. - """ - - element = type("KeyValueField" if six.PY3 else b"KeyValueField", - (Message,), {"fields": (key_field, value_field)}) - self.key_field = key_field - self.value_field = value_field - MessageArrayField.__init__(self, name, element, count) - - def decode(self, buffer, values={}): - entries, buffer = MessageArrayField.decode(self, buffer, values) - entries_dict = {} - for entry in entries: - entries_dict[entry[ - self.key_field.name]] = entry[self.value_field.name] - return entries_dict, buffer - - -class Message(collections.Mapping): - - fields = () - - def __init__(self, payload=None, **field_values): - self.fields = self.__class__.fields - self.payload = payload - self.values = field_values - - def __getitem__(self, key): - return self.values[key] + def __init__(self, **values): + if not values: + return - def __setitem__(self, key, value): - self.values[key] = value + self.split = values.pop("split") + if self.split not in [SPLIT, NO_SPLIT]: + raise BrokenMessageError( + "Invalid value ({}) for field 'split'".format( + self.split)) + self.payload = values.pop("payload", b"") - def __delitem__(self, key): - del self.values[key] + self._raise_unexpected(values) - def __len__(self): - return len(self.values) + def write(self, stream): + writer = ByteWriter(stream, endian="<", encoding="utf-8") - def __iter__(self): - return iter(self.values) + writer.write_int32(self.split) + writer.write(self.payload) - def encode(self, **field_values): - values = dict(self.values, **field_values) - buf = [] - for field in self.fields: - buf.append(field.encode(values.get(field.name, None), values)) - return b"".join(buf) + def read(self, stream): + reader = ByteReader(stream, endian="<", encoding="utf-8") - @classmethod - def decode(cls, packet): - buffer = packet - values = {} - for field in cls.fields: - values[field.name], buffer = field.decode(buffer, values) - return cls(buffer, **values) + self.split = reader.read_int32() + self.payload = reader.read() -class Header(Message): +class Fragment(Message): - fields = ( - LongField("split", validators=[lambda x: x in [SPLIT, NO_SPLIT]]), - ) + def read(self, stream): + reader = ByteReader(stream, endian="<", encoding="utf-8") + self.message_id = reader.read_int32() + self.fragment_count = reader.read_uint8() + self.fragment_id = reader.read_uint8() + self.mtu = reader.read_int16() -class Fragment(Message): + if self.is_compressed: + self.decompressed_size = reader.read_int32() + self.crc = reader.read_int32() - fields = ( - LongField("message_id"), - ByteField("fragment_count"), - ByteField("fragment_id"), # 0-indexed - ShortField("mtu") - ) + self.payload = reader.read() @property def is_compressed(self): - return bool(self["message_id"] & 2**(2*8)) - - -# TODO: FragmentCompressionData + return bool(self.message_id & (1 << 16)) class InfoRequest(Message): + def __init__(self, **values): + self.request_type = values.pop("request_type", A2S_INFO_REQUEST) + self.payload = values.pop("payload", "Source Engine Query") - fields = ( - ByteField("request_type", True, 0x54), - StringField("payload", True, "Source Engine Query") - ) + self._raise_unexpected(values) + def write(self, stream): + writer = ByteWriter(stream, endian="<", encoding="utf-8") -class InfoResponse(): + writer.write_uint8(self.request_type) + writer.write_cstring(self.payload) - def __init__(self, packet=None): - if packet is not None: - self.read(packet) - @staticmethod - def decode(packet): - return InfoResponse(packet) +class InfoResponse(Message): - def read(self, packet): - stream = io.BytesIO(packet) - reader = ByteReader(stream) + def read(self, stream): + reader = ByteReader(stream, endian="<", encoding="utf-8") self.response_type = reader.read_uint8() - if self.response_type != 0x49: - raise BrokenMessageError( - "Invalid value ({}) for field 'response_type'" \ - .format(self.response_type)) + self._validate_response_type(A2S_INFO_RESPONSE) + self.protocol = reader.read_uint8() self.server_name = reader.read_cstring() self.map = reader.read_cstring() @@ -478,15 +142,17 @@ def read(self, packet): self.player_count = reader.read_uint8() self.max_players = reader.read_uint8() self.bot_count = reader.read_uint8() - self.server_type = util.ServerType(reader.read_uint8()) - self.platform = util.Platform(reader.read_uint8()) + self.server_type = ServerType(reader.read_uint8()) + self.platform = Platform(reader.read_uint8()) self.password_protected = reader.read_bool() self.vac_enabled = reader.read_bool() self.version = reader.read_cstring() + try: self.edf = reader.read_uint8() - except struct.error: + except BufferExhaustedError: self.edf = 0 + if self.edf & 0x80: self.port = reader.read_int16() if self.edf & 0x10: @@ -500,119 +166,142 @@ def read(self, packet): self.game_id = reader.read_int64() -class GetChallengeResponse(Message): +class ChallengeResponse(Message): + + def read(self, stream): + reader = ByteReader(stream, endian="<", encoding="utf-8") + + self.response_type = reader.read_uint8() + self._validate_response_type(A2S_CHALLENGE_RESPONSE) - fields = ( - ByteField("response_type", validators=[lambda x: x == 0x41]), - LongField("challenge") - ) + self.challenge = reader.read_int32() class PlayersRequest(Message): + def __init__(self, challenge, **values): + self.request_type = values.pop("request_type", A2S_PLAYER_REQUEST) + self.challenge = challenge - fields = ( - ByteField("request_type", True, 0x55), - LongField("challenge") - ) + self._raise_unexpected(values) + + def write(self, stream): + writer = ByteWriter(stream, endian="<", encoding="utf-8") + + writer.write_uint8(self.request_type) + writer.write_int32(self.challenge) class PlayerEntry(Message): - fields = ( - ByteField("index"), - StringField("name"), - LongField("score"), - FloatField("duration") - ) + def read(self, stream): + reader = ByteReader(stream, endian="<", encoding="utf-8") + + self.index = reader.read_uint8() + self.name = reader.read_cstring() + self.score = reader.read_int32() + self.duration = reader.read_float() class PlayersResponse(Message): - fields = ( - ByteField("response_type", validators=[lambda x: x == 0x44]), - ByteField("player_count"), - MessageArrayField("players", - PlayerEntry, - MessageArrayField.value_of("player_count")) - ) + def read(self, stream): + reader = ByteReader(stream, endian="<", encoding="utf-8") + + self.response_type = reader.read_uint8() + self._validate_response_type(A2S_PLAYER_RESPONSE) + + self.player_count = reader.read_uint8() + self.players = [] + for player_num in range(self.player_count): + player = PlayerEntry() + player.read(stream) + self.players.append(player) class RulesRequest(Message): - fields = ( - ByteField("request_type", True, 0x56), - LongField("challenge") - ) + def __init__(self, challenge, **values): + self.request_type = values.pop("request_type", A2S_RULES_REQUEST) + self.challenge = challenge + + self._raise_unexpected(values) + + def write(self, stream): + writer = ByteWriter(stream, endian="<", encoding="utf-8") + + writer.write_uint8(self.request_type) + writer.write_int32(self.challenge) class RulesResponse(Message): - fields = ( - ByteField("response_type", validators=[lambda x: x == 0x45]), - ShortField("rule_count"), - MessageDictField("rules", - StringField("key"), - StringField("value"), - MessageArrayField.value_of("rule_count")) - ) + def read(self, stream): + reader = ByteReader(stream, endian="<", encoding="utf-8") - @classmethod - def decode(cls, packet): # A2S_RESPONSE misteriously seems to add a FF FF FF FF # long to the beginning of the response which isn't # mentioned on the wiki. # # Behaviour witnessed with TF2 server 94.23.226.200:2045 # As of 2015-11-22, Quake Live servers on steam do not - if packet.startswith(b'\xff\xff\xff\xff'): - packet = packet[4:] - return super(cls, RulesResponse).decode(packet) + if reader.peek(4) == b"\xFF\xFF\xFF\xFF": + reader.read(4) -# For Master Server -class MSAddressEntryPortField(MessageField): - fmt = "!H" + self.response_type = reader.read_uint8() + self._validate_response_type(A2S_RULES_RESPONSE) + self.rule_count = reader.read_int16() + self.rules = {} + for rule_num in range(self.rule_count): + name = reader.read_cstring() + value = reader.read_cstring() + self.rules[name] = value -class MSAddressEntryIPField(MessageField): - @needs_buffer - def decode(self, buffer, values={}): - if len(buffer) < 4: - raise BufferExhaustedError - field_data = buffer[:4] - left_overs = buffer[4:] - return (".".join(six.text_type(b) for b in - struct.unpack(b"".format(self=self) - def __unicode__(self): + def __str__(self): return { 76: "Linux", 108: "Linux", @@ -79,16 +89,6 @@ def __unicode__(self): 119: "Windows", }[self.value] - if six.PY3: - def __str__(self): - return self.__unicode__() - - def __bytes__(self): - return self.__unicode__().encode(sys.getdefaultencoding()) - else: - def __str__(self): - return self.__unicode__().encode(sys.getdefaultencoding()) - def __int__(self): return self.value @@ -142,7 +142,7 @@ def os_name(self): Platform.WINDOWS = Platform(119) -class ServerType(object): +class ServerType(): """A Source server platform identifier This class provides utilities for representing Source server types @@ -181,7 +181,7 @@ def __init__(self, value): * Non-Dedicated * SourceTV """ - if isinstance(value, six.text_type): + if isinstance(value, str): if len(value) == 1: value = ord(value) else: @@ -202,7 +202,7 @@ def __repr__(self): return "<{self.__class__.__name__} " \ "{self.value} '{self}'>".format(self=self) - def __unicode__(self): + def __str__(self): return { 68: "Dedicated", 100: "Dedicated", @@ -210,16 +210,6 @@ def __unicode__(self): 112: "SourceTV", }[self.value] - if six.PY3: - def __str__(self): - return self.__unicode__() - - def __bytes__(self): - return self.__unicode__().encode(sys.getdefaultencoding()) - else: - def __str__(self): - return self.__unicode__().encode(sys.getdefaultencoding()) - def __int__(self): return self.value From 19def2f6a3d59d2422c6e21284a25ea500c88b8c Mon Sep 17 00:00:00 2001 From: Gabriel Huber Date: Sun, 5 Nov 2017 04:56:09 +0100 Subject: [PATCH 4/9] Steam now uses 0 as the default challenge --- valve/source/a2s.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/valve/source/a2s.py b/valve/source/a2s.py index 634e225..6fa7059 100644 --- a/valve/source/a2s.py +++ b/valve/source/a2s.py @@ -190,7 +190,7 @@ def players(self): # TF2 and L4D2's A2S_SERVERQUERY_GETCHALLENGE doesn't work so # just use A2S_PLAYER to get challenge number which should work # fine for all servers - self.request(messages.PlayersRequest(challenge=-1)) + self.request(messages.PlayersRequest(challenge=0)) challenge = messages.ChallengeResponse.decode(self.get_response()) self.request(messages.PlayersRequest(challenge=challenge.challenge)) return messages.PlayersResponse.decode(self.get_response()) @@ -218,7 +218,7 @@ def rules(self): +--------------------+------------------------------------------------+ """ - self.request(messages.RulesRequest(challenge=-1)) + self.request(messages.RulesRequest(challenge=0)) challenge = messages.ChallengeResponse.decode(self.get_response()) self.request(messages.RulesRequest(challenge=challenge.challenge)) return messages.RulesResponse.decode(self.get_response()) From cdf5bd9b77dc7f2d4e8dc4fe3efe93bc871e6d32 Mon Sep 17 00:00:00 2001 From: Gabriel Huber Date: Sun, 5 Nov 2017 05:02:40 +0100 Subject: [PATCH 5/9] Handle player responses without a challenge step Fixes #29 --- valve/source/a2s.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/valve/source/a2s.py b/valve/source/a2s.py index 6fa7059..7fbc314 100644 --- a/valve/source/a2s.py +++ b/valve/source/a2s.py @@ -191,9 +191,15 @@ def players(self): # just use A2S_PLAYER to get challenge number which should work # fine for all servers self.request(messages.PlayersRequest(challenge=0)) - challenge = messages.ChallengeResponse.decode(self.get_response()) - self.request(messages.PlayersRequest(challenge=challenge.challenge)) - return messages.PlayersResponse.decode(self.get_response()) + resp = self.get_response() + if not resp: + raise BrokenMessageError("Empty Response") + elif resp[0] == messages.A2S_CHALLENGE_RESPONSE: + challenge = messages.ChallengeResponse.decode(resp) + self.request(messages.PlayersRequest(challenge=challenge.challenge)) + resp = self.get_response() + + return messages.PlayersResponse.decode(resp) def rules(self): """Retreive the server's game mode configuration From 3918869bf4fad36ce429182f857fc1a138818062 Mon Sep 17 00:00:00 2001 From: Gabriel Huber Date: Mon, 6 Nov 2017 01:39:32 +0100 Subject: [PATCH 6/9] Remove getitem and setitem methods While they provide nice backwards compatibility, I don't think they're worth the trouble when debugging. --- valve/source/messages.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/valve/source/messages.py b/valve/source/messages.py index 98a449a..7c323c6 100644 --- a/valve/source/messages.py +++ b/valve/source/messages.py @@ -26,12 +26,6 @@ class Message(): - def __getitem__(self, key): - return getattr(self, key) - - def __setitem__(self, key, value): - setattr(self, key, value) - @classmethod def decode(cls, packet): stream = io.BytesIO(packet) From f9ccf763ea838a5b0d33a5267235febcda3b0897 Mon Sep 17 00:00:00 2001 From: Gabriel Huber Date: Mon, 6 Nov 2017 02:17:54 +0100 Subject: [PATCH 7/9] Correct signedness of some values --- valve/source/messages.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/valve/source/messages.py b/valve/source/messages.py index 7c323c6..67dcb72 100644 --- a/valve/source/messages.py +++ b/valve/source/messages.py @@ -132,7 +132,7 @@ def read(self, stream): self.map = reader.read_cstring() self.folder = reader.read_cstring() self.game = reader.read_cstring() - self.app_id = reader.read_int16() + self.app_id = reader.read_uint16() self.player_count = reader.read_uint8() self.max_players = reader.read_uint8() self.bot_count = reader.read_uint8() @@ -148,16 +148,16 @@ def read(self, stream): self.edf = 0 if self.edf & 0x80: - self.port = reader.read_int16() + self.port = reader.read_uint16() if self.edf & 0x10: - self.steam_id = reader.read_int64() + self.steam_id = reader.read_uint64() if self.edf & 0x40: - self.stv_port = reader.read_int16() + self.stv_port = reader.read_uint16() self.stv_name = reader.read_cstring() if self.edf & 0x20: self.keywords = reader.read_cstring() if self.edf & 0x01: - self.game_id = reader.read_int64() + self.game_id = reader.read_uint64() class ChallengeResponse(Message): From 8601816f0cfac8a1de444050a7f16f40b5dabf53 Mon Sep 17 00:00:00 2001 From: Gabriel Huber Date: Tue, 7 Nov 2017 04:25:35 +0100 Subject: [PATCH 8/9] Simplify IP unpacking --- valve/source/byteio.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/valve/source/byteio.py b/valve/source/byteio.py index 820927c..5fe111c 100644 --- a/valve/source/byteio.py +++ b/valve/source/byteio.py @@ -92,8 +92,7 @@ def read_cstring(self, charsize=1): return string def read_ip(self): - octets = [self.read_uint8() for i in range(4)] - return ".".join(str(o) for o in octets) + return ".".join(str(o) for o in self.unpack("BBBB")) class ByteWriter(): From ff1b74bd63fe0abbc385446a25feddfa23940e11 Mon Sep 17 00:00:00 2001 From: Gabriel Huber Date: Fri, 12 Apr 2019 00:39:07 +0200 Subject: [PATCH 9/9] Increase response receive buffer size --- valve/source/basequerier.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/valve/source/basequerier.py b/valve/source/basequerier.py index 98fa1a7..0258c13 100644 --- a/valve/source/basequerier.py +++ b/valve/source/basequerier.py @@ -110,7 +110,7 @@ def get_response(self): if not ready[0]: raise NoResponseError("Timed out waiting for response") try: - data = ready[0][0].recv(1400) + data = ready[0][0].recv(65536) except socket.error as exc: raise NoResponseError(exc) return data