Goodbye Post Test Server + Source

Given my original goal of releasing Post Test Server and never thinking about it again, I certainly never thought I’d see the day I was writing a goodbye post. But, all good things must come to an end. In this case, a recent round of bot based spam drove my Google Bill higher than I’d like and after 15 years this seemed like a good enough reason to move on from this game.

So, without too much fanfare, I leave you with some things:

The source for the current version of Post Test Server v2. This is a Golang project meant to be hosted on Appengine. If anybody does anything with it, great. Just remember that if you host it publicly, you can wake up with a $1,000 Appengine bill.

The source for the original Post Test Server. I won’t beat around the bush on this one, this is a gross PHP script. But, it got the job done.

A link to Request Bin which is the closest similar thing I could find online.

Source for the Original Post Test Server

After over 10 years, I’m finally out of the post test serving game. But there will be details on that in the next post. This post is a celebration of the original version of Post Test Server!

This was something I did in 2008 while developing the first client library for Localytics. It was a simple PHP script hosted on Dreamhost that allowed me to see what my BlackBerry test device was doing to the headers of our uploads (15 years later I still don’t understand BlackBerry uploading). I left it online thinking maybe another developer would one day find some value out of the service.

Curiously, people did find the service and over the course 10 years I slowly added features if they were easy enough and people asked nicely. This is also when people started asking for the source but I was too embarrassed to share something that was originally started as a 3 line script for dumping some upload headers to a file. But, enough time has gone on that I’m over it, so here are 200 lines of PHP that receive and dump posts.

Warning: I ran this 10 years ago, on a server with nothing of value. It would be ill advised to run this without appreciating the security concerns associated with processing data uploaded from anybody on the internet.

<?php

$basedir = "/home/redacated/www.posttestserver.com/data/";
$filedir = "/home/redacted/www.posttestserver.com/files/";

# Enable CORS
header('Access-Control-Allow-Origin: *');

# mkdir fails if the directory already exists
function makeDir($dir)
{
  if(file_exists($dir) == false) {
    mkdir($dir);
  }
}

# create a directory based on the current date (if it doesn't already exist)
function dir_with_date($startDir)
{
  $dir = $startDir . date("Y");
  makeDir($dir);
  $dir = "$dir/".date("m");
  makeDir($dir);
  $dir = "$dir/".date("d");
  makeDir($dir);

  return $dir;
}

# Allow user to override the default 200 HTTP status code
if (isset($_GET['status_code']))
{
  $status = $_GET['status_code'];
  header("HTTP/1.0 $status Custom Status", true, $status);
}

# wait up to 30 seconds before returning a response (helpful for testing timeouts and the like)
if(isset($_GET['sleep']))
{
  $sleep_count = $_GET['sleep'];
  if($sleep_count > 30) { $sleep_count = 30; }
  sleep($sleep_count);
}

# start dumping the data
$output = "Time: " . date(DATE_RFC822) . "\n";
$output .= "Source ip: " . getenv('REMOTE_ADDR') . "\n";

# loop through all headers and dump them
$output .= "\nHeaders (Some may be inserted by server)\n";
foreach ($_SERVER as $name => $content) {
  # ignore server specific content (confuses people)
  if(preg_match("/^PATH/", $name) ||
     preg_match("/^RAILS/", $name) ||
	 preg_match("/^FCGI/", $name) ||
	 preg_match("/^SCRIPT_URL/", $name) ||
	 preg_match("/^SCRIPT_URI/", $name) ||
	 preg_match("/^dsid/", $name) ||
	 preg_match("/^ds_id/", $name) ||
	 preg_match("/^DH_USER/", $name) ||
	 preg_match("/^DOCUMENT/", $name) ||
	 preg_match("/^SERVER/", $name) ||
	 preg_match("/^SCRIPT/", $name) ||
	 preg_match("/^argv/", $name) ||
	 preg_match("/^argc/", $name) ||
	 preg_match("/^PHP/", $name) ||
	 preg_match("/^SCRIPT/", $name) ) {
    continue;
  }
    # This spammer was so prolific they get their own line
    if(false == preg_match("/islandshangrila.digitalcampaignasia.com/", $content)) {
	$output .= "$name = $content\n";
	}
}

$output .= "\n";

# dump any post params if they exist
if($_POST && count($_POST) > 0 )
{
  $output .= "Post Params:\n";
  foreach ($_POST as $key => $value) {
    if($key == "var1" && $value = "lol") {
      $ignore = true;
	}
    $output .= "key: '$key' value: '$value'\n";
  }
}
else
{
  $output .= "No Post Params.\n";
}

# Dump the post body
if($HTTP_RAW_POST_DATA)
{
  $output .= "\n== Begin post body ==\n";
  $output .= $HTTP_RAW_POST_DATA;
  $output .= "\n== End post body ==\n";
}
else
{
  $output .= "Empty post body.\n";
}

# Handle multipart/form-data
# $_FILES is a hash of hashes, one for each uploaded file
if(isset($_SERVER["CONTENT_TYPE"]) &&
   preg_match("/multipart\/form-data/i", $_SERVER["CONTENT_TYPE"] )
)
{
  $output .= "\n== Multipart File upload. ==\n";
  $output .= "Received " . count($_FILES) . " file(s)\n";
  $count = 0;
  foreach($_FILES as $key => $value)
  {
    $output .= " $count: posted name=$key\n";
	foreach($_FILES[$key] as $key2 => $value2)
	{
	  if(!strcmp($key2, "tmp_name")) {
	    continue;
	  }
	  $output .= "    $key2: $value2\n";
	}

	# move the file from temp storage to the actual destination
	$uploaded = $_FILES[$key]['tmp_name'];
    $target_filename = "f_" . date("H.i.s") . rand();
	$target_path = dir_with_date($filedir) . "/$target_filename";
	$target_url = "/files/".date("Y/m/d")."/$target_filename";

    if(is_uploaded_file($uploaded))
    {
      if(copy($uploaded, $target_path)) {
        $output .= "Uploaded File: http://posttestserver.com$target_url\n";
	  }
	  else {
	    $output .= "File uploaded successfully but could not be copied.\n";
  	  }
    }
    else {
      $output .= "File specified was not uploaded. Possible file upload attack.\n";
    }
  }
}

# dump any HTTP PUT data
$putdata = fopen("php://input", "r");
$didit = false;
while ($data = fread($putdata, 1024)) {
  if(!$didit) {
    $output .= "\nUpload contains PUT data:\n";
	$didit = true;
  }
  $output .= $data;
}
fclose($putdata);

$dir = dir_with_date($basedir);

if(! empty($_GET) && isset($_GET["dir"])) {
  # people get clever with their filenames
  $target = str_replace(".", "", $_GET["dir"]);
  $target = str_replace("/", "", $target);
  $target = str_replace(";", "", $target);

  if(strlen($target) > 1) {
    $dir = "$dir/$target";
	makeDir($dir);
  }
}

if($ignore) {
  exit;
}

$filename = date("H.i.s") . rand();
$file = $dir . "/$filename";
$fh = fopen($file, 'w');
fwrite($fh, $output);
fclose($fh);

if (isset($_GET['response_body']))
{
  echo $_GET['response_body'];
}
else
{
  if (isset($_GET['dump']) == false )
  {
      echo "Successfully dumped " . count($_POST) . " post variables.\n";
      $path = date("Y/m/d") . "/";
	  if(isset($_GET["dir"])) {
        $path .=  $_GET["dir"] . "/";
	  }
	  $path .= "$filename\n";

	  #echo "View it at http://www.posttestserver.com/data/" . date("Y/m/d") . "/$filename\n";
	  echo "View it at http://www.posttestserver.com/data/$path";
      echo "Post body was " . strlen($HTTP_RAW_POST_DATA) . " chars long.";
  }

  if (isset($_GET['dump']))
  {
     if(isset($_GET['html']))
	 {
	   echo '<html><head><title>Post test</title></head><body>';
       echo str_replace("\n", "<br />", $output);
       echo '</body></html>';
	 }
	 else {
       echo $output;
	 }
  }
}
?>