diff --git a/app/Exceptions/Api/ApiException.php b/app/Exceptions/Api/ApiException.php new file mode 100644 index 0000000..d2f36a6 --- /dev/null +++ b/app/Exceptions/Api/ApiException.php @@ -0,0 +1,42 @@ +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; + } + } +} diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index c0e1136..2afb883 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -8,6 +8,8 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; 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 @@ class Handler extends ExceptionHandler { 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 @@ class Handler extends ExceptionHandler { 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 @@ class Handler extends ExceptionHandler { 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); diff --git a/app/Http/Controllers/Api/ApiAnalyticsController.php b/app/Http/Controllers/Api/ApiAnalyticsController.php index 692316d..abb2d86 100644 --- a/app/Http/Controllers/Api/ApiAnalyticsController.php +++ b/app/Http/Controllers/Api/ApiAnalyticsController.php @@ -5,13 +5,14 @@ use Illuminate\Http\Request; 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 @@ class ApiAnalyticsController extends ApiController { ]); 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 @@ class ApiAnalyticsController extends ApiController { $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 @@ class ApiAnalyticsController extends ApiController { $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([ diff --git a/app/Http/Controllers/Api/ApiController.php b/app/Http/Controllers/Api/ApiController.php index 27d102b..ea3b96c 100644 --- a/app/Http/Controllers/Api/ApiController.php +++ b/app/Http/Controllers/Api/ApiController.php @@ -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 @@ class ApiController extends Controller { return response($result) ->header('Content-Type', 'text/plain') ->header('Access-Control-Allow-Origin', '*'); - } } } diff --git a/app/Http/Controllers/Api/ApiLinkController.php b/app/Http/Controllers/Api/ApiLinkController.php index 0880df2..d4c72f2 100644 --- a/app/Http/Controllers/Api/ApiLinkController.php +++ b/app/Http/Controllers/Api/ApiLinkController.php @@ -4,11 +4,13 @@ use Illuminate\Http\Request; 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 @@ class ApiLinkController extends ApiController { ]); 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,15 +34,17 @@ class ApiLinkController extends ApiController { $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(), [ @@ -48,7 +52,7 @@ class ApiLinkController extends ApiController { ]); 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 @@ class ApiLinkController extends ApiController { 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 @@ class ApiLinkController extends ApiController { ], 'lookup', $response_type, $link['long_url']); } else { - abort(404, "Link not found."); + throw new ApiException('NOT_FOUND', 'Link not found.', 404, $response_type); } - } } diff --git a/app/Http/Middleware/ApiMiddleware.php b/app/Http/Middleware/ApiMiddleware.php new file mode 100644 index 0000000..6c4e5eb --- /dev/null +++ b/app/Http/Middleware/ApiMiddleware.php @@ -0,0 +1,62 @@ +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); + } +} diff --git a/app/Http/Middleware/VerifyCsrfToken.php b/app/Http/Middleware/VerifyCsrfToken.php index a08af42..d1ee178 100644 --- a/app/Http/Middleware/VerifyCsrfToken.php +++ b/app/Http/Middleware/VerifyCsrfToken.php @@ -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); diff --git a/app/Http/routes.php b/app/Http/routes.php index bd85dce..7ec879d 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -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_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']); }); diff --git a/bootstrap/app.php b/bootstrap/app.php index ebf9dc2..6558119 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -61,12 +61,12 @@ $app->middleware([ // 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, +]); /* |--------------------------------------------------------------------------