Test Failed
Push — master ( 789e01...6a14d4 )
by Steffen
01:43
created

MasterServers.process_packet()   B

Complexity

Conditions 7

Size

Total Lines 27
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 7.2269

Importance

Changes 0
Metric Value
cc 7
eloc 14
nop 2
dl 0
loc 27
ccs 5
cts 6
cp 0.8333
crap 7.2269
rs 8
c 0
b 0
f 0
1
#!/usr/bin/python
2
# -*- coding: utf-8 -*-
3 1
import socket
4 1
from functools import lru_cache
5 1
from time import sleep, time
6
7 1
from tw_serverinfo import Network
8 1
from tw_serverinfo.models.game_server import GameServer
9 1
from tw_serverinfo.models.master_server import MasterServer
10
11
12 1
class MasterServers(object):
13
    """MasterServers Class containing the logic to request and parse responses from the master servers"""
14
    # ToDo: extract to json/yml file
15 1
    master_servers_cfg = [
16
        {
17
            'hostname': 'master1.teeworlds.com',
18
            'port': 8300
19
        },
20
        {
21
            'hostname': 'master2.teeworlds.com',
22
            'port': 8300
23
        },
24
        {
25
            'hostname': 'master3.teeworlds.com',
26
            'port': 8300
27
        },
28
        {
29
            'hostname': 'master4.teeworlds.com',
30
            'port': 8300
31
        }
32
    ]
33 1
    _master_servers = []
34
35 1
    @lru_cache(maxsize=None)
36 1
    def get_master_servers(self) -> list:
37
        """Generate generator of master servers with resolve IP address
38
39
        :return:
40
        """
41 1
        self._master_servers = []
42 1
        for master_server in self.master_servers_cfg:
43 1
            ip = socket.gethostbyname(master_server['hostname'])
44 1
            self._master_servers.append(
45
                MasterServer(ip=ip, port=master_server['port'], hostname=master_server['hostname'])
46
            )
47 1
        return self._master_servers
48
49 1
    @property
50 1
    def master_servers(self) -> list:
51
        """Returns the game servers and cache the result
52
53
        :return:
54
        """
55 1
        cache_info = self.update_master_server_info.cache_info()
56 1
        if cache_info.currsize > 0:
57
            # clear the cache if the last index was more than 10 minutes ago
58 1
            if time() >= self.update_master_server_info()['timestamp'] + 60 * 10:
59
                self.update_master_server_info.cache_clear()
60 1
        return self.update_master_server_info()['servers']
61
62 1
    @property
63 1
    def game_servers(self) -> list:
64
        """Returns the game servers from the updated master server results
65
66
        :return:
67
        """
68
        game_servers = []
69 1
        for master_server in self.master_servers:  # type: MasterServer
70
            for game_server in master_server.servers:  # type: GameServer
71 1
                if game_server not in game_servers:
72
                    game_servers.append(game_server)
73 1
        return game_servers
74 1
75 1
    @lru_cache(maxsize=None)
76
    def update_master_server_info(self) -> dict:
77
        """Check the master servers for the game server count and retrieve the server list
78 1
79 1
        :return:
80
        """
81 1
        # create an udp protocol socket
82 1
        sock = socket.socket(family=Network.PROTOCOL_FAMILY, type=socket.SOCK_DGRAM)
83 1
        # set the socket to non blocking to allow parallel requests
84
        sock.setblocking(False)
85 1
86 1
        for master_server in self.get_master_servers():
87
            Network.send_packet(sock=sock, data=Network.PACKETS['SERVERBROWSE_GETCOUNT'], extra_data=b'\xff\xff',
88
                                server=master_server, add_token=False)
89 1
            Network.send_packet(sock=sock, data=Network.PACKETS['SERVERBROWSE_GETLIST'], extra_data=b'\xff\xff',
90 1
                                server=master_server, add_token=False)
91
            # apparently since 06.10.2018 we need the get info packet for the master servers too to retrieve the
92
            # game servers, normally sent after parsing the response from the previous 2 packets, but works
93 1
            # as well if sent directly since no information of the response for the previous 2 packets is needed
94
            Network.send_packet(sock=sock, data=Network.PACKETS['SERVERBROWSE_GETINFO'], extra_data=b'xe',
95 1
                                server=master_server)
96
97
        duration_without_response = Network.CONNECTION_SLEEP_DURATION
98
        sleep(Network.CONNECTION_SLEEP_DURATION / 1000.0)
99
100 1
        while duration_without_response < Network.CONNECTION_TIMEOUT:
101 1
            if not Network.receive_packet(sock, self.get_master_servers(), self.process_packet):
102
                # increase the measured duration without a response and sleep for the set duration
103
                duration_without_response += Network.CONNECTION_SLEEP_DURATION
104
                sleep(Network.CONNECTION_SLEEP_DURATION / 1000.0)
105
            else:
106
                # if we got a response reset the duration in case we receive multiple packets
107
                duration_without_response = 0
108
109
        # we didn't receive any packets in time and cancel the connection here
110
        sock.close()
111 1
112
        return {
113 1
            'servers': self._master_servers,
114 1
            'timestamp': time()
115 1
        }
116 1
117 1
    @staticmethod
118 1
    def process_packet(data: bytes, master_server: MasterServer) -> None:
119
        """Process packet function for
120
         - SERVERBROWSE_COUNT
121
         - SERVERBROWSE_LIST
122 1
        packets
123
124 1
        :type data: bytes
125 1
        :type master_server: dict
126
        :return:
127
        """
128
        master_server.response = True
129
130
        if data[6:6 + 8] == Network.PACKETS['SERVERBROWSE_COUNT']:
131
            master_server.num_servers = (data[14] << 8) | data[15]
132
        elif data[6:6 + 8] == Network.PACKETS['SERVERBROWSE_LIST']:
133
            for i in range(14, len(data) - 14, 18):
134
                if data[i:i + 12] == b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff':
135
                    ip = socket.inet_ntop(socket.AF_INET, data[i + 12:i + 16])
136
                else:
137
                    ip = '[' + socket.inet_ntop(socket.AF_INET6, data[i:i + 16]) + ']'
138
139
                port = int.from_bytes(data[i + 16:i + 18], byteorder='big')
140
141
                if ip != master_server.ip or port != master_server.port:
142
                    game_server = GameServer(ip=ip, port=port)
143
                    master_server.servers.append(game_server)
144