In yesterday’s article, we took a look at the server’s reply format. You now have all the information you need to construct the core of a library and start making server requests.

Here is some sample PHP code for making POST and GET style queries. This library will let you make a query, and return a stdClass object containing the serialised fields as described yesterday, from this you can build wrapper functions for the methods.

$API_CLIENT = new stdClass;

// Configure your client
$API_CLIENT->api_key = "your api key";
$API_CLIENT->secret = "your secret key";
$API_CLIENT->api_endpoint = "http://elggvoices.com/api/v1/";
$API_CLIENT->hmac_algo = 'sha256';
$API_CLIENT->postdata_hash_algo = 'md5';

// Status variables we can query later
$LAST_CALL = null;
$LAST_CALL_RAW = "";
$LAST_ERROR = null;

/**
* Generate our HMAC.
*/
function calculate_hmac($algo, $time, $api_key, $secret_key, $get_variables, $post_hash = "")
{
$ctx = hash_init($algo, HASH_HMAC, $secret_key);
hash_update($ctx, trim($time));
hash_update($ctx, trim($api_key));
hash_update($ctx, trim($get_variables));
if (trim($post_hash)!="") hash_update($ctx, trim($post_hash));
return hash_final($ctx);
}

/**
* Generate our POST hash.
*/
function calculate_posthash($postdata, $algo)
{
$ctx = hash_init($algo);
hash_update($ctx, $postdata);
return hash_final($ctx);
}

/**
* Serialise HTTP headers.
*/
function serialise_headers(array $headers)
{
$headers_str = "";
foreach ($headers as $k => $v)
$headers_str .= trim($k) . ": " . trim($v) . "\r\n";
return trim($headers_str);
}

/**
* Make a raw call.
* @param array $method Method call parameters.
* @param string $postdata Optional POST data.
* @param string $content_type The content type.
* @return stdClass
*/
function call(array $method, $postdata = "", $content_type = 'application/octet-stream')
{

// Get the config
global $API_CLIENT, $LAST_CALL, $LAST_CALL_RAW, $LAST_ERROR;

$headers = array();
$encoded_params = array();

$time = microtime(true); // Get the current time in microseconds
$request = ($postdata!="" ? "POST" : "GET"); // Get the request method, either post or get

// Hard code the format - we're using PHP, so lets use PHP serialisation.
$method['format'] = "php";

// URL encode all the parameters
foreach ($method as $k => $v){
$encoded_params[] = urlencode($k).'='.urlencode($v);
}

$params = implode('&', $encoded_params);

// Put together the query string
$url = $API_CLIENT->api_endpoint."?". $params;

// Construct headers
$posthash = "";
if ($request=='POST')
{
$posthash = calculate_posthash($postdata, $API_CLIENT->postdata_hash_algo);
$headers['X-Searunner-posthash'] = $posthash;
$headers['X-Searunner-posthash-algo'] = $API_CLIENT->postdata_hash_algo;
$headers['Content-type'] = $content_type;
$headers['Content-Length'] = strlen($postdata);
}

$headers['X-Searunner-apikey'] = $API_CLIENT->api_key;
$headers['X-Searunner-time'] = $time;
$headers['X-Searunner-hmac-algo'] = $API_CLIENT->hmac_algo;
$headers['X-Searunner-hmac'] = calculate_hmac($API_CLIENT->hmac_algo,
$time,
$API_CLIENT->api_key,
$API_CLIENT->secret,
$params,
$posthash
);

// Configure stream options
$opts = array(
'http'=>array(
'method'=> $request,
'header'=> serialise_headers($headers)
)
);

// If this is a post request then set the content
if ($request=='POST')
$opts['http']['content'] = $postdata;

// Set stream options
$context = stream_context_create($opts);

// Send the query and get the result and decode.
$LAST_CALL_RAW = file_get_contents($url, false, $context);
$LAST_CALL = unserialize($LAST_CALL_RAW);

if (($LAST_CALL) && ($LAST_CALL->status!=0)) // Check to see if this was an error
$LAST_ERROR = $LAST_CALL;

return $LAST_CALL; // Return a stdClass containing the API result
}

// Example GET call
$get = call(
array (
'method' => 'test.test',
'variable1' => 1,
'variable2' => "test string"
)
);

// Example POST call
$post = call(
array (
'method' => 'test.test',
'variable1' => 1,
'variable2' => "test string"
),
"Some post data"
);

// Output
echo "Example GET result: \n";
print_r($get);

echo "Example POST result: \n";
print_r($post);

On Friday we discussed how to construct an API request, today we will discuss how to handle the response that the server returns.

Server replies

The elggVoices server will respond with a result packet containing a number of fields. The format of this reply depends on the format requested. From the manual:

  • ‘status’: The status code, 0 for success or a non-zero error code.
  • ‘message’: If the status code is non-zero this contains a human readable
    explanation.
  • ‘result’: The mixed result of the execution, this may be a simple type
    (int, string etc), an array or even an object, depending on the API being
    called (see also result objects).
  • ‘runtime_errors’: If there have been any runtime errors picked up by
    the internal error handler during the execution of the command, these
    are given here in an array.

The server result will be returned, serialised in the requested format (xml,php or json).

Errors will often give details pointing to what went wrong, these can be helpful when debugging your application or when looking for support on the forums.

Have a look at this XML formatted error message, XML success messages will be of a similar format:

<SearunnerResult>
<status type="integer">-1</status>
<message type="string">Missing X-Searunner-apikey HTTP header</message>
<result type="string">exception 'APIException' with message 'Missing X-Searunner-apikey HTTP header' in /home/liveshouts/html/lib/api/validation.php:109
Stack trace:
#0 /home/liveshouts/html/api/endpoints/rest.php(19): get_and_validate_api_headers()
#1 {main}</result>
<runtime_errors type="array">
<array_item name="0" type="string" >DEBUG: 2008-02-24 18:38:19 (UTC): & quot;Undefined index: method& quot;
in file /home/liveshouts/html/lib/engine/input.php (line 30)</array_item>
<array_item name="1" type="string">DEBUG: 2008-02-24 18:38:19 (UTC): &quot;Undefined index: HTTP_X_SEARUNNER_APIKEY&quot; in file /home/liveshouts/html/lib/api/validation.php (line 107)</array_item>
</runtime_errors>
</SearunnerResult>

HTTP Headers

In addition to the method call, format and variables sent on the URL line (discussed in yesterday’s article), you are required to send some extra information in the headers of the outgoing HTTP request.

From the API Documentation:

  • X-Searunner-apikey Your public API key you were given when signing
    up for a developer account.
  • X-Searunner-time A float representation of “now” including
    milliseconds (like that produced by the php function microtime(true))
  • X-Searunner-hmac-algo The algorithm used for generating the HMAC,
    like sha1, sha256, MD5 etc.
  • X-Searunner-hmac The HEX representation of the HMAC signing the
    request.

The following headers are only required when sending POST data:

  • X-Searunner-posthash A hash of the POST data packet.
  • X-Searunner-posthash-algo The algorithm used to create the POST
    hash, eg md5, sha1 etc.
  • Content-type The content type of the data (default:
    application/octet-stream)
  • Content-Length The length of the POST data in bytes.

Initially, this may look like rather a lot to take in, but it is actually quite simple.

The two algorithm selection fields X-Searunner-hmac-algo and X-Searunner-posthash-algo give the name of the algorithm used – md5, sha1, sha256 etc. Letting the client specify the algorithm used lets us get around issues with buggy/missing algorithm implementations. For example, we use MD5 for the Java library as this removes the need to install a third party JCE.

Where possible you should probably try and use “sha256” for HMAC and “sha1” for hashing your post data, since over time we will likely “retire” the weaker algorithms.

X-Searunner-time timestamps the request, while Content-type and Content-Length (for POST requests) are pretty much self explanatory.

The field X-Searunner-posthash ensures that the POST body can not be modified, and it is a lower case hexadecimal representation of the desired HASH algorithm, taken over all the data in the POST body, take a look at this pseudo code:

hash = algorithmFactory( //X-Searunner-posthash-algo// );
hash.update( //POST DATA// );
return hex_encode(hash.final());

Finally, the field X-Searunner-hmac contains a signature which lets the elggVoices server guarantee that it was you that sent the request (rather than someone pretending to be you) and that the contents of the request have not been modified. This is where your secret key comes in, and is the reason why it is important that you keep it safe!

From the manual:

A HMAC is a simple cryptographic signature, in this case is formed over the
following information:

  • The time header eg, 12345678.90
  • Your API key
  • The GET query string, eg “format=xml&method.example&variable=foo“. It is
    important to note two things, firstly that the preceding “?” character has
    been dropped, and second that the order is identical to the order they
    appear on the URL line.
  • If this is a POST request, the hash of the POST data.

Take a look at this pseudo code:

hmac_cipher = algorithmFactory( //X-Searunner-hmac-algo// );
hmac_cipher.init(//SECRET KEY//);
hmac_cipher.update( //X-Searunner-time// );
hmac_cipher.update( //X-Searunner-apikey// );
hmac_cipher.update( //GET VARIABLES IN CALL ORDER// );
if (POST CALL) hmac_cipher.update( //X-Searunner-posthash// );

return hex_encode(hmac_cipher.final());

To form the HMAC, each variable’s string representation is turned into a string of bytes. This byte string is fed in turn into the HMAC algorithm which spits out a HMAC signature which is then converted into a lower case hexadecimal string. These are Octet strings, ensure that when adding the strings to the cipher that your language doesn’t convert it into unicode!

Note that the order in which you add each field is VERY IMPORTANT.

Next comes the server response…