Passed
Push — master ( c3c03e...52831e )
by Steffen
02:16
created

GameServers.process_packet()   B

Complexity

Conditions 6

Size

Total Lines 31
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 6.6829

Importance

Changes 0
Metric Value
cc 6
eloc 15
nop 3
dl 0
loc 31
ccs 11
cts 15
cp 0.7332
crap 6.6829
rs 8.6666
c 0
b 0
f 0
1
#!/usr/bin/python
2
# -*- coding: utf-8 -*-
3 1
import logging
4 1
import socket
5 1
from collections import deque
6 1
from time import sleep
7 1
from typing import List
8
9 1
from tw_serverinfo import Network
10 1
from tw_serverinfo.models.game_server import GameServer
11 1
from tw_serverinfo.models.player import Player
12
13
14 1
class GameServers(object):
15
    """GameServers Class containing the logic to request and parse responses from the game servers"""
16 1
    SERVER_CHUNK_SIZE = 50
17 1
    SERVER_CHUNK_SLEEP_MS = 1000
18
19 1
    _game_servers = []
20
21 1
    def fill_server_info(self, game_servers: List[GameServer]) -> None:
22
        """Send the SERVERBROWSE_GETINFO and SERVERBROWSE_GETINFO_64_LEGACY packet to each game server in the
23
        passed game_servers list, parse the response and update the GameServer objects in the game_servers argument
24
25
        :type game_servers: list
26
        :return:
27
        """
28 1
        self._game_servers = game_servers
29
        # create an udp protocol socket
30 1
        sock = socket.socket(family=Network.PROTOCOL_FAMILY, type=socket.SOCK_DGRAM)
31
        # set the socket to non blocking to allow parallel requests
32 1
        sock.setblocking(False)
33
34 1
        i = 0
35 1
        for game_server in self._game_servers:  # type: GameServer
36 1
            i += 1
37 1
            Network.send_packet(sock=sock, data=Network.PACKETS['SERVERBROWSE_GETINFO'], extra_data=b'xe',
38
                                server=game_server)
39 1
            Network.send_packet(sock=sock, data=Network.PACKETS['SERVERBROWSE_GETINFO_64_LEGACY'], extra_data=b'xe',
40
                                server=game_server)
41
42 1
            if i % self.SERVER_CHUNK_SIZE == 0 or i >= len(self._game_servers):
43 1
                duration_without_response = Network.CONNECTION_SLEEP_DURATION
44 1
                sleep(Network.CONNECTION_SLEEP_DURATION / 1000.0)
45
46 1
                while duration_without_response < Network.CONNECTION_TIMEOUT:
47 1
                    if not Network.receive_packet(sock, self._game_servers, self.process_packet):
48
                        # increase the measured duration without a response and sleep for the set duration
49 1
                        duration_without_response += Network.CONNECTION_SLEEP_DURATION
50 1
                        sleep(Network.CONNECTION_SLEEP_DURATION / 1000.0)
51
                    else:
52
                        # if we got a response reset the duration in case we receive multiple packets
53 1
                        duration_without_response = 0
54
55
        # close the socket after checking all servers
56 1
        sock.close()
57
58 1
    def process_packet(self, data: bytes, server: GameServer) -> None:
59
        """Process packet function for
60
         - SERVERBROWSE_COUNT
61
         - SERVERBROWSE_LIST
62
        packets
63
64
        :type data: bytes
65
        :type server: GameServer
66
        :return:
67
        """
68 1
        server.response = True
69 1
        slots = deque(data[14:].split(b"\x00"))
70
71
        # validate the server token
72 1
        token = int(slots.popleft().decode('utf-8'))
73 1
        if int.from_bytes(server.token, byteorder='big') != token & 0xff:
74
            logging.log(logging.INFO, 'token validation failed for GameServer response. GameServer: ' + str(server))
75
            return
76
77 1
        if data[6:6 + 8] == Network.PACKETS['SERVERBROWSE_INFO']:
78
            # vanilla (inf3) packet
79 1
            self.parse_vanilla_response(slots, server)
80 1
        elif data[6:6 + 8] == Network.PACKETS['SERVERBROWSE_INFO_64_LEGACY']:
81
            # 64 legacy (dtsf) packet
82 1
            self.parse_64_legacy_response(slots, server)
83 1
        elif data[6:6 + 8] == Network.PACKETS['SERVERBROWSE_INFO_EXTENDED']:
84
            # extended response (iext) packet, current default of DDNet
85 1
            self.parse_extended_response(slots, server, token)
86
        elif data[6:6 + 8] == Network.PACKETS['SERVERBROWSE_INFO_EXTENDED_MORE']:
87
            # extended response (iex+) packet
88
            self.parse_extended_more_response(slots, server)
89
90 1
    @staticmethod
91 1
    def parse_vanilla_response(slots: deque, server: GameServer) -> None:
92
        """Parse the default response of the vanilla client
93
94
        :type slots: deque
95
        :type server: GameServer
96
        :return:
97
        """
98 1
        server.server_type = 'vanilla'
99 1
        server.version = slots.popleft().decode('utf-8')
100 1
        server.name = slots.popleft().decode('utf-8')
101 1
        server.map_name = slots.popleft().decode('utf-8')
102 1
        server.game_type = slots.popleft().decode('utf-8')
103 1
        server.flags = int(slots.popleft().decode('utf-8'))
104 1
        server.num_players = int(slots.popleft().decode('utf-8'))
105 1
        server.max_players = int(slots.popleft().decode('utf-8'))
106 1
        server.num_clients = int(slots.popleft().decode('utf-8'))
107 1
        server.max_clients = int(slots.popleft().decode('utf-8'))
108
109 1
        while len(slots) >= 5:
110 1
            server.append_player(Player(
111
                name=slots.popleft().decode('utf-8'),
112
                clan=slots.popleft().decode('utf-8'),
113
                country=int(slots.popleft().decode('utf-8')),
114
                score=int(slots.popleft().decode('utf-8')),
115
                ingame=bool(int(slots.popleft().decode('utf-8')))
116
            ))
117
118 1
    @staticmethod
119 1
    def parse_64_legacy_response(slots: deque, server: GameServer) -> None:
120
        """Parse the 64 slot legacy response
121
122
        :type slots: deque
123
        :type server: GameServer
124
        :return:
125
        """
126 1
        server.server_type = '64_legacy'
127 1
        server.version = slots.popleft().decode('utf-8')
128 1
        server.name = slots.popleft().decode('utf-8')
129 1
        server.map_name = slots.popleft().decode('utf-8')
130 1
        server.game_type = slots.popleft().decode('utf-8')
131 1
        server.flags = int(slots.popleft().decode('utf-8'))
132 1
        server.num_players = int(slots.popleft().decode('utf-8'))
133 1
        server.max_players = int(slots.popleft().decode('utf-8'))
134 1
        server.num_clients = int(slots.popleft().decode('utf-8'))
135 1
        server.max_clients = int(slots.popleft().decode('utf-8'))
136
137
        # contains sometimes control bytes or \x00 bytes, not sure what it is
138
        # since we split at \x00 bytes remove if it is currently \x00 else, split it from the next element
139 1
        if slots[0] == b'':
140 1
            slots.popleft()
141
        else:
142 1
            slots.appendleft(slots.popleft()[1:])
143
144 1
        while len(slots) >= 5:
145 1
            server.append_player(Player(
146
                name=slots.popleft().decode('utf-8'),
147
                clan=slots.popleft().decode('utf-8'),
148
                country=int(slots.popleft().decode('utf-8')),
149
                score=int(slots.popleft().decode('utf-8')),
150
                ingame=bool(int(slots.popleft().decode('utf-8')))
151
            ))
152
153 1
    @staticmethod
154 1
    def parse_extended_response(slots: deque, server: GameServer, token: int) -> None:
155
        """Parse the extended server info response(default for DDNet)
156
157
        :type slots: deque
158
        :type server: GameServer
159
        :type token: int
160
        :return:
161
        """
162 1
        if (token & 0xffff00) >> 8 != (server.request_token[0] << 8) + server.request_token[1]:
163
            # additional server token validation failed
164
            logging.log(logging.INFO, 'request token validation failed for GameServer response. GameServer: '
165
                        + str(server))
166
            return
167
168 1
        server.server_type = 'ext'
169 1
        server.version = slots.popleft().decode('utf-8')
170 1
        server.name = slots.popleft().decode('utf-8')
171 1
        server.map_name = slots.popleft().decode('utf-8')
172 1
        server.map_crc = int(slots.popleft().decode('utf-8'))
173 1
        server.map_size = int(slots.popleft().decode('utf-8'))
174 1
        server.game_type = slots.popleft().decode('utf-8')
175 1
        server.flags = int(slots.popleft().decode('utf-8'))
176 1
        server.num_players = int(slots.popleft().decode('utf-8'))
177 1
        server.max_players = int(slots.popleft().decode('utf-8'))
178 1
        server.num_clients = int(slots.popleft().decode('utf-8'))
179 1
        server.max_clients = int(slots.popleft().decode('utf-8'))
180
181 1 View Code Duplication
        while len(slots) >= 6:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
182
            # no idea what this is, always empty
183 1
            slots.popleft()
184 1
            server.append_player(Player(
185
                name=slots.popleft().decode('utf-8'),
186
                clan=slots.popleft().decode('utf-8'),
187
                country=int(slots.popleft().decode('utf-8')),
188
                score=int(slots.popleft().decode('utf-8')),
189
                ingame=bool(int(slots.popleft().decode('utf-8')))
190
            ))
191
192 1
    @staticmethod
193 1
    def parse_extended_more_response(slots: deque, server: GameServer) -> None:
194
        """Parse the extended server info response(default for DDNet)
195
196
        :type slots: deque
197
        :type server: GameServer
198
        :return:
199
        """
200
        slots.popleft()
201
202
        server.server_type = 'ext+'
203 View Code Duplication
        while len(slots) >= 6:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
204
            # no idea what this is, always empty
205
            slots.popleft()
206
            server.append_player(Player(
207
                name=slots.popleft().decode('utf-8'),
208
                clan=slots.popleft().decode('utf-8'),
209
                country=int(slots.popleft().decode('utf-8')),
210
                score=int(slots.popleft().decode('utf-8')),
211
                ingame=bool(int(slots.popleft().decode('utf-8')))
212
            ))
213