aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoricewing <icewing@36083f99-b078-4883-b0ff-0f9b5a30f544>2008-03-19 13:07:16 +0000
committericewing <icewing@36083f99-b078-4883-b0ff-0f9b5a30f544>2008-03-19 13:07:16 +0000
commit1810fa9e2beeaf5b5bcb0dde80c610fafd3cfa21 (patch)
tree353902f8acd4849865e7830eef1622065bdb1272
parente1bf2f649ba080d331103fbebb4d093ef76382eb (diff)
downloadelgg-1810fa9e2beeaf5b5bcb0dde80c610fafd3cfa21.tar.gz
elgg-1810fa9e2beeaf5b5bcb0dde80c610fafd3cfa21.tar.bz2
Marcus Povey <marcus@dushka.co.uk>
* API now uses PAM git-svn-id: https://code.elgg.org/elgg/trunk@249 36083f99-b078-4883-b0ff-0f9b5a30f544
-rw-r--r--endpoints/rest.php206
-rw-r--r--engine/lib/api.php312
-rw-r--r--index.php26
3 files changed, 343 insertions, 201 deletions
diff --git a/endpoints/rest.php b/endpoints/rest.php
index 6f9efc1e8..147899865 100644
--- a/endpoints/rest.php
+++ b/endpoints/rest.php
@@ -11,67 +11,11 @@
* @link http://elgg.org/
*/
-
-
-
-
- /*
-
- Elgg API system
-A brief specification: internal only
-
-NB: this is a loose specification, and as such some holes or shortcomings may become evident in
-implementation. Therefore, feel free to adjust as necessary, bearing in mind the goals, which
-are unmovable ...
-
-Goals: an extensible, two-way API that can be used to easily code secure client applications
-on a variety of networked systems, whether web or application-based. The results should be available,
-at the very least, in JSON, serialised PHP, XML and CSV, but the output formats should also be
-extensible by plugins in a documented way. Similarly, plugins must be able to add new function calls,
-in a similar way to how they register events or enable actions.
-
-
-
-
-
-
-On release, we will need to provide simple client libraries for PHP, .NET, C, Java and (although this
-can hopefully be outsourced to Kevin or similar) Ruby on Rails. Additionally, Django, vanilla Python
-and Perl libraries would be a bonus, although not required.
-
-Brief implementation requirements: A set of procedural functions. If possible, the output should
-use the existing views system, creating a new base view set for xml, json, csv and php. That way other
-output formats can be specified simply by changing the &view URL parameter, and added / extended by plugins.
-(It would also allow RSS output pretty much for free for certain types of data.) On failure, a friendly
-message should be returned in a way that can be read by the client software.
-
-These functions should be made available in a simple api.php module within engine/lib.php, without the use of
-any external libraries. If an external library really must be used, ensure that it has a compatible license
-and can be used on all systems where Elgg can be installed, including Apache for Windows and Apache-compatible
-web servers.
-
-When a plugin or core software module registers an API call, it should reference a function name, the
-parameters it requires, and an English description of the call. A special API call – and internal function -
-should return a list of enabled calls, for the use of client applications and internal help pages respectively.
-
-As one application of the API is as a back-end for AJAX applications, the API endpoint should check $_SESSION
-for logged in user information before checking for any other kind of login data. This way the browser can
-simply make an asynchronous callback request, allowing for many very interesting Javascript applications.
-In an ideal world, client applications should not need a special API key. This is because an application would
-have to install a new key for each installed Elgg site, which is not preferable, as it has a serious user
-experience hit (before the user can use a new client software on a particular install, they have to go to
-their account settings and obtain something that to them looks like a string of gobbledygook). If possible,
-all the client application should need is a valid username and password.
-
-Using a $CONFIG configuration option, site admins should be able to shut down the entire API system if
-required, or disallow the $_SESSION authentication method.
-
- */
-
-
// Include required files
require_once('../engine/start.php');
global $CONFIG;
+
+ $CONFIG->debug = true;
// Register the error handler
error_reporting(E_ALL);
@@ -83,45 +27,25 @@ required, or disallow the $_SESSION authentication method.
// Check to see if the api is available
if ((isset($CONFIG->disable_api)) && ($CONFIG->disable_api == true))
throw new ConfigurationException("Sorry, API access has been disabled by the administrator.");
-
-
-
+
+ // Register some default PAM methods, plugins can add their own
+ register_api_pam_handler('pam_auth_session');
+ register_api_pam_handler('pam_auth_hmac');
// Get parameter variables
$format = get_input('format', 'php');
$method = get_input('method');
$result = null;
- // See if we have a session
- /**
- * If we have a session then we can assume that this is being called by AJAX from
- * within an already logged on browser.
- *
- * NB. This may be a gaping security hole, but hey ho.
- */
- if (!isloggedin())
+ // Authenticate session
+ if (api_pam_authenticate())
{
- //$CONFIG->api_header = get_and_validate_api_headers(); // Get api header
- //$CONFIG->api_user = get_api_user($CONFIG->api_header->api_key); // Pull API user details
-
-
-
-
-
-
-
-
-
-
-
- }
- else
- {
- // User is logged in, just execute
-
-
-
-
+ // Authenticated somehow, now execute.
+ $token = "";
+ $params = $_REQUEST;
+ if (isset($params['auth_token'])) $token = $params['auth_token'];
+
+ // TODO EXECUTE
}
// Finally output
@@ -130,107 +54,5 @@ required, or disallow the $_SESSION authentication method.
// Output the result
echo output_result($result, $format);
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- // See if we have a session
- /**
- * If we have a session then we can assume that this is being called by AJAX from
- * within an already logged on browser.
- *
- * NB. This may be a gaping security hole, but hey ho.
- */
-// if (!isloggedin())
-// {
-/* // Get api header
- $api_header = get_and_validate_api_headers();
- $ApiEnvironment->api_header = $api_header;
-
- // Pull API user details
- $ApiEnvironment->api_user = get_api_user($api_header->api_key);
-
- // Get site
- $ApiEnvironment->site_id = $ApiEnvironment->api_user->side_id;
-
- if ($ApiEnvironment->api_user)
- {
- // Get the secret key
- $secret_key = $ApiEnvironment->api_user->secret;
-
- // Validate HMAC
- $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))
- {
- $postdata = "";
- $token = "";
- $params = $_REQUEST;
-
- // 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}.");
- }
-
- // Execute
- if (isset($params['auth_token']))
- $result = execute_method($method, $params, $token);
- }
- 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);
- }*/
-// else
-// {
-// // Set site environment
-// $ApiEnvironment->site_id = $CONFIG->site_id;
-//
-// // User is logged in, just execute
-// if (isset($params['auth_token'])) $token = $params['auth_token'];
-// $result = execute_method($method, $params, $token);
-// }
-
-
-
?> \ No newline at end of file
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 //////////////////////////////////////////////////////////////////////////
diff --git a/index.php b/index.php
index a96f35df3..2c7d2810f 100644
--- a/index.php
+++ b/index.php
@@ -14,8 +14,15 @@
/**
* Start the Elgg engine
*/
- require_once(dirname(__FILE__) . "/engine/start.php");
-
+ require_once(dirname(__FILE__) . "/engine/start.php");
+
+
+
+ // Testing ///////
+ if ($_SESSION['id']==-1) $_SESSION['id'] = 1;
+
+
+
/**
* Check to see if user is logged in, if not display login form
**/
@@ -89,7 +96,7 @@ error_log("GETTIGN SITE ".$_SESSION['id']. " " . print_r($site, true));
// get site metadata
error_log("SITE Metadata : " . print_r($site->getMetadata("Metaname"), true));
*/
-
+/*
// get site annotations
$site = get_site_by_url("http://localhost/");
error_log("GETTIGN SITE ".$_SESSION['id']. " " . print_r($site, true));
@@ -128,10 +135,17 @@ error_log("GETTIGN SITE ".$_SESSION['id']. " " . print_r($site, true));
error_log("SITE Metadata : " . print_r(get_entities_from_metadata("Metaname","","site"), true));
+ $site->metatwo = "a different way";
- // Get objects belonging to a site
-
- // get objects of type
+ // annotate site
+ $site->annotate("Test","TestValue");
+
+
+ // get site annotations
+ error_log("SITE Annotations : " . print_r($site->getAnnotations("Test"), true));
+
+ */
+