#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Query HLSW_ master servers via broadcast.
Based on hlswmaster_.
TODO
----
- Merge or drop duplicate server entries received from multiple servers.
:Copyright: 2008 `Jochen Kupperschmidt <http://homework.nwsnet.de/>`_
:Date: 18-Dec-2008
:License: GNU General Public License
.. _HLSW: http://www.hlsw.org/
.. _hlswmaster: http://www.kopf-tisch.de/hlswmaster/
"""
from itertools import islice
import socket
import struct
import warnings
GAME_TYPES = {
1: 'Half-Life',
2: 'Quake 1',
3: 'Quake 2',
4: 'Q3Comp',
5: 'Unreal Tournament',
6: 'Quake 3 Arena',
7: 'Elite Force',
8: 'Return to Castle Wolfenstein',
9: 'GSProt',
10: 'Command & Conquer Renegade',
11: 'Medal of Honor: Allied Assault',
12: 'Jedi Knight 2',
13: 'Soldier of Fortune',
14: 'Unreal Tournament 2003',
15: 'America\'s Army: Operations',
16: 'Battlefield 1942',
17: 'Alien vs. Predator 2',
18: 'Rune',
19: 'Project IGI2: Covert Strike',
20: 'Never Winter Nights',
21: 'Medal of Honor: Allied Assault Spearhead',
22: 'Operation Flashpoint',
23: 'Operation Flashpoint Resistance',
24: 'Devastation',
25: 'Wolfenstein: Enemy Territory',
26: 'Elite Force 2',
27: 'Jedi Knight 3',
28: 'Medal of Honor: Allied Assault Breakthrough',
29: 'Tribes 2',
30: 'Halo',
31: 'Call of Duty',
32: 'Savage: The Battle for Newerth',
33: 'Unreal Tournament 2004',
34: 'HLSteam',
35: 'Battlefield Vietnam',
36: 'GS2Prot',
37: 'Pain Killer',
38: 'Doom 3',
39: 'OGPProt',
40: 'Half-Life 2/Source Engine',
41: 'Tribes Vengeance',
42: 'Call of Duty: United Offensive',
43: 'Starwars: Battlefront (?)',
44: 'SWAT 4',
45: 'Battlefield 2',
47: 'Quake 4',
48: 'Call of Duty 2',
50: 'F.E.A.R.',
51: 'Warsow (?)',
67: 'Call of Duty 4',
}
HLSW_PREAMBLE = '\xFF\xFF\xFF\xFFHLSWLANSEARCH\x00'
HLSW_PREAMBLE_LEN = len(HLSW_PREAMBLE) # Should be 12.
server_struct = struct.Struct('<HLHH') # Size should be 10.
class MasterQuerySocket(socket.socket):
"""Broadcast socket to query HLSW masters for game servers."""
src_port = 7130
dst_port = 7140
timeout = 4
def __init__(self):
socket.socket.__init__(self, socket.AF_INET, socket.SOCK_DGRAM)
self.settimeout(self.timeout)
self.bind(('', self.src_port))
self.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
def query(self, buffer_size=4096):
"""Send broadcast query and collect responses."""
self.sendto(HLSW_PREAMBLE, ('<broadcast>', self.dst_port))
data, addr = self.recvfrom(buffer_size)
return parse_reply(data)
def fetch_string(iterable, length):
return ''.join(islice(iterable, length))
def parse_reply(reply):
"""Iterate over master reply and extract packed game server structs."""
chars = iter(reply)
if fetch_string(chars, HLSW_PREAMBLE_LEN) != HLSW_PREAMBLE:
return
while True:
fragment = fetch_string(chars, server_struct.size)
if not fragment:
break
if len(fragment) < server_struct.size:
warnings.warn('Buffer size was too low, data was truncated.')
break
unpacked = server_struct.unpack_from(fragment)
game_id, _ip_address, port1, port2 = unpacked
ip_address = socket.inet_ntoa(fragment[2:6])
yield (game_id, ip_address, port1, port2)
def tabularize(servers):
"""Display servers as rows in an entitled table.
``servers``
An iterable of 4-tuples.
"""
print '+-----------------+--------+--------+-----------------------------------------+'
print '| IP address | port 1 | port 2 | game |'
print '+-----------------+--------+--------+-----------------------------------------+'
template = '| %-15s | %6i | %6i | %-39s |'
for game_id, ip_address, port1, port2 in servers:
game_type = GAME_TYPES.get(game_id, 'unknown')
print template % (ip_address, port1, port2, game_type)
print '+-----------------+--------+--------+-----------------------------------------+'
if __name__ == '__main__':
sock = MasterQuerySocket()
servers = sock.query()
tabularize(servers)
sock.close()