Permalink
Browse files

REST API structure using Slim framework

  * REST API routes are handle by Slim.
  * Every API controller go through ApiMiddleware which handles security.
  * First service implemented `/info`, for tests purpose.
  • Loading branch information...
ArthurHoaro committed Dec 15, 2016
1 parent 423ab02 commit 18e6796726d73d7dc90ecdd16c181493941f5487
View
@@ -0,0 +1,4 @@
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ index.php [QSA,L]
View
@@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
### Added
- REST API: see [Shaarli API documentation](http://shaarli.github.io/api-documentation/)
### Changed
### Fixed
@@ -0,0 +1,132 @@
<?php
namespace Shaarli\Api;
use Shaarli\Api\Exceptions\ApiException;
use Shaarli\Api\Exceptions\ApiAuthorizationException;
use Slim\Container;
use Slim\Http\Request;
use Slim\Http\Response;
/**
* Class ApiMiddleware
*
* This will be called before accessing any API Controller.
* Its role is to make sure that the API is enabled, configured, and to validate the JWT token.
*
* If the request is validated, the controller is called, otherwise a JSON error response is returned.
*
* @package Api
*/
class ApiMiddleware
{
/**
* @var int JWT token validity in seconds (9 min).
*/
public static $TOKEN_DURATION = 540;
/**
* @var Container: contains conf, plugins, etc.
*/
protected $container;
/**
* @var \ConfigManager instance.
*/
protected $conf;
/**
* ApiMiddleware constructor.
*
* @param Container $container instance.
*/
public function __construct($container)
{
$this->container = $container;
$this->conf = $this->container->get('conf');
$this->setLinkDb($this->conf);
}
/**
* Middleware execution:
* - check the API request
* - execute the controller
* - return the response
*
* @param Request $request Slim request
* @param Response $response Slim response
* @param callable $next Next action
*
* @return Response response.
*/
public function __invoke($request, $response, $next)
{
try {
$this->checkRequest($request);
$response = $next($request, $response);
} catch(ApiException $e) {
$e->setResponse($response);
$e->setDebug($this->conf->get('dev.debug', false));
$response = $e->getApiResponse();
}
return $response;
}
/**
* Check the request validity (HTTP method, request value, etc.),
* that the API is enabled, and the JWT token validity.
*
* @param Request $request Slim request
*
* @throws ApiAuthorizationException The API is disabled or the token is invalid.
*/
protected function checkRequest($request)
{
if (! $this->conf->get('api.enabled', true)) {
throw new ApiAuthorizationException('API is disabled');
}
$this->checkToken($request);
}
/**
* Check that the JWT token is set and valid.
* The API secret setting must be set.
*
* @param Request $request Slim request
*
* @throws ApiAuthorizationException The token couldn't be validated.
*/
protected function checkToken($request) {
$jwt = $request->getHeaderLine('jwt');
if (empty($jwt)) {
throw new ApiAuthorizationException('JWT token not provided');
}
if (empty($this->conf->get('api.secret'))) {
throw new ApiAuthorizationException('Token secret must be set in Shaarli\'s administration');
}
ApiUtils::validateJwtToken($jwt, $this->conf->get('api.secret'));
}
/**
* Instantiate a new LinkDB including private links,
* and load in the Slim container.
*
* FIXME! LinkDB could use a refactoring to avoid this trick.
*
* @param \ConfigManager $conf instance.
*/
protected function setLinkDb($conf)
{
$linkDb = new \LinkDB(
$conf->get('resource.datastore'),
true,
$conf->get('privacy.hide_public_links'),
$conf->get('redirector.url'),
$conf->get('redirector.encode_url')
);
$this->container['db'] = $linkDb;
}
}
@@ -0,0 +1,51 @@
<?php
namespace Shaarli\Api;
use Shaarli\Api\Exceptions\ApiAuthorizationException;
/**
* Class ApiUtils
*
* Utility functions for the API.
*/
class ApiUtils
{
/**
* Validates a JWT token authenticity.
*
* @param string $token JWT token extracted from the headers.
* @param string $secret API secret set in the settings.
*
* @throws ApiAuthorizationException the token is not valid.
*/
public static function validateJwtToken($token, $secret)
{
$parts = explode('.', $token);
if (count($parts) != 3 || strlen($parts[0]) == 0 || strlen($parts[1]) == 0) {
throw new ApiAuthorizationException('Malformed JWT token');
}
$genSign = hash_hmac('sha512', $parts[0] .'.'. $parts[1], $secret);
if ($parts[2] != $genSign) {
throw new ApiAuthorizationException('Invalid JWT signature');
}
$header = json_decode(base64_decode($parts[0]));
if ($header === null) {
throw new ApiAuthorizationException('Invalid JWT header');
}
$payload = json_decode(base64_decode($parts[1]));
if ($payload === null) {
throw new ApiAuthorizationException('Invalid JWT payload');
}
if (empty($payload->iat)
|| $payload->iat > time()
|| time() - $payload->iat > ApiMiddleware::$TOKEN_DURATION
) {
throw new ApiAuthorizationException('Invalid JWT issued time');
}
}
}
@@ -0,0 +1,54 @@
<?php
namespace Shaarli\Api\Controllers;
use \Slim\Container;
/**
* Abstract Class ApiController
*
* Defines REST API Controller dependencies injected from the container.
*
* @package Api\Controllers
*/
abstract class ApiController
{
/**
* @var Container
*/
protected $ci;
/**
* @var \ConfigManager
*/
protected $conf;
/**
* @var \LinkDB
*/
protected $linkDb;
/**
* @var int|null JSON style option.
*/
protected $jsonStyle;
/**
* ApiController constructor.
*
* Note: enabling debug mode displays JSON with readable formatting.
*
* @param Container $ci Slim container.
*/
public function __construct(Container $ci)
{
$this->ci = $ci;
$this->conf = $ci->get('conf');
$this->linkDb = $ci->get('db');
if ($this->conf->get('dev.debug', false)) {
$this->jsonStyle = JSON_PRETTY_PRINT;
} else {
$this->jsonStyle = null;
}
}
}
@@ -0,0 +1,42 @@
<?php
namespace Shaarli\Api\Controllers;
use Slim\Http\Request;
use Slim\Http\Response;
/**
* Class Info
*
* REST API Controller: /info
*
* @package Api\Controllers
* @see http://shaarli.github.io/api-documentation/#links-instance-information-get
*/
class Info extends ApiController
{
/**
* Service providing various information about Shaarli instance.
*
* @param Request $request Slim request.
* @param Response $response Slim response.
*
* @return Response response.
*/
public function getInfo($request, $response)
{
$info = [
'global_counter' => count($this->linkDb),
'private_counter' => count_private($this->linkDb),
'settings' => array(
'title' => $this->conf->get('general.title', 'Shaarli'),
'header_link' => $this->conf->get('general.header_link', '?'),
'timezone' => $this->conf->get('general.timezone', 'UTC'),
'enabled_plugins' => $this->conf->get('general.enabled_plugins', []),
'default_private_links' => $this->conf->get('privacy.default_private_links', false),
),
];
return $response->withJson($info, 200, $this->jsonStyle);
}
}
@@ -0,0 +1,34 @@
<?php
namespace Shaarli\Api\Exceptions;
/**
* Class ApiAuthorizationException
*
* Request not authorized, return a 401 HTTP code.
*/
class ApiAuthorizationException extends ApiException
{
/**
* {@inheritdoc}
*/
public function getApiResponse()
{
$this->setMessage('Not authorized');
return $this->buildApiResponse(401);
}
/**
* Set the exception message.
*
* We only return a generic error message in production mode to avoid giving
* to much security information.
*
* @param $message string the exception message.
*/
public function setMessage($message)
{
$original = $this->debug === true ? ': '. $this->getMessage() : '';
$this->message = $message . $original;
}
}
@@ -0,0 +1,19 @@
<?php
namespace Shaarli\Api\Exceptions;
/**
* Class ApiBadParametersException
*
* Invalid request exception, return a 400 HTTP code.
*/
class ApiBadParametersException extends ApiException
{
/**
* {@inheritdoc}
*/
public function getApiResponse()
{
return $this->buildApiResponse(400);
}
}
Oops, something went wrong.

0 comments on commit 18e6796

Please sign in to comment.