aboutsummaryrefslogtreecommitdiff
path: root/engine/lib/api.php
diff options
context:
space:
mode:
Diffstat (limited to 'engine/lib/api.php')
-rw-r--r--engine/lib/api.php312
1 files changed, 309 insertions, 3 deletions
diff --git a/engine/lib/api.php b/engine/lib/api.php
index 53c548b19..b5371f605 100644
--- a/engine/lib/api.php
+++ b/engine/lib/api.php
@@ -75,11 +75,14 @@
* Override this to include any more specific information, however api results should be attached to the
* class using setResult().
*
+ * if $CONFIG->debug is set then additional information about the runtime environment and authentication will be
+ * returned.
+ *
* @return stdClass Object containing the serialised result.
*/
public function toStdClass()
{
- global $ERRORS;
+ global $ERRORS, $CONFIG, $PAM_HANDLER_MSG;
$result = new stdClass;
@@ -89,8 +92,14 @@
$resultdata = $this->getResult();
if (isset($resultdata)) $result->result = $resultdata;
- if (count($ERRORS))
- $result->runtime_errors = $ERRORS;
+ if ((isset($CONFIG->debug)) && ($CONFIG->debug == true))
+ {
+ if (count($ERRORS))
+ $result->runtime_errors = $ERRORS;
+
+ if (count($PAM_HANDLER_MSG))
+ $result->pam = $PAM_HANDLER_MSG;
+ }
return $result;
}
@@ -158,10 +167,307 @@
}
}
+ // PAM AUTH HMAC functions ////////////////////////////////////////////////////////////////
+ /**
+ * Map various algorithms to their PHP equivs.
+ * This also gives us an easy way to disable algorithms.
+ *
+ * @param string $algo The algorithm
+ * @return string The php algorithm
+ * @throws APIException if an algorithm is not supported.
+ */
+ function map_api_hash($algo)
+ {
+ $algo = strtolower(sanitise_string($algo));
+ $supported_algos = array(
+ "md5" => "md5",
+ "sha" => "sha1", // alias for sha1
+ "sha1" => "sha1",
+ "sha256" => "sha256"
+ );
+
+ if (array_key_exists($algo))
+ return $supported_algos[$algo];
+
+ throw new APIException("Algorithm '$algo' is not supported or has been disabled.");
+ }
+ /**
+ * Calculate the HMAC for the query.
+ * This function signs an api request using the information provided and is then verified by
+ * searunner.
+ *
+ * @param $algo string The HMAC algorithm used as stored in X-Searunner-hmac-algo.
+ * @param $time string String representation of unix time as stored in X-Searunner-time.
+ * @param $api_key string Your api key.
+ * @param $secret string Your secret key.
+ * @param $get_variables string URLEncoded string representation of the get variable parameters, eg "format=php&method=searunner.test".
+ * @param $post_hash string Optional sha1 hash of the post data.
+ * @return string The HMAC string.
+ */
+ function calculate_hmac($algo, $time, $api_key, $secret_key, $get_variables, $post_hash = "")
+ {
+ $ctx = hash_init(map_api_hash($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);
+ }
+
+ /**
+ * Calculate a hash for some post data.
+ *
+ * TODO: Work out how to handle really large bits of data.
+ *
+ * @param $postdata string The post data.
+ * @param $algo string The algorithm used.
+ * @return string The hash.
+ */
+ function calculate_posthash($postdata, $algo)
+ {
+ $ctx = hash_init(map_api_hash($algo));
+
+ hash_update($ctx, $postdata);
+
+ return hash_final($ctx);
+ }
+ /**
+ * This function will do two things. Firstly it verifys that a $hmac hasn't been seen before, and
+ * secondly it will add the given hmac to the cache.
+ *
+ * TODO : REWRITE TO NOT USE ZEND
+ *
+ * @param $hmac The hmac string.
+ * @return bool True if replay detected, false if not.
+ */
+ function cache_hmac_check_replay($hmac)
+ {
+ global $CONFIG;
+
+ throw new NotImplementedException("Writeme!");
+
+ return true;
+ }
+ /**
+ * Find an API User's details based on the provided public api key.
+ *
+ * @param int $site_guid The GUID of the site.
+ * @param string $api_key The API Key
+ * @return mixed stdClass representing the database row or false.
+ */
+ function get_api_user($site_guid, $api_key)
+ {
+ global $CONFIG;
+
+ $api_key = sanitise_string($api_key);
+ $site_guid = (int)$site_guid;
+
+ return get_data_row("SELECT * from {$CONFIG->dbprefix}api_users where api_key='$api_key' and site_guid=$site_guid and active=1");
+ }
+
+ /**
+ * This function looks at the super-global variable $_SERVER and extracts the various
+ * header variables needed to pass to the validation functions after performing basic validation.
+ *
+ * @return stdClass Containing all the values.
+ * @throws APIException Detailing any error.
+ */
+ function get_and_validate_api_headers()
+ {
+ $result = new stdClass;
+
+ $result->method = $_SERVER['REQUEST_METHOD'];
+ if (($result->method != "GET") && ($result->method!= "POST")) // Only allow these methods
+ throw new APIException("Request method must be GET or POST");
+
+ $result->api_key = $_SERVER['HTTP_X_ELGG_APIKEY'];
+ if ($result->api_key == "")
+ throw new APIException("Missing X-Elgg-apikey HTTP header");
+
+ $result->hmac = $_SERVER['HTTP_X_ELGG_HMAC'];
+ if ($result->hmac == "")
+ throw new APIException("Missing X-Elgg-hmac header");
+
+ $result->hmac_algo = $_SERVER['HTTP_X_ELGG_HMAC_ALGO'];
+ if ($result->hmac_algo == "")
+ throw new APIException("Missing X-Elgg-hmac-algo header");
+
+ $result->time = $_SERVER['HTTP_X_ELGG_TIME'];
+ if ($result->time == "")
+ throw new APIException("Missing X-Elgg-time header");
+ if (($result->time<(microtime(true)-86400.00)) || ($result->time>(microtime(true)+86400.00))) // Basic timecheck, think about making this smaller if we get loads of users and the cache gets really big.
+ throw new APIException("X-Elgg-time is too far in the past or future");
+
+ $result->get_variables = $_SERVER['QUERY_STRING'];
+ if ($result->get_variables == "")
+ throw new APIException("No data on the query string");
+
+ if ($result->method=="POST")
+ {
+ $result->posthash = $_SERVER['HTTP_X_ELGG_POSTHASH'];
+ if ($result->posthash == "")
+ throw new APIException("Missing X-Elgg-posthash header");
+
+ $result->posthash_algo = $_SERVER['HTTP_X_ELGG_POSTHASH_ALGO'];
+ if ($result->posthash_algo == "")
+ throw new APIException("Missing X-Elgg-posthash_algo header");
+
+ $result->content_type = $_SERVER['CONTENT_TYPE'];
+ if ($result->content_type == "")
+ throw new APIException("Missing content type for post data");
+ }
+
+ return $result;
+ }
+
+ /**
+ * Return a sanitised form of the POST data sent to the script
+ *
+ * @return string
+ */
+ function get_post_data()
+ {
+ global $GLOBALS;
+
+ return $GLOBALS['HTTP_RAW_POST_DATA'];
+ }
+
+ // PAM functions //////////////////////////////////////////////////////////////////////////
+
+ $PAM_HANDLERS = array();
+ $PAM_HANDLER_MSG = array(); // Messages
+
+ /**
+ * Register a method of authenticating an incoming API request.
+ * This function registers a PAM handler which is a function that matches the desciption pam_handler_name()
+ * and returns either 'true' if an incoming api request was authorised, false or throws an exception if not.
+ *
+ * The handlers are tried in turn until one of them successfully authenticates the session.
+ *
+ * This architecture lets an administrator choose what methods to accept for API authentication or
+ *
+ * @param unknown_type $handler
+ */
+ function register_api_pam_handler($handler)
+ {
+ global $PAM_HANDLERS;
+
+ if (is_callable($handler))
+ {
+ $PAM_HANDLERS[$handler] = $handler;
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Magically authenticate an API session using one of the registered methods.
+ *
+ * This function will return true if authentication was possible, otherwise it'll throw an exception.
+ *
+ * If $CONFIG->debug is set then additional debug information will be returned.
+ */
+ function api_pam_authenticate()
+ {
+ global $PAM_HANDLERS, $PAM_HANDLER_MSG;
+ global $CONFIG;
+
+ $dbg_msgs = array();
+
+ foreach ($PAM_HANDLERS as $k => $v)
+ {
+ try {
+ // Execute the handler
+ if ($v())
+ {
+ // Explicitly returned true
+ $PAM_HANDLER_MSG[$k] = "Authenticated!";
+
+ return true;
+ }
+ else
+ $PAM_HANDLER_MSG[$k] = "Not Authenticated.";
+ }
+ catch (Exception $e)
+ {
+ $PAM_HANDLER_MSG[$k] = "$e";
+ }
+ }
+
+ // Got this far, so no methods could be found to authenticate the session
+ throw new SecurityException("No authentication methods were found that could authenticate the session.");
+ }
+
+ /**
+ * See if the user has a valid login sesson.
+ */
+ function pam_auth_session()
+ {
+ return isloggedin();
+ }
+
+ /**
+ * Secure authentication through headers and HMAC.
+ */
+ function pam_auth_hmac()
+ {
+ global $CONFIG;
+
+ $api_header = get_and_validate_api_headers(); // Get api header
+ $api_user = get_api_user($CONFIG->api_header->api_key); // Pull API user details
+
+ if ($api_user)
+ {
+ // Get the secret key
+ $secret_key = $api_user->secret;
+
+ // Validate HMAC - TODO: Maybe find a simpler way to do this that is still secure...?
+ $hmac = calculate_hmac($api_header->hmac_algo,
+ $api_header->time,
+ $api_header->api_key,
+ $secret_key,
+ $api_header->get_variables,
+ $api_header->method == 'POST' ? $api_header->posthash : "");
+
+ if (strcmp(
+ $api_header->hmac,
+ $hmac
+ )==0)
+ {
+ // Now make sure this is not a replay
+ if (!cache_hmac_check_replay($hmac))
+ {
+ // Validate post data
+ if ($api_header->method=="POST")
+ {
+ $postdata = get_post_data();
+ $calculated_posthash = calculate_posthash($postdata, $api_header->posthash_algo);
+
+ if (strcmp($api_header->posthash, $calculated_posthash)!=0)
+ throw new SecurityException("POST data hash is invalid - Expected $calculated_posthash but got {$api_header->posthash}.");
+ }
+
+ // If we've passed all the checks so far then we can be reasonably certain that the request is authentic, so return this fact to the PAM engine.
+ return true;
+ }
+ else
+ throw new SecurityException("Packet signature already seen.");
+ }
+ else
+ throw new SecurityException("HMAC is invalid. {$api_header->hmac} != [calc]$hmac = {$api_header->hmac_algo}(**SECRET KEY**, time:{$api_header->time}, apikey:{$api_header->api_key}, get_vars:{$api_header->get_variables}" . ($api_header->method=="POST"? "posthash:$api_header->posthash}" : ")"));
+ }
+ else
+ throw new SecurityException("Invalid or missing API Key.",ErrorResult::$RESULT_FAIL_APIKEY_INVALID);
+
+ return false;
+ }
// XML functions //////////////////////////////////////////////////////////////////////////