-
Notifications
You must be signed in to change notification settings - Fork 69
Expand file tree
/
Copy pathmaster_server.py
More file actions
327 lines (282 loc) · 14.5 KB
/
master_server.py
File metadata and controls
327 lines (282 loc) · 14.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
# -*- 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
REGION_EUROPE = 0x03
REGION_ASIA = 0x04
REGION_AUSTRALIA = 0x05
REGION_MIDDLE_EAST = 0x06
REGION_AFRICA = 0x07
REGION_REST = 0xFF
MASTER_SERVER_ADDR = ("hl2master.steampowered.com", 27011)
class Duplicates(enum.Enum):
"""Behaviour for duplicate addresses.
These values are intended to be used with :meth:`MasterServerQuerier.find`
to control how duplicate addresses returned by the master server are
treated.
:cvar KEEP: All addresses are returned, even duplicates.
:cvar SKIP: Skip duplicate addresses.
:cvar STOP: Stop returning addresses when a duplicate is encountered.
"""
KEEP = "keep"
SKIP = "skip"
STOP = "stop"
class MasterServerQuerier(BaseQuerier):
"""Implements the Source master server query protocol
https://developer.valvesoftware.com/wiki/Master_Server_Query_Protocol
.. note::
Instantiating this class creates a socket. Be sure to close the
querier once finished with it. See :class:`valve.source.BaseQuerier`.
"""
def __init__(self, address=MASTER_SERVER_ADDR, timeout=10.0):
super(MasterServerQuerier, self).__init__(address, timeout)
def __iter__(self):
"""An unfitlered iterator of all Source servers
This will issue a request for an unfiltered set of server addresses
for each region. Addresses are received in batches but returning
a completely unfiltered set will still take a long time and be
prone to timeouts.
.. note::
If a request times out then the iterator will terminate early.
Previous versions would propagate a :exc:`NoResponseError`.
See :meth:`.find` for making filtered requests.
"""
return self.find(region="all")
def _query(self, region, filter_string):
"""Issue a request to the master server
Returns a generator which yields ``(host, port)`` addresses as
returned by the master server.
Addresses are returned in batches therefore multiple requests may be
dispatched. Because of this any of these requests may result in a
:exc:`NotResponseError` raised. In such circumstances the iterator
will exit early. Otherwise the iteration continues until the final
address is reached which is indicated by the master server returning
a 0.0.0.0:0 address.
.. note::
The terminating 0.0.0.0:0 is not yielded by the iterator.
``region`` should be a valid numeric region identifier and
``filter_string`` should be a formatted filter string as described
on the Valve develper wiki:
https://developer.valvesoftware.com/wiki/Master_Server_Query_Protocol#Filter
"""
last_addr = "0.0.0.0:0"
first_request = True
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))
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"])
if not address.is_null:
yield address["host"], address["port"]
def _deduplicate(self, method, query):
"""Deduplicate addresses in a :meth:`._query`.
The given ``method`` should be a :class:`Duplicates` object. The
``query`` is an iterator as returned by :meth:`._query`.
"""
seen = set()
if method is Duplicates.KEEP:
for address in query:
yield address
else:
for address in query:
if address in seen:
if method is Duplicates.SKIP:
continue
elif method is Duplicates.STOP:
break
yield address
seen.add(address)
def _map_region(self, region):
"""Convert string to numeric region identifier
If given a non-string then a check is performed to ensure it is a
valid region identifier. If it's not, ValueError is raised.
Returns a list of numeric region identifiers.
"""
if isinstance(region, six.text_type):
try:
regions = {
"na-east": [REGION_US_EAST_COAST],
"na-west": [REGION_US_WEST_COAST],
"na": [REGION_US_EAST_COAST, REGION_US_WEST_COAST],
"sa": [REGION_SOUTH_AMERICA],
"eu": [REGION_EUROPE],
"as": [REGION_ASIA, REGION_MIDDLE_EAST],
"oc": [REGION_AUSTRALIA],
"af": [REGION_AFRICA],
"rest": [REGION_REST],
"all": [REGION_US_EAST_COAST,
REGION_US_WEST_COAST,
REGION_SOUTH_AMERICA,
REGION_EUROPE,
REGION_ASIA,
REGION_AUSTRALIA,
REGION_MIDDLE_EAST,
REGION_AFRICA,
REGION_REST],
}[region.lower()]
except KeyError:
raise ValueError(
"Invalid region identifer {!r}".format(region))
else:
# Just assume it's an integer identifier, we'll validate below
regions = [region]
for reg in regions:
if reg not in {REGION_US_EAST_COAST,
REGION_US_WEST_COAST,
REGION_SOUTH_AMERICA,
REGION_EUROPE,
REGION_ASIA,
REGION_AUSTRALIA,
REGION_MIDDLE_EAST,
REGION_AFRICA,
REGION_REST}:
raise ValueError("Invalid region identifier {!r}".format(reg))
return regions
def find(self, region="all", duplicates=Duplicates.SKIP, **filters):
"""Find servers for a particular region and set of filtering rules
This returns an iterator which yields ``(host, port)`` server
addresses from the master server.
``region`` spcifies what regions to restrict the search to. It can
either be a ``REGION_`` constant or a string identifying the region.
Alternately a list of the strings or ``REGION_`` constants can be
used for specifying multiple regions.
The following region identification strings are supported:
+---------+-----------------------------------------+
| String | Region(s) |
+=========+=========================================+
| na-east | East North America |
+---------+-----------------------------------------+
| na-west | West North America |
+---------+-----------------------------------------+
| na | East North American, West North America |
+---------+-----------------------------------------+
| sa | South America |
+---------+-----------------------------------------+
| eu | Europe |
+---------+-----------------------------------------+
| as | Asia, the Middle East |
+---------+-----------------------------------------+
| oc | Oceania/Australia |
+---------+-----------------------------------------+
| af | Africa |
+---------+-----------------------------------------+
| rest | Unclassified servers |
+---------+-----------------------------------------+
| all | All of the above |
+---------+-----------------------------------------+
.. note::
"``rest``" corresponds to all servers that don't fit with any
other region. What causes a server to be placed in this region
by the master server isn't entirely clear.
The region strings are not case sensitive. Specifying an invalid
region identifier will raise a ValueError.
As well as region-based filtering, alternative filters are supported
which are documented on the Valve developer wiki.
https://developer.valvesoftware.com/wiki/Master_Server_Query_Protocol#Filter
This method accepts keyword arguments which are used for building the
filter string that is sent along with the request to the master server.
Below is a list of all the valid keyword arguments:
+------------+-------------------------------------------------------+
| Filter | Description |
+============+=======================================================+
| type | Server type, e.g. "dedicated". This can be a |
| | ``ServerType`` instance or any value that can be |
| | converted to a ``ServerType``. |
+------------+-------------------------------------------------------+
| secure | Servers using Valve anti-cheat (VAC). This should be |
| | a boolean. |
+------------+-------------------------------------------------------+
| gamedir | A string specifying the mod being ran by the server. |
| | For example: ``tf``, ``cstrike``, ``csgo``, etc.. |
+------------+-------------------------------------------------------+
| map | Which map the server is running. |
+------------+-------------------------------------------------------+
| linux | Servers running on Linux. Boolean. |
+------------+-------------------------------------------------------+
| empty | Servers which are not empty. Boolean. |
+------------+-------------------------------------------------------+
| full | Servers which are full. Boolean. |
+------------+-------------------------------------------------------+
| proxy | SourceTV relays only. Boolean. |
+------------+-------------------------------------------------------+
| napp | Servers not running the game specified by the given |
| | application ID. E.g. ``440`` would exclude all TF2 |
| | servers. |
+------------+-------------------------------------------------------+
| noplayers | Servers that are empty. Boolean |
+------------+-------------------------------------------------------+
| white | Whitelisted servers only. Boolean. |
+------------+-------------------------------------------------------+
| gametype | Server which match *all* the tags given. This should |
| | be set to a list of strings. |
+------------+-------------------------------------------------------+
| gamedata | Servers which match *all* the given hidden tags. |
| | Only applicable for L4D2 servers. |
+------------+-------------------------------------------------------+
| gamedataor | Servers which match *any* of the given hidden tags. |
| | Only applicable to L4D2 servers. |
+------------+-------------------------------------------------------+
.. note::
Your mileage may vary with some of these filters. There's no
real guarantee that the servers returned by the master server will
actually satisfy the filter. Because of this it's advisable to
explicitly check for compliance by querying each server
individually. See :mod:`valve.source.a2s`.
The master server may return duplicate addresses. By default, these
duplicates are excldued from the iterator returned by this method.
See :class:`Duplicates` for controller this behaviour.
"""
if isinstance(region, (int, six.text_type)):
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):
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)]
if not value:
continue
value = ",".join(value)
elif key == "napp":
value = int(value)
elif key == "type":
if not isinstance(value, util.ServerType):
value = util.ServerType(value).char
else:
value = value.char
filter_[key] = six.text_type(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])
if filter_string:
filter_string = "\\" + filter_string
queries = []
for region in regions:
queries.append(self._query(region, filter_string))
query = self._deduplicate(
Duplicates(duplicates), itertools.chain.from_iterable(queries))
for address in query:
yield address