PHP and HTTP

Flattr this!

So, after a tip from Jayenkai over at [url=http://www.socoder.com]Socoder[/url], I decided to use a webserver after all, but I wrote a couple of PHP scripts to take care of finding the manifests.

One, called “patch.php”, is the most important. It sends the right manifest(s) to a client based on what version is passed to it.


<?php

/*The contents of this file are subject to the Mozilla Public License Version 1.1
(the "License"); you may not use this file except in compliance with the
License. You may obtain a copy of the License at http://www.mozilla.org/MPL/

Software distributed under the License is distributed on an "AS IS" basis,
WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
the specific language governing rights and limitations under the License.

The Original Code is patch.php.

The Initial Developer of the Original Code is
Mats 'Afr0' Vederhus. All Rights Reserved.

Contributor(s): ______________________________________.
*/

include 'createzip.php';

if(isset($_GET['Version']))
{
	$ClientVersion = intval(htmlspecialchars($_GET['Version']));
	$NewManifest = "";

	if($handle = opendir(realpath('./patches/')))
	{
		//Find out if there are any manifests newer than the client's version.
		while (false !== ($entry = readdir($handle)))
		{
        		if(intval(str_replace('.manifest', '', $entry)) > $ClientVersion)
			{
				$NewManifest = realpath('./patches') . '/' . $entry . '.manifest';
				break;
			}
		}

		if(empty($NewManifest) || strcmp($NewManifest, realpath('./patches')) == 0)
		{
			header('Content-Description: No New Manifest');
			header('Content-Type: text/html; charset=utf-8');
			echo('<html><body><p>No new manifest!</p></body></html>');
		}
		else
		{
			//Open the new manifest and check if it has a child, which means an
			//incremental update needs to take place...
			$FileHandle = fopen($NewManifest, 'r');

			$Parent = fgets($FileHandle);
			$CurrentChild = fgets($FileHandle);
			$Path = realpath('./patches');

			$Parent = str_replace('Parent=', '', $Parent);
			$Parent = str_replace('"', '', $Parent);
			$Parent = trim($Parent);
			$CurrentChild = str_replace('Child=', '', $CurrentChild);
			$CurrentChild = str_replace('"', '', $CurrentChild);
			$CurrentChild = trim($CurrentChild);

			fclose($FileHandle);
			
			//This is a compromise. Instead of checking for all childs, just check for the first one,
			//and let the client handle the other children (if any).
			if(empty($CurrentChild) !== true && empty($Parent) !== true)
			{
				$Parent = $Path . '/' . $Parent;
				$CurrentChild = $Path . '/' . $CurrentChild;

				$FilesToZip = array($CurrentChild, $NewManifest);
				$ZipName = './' . strval(rand()) . '.zip';
				create_zip($FilesToZip, $ZipName);
				
				//Client should check the Content-Description header to figure out what to expect.
				header('Content-Description: Zipped File Transfer');
    				header('Content-Type: application/octet-stream');
    				header('Content-Disposition: attachment; filename='.basename($ZipName));
    				header('Content-Transfer-Encoding: binary');
    				header('Expires: 0');
    				header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
    				header('Pragma: public');
    				header('Content-Length: ' . filesize($ZipName));
    				ob_clean();
    				flush();
    				readfile($ZipName);

				unlink($ZipName);
				exit();
			}
			//There was a child, but no parent.
			else if(empty($CurrentChild) !== true && empty($Parent) == true)
			{
				$CurrentChild = $Path . '/' . $CurrentChild;

				$FilesToZip = array($CurrentChild, $NewManifest);
				$ZipName = './' . strval(rand()) . '.zip';
				create_zip($FilesToZip, $ZipName);
				
				//Client should check the Content-Description header to figure out what to expect.
				header('Content-Description: Zipped File Transfer');
    				header('Content-Type: application/octet-stream');
    				header('Content-Disposition: attachment; filename='.basename($ZipName));
    				header('Content-Transfer-Encoding: binary');
    				header('Expires: 0');
    				header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
    				header('Pragma: public');
    				header('Content-Length: ' . filesize($ZipName));
    				ob_clean();
    				flush();
    				readfile($ZipName);

				unlink($ZipName);
				exit();
			}
			//There was a parent, but no child.
			else if(empty($Parent) !== true && empty($CurrentChild) == true)
			{
				$Parent = $Path . '/' . $Parent;

				$FilesToZip = array($Parent, $NewManifest);
				$ZipName = './' . strval(rand()) . '.zip';
				create_zip($FilesToZip, $ZipName);
				
				//Client should check the Content-Description header to figure out what to expect.
				header('Content-Description: Zipped File Transfer');
    				header('Content-Type: application/octet-stream');
    				header('Content-Disposition: attachment; filename='.basename($ZipName));
    				header('Content-Transfer-Encoding: binary');
    				header('Expires: 0');
    				header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
    				header('Pragma: public');
    				header('Content-Length: ' . filesize($ZipName));
    				ob_clean();
    				flush();
    				readfile($ZipName);

				unlink($ZipName);
				exit();
			}
			//There was neither a parent nor a child. This should really never occur.
			else
			{
				header('Content-Description: File Transfer');
    				header('Content-Type: application/octet-stream');
    				header('Content-Disposition: attachment; filename='.basename($NewManifest));
    				header('Content-Transfer-Encoding: binary');
    				header('Expires: 0');
    				header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
    				header('Pragma: public');
    				header('Content-Length: ' . filesize($NewManifest));
    				ob_clean();
    				flush();
    				readfile($NewManifest);

				exit();
			}
		}
	}
}
else
{
	header('Content-Description: Invalid Request');
	header('Content-Type: text/html; charset=utf-8');
	echo('<html><body><p>Invalid request!</p></body></html>');
}
?>

This code depends on another script I found while rummaging through Google:

<?php
//From: http://davidwalsh.name/create-zip-php
function create_zip($files = array(),$destination = '',$overwrite = false) {
  //if the zip file already exists and overwrite is false, return false
  if(file_exists($destination) && !$overwrite) { return false; }
  //vars
  $valid_files = array();
  //if files were passed in...
  if(is_array($files)) {
    //cycle through each file
    foreach($files as $file) {
      //make sure the file exists
      if(file_exists($file)) {
        $valid_files[] = $file;
      }
    }
  }
  //if we have good files...
  if(count($valid_files)) {
    //create the archive
    $zip = new ZipArchive();
    if($zip->open($destination,$overwrite ? ZIPARCHIVE::OVERWRITE : ZIPARCHIVE::CREATE) !== true) {
      return false;
    }
    //add the files
    foreach($valid_files as $file) {
      $zip->addFile($file,$file);
    }
    //debug
    //echo 'The zip archive contains ',$zip->numFiles,' files with a status of ',$zip->status;
    
    //close the zip -- done!
    $zip->close();
    
    //check to make sure the file exists
    return file_exists($destination);
  }
  else
  {
    return false;
  }
}
?>

The ‘patch.php’ script can be utilized by clients as such;

http://www.webserver.com/patch.php?Version=ClientVersion

Lastly there’s the ‘getmanifest.php’ script which simply retrieves a specific manifest;

<?php
/*The contents of this file are subject to the Mozilla Public License Version 1.1
(the "License"); you may not use this file except in compliance with the
License. You may obtain a copy of the License at http://www.mozilla.org/MPL/

Software distributed under the License is distributed on an "AS IS" basis,
WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
the specific language governing rights and limitations under the License.

The Original Code is getmanifest.php.

The Initial Developer of the Original Code is
Mats 'Afr0' Vederhus. All Rights Reserved.

Contributor(s): ______________________________________.
*/

if(isset($_GET['Manifest']))
{
	$RequestedManifest = htmlspecialchars($_GET['Manifest']);
	$FoundManifest = "";

	if($handle = opendir('./patches/'))
	{
		//Find the requested manifest...
		while (false !== ($entry = readdir($handle)))
		{
        		if($entry === $RequestedManifest)
			{
				$FoundManifest = './patches/' . $entry;
				break;
			}
		}

		if(empty($FoundManifest) || strcmp($FoundManifest, './patches') == 0)
		{
			header('Content-Description: Invalid Request');
			header('Content-Type: text/html; charset=utf-8');
			echo('<html><body><p>No manifest by that name exists!</p></body></html>');
		}
		else
		{
			header('Content-Description: File Transfer');
    			header('Content-Type: application/octet-stream');
    			header('Content-Disposition: attachment; filename='.basename($FoundManifest));
    			header('Content-Transfer-Encoding: binary');
    			header('Expires: 0');
    			header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
    			header('Pragma: public');
    			header('Content-Length: ' . filesize($FoundManifest));
    			ob_clean();
    			flush();
    			readfile($FoundManifest);

			exit();
		}
	}
}
else
{
	header('Content-Description: Invalid Request');
	header('Content-Type: text/html; charset=utf-8');
	echo('<html><body><p>Invalid request!</p></body></html>');
}
?>

It can be utilized as such;

http://www.webserver.com/getmanifest.php?Manifest=ManifestVersion.manifest

I also created a tool for generating these manifests. The important thing, however, is that the two first lines of a manifest are stored as plain ASCII strings so they can be read by ‘patch.php’. Aside from that, manifests can look however you want them to.
Here’s a sample manifest generated by Manifestation;

Child=””
NumFiles=48
&DLLs\Aries.dll MD5: ?5??????
I?[/?
*DLLs\authlogin.dll MD5: ?????
(HitListsTemp.dat MD5: ~l?{??JL,?2?9?
.DLLs\HitMp3DecodeD.dll MD5: ????psCX?p’??2%6
&DLLs\ijl10.dll MD5: ?r?q.GcP??
1DLLs\InternetServiceD.dll MD5: ? y?fU?|[?ro???
8DLLs\TSONetServiceSimClientD.dll MD5: !???m??>`

The added MD5s after filenames are checksums that can be used by clients to verify file integrity.
If you’re looking for a general-purpose way to patch your game(s), I hope I’ve given you some food for thought!