Skip to content
Permalink
Browse files

Use ApiException to handle API errors and use ApiMiddleware to handle…

… API authentication
  • Loading branch information...
cydrobolt committed Mar 17, 2017
1 parent 8f6d976 commit 65794f83d72161bf6330ba04107035692d9ad261
@@ -0,0 +1,42 @@
<?php
namespace App\Exceptions\Api;
class ApiException extends \Exception {
/**
* Catch an API exception.
*
* @param string $text_code
* @param string $message
* @param integer $status_code
* @param string $response_type
* @param \Exception $previous
*
* @return mixed
*/
public function __construct($text_code='SERVER_ERROR', $message, $status_code = 0, $response_type='plain_text', Exception $previous = null) {
// TODO special Polr error codes for JSON
$this->response_type = $response_type;
$this->text_code = $text_code;
parent::__construct($message, $status_code, $previous);
}
private function encodeJsonResponse($status_code, $message, $text_code) {
$response = [
'status_code' => $status_code,
'error_code' => $text_code,
'error' => $message
];
return json_encode($response);
}
public function getEncodedErrorMessage() {
if ($this->response_type == 'json') {
return $this->encodeJsonResponse($this->code, $this->message, $this->text_code);
}
else {
return $this->code . ' ' . $this->message;
}
}
}
@@ -8,6 +8,8 @@
use Laravel\Lumen\Exceptions\Handler as ExceptionHandler;
use Illuminate\Support\Facades\Response;
use App\Exceptions\Api\ApiException;
class Handler extends ExceptionHandler {
/**
* A list of the exception types that should not be reported.
@@ -43,6 +45,7 @@ public function render($request, Exception $e)
if (env('APP_DEBUG') != true) {
// Render nice error pages if debug is off
if ($e instanceof NotFoundHttpException) {
// Handle 404 exceptions
if (env('SETTING_REDIRECT_404')) {
// Redirect 404s to SETTING_INDEX_REDIRECT
return redirect()->to(env('SETTING_INDEX_REDIRECT'));
@@ -51,6 +54,7 @@ public function render($request, Exception $e)
return view('errors.404');
}
if ($e instanceof HttpException) {
// Handle HTTP exceptions thrown by public-facing controllers
$status_code = $e->getStatusCode();
$status_message = $e->getMessage();
@@ -63,6 +67,20 @@ public function render($request, Exception $e)
return response(view('errors.generic', ['status_code' => $status_code, 'status_message' => $status_message]), $status_code);
}
}
if ($e instanceof ApiException) {
// Handle HTTP exceptions thrown by API controllers
$status_code = $e->getCode();
$encoded_status_message = $e->getEncodedErrorMessage();
if ($e->response_type == 'json') {
return response($encoded_status_message, $status_code)
->header('Content-Type', 'application/json')
->header('Access-Control-Allow-Origin', '*');
}
return response($encoded_status_message, $status_code)
->header('Content-Type', 'text/plain')
->header('Access-Control-Allow-Origin', '*');
}
}
return parent::render($request, $e);
@@ -5,13 +5,14 @@
use App\Helpers\LinkHelper;
use App\Helpers\UserHelper;
use App\Helpers\StatsHelper;
use App\Exceptions\Api\ApiException;
class ApiAnalyticsController extends ApiController {
public function lookupLinkStats (Request $request, $stats_type=false) {
$response_type = $request->input('response_type') ?: 'json';
if ($response_type != 'json') {
abort(401, 'Only JSON-encoded data is available for this endpoint.');
throw new ApiException('JSON_ONLY', 'Only JSON-encoded data is available for this endpoint.', 401, $response_type);
}
$user = self::getApiUserInfo($request);
@@ -24,7 +25,7 @@ public function lookupLinkStats (Request $request, $stats_type=false) {
]);
if ($validator->fails()) {
return abort(400, 'Invalid or missing parameters.');
throw new ApiException('MISSING_PARAMETERS', 'Invalid or missing parameters.', 400, $response_type);
}
$url_ending = $request->input('url_ending');
@@ -37,13 +38,13 @@ public function lookupLinkStats (Request $request, $stats_type=false) {
$link = LinkHelper::linkExists($url_ending);
if ($link === false) {
abort(404, 'Link not found.');
throw new ApiException('NOT_FOUND', 'Link not found.', 404, $response_type);
}
if (($link->creator != $user->username) &&
!(UserHelper::userIsAdmin($user->username))){
// If user does not own link and is not an admin
abort(401, 'Unauthorized.');
throw new ApiException('ACCESS_DENIED', 'Unauthorized.', 401, $response_type);
}
$stats = new StatsHelper($link->id, $left_bound, $right_bound);
@@ -58,7 +59,7 @@ public function lookupLinkStats (Request $request, $stats_type=false) {
$fetched_stats = $stats->getRefererStats();
}
else {
abort(400, 'Invalid analytics type requested.');
throw new ApiException('INVALID_ANALYTICS_TYPE', 'Invalid analytics type requested.', 400, $response_type);
}
return self::encodeResponse([
@@ -2,48 +2,8 @@
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\User;
use App\Helpers\ApiHelper;
class ApiController extends Controller {
protected static function getApiUserInfo(Request $request) {
$api_key = $request->input('key');
if (!$api_key) {
// no API key provided -- check whether anonymous API is on
if (env('SETTING_ANON_API')) {
$username = 'ANONIP-' . $request->ip();
}
else {
abort(401, "Authentication token required.");
}
$user = (object) [
'username' => $username
];
}
else {
$user = User::where('active', 1)
->where('api_key', $api_key)
->where('api_active', 1)
->first();
if (!$user) {
abort(401, "Invalid authentication token.");
}
$username = $user->username;
}
$api_limit_reached = ApiHelper::checkUserApiQuota($username);
if ($api_limit_reached) {
abort(403, "Quota exceeded.");
}
return $user;
}
protected static function encodeResponse($result, $action, $response_type='json', $plain_text_response=false) {
$response = [
"action" => $action,
@@ -64,7 +24,6 @@ protected static function encodeResponse($result, $action, $response_type='json'
return response($result)
->header('Content-Type', 'text/plain')
->header('Access-Control-Allow-Origin', '*');
}
}
}
@@ -4,11 +4,13 @@
use App\Factories\LinkFactory;
use App\Helpers\LinkHelper;
use App\Exceptions\Api\ApiException;
class ApiLinkController extends ApiController {
public function shortenLink(Request $request) {
$response_type = $request->input('response_type');
$user = self::getApiUserInfo($request);
// $user = self::getApiUserInfo($request);
$user = $request->user;
// Validate parameters
// Encode spaces as %20 to avoid validator conflicts
@@ -19,7 +21,7 @@ public function shortenLink(Request $request) {
]);
if ($validator->fails()) {
return abort(400, 'Invalid or missing parameters.');
throw new ApiException('MISSING_PARAMETERS', 'Invalid or missing parameters.', 400, $response_type);
}
$long_url = $request->input('url'); // * required
@@ -32,23 +34,25 @@ public function shortenLink(Request $request) {
$formatted_link = LinkFactory::createLink($long_url, $is_secret, $custom_ending, $link_ip, $user->username, false, true);
}
catch (\Exception $e) {
abort(400, $e->getMessage());
throw new ApiException('CREATE_ERROR', $e->getMessage(), 400, $response_type);
}
return self::encodeResponse($formatted_link, 'shorten', $response_type);
}
public function lookupLink(Request $request) {
$user = $request->user;
$response_type = $request->input('response_type');
$user = self::getApiUserInfo($request);
// $user = self::getApiUserInfo($request);
// Validate URL form data
$validator = \Validator::make($request->all(), [
'url_ending' => 'required|alpha_dash'
]);
if ($validator->fails()) {
return abort(400, 'Invalid or missing parameters.');
throw new ApiException('MISSING_PARAMETERS', 'Invalid or missing parameters.', 400, $response_type);
}
$url_ending = $request->input('url_ending');
@@ -60,7 +64,7 @@ public function lookupLink(Request $request) {
if ($link['secret_key']) {
if ($url_key != $link['secret_key']) {
abort(401, "Invalid URL code for secret URL.");
throw new ApiException('ACCESS_DENIED', 'Invalid URL code for secret URL.', 401, $response_type);
}
}
@@ -74,8 +78,7 @@ public function lookupLink(Request $request) {
], 'lookup', $response_type, $link['long_url']);
}
else {
abort(404, "Link not found.");
throw new ApiException('NOT_FOUND', 'Link not found.', 404, $response_type);
}
}
}
@@ -0,0 +1,62 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use App\Models\User;
use App\Helpers\ApiHelper;
use App\Exceptions\Api\ApiException;
class ApiMiddleware {
protected static function getApiUserInfo(Request $request) {
$api_key = $request->input('key');
$response_type = $request->input('response_type');
if (!$api_key) {
// no API key provided; check whether anonymous API is enabled
if (env('SETTING_ANON_API')) {
$username = 'ANONIP-' . $request->ip();
}
else {
throw new ApiException('AUTH_ERROR', 'Authentication token required.', 401, $response_type);
}
$user = (object) [
'username' => $username
];
}
else {
$user = User::where('active', 1)
->where('api_key', $api_key)
->where('api_active', 1)
->first();
if (!$user) {
abort(401, "Invalid authentication token.");
}
$username = $user->username;
}
$api_limit_reached = ApiHelper::checkUserApiQuota($username);
if ($api_limit_reached) {
throw new ApiException('QUOTA_EXCEEDED', 'Quota exceeded.', 429, $response_type);
}
return $user;
}
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next) {
$request->user = $this->getApiUserInfo($request);
return $next($request);
}
}
@@ -11,7 +11,7 @@ class VerifyCsrfToken extends BaseVerifier
* @var array
*/
public function handle($request, \Closure $next) {
if ($request->is('api/v*/action/*')) {
if ($request->is('api/v*/action/*') || $request->is('api/v*/data/*')) {
// Exclude public API from CSRF protection
// but do not exclude private API endpoints
return $next($request);
@@ -59,17 +59,18 @@
$app->get('admin/get_admin_users', ['as' => 'api_get_admin_users', 'uses' => 'AdminPaginationController@paginateAdminUsers']);
$app->get('admin/get_admin_links', ['as' => 'api_get_admin_links', 'uses' => 'AdminPaginationController@paginateAdminLinks']);
$app->get('admin/get_user_links', ['as' => 'api_get_user_links', 'uses' => 'AdminPaginationController@paginateUserLinks']);
});
$app->group(['prefix' => '/api/v2', 'namespace' => 'App\Http\Controllers\Api', 'middleware' => 'api'], function ($app) {
/* API shorten endpoints */
$app->post('action/shorten', ['as' => 'api_shorten_url', 'uses' => 'Api\ApiLinkController@shortenLink']);
$app->get('action/shorten', ['as' => 'api_shorten_url', 'uses' => 'Api\ApiLinkController@shortenLink']);
$app->post('action/shorten', ['as' => 'api_shorten_url', 'uses' => 'ApiLinkController@shortenLink']);
$app->get('action/shorten', ['as' => 'api_shorten_url', 'uses' => 'ApiLinkController@shortenLink']);
/* API lookup endpoints */
$app->post('action/lookup', ['as' => 'api_lookup_url', 'uses' => 'Api\ApiLinkController@lookupLink']);
$app->get('action/lookup', ['as' => 'api_lookup_url', 'uses' => 'Api\ApiLinkController@lookupLink']);
$app->post('action/lookup', ['as' => 'api_lookup_url', 'uses' => 'ApiLinkController@lookupLink']);
$app->get('action/lookup', ['as' => 'api_lookup_url', 'uses' => 'ApiLinkController@lookupLink']);
/* API data endpoints */
$app->get('data/link', ['as' => 'api_link_analytics', 'uses' => 'Api\ApiAnalyticsController@lookupLinkStats']);
$app->post('data/link', ['as' => 'api_link_analytics', 'uses' => 'Api\ApiAnalyticsController@lookupLinkStats']);
$app->get('data/link', ['as' => 'api_link_analytics', 'uses' => 'ApiAnalyticsController@lookupLinkStats']);
$app->post('data/link', ['as' => 'api_link_analytics', 'uses' => 'ApiAnalyticsController@lookupLinkStats']);
});
@@ -61,12 +61,12 @@
// Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
Illuminate\Session\Middleware\StartSession::class,
Illuminate\View\Middleware\ShareErrorsFromSession::class,
App\Http\Middleware\VerifyCsrfToken::class
App\Http\Middleware\VerifyCsrfToken::class,
]);
// $app->routeMiddleware([
// ]);
$app->routeMiddleware([
'api' => App\Http\Middleware\ApiMiddleware::class,
]);
/*
|--------------------------------------------------------------------------

0 comments on commit 65794f8

Please sign in to comment.
You can’t perform that action at this time.