Implemented access control handling.

This commit is contained in:
flash 2025-03-19 21:24:52 +00:00
parent c481a9eb3a
commit d54ca02b38
Signed by: flash
GPG key ID: 2C9C2C574D47FE3E
10 changed files with 1289 additions and 106 deletions

View file

@ -1 +1 @@
0.2503.150248
0.2503.192123

42
composer.lock generated
View file

@ -1034,16 +1034,16 @@
},
{
"name": "phpunit/php-code-coverage",
"version": "12.0.4",
"version": "12.1.0",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
"reference": "79e5ef5897068689c7c325554d6df905480ce942"
"reference": "d331a5ced3d9a2b917baa9841b2211e72f9e780d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/79e5ef5897068689c7c325554d6df905480ce942",
"reference": "79e5ef5897068689c7c325554d6df905480ce942",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/d331a5ced3d9a2b917baa9841b2211e72f9e780d",
"reference": "d331a5ced3d9a2b917baa9841b2211e72f9e780d",
"shasum": ""
},
"require": {
@ -1070,7 +1070,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "12.0.x-dev"
"dev-main": "12.1.x-dev"
}
},
"autoload": {
@ -1099,7 +1099,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
"security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy",
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.0.4"
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.1.0"
},
"funding": [
{
@ -1107,7 +1107,7 @@
"type": "github"
}
],
"time": "2025-02-25T13:27:48+00:00"
"time": "2025-03-17T13:56:07+00:00"
},
{
"name": "phpunit/php-file-iterator",
@ -1356,16 +1356,16 @@
},
{
"name": "phpunit/phpunit",
"version": "12.0.7",
"version": "12.0.9",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "2845e49082ef7acc4a71a2ef71bbf32f31da22c9"
"reference": "7835bb4276780e0bbb385ce0a777b839e03096db"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/2845e49082ef7acc4a71a2ef71bbf32f31da22c9",
"reference": "2845e49082ef7acc4a71a2ef71bbf32f31da22c9",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/7835bb4276780e0bbb385ce0a777b839e03096db",
"reference": "7835bb4276780e0bbb385ce0a777b839e03096db",
"shasum": ""
},
"require": {
@ -1379,7 +1379,7 @@
"phar-io/manifest": "^2.0.4",
"phar-io/version": "^3.2.1",
"php": ">=8.3",
"phpunit/php-code-coverage": "^12.0.4",
"phpunit/php-code-coverage": "^12.1.0",
"phpunit/php-file-iterator": "^6.0.0",
"phpunit/php-invoker": "^6.0.0",
"phpunit/php-text-template": "^5.0.0",
@ -1391,7 +1391,7 @@
"sebastian/exporter": "^7.0.0",
"sebastian/global-state": "^8.0.0",
"sebastian/object-enumerator": "^7.0.0",
"sebastian/type": "^6.0.0",
"sebastian/type": "^6.0.2",
"sebastian/version": "^6.0.0",
"staabm/side-effects-detector": "^1.0.5"
},
@ -1433,7 +1433,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
"source": "https://github.com/sebastianbergmann/phpunit/tree/12.0.7"
"source": "https://github.com/sebastianbergmann/phpunit/tree/12.0.9"
},
"funding": [
{
@ -1449,7 +1449,7 @@
"type": "tidelift"
}
],
"time": "2025-03-07T07:32:22+00:00"
"time": "2025-03-19T13:47:33+00:00"
},
{
"name": "sebastian/cli-parser",
@ -2155,16 +2155,16 @@
},
{
"name": "sebastian/type",
"version": "6.0.0",
"version": "6.0.2",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/type.git",
"reference": "533fe082889a616f330bcba6f50965135f4f2fab"
"reference": "1d7cd6e514384c36d7a390347f57c385d4be6069"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/type/zipball/533fe082889a616f330bcba6f50965135f4f2fab",
"reference": "533fe082889a616f330bcba6f50965135f4f2fab",
"url": "https://api.github.com/repos/sebastianbergmann/type/zipball/1d7cd6e514384c36d7a390347f57c385d4be6069",
"reference": "1d7cd6e514384c36d7a390347f57c385d4be6069",
"shasum": ""
},
"require": {
@ -2200,7 +2200,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/type/issues",
"security": "https://github.com/sebastianbergmann/type/security/policy",
"source": "https://github.com/sebastianbergmann/type/tree/6.0.0"
"source": "https://github.com/sebastianbergmann/type/tree/6.0.2"
},
"funding": [
{
@ -2208,7 +2208,7 @@
"type": "github"
}
],
"time": "2025-02-07T05:00:19+00:00"
"time": "2025-03-18T13:37:31+00:00"
},
{
"name": "sebastian/version",

View file

@ -0,0 +1,170 @@
<?php
// AccessControl.php
// Created: 2025-03-19
// Updated: 2025-03-19
namespace Index\Http\Routing\AccessControl;
use Attribute;
use Closure;
use ReflectionAttribute;
use ReflectionFunction;
use RuntimeException;
use Index\XArray;
use Index\Http\Routing\Routes\RouteInfo;
/**
* Contains access control info for cross origin requests.
*/
#[Attribute(Attribute::TARGET_FUNCTION | Attribute::TARGET_METHOD)]
final class AccessControl {
/**
* Headers that are always allowed regardless of Access-Control-Allow-Headers.
* Also included the CORS ones cus they basically are anyway.
*
* @see https://developer.mozilla.org/en-US/docs/Glossary/CORS-safelisted_response_header
* @var string[]
*/
public const array HEADER_SAFELIST = [
'Access-Control-Allow-Headers',
'Access-Control-Allow-Methods',
'Access-Control-Allow-Origin',
'Access-Control-Expose-Headers',
'Access-Control-Request-Headers',
'Access-Control-Request-Method',
'Cache-Control',
'Content-Language',
'Content-Length',
'Content-Type',
'Expires',
'Last-Modified',
'Pragma',
];
/**
* @param bool $allow Whether to allow cross origin requests for this route.
* If false, everything else will be ignored since there's no need to specify it.
* Provided if necessary to explicitly deny.
* @param bool $credentials Whether to permit the client to include the credentials for our domain in requests using withCredentials = true.
* @param string[]|true $allowMethods What additional methods to allow for this route. true function as wildcard.
* Setting $allow to true will already include the $method provided in the RouteInfo constructor.
* @param string[]|true $allowHeaders What headers to allow for this route. true functions as wildcard.
* @param string[]|true $exposeHeaders What headers to expose to the remote origin. true function as wildcard, false as an explicit empty list.
* In addition to Cache-Control, Content-Language, Content-Length, Content-Type, Expires, Last-Modified and Pragma.
* false and empty array are seemingly swapped in terms of functionality at first glance
* but i think having false represent an explicit Nothing will be nicer in the long run since it changes the type entirely
*/
public function __construct(
public private(set) bool $allow = true,
public private(set) bool $credentials = false,
public private(set) array|true $allowMethods = [],
public private(set) array|true $allowHeaders = [],
public private(set) array|true $exposeHeaders = [],
) {}
/**
* Verifies if provided headers are allowed.
*
* @param string[] $headers Request headers.
* @return bool
*/
public function verifyHeadersAllowed(array $headers): bool {
return $this->allowHeaders === true || XArray::all($headers, fn($name) => (
XArray::any(self::HEADER_SAFELIST, fn($safeName) => strcasecmp($safeName, $name) === 0)
|| XArray::any($this->allowHeaders, fn($allowName) => strcasecmp($allowName, $name) === 0)
));
}
/**
* Reads methods on a provided Closure and creates or amends an AccessControl object.
*
* @param Closure $closure Closure to read attributes from.
* @param ?AccessControl $info Base access control info object.
* @return ?AccessControl
*/
public static function read(Closure $closure, ?AccessControl $info): ?AccessControl {
$attrs = (new ReflectionFunction($closure))->getAttributes(self::class, ReflectionAttribute::IS_INSTANCEOF);
$attr = array_shift($attrs);
if($attr === null)
return $info;
$attr = $attr->newInstance();
// explicit allow === false always takes precedence over everything else
if(!$attr->allow)
return $attr;
if($info !== null) {
// see the above
if(!$info->allow)
return $info;
// false is default so true takes precedence
// maybe not the best strat but its the one you're getting
if($info->credentials)
$attr->credentials = true;
if($attr->allowMethods !== true)
$attr->allowMethods = $info->allowMethods === true
? true : array_values(array_unique(array_merge($attr->allowMethods, $info->allowMethods)));
if($attr->allowHeaders !== true)
$attr->allowHeaders = $info->allowHeaders === true
? true : array_values(array_unique(array_merge($attr->allowHeaders, $info->allowHeaders)));
if($attr->exposeHeaders !== true)
$attr->exposeHeaders = $info->exposeHeaders === true
? true : array_values(array_unique(array_merge($attr->exposeHeaders, $info->exposeHeaders)));
}
return $attr;
}
/**
* Aggregrates access control declarations for multiple
*
* @param array<object{info: RouteInfo}|RouteInfo> $routes Routes that were caught for this preflight request.
* @param string $requestMethod Intended request method for this preflight.
* @return AccessControl Aggregated access control info.
*/
public static function aggregate(array $routes, string $requestMethod): AccessControl {
$allow = false;
$credentials = false;
$allowMethods = [];
$allowHeaders = [];
$exposeHeaders = [];
foreach($routes as $routeInfo) {
if(!($routeInfo instanceof RouteInfo)) {
if(property_exists($routeInfo, 'info') && $routeInfo->info instanceof RouteInfo)
$routeInfo = $routeInfo->info;
else continue;
}
$accessControl = $routeInfo->accessControl;
if($accessControl?->allow !== true)
continue;
$allow = true;
if($allowMethods !== true)
if($accessControl->allowMethods === true) {
$allowMethods = true;
} else {
$allowMethods[] = $routeInfo->method;
if(!empty($accessControl->allowMethods))
$allowMethods = array_merge($allowMethods, $accessControl->allowMethods);
}
if($routeInfo->method === $requestMethod) {
if($accessControl->credentials)
$credentials = true;
$allowHeaders = $accessControl->allowHeaders;
$exposeHeaders = $accessControl->exposeHeaders;
}
}
return new AccessControl($allow, $credentials, $allowMethods, $allowHeaders, $exposeHeaders);
}
}

View file

@ -0,0 +1,52 @@
<?php
// AccessControlHandler.php
// Created: 2025-03-19
// Updated: 2025-03-19
namespace Index\Http\Routing\AccessControl;
use Index\XArray;
use Index\Http\HttpUri;
use Index\Http\Routing\HandlerContext;
use Index\Http\Routing\Routes\RouteInfo;
/**
* CORS handler interface.
*/
interface AccessControlHandler {
/**
* Aggregates info needed for built-in HTTP OPTIONS method handler.
*
* @param HandlerContext $context Route handler context.
* @param AccessControl $accessControl Aggregated access control rules.
* @param RouteInfo[] $routes Routes that this OPTIONS request concern.
* @param HttpUri $requestOrigin HTTP request Origin header value.
* @param string $requestMethod Intended HTTP request method.
* @param string[]|null $requestHeaders Intended HTTP request headers.
* @return AccessControlPreflight
*/
public function handlePreflight(
HandlerContext $context,
AccessControl $accessControl,
array $routes,
HttpUri $requestOrigin,
string $requestMethod,
array|null $requestHeaders,
): AccessControlPreflight;
/**
* Adds CORS headers to a request if applicable.
*
* @param HandlerContext $context Route handler context.
* @param RouteInfo $routeInfo Route info.
* @param AccessControl $accessControl Access control for the given route info.
* @param HttpUri $requestOrigin HTTP request Origin header value.
* @return AccessControlResult
*/
public function handleRequest(
HandlerContext $context,
RouteInfo $routeInfo,
AccessControl $accessControl,
HttpUri $requestOrigin,
): AccessControlResult;
}

View file

@ -0,0 +1,69 @@
<?php
// AccessControlPreflight.php
// Created: 2025-03-19
// Updated: 2025-03-19
namespace Index\Http\Routing\AccessControl;
use Index\Http\HttpResponseBuilder;
/**
* Contains the result for a preflight request.
*/
class AccessControlPreflight {
/**
* @param string|bool $origin Origin to use in the Access-Control-Allow-Origin header, true for *, false for deny.
* @param string[]|true $methods Methods to use in the Access-Control-Allow-Methods header, true for *.
* @param string[]|true|null $headers Headers to use in the Access-Control-Allow-Headers header, true for *, null for none.
* @param bool $credentials Value for the Access-Control-Allow-Credentials header.
* @param ?int $ttl Override value for Access-Control-Max-Age header, null for default supplied in AccessControlHandler constructor.
*/
public function __construct(
public private(set) string|bool $origin = false,
public private(set) array|true $methods = [],
public private(set) array|true|null $headers = null,
public private(set) bool $credentials = false,
public private(set) ?int $ttl = null,
) {}
/**
* Applies this collection of headers to a HttpResponseBuilder.
*
* @param HttpResponseBuilder $response Response to apply to.
* @param int $ttl Default TTL value, if $ttl is specified in the constructor it should be overridden.
*/
public function applyPreflight(HttpResponseBuilder $response, int $ttl): void {
if($this->origin === false)
return;
if($this->credentials)
$response->setHeader('Access-Control-Allow-Credentials', 'true');
if($this->headers !== null) {
if($this->headers === true)
$response->setHeader('Access-Control-Allow-Headers', '*');
else {
$headers = array_unique($this->headers);
sort($headers);
$response->setHeader('Access-Control-Allow-Headers', implode(', ', $headers));
}
}
if($this->methods === true) {
$response->setHeader('Access-Control-Allow-Methods', '*');
} elseif(!empty($this->methods)) {
$methods = array_unique($this->methods);
sort($methods);
$response->setHeader('Access-Control-Allow-Methods', implode(', ', $methods));
}
if($this->origin === true) {
$response->setHeader('Access-Control-Allow-Origin', '*');
} else {
$response->addVary('Origin');
$response->setHeader('Access-Control-Allow-Origin', $this->origin);
}
$response->setHeader('Access-Control-Max-Age', (string)($this->ttl ?? $ttl));
}
}

View file

@ -0,0 +1,54 @@
<?php
// AccessControlResult.php
// Created: 2025-03-19
// Updated: 2025-03-19
namespace Index\Http\Routing\AccessControl;
use Index\Http\HttpResponseBuilder;
/**
* Contains the result for a normal request.
*/
class AccessControlResult {
/**
* @param string|bool $origin Origin to use in the Access-Control-Allow-Origin header, true for *, false for deny.
* @param string[]|bool $headers Headers to use in the Access-Control-Expose-Headers header, true for *, false for none.
* @param bool $credentials Value for the Access-Control-Allow-Credentials header.
*/
public function __construct(
public private(set) string|bool $origin = false,
public private(set) array|bool $headers = [],
public private(set) bool $credentials = false,
) {}
/**
* Applies this collection of headers to a HttpResponseBuilder.
*
* @param HttpResponseBuilder $response Response to apply to.
*/
public function applyResult(HttpResponseBuilder $response): void {
if($this->origin === false)
return;
if($this->credentials)
$response->setHeader('Access-Control-Allow-Credentials', 'true');
if($this->origin === true) {
$response->setHeader('Access-Control-Allow-Origin', '*');
} else {
$response->addVary('Origin');
$response->setHeader('Access-Control-Allow-Origin', $this->origin);
}
if($this->headers === true)
$response->setHeader('Access-Control-Expose-Headers', '*');
elseif($this->headers === false)
$response->setHeader('Access-Control-Expose-Headers', '');
elseif(!empty($this->headers)) {
$headers = array_unique($this->headers);
sort($headers);
$response->setHeader('Access-Control-Expose-Headers', implode(', ', $headers));
}
}
}

View file

@ -0,0 +1,64 @@
<?php
// SimpleAccessControlHandler.php
// Created: 2025-03-19
// Updated: 2025-03-19
namespace Index\Http\Routing\AccessControl;
use Index\XArray;
use Index\Http\HttpUri;
use Index\Http\Routing\HandlerContext;
use Index\Http\Routing\Routes\RouteInfo;
/**
* Base CORS handler.
*
* Override checkAccess to make this not function as a wildcard.
*/
class SimpleAccessControlHandler implements AccessControlHandler {
public function checkAccess(
HandlerContext $context,
AccessControl $accessControl,
HttpUri $origin,
?RouteInfo $routeInfo = null,
): string|bool {
return $accessControl->credentials ? (string)$origin : true;
}
public function handlePreflight(
HandlerContext $context,
AccessControl $accessControl,
array $routes,
HttpUri $requestOrigin,
string $requestMethod,
array|null $requestHeaders,
): AccessControlPreflight {
$requestOrigin = $this->checkAccess($context, $accessControl, $requestOrigin);
if($requestOrigin === false)
return new AccessControlPreflight;
return new AccessControlPreflight(
$requestOrigin,
$accessControl->allowMethods,
$requestHeaders === null ? null : ($accessControl->allowHeaders === true ? true : $requestHeaders),
$accessControl->credentials,
);
}
public function handleRequest(
HandlerContext $context,
RouteInfo $routeInfo,
AccessControl $accessControl,
HttpUri $requestOrigin,
): AccessControlResult {
$requestOrigin = $this->checkAccess($context, $accessControl, $requestOrigin, $routeInfo);
if($requestOrigin === false)
return new AccessControlResult;
return new AccessControlResult(
$requestOrigin,
$accessControl->exposeHeaders,
$accessControl->credentials,
);
}
}

View file

@ -1,7 +1,7 @@
<?php
// Router.php
// Created: 2024-03-28
// Updated: 2025-03-15
// Updated: 2025-03-19
namespace Index\Http\Routing;
@ -9,8 +9,10 @@ use InvalidArgumentException;
use JsonSerializable;
use RuntimeException;
use Stringable;
use Index\XArray;
use Index\Bencode\{Bencode,BencodeSerializable};
use Index\Http\HttpRequest;
use Index\Http\{HttpRequest,HttpUri};
use Index\Http\Routing\AccessControl\{AccessControl,AccessControlHandler,SimpleAccessControlHandler};
use Index\Http\Routing\ErrorHandling\{ErrorHandler,HtmlErrorHandler,PlainErrorHandler};
use Index\Http\Routing\Filters\FilterInfo;
use Index\Http\Routing\Processors\{ProcessorInfo,ProcessorTarget};
@ -46,22 +48,32 @@ class Router implements RequestHandlerInterface {
private array $postprocs = [];
/**
* @param ErrorHandler|'html'|'plain' $errorHandler Error handling to use for error responses with an empty body. 'html' for the default HTML implementation, 'plain' for the plaintext implementation.
* @param bool $registerDefaultProcessors Whether the default processors should be registered.
* Access control handler instance.
*/
public function __construct(
ErrorHandler|string $errorHandler = 'html',
bool $registerDefaultProcessors = true
) {
$this->setErrorHandler($errorHandler);
if($registerDefaultProcessors)
$this->register(new RouterProcessors);
}
public private(set) AccessControlHandler $accessControlHandler;
/**
* Error handler instance.
*/
public ErrorHandler $errorHandler;
public private(set) ErrorHandler $errorHandler;
/**
* @param ErrorHandler|'html'|'plain' $errorHandler Error handling to use for error responses with an empty body. 'html' for the default HTML implementation, 'plain' for the plaintext implementation.
* @param ?AccessControlHandler $accessControlHandler Handler for CORS requests.
* @param int $accessControlTtl Default Access-Control-Max-Age value.
* @param bool $registerDefaultProcessors Whether the default processors should be registered.
*/
public function __construct(
ErrorHandler|string $errorHandler = 'html',
?AccessControlHandler $accessControlHandler = null,
public private(set) int $accessControlTtl = 300,
bool $registerDefaultProcessors = true
) {
$this->setErrorHandler($errorHandler);
$this->accessControlHandler = $accessControlHandler ?? new SimpleAccessControlHandler;
if($registerDefaultProcessors)
$this->register(new RouterProcessors);
}
/**
* Sets an error handler.
@ -250,14 +262,17 @@ class Router implements RequestHandlerInterface {
}
}
/** @var array<string, object{info: RouteInfo, matches: ?array<int|string, mixed>}> */
$methods = [];
foreach($this->routes as $routeInfo) {
$match = $routeInfo->matcher->match($context->request->uri);
if($match !== false)
if($match !== false) {
$routeInfo->readAccessControl();
$methods[$routeInfo->method] = (object)[
'info' => $routeInfo,
'matches' => is_array($match) ? $match : null,
];
}
}
if(empty($methods)) {
@ -276,14 +291,39 @@ class Router implements RequestHandlerInterface {
$allow[] = 'OPTIONS';
if(in_array('GET', $allow))
$allow[] = 'HEAD';
$context->response->setAllow($allow);
$context->response->reasonPhrase = '';
if($context->request->method === 'OPTIONS') {
// this should include CORS stuff
// should have an entry in RouteInfo which get aggregated here, but also attributes
$context->response->statusCode = 204;
$context->response->statusCode = 200;
$context->response->setHeader('Content-Length', '0');
if($context->request->hasHeader('Origin')) {
$requestMethod = strtoupper($context->request->getHeaderLine('Access-Control-Request-Method'));
$routeInfos = array_map(fn($item) => $item->info, $methods);
$accessControl = AccessControl::aggregate($routeInfos, $requestMethod);
if($accessControl->allow) {
if($context->request->hasHeader('Access-Control-Request-Headers')) {
$requestHeaders = trim($context->request->getHeaderLine('Access-Control-Request-Headers'));
$requestHeaders = $requestHeaders === '' ? [] : XArray::select(
explode(',', $requestHeaders),
fn($part) => trim($part)
);
} else
$requestHeaders = null;
if($requestHeaders === null || $accessControl->verifyHeadersAllowed($requestHeaders))
$this->accessControlHandler->handlePreflight(
$context,
$accessControl,
$routeInfos,
HttpUri::createUri($context->request->getHeaderLine('Origin')),
$requestMethod,
$requestHeaders,
)->applyPreflight($context->response, $this->accessControlTtl);
}
}
} else {
$context->response->statusCode = 405;
$this->errorHandler->handle($context);
@ -295,6 +335,18 @@ class Router implements RequestHandlerInterface {
}
$route = $methods[$context->request->method];
// if you're overriding OPTIONS you're on your own
if($context->request->method !== 'OPTIONS'
&& $context->request->hasHeader('Origin')
&& $route->info->accessControl?->allow === true)
$this->accessControlHandler->handleRequest(
$context,
$route->info,
$route->info->accessControl,
HttpUri::createUri($context->request->getHeaderLine('Origin')),
)->applyResult($context->response);
$route->info->readProcessors();
foreach($route->info->preprocs as $name => $args) {

View file

@ -1,12 +1,13 @@
<?php
// RouteInfo.php
// Created: 2025-03-02
// Updated: 2025-03-07
// Updated: 2025-03-19
namespace Index\Http\Routing\Routes;
use Closure;
use InvalidArgumentException;
use Index\Http\Routing\AccessControl\AccessControl;
use Index\Http\Routing\Processors\ProcessAttribute;
use Index\Http\Routing\UriMatchers\{ExactPathUriMatcher,PatternPathUriMatcher,UriMatcher};
@ -33,6 +34,12 @@ class RouteInfo {
*/
public private(set) array $postprocs = [];
/**
* CORS info for this route.
*/
public ?AccessControl $accessControl = null;
private bool $aclRead = false;
private bool $processorsRead = false;
/**
@ -97,6 +104,17 @@ class RouteInfo {
$this->before($name, $args);
}
/**
* Reads access control attributes and merges them with the existing declarations.
*/
public function readAccessControl(): void {
if($this->aclRead)
return;
$this->aclRead = true;
$this->accessControl = AccessControl::read($this->handler, $this->accessControl);
}
/**
* Creates RouteInfo instance using ExactPathUriMatcher.
*

View file

@ -1,7 +1,7 @@
<?php
// RouterTest.php
// Created: 2022-01-20
// Updated: 2025-03-15
// Updated: 2025-03-19
declare(strict_types=1);
@ -10,6 +10,7 @@ use PHPUnit\Framework\Attributes\CoversClass;
use Index\Http\{HttpHeaders,HttpResponseBuilder,HttpRequest,HttpUri};
use Index\Http\Content\{FormContent,MultipartFormContent,UrlEncodedFormContent};
use Index\Http\Routing\{HandlerContext,RouteHandler,RouteHandlerCommon,Router};
use Index\Http\Routing\AccessControl\{AccessControl};
use Index\Http\Routing\Filters\{FilterInfo,PrefixFilter};
use Index\Http\Routing\Processors\{After,Before,Postprocessor,Preprocessor,ProcessorInfo};
use Index\Http\Routing\Routes\{ExactRoute,PatternRoute,RouteInfo};
@ -27,6 +28,7 @@ use Index\Http\Streams\Stream;
#[CoversClass(RouteHandlerCommon::class)]
#[CoversClass(PatternRoute::class)]
#[CoversClass(FilterInfo::class)]
#[CoversClass(AccessControl::class)]
final class RouterTest extends TestCase {
public function testRouter(): void {
$router1 = new Router;
@ -38,8 +40,8 @@ final class RouterTest extends TestCase {
$router1->route(RouteInfo::exact('PUT', '/', fn() => 'put'));
$router1->route(RouteInfo::exact('CUSTOM', '/', fn() => 'wacky'));
$this->assertEquals('get', (string)$router1->handle(HttpRequest::createRequestWithoutBody('GET', '/'))->getBody());
$this->assertEquals('wacky', (string)$router1->handle(HttpRequest::createRequestWithoutBody('CUSTOM', '/'))->getBody());
$this->assertSame('get', (string)$router1->handle(HttpRequest::createRequestWithoutBody('GET', '/'))->getBody());
$this->assertSame('wacky', (string)$router1->handle(HttpRequest::createRequestWithoutBody('CUSTOM', '/'))->getBody());
$router1->filter(FilterInfo::prefix('/', function() { /* this one intentionally does nothing */ }));
@ -53,7 +55,7 @@ final class RouterTest extends TestCase {
$router1->route(RouteInfo::pattern('GET', '#^/user/([A-Za-z0-9]+)$#uD', fn(string $user) => $user));
$router1->route(RouteInfo::pattern('GET', '#^/user/([A-Za-z0-9]+)/below$#uD', fn(string $user) => 'below ' . $user));
$this->assertEquals('warioware below static', (string)$router1->handle(HttpRequest::createRequestWithoutBody('GET', '/user/static/below'))->getBody());
$this->assertSame('warioware below static', (string)$router1->handle(HttpRequest::createRequestWithoutBody('GET', '/user/static/below'))->getBody());
$router2 = new Router;
$router2->filter(FilterInfo::prefix('/', fn() => 'meow'));
@ -61,7 +63,7 @@ final class RouterTest extends TestCase {
$router2->route(RouteInfo::exact('GET', '/contact', fn() => 'contact page'));
$router2->route(RouteInfo::exact('GET', '/25252', fn() => 'numeric test'));
$this->assertEquals('meow', (string)$router2->handle(HttpRequest::createRequestWithoutBody('GET', '/rules'))->getBody());
$this->assertSame('meow', (string)$router2->handle(HttpRequest::createRequestWithoutBody('GET', '/rules'))->getBody());
}
public function testAttribute(): void {
@ -117,13 +119,13 @@ final class RouterTest extends TestCase {
$router->register($handler);
$this->assertEquals('index', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/'))->getBody());
$this->assertEquals('avatar', (string)$router->handle(HttpRequest::createRequestWithoutBody('POST', '/avatar'))->getBody());
$this->assertEquals('static', (string)$router->handle(HttpRequest::createRequestWithoutBody('PUT', '/static'))->getBody());
$this->assertEquals('meow', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/meow'))->getBody());
$this->assertEquals('meow', (string)$router->handle(HttpRequest::createRequestWithoutBody('POST', '/meow'))->getBody());
$this->assertEquals('profile of Cool134', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/profile/Cool134'))->getBody());
$this->assertEquals('still the profile of Cool134', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/profile-but-raw/Cool134'))->getBody());
$this->assertSame('index', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/'))->getBody());
$this->assertSame('avatar', (string)$router->handle(HttpRequest::createRequestWithoutBody('POST', '/avatar'))->getBody());
$this->assertSame('static', (string)$router->handle(HttpRequest::createRequestWithoutBody('PUT', '/static'))->getBody());
$this->assertSame('meow', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/meow'))->getBody());
$this->assertSame('meow', (string)$router->handle(HttpRequest::createRequestWithoutBody('POST', '/meow'))->getBody());
$this->assertSame('profile of Cool134', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/profile/Cool134'))->getBody());
$this->assertSame('still the profile of Cool134', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/profile-but-raw/Cool134'))->getBody());
}
public function testEEPROMSituation(): void {
@ -132,8 +134,8 @@ final class RouterTest extends TestCase {
$router->route(RouteInfo::pattern('DELETE', '#^/uploads/([A-Za-z0-9\-_]+)$#uD', fn(string $id) => "Delete {$id}"));
// make sure both GET and DELETE are able to execute with a different pattern
$this->assertEquals('Get AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/uploads/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'))->getBody());
$this->assertEquals('Delete BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB', (string)$router->handle(HttpRequest::createRequestWithoutBody('DELETE', '/uploads/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB'))->getBody());
$this->assertSame('Get AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/uploads/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'))->getBody());
$this->assertSame('Delete BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB', (string)$router->handle(HttpRequest::createRequestWithoutBody('DELETE', '/uploads/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB'))->getBody());
}
public function testFilterInterceptionOnRoot(): void {
@ -142,9 +144,9 @@ final class RouterTest extends TestCase {
$router->route(RouteInfo::exact('GET', '/', fn() => 'unexpected'));
$router->route(RouteInfo::exact('GET', '/test', fn() => 'also unexpected'));
$this->assertEquals('expected', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/'))->getBody());
$this->assertEquals('expected', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/test'))->getBody());
$this->assertEquals('expected', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/error'))->getBody());
$this->assertSame('expected', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/'))->getBody());
$this->assertSame('expected', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/test'))->getBody());
$this->assertSame('expected', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/error'))->getBody());
}
public function testDefaultOptionsImplementation(): void {
@ -154,9 +156,10 @@ final class RouterTest extends TestCase {
$router->route(RouteInfo::exact('PATCH', '/test', fn() => 'patch'));
$response = $router->handle(HttpRequest::createRequestWithoutBody('OPTIONS', '/test'));
$this->assertEquals(204, $response->getStatusCode());
$this->assertEquals('No Content', $response->getReasonPhrase());
$this->assertEquals('GET, HEAD, OPTIONS, PATCH, POST', $response->getHeaderLine('Allow'));
$this->assertSame(200, $response->getStatusCode());
$this->assertSame('OK', $response->getReasonPhrase());
$this->assertSame('0', $response->getHeaderLine('Content-Length'));
$this->assertSame('GET, HEAD, OPTIONS, PATCH, POST', $response->getHeaderLine('Allow'));
}
public function testDefaultProcessorsNoRegister(): void {
@ -238,69 +241,69 @@ final class RouterTest extends TestCase {
return '<!doctype html><h1>ensuring autodetect gets skipped</h1>';
}
/** @return array{wow: string} */
#[After('output:json', flags: 0)]
#[ExactRoute('GET', '/output/json')]
/** @return array{wow: string} */
public function testOutputJson(): array {
return ['wow' => 'objects?? / epic!!'];
}
/** @return array{benben: int} */
#[After('output:bencode')]
#[ExactRoute('GET', '/output/bencode')]
/** @return array{benben: int} */
public function testOutputBencode(): array {
return ['benben' => 12345];
}
});
$response = $router->handle(HttpRequest::createRequestWithoutBody('GET', '/output/stream'));
$this->assertEquals('application/octet-stream', $response->getHeaderLine('Content-Type'));
$this->assertEquals('<!doctype html><h1>ensuring autodetect gets skipped</h1>', (string)$response->getBody());
$this->assertSame('application/octet-stream', $response->getHeaderLine('Content-Type'));
$this->assertSame('<!doctype html><h1>ensuring autodetect gets skipped</h1>', (string)$response->getBody());
$response = $router->handle(HttpRequest::createRequestWithoutBody('GET', '/output/plain'));
$this->assertEquals('text/plain;charset=utf-8', $response->getHeaderLine('Content-Type'));
$this->assertEquals('<!doctype html><h1>ensuring autodetect gets skipped</h1>', (string)$response->getBody());
$this->assertSame('text/plain;charset=utf-8', $response->getHeaderLine('Content-Type'));
$this->assertSame('<!doctype html><h1>ensuring autodetect gets skipped</h1>', (string)$response->getBody());
$response = $router->handle(HttpRequest::createRequestWithoutBody('GET', '/output/html'));
$this->assertEquals('text/html;charset=us-ascii', $response->getHeaderLine('Content-Type'));
$this->assertEquals('<?xml idk how xml opens the prefix is enough><beans></beans>', (string)$response->getBody());
$this->assertSame('text/html;charset=us-ascii', $response->getHeaderLine('Content-Type'));
$this->assertSame('<?xml idk how xml opens the prefix is enough><beans></beans>', (string)$response->getBody());
$response = $router->handle(HttpRequest::createRequestWithoutBody('GET', '/output/xml'));
$this->assertEquals('application/xml', $response->getHeaderLine('Content-Type'));
$this->assertEquals('soup', (string)$response->getBody());
$this->assertSame('application/xml', $response->getHeaderLine('Content-Type'));
$this->assertSame('soup', (string)$response->getBody());
$response = $router->handle(HttpRequest::createRequestWithoutBody('GET', '/output/css'));
$this->assertEquals('text/css', $response->getHeaderLine('Content-Type'));
$this->assertEquals('<!doctype html><h1>ensuring autodetect gets skipped</h1>', (string)$response->getBody());
$this->assertSame('text/css', $response->getHeaderLine('Content-Type'));
$this->assertSame('<!doctype html><h1>ensuring autodetect gets skipped</h1>', (string)$response->getBody());
$response = $router->handle(HttpRequest::createRequestWithoutBody('GET', '/output/js'));
$this->assertEquals('application/javascript', $response->getHeaderLine('Content-Type'));
$this->assertEquals('<!doctype html><h1>ensuring autodetect gets skipped</h1>', (string)$response->getBody());
$this->assertSame('application/javascript', $response->getHeaderLine('Content-Type'));
$this->assertSame('<!doctype html><h1>ensuring autodetect gets skipped</h1>', (string)$response->getBody());
$response = $router->handle(HttpRequest::createRequestWithoutBody('GET', '/output/json'));
$this->assertEquals('application/json', $response->getHeaderLine('Content-Type'));
$this->assertEquals('{"wow":"objects?? \/ epic!!"}', (string)$response->getBody());
$this->assertSame('application/json', $response->getHeaderLine('Content-Type'));
$this->assertSame('{"wow":"objects?? \/ epic!!"}', (string)$response->getBody());
$response = $router->handle(HttpRequest::createRequestWithoutBody('GET', '/output/bencode'));
$this->assertEquals('application/x-bittorrent', $response->getHeaderLine('Content-Type'));
$this->assertEquals('d6:benbeni12345ee', (string)$response->getBody());
$this->assertSame('application/x-bittorrent', $response->getHeaderLine('Content-Type'));
$this->assertSame('d6:benbeni12345ee', (string)$response->getBody());
$response = $router->handle(HttpRequest::createRequestWithoutBody('POST', '/optional-form'));
$this->assertEquals(200, $response->getStatusCode());
$this->assertEquals('none', (string)$response->getBody());
$this->assertSame(200, $response->getStatusCode());
$this->assertSame('none', (string)$response->getBody());
$response = $router->handle(HttpRequest::createRequestWithBody('POST', '/optional-form', [], Stream::createStream('test=mewow')));
$this->assertEquals(200, $response->getStatusCode());
$this->assertEquals('none', (string)$response->getBody());
$this->assertSame(200, $response->getStatusCode());
$this->assertSame('none', (string)$response->getBody());
$response = $router->handle(HttpRequest::createRequestWithBody('POST', '/optional-form', ['Content-Type' => ['application/x-www-form-urlencoded']], Stream::createStream('test=mewow')));
$this->assertEquals(200, $response->getStatusCode());
$this->assertEquals('urlencoded:mewow', (string)$response->getBody());
$this->assertSame(200, $response->getStatusCode());
$this->assertSame('urlencoded:mewow', (string)$response->getBody());
// an empty string is valid too
$response = $router->handle(HttpRequest::createRequestWithBody('POST', '/optional-form', ['Content-Type' => ['application/x-www-form-urlencoded']], Stream::createStream('')));
$this->assertEquals(200, $response->getStatusCode());
$this->assertEquals('urlencoded:', (string)$response->getBody());
$this->assertSame(200, $response->getStatusCode());
$this->assertSame('urlencoded:', (string)$response->getBody());
$response = $router->handle(HttpRequest::createRequestWithBody('POST', '/optional-form', ['Content-Type' => ['multipart/form-data; boundary="--soap12345"']], Stream::createStream(implode("\r\n", [
'----soap12345',
@ -310,25 +313,25 @@ final class RouterTest extends TestCase {
'----soap12345--',
'',
]))));
$this->assertEquals(200, $response->getStatusCode());
$this->assertEquals('multipart:wowof', (string)$response->getBody());
$this->assertSame(200, $response->getStatusCode());
$this->assertSame('multipart:wowof', (string)$response->getBody());
$response = $router->handle(HttpRequest::createRequestWithBody('POST', '/optional-form', ['Content-Type' => ['multipart/form-data; boundary="--soap12345"']], Stream::createStream(implode("\r\n", [
'----soap12345--',
'',
]))));
$this->assertEquals(200, $response->getStatusCode());
$this->assertEquals('multipart:', (string)$response->getBody());
$this->assertSame(200, $response->getStatusCode());
$this->assertSame('multipart:', (string)$response->getBody());
$response = $router->handle(HttpRequest::createRequestWithoutBody('POST', '/required-urlencoded'));
$this->assertEquals(400, $response->getStatusCode());
$this->assertSame(400, $response->getStatusCode());
$response = $router->handle(HttpRequest::createRequestWithoutBody('POST', '/required-multipart'));
$this->assertEquals(400, $response->getStatusCode());
$this->assertSame(400, $response->getStatusCode());
$response = $router->handle(HttpRequest::createRequestWithBody('POST', '/required-urlencoded', ['Content-Type' => ['application/x-www-form-urlencoded']], Stream::createStream('test=meow')));
$this->assertEquals(200, $response->getStatusCode());
$this->assertEquals('meow', (string)$response->getBody());
$this->assertSame(200, $response->getStatusCode());
$this->assertSame('meow', (string)$response->getBody());
$response = $router->handle(HttpRequest::createRequestWithBody('POST', '/required-multipart', ['Content-Type' => ['multipart/form-data; boundary="--soap56789"']], Stream::createStream(implode("\r\n", [
'----soap56789',
@ -338,11 +341,11 @@ final class RouterTest extends TestCase {
'----soap56789--',
'',
]))));
$this->assertEquals(200, $response->getStatusCode());
$this->assertEquals('woof', (string)$response->getBody());
$this->assertSame(200, $response->getStatusCode());
$this->assertSame('woof', (string)$response->getBody());
$response = $router->handle(HttpRequest::createRequestWithBody('POST', '/required-urlencoded', ['Content-Type' => ['multipart/form-data; boundary="--soap56789"']], Stream::createStream('test=meow')));
$this->assertEquals(400, $response->getStatusCode());
$this->assertSame(400, $response->getStatusCode());
$response = $router->handle(HttpRequest::createRequestWithBody('POST', '/required-multipart', ['Content-Type' => ['application/x-www-form-urlencoded']], Stream::createStream(implode("\r\n", [
'----soap56789',
@ -352,7 +355,7 @@ final class RouterTest extends TestCase {
'----soap56789--',
'',
]))));
$this->assertEquals(400, $response->getStatusCode());
$this->assertSame(400, $response->getStatusCode());
$response = $router->handle(HttpRequest::createRequestWithBody('POST', '/required-multipart', ['Content-Type' => ['multipart/form-data']], Stream::createStream(implode("\r\n", [
'----aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
@ -362,7 +365,7 @@ final class RouterTest extends TestCase {
'----aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa--',
'',
]))));
$this->assertEquals(400, $response->getStatusCode());
$this->assertSame(400, $response->getStatusCode());
}
public function testProcessors(): void {
@ -424,15 +427,716 @@ final class RouterTest extends TestCase {
));
$response = $router->handle(HttpRequest::createRequestWithBody('POST', '/test', [], Stream::createStream(base64_encode('mewow'))));
$this->assertEquals(200, $response->getStatusCode());
$this->assertEquals("{\n \"data\": \"mewow\"\n}", (string)$response->getBody());
$this->assertSame(200, $response->getStatusCode());
$this->assertSame("{\n \"data\": \"mewow\"\n}", (string)$response->getBody());
$response = $router->handle(HttpRequest::createRequestWithBody('PUT', '/alternate', [], Stream::createStream(base64_encode('soap'))));
$this->assertEquals(200, $response->getStatusCode());
$this->assertEquals("{\n \"path\": \"/alternate\",\n \"data\": \"soap\"\n}", (string)$response->getBody());
$this->assertSame(200, $response->getStatusCode());
$this->assertSame("{\n \"path\": \"/alternate\",\n \"data\": \"soap\"\n}", (string)$response->getBody());
$response = $router->handle(HttpRequest::createRequestWithoutBody('GET', '/filtered'));
$this->assertEquals(403, $response->getStatusCode());
$this->assertEquals('it can work like a filter too', (string)$response->getBody());
$this->assertSame(403, $response->getStatusCode());
$this->assertSame('it can work like a filter too', (string)$response->getBody());
}
public function testAccessControl(): void {
$router = new Router;
$router->register(new class implements RouteHandler {
use RouteHandlerCommon;
#[ExactRoute('GET', '/nothing')]
public function nothing(): void {}
#[AccessControl]
#[ExactRoute('GET', '/acld')]
public function getAccessControlDifferent(): void {}
#[ExactRoute('POST', '/acld')]
public function postAccessControlDifferent(): void {}
#[AccessControl]
#[ExactRoute('GET', '/copdodnop')]
public function getCredentialsOnPostDenyOnDeleteNothingOnPut(): void {}
#[AccessControl(credentials: true)]
#[ExactRoute('POST', '/copdodnop')]
public function postCredentialsOnPostDenyOnDeleteNothingOnPut(): void {}
#[AccessControl(allow: false)]
#[ExactRoute('DELETE', '/copdodnop')]
public function deleteCredentialsOnPostDenyOnDeleteNothingOnPut(): void {}
#[ExactRoute('PUT', '/copdodnop')]
public function putCredentialsOnPostDenyOnDeleteNothingOnPut(): void {}
#[AccessControl(allowMethods: ['HEAD'])]
#[ExactRoute('GET', '/methods/extra')]
public function getMethodsExtra(): void {}
#[AccessControl(allowHeaders: true)]
#[ExactRoute('GET', '/headers/allow-all')]
public function getHeadersAllowAll(): void {}
#[AccessControl(allowHeaders: ['Authorization', 'X-Content-Index', 'Content-Type'])]
#[ExactRoute('GET', '/headers/allow-specific')]
public function getHeadersAllowSpecific(): void {}
#[AccessControl]
#[ExactRoute('GET', '/headers/expose-safe')]
public function getHeadersExposeSafe(): void {}
#[AccessControl(exposeHeaders: true)]
#[ExactRoute('GET', '/headers/expose-all')]
public function getHeadersExposeAll(): void {}
#[AccessControl(exposeHeaders: ['X-EEPROM-Max-Size', 'Vary'])]
#[ExactRoute('GET', '/headers/expose-specific')]
public function getHeadersExposeSpecific(): void {}
// including a little curveball! i'm sure this won't lead to confusion
#[AccessControl(allow: true, credentials: true, allowMethods: ['POST', 'HEAD'], allowHeaders: ['Content-Length', 'X-Content-Index'], exposeHeaders: ['X-Satori-Time', 'X-Satori-Hash'])]
#[ExactRoute('GET', '/all-together-now')]
public function getAllTogetherNow(): void {}
#[ExactRoute('POST', '/all-together-now')]
public function postAllTogetherNow(): void {}
});
$routeWithNo = RouteInfo::exact(
'PUT', '/no-override',
#[AccessControl(allow: false)]
function(): void {}
);
$routeWithNo->accessControl = new AccessControl; // allow: true
$router->route($routeWithNo);
$response = $router->handle(HttpRequest::createRequestWithoutBody('OPTIONS', '/nothing'));
$this->assertSame(200, $response->getStatusCode());
$this->assertSame('0', $response->getHeaderLine('Content-Length'));
$this->assertSame('GET, HEAD, OPTIONS', $response->getHeaderLine('Allow'));
$response = $router->handle(HttpRequest::createRequestWithoutBody(
'OPTIONS', '/nothing',
[
'Origin' => ['https://railgun.sh'],
'Access-Control-Request-Method' => ['GET'],
],
));
$this->assertSame(200, $response->getStatusCode());
$this->assertSame('0', $response->getHeaderLine('Content-Length'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Credentials'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Headers'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Methods'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Origin'));
$this->assertFalse($response->hasHeader('Access-Control-Expose-Headers'));
$this->assertFalse($response->hasHeader('Access-Control-Max-Age'));
$response = $router->handle(HttpRequest::createRequestWithoutBody(
'GET', '/nothing',
[
'Origin' => ['https://railgun.sh'],
],
));
$this->assertSame(200, $response->getStatusCode());
$this->assertFalse($response->hasHeader('Content-Length'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Credentials'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Headers'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Methods'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Origin'));
$this->assertFalse($response->hasHeader('Access-Control-Expose-Headers'));
$this->assertFalse($response->hasHeader('Access-Control-Max-Age'));
$response = $router->handle(HttpRequest::createRequestWithoutBody(
'OPTIONS', '/no-override',
[
'Origin' => ['https://railgun.sh'],
'Access-Control-Request-Method' => ['PUT'],
],
));
$this->assertSame(200, $response->getStatusCode());
$this->assertSame('0', $response->getHeaderLine('Content-Length'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Credentials'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Headers'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Methods'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Origin'));
$this->assertFalse($response->hasHeader('Access-Control-Expose-Headers'));
$this->assertFalse($response->hasHeader('Access-Control-Max-Age'));
$response = $router->handle(HttpRequest::createRequestWithoutBody(
'PUT', '/no-override',
[
'Origin' => ['https://railgun.sh'],
],
));
$this->assertSame(200, $response->getStatusCode());
$this->assertFalse($response->hasHeader('Content-Length'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Credentials'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Headers'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Methods'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Origin'));
$this->assertFalse($response->hasHeader('Access-Control-Expose-Headers'));
$this->assertFalse($response->hasHeader('Access-Control-Max-Age'));
$response = $router->handle(HttpRequest::createRequestWithoutBody(
'OPTIONS', '/acld',
[
'Origin' => ['https://railgun.sh'],
'Access-Control-Request-Method' => ['GET'],
],
));
$this->assertSame(200, $response->getStatusCode());
$this->assertSame('0', $response->getHeaderLine('Content-Length'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Credentials'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Headers'));
$this->assertSame('GET', $response->getHeaderLine('Access-Control-Allow-Methods'));
$this->assertSame('*', $response->getHeaderLine('Access-Control-Allow-Origin'));
$this->assertFalse($response->hasHeader('Access-Control-Expose-Headers'));
$this->assertSame('300', $response->getHeaderLine('Access-Control-Max-Age'));
$response = $router->handle(HttpRequest::createRequestWithoutBody(
'GET', '/acld',
[
'Origin' => ['https://railgun.sh'],
],
));
$this->assertSame(200, $response->getStatusCode());
$this->assertFalse($response->hasHeader('Content-Length'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Credentials'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Headers'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Methods'));
$this->assertSame('*', $response->getHeaderLine('Access-Control-Allow-Origin'));
$this->assertFalse($response->hasHeader('Access-Control-Expose-Headers'));
$this->assertFalse($response->hasHeader('Access-Control-Max-Age'));
$response = $router->handle(HttpRequest::createRequestWithoutBody(
'POST', '/acld',
[
'Origin' => ['https://railgun.sh'],
],
));
$this->assertSame(200, $response->getStatusCode());
$this->assertFalse($response->hasHeader('Content-Length'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Credentials'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Headers'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Methods'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Origin'));
$this->assertFalse($response->hasHeader('Access-Control-Expose-Headers'));
$this->assertFalse($response->hasHeader('Access-Control-Max-Age'));
$response = $router->handle(HttpRequest::createRequestWithoutBody(
'OPTIONS', '/copdodnop',
[
'Origin' => ['https://railgun.sh'],
'Access-Control-Request-Method' => ['GET'],
],
));
$this->assertSame(200, $response->getStatusCode());
$this->assertSame('0', $response->getHeaderLine('Content-Length'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Credentials'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Headers'));
$this->assertSame('GET, POST', $response->getHeaderLine('Access-Control-Allow-Methods'));
$this->assertSame('*', $response->getHeaderLine('Access-Control-Allow-Origin'));
$this->assertFalse($response->hasHeader('Access-Control-Expose-Headers'));
$this->assertSame('300', $response->getHeaderLine('Access-Control-Max-Age'));
$response = $router->handle(HttpRequest::createRequestWithoutBody(
'GET', '/copdodnop',
[
'Origin' => ['https://railgun.sh'],
],
));
$this->assertSame(200, $response->getStatusCode());
$this->assertFalse($response->hasHeader('Content-Length'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Credentials'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Headers'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Methods'));
$this->assertSame('*', $response->getHeaderLine('Access-Control-Allow-Origin'));
$this->assertFalse($response->hasHeader('Access-Control-Expose-Headers'));
$this->assertFalse($response->hasHeader('Access-Control-Max-Age'));
$response = $router->handle(HttpRequest::createRequestWithoutBody(
'OPTIONS', '/copdodnop',
[
'Origin' => ['https://railgun.sh'],
'Access-Control-Request-Method' => ['POST'],
],
));
$this->assertSame(200, $response->getStatusCode());
$this->assertSame('0', $response->getHeaderLine('Content-Length'));
$this->assertSame('true', $response->getHeaderLine('Access-Control-Allow-Credentials'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Headers'));
$this->assertSame('GET, POST', $response->getHeaderLine('Access-Control-Allow-Methods'));
$this->assertSame('https://railgun.sh', $response->getHeaderLine('Access-Control-Allow-Origin'));
$this->assertFalse($response->hasHeader('Access-Control-Expose-Headers'));
$this->assertSame('300', $response->getHeaderLine('Access-Control-Max-Age'));
$response = $router->handle(HttpRequest::createRequestWithoutBody(
'POST', '/copdodnop',
[
'Origin' => ['https://railgun.sh'],
],
));
$this->assertSame(200, $response->getStatusCode());
$this->assertFalse($response->hasHeader('Content-Length'));
$this->assertSame('true', $response->getHeaderLine('Access-Control-Allow-Credentials'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Headers'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Methods'));
$this->assertSame('https://railgun.sh', $response->getHeaderLine('Access-Control-Allow-Origin'));
$this->assertFalse($response->hasHeader('Access-Control-Expose-Headers'));
$this->assertFalse($response->hasHeader('Access-Control-Max-Age'));
$response = $router->handle(HttpRequest::createRequestWithoutBody(
'OPTIONS', '/copdodnop',
[
'Origin' => ['https://railgun.sh'],
'Access-Control-Request-Method' => ['DELETE'],
],
));
$this->assertSame(200, $response->getStatusCode());
$this->assertSame('0', $response->getHeaderLine('Content-Length'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Credentials'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Headers'));
$this->assertSame('GET, POST', $response->getHeaderLine('Access-Control-Allow-Methods'));
$this->assertSame('*', $response->getHeaderLine('Access-Control-Allow-Origin'));
$this->assertFalse($response->hasHeader('Access-Control-Expose-Headers'));
$this->assertSame('300', $response->getHeaderLine('Access-Control-Max-Age'));
$response = $router->handle(HttpRequest::createRequestWithoutBody(
'DELETE', '/copdodnop',
[
'Origin' => ['https://railgun.sh'],
],
));
$this->assertSame(200, $response->getStatusCode());
$this->assertFalse($response->hasHeader('Content-Length'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Credentials'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Headers'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Methods'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Origin'));
$this->assertFalse($response->hasHeader('Access-Control-Expose-Headers'));
$this->assertFalse($response->hasHeader('Access-Control-Max-Age'));
$response = $router->handle(HttpRequest::createRequestWithoutBody(
'OPTIONS', '/copdodnop',
[
'Origin' => ['https://railgun.sh'],
'Access-Control-Request-Method' => ['PUT'],
],
));
$this->assertSame(200, $response->getStatusCode());
$this->assertSame('0', $response->getHeaderLine('Content-Length'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Credentials'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Headers'));
$this->assertSame('GET, POST', $response->getHeaderLine('Access-Control-Allow-Methods'));
$this->assertSame('*', $response->getHeaderLine('Access-Control-Allow-Origin'));
$this->assertFalse($response->hasHeader('Access-Control-Expose-Headers'));
$this->assertSame('300', $response->getHeaderLine('Access-Control-Max-Age'));
$response = $router->handle(HttpRequest::createRequestWithoutBody(
'PUT', '/copdodnop',
[
'Origin' => ['https://railgun.sh'],
],
));
$this->assertSame(200, $response->getStatusCode());
$this->assertFalse($response->hasHeader('Content-Length'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Credentials'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Headers'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Methods'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Origin'));
$this->assertFalse($response->hasHeader('Access-Control-Expose-Headers'));
$this->assertFalse($response->hasHeader('Access-Control-Max-Age'));
$response = $router->handle(HttpRequest::createRequestWithoutBody(
'OPTIONS', '/methods/extra',
[
'Origin' => ['https://railgun.sh'],
'Access-Control-Request-Method' => ['HEAD'],
],
));
$this->assertSame(200, $response->getStatusCode());
$this->assertSame('0', $response->getHeaderLine('Content-Length'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Credentials'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Headers'));
$this->assertSame('GET, HEAD', $response->getHeaderLine('Access-Control-Allow-Methods'));
$this->assertSame('*', $response->getHeaderLine('Access-Control-Allow-Origin'));
$this->assertFalse($response->hasHeader('Access-Control-Expose-Headers'));
$this->assertSame('300', $response->getHeaderLine('Access-Control-Max-Age'));
$response = $router->handle(HttpRequest::createRequestWithoutBody(
'GET', '/methods/extra',
[
'Origin' => ['https://railgun.sh'],
],
));
$this->assertSame(200, $response->getStatusCode());
$this->assertFalse($response->hasHeader('Content-Length'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Credentials'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Headers'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Methods'));
$this->assertSame('*', $response->getHeaderLine('Access-Control-Allow-Origin'));
$this->assertFalse($response->hasHeader('Access-Control-Expose-Headers'));
$this->assertFalse($response->hasHeader('Access-Control-Max-Age'));
$response = $router->handle(HttpRequest::createRequestWithoutBody(
'HEAD', '/methods/extra',
[
'Origin' => ['https://railgun.sh'],
],
));
$this->assertSame(200, $response->getStatusCode());
$this->assertFalse($response->hasHeader('Content-Length'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Credentials'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Headers'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Methods'));
$this->assertSame('*', $response->getHeaderLine('Access-Control-Allow-Origin'));
$this->assertFalse($response->hasHeader('Access-Control-Expose-Headers'));
$this->assertFalse($response->hasHeader('Access-Control-Max-Age'));
$response = $router->handle(HttpRequest::createRequestWithoutBody(
'OPTIONS', '/headers/allow-all',
[
'Origin' => ['https://railgun.sh'],
'Access-Control-Request-Method' => ['GET'],
],
));
$this->assertSame(200, $response->getStatusCode());
$this->assertSame('0', $response->getHeaderLine('Content-Length'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Credentials'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Headers'));
$this->assertSame('GET', $response->getHeaderLine('Access-Control-Allow-Methods'));
$this->assertSame('*', $response->getHeaderLine('Access-Control-Allow-Origin'));
$this->assertFalse($response->hasHeader('Access-Control-Expose-Headers'));
$this->assertSame('300', $response->getHeaderLine('Access-Control-Max-Age'));
$response = $router->handle(HttpRequest::createRequestWithoutBody(
'OPTIONS', '/headers/allow-all',
[
'Origin' => ['https://railgun.sh'],
'Access-Control-Request-Method' => ['GET'],
'Access-Control-Request-Headers' => ['X-Soap'],
],
));
$this->assertSame(200, $response->getStatusCode());
$this->assertSame('0', $response->getHeaderLine('Content-Length'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Credentials'));
$this->assertSame('*', $response->getHeaderLine('Access-Control-Allow-Headers'));
$this->assertSame('GET', $response->getHeaderLine('Access-Control-Allow-Methods'));
$this->assertSame('*', $response->getHeaderLine('Access-Control-Allow-Origin'));
$this->assertFalse($response->hasHeader('Access-Control-Expose-Headers'));
$this->assertSame('300', $response->getHeaderLine('Access-Control-Max-Age'));
$response = $router->handle(HttpRequest::createRequestWithoutBody(
'GET', '/headers/allow-all',
[
'Origin' => ['https://railgun.sh'],
],
));
$this->assertSame(200, $response->getStatusCode());
$this->assertFalse($response->hasHeader('Content-Length'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Credentials'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Headers'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Methods'));
$this->assertSame('*', $response->getHeaderLine('Access-Control-Allow-Origin'));
$this->assertFalse($response->hasHeader('Access-Control-Expose-Headers'));
$this->assertFalse($response->hasHeader('Access-Control-Max-Age'));
$response = $router->handle(HttpRequest::createRequestWithoutBody(
'OPTIONS', '/headers/allow-all',
[
'Origin' => ['https://railgun.sh'],
'Access-Control-Request-Method' => ['GET'],
'Access-Control-Request-Headers' => [''],
],
));
$this->assertSame(200, $response->getStatusCode());
$this->assertSame('0', $response->getHeaderLine('Content-Length'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Credentials'));
$this->assertSame('*', $response->getHeaderLine('Access-Control-Allow-Headers'));
$this->assertSame('GET', $response->getHeaderLine('Access-Control-Allow-Methods'));
$this->assertSame('*', $response->getHeaderLine('Access-Control-Allow-Origin'));
$this->assertFalse($response->hasHeader('Access-Control-Expose-Headers'));
$this->assertSame('300', $response->getHeaderLine('Access-Control-Max-Age'));
$response = $router->handle(HttpRequest::createRequestWithoutBody(
'GET', '/headers/allow-all',
[
'Origin' => ['https://railgun.sh'],
],
));
$this->assertSame(200, $response->getStatusCode());
$this->assertFalse($response->hasHeader('Content-Length'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Credentials'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Headers'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Methods'));
$this->assertSame('*', $response->getHeaderLine('Access-Control-Allow-Origin'));
$this->assertFalse($response->hasHeader('Access-Control-Expose-Headers'));
$this->assertFalse($response->hasHeader('Access-Control-Max-Age'));
$response = $router->handle(HttpRequest::createRequestWithoutBody(
'OPTIONS', '/headers/allow-specific',
[
'Origin' => ['https://railgun.sh'],
'Access-Control-Request-Method' => ['GET'],
],
));
$this->assertSame(200, $response->getStatusCode());
$this->assertSame('0', $response->getHeaderLine('Content-Length'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Credentials'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Headers'));
$this->assertSame('GET', $response->getHeaderLine('Access-Control-Allow-Methods'));
$this->assertSame('*', $response->getHeaderLine('Access-Control-Allow-Origin'));
$this->assertFalse($response->hasHeader('Access-Control-Expose-Headers'));
$this->assertSame('300', $response->getHeaderLine('Access-Control-Max-Age'));
$response = $router->handle(HttpRequest::createRequestWithoutBody(
'OPTIONS', '/headers/allow-specific',
[
'Origin' => ['https://railgun.sh'],
'Access-Control-Request-Method' => ['GET'],
'Access-Control-Request-Headers' => ['X-Content-Index', 'Authorization'],
],
));
$this->assertSame(200, $response->getStatusCode());
$this->assertSame('0', $response->getHeaderLine('Content-Length'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Credentials'));
$this->assertSame('Authorization, X-Content-Index', $response->getHeaderLine('Access-Control-Allow-Headers'));
$this->assertSame('GET', $response->getHeaderLine('Access-Control-Allow-Methods'));
$this->assertSame('*', $response->getHeaderLine('Access-Control-Allow-Origin'));
$this->assertFalse($response->hasHeader('Access-Control-Expose-Headers'));
$this->assertSame('300', $response->getHeaderLine('Access-Control-Max-Age'));
$response = $router->handle(HttpRequest::createRequestWithoutBody(
'OPTIONS', '/headers/allow-specific',
[
'Origin' => ['https://railgun.sh'],
'Access-Control-Request-Method' => ['GET'],
'Access-Control-Request-Headers' => ['X-Beans'],
],
));
$this->assertSame(200, $response->getStatusCode());
$this->assertSame('0', $response->getHeaderLine('Content-Length'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Credentials'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Headers'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Methods'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Origin'));
$this->assertFalse($response->hasHeader('Access-Control-Expose-Headers'));
$this->assertFalse($response->hasHeader('Access-Control-Max-Age'));
$response = $router->handle(HttpRequest::createRequestWithoutBody(
'GET', '/headers/allow-specific',
[
'Origin' => ['https://railgun.sh'],
],
));
$this->assertSame(200, $response->getStatusCode());
$this->assertFalse($response->hasHeader('Content-Length'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Credentials'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Headers'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Methods'));
$this->assertSame('*', $response->getHeaderLine('Access-Control-Allow-Origin'));
$this->assertFalse($response->hasHeader('Access-Control-Expose-Headers'));
$this->assertFalse($response->hasHeader('Access-Control-Max-Age'));
$response = $router->handle(HttpRequest::createRequestWithoutBody(
'OPTIONS', '/headers/expose-safe',
[
'Origin' => ['https://railgun.sh'],
'Access-Control-Request-Method' => ['GET'],
],
));
$this->assertSame(200, $response->getStatusCode());
$this->assertSame('0', $response->getHeaderLine('Content-Length'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Credentials'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Headers'));
$this->assertSame('GET', $response->getHeaderLine('Access-Control-Allow-Methods'));
$this->assertSame('*', $response->getHeaderLine('Access-Control-Allow-Origin'));
$this->assertFalse($response->hasHeader('Access-Control-Expose-Headers'));
$this->assertSame('300', $response->getHeaderLine('Access-Control-Max-Age'));
$response = $router->handle(HttpRequest::createRequestWithoutBody(
'GET', '/headers/expose-safe',
[
'Origin' => ['https://railgun.sh'],
],
));
$this->assertSame(200, $response->getStatusCode());
$this->assertFalse($response->hasHeader('Content-Length'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Credentials'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Headers'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Methods'));
$this->assertSame('*', $response->getHeaderLine('Access-Control-Allow-Origin'));
$this->assertFalse($response->hasHeader('Access-Control-Expose-Headers'));
$this->assertFalse($response->hasHeader('Access-Control-Max-Age'));
$response = $router->handle(HttpRequest::createRequestWithoutBody(
'OPTIONS', '/headers/expose-all',
[
'Origin' => ['https://railgun.sh'],
'Access-Control-Request-Method' => ['GET'],
],
));
$this->assertSame(200, $response->getStatusCode());
$this->assertSame('0', $response->getHeaderLine('Content-Length'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Credentials'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Headers'));
$this->assertSame('GET', $response->getHeaderLine('Access-Control-Allow-Methods'));
$this->assertSame('*', $response->getHeaderLine('Access-Control-Allow-Origin'));
$this->assertFalse($response->hasHeader('Access-Control-Expose-Headers'));
$this->assertSame('300', $response->getHeaderLine('Access-Control-Max-Age'));
$response = $router->handle(HttpRequest::createRequestWithoutBody(
'GET', '/headers/expose-all',
[
'Origin' => ['https://railgun.sh'],
],
));
$this->assertSame(200, $response->getStatusCode());
$this->assertFalse($response->hasHeader('Content-Length'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Credentials'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Headers'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Methods'));
$this->assertSame('*', $response->getHeaderLine('Access-Control-Allow-Origin'));
$this->assertTrue($response->hasHeader('Access-Control-Expose-Headers'));
$this->assertSame('*', $response->getHeaderLine('Access-Control-Expose-Headers'));
$this->assertFalse($response->hasHeader('Access-Control-Max-Age'));
$response = $router->handle(HttpRequest::createRequestWithoutBody(
'OPTIONS', '/headers/expose-specific',
[
'Origin' => ['https://railgun.sh'],
'Access-Control-Request-Method' => ['GET'],
],
));
$this->assertSame(200, $response->getStatusCode());
$this->assertSame('0', $response->getHeaderLine('Content-Length'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Credentials'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Headers'));
$this->assertSame('GET', $response->getHeaderLine('Access-Control-Allow-Methods'));
$this->assertSame('*', $response->getHeaderLine('Access-Control-Allow-Origin'));
$this->assertFalse($response->hasHeader('Access-Control-Expose-Headers'));
$this->assertSame('300', $response->getHeaderLine('Access-Control-Max-Age'));
$response = $router->handle(HttpRequest::createRequestWithoutBody(
'GET', '/headers/expose-specific',
[
'Origin' => ['https://railgun.sh'],
],
));
$this->assertSame(200, $response->getStatusCode());
$this->assertFalse($response->hasHeader('Content-Length'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Credentials'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Headers'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Methods'));
$this->assertSame('*', $response->getHeaderLine('Access-Control-Allow-Origin'));
$this->assertTrue($response->hasHeader('Access-Control-Expose-Headers'));
$this->assertSame('Vary, X-EEPROM-Max-Size', $response->getHeaderLine('Access-Control-Expose-Headers'));
$this->assertFalse($response->hasHeader('Access-Control-Max-Age'));
$response = $router->handle(HttpRequest::createRequestWithoutBody(
'OPTIONS', '/all-together-now',
[
'Origin' => ['https://flash.moe:8443'],
'Access-Control-Request-Method' => ['GET'],
],
));
$this->assertSame(200, $response->getStatusCode());
$this->assertSame('0', $response->getHeaderLine('Content-Length'));
$this->assertSame('true', $response->getHeaderLine('Access-Control-Allow-Credentials'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Headers'));
$this->assertSame('GET, HEAD, POST', $response->getHeaderLine('Access-Control-Allow-Methods'));
$this->assertSame('https://flash.moe:8443', $response->getHeaderLine('Access-Control-Allow-Origin'));
$this->assertFalse($response->hasHeader('Access-Control-Expose-Headers'));
$this->assertSame('300', $response->getHeaderLine('Access-Control-Max-Age'));
$response = $router->handle(HttpRequest::createRequestWithoutBody(
'OPTIONS', '/all-together-now',
[
'Origin' => ['https://flash.moe:8443'],
'Access-Control-Request-Method' => ['GET'],
'Access-Control-Request-Headers' => [''],
],
));
$this->assertSame(200, $response->getStatusCode());
$this->assertSame('0', $response->getHeaderLine('Content-Length'));
$this->assertSame('true', $response->getHeaderLine('Access-Control-Allow-Credentials'));
$this->assertTrue($response->hasHeader('Access-Control-Allow-Headers'));
$this->assertSame('', $response->getHeaderLine('Access-Control-Allow-Headers'));
$this->assertSame('GET, HEAD, POST', $response->getHeaderLine('Access-Control-Allow-Methods'));
$this->assertSame('https://flash.moe:8443', $response->getHeaderLine('Access-Control-Allow-Origin'));
$this->assertFalse($response->hasHeader('Access-Control-Expose-Headers'));
$this->assertSame('300', $response->getHeaderLine('Access-Control-Max-Age'));
$response = $router->handle(HttpRequest::createRequestWithoutBody(
'OPTIONS', '/all-together-now',
[
'Origin' => ['https://flash.moe:8443'],
'Access-Control-Request-Method' => ['GET'],
'Access-Control-Request-Headers' => ['x-content-index'],
],
));
$this->assertSame(200, $response->getStatusCode());
$this->assertSame('0', $response->getHeaderLine('Content-Length'));
$this->assertSame('true', $response->getHeaderLine('Access-Control-Allow-Credentials'));
$this->assertSame('x-content-index', $response->getHeaderLine('Access-Control-Allow-Headers'));
$this->assertSame('GET, HEAD, POST', $response->getHeaderLine('Access-Control-Allow-Methods'));
$this->assertSame('https://flash.moe:8443', $response->getHeaderLine('Access-Control-Allow-Origin'));
$this->assertFalse($response->hasHeader('Access-Control-Expose-Headers'));
$this->assertSame('300', $response->getHeaderLine('Access-Control-Max-Age'));
$response = $router->handle(HttpRequest::createRequestWithoutBody(
'GET', '/all-together-now',
[
'Origin' => ['https://flash.moe:8443'],
],
));
$this->assertSame(200, $response->getStatusCode());
$this->assertFalse($response->hasHeader('Content-Length'));
$this->assertSame('true', $response->getHeaderLine('Access-Control-Allow-Credentials'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Headers'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Methods'));
$this->assertSame('https://flash.moe:8443', $response->getHeaderLine('Access-Control-Allow-Origin'));
$this->assertSame('X-Satori-Hash, X-Satori-Time', $response->getHeaderLine('Access-Control-Expose-Headers'));
$this->assertFalse($response->hasHeader('Access-Control-Max-Age'));
// although routes can declare other methods than their own as CORS supported,
// allow credentials and headers ONLY apply to their own method
$response = $router->handle(HttpRequest::createRequestWithoutBody(
'OPTIONS', '/all-together-now',
[
'Origin' => ['https://flash.moe:8443'],
'Access-Control-Request-Method' => ['POST'],
],
));
$this->assertSame(200, $response->getStatusCode());
$this->assertSame('0', $response->getHeaderLine('Content-Length'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Credentials'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Headers'));
$this->assertSame('GET, HEAD, POST', $response->getHeaderLine('Access-Control-Allow-Methods'));
$this->assertSame('*', $response->getHeaderLine('Access-Control-Allow-Origin'));
$this->assertFalse($response->hasHeader('Access-Control-Expose-Headers'));
$this->assertSame('300', $response->getHeaderLine('Access-Control-Max-Age'));
$response = $router->handle(HttpRequest::createRequestWithoutBody(
'POST', '/all-together-now',
[
'Origin' => ['https://flash.moe:8443'],
],
));
$this->assertSame(200, $response->getStatusCode());
$this->assertFalse($response->hasHeader('Content-Length'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Credentials'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Headers'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Methods'));
$this->assertFalse($response->hasHeader('Access-Control-Allow-Origin'));
$this->assertFalse($response->hasHeader('Access-Control-Expose-Headers'));
$this->assertFalse($response->hasHeader('Access-Control-Max-Age'));
}
}