From e83ce53a3906cc1e031f74857de7d1a1b7f04006 Mon Sep 17 00:00:00 2001 From: Simon Howard Date: Tue, 7 Dec 2010 21:02:06 +0000 Subject: [PATCH] Add master server web page CGI script to display a list of active servers. Subversion-branch: /master Subversion-revision: 2199 --- web/cocoademon.png | Bin 0 -> 11197 bytes web/index.py | 170 +++++++++++++++++++++++++++++++++++++++++++++ web/index.template | 32 +++++++++ web/style.css | 29 ++++++++ 4 files changed, 231 insertions(+) create mode 100644 web/cocoademon.png create mode 100644 web/index.py create mode 100644 web/index.template create mode 100644 web/style.css diff --git a/web/cocoademon.png b/web/cocoademon.png new file mode 100644 index 0000000000000000000000000000000000000000..5c2553d335b0548af08ef41d747e5b34ec610fd9 GIT binary patch literal 11197 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 literal 0 HcmV?d00001 diff --git a/web/index.py b/web/index.py new file mode 100644 index 0000000..bc3d43f --- /dev/null +++ b/web/index.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python +# +# Copyright(C) 2010 Simon Howard +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA +# 02111-1307, USA. +# +# +# CGI script that generates master server web page. +# + +from cgi import escape +from time import time +from select import select +import socket +import sys +import struct +import simplejson + +NET_MASTER_PACKET_TYPE_GET_METADATA = 4 +NET_MASTER_PACKET_TYPE_GET_METADATA_RESPONSE = 5 + +METADATA_GATHER_TIME = 0.1 # seconds + +MASTER_SERVER = "master.chocolate-doom.org" +UDP_PORT = 2342 + +def send_message(sock, addr, message_type, payload=None): + header = struct.pack(">h", message_type) + packet = header + + if payload is not None: + packet += payload + + sock.sendto(packet, addr) + +def read_string(packet): + terminator = struct.pack("b", 0) + strlen = packet.index(terminator) + + result, = struct.unpack("%ss" % strlen, packet[0:strlen]) + + return packet[strlen + 1:], result + +def decode_string_list(packet): + """ Decode binary data containing NUL-terminated strings. """ + + strings = [] + + while len(packet) > 0: + packet, string = read_string(packet) + + strings.append(string) + + return strings + +def process_metadata_response(packet): + """ Process a response received from the master server. """ + + type, = struct.unpack(">h", packet[0:2]) + + if type != NET_MASTER_PACKET_TYPE_GET_METADATA_RESPONSE: + raise Exception("Wrong packet type received: %i" % type) + + # Process the payload data (list of NUL-terminated strings) + + strings = decode_string_list(packet[2:]) + + # Each string is a JSON-encoded dictionary. Decode these. + + return map(simplejson.loads, strings) + +def get_metadata(addr_str): + """ Query a master server for metadata about its servers. """ + + addr = (socket.gethostbyname(addr_str), UDP_PORT) + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + + # Send request to master. + + send_message(sock, addr, NET_MASTER_PACKET_TYPE_GET_METADATA) + + # Wait for METADATA_GATHER_TIME seconds to receive responses from + # the master. There may be multiple response packets if there are + # lots of servers. + + servers = [] + start_time = time() + + while time() < start_time + METADATA_GATHER_TIME: + r, w, x = select([sock], [], [], METADATA_GATHER_TIME) + + if sock in r: + packet, remote_addr = sock.recvfrom(1024) + + servers += process_metadata_response(packet) + + return servers + +# CGI script to print out server list. + +def get_server_data(): + """ Query the master server and retrieve server metadata. """ + + return get_metadata(MASTER_SERVER) + +def generate_table_row(server): + """ Generate a row of the HTML table, containing data for a + particular server. """ + + data = [ + "%s:%i" % (server["address"], server["port"]), + escape(server["name"]), + escape(server["version"]), + server["max_players"] + ] + + result = [] + + for col in data: + result.append(" %s\n" % col) + + return "\n" + ("".join(result)) + "\n" + +def generate_table(server_data): + """ Generate an HTML table from list of server metadata. """ + + result = [] + + for server in server_data: + result.append(generate_table_row(server)) + + return "\n".join(result) + +def read_template(filename): + """ Read HTML template file. """ + + file = open(filename) + result = file.read() + file.close() + + return result + +def output_html(html): + """ Output HTML data back to client. """ + + print "Content-Type: text/html" + print + print html + +template = read_template("index.template") + +server_data = get_server_data() +table_data = generate_table(server_data) +html = template.replace("___TABLE_DATA___", table_data) + +output_html(html) + diff --git a/web/index.template b/web/index.template new file mode 100644 index 0000000..42bcb3c --- /dev/null +++ b/web/index.template @@ -0,0 +1,32 @@ + + + + + Chocolate Doom master server + + + +Cocoademon +

Chocolate Doom master server

+ +

Currently active servers:

+ + + +
Server address + Server name + Version + Max. players + +___TABLE_DATA___ + +
+ +

+For more information, see +the website. +

+ + + + diff --git a/web/style.css b/web/style.css new file mode 100644 index 0000000..8828c73 --- /dev/null +++ b/web/style.css @@ -0,0 +1,29 @@ +body { + background-color: black; + color: white; +} + +table { + border-color: white; + border-width: 0 0 1px 1px; + border-style: solid; + border-collapse: collapse; +} + +td, th { + text-align: left; + border-color: white; + border-width: 1px 1px 0 0; + border-style: solid; + margin: 0; + padding: 4px; +} + +#cocoademon { + float: right; + width: 150px; + border: 0px; + padding-right: 20px; + padding-top: 20px; +} +