Add some rudimentary authentication to the API

This commit is contained in:
Lewis Dale 2023-03-21 09:09:24 +00:00
parent 7dc9d0a5cb
commit 4c4fffaf9d
9 changed files with 397 additions and 34 deletions

3
.env.sample Normal file
View File

@ -0,0 +1,3 @@
USERNAME="a-username"
PASSWORD="a-password"
DB="db-name"

1
.gitignore vendored
View File

@ -2,3 +2,4 @@
/.phpunit.result.cache
.idea
/*.db
.env

View File

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

306
composer.lock generated
View File

@ -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": [

View File

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

View File

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

View File

@ -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 = [],
)
{
}
}
?>

View File

@ -29,37 +29,46 @@ use Exception;
*
* 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$#";
}
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,7 +103,8 @@ class Router {
}
private function respond(Response $response) {
private function respond(Response $response)
{
http_response_code($response->status_code->code());
echo $response->body;
@ -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));
}
}

View File

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