1
1
Fork 0
mirror of https://code.mensbeam.com/MensBeam/Arsse.git synced 2025-01-09 17:32:40 +00:00

First battery of IndieAuth tests, with fixes

This commit is contained in:
J. King 2019-09-26 19:44:25 -04:00
parent 6d2b587e38
commit 26fa9461eb
3 changed files with 125 additions and 18 deletions

View file

@ -9,6 +9,7 @@ namespace JKingWeb\Arsse\REST\Microsub;
use JKingWeb\Arsse\Arsse; use JKingWeb\Arsse\Arsse;
use JKingWeb\Arsse\Misc\URL; use JKingWeb\Arsse\Misc\URL;
use JKingWeb\Arsse\Misc\Date; use JKingWeb\Arsse\Misc\Date;
use JKingWeb\Arsse\Misc\ValueInfo;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Zend\Diactoros\Response\HtmlResponse; use Zend\Diactoros\Response\HtmlResponse;
@ -31,6 +32,14 @@ class Auth extends \JKingWeb\Arsse\REST\AbstractHandler {
'/' => "%2F", '/' => "%2F",
'?' => "%3F", '?' => "%3F",
]; ];
/** The minimal set of reserved URL characters which must be escaped in query values */
const QUERY_ESCAPES = [
'#' => "%23",
'%' => "%25",
'&' => "%26",
];
/** The acceptable media type of input for POST requests */
const ACCEPTED_TYPES = "application/x-www-form-urlencoded";
public function __construct() { public function __construct() {
} }
@ -44,19 +53,25 @@ class Auth extends \JKingWeb\Arsse\REST\AbstractHandler {
// gather the query parameters and act on the "f" (function) parameter // gather the query parameters and act on the "f" (function) parameter
$process = $req->getQueryParams()['f'] ?? ""; $process = $req->getQueryParams()['f'] ?? "";
$method = $req->getMethod(); $method = $req->getMethod();
if (isset(self::FUNCTIONS[$process]) || ($process === "" && !strlen($path)) || ($process !== "" && strlen($path))) { if (!isset(self::FUNCTIONS[$process]) || ($process === "" && !strlen($path)) || ($process !== "" && strlen($path))) {
// the function requested needs to exist // the function requested needs to exist
// the path should also be empty unless we're doing discovery // the path should also be empty unless we're doing discovery
return new EmptyResponse(404); return new EmptyResponse(404);
} elseif ($method === "OPTIONS") { } elseif ($method === "OPTIONS") {
$fields = ['Allow' => implode(",", array_keys(self::FUNCTIONS[$process]))]; $fields = ['Allow' => implode(",", array_keys(self::FUNCTIONS[$process]))];
if (isset(self::FUNCTIONS[$process]['POST'])) { if (isset(self::FUNCTIONS[$process]['POST'])) {
$fields['Accept'] = "application/x-www-form-urlencoded"; $fields['Accept'] = self::ACCEPTED_TYPES;
} }
return new EmptyResponse(204, $fields); return new EmptyResponse(204, $fields);
} elseif (isset(self::FUNCTIONS[$process][$method])) { } elseif (!isset(self::FUNCTIONS[$process][$method])) {
return new EmptyResponse(405, ['Allow' => implode(",", array_keys(self::FUNCTIONS[$process]))]); return new EmptyResponse(405, ['Allow' => implode(",", array_keys(self::FUNCTIONS[$process]))]);
} else { } else {
if ($req->getMethod() !== "GET") {
$type = $req->getHeaderLine("Content-Type") ?? "";
if (strlen($type) && strtolower($type) !== self::ACCEPTED_TYPES) {
return new EmptyResponse(415, ['Accept' => self::ACCEPTED_TYPES]);
}
}
try { try {
$func = self::FUNCTIONS[$process][$method]; $func = self::FUNCTIONS[$process][$method];
return $this->$func($req); return $this->$func($req);
@ -76,8 +91,7 @@ class Auth extends \JKingWeb\Arsse\REST\AbstractHandler {
protected function buildBaseURL(ServerRequestInterface $req): string { protected function buildBaseURL(ServerRequestInterface $req): string {
// construct the base user identifier URL; the user is never checked against the database // construct the base user identifier URL; the user is never checked against the database
$s = $req->getServerParams(); $s = $req->getServerParams();
$path = $req->getRequestTarget()['path']; $https = ValueInfo::normalize($s['HTTPS'] ?? "", ValueInfo::T_BOOL);
$https = (strlen($s['HTTPS'] ?? "") && $s['HTTPS'] !== "off");
$port = (int) ($s['SERVER_PORT'] ?? 0); $port = (int) ($s['SERVER_PORT'] ?? 0);
$port = (!$port || ($https && $port == 443) || (!$https && $port == 80)) ? "" : ":$port"; $port = (!$port || ($https && $port == 443) || (!$https && $port == 80)) ? "" : ":$port";
return URL::normalize(($https ? "https" : "http")."://".$s['HTTP_HOST'].$port."/"); return URL::normalize(($https ? "https" : "http")."://".$s['HTTP_HOST'].$port."/");
@ -149,11 +163,11 @@ class Auth extends \JKingWeb\Arsse\REST\AbstractHandler {
$urlService = $base."microsub"; $urlService = $base."microsub";
// output an extremely basic identity resource // output an extremely basic identity resource
$html = '<meta charset="UTF-8"><link rel="authorization_endpoint" href="'.htmlspecialchars($urlAuth).'"><link rel="token_endpoint" href="'.htmlspecialchars($urlToken).'"><link rel="microsub" href="'.htmlspecialchars($urlService).'">'; $html = '<meta charset="UTF-8"><link rel="authorization_endpoint" href="'.htmlspecialchars($urlAuth).'"><link rel="token_endpoint" href="'.htmlspecialchars($urlToken).'"><link rel="microsub" href="'.htmlspecialchars($urlService).'">';
return new HtmlResponse($html, 200, [ return new HtmlResponse($html, 200, ['Link' => [
"Link: <$urlAuth>; rel=\"authorization_endpoint\"", "<$urlAuth>; rel=\"authorization_endpoint\"",
"Link: <$urlToken>; rel=\"token_endpoint\"", "<$urlToken>; rel=\"token_endpoint\"",
"Link: <$urlService>; rel=\"microsub\"", "<$urlService>; rel=\"microsub\"",
]); ]]);
} }
/** Handles the authentication process /** Handles the authentication process
@ -311,7 +325,7 @@ class Auth extends \JKingWeb\Arsse\REST\AbstractHandler {
'invalid_request' => 400, 'invalid_request' => 400,
'invalid_token' => 401, 'invalid_token' => 401,
][$errCode] ?? 500; ][$errCode] ?? 500;
return new EmptyResponse($httpCode, ['WWW-Authenticate' => "Bearer error=\"$erroCode\""]); return new EmptyResponse($httpCode, ['WWW-Authenticate' => "Bearer error=\"$errCode\""]);
} }
return new JsonResponse([ return new JsonResponse([
'me' => $data['me'] ?? "", 'me' => $data['me'] ?? "",

View file

@ -7,10 +7,80 @@ declare(strict_types=1);
namespace JKingWeb\Arsse\TestCase\REST\Microsub; namespace JKingWeb\Arsse\TestCase\REST\Microsub;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Zend\Diactoros\ServerRequest;
use Zend\Diactoros\Response\JsonResponse as Response; use Zend\Diactoros\Response\JsonResponse as Response;
use Zend\Diactoros\Response\EmptyResponse; use Zend\Diactoros\Response\EmptyResponse;
use Zend\Diactoros\Response\HtmlResponse;
/** @covers \JKingWeb\Arsse\REST\Microsub\Auth<extended> */ /** @covers \JKingWeb\Arsse\REST\Microsub\Auth<extended> */
class TestAuth extends \JKingWeb\Arsse\Test\AbstractTest { class TestAuth extends \JKingWeb\Arsse\Test\AbstractTest {
public function req(string $url, string $method = "GET", array $data = [], array $headers = [], string $type = "application/x-www-form-urlencoded", string $body = null): ResponseInterface {
$type = (strtoupper($method) === "GET") ? "" : $type;
$req = $this->serverRequest($method, $url, "/u/", $headers, [], $body ?? $data, $type);
return (new \JKingWeb\Arsse\REST\Microsub\Auth)->dispatch($req);
}
/** @dataProvider provideInvalidRequests */
public function testHandleInvalidRequests(ResponseInterface $exp, string $method, string $url, string $type = null) {
$act = $this->req("http://example.com".$url, $method, [], [], $type ?? "");
$this->assertMessage($exp, $act);
}
public function provideInvalidRequests() {
$r404 = new EmptyResponse(404);
$r405g = new EmptyResponse(405, ['Allow' => "GET"]);
$r405gp = new EmptyResponse(405, ['Allow' => "GET,POST"]);
$r415 = new EmptyResponse(415, ['Accept' => "application/x-www-form-urlencoded"]);
return [
[$r404, "GET", "/u/"],
[$r404, "GET", "/u/john.doe/hello"],
[$r404, "GET", "/u/john.doe/"],
[$r404, "GET", "/u/john.doe?f=hello"],
[$r404, "GET", "/u/?f="],
[$r404, "GET", "/u/?f=goodbye"],
[$r405g, "POST", "/u/john.doe"],
[$r405gp, "PUT", "/u/?f=token"],
[$r404, "POST", "/u/john.doe?f=token"],
[$r415, "POST", "/u/?f=token", "application/json"],
];
}
/** @dataProvider provideOptionsRequests */
public function testHandleOptionsRequests(string $url, array $headerFields) {
$exp = new EmptyResponse(204, $headerFields);
$this->assertMessage($exp, $this->req("http://example.com".$url, "OPTIONS"));
}
public function provideOptionsRequests() {
$ident = ['Allow' => "GET"];
$other = ['Allow' => "GET,POST", 'Accept' => "application/x-www-form-urlencoded"];
return [
["/u/john.doe", $ident],
["/u/?f=token", $other],
["/u/?f=auth", $other],
];
}
/** @dataProvider provideDiscoveryRequests */
public function testDiscoverAUser(string $url, string $origin) {
$auth = $origin."/u/?f=auth";
$token = $origin."/u/?f=token";
$microsub = $origin."/microsub";
$exp = new HtmlResponse('<meta charset="UTF-8"><link rel="authorization_endpoint" href="'.htmlspecialchars($auth).'"><link rel="token_endpoint" href="'.htmlspecialchars($token).'"><link rel="microsub" href="'.htmlspecialchars($microsub).'">', 200, ['Link' => [
"<$auth>; rel=\"authorization_endpoint\"",
"<$token>; rel=\"token_endpoint\"",
"<$microsub>; rel=\"microsub\"",
]]);
$this->assertMessage($exp, $this->req($url));
}
public function provideDiscoveryRequests() {
return [
["http://example.com/u/john.doe", "http://example.com"],
["http://example.com:80/u/john.doe", "http://example.com"],
["https://example.com/u/john.doe", "https://example.com"],
["https://example.com:443/u/john.doe", "https://example.com"],
["http://example.com:443/u/john.doe", "http://example.com:443"],
["https://example.com:80/u/john.doe", "https://example.com:80"],
];
}
} }

View file

@ -64,13 +64,12 @@ abstract class AbstractTest extends \PHPUnit\Framework\TestCase {
} }
protected function serverRequest(string $method, string $url, string $urlPrefix, array $headers = [], array $vars = [], $body = null, string $type = "", $params = [], string $user = null): ServerRequestInterface { protected function serverRequest(string $method, string $url, string $urlPrefix, array $headers = [], array $vars = [], $body = null, string $type = "", $params = [], string $user = null): ServerRequestInterface {
$server = [ // build an initial $_SERVER array
'REQUEST_METHOD' => $method, $server = ['REQUEST_METHOD' => strtoupper($method)];
'REQUEST_URI' => $url,
];
if (strlen($type)) { if (strlen($type)) {
$server['HTTP_CONTENT_TYPE'] = $type; $server['HTTP_CONTENT_TYPE'] = $type;
} }
// add any specified parameters to the URL
if (isset($params)) { if (isset($params)) {
if (is_array($params)) { if (is_array($params)) {
$params = implode("&", array_map(function($v, $k) { $params = implode("&", array_map(function($v, $k) {
@ -79,12 +78,27 @@ abstract class AbstractTest extends \PHPUnit\Framework\TestCase {
} }
$url = URL::queryAppend($url, (string) $params); $url = URL::queryAppend($url, (string) $params);
} }
// glean the scheme, hostname, and port from the URL
$urlParts = parse_url($url);
if (isset($urlParts['scheme'])) {
$server['HTTPS'] = strtolower($urlParts['scheme']) === "https" ? "on" : "off";
}
if (isset($urlParts['host'])) {
$server['HTTP_HOST'] = $urlParts['host'];
if (isset($urlParts['port'])) {
$server['SERVER_PORT'] = $urlParts['port'];
}
$url = $urlParts['path'].(isset($urlParts['query']) ? "?".$urlParts['query'] : "");
}
$server['REQUEST_URI'] = $url;
// rebuild the parsed query parameters from the URL
$q = parse_url($url, \PHP_URL_QUERY); $q = parse_url($url, \PHP_URL_QUERY);
if (strlen($q ?? "")) { if (strlen($q ?? "")) {
parse_str($q, $params); parse_str($q, $params);
} else { } else {
$params = []; $params = [];
} }
// prepare the body and parsed body
$parsedBody = null; $parsedBody = null;
if (isset($body)) { if (isset($body)) {
if (is_string($body) && in_array(strtolower($type), ["", "application/x-www-form-urlencoded"])) { if (is_string($body) && in_array(strtolower($type), ["", "application/x-www-form-urlencoded"])) {
@ -96,8 +110,12 @@ abstract class AbstractTest extends \PHPUnit\Framework\TestCase {
$body = http_build_query($body, "a", "&"); $body = http_build_query($body, "a", "&");
} }
} }
// add any override values to the $_SERVER array
$server = array_merge($server, $vars); $server = array_merge($server, $vars);
// create the request
$req = new ServerRequest($server, [], $url, $method, "php://memory", [], [], $params, $parsedBody); $req = new ServerRequest($server, [], $url, $method, "php://memory", [], [], $params, $parsedBody);
// if a user is specified, act as if they were authenticated by the global handler
// the empty string denotes failed authentication
if (isset($user)) { if (isset($user)) {
if (strlen($user)) { if (strlen($user)) {
$req = $req->withAttribute("authenticated", true)->withAttribute("authenticatedUser", $user); $req = $req->withAttribute("authenticated", true)->withAttribute("authenticatedUser", $user);
@ -105,9 +123,11 @@ abstract class AbstractTest extends \PHPUnit\Framework\TestCase {
$req = $req->withAttribute("authenticationFailed", true); $req = $req->withAttribute("authenticationFailed", true);
} }
} }
if (strlen($type) &&strlen($body ?? "")) { // if a content type was specified, add it as a header
if (strlen($type)) {
$req = $req->withHeader("Content-Type", $type); $req = $req->withHeader("Content-Type", $type);
} }
// add any other headers
foreach ($headers as $key => $value) { foreach ($headers as $key => $value) {
if (!is_null($value)) { if (!is_null($value)) {
$req = $req->withHeader($key, $value); $req = $req->withHeader($key, $value);
@ -115,13 +135,16 @@ abstract class AbstractTest extends \PHPUnit\Framework\TestCase {
$req = $req->withoutHeader($key); $req = $req->withoutHeader($key);
} }
} }
// strip the URL prefix from the request target, as the global handler does
$target = substr(URL::normalize($url), strlen($urlPrefix)); $target = substr(URL::normalize($url), strlen($urlPrefix));
$req = $req->withRequestTarget($target); $req = $req->withRequestTarget($target);
// add the body text, if any
if (strlen($body ?? "")) { if (strlen($body ?? "")) {
$p = $req->getBody(); $p = $req->getBody();
$p->write($body); $p->write($body);
$req = $req->withBody($p); $req = $req->withBody($p);
} }
// return the prepared request
return $req; return $req;
} }