Skip to content

Commit

Permalink
Incrementally parse form bodies
Browse files Browse the repository at this point in the history
Also now using header parser from amphp/http.
  • Loading branch information
trowski committed Jan 9, 2019
1 parent 40103b2 commit 772799d
Show file tree
Hide file tree
Showing 3 changed files with 74 additions and 45 deletions.
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"php": ">=7",
"amphp/amp": "^2",
"amphp/byte-stream": "^1.3",
"amphp/http": "^1",
"amphp/http-server": "^1 || ^0.8"
},
"require-dev": {
Expand Down
87 changes: 59 additions & 28 deletions src/BufferingParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace Amp\Http\Server\FormParser;

use Amp\Http\InvalidHeaderException;
use Amp\Http\Rfc7230;
use Amp\Http\Server\Request;
use Amp\Promise;
use Amp\Success;
Expand Down Expand Up @@ -35,13 +37,14 @@ public function parseForm(Request $request): Promise
$type = $request->getHeader("content-type");
$boundary = null;

if ($type !== null && \strncmp($type, "application/x-www-form-urlencoded", \strlen("application/x-www-form-urlencoded"))) {
if (!\preg_match('#^\s*multipart/(?:form-data|mixed)(?:\s*;\s*boundary\s*=\s*("?)([^"]*)\1)?$#', $type, $matches)) {
return new Success(new Form([]));
}
if ($type === null) {
return new Success(new Form([]));
}

if (\preg_match('#^multipart/(?:form-data|mixed)(?:\s*;\s*boundary\s*=\s*("?)([^"]*)\1)?$#', $type, $matches)) {
$boundary = $matches[2];
unset($matches);
} elseif (\strncmp($type, "application/x-www-form-urlencoded", \strlen("application/x-www-form-urlencoded")) !== 0) {
return new Success(new Form([]));
}

$body = $request->getBody();
Expand All @@ -60,20 +63,36 @@ public function parseForm(Request $request): Promise
*/
private function parseBody(string $body, string $boundary = null): Form
{
$fieldCount = 0;

// If there's no boundary, we're in urlencoded mode.
if ($boundary === null) {
$fields = [];
$fieldCount = 0;

foreach (\explode("&", $body) as $pair) {
while ($body !== "") {
if (($position = \strpos($body, "&")) === false) {
$position = \strlen($body);
}

if ($position === 0) {
throw new ParseException("Empty field/value pair");
}

if (++$fieldCount === $this->fieldCountLimit) {
throw new ParseException("Maximum number of variables exceeded");
}

$pair = \substr($body, 0, $position);
$body = (string) \substr($body, $position + 1);

$pair = \explode("=", $pair, 2);
$field = \urldecode($pair[0]);
$value = \urldecode($pair[1] ?? "");

if ($field === "") {
throw new ParseException("Empty field name");
}

$fields[$field][] = $value;
}

Expand All @@ -82,46 +101,58 @@ private function parseBody(string $body, string $boundary = null): Form

$fields = $files = [];

// RFC 7578, RFC 2046 Section 5.1.1
if (\strncmp($body, "--$boundary\r\n", \strlen($boundary) + 4) !== 0) {
return new Form([]);
}
$end = "--$boundary--\r\n";

$exp = \explode("\r\n--$boundary\r\n", $body);
$exp[0] = \substr($exp[0], \strlen($boundary) + 4);
$exp[\count($exp) - 1] = \substr(\end($exp), 0, -\strlen($boundary) - 8);
while ($body !== $end) {
// RFC 7578, RFC 2046 Section 5.1.1
if (\strncmp($body, "--$boundary\r\n", \strlen($boundary) + 4) !== 0) {
throw new ParseException("Invalid boundry format");
}

foreach ($exp as $entry) {
list($rawHeaders, $text) = \explode("\r\n\r\n", $entry, 2);
$headers = [];
$body = \substr($body, \strlen($boundary) + 4);

foreach (\explode("\r\n", $rawHeaders) as $header) {
$split = \explode(":", $header, 2);
if (!isset($split[1])) {
return new Form([]);
}
$headers[\strtolower($split[0])] = \trim($split[1]);
if (($position = \strpos($body, "--$boundary")) === false) {
throw new ParseException("Could not locate part boundary");
}

$entry = \substr($body, 0, $position - 2);
$body = \substr($body, $position);

if (++$fieldCount === $this->fieldCountLimit) {
throw new ParseException("Maximum number of variables exceeded");
}

if (($headerPos = \strpos($entry, "\r\n\r\n")) === false) {
throw new ParseException("No header/body boundry found");
}

try {
$headers = Rfc7230::parseHeaders(\substr($entry, 0, $headerPos + 2));
} catch (InvalidHeaderException $e) {
throw new ParseException("Invalid headers in body part", 0, $e);
}

$entry = \substr($entry, $headerPos + 4);

$count = \preg_match(
'#^\s*form-data(?:\s*;\s*(?:name\s*=\s*"([^"]+)"|filename\s*=\s*"([^"]+)"))+\s*$#',
$headers["content-disposition"] ?? "",
$headers["content-disposition"][0] ?? "",
$matches
);

if (!$count || !isset($matches[1])) {
return new Form([]);
throw new ParseException("Missing or invalid content disposition");
}

// Ignore Content-Transfer-Encoding as deprecated and hence we won't support it

$name = $matches[1];
$contentType = $headers["content-type"] ?? "text/plain";
$contentType = $headers["content-type"][0] ?? "text/plain";

if (isset($matches[2])) {
$files[$name][] = new File($matches[2], $text, $contentType);
$files[$name][] = new File($matches[2], $entry, $contentType);
} else {
$fields[$name][] = $text;
$fields[$name][] = $entry;
}
}

Expand Down
31 changes: 14 additions & 17 deletions src/StreamingParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
use Amp\ByteStream\InputStream;
use Amp\ByteStream\IteratorStream;
use Amp\Emitter;
use Amp\Http\InvalidHeaderException;
use Amp\Http\Rfc7230;
use Amp\Http\Server\Request;
use Amp\Iterator;
use function Amp\asyncCall;
Expand All @@ -24,13 +26,14 @@ public function parseForm(Request $request): Iterator
$type = $request->getHeader("content-type");
$boundary = null;

if ($type !== null && \strncmp($type, "application/x-www-form-urlencoded", \strlen("application/x-www-form-urlencoded"))) {
if (!\preg_match('#^\s*multipart/(?:form-data|mixed)(?:\s*;\s*boundary\s*=\s*("?)([^"]*)\1)?$#', $type, $matches)) {
return Iterator\fromIterable([]);
}
if ($type === null) {
return Iterator\fromIterable([]);
}

if (\preg_match('#^multipart/(?:form-data|mixed)(?:\s*;\s*boundary\s*=\s*("?)([^"]*)\1)?$#', $type, $matches)) {
$boundary = $matches[2];
unset($matches);
} elseif (\strncmp($type, "application/x-www-form-urlencoded", \strlen("application/x-www-form-urlencoded")) !== 0) {
return Iterator\fromIterable([]);
}

$body = $request->getBody();
Expand Down Expand Up @@ -104,21 +107,15 @@ private function incrementalBoundaryParse(Emitter $emitter, InputStream $body, s
throw new ParseException("Maximum number of variables exceeded");
}

$headers = [];

foreach (\explode("\r\n", \substr($buffer, $offset, $end - $offset)) as $header) {
$split = \explode(":", $header, 2);

if (!isset($split[1])) {
throw new ParseException("Invalid content header within multipart form");
}

$headers[\strtolower($split[0])] = \trim($split[1]);
try {
$headers = Rfc7230::parseHeaders(\substr($buffer, $offset, $end + 2 - $offset));
} catch (InvalidHeaderException $e) {
throw new ParseException("Invalid headers in body part", 0, $e);
}

$count = \preg_match(
'#^\s*form-data(?:\s*;\s*(?:name\s*=\s*"([^"]+)"|filename\s*=\s*"([^"]+)"))+\s*$#',
$headers["content-disposition"] ?? "",
$headers["content-disposition"][0] ?? "",
$matches
);

Expand All @@ -132,7 +129,7 @@ private function incrementalBoundaryParse(Emitter $emitter, InputStream $body, s

$dataEmitter = new Emitter;
$stream = new IteratorStream($dataEmitter->iterate());
$field = new StreamedField($fieldName, $stream, $headers["content-type"] ?? "text/plain", $matches[2] ?? null);
$field = new StreamedField($fieldName, $stream, $headers["content-type"][0] ?? "text/plain", $matches[2] ?? null);

$emitPromise = $emitter->emit($field);

Expand Down

0 comments on commit 772799d

Please sign in to comment.