diff options
Diffstat (limited to 'vendor/react/http/src/Io')
-rw-r--r-- | vendor/react/http/src/Io/BufferedBody.php | 179 | ||||
-rw-r--r-- | vendor/react/http/src/Io/ChunkedDecoder.php | 175 | ||||
-rw-r--r-- | vendor/react/http/src/Io/ChunkedEncoder.php | 92 | ||||
-rw-r--r-- | vendor/react/http/src/Io/CloseProtectionStream.php | 111 | ||||
-rw-r--r-- | vendor/react/http/src/Io/EmptyBodyStream.php | 142 | ||||
-rw-r--r-- | vendor/react/http/src/Io/HttpBodyStream.php | 182 | ||||
-rw-r--r-- | vendor/react/http/src/Io/IniUtil.php | 48 | ||||
-rw-r--r-- | vendor/react/http/src/Io/LengthLimitedStream.php | 108 | ||||
-rw-r--r-- | vendor/react/http/src/Io/MiddlewareRunner.php | 61 | ||||
-rw-r--r-- | vendor/react/http/src/Io/MultipartParser.php | 328 | ||||
-rw-r--r-- | vendor/react/http/src/Io/PauseBufferStream.php | 188 | ||||
-rw-r--r-- | vendor/react/http/src/Io/ReadableBodyStream.php | 153 | ||||
-rw-r--r-- | vendor/react/http/src/Io/RequestHeaderParser.php | 281 | ||||
-rw-r--r-- | vendor/react/http/src/Io/Sender.php | 160 | ||||
-rw-r--r-- | vendor/react/http/src/Io/StreamingServer.php | 383 | ||||
-rw-r--r-- | vendor/react/http/src/Io/Transaction.php | 303 | ||||
-rw-r--r-- | vendor/react/http/src/Io/UploadedFile.php | 130 |
17 files changed, 3024 insertions, 0 deletions
diff --git a/vendor/react/http/src/Io/BufferedBody.php b/vendor/react/http/src/Io/BufferedBody.php new file mode 100644 index 0000000..4a4d839 --- /dev/null +++ b/vendor/react/http/src/Io/BufferedBody.php @@ -0,0 +1,179 @@ +<?php + +namespace React\Http\Io; + +use Psr\Http\Message\StreamInterface; + +/** + * [Internal] PSR-7 message body implementation using an in-memory buffer + * + * @internal + */ +class BufferedBody implements StreamInterface +{ + private $buffer = ''; + private $position = 0; + private $closed = false; + + /** + * @param string $buffer + */ + public function __construct($buffer) + { + $this->buffer = $buffer; + } + + public function __toString() + { + if ($this->closed) { + return ''; + } + + $this->seek(0); + + return $this->getContents(); + } + + public function close() + { + $this->buffer = ''; + $this->position = 0; + $this->closed = true; + } + + public function detach() + { + $this->close(); + + return null; + } + + public function getSize() + { + return $this->closed ? null : \strlen($this->buffer); + } + + public function tell() + { + if ($this->closed) { + throw new \RuntimeException('Unable to tell position of closed stream'); + } + + return $this->position; + } + + public function eof() + { + return $this->position >= \strlen($this->buffer); + } + + public function isSeekable() + { + return !$this->closed; + } + + public function seek($offset, $whence = \SEEK_SET) + { + if ($this->closed) { + throw new \RuntimeException('Unable to seek on closed stream'); + } + + $old = $this->position; + + if ($whence === \SEEK_SET) { + $this->position = $offset; + } elseif ($whence === \SEEK_CUR) { + $this->position += $offset; + } elseif ($whence === \SEEK_END) { + $this->position = \strlen($this->buffer) + $offset; + } else { + throw new \InvalidArgumentException('Invalid seek mode given'); + } + + if (!\is_int($this->position) || $this->position < 0) { + $this->position = $old; + throw new \RuntimeException('Unable to seek to position'); + } + } + + public function rewind() + { + $this->seek(0); + } + + public function isWritable() + { + return !$this->closed; + } + + public function write($string) + { + if ($this->closed) { + throw new \RuntimeException('Unable to write to closed stream'); + } + + if ($string === '') { + return 0; + } + + if ($this->position > 0 && !isset($this->buffer[$this->position - 1])) { + $this->buffer = \str_pad($this->buffer, $this->position, "\0"); + } + + $len = \strlen($string); + $this->buffer = \substr($this->buffer, 0, $this->position) . $string . \substr($this->buffer, $this->position + $len); + $this->position += $len; + + return $len; + } + + public function isReadable() + { + return !$this->closed; + } + + public function read($length) + { + if ($this->closed) { + throw new \RuntimeException('Unable to read from closed stream'); + } + + if ($length < 1) { + throw new \InvalidArgumentException('Invalid read length given'); + } + + if ($this->position + $length > \strlen($this->buffer)) { + $length = \strlen($this->buffer) - $this->position; + } + + if (!isset($this->buffer[$this->position])) { + return ''; + } + + $pos = $this->position; + $this->position += $length; + + return \substr($this->buffer, $pos, $length); + } + + public function getContents() + { + if ($this->closed) { + throw new \RuntimeException('Unable to read from closed stream'); + } + + if (!isset($this->buffer[$this->position])) { + return ''; + } + + $pos = $this->position; + $this->position = \strlen($this->buffer); + + return \substr($this->buffer, $pos); + } + + public function getMetadata($key = null) + { + return $key === null ? array() : null; + } +} diff --git a/vendor/react/http/src/Io/ChunkedDecoder.php b/vendor/react/http/src/Io/ChunkedDecoder.php new file mode 100644 index 0000000..2f58f42 --- /dev/null +++ b/vendor/react/http/src/Io/ChunkedDecoder.php @@ -0,0 +1,175 @@ +<?php + +namespace React\Http\Io; + +use Evenement\EventEmitter; +use React\Stream\ReadableStreamInterface; +use React\Stream\Util; +use React\Stream\WritableStreamInterface; +use Exception; + +/** + * [Internal] Decodes "Transfer-Encoding: chunked" from given stream and returns only payload data. + * + * This is used internally to decode incoming requests with this encoding. + * + * @internal + */ +class ChunkedDecoder extends EventEmitter implements ReadableStreamInterface +{ + const CRLF = "\r\n"; + const MAX_CHUNK_HEADER_SIZE = 1024; + + private $closed = false; + private $input; + private $buffer = ''; + private $chunkSize = 0; + private $transferredSize = 0; + private $headerCompleted = false; + + public function __construct(ReadableStreamInterface $input) + { + $this->input = $input; + + $this->input->on('data', array($this, 'handleData')); + $this->input->on('end', array($this, 'handleEnd')); + $this->input->on('error', array($this, 'handleError')); + $this->input->on('close', array($this, 'close')); + } + + public function isReadable() + { + return !$this->closed && $this->input->isReadable(); + } + + public function pause() + { + $this->input->pause(); + } + + public function resume() + { + $this->input->resume(); + } + + public function pipe(WritableStreamInterface $dest, array $options = array()) + { + Util::pipe($this, $dest, $options); + + return $dest; + } + + public function close() + { + if ($this->closed) { + return; + } + + $this->buffer = ''; + + $this->closed = true; + + $this->input->close(); + + $this->emit('close'); + $this->removeAllListeners(); + } + + /** @internal */ + public function handleEnd() + { + if (!$this->closed) { + $this->handleError(new Exception('Unexpected end event')); + } + } + + /** @internal */ + public function handleError(Exception $e) + { + $this->emit('error', array($e)); + $this->close(); + } + + /** @internal */ + public function handleData($data) + { + $this->buffer .= $data; + + while ($this->buffer !== '') { + if (!$this->headerCompleted) { + $positionCrlf = \strpos($this->buffer, static::CRLF); + + if ($positionCrlf === false) { + // Header shouldn't be bigger than 1024 bytes + if (isset($this->buffer[static::MAX_CHUNK_HEADER_SIZE])) { + $this->handleError(new Exception('Chunk header size inclusive extension bigger than' . static::MAX_CHUNK_HEADER_SIZE. ' bytes')); + } + return; + } + + $header = \strtolower((string)\substr($this->buffer, 0, $positionCrlf)); + $hexValue = $header; + + if (\strpos($header, ';') !== false) { + $array = \explode(';', $header); + $hexValue = $array[0]; + } + + if ($hexValue !== '') { + $hexValue = \ltrim(\trim($hexValue), "0"); + if ($hexValue === '') { + $hexValue = "0"; + } + } + + $this->chunkSize = @\hexdec($hexValue); + if (!\is_int($this->chunkSize) || \dechex($this->chunkSize) !== $hexValue) { + $this->handleError(new Exception($hexValue . ' is not a valid hexadecimal number')); + return; + } + + $this->buffer = (string)\substr($this->buffer, $positionCrlf + 2); + $this->headerCompleted = true; + if ($this->buffer === '') { + return; + } + } + + $chunk = (string)\substr($this->buffer, 0, $this->chunkSize - $this->transferredSize); + + if ($chunk !== '') { + $this->transferredSize += \strlen($chunk); + $this->emit('data', array($chunk)); + $this->buffer = (string)\substr($this->buffer, \strlen($chunk)); + } + + $positionCrlf = \strpos($this->buffer, static::CRLF); + + if ($positionCrlf === 0) { + if ($this->chunkSize === 0) { + $this->emit('end'); + $this->close(); + return; + } + $this->chunkSize = 0; + $this->headerCompleted = false; + $this->transferredSize = 0; + $this->buffer = (string)\substr($this->buffer, 2); + } elseif ($this->chunkSize === 0) { + // end chunk received, skip all trailer data + $this->buffer = (string)\substr($this->buffer, $positionCrlf); + } + + if ($positionCrlf !== 0 && $this->chunkSize !== 0 && $this->chunkSize === $this->transferredSize && \strlen($this->buffer) > 2) { + // the first 2 characters are not CRLF, send error event + $this->handleError(new Exception('Chunk does not end with a CRLF')); + return; + } + + if ($positionCrlf !== 0 && \strlen($this->buffer) < 2) { + // No CRLF found, wait for additional data which could be a CRLF + return; + } + } + } +} diff --git a/vendor/react/http/src/Io/ChunkedEncoder.php b/vendor/react/http/src/Io/ChunkedEncoder.php new file mode 100644 index 0000000..c84ef54 --- /dev/null +++ b/vendor/react/http/src/Io/ChunkedEncoder.php @@ -0,0 +1,92 @@ +<?php + +namespace React\Http\Io; + +use Evenement\EventEmitter; +use React\Stream\ReadableStreamInterface; +use React\Stream\Util; +use React\Stream\WritableStreamInterface; + +/** + * [Internal] Encodes given payload stream with "Transfer-Encoding: chunked" and emits encoded data + * + * This is used internally to encode outgoing requests with this encoding. + * + * @internal + */ +class ChunkedEncoder extends EventEmitter implements ReadableStreamInterface +{ + private $input; + private $closed = false; + + public function __construct(ReadableStreamInterface $input) + { + $this->input = $input; + + $this->input->on('data', array($this, 'handleData')); + $this->input->on('end', array($this, 'handleEnd')); + $this->input->on('error', array($this, 'handleError')); + $this->input->on('close', array($this, 'close')); + } + + public function isReadable() + { + return !$this->closed && $this->input->isReadable(); + } + + public function pause() + { + $this->input->pause(); + } + + public function resume() + { + $this->input->resume(); + } + + public function pipe(WritableStreamInterface $dest, array $options = array()) + { + return Util::pipe($this, $dest, $options); + } + + public function close() + { + if ($this->closed) { + return; + } + + $this->closed = true; + $this->input->close(); + + $this->emit('close'); + $this->removeAllListeners(); + } + + /** @internal */ + public function handleData($data) + { + if ($data !== '') { + $this->emit('data', array( + \dechex(\strlen($data)) . "\r\n" . $data . "\r\n" + )); + } + } + + /** @internal */ + public function handleError(\Exception $e) + { + $this->emit('error', array($e)); + $this->close(); + } + + /** @internal */ + public function handleEnd() + { + $this->emit('data', array("0\r\n\r\n")); + + if (!$this->closed) { + $this->emit('end'); + $this->close(); + } + } +} diff --git a/vendor/react/http/src/Io/CloseProtectionStream.php b/vendor/react/http/src/Io/CloseProtectionStream.php new file mode 100644 index 0000000..2e1ed6e --- /dev/null +++ b/vendor/react/http/src/Io/CloseProtectionStream.php @@ -0,0 +1,111 @@ +<?php + +namespace React\Http\Io; + +use Evenement\EventEmitter; +use React\Stream\ReadableStreamInterface; +use React\Stream\Util; +use React\Stream\WritableStreamInterface; + +/** + * [Internal] Protects a given stream from actually closing and only discards its incoming data instead. + * + * This is used internally to prevent the underlying connection from closing, so + * that we can still send back a response over the same stream. + * + * @internal + * */ +class CloseProtectionStream extends EventEmitter implements ReadableStreamInterface +{ + private $input; + private $closed = false; + private $paused = false; + + /** + * @param ReadableStreamInterface $input stream that will be discarded instead of closing it on an 'close' event. + */ + public function __construct(ReadableStreamInterface $input) + { + $this->input = $input; + + $this->input->on('data', array($this, 'handleData')); + $this->input->on('end', array($this, 'handleEnd')); + $this->input->on('error', array($this, 'handleError')); + $this->input->on('close', array($this, 'close')); + } + + public function isReadable() + { + return !$this->closed && $this->input->isReadable(); + } + + public function pause() + { + if ($this->closed) { + return; + } + + $this->paused = true; + $this->input->pause(); + } + + public function resume() + { + if ($this->closed) { + return; + } + + $this->paused = false; + $this->input->resume(); + } + + public function pipe(WritableStreamInterface $dest, array $options = array()) + { + Util::pipe($this, $dest, $options); + + return $dest; + } + + public function close() + { + if ($this->closed) { + return; + } + + $this->closed = true; + + // stop listening for incoming events + $this->input->removeListener('data', array($this, 'handleData')); + $this->input->removeListener('error', array($this, 'handleError')); + $this->input->removeListener('end', array($this, 'handleEnd')); + $this->input->removeListener('close', array($this, 'close')); + + // resume the stream to ensure we discard everything from incoming connection + if ($this->paused) { + $this->paused = false; + $this->input->resume(); + } + + $this->emit('close'); + $this->removeAllListeners(); + } + + /** @internal */ + public function handleData($data) + { + $this->emit('data', array($data)); + } + + /** @internal */ + public function handleEnd() + { + $this->emit('end'); + $this->close(); + } + + /** @internal */ + public function handleError(\Exception $e) + { + $this->emit('error', array($e)); + } +} diff --git a/vendor/react/http/src/Io/EmptyBodyStream.php b/vendor/react/http/src/Io/EmptyBodyStream.php new file mode 100644 index 0000000..5056219 --- /dev/null +++ b/vendor/react/http/src/Io/EmptyBodyStream.php @@ -0,0 +1,142 @@ +<?php + +namespace React\Http\Io; + +use Evenement\EventEmitter; +use Psr\Http\Message\StreamInterface; +use React\Stream\ReadableStreamInterface; +use React\Stream\Util; +use React\Stream\WritableStreamInterface; + +/** + * [Internal] Bridge between an empty StreamInterface from PSR-7 and ReadableStreamInterface from ReactPHP + * + * This class is used in the server to represent an empty body stream of an + * incoming response from the client. This is similar to the `HttpBodyStream`, + * but is specifically designed for the common case of having an empty message + * body. + * + * Note that this is an internal class only and nothing you should usually care + * about. See the `StreamInterface` and `ReadableStreamInterface` for more + * details. + * + * @see HttpBodyStream + * @see StreamInterface + * @see ReadableStreamInterface + * @internal + */ +class EmptyBodyStream extends EventEmitter implements StreamInterface, ReadableStreamInterface +{ + private $closed = false; + + public function isReadable() + { + return !$this->closed; + } + + public function pause() + { + // NOOP + } + + public function resume() + { + // NOOP + } + + public function pipe(WritableStreamInterface $dest, array $options = array()) + { + Util::pipe($this, $dest, $options); + + return $dest; + } + + public function close() + { + if ($this->closed) { + return; + } + + $this->closed = true; + + $this->emit('close'); + $this->removeAllListeners(); + } + + public function getSize() + { + return 0; + } + + /** @ignore */ + public function __toString() + { + return ''; + } + + /** @ignore */ + public function detach() + { + return null; + } + + /** @ignore */ + public function tell() + { + throw new \BadMethodCallException(); + } + + /** @ignore */ + public function eof() + { + throw new \BadMethodCallException(); + } + + /** @ignore */ + public function isSeekable() + { + return false; + } + + /** @ignore */ + public function seek($offset, $whence = SEEK_SET) + { + throw new \BadMethodCallException(); + } + + /** @ignore */ + public function rewind() + { + throw new \BadMethodCallException(); + } + + /** @ignore */ + public function isWritable() + { + return false; + } + + /** @ignore */ + public function write($string) + { + throw new \BadMethodCallException(); + } + + /** @ignore */ + public function read($length) + { + throw new \BadMethodCallException(); + } + + /** @ignore */ + public function getContents() + { + return ''; + } + + /** @ignore */ + public function getMetadata($key = null) + { + return ($key === null) ? array() : null; + } +} diff --git a/vendor/react/http/src/Io/HttpBodyStream.php b/vendor/react/http/src/Io/HttpBodyStream.php new file mode 100644 index 0000000..25d15a1 --- /dev/null +++ b/vendor/react/http/src/Io/HttpBodyStream.php @@ -0,0 +1,182 @@ +<?php + +namespace React\Http\Io; + +use Evenement\EventEmitter; +use Psr\Http\Message\StreamInterface; +use React\Stream\ReadableStreamInterface; +use React\Stream\Util; +use React\Stream\WritableStreamInterface; + +/** + * [Internal] Bridge between StreamInterface from PSR-7 and ReadableStreamInterface from ReactPHP + * + * This class is used in the server to stream the body of an incoming response + * from the client. This allows us to stream big amounts of data without having + * to buffer this data. Similarly, this used to stream the body of an outgoing + * request body to the client. The data will be sent directly to the client. + * + * Note that this is an internal class only and nothing you should usually care + * about. See the `StreamInterface` and `ReadableStreamInterface` for more + * details. + * + * @see StreamInterface + * @see ReadableStreamInterface + * @internal + */ +class HttpBodyStream extends EventEmitter implements StreamInterface, ReadableStreamInterface +{ + public $input; + private $closed = false; + private $size; + + /** + * @param ReadableStreamInterface $input Stream data from $stream as a body of a PSR-7 object4 + * @param int|null $size size of the data body + */ + public function __construct(ReadableStreamInterface $input, $size) + { + $this->input = $input; + $this->size = $size; + + $this->input->on('data', array($this, 'handleData')); + $this->input->on('end', array($this, 'handleEnd')); + $this->input->on('error', array($this, 'handleError')); + $this->input->on('close', array($this, 'close')); + } + + public function isReadable() + { + return !$this->closed && $this->input->isReadable(); + } + + public function pause() + { + $this->input->pause(); + } + + public function resume() + { + $this->input->resume(); + } + + public function pipe(WritableStreamInterface $dest, array $options = array()) + { + Util::pipe($this, $dest, $options); + + return $dest; + } + + public function close() + { + if ($this->closed) { + return; + } + + $this->closed = true; + + $this->input->close(); + + $this->emit('close'); + $this->removeAllListeners(); + } + + public function getSize() + { + return $this->size; + } + + /** @ignore */ + public function __toString() + { + return ''; + } + + /** @ignore */ + public function detach() + { + return null; + } + + /** @ignore */ + public function tell() + { + throw new \BadMethodCallException(); + } + + /** @ignore */ + public function eof() + { + throw new \BadMethodCallException(); + } + + /** @ignore */ + public function isSeekable() + { + return false; + } + + /** @ignore */ + public function seek($offset, $whence = SEEK_SET) + { + throw new \BadMethodCallException(); + } + + /** @ignore */ + public function rewind() + { + throw new \BadMethodCallException(); + } + + /** @ignore */ + public function isWritable() + { + return false; + } + + /** @ignore */ + public function write($string) + { + throw new \BadMethodCallException(); + } + + /** @ignore */ + public function read($length) + { + throw new \BadMethodCallException(); + } + + /** @ignore */ + public function getContents() + { + return ''; + } + + /** @ignore */ + public function getMetadata($key = null) + { + return null; + } + + /** @internal */ + public function handleData($data) + { + $this->emit('data', array($data)); + } + + /** @internal */ + public function handleError(\Exception $e) + { + $this->emit('error', array($e)); + $this->close(); + } + + /** @internal */ + public function handleEnd() + { + if (!$this->closed) { + $this->emit('end'); + $this->close(); + } + } +} diff --git a/vendor/react/http/src/Io/IniUtil.php b/vendor/react/http/src/Io/IniUtil.php new file mode 100644 index 0000000..612aae2 --- /dev/null +++ b/vendor/react/http/src/Io/IniUtil.php @@ -0,0 +1,48 @@ +<?php + +namespace React\Http\Io; + +/** + * @internal + */ +final class IniUtil +{ + /** + * Convert a ini like size to a numeric size in bytes. + * + * @param string $size + * @return int + */ + public static function iniSizeToBytes($size) + { + if (\is_numeric($size)) { + return (int)$size; + } + + $suffix = \strtoupper(\substr($size, -1)); + $strippedSize = \substr($size, 0, -1); + + if (!\is_numeric($strippedSize)) { + throw new \InvalidArgumentException("$size is not a valid ini size"); + } + + if ($strippedSize <= 0) { + throw new \InvalidArgumentException("Expect $size to be higher isn't zero or lower"); + } + + if ($suffix === 'K') { + return $strippedSize * 1024; + } + if ($suffix === 'M') { + return $strippedSize * 1024 * 1024; + } + if ($suffix === 'G') { + return $strippedSize * 1024 * 1024 * 1024; + } + if ($suffix === 'T') { + return $strippedSize * 1024 * 1024 * 1024 * 1024; + } + + return (int)$size; + } +} diff --git a/vendor/react/http/src/Io/LengthLimitedStream.php b/vendor/react/http/src/Io/LengthLimitedStream.php new file mode 100644 index 0000000..bc64c54 --- /dev/null +++ b/vendor/react/http/src/Io/LengthLimitedStream.php @@ -0,0 +1,108 @@ +<?php + +namespace React\Http\Io; + +use Evenement\EventEmitter; +use React\Stream\ReadableStreamInterface; +use React\Stream\Util; +use React\Stream\WritableStreamInterface; + +/** + * [Internal] Limits the amount of data the given stream can emit + * + * This is used internally to limit the size of the underlying connection stream + * to the size defined by the "Content-Length" header of the incoming request. + * + * @internal + */ +class LengthLimitedStream extends EventEmitter implements ReadableStreamInterface +{ + private $stream; + private $closed = false; + private $transferredLength = 0; + private $maxLength; + + public function __construct(ReadableStreamInterface $stream, $maxLength) + { + $this->stream = $stream; + $this->maxLength = $maxLength; + + $this->stream->on('data', array($this, 'handleData')); + $this->stream->on('end', array($this, 'handleEnd')); + $this->stream->on('error', array($this, 'handleError')); + $this->stream->on('close', array($this, 'close')); + } + + public function isReadable() + { + return !$this->closed && $this->stream->isReadable(); + } + + public function pause() + { + $this->stream->pause(); + } + + public function resume() + { + $this->stream->resume(); + } + + public function pipe(WritableStreamInterface $dest, array $options = array()) + { + Util::pipe($this, $dest, $options); + + return $dest; + } + + public function close() + { + if ($this->closed) { + return; + } + + $this->closed = true; + + $this->stream->close(); + + $this->emit('close'); + $this->removeAllListeners(); + } + + /** @internal */ + public function handleData($data) + { + if (($this->transferredLength + \strlen($data)) > $this->maxLength) { + // Only emit data until the value of 'Content-Length' is reached, the rest will be ignored + $data = (string)\substr($data, 0, $this->maxLength - $this->transferredLength); + } + + if ($data !== '') { + $this->transferredLength += \strlen($data); + $this->emit('data', array($data)); + } + + if ($this->transferredLength === $this->maxLength) { + // 'Content-Length' reached, stream will end + $this->emit('end'); + $this->close(); + $this->stream->removeListener('data', array($this, 'handleData')); + } + } + + /** @internal */ + public function handleError(\Exception $e) + { + $this->emit('error', array($e)); + $this->close(); + } + + /** @internal */ + public function handleEnd() + { + if (!$this->closed) { + $this->handleError(new \Exception('Unexpected end event')); + } + } + +} diff --git a/vendor/react/http/src/Io/MiddlewareRunner.php b/vendor/react/http/src/Io/MiddlewareRunner.php new file mode 100644 index 0000000..dedf6ff --- /dev/null +++ b/vendor/react/http/src/Io/MiddlewareRunner.php @@ -0,0 +1,61 @@ +<?php + +namespace React\Http\Io; + +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use React\Promise\PromiseInterface; + +/** + * [Internal] Middleware runner to expose an array of middleware request handlers as a single request handler callable + * + * @internal + */ +final class MiddlewareRunner +{ + /** + * @var callable[] + */ + private $middleware; + + /** + * @param callable[] $middleware + */ + public function __construct(array $middleware) + { + $this->middleware = \array_values($middleware); + } + + /** + * @param ServerRequestInterface $request + * @return ResponseInterface|PromiseInterface<ResponseInterface> + * @throws \Exception + */ + public function __invoke(ServerRequestInterface $request) + { + if (empty($this->middleware)) { + throw new \RuntimeException('No middleware to run'); + } + + return $this->call($request, 0); + } + + /** @internal */ + public function call(ServerRequestInterface $request, $position) + { + // final request handler will be invoked without a next handler + if (!isset($this->middleware[$position + 1])) { + $handler = $this->middleware[$position]; + return $handler($request); + } + + $that = $this; + $next = function (ServerRequestInterface $request) use ($that, $position) { + return $that->call($request, $position + 1); + }; + + // invoke middleware request handler with next handler + $handler = $this->middleware[$position]; + return $handler($request, $next); + } +} diff --git a/vendor/react/http/src/Io/MultipartParser.php b/vendor/react/http/src/Io/MultipartParser.php new file mode 100644 index 0000000..536694f --- /dev/null +++ b/vendor/react/http/src/Io/MultipartParser.php @@ -0,0 +1,328 @@ +<?php + +namespace React\Http\Io; + +use Psr\Http\Message\ServerRequestInterface; + +/** + * [Internal] Parses a string body with "Content-Type: multipart/form-data" into structured data + * + * This is use internally to parse incoming request bodies into structured data + * that resembles PHP's `$_POST` and `$_FILES` superglobals. + * + * @internal + * @link https://tools.ietf.org/html/rfc7578 + * @link https://tools.ietf.org/html/rfc2046#section-5.1.1 + */ +final class MultipartParser +{ + /** + * @var ServerRequestInterface|null + */ + private $request; + + /** + * @var int|null + */ + private $maxFileSize; + + /** + * ini setting "max_input_vars" + * + * Does not exist in PHP < 5.3.9 or HHVM, so assume PHP's default 1000 here. + * + * @var int + * @link http://php.net/manual/en/info.configuration.php#ini.max-input-vars + */ + private $maxInputVars = 1000; + + /** + * ini setting "max_input_nesting_level" + * + * Does not exist in HHVM, but assumes hard coded to 64 (PHP's default). + * + * @var int + * @link http://php.net/manual/en/info.configuration.php#ini.max-input-nesting-level + */ + private $maxInputNestingLevel = 64; + + /** + * ini setting "upload_max_filesize" + * + * @var int + */ + private $uploadMaxFilesize; + + /** + * ini setting "max_file_uploads" + * + * Additionally, setting "file_uploads = off" effectively sets this to zero. + * + * @var int + */ + private $maxFileUploads; + + private $postCount = 0; + private $filesCount = 0; + private $emptyCount = 0; + + /** + * @param int|string|null $uploadMaxFilesize + * @param int|null $maxFileUploads + */ + public function __construct($uploadMaxFilesize = null, $maxFileUploads = null) + { + $var = \ini_get('max_input_vars'); + if ($var !== false) { + $this->maxInputVars = (int)$var; + } + $var = \ini_get('max_input_nesting_level'); + if ($var !== false) { + $this->maxInputNestingLevel = (int)$var; + } + + if ($uploadMaxFilesize === null) { + $uploadMaxFilesize = \ini_get('upload_max_filesize'); + } + + $this->uploadMaxFilesize = IniUtil::iniSizeToBytes($uploadMaxFilesize); + $this->maxFileUploads = $maxFileUploads === null ? (\ini_get('file_uploads') === '' ? 0 : (int)\ini_get('max_file_uploads')) : (int)$maxFileUploads; + } + + public function parse(ServerRequestInterface $request) + { + $contentType = $request->getHeaderLine('content-type'); + if(!\preg_match('/boundary="?(.*?)"?$/', $contentType, $matches)) { + return $request; + } + + $this->request = $request; + $this->parseBody('--' . $matches[1], (string)$request->getBody()); + + $request = $this->request; + $this->request = null; + $this->postCount = 0; + $this->filesCount = 0; + $this->emptyCount = 0; + $this->maxFileSize = null; + + return $request; + } + + private function parseBody($boundary, $buffer) + { + $len = \strlen($boundary); + + // ignore everything before initial boundary (SHOULD be empty) + $start = \strpos($buffer, $boundary . "\r\n"); + + while ($start !== false) { + // search following boundary (preceded by newline) + // ignore last if not followed by boundary (SHOULD end with "--") + $start += $len + 2; + $end = \strpos($buffer, "\r\n" . $boundary, $start); + if ($end === false) { + break; + } + + // parse one part and continue searching for next + $this->parsePart(\substr($buffer, $start, $end - $start)); + $start = $end; + } + } + + private function parsePart($chunk) + { + $pos = \strpos($chunk, "\r\n\r\n"); + if ($pos === false) { + return; + } + + $headers = $this->parseHeaders((string)substr($chunk, 0, $pos)); + $body = (string)\substr($chunk, $pos + 4); + + if (!isset($headers['content-disposition'])) { + return; + } + + $name = $this->getParameterFromHeader($headers['content-disposition'], 'name'); + if ($name === null) { + return; + } + + $filename = $this->getParameterFromHeader($headers['content-disposition'], 'filename'); + if ($filename !== null) { + $this->parseFile( + $name, + $filename, + isset($headers['content-type'][0]) ? $headers['content-type'][0] : null, + $body + ); + } else { + $this->parsePost($name, $body); + } + } + + private function parseFile($name, $filename, $contentType, $contents) + { + $file = $this->parseUploadedFile($filename, $contentType, $contents); + if ($file === null) { + return; + } + + $this->request = $this->request->withUploadedFiles($this->extractPost( + $this->request->getUploadedFiles(), + $name, + $file + )); + } + + private function parseUploadedFile($filename, $contentType, $contents) + { + $size = \strlen($contents); + + // no file selected (zero size and empty filename) + if ($size === 0 && $filename === '') { + // ignore excessive number of empty file uploads + if (++$this->emptyCount + $this->filesCount > $this->maxInputVars) { + return; + } + + return new UploadedFile( + new BufferedBody(''), + $size, + \UPLOAD_ERR_NO_FILE, + $filename, + $contentType + ); + } + + // ignore excessive number of file uploads + if (++$this->filesCount > $this->maxFileUploads) { + return; + } + + // file exceeds "upload_max_filesize" ini setting + if ($size > $this->uploadMaxFilesize) { + return new UploadedFile( + new BufferedBody(''), + $size, + \UPLOAD_ERR_INI_SIZE, + $filename, + $contentType + ); + } + + // file exceeds MAX_FILE_SIZE value + if ($this->maxFileSize !== null && $size > $this->maxFileSize) { + return new UploadedFile( + new BufferedBody(''), + $size, + \UPLOAD_ERR_FORM_SIZE, + $filename, + $contentType + ); + } + + return new UploadedFile( + new BufferedBody($contents), + $size, + \UPLOAD_ERR_OK, + $filename, + $contentType + ); + } + + private function parsePost($name, $value) + { + // ignore excessive number of post fields + if (++$this->postCount > $this->maxInputVars) { + return; + } + + $this->request = $this->request->withParsedBody($this->extractPost( + $this->request->getParsedBody(), + $name, + $value + )); + + if (\strtoupper($name) === 'MAX_FILE_SIZE') { + $this->maxFileSize = (int)$value; + + if ($this->maxFileSize === 0) { + $this->maxFileSize = null; + } + } + } + + private function parseHeaders($header) + { + $headers = array(); + + foreach (\explode("\r\n", \trim($header)) as $line) { + $parts = \explode(':', $line, 2); + if (!isset($parts[1])) { + continue; + } + + $key = \strtolower(trim($parts[0])); + $values = \explode(';', $parts[1]); + $values = \array_map('trim', $values); + $headers[$key] = $values; + } + + return $headers; + } + + private function getParameterFromHeader(array $header, $parameter) + { + foreach ($header as $part) { + if (\preg_match('/' . $parameter . '="?(.*?)"?$/', $part, $matches)) { + return $matches[1]; + } + } + + return null; + } + + private function extractPost($postFields, $key, $value) + { + $chunks = \explode('[', $key); + if (\count($chunks) == 1) { + $postFields[$key] = $value; + return $postFields; + } + + // ignore this key if maximum nesting level is exceeded + if (isset($chunks[$this->maxInputNestingLevel])) { + return $postFields; + } + + $chunkKey = \rtrim($chunks[0], ']'); + $parent = &$postFields; + for ($i = 1; isset($chunks[$i]); $i++) { + $previousChunkKey = $chunkKey; + + if ($previousChunkKey === '') { + $parent[] = array(); + \end($parent); + $parent = &$parent[\key($parent)]; + } else { + if (!isset($parent[$previousChunkKey]) || !\is_array($parent[$previousChunkKey])) { + $parent[$previousChunkKey] = array(); + } + $parent = &$parent[$previousChunkKey]; + } + + $chunkKey = \rtrim($chunks[$i], ']'); + } + + if ($chunkKey === '') { + $parent[] = $value; + } else { + $parent[$chunkKey] = $value; + } + + return $postFields; + } +} diff --git a/vendor/react/http/src/Io/PauseBufferStream.php b/vendor/react/http/src/Io/PauseBufferStream.php new file mode 100644 index 0000000..fb5ed45 --- /dev/null +++ b/vendor/react/http/src/Io/PauseBufferStream.php @@ -0,0 +1,188 @@ +<?php + +namespace React\Http\Io; + +use Evenement\EventEmitter; +use React\Stream\ReadableStreamInterface; +use React\Stream\Util; +use React\Stream\WritableStreamInterface; + +/** + * [Internal] Pauses a given stream and buffers all events while paused + * + * This class is used to buffer all events that happen on a given stream while + * it is paused. This allows you to pause a stream and no longer watch for any + * of its events. Once the stream is resumed, all buffered events will be + * emitted. Explicitly closing the resulting stream clears all buffers. + * + * Note that this is an internal class only and nothing you should usually care + * about. + * + * @see ReadableStreamInterface + * @internal + */ +class PauseBufferStream extends EventEmitter implements ReadableStreamInterface +{ + private $input; + private $closed = false; + private $paused = false; + private $dataPaused = ''; + private $endPaused = false; + private $closePaused = false; + private $errorPaused; + private $implicit = false; + + public function __construct(ReadableStreamInterface $input) + { + $this->input = $input; + + $this->input->on('data', array($this, 'handleData')); + $this->input->on('end', array($this, 'handleEnd')); + $this->input->on('error', array($this, 'handleError')); + $this->input->on('close', array($this, 'handleClose')); + } + + /** + * pause and remember this was not explicitly from user control + * + * @internal + */ + public function pauseImplicit() + { + $this->pause(); + $this->implicit = true; + } + + /** + * resume only if this was previously paused implicitly and not explicitly from user control + * + * @internal + */ + public function resumeImplicit() + { + if ($this->implicit) { + $this->resume(); + } + } + + public function isReadable() + { + return !$this->closed; + } + + public function pause() + { + if ($this->closed) { + return; + } + + $this->input->pause(); + $this->paused = true; + $this->implicit = false; + } + + public function resume() + { + if ($this->closed) { + return; + } + + $this->paused = false; + $this->implicit = false; + + if ($this->dataPaused !== '') { + $this->emit('data', array($this->dataPaused)); + $this->dataPaused = ''; + } + + if ($this->errorPaused) { + $this->emit('error', array($this->errorPaused)); + return $this->close(); + } + + if ($this->endPaused) { + $this->endPaused = false; + $this->emit('end'); + return $this->close(); + } + + if ($this->closePaused) { + $this->closePaused = false; + return $this->close(); + } + + $this->input->resume(); + } + + public function pipe(WritableStreamInterface $dest, array $options = array()) + { + Util::pipe($this, $dest, $options); + + return $dest; + } + + public function close() + { + if ($this->closed) { + return; + } + + $this->closed = true; + $this->dataPaused = ''; + $this->endPaused = $this->closePaused = false; + $this->errorPaused = null; + + $this->input->close(); + + $this->emit('close'); + $this->removeAllListeners(); + } + + /** @internal */ + public function handleData($data) + { + if ($this->paused) { + $this->dataPaused .= $data; + return; + } + + $this->emit('data', array($data)); + } + + /** @internal */ + public function handleError(\Exception $e) + { + if ($this->paused) { + $this->errorPaused = $e; + return; + } + + $this->emit('error', array($e)); + $this->close(); + } + + /** @internal */ + public function handleEnd() + { + if ($this->paused) { + $this->endPaused = true; + return; + } + + if (!$this->closed) { + $this->emit('end'); + $this->close(); + } + } + + /** @internal */ + public function handleClose() + { + if ($this->paused) { + $this->closePaused = true; + return; + } + + $this->close(); + } +} diff --git a/vendor/react/http/src/Io/ReadableBodyStream.php b/vendor/react/http/src/Io/ReadableBodyStream.php new file mode 100644 index 0000000..daef45f --- /dev/null +++ b/vendor/react/http/src/Io/ReadableBodyStream.php @@ -0,0 +1,153 @@ +<?php + +namespace React\Http\Io; + +use Evenement\EventEmitter; +use Psr\Http\Message\StreamInterface; +use React\Stream\ReadableStreamInterface; +use React\Stream\Util; +use React\Stream\WritableStreamInterface; + +/** + * @internal + */ +class ReadableBodyStream extends EventEmitter implements ReadableStreamInterface, StreamInterface +{ + private $input; + private $position = 0; + private $size; + private $closed = false; + + public function __construct(ReadableStreamInterface $input, $size = null) + { + $this->input = $input; + $this->size = $size; + + $that = $this; + $pos =& $this->position; + $input->on('data', function ($data) use ($that, &$pos, $size) { + $that->emit('data', array($data)); + + $pos += \strlen($data); + if ($size !== null && $pos >= $size) { + $that->handleEnd(); + } + }); + $input->on('error', function ($error) use ($that) { + $that->emit('error', array($error)); + $that->close(); + }); + $input->on('end', array($that, 'handleEnd')); + $input->on('close', array($that, 'close')); + } + + public function close() + { + if (!$this->closed) { + $this->closed = true; + $this->input->close(); + + $this->emit('close'); + $this->removeAllListeners(); + } + } + + public function isReadable() + { + return $this->input->isReadable(); + } + + public function pause() + { + $this->input->pause(); + } + + public function resume() + { + $this->input->resume(); + } + + public function pipe(WritableStreamInterface $dest, array $options = array()) + { + Util::pipe($this, $dest, $options); + + return $dest; + } + + public function eof() + { + return !$this->isReadable(); + } + + public function __toString() + { + return ''; + } + + public function detach() + { + throw new \BadMethodCallException(); + } + + public function getSize() + { + return $this->size; + } + + public function tell() + { + throw new \BadMethodCallException(); + } + + public function isSeekable() + { + return false; + } + + public function seek($offset, $whence = SEEK_SET) + { + throw new \BadMethodCallException(); + } + + public function rewind() + { + throw new \BadMethodCallException(); + } + + public function isWritable() + { + return false; + } + + public function write($string) + { + throw new \BadMethodCallException(); + } + + public function read($length) + { + throw new \BadMethodCallException(); + } + + public function getContents() + { + throw new \BadMethodCallException(); + } + + public function getMetadata($key = null) + { + return ($key === null) ? array() : null; + } + + /** @internal */ + public function handleEnd() + { + if ($this->position !== $this->size && $this->size !== null) { + $this->emit('error', array(new \UnderflowException('Unexpected end of response body after ' . $this->position . '/' . $this->size . ' bytes'))); + } else { + $this->emit('end'); + } + + $this->close(); + } +} diff --git a/vendor/react/http/src/Io/RequestHeaderParser.php b/vendor/react/http/src/Io/RequestHeaderParser.php new file mode 100644 index 0000000..e5554c4 --- /dev/null +++ b/vendor/react/http/src/Io/RequestHeaderParser.php @@ -0,0 +1,281 @@ +<?php + +namespace React\Http\Io; + +use Evenement\EventEmitter; +use Psr\Http\Message\ServerRequestInterface; +use React\Http\Message\Response; +use React\Http\Message\ServerRequest; +use React\Socket\ConnectionInterface; +use Exception; + +/** + * [Internal] Parses an incoming request header from an input stream + * + * This is used internally to parse the request header from the connection and + * then process the remaining connection as the request body. + * + * @event headers + * @event error + * + * @internal + */ +class RequestHeaderParser extends EventEmitter +{ + private $maxSize = 8192; + + public function handle(ConnectionInterface $conn) + { + $buffer = ''; + $maxSize = $this->maxSize; + $that = $this; + $conn->on('data', $fn = function ($data) use (&$buffer, &$fn, $conn, $maxSize, $that) { + // append chunk of data to buffer and look for end of request headers + $buffer .= $data; + $endOfHeader = \strpos($buffer, "\r\n\r\n"); + + // reject request if buffer size is exceeded + if ($endOfHeader > $maxSize || ($endOfHeader === false && isset($buffer[$maxSize]))) { + $conn->removeListener('data', $fn); + $fn = null; + + $that->emit('error', array( + new \OverflowException("Maximum header size of {$maxSize} exceeded.", Response::STATUS_REQUEST_HEADER_FIELDS_TOO_LARGE), + $conn + )); + return; + } + + // ignore incomplete requests + if ($endOfHeader === false) { + return; + } + + // request headers received => try to parse request + $conn->removeListener('data', $fn); + $fn = null; + + try { + $request = $that->parseRequest( + (string)\substr($buffer, 0, $endOfHeader + 2), + $conn->getRemoteAddress(), + $conn->getLocalAddress() + ); + } catch (Exception $exception) { + $buffer = ''; + $that->emit('error', array( + $exception, + $conn + )); + return; + } + + $contentLength = 0; + if ($request->hasHeader('Transfer-Encoding')) { + $contentLength = null; + } elseif ($request->hasHeader('Content-Length')) { + $contentLength = (int)$request->getHeaderLine('Content-Length'); + } + + if ($contentLength === 0) { + // happy path: request body is known to be empty + $stream = new EmptyBodyStream(); + $request = $request->withBody($stream); + } else { + // otherwise body is present => delimit using Content-Length or ChunkedDecoder + $stream = new CloseProtectionStream($conn); + if ($contentLength !== null) { + $stream = new LengthLimitedStream($stream, $contentLength); + } else { + $stream = new ChunkedDecoder($stream); + } + + $request = $request->withBody(new HttpBodyStream($stream, $contentLength)); + } + + $bodyBuffer = isset($buffer[$endOfHeader + 4]) ? \substr($buffer, $endOfHeader + 4) : ''; + $buffer = ''; + $that->emit('headers', array($request, $conn)); + + if ($bodyBuffer !== '') { + $conn->emit('data', array($bodyBuffer)); + } + + // happy path: request body is known to be empty => immediately end stream + if ($contentLength === 0) { + $stream->emit('end'); + $stream->close(); + } + }); + } + + /** + * @param string $headers buffer string containing request headers only + * @param ?string $remoteSocketUri + * @param ?string $localSocketUri + * @return ServerRequestInterface + * @throws \InvalidArgumentException + * @internal + */ + public function parseRequest($headers, $remoteSocketUri, $localSocketUri) + { + // additional, stricter safe-guard for request line + // because request parser doesn't properly cope with invalid ones + $start = array(); + if (!\preg_match('#^(?<method>[^ ]+) (?<target>[^ ]+) HTTP/(?<version>\d\.\d)#m', $headers, $start)) { + throw new \InvalidArgumentException('Unable to parse invalid request-line'); + } + + // only support HTTP/1.1 and HTTP/1.0 requests + if ($start['version'] !== '1.1' && $start['version'] !== '1.0') { + throw new \InvalidArgumentException('Received request with invalid protocol version', Response::STATUS_VERSION_NOT_SUPPORTED); + } + + // match all request header fields into array, thanks to @kelunik for checking the HTTP specs and coming up with this regex + $matches = array(); + $n = \preg_match_all('/^([^()<>@,;:\\\"\/\[\]?={}\x01-\x20\x7F]++):[\x20\x09]*+((?:[\x20\x09]*+[\x21-\x7E\x80-\xFF]++)*+)[\x20\x09]*+[\r]?+\n/m', $headers, $matches, \PREG_SET_ORDER); + + // check number of valid header fields matches number of lines + request line + if (\substr_count($headers, "\n") !== $n + 1) { + throw new \InvalidArgumentException('Unable to parse invalid request header fields'); + } + + // format all header fields into associative array + $host = null; + $fields = array(); + foreach ($matches as $match) { + $fields[$match[1]][] = $match[2]; + + // match `Host` request header + if ($host === null && \strtolower($match[1]) === 'host') { + $host = $match[2]; + } + } + + // create new obj implementing ServerRequestInterface by preserving all + // previous properties and restoring original request-target + $serverParams = array( + 'REQUEST_TIME' => \time(), + 'REQUEST_TIME_FLOAT' => \microtime(true) + ); + + // scheme is `http` unless TLS is used + $localParts = $localSocketUri === null ? array() : \parse_url($localSocketUri); + if (isset($localParts['scheme']) && $localParts['scheme'] === 'tls') { + $scheme = 'https://'; + $serverParams['HTTPS'] = 'on'; + } else { + $scheme = 'http://'; + } + + // default host if unset comes from local socket address or defaults to localhost + $hasHost = $host !== null; + if ($host === null) { + $host = isset($localParts['host'], $localParts['port']) ? $localParts['host'] . ':' . $localParts['port'] : '127.0.0.1'; + } + + if ($start['method'] === 'OPTIONS' && $start['target'] === '*') { + // support asterisk-form for `OPTIONS *` request line only + $uri = $scheme . $host; + } elseif ($start['method'] === 'CONNECT') { + $parts = \parse_url('tcp://' . $start['target']); + + // check this is a valid authority-form request-target (host:port) + if (!isset($parts['scheme'], $parts['host'], $parts['port']) || \count($parts) !== 3) { + throw new \InvalidArgumentException('CONNECT method MUST use authority-form request target'); + } + $uri = $scheme . $start['target']; + } else { + // support absolute-form or origin-form for proxy requests + if ($start['target'][0] === '/') { + $uri = $scheme . $host . $start['target']; + } else { + // ensure absolute-form request-target contains a valid URI + $parts = \parse_url($start['target']); + + // make sure value contains valid host component (IP or hostname), but no fragment + if (!isset($parts['scheme'], $parts['host']) || $parts['scheme'] !== 'http' || isset($parts['fragment'])) { + throw new \InvalidArgumentException('Invalid absolute-form request-target'); + } + + $uri = $start['target']; + } + } + + // apply REMOTE_ADDR and REMOTE_PORT if source address is known + // address should always be known, unless this is over Unix domain sockets (UDS) + if ($remoteSocketUri !== null) { + $remoteAddress = \parse_url($remoteSocketUri); + $serverParams['REMOTE_ADDR'] = $remoteAddress['host']; + $serverParams['REMOTE_PORT'] = $remoteAddress['port']; + } + + // apply SERVER_ADDR and SERVER_PORT if server address is known + // address should always be known, even for Unix domain sockets (UDS) + // but skip UDS as it doesn't have a concept of host/port. + if ($localSocketUri !== null && isset($localParts['host'], $localParts['port'])) { + $serverParams['SERVER_ADDR'] = $localParts['host']; + $serverParams['SERVER_PORT'] = $localParts['port']; + } + + $request = new ServerRequest( + $start['method'], + $uri, + $fields, + '', + $start['version'], + $serverParams + ); + + // only assign request target if it is not in origin-form (happy path for most normal requests) + if ($start['target'][0] !== '/') { + $request = $request->withRequestTarget($start['target']); + } + + if ($hasHost) { + // Optional Host request header value MUST be valid (host and optional port) + $parts = \parse_url('http://' . $request->getHeaderLine('Host')); + + // make sure value contains valid host component (IP or hostname) + if (!$parts || !isset($parts['scheme'], $parts['host'])) { + $parts = false; + } + + // make sure value does not contain any other URI component + if (\is_array($parts)) { + unset($parts['scheme'], $parts['host'], $parts['port']); + } + if ($parts === false || $parts) { + throw new \InvalidArgumentException('Invalid Host header value'); + } + } elseif (!$hasHost && $start['version'] === '1.1' && $start['method'] !== 'CONNECT') { + // require Host request header for HTTP/1.1 (except for CONNECT method) + throw new \InvalidArgumentException('Missing required Host request header'); + } elseif (!$hasHost) { + // remove default Host request header for HTTP/1.0 when not explicitly given + $request = $request->withoutHeader('Host'); + } + + // ensure message boundaries are valid according to Content-Length and Transfer-Encoding request headers + if ($request->hasHeader('Transfer-Encoding')) { + if (\strtolower($request->getHeaderLine('Transfer-Encoding')) !== 'chunked') { + throw new \InvalidArgumentException('Only chunked-encoding is allowed for Transfer-Encoding', Response::STATUS_NOT_IMPLEMENTED); + } + + // Transfer-Encoding: chunked and Content-Length header MUST NOT be used at the same time + // as per https://tools.ietf.org/html/rfc7230#section-3.3.3 + if ($request->hasHeader('Content-Length')) { + throw new \InvalidArgumentException('Using both `Transfer-Encoding: chunked` and `Content-Length` is not allowed', Response::STATUS_BAD_REQUEST); + } + } elseif ($request->hasHeader('Content-Length')) { + $string = $request->getHeaderLine('Content-Length'); + + if ((string)(int)$string !== $string) { + // Content-Length value is not an integer or not a single integer + throw new \InvalidArgumentException('The value of `Content-Length` is not valid', Response::STATUS_BAD_REQUEST); + } + } + + return $request; + } +} diff --git a/vendor/react/http/src/Io/Sender.php b/vendor/react/http/src/Io/Sender.php new file mode 100644 index 0000000..2f04c79 --- /dev/null +++ b/vendor/react/http/src/Io/Sender.php @@ -0,0 +1,160 @@ +<?php + +namespace React\Http\Io; + +use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ResponseInterface; +use React\EventLoop\LoopInterface; +use React\Http\Client\Client as HttpClient; +use React\Http\Message\Response; +use React\Promise\PromiseInterface; +use React\Promise\Deferred; +use React\Socket\ConnectorInterface; +use React\Stream\ReadableStreamInterface; + +/** + * [Internal] Sends requests and receives responses + * + * The `Sender` is responsible for passing the [`RequestInterface`](#requestinterface) objects to + * the underlying [`HttpClient`](https://github.com/reactphp/http-client) library + * and keeps track of its transmission and converts its reponses back to [`ResponseInterface`](#responseinterface) objects. + * + * It also registers everything with the main [`EventLoop`](https://github.com/reactphp/event-loop#usage) + * and the default [`Connector`](https://github.com/reactphp/socket-client) and [DNS `Resolver`](https://github.com/reactphp/dns). + * + * The `Sender` class mostly exists in order to abstract changes on the underlying + * components away from this package in order to provide backwards and forwards + * compatibility. + * + * @internal You SHOULD NOT rely on this API, it is subject to change without prior notice! + * @see Browser + */ +class Sender +{ + /** + * create a new default sender attached to the given event loop + * + * This method is used internally to create the "default sender". + * + * You may also use this method if you need custom DNS or connector + * settings. You can use this method manually like this: + * + * ```php + * $connector = new \React\Socket\Connector(array(), $loop); + * $sender = \React\Http\Io\Sender::createFromLoop($loop, $connector); + * ``` + * + * @param LoopInterface $loop + * @param ConnectorInterface|null $connector + * @return self + */ + public static function createFromLoop(LoopInterface $loop, ConnectorInterface $connector = null) + { + return new self(new HttpClient($loop, $connector)); + } + + private $http; + + /** + * [internal] Instantiate Sender + * + * @param HttpClient $http + * @internal + */ + public function __construct(HttpClient $http) + { + $this->http = $http; + } + + /** + * + * @internal + * @param RequestInterface $request + * @return PromiseInterface Promise<ResponseInterface, Exception> + */ + public function send(RequestInterface $request) + { + $body = $request->getBody(); + $size = $body->getSize(); + + if ($size !== null && $size !== 0) { + // automatically assign a "Content-Length" request header if the body size is known and non-empty + $request = $request->withHeader('Content-Length', (string)$size); + } elseif ($size === 0 && \in_array($request->getMethod(), array('POST', 'PUT', 'PATCH'))) { + // only assign a "Content-Length: 0" request header if the body is expected for certain methods + $request = $request->withHeader('Content-Length', '0'); + } elseif ($body instanceof ReadableStreamInterface && $body->isReadable() && !$request->hasHeader('Content-Length')) { + // use "Transfer-Encoding: chunked" when this is a streaming body and body size is unknown + $request = $request->withHeader('Transfer-Encoding', 'chunked'); + } else { + // do not use chunked encoding if size is known or if this is an empty request body + $size = 0; + } + + $headers = array(); + foreach ($request->getHeaders() as $name => $values) { + $headers[$name] = implode(', ', $values); + } + + $requestStream = $this->http->request($request->getMethod(), (string)$request->getUri(), $headers, $request->getProtocolVersion()); + + $deferred = new Deferred(function ($_, $reject) use ($requestStream) { + // close request stream if request is cancelled + $reject(new \RuntimeException('Request cancelled')); + $requestStream->close(); + }); + + $requestStream->on('error', function($error) use ($deferred) { + $deferred->reject($error); + }); + + $requestStream->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($deferred, $request) { + $length = null; + $code = $response->getStatusCode(); + if ($request->getMethod() === 'HEAD' || ($code >= 100 && $code < 200) || $code == Response::STATUS_NO_CONTENT || $code == Response::STATUS_NOT_MODIFIED) { + $length = 0; + } elseif (\strtolower($response->getHeaderLine('Transfer-Encoding')) === 'chunked') { + $body = new ChunkedDecoder($body); + } elseif ($response->hasHeader('Content-Length')) { + $length = (int) $response->getHeaderLine('Content-Length'); + } + + $deferred->resolve($response->withBody(new ReadableBodyStream($body, $length))); + }); + + if ($body instanceof ReadableStreamInterface) { + if ($body->isReadable()) { + // length unknown => apply chunked transfer-encoding + if ($size === null) { + $body = new ChunkedEncoder($body); + } + + // pipe body into request stream + // add dummy write to immediately start request even if body does not emit any data yet + $body->pipe($requestStream); + $requestStream->write(''); + + $body->on('close', $close = function () use ($deferred, $requestStream) { + $deferred->reject(new \RuntimeException('Request failed because request body closed unexpectedly')); + $requestStream->close(); + }); + $body->on('error', function ($e) use ($deferred, $requestStream, $close, $body) { + $body->removeListener('close', $close); + $deferred->reject(new \RuntimeException('Request failed because request body reported an error', 0, $e)); + $requestStream->close(); + }); + $body->on('end', function () use ($close, $body) { + $body->removeListener('close', $close); + }); + } else { + // stream is not readable => end request without body + $requestStream->end(); + } + } else { + // body is fully buffered => write as one chunk + $requestStream->end((string)$body); + } + + return $deferred->promise(); + } +} diff --git a/vendor/react/http/src/Io/StreamingServer.php b/vendor/react/http/src/Io/StreamingServer.php new file mode 100644 index 0000000..7818f0b --- /dev/null +++ b/vendor/react/http/src/Io/StreamingServer.php @@ -0,0 +1,383 @@ +<?php + +namespace React\Http\Io; + +use Evenement\EventEmitter; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use React\EventLoop\LoopInterface; +use React\Http\Message\Response; +use React\Http\Message\ServerRequest; +use React\Promise; +use React\Promise\CancellablePromiseInterface; +use React\Promise\PromiseInterface; +use React\Socket\ConnectionInterface; +use React\Socket\ServerInterface; +use React\Stream\ReadableStreamInterface; +use React\Stream\WritableStreamInterface; + +/** + * The internal `StreamingServer` class is responsible for handling incoming connections and then + * processing each incoming HTTP request. + * + * Unlike the [`HttpServer`](#httpserver) class, it does not buffer and parse the incoming + * HTTP request body by default. This means that the request handler will be + * invoked with a streaming request body. Once the request headers have been + * received, it will invoke the request handler function. This request handler + * function needs to be passed to the constructor and will be invoked with the + * respective [request](#request) object and expects a [response](#response) + * object in return: + * + * ```php + * $server = new StreamingServer($loop, function (ServerRequestInterface $request) { + * return new Response( + * Response::STATUS_OK, + * array( + * 'Content-Type' => 'text/plain' + * ), + * "Hello World!\n" + * ); + * }); + * ``` + * + * Each incoming HTTP request message is always represented by the + * [PSR-7 `ServerRequestInterface`](https://www.php-fig.org/psr/psr-7/#321-psrhttpmessageserverrequestinterface), + * see also following [request](#request) chapter for more details. + * Each outgoing HTTP response message is always represented by the + * [PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface), + * see also following [response](#response) chapter for more details. + * + * In order to process any connections, the server needs to be attached to an + * instance of `React\Socket\ServerInterface` through the [`listen()`](#listen) method + * as described in the following chapter. In its most simple form, you can attach + * this to a [`React\Socket\SocketServer`](https://github.com/reactphp/socket#socketserver) + * in order to start a plaintext HTTP server like this: + * + * ```php + * $server = new StreamingServer($loop, $handler); + * + * $socket = new React\Socket\SocketServer('0.0.0.0:8080', array(), $loop); + * $server->listen($socket); + * ``` + * + * See also the [`listen()`](#listen) method and the [first example](examples) for more details. + * + * The `StreamingServer` class is considered advanced usage and unless you know + * what you're doing, you're recommended to use the [`HttpServer`](#httpserver) class + * instead. The `StreamingServer` class is specifically designed to help with + * more advanced use cases where you want to have full control over consuming + * the incoming HTTP request body and concurrency settings. + * + * In particular, this class does not buffer and parse the incoming HTTP request + * in memory. It will invoke the request handler function once the HTTP request + * headers have been received, i.e. before receiving the potentially much larger + * HTTP request body. This means the [request](#request) passed to your request + * handler function may not be fully compatible with PSR-7. See also + * [streaming request](#streaming-request) below for more details. + * + * @see \React\Http\HttpServer + * @see \React\Http\Message\Response + * @see self::listen() + * @internal + */ +final class StreamingServer extends EventEmitter +{ + private $callback; + private $parser; + private $loop; + + /** + * Creates an HTTP server that invokes the given callback for each incoming HTTP request + * + * In order to process any connections, the server needs to be attached to an + * instance of `React\Socket\ServerInterface` which emits underlying streaming + * connections in order to then parse incoming data as HTTP. + * See also [listen()](#listen) for more details. + * + * @param LoopInterface $loop + * @param callable $requestHandler + * @see self::listen() + */ + public function __construct(LoopInterface $loop, $requestHandler) + { + if (!\is_callable($requestHandler)) { + throw new \InvalidArgumentException('Invalid request handler given'); + } + + $this->loop = $loop; + + $this->callback = $requestHandler; + $this->parser = new RequestHeaderParser(); + + $that = $this; + $this->parser->on('headers', function (ServerRequestInterface $request, ConnectionInterface $conn) use ($that) { + $that->handleRequest($conn, $request); + }); + + $this->parser->on('error', function(\Exception $e, ConnectionInterface $conn) use ($that) { + $that->emit('error', array($e)); + + // parsing failed => assume dummy request and send appropriate error + $that->writeError( + $conn, + $e->getCode() !== 0 ? $e->getCode() : Response::STATUS_BAD_REQUEST, + new ServerRequest('GET', '/') + ); + }); + } + + /** + * Starts listening for HTTP requests on the given socket server instance + * + * @param ServerInterface $socket + * @see \React\Http\HttpServer::listen() + */ + public function listen(ServerInterface $socket) + { + $socket->on('connection', array($this->parser, 'handle')); + } + + /** @internal */ + public function handleRequest(ConnectionInterface $conn, ServerRequestInterface $request) + { + if ($request->getProtocolVersion() !== '1.0' && '100-continue' === \strtolower($request->getHeaderLine('Expect'))) { + $conn->write("HTTP/1.1 100 Continue\r\n\r\n"); + } + + // execute request handler callback + $callback = $this->callback; + try { + $response = $callback($request); + } catch (\Exception $error) { + // request handler callback throws an Exception + $response = Promise\reject($error); + } catch (\Throwable $error) { // @codeCoverageIgnoreStart + // request handler callback throws a PHP7+ Error + $response = Promise\reject($error); // @codeCoverageIgnoreEnd + } + + // cancel pending promise once connection closes + if ($response instanceof CancellablePromiseInterface) { + $conn->on('close', function () use ($response) { + $response->cancel(); + }); + } + + // happy path: response returned, handle and return immediately + if ($response instanceof ResponseInterface) { + return $this->handleResponse($conn, $request, $response); + } + + // did not return a promise? this is an error, convert into one for rejection below. + if (!$response instanceof PromiseInterface) { + $response = Promise\resolve($response); + } + + $that = $this; + $response->then( + function ($response) use ($that, $conn, $request) { + if (!$response instanceof ResponseInterface) { + $message = 'The response callback is expected to resolve with an object implementing Psr\Http\Message\ResponseInterface, but resolved with "%s" instead.'; + $message = \sprintf($message, \is_object($response) ? \get_class($response) : \gettype($response)); + $exception = new \RuntimeException($message); + + $that->emit('error', array($exception)); + return $that->writeError($conn, Response::STATUS_INTERNAL_SERVER_ERROR, $request); + } + $that->handleResponse($conn, $request, $response); + }, + function ($error) use ($that, $conn, $request) { + $message = 'The response callback is expected to resolve with an object implementing Psr\Http\Message\ResponseInterface, but rejected with "%s" instead.'; + $message = \sprintf($message, \is_object($error) ? \get_class($error) : \gettype($error)); + + $previous = null; + + if ($error instanceof \Throwable || $error instanceof \Exception) { + $previous = $error; + } + + $exception = new \RuntimeException($message, 0, $previous); + + $that->emit('error', array($exception)); + return $that->writeError($conn, Response::STATUS_INTERNAL_SERVER_ERROR, $request); + } + ); + } + + /** @internal */ + public function writeError(ConnectionInterface $conn, $code, ServerRequestInterface $request) + { + $response = new Response( + $code, + array( + 'Content-Type' => 'text/plain', + 'Connection' => 'close' // we do not want to keep the connection open after an error + ), + 'Error ' . $code + ); + + // append reason phrase to response body if known + $reason = $response->getReasonPhrase(); + if ($reason !== '') { + $body = $response->getBody(); + $body->seek(0, SEEK_END); + $body->write(': ' . $reason); + } + + $this->handleResponse($conn, $request, $response); + } + + + /** @internal */ + public function handleResponse(ConnectionInterface $connection, ServerRequestInterface $request, ResponseInterface $response) + { + // return early and close response body if connection is already closed + $body = $response->getBody(); + if (!$connection->isWritable()) { + $body->close(); + return; + } + + $code = $response->getStatusCode(); + $method = $request->getMethod(); + + // assign HTTP protocol version from request automatically + $version = $request->getProtocolVersion(); + $response = $response->withProtocolVersion($version); + + // assign default "Server" header automatically + if (!$response->hasHeader('Server')) { + $response = $response->withHeader('Server', 'ReactPHP/1'); + } elseif ($response->getHeaderLine('Server') === ''){ + $response = $response->withoutHeader('Server'); + } + + // assign default "Date" header from current time automatically + if (!$response->hasHeader('Date')) { + // IMF-fixdate = day-name "," SP date1 SP time-of-day SP GMT + $response = $response->withHeader('Date', gmdate('D, d M Y H:i:s') . ' GMT'); + } elseif ($response->getHeaderLine('Date') === ''){ + $response = $response->withoutHeader('Date'); + } + + // assign "Content-Length" header automatically + $chunked = false; + if (($method === 'CONNECT' && $code >= 200 && $code < 300) || ($code >= 100 && $code < 200) || $code === Response::STATUS_NO_CONTENT) { + // 2xx response to CONNECT and 1xx and 204 MUST NOT include Content-Length or Transfer-Encoding header + $response = $response->withoutHeader('Content-Length'); + } elseif ($code === Response::STATUS_NOT_MODIFIED && ($response->hasHeader('Content-Length') || $body->getSize() === 0)) { + // 304 Not Modified: preserve explicit Content-Length and preserve missing header if body is empty + } elseif ($body->getSize() !== null) { + // assign Content-Length header when using a "normal" buffered body string + $response = $response->withHeader('Content-Length', (string)$body->getSize()); + } elseif (!$response->hasHeader('Content-Length') && $version === '1.1') { + // assign chunked transfer-encoding if no 'content-length' is given for HTTP/1.1 responses + $chunked = true; + } + + // assign "Transfer-Encoding" header automatically + if ($chunked) { + $response = $response->withHeader('Transfer-Encoding', 'chunked'); + } else { + // remove any Transfer-Encoding headers unless automatically enabled above + $response = $response->withoutHeader('Transfer-Encoding'); + } + + // assign "Connection" header automatically + $persist = false; + if ($code === Response::STATUS_SWITCHING_PROTOCOLS) { + // 101 (Switching Protocols) response uses Connection: upgrade header + // This implies that this stream now uses another protocol and we + // may not persist this connection for additional requests. + $response = $response->withHeader('Connection', 'upgrade'); + } elseif (\strtolower($request->getHeaderLine('Connection')) === 'close' || \strtolower($response->getHeaderLine('Connection')) === 'close') { + // obey explicit "Connection: close" request header or response header if present + $response = $response->withHeader('Connection', 'close'); + } elseif ($version === '1.1') { + // HTTP/1.1 assumes persistent connection support by default, so we don't need to inform client + $persist = true; + } elseif (strtolower($request->getHeaderLine('Connection')) === 'keep-alive') { + // obey explicit "Connection: keep-alive" request header and inform client + $persist = true; + $response = $response->withHeader('Connection', 'keep-alive'); + } else { + // remove any Connection headers unless automatically enabled above + $response = $response->withoutHeader('Connection'); + } + + // 101 (Switching Protocols) response (for Upgrade request) forwards upgraded data through duplex stream + // 2xx (Successful) response to CONNECT forwards tunneled application data through duplex stream + if (($code === Response::STATUS_SWITCHING_PROTOCOLS || ($method === 'CONNECT' && $code >= 200 && $code < 300)) && $body instanceof HttpBodyStream && $body->input instanceof WritableStreamInterface) { + if ($request->getBody()->isReadable()) { + // request is still streaming => wait for request close before forwarding following data from connection + $request->getBody()->on('close', function () use ($connection, $body) { + if ($body->input->isWritable()) { + $connection->pipe($body->input); + $connection->resume(); + } + }); + } elseif ($body->input->isWritable()) { + // request already closed => forward following data from connection + $connection->pipe($body->input); + $connection->resume(); + } + } + + // build HTTP response header by appending status line and header fields + $headers = "HTTP/" . $version . " " . $code . " " . $response->getReasonPhrase() . "\r\n"; + foreach ($response->getHeaders() as $name => $values) { + foreach ($values as $value) { + $headers .= $name . ": " . $value . "\r\n"; + } + } + + // response to HEAD and 1xx, 204 and 304 responses MUST NOT include a body + // exclude status 101 (Switching Protocols) here for Upgrade request handling above + if ($method === 'HEAD' || ($code >= 100 && $code < 200 && $code !== Response::STATUS_SWITCHING_PROTOCOLS) || $code === Response::STATUS_NO_CONTENT || $code === Response::STATUS_NOT_MODIFIED) { + $body->close(); + $body = ''; + } + + // this is a non-streaming response body or the body stream already closed? + if (!$body instanceof ReadableStreamInterface || !$body->isReadable()) { + // add final chunk if a streaming body is already closed and uses `Transfer-Encoding: chunked` + if ($body instanceof ReadableStreamInterface && $chunked) { + $body = "0\r\n\r\n"; + } + + // write response headers and body + $connection->write($headers . "\r\n" . $body); + + // either wait for next request over persistent connection or end connection + if ($persist) { + $this->parser->handle($connection); + } else { + $connection->end(); + } + return; + } + + $connection->write($headers . "\r\n"); + + if ($chunked) { + $body = new ChunkedEncoder($body); + } + + // Close response stream once connection closes. + // Note that this TCP/IP close detection may take some time, + // in particular this may only fire on a later read/write attempt. + $connection->on('close', array($body, 'close')); + + // write streaming body and then wait for next request over persistent connection + if ($persist) { + $body->pipe($connection, array('end' => false)); + $parser = $this->parser; + $body->on('end', function () use ($connection, $parser, $body) { + $connection->removeListener('close', array($body, 'close')); + $parser->handle($connection); + }); + } else { + $body->pipe($connection); + } + } +} diff --git a/vendor/react/http/src/Io/Transaction.php b/vendor/react/http/src/Io/Transaction.php new file mode 100644 index 0000000..7bf7008 --- /dev/null +++ b/vendor/react/http/src/Io/Transaction.php @@ -0,0 +1,303 @@ +<?php + +namespace React\Http\Io; + +use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\UriInterface; +use RingCentral\Psr7\Request; +use RingCentral\Psr7\Uri; +use React\EventLoop\LoopInterface; +use React\Http\Message\ResponseException; +use React\Promise\Deferred; +use React\Promise\PromiseInterface; +use React\Stream\ReadableStreamInterface; + +/** + * @internal + */ +class Transaction +{ + private $sender; + private $loop; + + // context: http.timeout (ini_get('default_socket_timeout'): 60) + private $timeout; + + // context: http.follow_location (true) + private $followRedirects = true; + + // context: http.max_redirects (10) + private $maxRedirects = 10; + + // context: http.ignore_errors (false) + private $obeySuccessCode = true; + + private $streaming = false; + + private $maximumSize = 16777216; // 16 MiB = 2^24 bytes + + public function __construct(Sender $sender, LoopInterface $loop) + { + $this->sender = $sender; + $this->loop = $loop; + } + + /** + * @param array $options + * @return self returns new instance, without modifying existing instance + */ + public function withOptions(array $options) + { + $transaction = clone $this; + foreach ($options as $name => $value) { + if (property_exists($transaction, $name)) { + // restore default value if null is given + if ($value === null) { + $default = new self($this->sender, $this->loop); + $value = $default->$name; + } + + $transaction->$name = $value; + } + } + + return $transaction; + } + + public function send(RequestInterface $request) + { + $deferred = new Deferred(function () use (&$deferred) { + if (isset($deferred->pending)) { + $deferred->pending->cancel(); + unset($deferred->pending); + } + }); + + $deferred->numRequests = 0; + + // use timeout from options or default to PHP's default_socket_timeout (60) + $timeout = (float)($this->timeout !== null ? $this->timeout : ini_get("default_socket_timeout")); + + $loop = $this->loop; + $this->next($request, $deferred)->then( + function (ResponseInterface $response) use ($deferred, $loop, &$timeout) { + if (isset($deferred->timeout)) { + $loop->cancelTimer($deferred->timeout); + unset($deferred->timeout); + } + $timeout = -1; + $deferred->resolve($response); + }, + function ($e) use ($deferred, $loop, &$timeout) { + if (isset($deferred->timeout)) { + $loop->cancelTimer($deferred->timeout); + unset($deferred->timeout); + } + $timeout = -1; + $deferred->reject($e); + } + ); + + if ($timeout < 0) { + return $deferred->promise(); + } + + $body = $request->getBody(); + if ($body instanceof ReadableStreamInterface && $body->isReadable()) { + $that = $this; + $body->on('close', function () use ($that, $deferred, &$timeout) { + if ($timeout >= 0) { + $that->applyTimeout($deferred, $timeout); + } + }); + } else { + $this->applyTimeout($deferred, $timeout); + } + + return $deferred->promise(); + } + + /** + * @internal + * @param Deferred $deferred + * @param number $timeout + * @return void + */ + public function applyTimeout(Deferred $deferred, $timeout) + { + $deferred->timeout = $this->loop->addTimer($timeout, function () use ($timeout, $deferred) { + $deferred->reject(new \RuntimeException( + 'Request timed out after ' . $timeout . ' seconds' + )); + if (isset($deferred->pending)) { + $deferred->pending->cancel(); + unset($deferred->pending); + } + }); + } + + private function next(RequestInterface $request, Deferred $deferred) + { + $this->progress('request', array($request)); + + $that = $this; + ++$deferred->numRequests; + + $promise = $this->sender->send($request); + + if (!$this->streaming) { + $promise = $promise->then(function ($response) use ($deferred, $that) { + return $that->bufferResponse($response, $deferred); + }); + } + + $deferred->pending = $promise; + + return $promise->then( + function (ResponseInterface $response) use ($request, $that, $deferred) { + return $that->onResponse($response, $request, $deferred); + } + ); + } + + /** + * @internal + * @param ResponseInterface $response + * @return PromiseInterface Promise<ResponseInterface, Exception> + */ + public function bufferResponse(ResponseInterface $response, $deferred) + { + $stream = $response->getBody(); + + $size = $stream->getSize(); + if ($size !== null && $size > $this->maximumSize) { + $stream->close(); + return \React\Promise\reject(new \OverflowException( + 'Response body size of ' . $size . ' bytes exceeds maximum of ' . $this->maximumSize . ' bytes', + \defined('SOCKET_EMSGSIZE') ? \SOCKET_EMSGSIZE : 0 + )); + } + + // body is not streaming => already buffered + if (!$stream instanceof ReadableStreamInterface) { + return \React\Promise\resolve($response); + } + + // buffer stream and resolve with buffered body + $maximumSize = $this->maximumSize; + $promise = \React\Promise\Stream\buffer($stream, $maximumSize)->then( + function ($body) use ($response) { + return $response->withBody(new BufferedBody($body)); + }, + function ($e) use ($stream, $maximumSize) { + // try to close stream if buffering fails (or is cancelled) + $stream->close(); + + if ($e instanceof \OverflowException) { + $e = new \OverflowException( + 'Response body size exceeds maximum of ' . $maximumSize . ' bytes', + \defined('SOCKET_EMSGSIZE') ? \SOCKET_EMSGSIZE : 0 + ); + } + + throw $e; + } + ); + + $deferred->pending = $promise; + + return $promise; + } + + /** + * @internal + * @param ResponseInterface $response + * @param RequestInterface $request + * @throws ResponseException + * @return ResponseInterface|PromiseInterface + */ + public function onResponse(ResponseInterface $response, RequestInterface $request, $deferred) + { + $this->progress('response', array($response, $request)); + + // follow 3xx (Redirection) response status codes if Location header is present and not explicitly disabled + // @link https://tools.ietf.org/html/rfc7231#section-6.4 + if ($this->followRedirects && ($response->getStatusCode() >= 300 && $response->getStatusCode() < 400) && $response->hasHeader('Location')) { + return $this->onResponseRedirect($response, $request, $deferred); + } + + // only status codes 200-399 are considered to be valid, reject otherwise + if ($this->obeySuccessCode && ($response->getStatusCode() < 200 || $response->getStatusCode() >= 400)) { + throw new ResponseException($response); + } + + // resolve our initial promise + return $response; + } + + /** + * @param ResponseInterface $response + * @param RequestInterface $request + * @return PromiseInterface + * @throws \RuntimeException + */ + private function onResponseRedirect(ResponseInterface $response, RequestInterface $request, Deferred $deferred) + { + // resolve location relative to last request URI + $location = Uri::resolve($request->getUri(), $response->getHeaderLine('Location')); + + $request = $this->makeRedirectRequest($request, $location); + $this->progress('redirect', array($request)); + + if ($deferred->numRequests >= $this->maxRedirects) { + throw new \RuntimeException('Maximum number of redirects (' . $this->maxRedirects . ') exceeded'); + } + + return $this->next($request, $deferred); + } + + /** + * @param RequestInterface $request + * @param UriInterface $location + * @return RequestInterface + */ + private function makeRedirectRequest(RequestInterface $request, UriInterface $location) + { + $originalHost = $request->getUri()->getHost(); + $request = $request + ->withoutHeader('Host') + ->withoutHeader('Content-Type') + ->withoutHeader('Content-Length'); + + // Remove authorization if changing hostnames (but not if just changing ports or protocols). + if ($location->getHost() !== $originalHost) { + $request = $request->withoutHeader('Authorization'); + } + + // naïve approach.. + $method = ($request->getMethod() === 'HEAD') ? 'HEAD' : 'GET'; + + return new Request($method, $location, $request->getHeaders()); + } + + private function progress($name, array $args = array()) + { + return; + + echo $name; + + foreach ($args as $arg) { + echo ' '; + if ($arg instanceof ResponseInterface) { + echo 'HTTP/' . $arg->getProtocolVersion() . ' ' . $arg->getStatusCode() . ' ' . $arg->getReasonPhrase(); + } elseif ($arg instanceof RequestInterface) { + echo $arg->getMethod() . ' ' . $arg->getRequestTarget() . ' HTTP/' . $arg->getProtocolVersion(); + } else { + echo $arg; + } + } + + echo PHP_EOL; + } +} diff --git a/vendor/react/http/src/Io/UploadedFile.php b/vendor/react/http/src/Io/UploadedFile.php new file mode 100644 index 0000000..f2a6c9e --- /dev/null +++ b/vendor/react/http/src/Io/UploadedFile.php @@ -0,0 +1,130 @@ +<?php + +namespace React\Http\Io; + +use Psr\Http\Message\StreamInterface; +use Psr\Http\Message\UploadedFileInterface; +use InvalidArgumentException; +use RuntimeException; + +/** + * [Internal] Implementation of the PSR-7 `UploadedFileInterface` + * + * This is used internally to represent each incoming file upload. + * + * Note that this is an internal class only and nothing you should usually care + * about. See the `UploadedFileInterface` for more details. + * + * @see UploadedFileInterface + * @internal + */ +final class UploadedFile implements UploadedFileInterface +{ + /** + * @var StreamInterface + */ + private $stream; + + /** + * @var int + */ + private $size; + + /** + * @var int + */ + private $error; + + /** + * @var string + */ + private $filename; + + /** + * @var string + */ + private $mediaType; + + /** + * @param StreamInterface $stream + * @param int $size + * @param int $error + * @param string $filename + * @param string $mediaType + */ + public function __construct(StreamInterface $stream, $size, $error, $filename, $mediaType) + { + $this->stream = $stream; + $this->size = $size; + + if (!\is_int($error) || !\in_array($error, array( + \UPLOAD_ERR_OK, + \UPLOAD_ERR_INI_SIZE, + \UPLOAD_ERR_FORM_SIZE, + \UPLOAD_ERR_PARTIAL, + \UPLOAD_ERR_NO_FILE, + \UPLOAD_ERR_NO_TMP_DIR, + \UPLOAD_ERR_CANT_WRITE, + \UPLOAD_ERR_EXTENSION, + ))) { + throw new InvalidArgumentException( + 'Invalid error code, must be an UPLOAD_ERR_* constant' + ); + } + $this->error = $error; + $this->filename = $filename; + $this->mediaType = $mediaType; + } + + /** + * {@inheritdoc} + */ + public function getStream() + { + if ($this->error !== \UPLOAD_ERR_OK) { + throw new RuntimeException('Cannot retrieve stream due to upload error'); + } + + return $this->stream; + } + + /** + * {@inheritdoc} + */ + public function moveTo($targetPath) + { + throw new RuntimeException('Not implemented'); + } + + /** + * {@inheritdoc} + */ + public function getSize() + { + return $this->size; + } + + /** + * {@inheritdoc} + */ + public function getError() + { + return $this->error; + } + + /** + * {@inheritdoc} + */ + public function getClientFilename() + { + return $this->filename; + } + + /** + * {@inheritdoc} + */ + public function getClientMediaType() + { + return $this->mediaType; + } +} |