Passed
Push — master ( 55ce0d...8237dc )
by Steffen
01:38
created

GameServers.process_packet()   B

Complexity

Conditions 6

Size

Total Lines 31
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 6.0852

Importance

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