From 18e6796726d73d7dc90ecdd16c181493941f5487 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Thu, 15 Dec 2016 10:13:00 +0100 Subject: 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. --- application/api/ApiMiddleware.php | 132 +++++++++++++++++++++ application/api/ApiUtils.php | 51 ++++++++ application/api/controllers/ApiController.php | 54 +++++++++ application/api/controllers/Info.php | 42 +++++++ .../api/exceptions/ApiAuthorizationException.php | 34 ++++++ .../api/exceptions/ApiBadParametersException.php | 19 +++ application/api/exceptions/ApiException.php | 77 ++++++++++++ .../api/exceptions/ApiInternalException.php | 19 +++ 8 files changed, 428 insertions(+) create mode 100644 application/api/ApiMiddleware.php create mode 100644 application/api/ApiUtils.php create mode 100644 application/api/controllers/ApiController.php create mode 100644 application/api/controllers/Info.php create mode 100644 application/api/exceptions/ApiAuthorizationException.php create mode 100644 application/api/exceptions/ApiBadParametersException.php create mode 100644 application/api/exceptions/ApiException.php create mode 100644 application/api/exceptions/ApiInternalException.php (limited to 'application/api') diff --git a/application/api/ApiMiddleware.php b/application/api/ApiMiddleware.php new file mode 100644 index 00000000..162e88e0 --- /dev/null +++ b/application/api/ApiMiddleware.php @@ -0,0 +1,132 @@ +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; + } +} diff --git a/application/api/ApiUtils.php b/application/api/ApiUtils.php new file mode 100644 index 00000000..fbb1e72f --- /dev/null +++ b/application/api/ApiUtils.php @@ -0,0 +1,51 @@ +iat) + || $payload->iat > time() + || time() - $payload->iat > ApiMiddleware::$TOKEN_DURATION + ) { + throw new ApiAuthorizationException('Invalid JWT issued time'); + } + } +} diff --git a/application/api/controllers/ApiController.php b/application/api/controllers/ApiController.php new file mode 100644 index 00000000..1dd47f17 --- /dev/null +++ b/application/api/controllers/ApiController.php @@ -0,0 +1,54 @@ +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; + } + } +} diff --git a/application/api/controllers/Info.php b/application/api/controllers/Info.php new file mode 100644 index 00000000..25433f72 --- /dev/null +++ b/application/api/controllers/Info.php @@ -0,0 +1,42 @@ + 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); + } +} diff --git a/application/api/exceptions/ApiAuthorizationException.php b/application/api/exceptions/ApiAuthorizationException.php new file mode 100644 index 00000000..0e3f4776 --- /dev/null +++ b/application/api/exceptions/ApiAuthorizationException.php @@ -0,0 +1,34 @@ +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; + } +} diff --git a/application/api/exceptions/ApiBadParametersException.php b/application/api/exceptions/ApiBadParametersException.php new file mode 100644 index 00000000..e5cc19ea --- /dev/null +++ b/application/api/exceptions/ApiBadParametersException.php @@ -0,0 +1,19 @@ +buildApiResponse(400); + } +} diff --git a/application/api/exceptions/ApiException.php b/application/api/exceptions/ApiException.php new file mode 100644 index 00000000..c8490e0c --- /dev/null +++ b/application/api/exceptions/ApiException.php @@ -0,0 +1,77 @@ +debug !== true) { + return $this->getMessage(); + } + return [ + 'message' => $this->getMessage(), + 'stacktrace' => get_class($this) .': '. $this->getTraceAsString() + ]; + } + + /** + * Build the Response object to return. + * + * @param int $code HTTP status. + * + * @return Response with status + body. + */ + protected function buildApiResponse($code) + { + $style = $this->debug ? JSON_PRETTY_PRINT : null; + return $this->response->withJson($this->getApiResponseBody(), $code, $style); + } + + /** + * @param Response $response + */ + public function setResponse($response) + { + $this->response = $response; + } + + /** + * @param bool $debug + */ + public function setDebug($debug) + { + $this->debug = $debug; + } +} diff --git a/application/api/exceptions/ApiInternalException.php b/application/api/exceptions/ApiInternalException.php new file mode 100644 index 00000000..1cb05532 --- /dev/null +++ b/application/api/exceptions/ApiInternalException.php @@ -0,0 +1,19 @@ +buildApiResponse(500); + } +} -- cgit v1.2.3