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;
	 }
  }
}
?>

Introducing Post Test Server V2

Screenshot of the PTSV2 Homepage

This post is such a big deal for me. After about 10 years of lamenting the limitations of Post Test Server, I’ve finally released a new version, Post Test Server V2. I talk more about it on the site, but the high level story is that the old version had a lot of drawbacks. People couldn’t delete their content, it wasn’t flexible enough, and it did not checking before saving the dumps so once botnets found it the cost to run the service became untenable. This new version has been rewritten in Go and is hosted on Google App Engine. Check it out: http://ptsv2.com.

Over the years a lot of people asked me for the source for Post Test Server. I didn’t share it because I was embarrassed that I wrote something in PHP (even if it was 15 years ago) and it was literally just dumping the values of some post variables so it didn’t seem worthwhile. But, now that I have a real replacement service I feel less bad sharing this:

Continue reading “Introducing Post Test Server V2”

Photos from my first day at Palmer

This site didn’t have media on it so I dug up some photos taken by Josh Sweeney at Shoot For Details from a track day in the rain at Palmer Motorsports Park with my car and car from Ace Performance. This is quickly becoming my favorite track, not just on the east coast but of any track I have ever been to. The combination of elevation and blind apex turns make this track a treat.

Testing multipart/form-data uploads with Post Test Server

The fact that I am getting feature requests means people are actually using my post test server. This makes me happy :) The most recent thing people asked for was multipart/form-data uploads. As an example for this behavior see my test form (which was submitted by a user who was super helpful).

Update:This poor post has been imported from blog to blog over the years and as a result this code snippet has been horribly maimed and will not be formatted. C’est la vie.

<html>
<body>
<form action="http://posttestserver.com/post.php?dir=example" method="post" enctype="multipart/form-data">
File: <input type="file" name="submitted">
<input type="hidden" name="someParam" value="someValue"/>
<input type="submit" value="send">
</form>
</body>
</html>

The resulting output should contain information about the file and a link to the actual uploaded file. Remember, this data is public and you should NOT upload private data.

Post Test Server now supports custom status messages

Today, the system worked! I received an email from a user asking if I could add support for custom status codes to Post Test Server and I was happy to oblige. So I have added a new parameter which can be passed in the URL called: status_code which causes the server to return a response with the header set to: HTTP/1.0 $status Custom Status (where $status is the value of status_code). Regardless of what code is requested the post will still be dumped in folder in the usual way.

To make it very clear, hitting this url:
http://posttestserver.com/post.php?dir=bot&status_code=650
will cause the server to respond with HTTP/1.0 650 Custom Status.

Added features to my free HTTP Post Dumping / Testing service.

A while ago I put together a very simple php script that dumps any HTTP Post it receives:

To my great surprise, people have actually started using it! I also found myself using it more and more in my own debugging and so I have added two features which further my original mission of maximizing this project’s value to effort ratio. Total effort is still under one hour.

New Features:

  1. Now dumps all Header parameters. Previously I was only dumping the ones that I thought relevant. Now you can see carrier and ISP inserted fields as well.
  2. You may now specify a directory in the query string to have your post written there. So instead of hitting: http://posttestserver.com/post.php you instead hit: http://posttestserver.com/post.php?dir=myself and then after selecting the current date you will see a myself directory containing your uploads

Hopefully this continues to help people. Even if it doesn’t, it helps me so I’m satisfied.

Let me dump your post – Free HTTP Post test server

This past week I found myself writing code that had to submit some data to a webservice via an HTTP POST request. Not a particularly difficult task but it was on a platform I didn’t have much experience with and I wasn’t sure if I had formed the packet properly. In order to validate my bits I wrote a small php page which accepts POST requests and dumps them locally. Thanks to Dreamhost I’m now able to share this with everybody:

http://www.posttestserver.com

I’ll be slightly taken aback if anybody actually uses this thing, but when is that actually the point?

UPDATE: I added features!