1<?php  /*\
2        *   Web Game List Script
3        *
4        *   This PHP program work with RakNet to maintain a list of
5        *   currently running game servers.  The servers communicate
6        *   call this script via HTTP periodically and submit thier
7        *   updated information.
8        *
9        *   Requirements:
10        *       (1) A place to write a file
11        *       (2) PHP version 4.3.0 or newer (including PHP 5)
12        *
13        *   To install:
14        *       (1) Place this file anywhere your web-server can access.
15        *       (2) Make sure the configuration file can be written.
16        *           If your script can't write to the current
17        *           directory, this change the path CONF_FILE below.
18        *       (3) Run the script via a web browser, for example like
19        *           http://www.yourserver.com/directoryserver.php
20        *       (4) Set a password and save the configuration.
21        *
22        *   Troubleshooting:
23        *       We use the standard PHP logging mechanism, so check your
24        *       web server error log if you encounter any problems.
25        *
26        *   History:
27        *       2008 April 21 - Initial creation
28        *       2008 May 1 - Update escape handling
29       \*/
30
31    header("Cache-Control: no-cache, must-revalidate");
32
33    define("CONF_FILE",'./directoryserver.cfg.php');
34
35    // Initialize configuration variables
36    // (configuration includes the game database)
37    $gv = array(
38        'title' => 'directoryserver',
39        'password' => '', // for changing the configuration
40        'gamePassword' => '', // game password
41        'max_length' => 5000, // for a game, total # of field
42        'max_timeout' => 5000, // timeouts are in seconds
43        'games' => array() // game database, indexed by IP address
44    );
45
46    $config_message = "";
47
48    $posted = strcmp($_SERVER["REQUEST_METHOD"],"POST") == 0;
49
50    // Lock configuration if we may have updates
51    if($posted)
52        $lock_file = fopen(CONF_FILE . ".lock.php", "w")
53        and flock($lock_file, LOCK_EX);
54
55    if(! file_exists(CONF_FILE)) {
56		$cfgExists = 0;
57	}
58    else {
59        include(CONF_FILE);
60		$cfgExists = 1;
61	}
62
63    // update handling
64    if($posted) {
65        $password_correct = false;
66
67        if(isset($_GET['update']) || isset($_GET['query']))
68            update_game_list();
69        else
70            update_config();
71
72        // Remove stale servers (over 10X max_timeout expired)
73        $ips = array_keys($gv['games']);
74        foreach($ips as $ip) {
75            if($gv['games'][$ip]['__SEC_AFTER_EPOCH_SINCE_LAST_UPDATE'] +
76               $gv['max_timeout'] * 10 < time())
77                unset($gv['games'][$ip]);
78        }
79
80		@unlink(CONF_FILE);
81        // Save configuration variable ($gv) to file
82        $new_cfg = fopen(CONF_FILE . ".tmp.php","w")
83        and fwrite($new_cfg,"<?php \$gv = \n")
84        and fwrite($new_cfg,var_export($gv,true))
85        and fwrite($new_cfg,"?>")
86        and fclose($new_cfg)
87        and @rename(CONF_FILE . ".tmp.php",CONF_FILE);
88
89        fclose($lock_file);
90    }
91
92	if(! file_exists(CONF_FILE)) {
93		trigger_error(
94            "The configuration file \"" . CONF_FILE . "\"
95             does not yet exist.  Try revealing the directoryserver configuration, setting the <em>title</em>,
96             and a new <em>password</em> below, then submit the
97             updated information. ");
98	}
99
100
101    // don't send HTML to a game server
102    if(isset($_GET['update']) or isset($_GET['query']))
103        {
104        respond_to_game_server();
105        exit(0);
106        }
107?>
108<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "DTD/xhtml1-transitional.dtd">
109<html>
110<head>
111    <title><?php echo $gv['title'] ?></title>
112    <style type="text/css">
113
114        /* Main body styles */
115        body { font-family: "Verdana", sans-serif; margin: 0;
116               padding: 0; background: #EEE; }
117        h1 { line-height: 3em; font-size: xx-large; vertical-align: middle;
118             background: #116; color: white; font-weight: bold;
119             font-family: "Georgia", serif; margin: 0;
120             border-bottom: thin solid white; }
121        #config_msg { background: red; margin: 0; padding: 0.5em; }
122        p { margin: 0.5em 1em; }
123
124        /* Styling of game records */
125        .games div { width: 35em; float: left; margin: 1em; background: #CCE;
126                     border-width: 1px 2px 2px 1px; border-style: solid; }
127        .games p span { display: block; clear: both; }
128        .games p strong { font-size: larger; float: left; }
129        .games p em { float: right; }
130        .games ul { border-top: dashed thin grey; margin: 0.5em;
131                    padding: 0.5em; text-align: center; }
132        .games li { list-style-position: inside; }
133
134        /* Styling of configuration box */
135        form { clear: both; background: #AAF; margin: 1em 1em 0 1em;
136               padding: 0.2em 0.5em; font-size: small;
137               border: thin solid black; }
138        form h2 { margin: 0; font-weight: normal; font-family: "Georgia", serif; }
139        form #submit { width: 60%; display: block; margin: 0.1em auto 0.5em auto; }
140        form.closed_form p { display: none; }
141        form.closed_form #submit { display: none; }
142    </style>
143	<?php
144		if (!$cfgExists) {
145			$classname = "";
146		}
147		else {
148			$classname = "closed_form";
149		}
150?>
151
152    <script type="text/javascript">
153        function init() {
154            var block =  document.getElementById("conf_form");
155
156            block.getElementsByTagName("input")[0].onclick = function () {
157                block.className = block.className == "" ? "closed_form" : "";
158            }
159
160            block.className = "<?php echo $classname; ?>";
161        }
162    </script>
163</head>
164
165<body onload="init()">
166    <h1><?php echo $gv['title'] ?></h1>
167
168    <?php if(! empty($config_message))
169              echo "<p id='config_msg'>" . $config_message . "</p>\n\t";
170
171        $myurl = $_SERVER['SCRIPT_NAME'];
172        if(isset($_GET['showold']))
173            $msg = "<a href='$myurl'>Show only functioning servers</a>";
174        else
175            $msg = "<a href='$myurl?showold'>Show all servers, including unresponsive ones</a>";
176        $game_count = count($gv['games']);
177        echo "<p>Known Servers: $game_count $msg</p>\n";
178        ?>
179
180
181    <div class="games">
182        <?php foreach($gv['games'] as $address => $fields)
183                    output_game($address, $fields);
184        ?>
185    </div>
186
187
188
189    <form method="post" autocomplete="off" id="conf_form">
190        <h2>directoryserver configuration <input type="button" value="Reveal / Hide" /></h2>
191        <p>To make changes to the configuration, supply the <label>current password:
192            <input type="password" name="password" /></label>
193            <em>(by default the password is blank)</em>.</p>
194
195        <p>To create a new password type it twice, here:
196            <input type="password" name="newpass1" /> and here:
197            <input type="password" name="newpass2" />.</p>
198
199        <p>Change the page title:
200            <input name="title" value="<?php echo $gv['title'] ?>" /></p>
201
202        <p>Change the game password (usually blank):
203            <input type="password" name="gamePassword" /></p>
204
205		<p>Change the maximum number of characters allowed in a record:
206            <input name="max_length" value="<?php echo $gv['max_length'] ?>" />.
207            Records longer than this will be truncated.</p>
208
209        <p>Change the longest timeout value in seconds game servers can use:
210            <input name="max_timeout" value="<?php echo $gv['max_timeout'] ?>" />.
211            Records older than this will not be shown.</p>
212
213        <input id="submit" type="submit" value="Submit Updated Configuration Information" />
214    </form>
215</body>
216</html>
217<?php // functions follow
218
219function update_game_list() {
220    global $gv;
221    global $delete_record;
222    global $password_correct;
223
224    $port = 0;
225    $updates = array();
226
227
228    // apply updates
229    while(list($akey, $avalue) = each($_POST)){
230
231        if(0 == strcasecmp("__GAME_PORT",$akey)) {
232            $port = $avalue;
233            continue;
234        } elseif(0 == strcasecmp("__DELETE_ROW",$akey)) {
235            $delete_record = true;
236        } elseif(0 == strcasecmp("__PHP_DIRECTORY_SERVER_PASSWORD",$akey)) {
237            if(0 == strcmp($avalue, $gv['gamePassword']))
238
239                $password_correct = true;
240            continue;
241        }
242
243        $updates[$akey] = $avalue;
244
245        if(strcasecmp("__GAME_LISTING_TIMEOUT",$akey) == 0 && $avalue > $gv['max_timeout'])
246            trigger_error("Updated record timeout too long", E_USER_WARNING);
247    }
248
249    if(!$password_correct && !empty($gv['gamePassword'])) {
250        trigger_error("Supplied password is wrong", E_USER_ERROR);
251        return;
252    }
253
254    if(! isset($_GET['update']))
255        return;
256
257    if($delete_record) {
258        unset($gv['games'][$_SERVER["REMOTE_ADDR"] . ":" . $port]);
259        return;
260    }
261
262    $game =& $gv['games'][$_SERVER["REMOTE_ADDR"] . ":" . $port];
263		if(strlen(@implode(@array_keys($game))) + strlen(@implode($game))
264			   + strlen(@array_keys($updates)) + strlen(array_values($updates)) > $gv['max_length']) {
265			trigger_error("Dumping old record to make room for new information");
266			$game = array();
267		}
268
269		foreach($updates as $akey => $avalue) {
270			$game[$akey] = $avalue;
271/*			if(empty($avalue))
272				unset($game[$akey]); */
273		}
274
275		$game['__SEC_AFTER_EPOCH_SINCE_LAST_UPDATE'] = time();
276
277}
278
279function respond_to_game_server() {
280    global $gv;
281    global $delete_record;
282    global $password_correct;
283
284    echo "Configuration follows\n";
285    foreach($gv as $key => $value) {
286        if(strcmp($key,"games") == 0 or strcmp($key,"password") == 0)
287            continue;
288        echo $key . ": " . strtr($value, "\n\001\002", "\r  ") . "\n";
289    }
290
291    if($delete_record)
292        echo "Entry Deleted";
293
294    if(! isset($_GET['query']))
295        return;
296
297    if(!$password_correct && !empty($gv['gamePassword'])) {
298        trigger_error("Supplied password is wrong", E_USER_ERROR);
299        return;
300    }
301
302    echo "Game List follows\n";
303    foreach($gv['games'] as $ip => $game) {
304        if($game['__GAME_LISTING_TIMEOUT'] + $game['__SEC_AFTER_EPOCH_SINCE_LAST_UPDATE'] < time())
305            continue;
306        foreach($game as $key => $value) {
307
308            echo strtr($key, "\n\001\002", "\r  ") .
309                "\002" . strtr($value, "\n\001\002", "\r  ") . "\001";
310        }
311        echo "__SystemAddress\002$ip\n";
312    }
313
314    echo "End of Response\n";
315}
316
317function update_config() {
318    global $gv, $config_message;
319
320    // check password
321    if(strcmp($gv['password'],$_POST['password'])) {
322        $config_message .= "Invalid password supplied\n";
323        return;
324    }
325
326    if(! empty($_POST['max_length']))
327        $gv['max_length'] = (int)$_POST['max_length'];
328    if(! empty($_POST['max_timeout']))
329        $gv['max_timeout'] = (int)$_POST['max_timeout'];
330    if(! empty($_POST['title']))
331        $gv['title'] = $_POST['title'];
332
333    if(! empty($_POST['gamePassword']))
334        $gv['gamePassword'] = $_POST['gamePassword'];
335
336	if(isset($_POST['newpass1'])) {
337        if(strcmp($_POST['newpass1'], $_POST['newpass2']))
338            $config_message = "New passwords do not match.\n";
339        else
340            $gv['password'] = $_POST['newpass1'];
341    }
342
343    $config_message .= "Configuration updated.";
344}
345
346function output_game($address, $fields) {
347    global $gv;
348
349    // default field values
350    $generics = "";
351    $timeout = $gv['max_timeout'];
352    $name = "Untitled";
353
354    // assign special fields to a variable or add to generic list
355    // (field names are case-insensitve)
356    foreach($fields as $key => $value) {
357        if(strcasecmp($key,"__GAME_NAME") == 0)
358            $name = $value;
359        elseif(strcasecmp($key,"__GAME_LISTING_TIMEOUT") == 0)
360            $timeout = min($value, $timeout);
361        elseif(strcasecmp($key,"__SEC_AFTER_EPOCH_SINCE_LAST_UPDATE"))
362            $generics .= "<li><strong>$key</strong>: $value</li>\n\t\t";
363    }
364
365    // when does this record expire?
366    $update = date("D M j G:i:s T Y", $fields['__SEC_AFTER_EPOCH_SINCE_LAST_UPDATE']);
367    $expiration = $fields['__SEC_AFTER_EPOCH_SINCE_LAST_UPDATE'] + $timeout;
368    $remaining = $expiration - time();
369    if(! isset($_GET['showold']) && $remaining < 0)
370        return;
371
372    // output record
373    echo <<<DOC
374        <div>
375            <p>
376                <strong>$name</strong>
377                <em>$address</em>
378                <span>last updated at $update<br />
379                    (this record expires in $remaining seconds)</span>
380            </p>
381            <ul>
382                $generics
383            </ul>
384        </div>
385DOC;
386}
387
388?>
389
390