Skip to content

Commit

Permalink
Use ApiException to handle API errors and use ApiMiddleware to handle…
Browse files Browse the repository at this point in the history
… API authentication
  • Loading branch information
cydrobolt committed Mar 17, 2017
1 parent 8f6d976 commit 65794f8
Show file tree
Hide file tree
Showing 9 changed files with 152 additions and 66 deletions.
42 changes: 42 additions & 0 deletions app/Exceptions/Api/ApiException.php
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
18 changes: 18 additions & 0 deletions app/Exceptions/Handler.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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'));
Expand All @@ -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();

Expand All @@ -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);
Expand Down
11 changes: 6 additions & 5 deletions app/Http/Controllers/Api/ApiAnalyticsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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');
Expand All @@ -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);
Expand All @@ -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([
Expand Down
41 changes: 0 additions & 41 deletions app/Http/Controllers/Api/ApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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', '*');

}
}
}
19 changes: 11 additions & 8 deletions app/Http/Controllers/Api/ApiLinkController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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');
Expand All @@ -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);
}
}

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

}
}
62 changes: 62 additions & 0 deletions app/Http/Middleware/ApiMiddleware.php
Original file line number Diff line number Diff line change
@@ -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);
}
}
2 changes: 1 addition & 1 deletion app/Http/Middleware/VerifyCsrfToken.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
15 changes: 8 additions & 7 deletions app/Http/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -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']);
});
8 changes: 4 additions & 4 deletions bootstrap/app.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
]);

/*
|--------------------------------------------------------------------------
Expand Down

0 comments on commit 65794f8

Please sign in to comment.