May 5, 2010

Tunneling a multipart POST message through PHP and CURL

In this post I’m going to cover a particular solution to a given problem we’ve faced up in our last project. Let’s set the background: there is a complex web application under development on a shared virtual machine, installed on a subdomain of a public network that will change when going to production. We have also a commercial website hosted in a shared server under a domain that should not change, since it is our first step out of this virtual farm.

For this project, we require to integrate some cameras which send pictures via GPRS connections to the server, and we want to provide some user authentication to avoid having anybody posting whatever to this web service. So, we have developed a controller in Code Igniter which gets a camera ID / password from POST, and also the bytes for the image, and proceeds to do some filtering. For instance, camera ID and password should match with the values previously registered in the database, and the picture should be MIME image/jpeg, with a limit of 2MBs. Up to now, nothing special, tested and working with POSTS from the within the website.

The problem came when we found out that cameras only allow to send their information to a root domain, to a particular file, let’s say upload.php. So, we would only need to put this file in the root path and it’s done. Unfortunately, doing so in the application virtual server would result on flashing again all the cameras once we move to a new server and set up a final domain name. We might be able to install this receiver in the commercial website root path, but then we would require to send the message back to the application server in any way, so it is not the best solution we want to solve this issue.

‘Just add a domain that points to this subdomain and you are done’ would you say. Well, yes, that would have done the trick. Unfortunately, we cannot host this domain in the machine since we have no administrator rights on the servers farm. Nor can we just use this external domain because it produces an HTTP redirection which, to make things worse, is not supported by the camera. We’ve even tried with cloaking the url of the receiver with an external subdomain, with no success – since cloaking wraps the real content inside an iframe and it breaks the POST message.

What a headache! it seemed there was no way to implement the system with the full functionality we wanted…

At thas moment we were a bit puzzled because we had to choose between two main options, which in any case took us away from the initial implementation design:

  • hosting the receiver script in a subdomain that would change
  • hosting it in a fixed external domain (the commercial website) and try to send the messages back to the application server
  • in any case, we would have to implement the features planned for the Code Igniter controller in this new stand-alone script, which would produce extra work and remove some built-in capabilities

How to fix this problem? Well, he have chosen a variant of #2: try to send the messages back to the application server. But how exactly? Well, by tunneling the multipart POST message with CURL.

To achieve our goal we have written a script which is hosted in the domain root of the commercial website, with does the following:

  1. Reads the serial number sent by the camera
  2. Reads the temporary upload file, and moves it to a specified folder
  3. Initializes and configures PHP-CURL to generate a new POST message with the data recovered and the image content as multipart form
  4. Deletes the temporary file created

And the code:

/**
 * @file upload.php
 * This pipeline grabs a POST message and sends it back to a processing server
 * It is meant to solve a particular problem where the sender device has limited
 *  HTTP functionalities like being unable to deal with Apache redirections
 *
 * This code is just an example, you may use for learning purposes
 *
 */
// camera serial number, it is barely filtered because the real processing
//  is to be done in the final server
$csn	= ( isset( $_POST['CSN'] ) ? $_POST['CSN'] : 0 );

// we need to locally store the file to resend it
$file 	= $_FILES['userfile'];
$tmp 	= "tmp/" . $file["name"];
move_uploaded_file( $file["tmp_name"], $tmp );

// post information is rebuilt
$data = array( 'CSN' => $csn, 'userfile' => '@' . $tmp );
// note the @ which tells CURL that this field is a file
// this will be automatically interpreted as a application/multipart form

// init and configure curl
$ch = curl_init();
curl_setopt( $ch, CURLOPT_URL, 'http://the-application-server/path/controller/method' );
curl_setopt( $ch, CURLOPT_POST, true );
curl_setopt( $ch, CURLOPT_POSTFIELDS, $data );

// finally, send
curl_exec ($ch );

// and delete the locally stored file
unlink($tmp);