The Nokia Image Upload Protocol

(Freshly updated after a few fixes. Read on, source code included)

I've spent a couple of hours implementing the Nokia Image Upload API. I had already read through the protocol specs and they seemed straightforward enough, so it was quite a surprise to see my 7650 trying to post to an ASP (/login/uploaderLogin.asp) when the entire protocol document goes to great pains to be URL-independent.

Nothing to it, though. I just put up an .htaccess that reads:

<Files login>
    AcceptPathInfo on
    SetOutputFilter PHP
    SetInputFilter PHP
    LimitRequestBody 65535

This invokes login as a PHP script, forces the URL path to be sent to the script as REQUEST_URI and sets a hard limit (Apache 2-specific, I think) on the upload size. No sweat. And the rest of the protocol is pretty straightforward - you GET or POST a specific URL, and you get plaintext with the info you want.

Then came the "late night hacking" bit. I'm dog tired, but here are a few notes: You login by POSTing four fields: Username, Password, Language ("EN"), and a protocol Version ("1.0"), getting this as a reply:


You can optionally return your image server capabilities right away, but I decided to implement things step-by-step. The next transaction the 7650 performs (after an unexplicable delay, since it seems to spend quite some time brooding) is a GET to the remoteStorageCapabilities URL, and it expects something like this:


Then it tries to get a directory listing by GETting dirList and, no matter what I send back (the format is pretty straightforward), my 7650 complained of not being able to contact the server - that turned out to be due to the lack of a "Content-Length" header. After googling around for a bit after noon, I found a thread where some of this was being discussed, and proceeded to get "upload" working.

There is, however, a PHP bug that prevents me from getting this to work on my box, so I've decided to hold off for a bit until I can shift my mental gear box from scripting to RPM building and get 4.3.2 compiled on my development server.

For the curious, here is my code - a "harness" class, designed to test all of this before doing a proper implementation. It is a gross hack that maps URL bits straight to class methods, spitting out stuff to the error_log as it goes along. It uses the PHP session mechanism as "storage", so the 7650 creates "directories" and "files" on a session-based hash table that is persistent across requests (I was feeling too lazy to go off and implement a full-blown storage mechanism, and letting a development script write to your filesystem is a BAD idea any way you put it...).

Mind you, I can read serialize() output like other people read plaintext (so the debug output may need some work for non-PHP geeks), and I think in terms of hash tables and eval() (a Perlism that stuck with me). This code works entirely off /login/anythingGoesHere, since I couldn't bother setting up separate scripts, and I may have gotten something definetly wrong (It was started at around 2:30 AM, after all...). YMMV.

ini_set( "log_errors", "1" );
ini_set( "display_errors", "0" );

define( "SERVER_HOSTNAME", "" );

class NokiaUploader {
  function NokiaUploader() {
    $aURLMap = array( "/uploaderLogin/" => "login",
                      "/RS/"            => "remoteStorage",
                      "/CreateDir/"     => "createDir",
                      "/Upload/"        => "upload",
                      "/DirList/"       => "dirList",
                      "/DirContents/"   => "_default",
                      "/GetItem/"       => "_default",
                      "/DeleteDir/"     => "_default" );
    $szEnd = basename( $_SERVER["REQUEST_URI"] );
error_log( serialize( $_SERVER ) );
error_log( serialize( $_POST ) );
    foreach( $aURLMap as $szKey => $szMethod ) {
      if( preg_match( $szKey, $szEnd ) ) {
        if( $_SERVER["QUERY_STRING"] ) {
          session_id( $_SERVER["QUERY_STRING"] ); // bind to the ID we generated upon login
          session_start(); // retrieve session context
        $szResponse = eval( "return \$this->" . $szMethod . "();" );
error_log( serialize( $_SESSION ) );
    $this->reply( $szResponse );

  function login() {
    $aURLs = array( "RS" );
    $szBaseURL = "http://" . SERVER_HOSTNAME . dirname($_SERVER["PHP_SELF"]) . "/";
    if( $this->authenticate() ) {
      $szResponse  = "0\r\nVersion=1.00\r\n";
      $szResponse .="SId=" . $this->getsessionid() . "\r\n";
      foreach( $aURLs as $szURL ) {
        $szResponse .= $szURL . "URL=" . $szBaseURL . $szURL . "\r\n";
    else {
      $szResponse = "2\r\n";
    return $szResponse;
  } // login

  function remoteStorage() {
    $aURLs = array( "CreateDir", "Upload", "DirList" ); // mandatory
    //$aURLs = array( "CreateDir", "Upload", "DirList", "DirContents", "GetItem", "DeleteDir" );
    $szBaseURL = "http://" . SERVER_HOSTNAME . dirname($_SERVER["PHP_SELF"]) . "/";

    $szResponse = "0\r\n";
    foreach( $aURLs as $szURL ) {
      $szResponse .= $szURL . "URL=" . $szBaseURL . $szURL . "\r\n";
    return $szResponse;
  } // remoteStorage

  function authenticate() {
    return true; // assume $_POST["Username"] , etc. was OK
  } // authenticate

  function getsessionid() {
    $szSessionID = md5( $_POST["Username"] . $_POST["Password"] );
    return $szSessionID;
  } // getsessionid

  function dirList() {
    return $this->unserialize(); // do this someplace else for now
  } // dirList

  function createDir() {
    $aStorage = $_SESSION["RS"];
    $szID = md5( microtime() );
    $aStorage[$szID] = array( "_name" => $_POST["DirName"],
                              "_desc" => $_POST["DirDesc"],
                              "_parent" => $_POST["Pid"] );
    $szResponse = "0\r\nDid=$szID\r\n";
    $_SESSION["RS"] = $aStorage;
    return $szResponse;
  } // createDir

  function upload() {
    // just say OK and see the data in the $_POST dump for now
    // $_FILES holds the uploaded file data, if any
    $szResponse = "0\r\nFree=" . 1024*1024 . "\r\n" . "Id=" . md5( microtime() );
    return $szResponse;
  } // upload

  function _default() {
error_log( $_SERVER["PHP_SELF"] );

  function unserialize() {
    $aStorage = $_SESSION["RS"];
    if( empty( $aStorage ) )
      return "0\r\n0\r\n";

    $szResponse = "0\r\n";
    $szResponse .= count($aStorage) . "\r\n2\r\n";
    foreach( $aStorage as $szDirectoryID => $aDirectory ) {
      $szResponse .= "DId=" . $szDirectoryID . "\r\nDirName=" . $aDirectory["_name"] . "\r\n";
    return $szResponse;
  } // unserialize

  function reply( $szResponse ) {
    //header( "Content-Type: text-plain; charset=utf-8" );
    $szResponse = utf8_encode( $szResponse );
    header( "Content-Length: " . strlen( $szResponse ) );
    echo $szResponse;
    error_log( $szResponse );
  } // reply

$oUploader = new NokiaUploader();

Feel free to send me any thoughts on this by e-mail (rui dot carmo at a domain called, but I'm not going to pursue this much further for a few days (maybe weeks), unless I can get PHP 4.3.2 compiled sooner.

Update: PHP 4.3.2 fixed the file upload issue I had - the code works, and might be the simplest available PHP example for dealing with Series 60 image uploads.