1
0
mirror of https://github.com/cydrobolt/polr.git synced 2024-09-18 19:52:26 +02:00

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

This commit is contained in:
Chaoyi Zha 2017-03-17 17:00:13 -04:00
parent 8f6d9762e9
commit 65794f83d7
9 changed files with 152 additions and 66 deletions

View File

@ -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;
}
}
}

View File

@ -8,6 +8,8 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Laravel\Lumen\Exceptions\Handler as ExceptionHandler; use Laravel\Lumen\Exceptions\Handler as ExceptionHandler;
use Illuminate\Support\Facades\Response; use Illuminate\Support\Facades\Response;
use App\Exceptions\Api\ApiException;
class Handler extends ExceptionHandler { class Handler extends ExceptionHandler {
/** /**
* A list of the exception types that should not be reported. * A list of the exception types that should not be reported.
@ -43,6 +45,7 @@ class Handler extends ExceptionHandler {
if (env('APP_DEBUG') != true) { if (env('APP_DEBUG') != true) {
// Render nice error pages if debug is off // Render nice error pages if debug is off
if ($e instanceof NotFoundHttpException) { if ($e instanceof NotFoundHttpException) {
// Handle 404 exceptions
if (env('SETTING_REDIRECT_404')) { if (env('SETTING_REDIRECT_404')) {
// Redirect 404s to SETTING_INDEX_REDIRECT // Redirect 404s to SETTING_INDEX_REDIRECT
return redirect()->to(env('SETTING_INDEX_REDIRECT')); return redirect()->to(env('SETTING_INDEX_REDIRECT'));
@ -51,6 +54,7 @@ class Handler extends ExceptionHandler {
return view('errors.404'); return view('errors.404');
} }
if ($e instanceof HttpException) { if ($e instanceof HttpException) {
// Handle HTTP exceptions thrown by public-facing controllers
$status_code = $e->getStatusCode(); $status_code = $e->getStatusCode();
$status_message = $e->getMessage(); $status_message = $e->getMessage();
@ -63,6 +67,20 @@ class Handler extends ExceptionHandler {
return response(view('errors.generic', ['status_code' => $status_code, 'status_message' => $status_message]), $status_code); 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); return parent::render($request, $e);

View File

@ -5,13 +5,14 @@ use Illuminate\Http\Request;
use App\Helpers\LinkHelper; use App\Helpers\LinkHelper;
use App\Helpers\UserHelper; use App\Helpers\UserHelper;
use App\Helpers\StatsHelper; use App\Helpers\StatsHelper;
use App\Exceptions\Api\ApiException;
class ApiAnalyticsController extends ApiController { class ApiAnalyticsController extends ApiController {
public function lookupLinkStats (Request $request, $stats_type=false) { public function lookupLinkStats (Request $request, $stats_type=false) {
$response_type = $request->input('response_type') ?: 'json'; $response_type = $request->input('response_type') ?: 'json';
if ($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); $user = self::getApiUserInfo($request);
@ -24,7 +25,7 @@ class ApiAnalyticsController extends ApiController {
]); ]);
if ($validator->fails()) { 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'); $url_ending = $request->input('url_ending');
@ -37,13 +38,13 @@ class ApiAnalyticsController extends ApiController {
$link = LinkHelper::linkExists($url_ending); $link = LinkHelper::linkExists($url_ending);
if ($link === false) { if ($link === false) {
abort(404, 'Link not found.'); throw new ApiException('NOT_FOUND', 'Link not found.', 404, $response_type);
} }
if (($link->creator != $user->username) && if (($link->creator != $user->username) &&
!(UserHelper::userIsAdmin($user->username))){ !(UserHelper::userIsAdmin($user->username))){
// If user does not own link and is not an admin // 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); $stats = new StatsHelper($link->id, $left_bound, $right_bound);
@ -58,7 +59,7 @@ class ApiAnalyticsController extends ApiController {
$fetched_stats = $stats->getRefererStats(); $fetched_stats = $stats->getRefererStats();
} }
else { else {
abort(400, 'Invalid analytics type requested.'); throw new ApiException('INVALID_ANALYTICS_TYPE', 'Invalid analytics type requested.', 400, $response_type);
} }
return self::encodeResponse([ return self::encodeResponse([

View File

@ -2,48 +2,8 @@
namespace App\Http\Controllers\Api; namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\User;
use App\Helpers\ApiHelper;
class ApiController extends Controller { 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) { protected static function encodeResponse($result, $action, $response_type='json', $plain_text_response=false) {
$response = [ $response = [
"action" => $action, "action" => $action,
@ -64,7 +24,6 @@ class ApiController extends Controller {
return response($result) return response($result)
->header('Content-Type', 'text/plain') ->header('Content-Type', 'text/plain')
->header('Access-Control-Allow-Origin', '*'); ->header('Access-Control-Allow-Origin', '*');
} }
} }
} }

View File

@ -4,11 +4,13 @@ use Illuminate\Http\Request;
use App\Factories\LinkFactory; use App\Factories\LinkFactory;
use App\Helpers\LinkHelper; use App\Helpers\LinkHelper;
use App\Exceptions\Api\ApiException;
class ApiLinkController extends ApiController { class ApiLinkController extends ApiController {
public function shortenLink(Request $request) { public function shortenLink(Request $request) {
$response_type = $request->input('response_type'); $response_type = $request->input('response_type');
$user = self::getApiUserInfo($request); // $user = self::getApiUserInfo($request);
$user = $request->user;
// Validate parameters // Validate parameters
// Encode spaces as %20 to avoid validator conflicts // Encode spaces as %20 to avoid validator conflicts
@ -19,7 +21,7 @@ class ApiLinkController extends ApiController {
]); ]);
if ($validator->fails()) { 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 $long_url = $request->input('url'); // * required
@ -32,15 +34,17 @@ class ApiLinkController extends ApiController {
$formatted_link = LinkFactory::createLink($long_url, $is_secret, $custom_ending, $link_ip, $user->username, false, true); $formatted_link = LinkFactory::createLink($long_url, $is_secret, $custom_ending, $link_ip, $user->username, false, true);
} }
catch (\Exception $e) { 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); return self::encodeResponse($formatted_link, 'shorten', $response_type);
} }
public function lookupLink(Request $request) { public function lookupLink(Request $request) {
$user = $request->user;
$response_type = $request->input('response_type'); $response_type = $request->input('response_type');
$user = self::getApiUserInfo($request); // $user = self::getApiUserInfo($request);
// Validate URL form data // Validate URL form data
$validator = \Validator::make($request->all(), [ $validator = \Validator::make($request->all(), [
@ -48,7 +52,7 @@ class ApiLinkController extends ApiController {
]); ]);
if ($validator->fails()) { 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'); $url_ending = $request->input('url_ending');
@ -60,7 +64,7 @@ class ApiLinkController extends ApiController {
if ($link['secret_key']) { if ($link['secret_key']) {
if ($url_key != $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 @@ class ApiLinkController extends ApiController {
], 'lookup', $response_type, $link['long_url']); ], 'lookup', $response_type, $link['long_url']);
} }
else { else {
abort(404, "Link not found."); throw new ApiException('NOT_FOUND', 'Link not found.', 404, $response_type);
} }
} }
} }

View File

@ -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);
}
}

View File

@ -11,7 +11,7 @@ class VerifyCsrfToken extends BaseVerifier
* @var array * @var array
*/ */
public function handle($request, \Closure $next) { 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 // Exclude public API from CSRF protection
// but do not exclude private API endpoints // but do not exclude private API endpoints
return $next($request); return $next($request);

View File

@ -59,17 +59,18 @@ $app->group(['prefix' => '/api/v2', 'namespace' => 'App\Http\Controllers'], func
$app->get('admin/get_admin_users', ['as' => 'api_get_admin_users', 'uses' => 'AdminPaginationController@paginateAdminUsers']); $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_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->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 */ /* API shorten endpoints */
$app->post('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' => 'Api\ApiLinkController@shortenLink']); $app->get('action/shorten', ['as' => 'api_shorten_url', 'uses' => 'ApiLinkController@shortenLink']);
/* API lookup endpoints */ /* API lookup endpoints */
$app->post('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' => 'Api\ApiLinkController@lookupLink']); $app->get('action/lookup', ['as' => 'api_lookup_url', 'uses' => 'ApiLinkController@lookupLink']);
/* API data endpoints */ /* API data endpoints */
$app->get('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' => 'Api\ApiAnalyticsController@lookupLinkStats']); $app->post('data/link', ['as' => 'api_link_analytics', 'uses' => 'ApiAnalyticsController@lookupLinkStats']);
}); });

View File

@ -61,12 +61,12 @@ $app->middleware([
// Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, // Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
Illuminate\Session\Middleware\StartSession::class, Illuminate\Session\Middleware\StartSession::class,
Illuminate\View\Middleware\ShareErrorsFromSession::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,
// ]); ]);
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------