From 4c4fffaf9d7dc7a65730a94862209e257b4cfddc Mon Sep 17 00:00:00 2001 From: Lewis Dale Date: Tue, 21 Mar 2023 09:09:24 +0000 Subject: [PATCH] Add some rudimentary authentication to the API --- .env.sample | 3 + .gitignore | 1 + composer.json | 3 +- composer.lock | 306 +++++++++++++++++++++++++++++++++++++- index.php | 10 +- src/Api/WebmentionApi.php | 31 ++++ src/Router/Request.php | 14 +- src/Router/Router.php | 52 ++++--- src/Router/StatusCode.php | 11 +- 9 files changed, 397 insertions(+), 34 deletions(-) create mode 100644 .env.sample diff --git a/.env.sample b/.env.sample new file mode 100644 index 0000000..d7e168d --- /dev/null +++ b/.env.sample @@ -0,0 +1,3 @@ +USERNAME="a-username" +PASSWORD="a-password" +DB="db-name" \ No newline at end of file diff --git a/.gitignore b/.gitignore index e6a4b2d..ee857e5 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ /.phpunit.result.cache .idea /*.db +.env \ No newline at end of file diff --git a/composer.json b/composer.json index dc3bcfe..d920341 100644 --- a/composer.json +++ b/composer.json @@ -19,6 +19,7 @@ "symfony/dom-crawler": "^6.2", "symfony/css-selector": "^6.2", "symfony/http-client": "^6.2", - "league/uri": "^6.8" + "league/uri": "^6.8", + "vlucas/phpdotenv": "^5.5" } } diff --git a/composer.lock b/composer.lock index dcd7582..ffbb400 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,70 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "352975aee49cdf2b7ee0c1f9a6ced635", + "content-hash": "f62bc46a34451432be93743680515868", "packages": [ + { + "name": "graham-campbell/result-type", + "version": "v1.1.1", + "source": { + "type": "git", + "url": "https://github.com/GrahamCampbell/Result-Type.git", + "reference": "672eff8cf1d6fe1ef09ca0f89c4b287d6a3eb831" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/672eff8cf1d6fe1ef09ca0f89c4b287d6a3eb831", + "reference": "672eff8cf1d6fe1ef09ca0f89c4b287d6a3eb831", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.1" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.32 || ^9.6.3 || ^10.0.12" + }, + "type": "library", + "autoload": { + "psr-4": { + "GrahamCampbell\\ResultType\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "An Implementation Of The Result Type", + "keywords": [ + "Graham Campbell", + "GrahamCampbell", + "Result Type", + "Result-Type", + "result" + ], + "support": { + "issues": "https://github.com/GrahamCampbell/Result-Type/issues", + "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.1" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/graham-campbell/result-type", + "type": "tidelift" + } + ], + "time": "2023-02-25T20:23:15+00:00" + }, { "name": "league/uri", "version": "6.8.0", @@ -246,6 +308,81 @@ }, "time": "2022-08-18T16:18:26+00:00" }, + { + "name": "phpoption/phpoption", + "version": "1.9.1", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/php-option.git", + "reference": "dd3a383e599f49777d8b628dadbb90cae435b87e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/dd3a383e599f49777d8b628dadbb90cae435b87e", + "reference": "dd3a383e599f49777d8b628dadbb90cae435b87e", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.32 || ^9.6.3 || ^10.0.12" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": true + }, + "branch-alias": { + "dev-master": "1.9-dev" + } + }, + "autoload": { + "psr-4": { + "PhpOption\\": "src/PhpOption/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com", + "homepage": "https://github.com/schmittjoh" + }, + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "Option Type for PHP", + "keywords": [ + "language", + "option", + "php", + "type" + ], + "support": { + "issues": "https://github.com/schmittjoh/php-option/issues", + "source": "https://github.com/schmittjoh/php-option/tree/1.9.1" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption", + "type": "tidelift" + } + ], + "time": "2023-02-25T19:38:58+00:00" + }, { "name": "psr/container", "version": "2.0.2", @@ -935,6 +1072,89 @@ ], "time": "2022-11-03T14:55:06+00:00" }, + { + "name": "symfony/polyfill-php80", + "version": "v1.27.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936", + "reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.27-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.27.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-11-03T14:55:06+00:00" + }, { "name": "symfony/service-contracts", "version": "v3.2.1", @@ -1019,6 +1239,90 @@ } ], "time": "2023-03-01T10:32:47+00:00" + }, + { + "name": "vlucas/phpdotenv", + "version": "v5.5.0", + "source": { + "type": "git", + "url": "https://github.com/vlucas/phpdotenv.git", + "reference": "1a7ea2afc49c3ee6d87061f5a233e3a035d0eae7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/1a7ea2afc49c3ee6d87061f5a233e3a035d0eae7", + "reference": "1a7ea2afc49c3ee6d87061f5a233e3a035d0eae7", + "shasum": "" + }, + "require": { + "ext-pcre": "*", + "graham-campbell/result-type": "^1.0.2", + "php": "^7.1.3 || ^8.0", + "phpoption/phpoption": "^1.8", + "symfony/polyfill-ctype": "^1.23", + "symfony/polyfill-mbstring": "^1.23.1", + "symfony/polyfill-php80": "^1.23.1" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4.1", + "ext-filter": "*", + "phpunit/phpunit": "^7.5.20 || ^8.5.30 || ^9.5.25" + }, + "suggest": { + "ext-filter": "Required to use the boolean validator." + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": true + }, + "branch-alias": { + "dev-master": "5.5-dev" + } + }, + "autoload": { + "psr-4": { + "Dotenv\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Vance Lucas", + "email": "vance@vancelucas.com", + "homepage": "https://github.com/vlucas" + } + ], + "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.", + "keywords": [ + "dotenv", + "env", + "environment" + ], + "support": { + "issues": "https://github.com/vlucas/phpdotenv/issues", + "source": "https://github.com/vlucas/phpdotenv/tree/v5.5.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv", + "type": "tidelift" + } + ], + "time": "2022-10-16T01:01:54+00:00" } ], "packages-dev": [ diff --git a/index.php b/index.php index 794f850..61578e5 100644 --- a/index.php +++ b/index.php @@ -2,6 +2,7 @@ require_once __DIR__ . "/vendor/autoload.php"; +use Lewisdale\Webmentions\Api\WebmentionApi; use Lewisdale\Webmentions\Endpoint; use Lewisdale\Webmentions\Exceptions\InvalidSourceException; use Lewisdale\Webmentions\Exceptions\InvalidTargetException; @@ -16,10 +17,13 @@ use Lewisdale\Webmentions\Router\StatusCode; use Lewisdale\Webmentions\Webmention; use Symfony\Component\HttpClient\HttpClient; -$router = new Router(); -$gateway = new SqliteGateway("webmentions.db"); +$dotenv = Dotenv\Dotenv::createImmutable(__DIR__); +$dotenv->load(); -$apiController = new \Lewisdale\Webmentions\Api\WebmentionApi($gateway); +$router = new Router(); +$gateway = new SqliteGateway($_ENV["DB"]); + +$apiController = new WebmentionApi($gateway); $router->post("/send", function (Request $req, Response $response) { $mentioner = new Webmention(); diff --git a/src/Api/WebmentionApi.php b/src/Api/WebmentionApi.php index d17d2b2..2fee7db 100644 --- a/src/Api/WebmentionApi.php +++ b/src/Api/WebmentionApi.php @@ -19,6 +19,11 @@ class WebmentionApi // List Webmentions. Optionally filter by target. public function list(Request $request, Response $response) { + if (!$this->verifyUser($request)) { + $response->status_code = StatusCode::Unauthorized; + return "Unauthorized"; + } + $target = array_key_exists("post", $request->query) ? $request->query["post"] : null; $mentions = !empty($target) ? $this->gateway->getByPost($target) : $this->gateway->list(); return $response->json($mentions); @@ -27,6 +32,11 @@ class WebmentionApi // Get a webmention by ID public function get(Request $request, Response $response) { + if (!$this->verifyUser($request)) { + $response->status_code = StatusCode::Unauthorized; + return "Unauthorized"; + } + $id = (int)$request->params[0]; if ($id) { @@ -40,4 +50,25 @@ class WebmentionApi $response->status_code = StatusCode::NotFound; } + + private function verifyUser(Request $request): bool + { + if (!array_key_exists("authorization", $request->headers)) { + return false; + } + + [, $auth] = explode("Basic ", $request->headers["authorization"]); + + if (empty($auth)) { + return false; + } + + [$username, $password] = explode(":", base64_decode($auth)); + + if (!empty($username) && $username === $_ENV["USERNAME"] && !empty($password) && $password === $_ENV["PASSWORD"]) { + return true; + } + + return false; + } } \ No newline at end of file diff --git a/src/Router/Request.php b/src/Router/Request.php index 1447749..b11df5c 100644 --- a/src/Router/Request.php +++ b/src/Router/Request.php @@ -2,14 +2,18 @@ namespace Lewisdale\Webmentions\Router; -class Request { +class Request +{ public function __construct( - public array $params, + public array $params, public readonly string $uri, public readonly Method $method, - public readonly array $post = [], - public readonly array $query = [], - ) {} + public readonly array $post = [], + public readonly array $query = [], + public readonly array $headers = [], + ) + { + } } ?> \ No newline at end of file diff --git a/src/Router/Router.php b/src/Router/Router.php index d47ef79..1e6498f 100644 --- a/src/Router/Router.php +++ b/src/Router/Router.php @@ -6,60 +6,69 @@ use Exception; /** * This is a dumb, custom Router I slapped together as a way of reminding myself how to PHP. - * + * * It's not great, and currently on supports two methods: GET and POST. - * + * * Usage is as follows: - * + * * ``` * $router = new Lewisdale\Webmentions\Router\Router(); * $router->get('/a/get/route', function($request, $response) { * $response->status_code = StatusCode::OK; * return "Hello world!"; * }); - * + * * $router->get('/a/json/route', function($request, $response) { * return $response->json(new MyClass()); * }); - * + * * $router->dispatch(); * ``` - * + * * TL;DR? Don't use this. There are some good routing libraries already. - * + * * But if you're reading this, it's already too late. */ -class Router { +class Router +{ private array $routes; - private function trim_route(string $route): string { + private function trim_route(string $route): string + { $route = $route == "/" ? $route : rtrim($route, "/"); $route = str_replace("/", "\/", $route); return $route; } - private function format_route(string $route): string { + private function format_route(string $route): string + { $formatted = $this->trim_route($route); - return "#^$formatted$#"; + return "#^$formatted$#"; } - public function get(string $route, callable $fn) { + public function get(string $route, callable $fn) + { $this->routes[] = new Route(Method::GET, $this->format_route($route), $fn); } - public function post(string $route, callable $fn) { + public function post(string $route, callable $fn) + { $this->routes[] = new Route(Method::POST, $this->format_route($route), $fn); } - private function get_params(array $matches): array { + private function get_params(array $matches): array + { return array_map( - function($match) { return $match[0]; }, + function ($match) { + return $match[0]; + }, array_shift($matches) ); } - public function dispatch() { + public function dispatch() + { $uri = $_SERVER['REQUEST_URI']; $method = Method::from($_SERVER['REQUEST_METHOD']); @@ -67,9 +76,9 @@ class Router { $response = new Response(); - foreach($this->routes as $route) { + foreach ($this->routes as $route) { if ($method == $route->type) { - $matches = array(); + $matches = []; if (preg_match_all($route->pattern, $uri, $matches)) { $num_matched++; $fn = $route->fn; @@ -94,12 +103,13 @@ class Router { } - private function respond(Response $response) { + private function respond(Response $response) + { http_response_code($response->status_code->code()); echo $response->body; } - + private function construct_request_object(array $params) { $uri = $_SERVER['REQUEST_URI']; @@ -107,7 +117,7 @@ class Router { $query = $_GET; $post = $_POST; - return new Request($params, $uri, $method, $post, $query); + return new Request($params, $uri, $method, $post, $query, array_change_key_case(getallheaders(), CASE_LOWER)); } } diff --git a/src/Router/StatusCode.php b/src/Router/StatusCode.php index 057224b..4b48826 100644 --- a/src/Router/StatusCode.php +++ b/src/Router/StatusCode.php @@ -2,22 +2,27 @@ namespace Lewisdale\Webmentions\Router; -enum StatusCode { +enum StatusCode +{ case NotFound; case Ok; case Redirect; case InternalError; case BadRequest; case Created; + case Forbidden; + case Unauthorized; - public function code() : int + public function code(): int { - return match($this) { + return match ($this) { StatusCode::NotFound => 404, StatusCode::Ok => 200, StatusCode::InternalError => 500, StatusCode::Redirect => 301, StatusCode::BadRequest => 400, + StatusCode::Unauthorized => 401, + StatusCode::Forbidden => 403, StatusCode::Created => 201, }; }