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