Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
168 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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(); | ||
} | ||
} |