Example REST API Structure


Supporting REST calls is pretty easy; handling the data has a few sequential steps:

  1. Ensure that the user is authenticated. If you are using HTTP Basic Authentication or SSL with client-side certificates, this is already handled. If not, take care of it first.

  2. If you are enforcing any sort of limit on the number of requests handled per day, record that the call was made and check to see if the user was throttled.

  3. Ensure that the incoming request is valid, in that it contains all the necessary parameters and does not include any unknown parameters. Accepting useless parameters may sound innocuous, but it will only confuse developers who expected that parameter to be applied.

  4. Hand the call off to the supporting function, and allow it to return an error or a good response, whichever is appropriate.

User Authentication

Assuming message-based authentication, the calling function would look a little like this.

 if (!checkUser(mysql_escape_string($_GET['username']),     mysql_escape_string($_GET['password']))) {  echo <<< endquote <response>   <error no="001">Invalid Username or Password</error> </response> endquote;  exit; } 

The checkUser() function called is identical to the one presented earlier; it won't be repeated here. Note that both the username and password are escaped before being passed off; remember that SQL Injection attacks can occur with any user-presented data, so your API is just as vulnerable as any other interface your site presents. Should the user present invalid credentials, an error is returned and the script ends.

Query Limits

Enforcing a limit on the number of queries a particular user can run per day is a big reason to authenticate users in the first place.

 $queriesPerDay = 2000; ... $username = mysql_real_escape_string($_GET['username']); $query = "UPDATE `12_user_throttle` SET queries = queries + 1 WHERE username =   ‘$username' AND queries < '$queriesPerDay'"; 

First, at the beginning of the code block, the maximum number of queries a user may run per day is set (for easy access later). Then an update query is generated using the user's credentials and the maximum number of queries a user may run per day. This query will affect one row if the user is still under the max queries per day limit, and zero rows if not, which saves you running two queries: one to increment, and the other to check the present value.

 if (replaceQuery($query) == 0) { echo <<< endquote <response>   <error no="2">Query limit reached, please try again tomorrow</error> </response> endquote;  exit; } 

Second, the replaceQuery() function is called (shown in Appendix A), returning as indicated previously. Should users have already exhausted their max queries per day, an error is returned.

Note 

It may be tempting to track other variables, such as the remote user's IP address. This is fine for reporting purposes, but should not be part of your max query enforcement. Multiple developers on a corporate intranet behind proxies may all appear to be a single IP address; the same goes for home users behind similar devices. Should a user's repeated queries become a problem, then and only then do you want to look into blocking their requests on an IP level, but be sure to make appropriate contact information available to innocent users who may have been affected by this action.

In order for this function to work in its desired fashion, you will need to reset the number of queries each user has remaining each night. A cron job or Windows scheduled task can be easily configured to run a small script each night at midnight. Remember to store this script outside the document root. There is no need for it to be accessible to the Internet.

 <?php  require("../common_db.php");  $query = "UPDATE `12_user_throttle` SET `queries` = 0";  $resetUsers = insertQuery($query);  echo "$resetUsers users have had their queries reset"; ?> 

With a cron job, the output should be present in your daily reports; under Windows you will need to save the data manually. Using file_put_contents() makes this trivial.

Note 

To give managing your query limits a bit more granularity, add an additional column to your database to hold the max queries per day for that user, have the reset script set the number of queries remaining to that number each night, and switch the checking query to compare the two columns.

If performance is a key concern, you can easily merge the valid user check and the sufficient queries remaining checks, cutting the number of database hits per call in half. Should the new query return zero rows, you should perform an additional query to determine if the failure was because of invalid credentials or exhausted queries per day so that a meaningful error can be returned.

Request Validity

Checking the request is the first step that actually cares about the API behind the function; as such, to determine if the request is valid, the API itself must be considered. In this format, the API is defined in an array:

 $API = array(); $expectedValues = array("author", "title"); $optionalValues = array(); $API[] = array("lookup", "lookupCall", $expectedValues, $optionalValues); $expectedValues = array("keyword"); $optionalValues = array("year", "publisher"); $API[] = array("search", "searchCall", $expectedValues, $optionalValues); 

This API supports two methods: lookup and search. lookup has two required parameters, name and title, and no optional parameters. search has a single required parameter, keyword, and two optional parameters, year and publisher. The lookupCall and searchCall values represent the name of the function that will be called if the request is valid.

A function will be used to determine if the request is valid (how it is called is presented next). It will ensure that all of the required parameters are present, and that the request only contains the allowed parameters (the required and optional parameters), nothing else.

 function checkValues($request, $required, $optional, &$error) {   $required[] = "method";   $required[] = "username";   $required[] = "password"; 

The function is passed four parameters: the request sent by the user, the required and optional parameters for this type of request, and finally an array passed by reference to contain any errors detected while examining the request. Three elements are added to the required array: method, username, and password; these are present in the request (or an error would have been returned already). They need to be added to avoid raising an error due to unnecessary parameters.

 $requestTemp = array(); $requestTemp = array_diff(array_keys($request), $optional); $requestTemp = array_diff($requestTemp, $required); 

Here the request is checked to ensure that it only contains the required and optional parameters. The array_keys() function returns only the keys from the associative array; this is needed to ensure that the comparison occurs between the keys in the request, not the values.

 if (count($requestTemp) > 0) {   foreach ($requestTemp as $unknownElement => $unknownValue)   {     $error[] = "<error no=\"101\">Unknown Element: $unknownElement</error>";   } } 

If anything is left over in the $requestTemp array, it indicates that there were more parameters passed to the API than allowed. Execution isn't terminated immediately, in an effort to present the developer with as much information about problems with the request as possible.

 $requiredTemp = array(); $requiredTemp = array_diff($required, array_keys($request));  if (count($requiredTemp) > 0)  {    foreach ($requiredTemp as $missingElement)    {      $error[] = "<error no=\"102\">Missing required element:        $missingElement</error>";    } } 

Identical processing is done to ensure that all required elements are present.

 if (count($error) == 0) {   return true;   }else   {     return false;   } } 

Assuming all went well, return true; if not, return false. Remember that the $error array was passed by reference; any errors added will be available to the calling function.

This next segment of code will take care of calling the function just introduced:

 $error = array(); $matchedMethod = false; $validRequestFormat = false; foreach($API as $item) {   if ($item[0] == $_GET[‘method'])   {      $matchedMethod = true;      $validRequestFormat = checkValues($_GET, $item[2], $item[3], &$error);      break;   } } 

Here the API array is iterated through to try and match the method the client is requesting to a method offered by the API. If a match is found, it is passed off to the checkValues() function to determine if the other passed parameters are appropriate.

 if ($matchedMethod == false) {           echo <<< endquote <response>   <error no="100">Unknown or missing method</error> </response> endquote;           exit; }else if ($validRequestFormat == false) {  echo "<response>\n" . implode("\n",$error) . "\n</response>";  exit; } 

If matchedMethod remains false, the method the user requested doesn't exist and an appropriate error is raised. Alternatively, if validRequestFormat was set to false by the checkValues() function, the parameters passed are invalid (either the required parameters are missing, or there are parameters present that the API does not know how to handle). In this case, the error array that was populated by the checkValues() function is imploded and returned.

Finally, if all the previous checks were passed, the appropriate function can be called:

 call_user_func($item[1], $_GET); 

Framework Limitations and Notes

This framework has a few built-in limitations that should be kept in mind:

  • This framework assumes that the entire API is accessible via a single endpoint, which (while becoming popular) isn't what the spec dictates. If you want to allow for multiple endpoints while still using a single script, save the script without an extension (for example, as api rather than api.php) and store it on your web server, then configure your web server to present it as shown in the following code. You will then need to explode the $_SERVER[‘REQUEST_URI'] value to determine the desired method.

     <Files api>   ForceType application/x-httpd-php </Files> 

  • The framework doesn't look at the data within the request. As long as the appropriate parameters are there, the data is passed off to the appropriate function. Checking is not done to ensure that a value is numeric, for example.

  • The data isn't filtered or escaped; it is passed off as-is. This may allow unscrupulous developers to compromise your data in unintended ways.

Using the Framework

The framework only provides the mechanism to receive and pass on requests; you will need to plug in your own authentication function, as well as the functions necessary to return useful data to the end user. Because APIs are generally used to replicate data available through other interfaces, you will likely be implementing a thin interface layer, so both your website and the API can use the same end function to retrieve the data. I would suggest either using the same code to complete the filtering, or performing all the filtering in a single place; too often, data not specifically coming from a website isn't filtered at all.

Here's the code of restAPI.php in full:

 <?php require("../common_db.php"); //Plug in authentication function here, remember to escape strings if the // destination function doesn't do it for you. if (!checkUser(mysql_escape_string($_GET[‘username']), mysql_escape_string($_GET['password']))) {  echo <<< endquote <response>   <error no="1">Invalid Username or Password</error> </response> endquote;  exit; } //Plug in throttling function here if desired if (userThrottled($_GET[‘username'])) { echo <<< endquote <response>   <error no="2">Query limit reached, please try again tommorrow</error> </response> endquote;  exit; } //Set up your own array functions here $API = array(); /* Example: $expectedValues = array("name", "title"); $optionalValues = array("year", "publisher"); $API[] = array("lookup", "lookupCall", $expectedValues, $optionalValues); $expectedValues = array("keyword"); $optionalValues = array(); $API[] = array("search", "searchCall", $expectedValues, $optionalValues); describeAPI($API); */ //Framework iterates through array looking to match the requested method // with a service the framework provides $error = array(); $matchedMethod = false; $validRequestFormat = false; foreach($API as $item) {   if ($item[0] == $_GET['method'])   {      $matchedMethod = true;      $validRequestFormat = checkValues($_GET, $item[2], $item[3], &$error);      break;   } } //Framework was unable to match method, return an error if ($matchedMethod == false) {          echo <<< endquote <response>   <error no="100">Unknown or missing method</error> </response> endquote;          exit; }else if ($validRequestFormat == false) {  echo "<response>\n" . implode("\n",$error) . "</response>";  exit; } //Method was matched, and contained required parameters, call the appropriate // function call_user_func($item[1], $_GET); function checkValues($request, $required, $optional, &$error) {   $required[] = "method";   $required[] = "username";   $required[] = "password";  // Ensure all elements passed are either required or optional  $requestTemp = array();  $requestTemp = array_diff(array_keys($request), $optional);  $requestTemp = array_diff($requestTemp, $required);   if (count($requestTemp) > 0)   {     print_r($requestTemp);     foreach ($requestTemp as $unknownElement => $unknownValue)     {       $error[] = "<error no=\"101\">Unknown Element: $unknownElement</error>";     }   }  // Ensure all required elements are present  $requiredTemp = array();  $requiredTemp = array_diff($required, array_keys($request));   if (count($requiredTemp) > 0)   {     foreach ($requiredTemp as $missingElement)     {       $error[] = "<error no=\"102\">Missing required element:         $missingElement</error>";     }   }   if (count($error) == 0)   {     return true;   }else   {     return false;   }  }  // A quick function for testing, or as a first step to API documentation  function describeAPI($API)  {    foreach($API as $service)    {      echo "<b>Method Name:</b> {$service[0]} <br>";      echo "<b>Required Parameters:</b> ". implode(",", $service[2]) . "<br>";      echo "<b>Optional Parameters:</b> ". implode(",", $service[3]) . "<br><br>";    }    exit;  } 

Rest Conclusion

REST is more popular than SOAP, due in no small part to its simplicity, for both the server and the client. This section introduced a few key steps to keep in mind when developing a REST feed, and presented some sample code to help quickly flesh out any new REST server.




Professional Web APIs with PHP. eBay, Google, PayPal, Amazon, FedEx, Plus Web Feeds
Professional Web APIs with PHP. eBay, Google, PayPal, Amazon, FedEx, Plus Web Feeds
ISBN: 764589547
EAN: N/A
Year: 2006
Pages: 130

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net