From 1810fa9e2beeaf5b5bcb0dde80c610fafd3cfa21 Mon Sep 17 00:00:00 2001 From: icewing Date: Wed, 19 Mar 2008 13:07:16 +0000 Subject: Marcus Povey * API now uses PAM git-svn-id: https://code.elgg.org/elgg/trunk@249 36083f99-b078-4883-b0ff-0f9b5a30f544 --- endpoints/rest.php | 206 +++-------------------------------- engine/lib/api.php | 312 ++++++++++++++++++++++++++++++++++++++++++++++++++++- index.php | 26 +++-- 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)); + + */ + -- cgit v1.2.3