Passed
Push — master ( 6a14d4...f99a48 )
by Steffen
01:38
created

MasterServers.parse_count_response()   A

Complexity

Conditions 1

Size

Total Lines 9
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1.037

Importance

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