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:

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:

# I ran this on a virtualized server, so accessing the filesystem directly wasn't a risk.
# I would not recommend this any other way.
$basedir = "/home/henryci/";
$filedir = "/home/henryci/";
# Enable CORS
header('Access-Control-Allow-Origin: *');
# Creates a local directory
function makeDir($dir)
if(file_exists($dir) == false) {
# Creates the path for today's uploads
function dir_with_date($startDir)
$dir = $startDir . date("Y");
$dir = "$dir/".date("m");
$dir = "$dir/".date("d");
return $dir;
# use &status_code to force the server to return a specific status code
if (isset($_GET['status_code']))
$status = $_GET['status_code'];
header("HTTP/1.0 $status Custom Status", true, $status);
# use &sleep to delay the input. Set a max on this to avoid all threads getting tied up
$sleep_count = $_GET['sleep'];
if($sleep_count > 30) { $sleep_count = 30; }
$output = "Time: " . date(DATE_RFC822) . "\n";
$output .= "Source ip: " . getenv('REMOTE_ADDR') . "\n";
# Grab the headers present in the upload
$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) ) {
$output .= "\n";
# Avoid writing huge files
$totalsize = (int) $_SERVER['CONTENT_LENGTH'];
if($totalsize > 537387 ) { # Honestly, I have no idea where I got this magic number from. :)
echo "Posted message too large. :(";
# Parse the post parameters
if($_POST && count($_POST) > 0 )
$output .= "Post Params:\n";
foreach ($_POST as $key => $value) {
$output .= "key: '$key' value: '$value'\n";
$output .= "No Post Params.\n";
# If the post contains a raw data block
$output .= "\n== Begin post body ==\n";
$output .= $HTTP_RAW_POST_DATA;
$output .= "\n== End post body ==\n";
$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")) {
$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(copy($uploaded, $target_path)) {
$output .= "Uploaded File:$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";
# read in any data uploaded via a PUT
$putdata = fopen("php://input", "r");
$didit = false;
while ($data = fread($putdata, 1024)) {
if(!$didit) {
$output .= "\nUpload contains PUT data:\n";
$didit = true;
$output .= $data;
$dir = dir_with_date($basedir);
# Allowing the end user to name the file is a risk.
if(! empty($_GET) && isset($_GET["dir"])) {
$target = str_replace(".", "", $_GET["dir"]);
$target = str_replace("/", "", $target);
$target = str_replace(";", "", $target);
if(strlen($target) > 1) {
$dir = "$dir/$target";
# Name the upload w/ a timestamp and random number
$filename = date("H.i.s") . rand();
$file = $dir . "/$filename";
$fh = fopen($file, 'w');
fwrite($fh, $output);
# Allow the user to specify a custom response
if (isset($_GET['response_body']))
echo $_GET['response_body'];
else # or else output the results.
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$path";
echo "Post body was " . strlen($HTTP_RAW_POST_DATA) . " chars long.";
if (isset($_GET['dump']))
echo '<html><head><title>Post test</title></head><body>';
echo str_replace("\n", "<br />", $output);
echo '</body></html>';
else {
echo $output;

view raw


hosted with ❤ by GitHub

13 thoughts on “Introducing Post Test Server V2

    1. No idea. This is > 10 years old and hasn’t been touched since so something fairly old I’m presuming. I also don’t believe it is using any real features of the language so I don’t think language version will cause any troubles.


  1. Hi Henry,

    I have started getting the following “Over Quota” error when testing with your server. Any idea when it is going to be fixed?


    Over Quota
    This application is temporarily over its serving quota. Please try again later.

    I love this server and thanks in advance for providing it for the public!


    1. It will reset tomorrow. (it’s a daily quota). I had somebody hitting the server 185,000 times an hour:
      I’ve deleted their bucket. Bear with me until this weekend and I’ll add code to block IPs of anybody who hits the service that much.

      I’m sorry I don’t have a better answer, but the quota limit just saved my butt so I’m not tweaking it just yet.


    1. Thanks for letting me know! It actually wasn’t being beaten up by any one actor this time. It just seems to be getting enough use that it is hitting the set $5 per day limit. I upped the limit. It will reset tonight (I can’t do anything about it until then) and then I’ll use the new limit tomorrow to push some changes that make it more efficient.

      Always something!


Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s