📜 ⬆️ ⬇️

How I made the API work in Yiinitializr Advanced



In continuation of my previous post about such an interesting tool as Yiinitializr , I decided to answer the question about the possibilities of the API, provided by the Advanced template. In the framework of the commentary or an additional paragraph to the last article, the material could not be accommodated, therefore I invite all those who are interested in this topic under cat. In it, we will not deal with the principles of designing the correct API architecture, but let's figure out how to take advantage of the work of the 2amigos guys, who gave us the opportunity to quickly (after reading the article - exactly quickly) deploy the API for our projects on Yii.

The way to implement working with the API in Yiinitializr


API - application programming interface that serves for use in external software products. If we want other developers to use the capabilities of our application in their projects, we cannot do without a well-designed API. Unfortunately, Yii of the first version will not be able to help in this matter. Probably, Yiinitializr will suit you, which will solve some of the issues, but, as we know, the lack of documentation is a serious obstacle.
')
Imagine that the work on our wonderful application is completed, the work of the API is adjusted, and the first developer who wants to take advantage of the capabilities of our system has appeared. What is the principle of its use?

Our system generates, issues and stores in the database the public key (external application identifier), the private key, as well as the user for whom these keys are reserved. Registration is over. The interaction of the user API with our system is based on the principles of REST. The user application sends a request to our system using a specific HTTP method, which includes the HTTP header with the public key, as well as a message in JSON format, which necessarily contains the signature and its expiration date, as well as various additional parameters. After processing the request and making sure that it is correct, the system gives the answer.

Advanced Pattern Differences


If earlier it was a question of the Intermediate pattern, now let's take a look at what has been added to the Advanced pattern. As you already understood - all the additional features that it gives us are associated with the API. Download, unpack and go to the directory ./api to see what we now have. And we have:

./api/extensions/filters/EApiAccessControlFilter.php is a filter class for checking API access rules.
./api/extensions/components/EApiAccessRule.php is a class that represents an API access rule.
./api/extensions/components/EApiActiveRecord.php - class for auxiliary methods of working with AR-models with API.
./api/extensions/components/EApiController.php - controller class for processing requests to API.
./api/extensions/components/EApiError.php is an API error class that exists for the convenience of reading logs.
./api/extensions/components/EApiErrorHandler.php is a class for handling API errors. If we decide to log errors to the database, then we will use this particular class.
./api/models/ApiUser.php is an example of a model with which we will manage external users of our API.
./common/lib/YiiRestTools/ - helper classes for the functioning of the REST API.

This general information will be enough to go directly to the implementation of the interaction of a third-party application with our system through the API.

Configuring and fixing bugs


The developers of Yiinitializr apparently followed the Pareto principle, and having completed 80% of the work, they decided not to spend 80% of the time on documentation and fixing bugs, putting it on the shoulders of sophisticated developers (that is, us) .

Once we decided to do the configuration of Yiinitializr, then of course we are interested in the configuration file. Open it and look at the API routing rules ( ./api/config/api.php ):

 'rules' => array( // REST patterns array('<controller>/index', 'pattern' => 'api/<controller:\w+>', 'verb' => 'POST'), array('<controller>/view', 'pattern' => 'api/<controller:\w+>/view', 'verb' => 'POST'), array('<controller>/update', 'pattern' => 'api/<controller:\w+>/update', 'verb' => 'PUT'), array('<controller>/delete', 'pattern' => 'api/<controller:\w+>/delete', 'verb' => 'DELETE'), array('<controller>/create', 'pattern' => 'api/<controller:\w+>/create', 'verb' => 'POST'), ), 

We see something incomprehensible. The comment tells us that this is a REST template, but in fact we get not quite that. REST assumes that all requests go to a single URL, and actions are selected based on HTTP methods and request parameters, i.e. it should be like this:
AddressHTTP methodAction caused
api.yiinitializr.dev/test/GetTestController \ actionIndex ()
api.yiinitializr.dev/test/1/GetTestController \ actionView (1)
api.yiinitializr.dev/test/POSTTestController \ actionCreate ()
api.yiinitializr.dev/test/1/PUTTestController \ actionUpdate (1)
api.yiinitializr.dev/test/1/DELETETestController \ actionDelete (1)

We bring the rules to the desired form:

 'rules' => array( // REST patterns array('<controller>/index', 'pattern' => '<controller:\w+>', 'verb' => 'GET'), array('<controller>/view', 'pattern' => '<controller:\w+>/<id:\d+>', 'verb' => 'GET'), array('<controller>/update', 'pattern' => '<controller:\w+>/<id:\d+>', 'verb' => 'PUT'), array('<controller>/delete', 'pattern' => '<controller:\w+>/<id:\d+>', 'verb' => 'DELETE'), array('<controller>/create', 'pattern' => '<controller:\w+>', 'verb' => 'POST'), ), 

We will not find anything really important for the current stage in the configuration file, so we move on to other problems. The solution to the first is absolutely simple - portable

 use YiiRestTools\Helpers\RequestData; use Yiinitializr\Helpers\ArrayX; 

from EApiAccessControlFilter.php in EApiAccessRule.php , EApiAccessRule.php these classes are used in the second file.

The next problem is more interesting. Perhaps I did not understand something, so I propose to speculate together. Take a close look at the code below ( ./api/extensions/components/EApiAccessRule.php ):

 public function isRequestAllowed($user, $controller, $action, $ip, $verb) { if ($this->isActionMatched($action) && $this->isUserMatched(Yii::app()->user) && $this->isRoleMatched(Yii::app()->user) && $this->isSignatureMatched($user) && $this->isIpMatched($ip) && $this->isVerbMatched($verb) && $this->isControllerMatched($controller) ) { return $this->allow ? 1 : -1; } else { return 0; } } 

The isRequestAllowed method checks the compliance of the request with the rules. If the chain of checks in the if block is true, then this rule is applied, returning 1 or -1, depending on what this rule does - allows or denies. Otherwise, this rule is not applicable to a specific request and the method returns 0. To make it clearer, I remind you what the rules for filters look like:

 public function filters() { return array( array( 'EApiAccessControlFilter -error', 'rules' => array( array('allow', 'users' => array('@')), ) ) ); } 

Confuses one thing, namely the verification of the signature of $this->isSignatureMatched($user) in this thread. Obtaining an incorrect signature, the system decides that this rule is not applicable and accordingly passes the user (or hacker) inside. Most likely, verification of the signature should be made at the correct request after, and according to the result, let or not let us into the system. Therefore, it is necessary to slightly change this method:

 public function isRequestAllowed($user, $controller, $action, $ip, $verb) { if ($this->isActionMatched($action) && $this->isUserMatched(Yii::app()->user) && $this->isRoleMatched(Yii::app()->user) && $this->isIpMatched($ip) && $this->isVerbMatched($verb) && $this->isControllerMatched($controller) ) { return ($this->allow && $this->isSignatureMatched($user)) ? 1 : -1; } else { return 0; } } 

With flaws like over. The lack of documentation comes into play. Using the debugger step-by-step, I studied the interaction mechanisms of the API classes and I hasten to share my observations with you.

To get started, do the basic settings as described in the Large manual . Then let's define in the configuration the name of the HTTP header in which we will send the public key ( ./api/config/api.php ):

 'params' => array( 'api.key.name' => 'APIKEY', ) 

Do not forget to set the correct time zone (it should be the same for our system and the client application) ( common/config/main.php ):

 'params' => array( ... 'php.timezone' => 'Europe/Moscow', ), 

We start installation through Composer, it is already possible .
Now, to test the API, we need to register an external user. Create a new migration:

 > yiic migrate create create_api_user_table 

And we bring up() and down() methods to the following form:

 public function up() { $this->createTable('{{api_user}}', array( 'id' => 'pk', 'username' => 'varchar(32) NOT NULL', 'api_key' => 'varchar(32) NOT NULL', 'api_secret' => 'varchar(32) NOT NULL', )); $this->insert('{{api_user}}', array( 'username' => 'test_user', 'api_key' => 'e4afe26b5b57083f74b2d01c7066379c', // md5('public_key') 'api_secret' => '156a17333e77a3c504018cae5ada8c3b', // md5('private_key') )); } public function down() { $this->dropTable('{{api_user}}'); } 

We also edit the name of the table containing the users of our API in the ApiUser model.

 class ApiUser extends EApiActiveRecord { ... public function tableName() { return '{{api_user}}'; } ... } 

We apply our migration. The result will be a table in the database with a single hypothetical user of our API. Go ahead.

We write a simple client application


The final step is to write a simple client application for working with the Yiinitializr API. For its work, you must be able to send requests using various HTTP methods, the cURL library will help us with this. Without further ado all the code is decomposed under spoilers. We open, we look, we copy.

SimpleClient Client Class
The generateSignature() method for generating a signature is based on the prepareData($secretKey) method of the prepareData($secretKey) class of the RequestData library.

 /** * Class SimpleClient * * Simple REST-client for Yiinitializr Advanced API. */ class SimpleClient { private $baseUrl; private $apiPublic; private $apiSecret; private $expiration; public function __construct($url, $publicKey, $secretKey, $expiration = '+1 hour') { $this->baseUrl = $url; $this->apiPublic = $publicKey; $this->apiSecret = $secretKey; $this->expiration = $expiration; } public function makeRequest($verb, $controller, $params = array()) { $ch = curl_init(); $signature = $this->generateSignature(); $url = $this->makeUrl($controller); if (!empty($params) && isset($params['id'])) { $url .= $params['id']; } curl_setopt_array($ch, array( CURLOPT_URL => $url, CURLOPT_CUSTOMREQUEST => $verb, CURLOPT_HTTPHEADER => array('APIKEY: ' . $this->apiPublic), CURLOPT_POSTFIELDS => json_encode(array( 'signature' => $signature, 'expiration' => $this->relativeTimeToAbsolute($this->expiration), )), )); $result = curl_exec($ch); curl_close($ch); return $result; } private function generateSignature() { $ttdInt = strtotime($this->expiration); $raw = json_encode(array('expiration' => gmdate('Ymd\TH:i:s\Z', $ttdInt))); $jsonPolicy64 = base64_encode($raw); $signature = base64_encode(hash_hmac( 'sha1', $jsonPolicy64, $this->apiSecret, true )); return $signature; } private function makeUrl($controller) { return 'http://' . rtrim($this->baseUrl, '/') . '/' . $controller . '/'; } private function relativeTimeToAbsolute ($relativeTime) { return date('M d Y, H:i:s', strtotime($relativeTime)); } } 


Working with the SimpleClient class
 $api = new SimpleClient('api.yiinitializr.dev', 'e4afe26b5b57083f74b2d01c7066379c', '156a17333e77a3c504018cae5ada8c3b'); $api->makeRequest('GET', 'test'); $api->makeRequest('GET', 'test', array('id' => 1)); $api->makeRequest('POST', 'test'); $api->makeRequest('PUT', 'test', array('id' => 1)); $api->makeRequest('DELETE', 'test', array('id' => 1)); 


Modified Test Controller Version
 class TestController extends EApiController { public function actionIndex() { // just drop API request :) $this->renderJson(array('response' => 'index')); } public function actionView($id) { $this->renderJson(array('response' => 'view#' . $id)); } public function actionCreate() { $this->renderJson(array('response' => 'created')); } public function actionUpdate($id) { $this->renderJson(array('response' => 'updated#' . $id)); } public function actionDelete($id) { $this->renderJson(array('response' => 'deleted#' . $id)); } } 


Apache setup
In order for Apache to stop blocking PUT and DELETE requests, you need to add the following lines to the ./api/www/.htaccess file:

 <Limit GET POST PUT DELETE> order deny,allow allow from all </Limit> 

The decision is made here .

Summing up


This is the simple way we made our car start. It took me more than one day (and not even two) to understand what was happening. In any case, such a decision would be quite appropriate as a starting point, based on which it is much easier to make a high-quality API, without having deep knowledge in this matter. Of the additional links, I can advise you to look at the Great Guide to Yiinitializr (haven't you done it yet?) And the article How to make a REST API for Yii (in English) .

In the comments I propose to share your thoughts on the implementation of the API on Yii, criticize this method and suggest improvements. Thanks for attention.

Source: https://habr.com/ru/post/208376/


All Articles