diff --git a/fixtures/unbounded-post.txt b/fixtures/unbounded-post.txt new file mode 100644 index 0000000..d732cb2 --- /dev/null +++ b/fixtures/unbounded-post.txt @@ -0,0 +1,9 @@ +POST /some-form HTTP/1.1 +User-Agent: Mozilla/4.0 (compatible; MSIE5.01; Windows NT) +Host: innmind.com +Content-Type: application/x-www-form-urlencoded +Accept-Language: fr-fr +Accept-Encoding: gzip, deflate +Connection: Keep-Alive + +some[key]=value&foo=bar diff --git a/src/Request/Buffer/Body.php b/src/Request/Buffer/Body.php index 9cb69b6..a17340c 100644 --- a/src/Request/Buffer/Body.php +++ b/src/Request/Buffer/Body.php @@ -8,6 +8,7 @@ Message\Method, ProtocolVersion, Headers, + Header\ContentLength, }; use Innmind\Filesystem\File\Content; use Innmind\Url\Url; @@ -22,19 +23,26 @@ final class Body implements State private Url $url; private ProtocolVersion $protocol; private Headers $headers; + /** @var Maybe<0|positive-int> */ + private Maybe $length; private Str $body; + /** + * @param Maybe<0|positive-int> $length + */ private function __construct( Method $method, Url $url, ProtocolVersion $protocol, Headers $headers, + Maybe $length, Str $body, ) { $this->method = $method; $this->url = $url; $this->protocol = $protocol; $this->headers = $headers; + $this->length = $length; $this->body = $body; } @@ -44,17 +52,38 @@ public static function new( ProtocolVersion $protocol, Headers $headers, ): self { - return new self($method, $url, $protocol, $headers, Str::of('')); + /** @var Maybe<0|positive-int> */ + $length = $headers + ->find(ContentLength::class) + ->flatMap(static fn($header) => $header->values()->find(static fn() => true)) // first + ->map(static fn($length) => (int) $length->toString()) // at this moment the header doesn't expose directly the int + ->filter(static fn($length) => $length >= 0); + + return new self( + $method, + $url, + $protocol, + $headers, + $length, + Str::of(''), + ); } public function add(Str $chunk): self { + $body = $this->body->append($chunk->toString()); + $body = $this->length->match( + static fn($length) => $body->take($length), + static fn() => $body, + ); + return new self( $this->method, $this->url, $this->protocol, $this->headers, - $this->body->append($chunk->toString()), + $this->length, + $body, ); } diff --git a/tests/Request/ParseTest.php b/tests/Request/ParseTest.php index 2ea96d1..dc5d820 100644 --- a/tests/Request/ParseTest.php +++ b/tests/Request/ParseTest.php @@ -144,6 +144,73 @@ public function testParsePost() static fn() => null, ), ); + $this->assertSame( + 'some[key]=value&foo=bar', + $request->body()->toString(), + ); + }); + } + + public function testParseUnboundedPost() + { + $this + ->forAll(Set\Integers::above(1)) + ->then(function($size) { + $chunks = Str::of(\file_get_contents('fixtures/unbounded-post.txt'))->chunk($size); + $parse = new Parse(new Clock); + + $request = $parse($chunks)->match( + static fn($request) => $request, + static fn() => null, + ); + + $this->assertInstanceOf(Request::class, $request); + $this->assertSame(Method::post, $request->method()); + $this->assertSame('/some-form', $request->url()->toString()); + $this->assertSame(ProtocolVersion::v11, $request->protocolVersion()); + $this->assertCount(6, $request->headers()); + $this->assertSame( + 'User-Agent: Mozilla/4.0 (compatible; MSIE5.01; Windows NT)', + $request->headers()->get('user-agent')->match( + static fn($header) => $header->toString(), + static fn() => null, + ), + ); + $this->assertSame( + 'Host: innmind.com', + $request->headers()->get('host')->match( + static fn($header) => $header->toString(), + static fn() => null, + ), + ); + $this->assertSame( + 'Content-Type: application/x-www-form-urlencoded', + $request->headers()->get('content-type')->match( + static fn($header) => $header->toString(), + static fn() => null, + ), + ); + $this->assertSame( + 'Accept-Language: fr-fr;q=1', + $request->headers()->get('accept-language')->match( + static fn($header) => $header->toString(), + static fn() => null, + ), + ); + $this->assertSame( + 'Accept-Encoding: gzip;q=1, deflate;q=1', + $request->headers()->get('accept-encoding')->match( + static fn($header) => $header->toString(), + static fn() => null, + ), + ); + $this->assertSame( + 'Connection: Keep-Alive', + $request->headers()->get('connection')->match( + static fn($header) => $header->toString(), + static fn() => null, + ), + ); $this->assertSame( "some[key]=value&foo=bar\n", $request->body()->toString(),