Skip to content

Commit

Permalink
Merge 17cdc21 into b7ea3dd
Browse files Browse the repository at this point in the history
  • Loading branch information
trowski committed Aug 6, 2019
2 parents b7ea3dd + 17cdc21 commit 2e24b9f
Show file tree
Hide file tree
Showing 3 changed files with 168 additions and 2 deletions.
6 changes: 4 additions & 2 deletions composer.json
Expand Up @@ -18,11 +18,13 @@
]
},
"require": {
"php": ">=7.1"
"php": ">=7.1",
"amphp/amp": "^2"
},
"require-dev": {
"phpunit/phpunit": "^6.5",
"amphp/php-cs-fixer-config": "dev-master"
"amphp/php-cs-fixer-config": "dev-master",
"amphp/phpunit-util": "^1.1"
},
"config": {
"platform": {
Expand Down
113 changes: 113 additions & 0 deletions src/Trailers.php
@@ -0,0 +1,113 @@
<?php

namespace Amp\Http;

use Amp\Deferred;
use Amp\Promise;

final class Trailers
{
/** @see https://tools.ietf.org/html/rfc7230#section-4.1.2 */
public const DISALLOWED_TRAILERS = [
"authorization" => 1,
"content-encoding" => 1,
"content-length" => 1,
"content-range" => 1,
"content-type" => 1,
"cookie" => 1,
"expect" => 1,
"host" => 1,
"pragma" => 1,
"proxy-authenticate" => 1,
"proxy-authorization" => 1,
"range" => 1,
"te" => 1,
"trailer" => 1,
"transfer-encoding" => 1,
"www-authenticate" => 1,
];

/** @var string[] */
private $fields = [];

/** @var Promise<Message> */
private $headers;

/**
* @param Promise<string[]|string[][]> $promise Resolved with the trailer values.
* @param string[] $fields Expected header fields. May be empty, but if provided, the array of headers
* used to resolve the given promise must contain exactly the fields given in
* this array.
*
* @throws InvalidHeaderException If the fields list contains a disallowed field.
*/
public function __construct(Promise $promise, array $fields = [])
{
if (!empty($fields)) {
$this->fields = $fields = \array_map('strtolower', $fields);

foreach ($this->fields as $field) {
if (isset(self::DISALLOWED_TRAILERS[$field])) {
throw new InvalidHeaderException(\sprintf("Field '%s' is not allowed in trailers", $field));
}
}
}

$deferred = new Deferred;

$promise->onResolve(static function (?\Throwable $exception, ?array $headers) use ($fields, $deferred): void {
if ($exception) {
$deferred->fail($exception);
return;
}

try {
$deferred->resolve(new class($headers, $fields) extends Message {
public function __construct(array $headers, array $fields)
{
$this->setHeaders($headers);

$keys = \array_keys($this->getHeaders());

if (!empty($fields)) {
// Note that the Trailer header does not need to be set for the message to include trailers.
// @see https://tools.ietf.org/html/rfc7230#section-4.4

if (\array_diff($fields, $keys)) {
throw new InvalidHeaderException("Trailers do not contain the expected fields");
}

return; // Check below unnecessary if fields list is set.
}

foreach ($keys as $field) {
if (isset(Trailers::DISALLOWED_TRAILERS[$field])) {
throw new InvalidHeaderException(\sprintf("Field '%s' is not allowed in trailers", $field));
}
}
}
});
} catch (\Throwable $exception) {
$deferred->fail($exception);
}
});

$this->headers = $deferred->promise();
}

/**
* @return string[] List of expected trailer fields. May be empty, but still receive trailers.
*/
public function getFields(): array
{
return $this->fields;
}

/**
* @return Promise<Message>
*/
public function getTrailers(): Promise
{
return $this->headers;
}
}
51 changes: 51 additions & 0 deletions test/TrailersTest.php
@@ -0,0 +1,51 @@
<?php

namespace Amp\Http\Test;

use Amp\Http\InvalidHeaderException;
use Amp\Http\Trailers;
use Amp\PHPUnit\AsyncTestCase;
use Amp\Success;

class TrailersTest extends AsyncTestCase
{
public function testMessageHasHeader()
{
$promise = new Success(['fooHeader' => 'barValue']);

$trailers = new Trailers($promise, ['fooHeader']);
$trailers = yield $trailers->getTrailers();

$this->assertTrue($trailers->hasHeader('fooHeader'));
$this->assertSame('barValue', $trailers->getHeader('fooHeader'));
}

public function testHasHeaderReturnsFalseForEmptyArrayValue()
{
$promise = new Success(['fooHeader' => []]);

$this->expectException(InvalidHeaderException::class);
$this->expectExceptionMessage('Trailers do not contain the expected fields');

$trailers = new Trailers($promise, ['fooHeader']);
$this->assertFalse((yield $trailers->getTrailers())->hasHeader('fooHeader'));
}

public function testDisallowedFieldsInConstructor()
{
$this->expectException(InvalidHeaderException::class);
$this->expectExceptionMessage("Field 'content-length' is not allowed in trailers");

$trailers = new Trailers(new Success, ['content-length']);
}

public function testDisallowedFieldsInPromiseResolution()
{
$this->expectException(InvalidHeaderException::class);
$this->expectExceptionMessage("Field 'content-length' is not allowed in trailers");

$trailers = new Trailers(new Success(['content-length' => 0]));

yield $trailers->getTrailers();
}
}

0 comments on commit 2e24b9f

Please sign in to comment.