#!/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()