This commit is contained in:
2024-11-27 21:34:07 +02:00
parent 638bcba894
commit b6d1215999
190 changed files with 31518 additions and 0 deletions

View File

@@ -0,0 +1,172 @@
<?php
namespace React\Http\Io;
use Psr\Http\Message\MessageInterface;
use Psr\Http\Message\StreamInterface;
/**
* [Internal] Abstract HTTP message base class (PSR-7)
*
* @internal
* @see MessageInterface
*/
abstract class AbstractMessage implements MessageInterface
{
/**
* [Internal] Regex used to match all request header fields into an array, thanks to @kelunik for checking the HTTP specs and coming up with this regex
*
* @internal
* @var string
*/
const REGEX_HEADERS = '/^([^()<>@,;:\\\"\/\[\]?={}\x00-\x20\x7F]++):[\x20\x09]*+((?:[\x20\x09]*+[\x21-\x7E\x80-\xFF]++)*+)[\x20\x09]*+[\r]?+\n/m';
/** @var array<string,string[]> */
private $headers = array();
/** @var array<string,string> */
private $headerNamesLowerCase = array();
/** @var string */
private $protocolVersion;
/** @var StreamInterface */
private $body;
/**
* @param string $protocolVersion
* @param array<string,string|string[]> $headers
* @param StreamInterface $body
*/
protected function __construct($protocolVersion, array $headers, StreamInterface $body)
{
foreach ($headers as $name => $value) {
if ($value !== array()) {
if (\is_array($value)) {
foreach ($value as &$one) {
$one = (string) $one;
}
} else {
$value = array((string) $value);
}
$lower = \strtolower($name);
if (isset($this->headerNamesLowerCase[$lower])) {
$value = \array_merge($this->headers[$this->headerNamesLowerCase[$lower]], $value);
unset($this->headers[$this->headerNamesLowerCase[$lower]]);
}
$this->headers[$name] = $value;
$this->headerNamesLowerCase[$lower] = $name;
}
}
$this->protocolVersion = (string) $protocolVersion;
$this->body = $body;
}
public function getProtocolVersion()
{
return $this->protocolVersion;
}
public function withProtocolVersion($version)
{
if ((string) $version === $this->protocolVersion) {
return $this;
}
$message = clone $this;
$message->protocolVersion = (string) $version;
return $message;
}
public function getHeaders()
{
return $this->headers;
}
public function hasHeader($name)
{
return isset($this->headerNamesLowerCase[\strtolower($name)]);
}
public function getHeader($name)
{
$lower = \strtolower($name);
return isset($this->headerNamesLowerCase[$lower]) ? $this->headers[$this->headerNamesLowerCase[$lower]] : array();
}
public function getHeaderLine($name)
{
return \implode(', ', $this->getHeader($name));
}
public function withHeader($name, $value)
{
if ($value === array()) {
return $this->withoutHeader($name);
} elseif (\is_array($value)) {
foreach ($value as &$one) {
$one = (string) $one;
}
} else {
$value = array((string) $value);
}
$lower = \strtolower($name);
if (isset($this->headerNamesLowerCase[$lower]) && $this->headerNamesLowerCase[$lower] === (string) $name && $this->headers[$this->headerNamesLowerCase[$lower]] === $value) {
return $this;
}
$message = clone $this;
if (isset($message->headerNamesLowerCase[$lower])) {
unset($message->headers[$message->headerNamesLowerCase[$lower]]);
}
$message->headers[$name] = $value;
$message->headerNamesLowerCase[$lower] = $name;
return $message;
}
public function withAddedHeader($name, $value)
{
if ($value === array()) {
return $this;
}
return $this->withHeader($name, \array_merge($this->getHeader($name), \is_array($value) ? $value : array($value)));
}
public function withoutHeader($name)
{
$lower = \strtolower($name);
if (!isset($this->headerNamesLowerCase[$lower])) {
return $this;
}
$message = clone $this;
unset($message->headers[$message->headerNamesLowerCase[$lower]], $message->headerNamesLowerCase[$lower]);
return $message;
}
public function getBody()
{
return $this->body;
}
public function withBody(StreamInterface $body)
{
if ($body === $this->body) {
return $this;
}
$message = clone $this;
$message->body = $body;
return $message;
}
}

View File

@@ -0,0 +1,156 @@
<?php
namespace React\Http\Io;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\UriInterface;
use React\Http\Message\Uri;
/**
* [Internal] Abstract HTTP request base class (PSR-7)
*
* @internal
* @see RequestInterface
*/
abstract class AbstractRequest extends AbstractMessage implements RequestInterface
{
/** @var ?string */
private $requestTarget;
/** @var string */
private $method;
/** @var UriInterface */
private $uri;
/**
* @param string $method
* @param string|UriInterface $uri
* @param array<string,string|string[]> $headers
* @param StreamInterface $body
* @param string unknown $protocolVersion
*/
protected function __construct(
$method,
$uri,
array $headers,
StreamInterface $body,
$protocolVersion
) {
if (\is_string($uri)) {
$uri = new Uri($uri);
} elseif (!$uri instanceof UriInterface) {
throw new \InvalidArgumentException(
'Argument #2 ($uri) expected string|Psr\Http\Message\UriInterface'
);
}
// assign default `Host` request header from URI unless already given explicitly
$host = $uri->getHost();
if ($host !== '') {
foreach ($headers as $name => $value) {
if (\strtolower($name) === 'host' && $value !== array()) {
$host = '';
break;
}
}
if ($host !== '') {
$port = $uri->getPort();
if ($port !== null && (!($port === 80 && $uri->getScheme() === 'http') || !($port === 443 && $uri->getScheme() === 'https'))) {
$host .= ':' . $port;
}
$headers = array('Host' => $host) + $headers;
}
}
parent::__construct($protocolVersion, $headers, $body);
$this->method = $method;
$this->uri = $uri;
}
public function getRequestTarget()
{
if ($this->requestTarget !== null) {
return $this->requestTarget;
}
$target = $this->uri->getPath();
if ($target === '') {
$target = '/';
}
if (($query = $this->uri->getQuery()) !== '') {
$target .= '?' . $query;
}
return $target;
}
public function withRequestTarget($requestTarget)
{
if ((string) $requestTarget === $this->requestTarget) {
return $this;
}
$request = clone $this;
$request->requestTarget = (string) $requestTarget;
return $request;
}
public function getMethod()
{
return $this->method;
}
public function withMethod($method)
{
if ((string) $method === $this->method) {
return $this;
}
$request = clone $this;
$request->method = (string) $method;
return $request;
}
public function getUri()
{
return $this->uri;
}
public function withUri(UriInterface $uri, $preserveHost = false)
{
if ($uri === $this->uri) {
return $this;
}
$request = clone $this;
$request->uri = $uri;
$host = $uri->getHost();
$port = $uri->getPort();
if ($port !== null && $host !== '' && (!($port === 80 && $uri->getScheme() === 'http') || !($port === 443 && $uri->getScheme() === 'https'))) {
$host .= ':' . $port;
}
// update `Host` request header if URI contains a new host and `$preserveHost` is false
if ($host !== '' && (!$preserveHost || $request->getHeaderLine('Host') === '')) {
// first remove all headers before assigning `Host` header to ensure it always comes first
foreach (\array_keys($request->getHeaders()) as $name) {
$request = $request->withoutHeader($name);
}
// add `Host` header first, then all other original headers
$request = $request->withHeader('Host', $host);
foreach ($this->withoutHeader('Host')->getHeaders() as $name => $value) {
$request = $request->withHeader($name, $value);
}
}
return $request;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -0,0 +1,137 @@
<?php
namespace React\Http\Io;
use Psr\Http\Message\UriInterface;
use React\EventLoop\LoopInterface;
use React\EventLoop\TimerInterface;
use React\Promise\PromiseInterface;
use React\Socket\ConnectionInterface;
use React\Socket\ConnectorInterface;
/**
* [Internal] Manages outgoing HTTP connections for the HTTP client
*
* @internal
* @final
*/
class ClientConnectionManager
{
/** @var ConnectorInterface */
private $connector;
/** @var LoopInterface */
private $loop;
/** @var string[] */
private $idleUris = array();
/** @var ConnectionInterface[] */
private $idleConnections = array();
/** @var TimerInterface[] */
private $idleTimers = array();
/** @var \Closure[] */
private $idleStreamHandlers = array();
/** @var float */
private $maximumTimeToKeepAliveIdleConnection = 0.001;
public function __construct(ConnectorInterface $connector, LoopInterface $loop)
{
$this->connector = $connector;
$this->loop = $loop;
}
/**
* @return PromiseInterface<ConnectionInterface>
*/
public function connect(UriInterface $uri)
{
$scheme = $uri->getScheme();
if ($scheme !== 'https' && $scheme !== 'http') {
return \React\Promise\reject(new \InvalidArgumentException(
'Invalid request URL given'
));
}
$port = $uri->getPort();
if ($port === null) {
$port = $scheme === 'https' ? 443 : 80;
}
$uri = ($scheme === 'https' ? 'tls://' : '') . $uri->getHost() . ':' . $port;
// Reuse idle connection for same URI if available
foreach ($this->idleConnections as $id => $connection) {
if ($this->idleUris[$id] === $uri) {
assert($this->idleStreamHandlers[$id] instanceof \Closure);
$connection->removeListener('close', $this->idleStreamHandlers[$id]);
$connection->removeListener('data', $this->idleStreamHandlers[$id]);
$connection->removeListener('error', $this->idleStreamHandlers[$id]);
assert($this->idleTimers[$id] instanceof TimerInterface);
$this->loop->cancelTimer($this->idleTimers[$id]);
unset($this->idleUris[$id], $this->idleConnections[$id], $this->idleTimers[$id], $this->idleStreamHandlers[$id]);
return \React\Promise\resolve($connection);
}
}
// Create new connection if no idle connection to same URI is available
return $this->connector->connect($uri);
}
/**
* Hands back an idle connection to the connection manager for possible future reuse.
*
* @return void
*/
public function keepAlive(UriInterface $uri, ConnectionInterface $connection)
{
$scheme = $uri->getScheme();
assert($scheme === 'https' || $scheme === 'http');
$port = $uri->getPort();
if ($port === null) {
$port = $scheme === 'https' ? 443 : 80;
}
$this->idleUris[] = ($scheme === 'https' ? 'tls://' : '') . $uri->getHost() . ':' . $port;
$this->idleConnections[] = $connection;
$that = $this;
$cleanUp = function () use ($connection, $that) {
// call public method to support legacy PHP 5.3
$that->cleanUpConnection($connection);
};
// clean up and close connection when maximum time to keep-alive idle connection has passed
$this->idleTimers[] = $this->loop->addTimer($this->maximumTimeToKeepAliveIdleConnection, $cleanUp);
// clean up and close connection when unexpected close/data/error event happens during idle time
$this->idleStreamHandlers[] = $cleanUp;
$connection->on('close', $cleanUp);
$connection->on('data', $cleanUp);
$connection->on('error', $cleanUp);
}
/**
* @internal
* @return void
*/
public function cleanUpConnection(ConnectionInterface $connection) // private (PHP 5.4+)
{
$id = \array_search($connection, $this->idleConnections, true);
if ($id === false) {
return;
}
assert(\is_int($id));
assert($this->idleTimers[$id] instanceof TimerInterface);
$this->loop->cancelTimer($this->idleTimers[$id]);
unset($this->idleUris[$id], $this->idleConnections[$id], $this->idleTimers[$id], $this->idleStreamHandlers[$id]);
$connection->close();
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace React\Http\Io;
/** @internal */
class ClientRequestState
{
/** @var int */
public $numRequests = 0;
/** @var ?\React\Promise\PromiseInterface */
public $pending = null;
/** @var ?\React\EventLoop\TimerInterface */
public $timeout = null;
}

View File

@@ -0,0 +1,307 @@
<?php
namespace React\Http\Io;
use Evenement\EventEmitter;
use Psr\Http\Message\MessageInterface;
use Psr\Http\Message\RequestInterface;
use React\Http\Message\Response;
use React\Socket\ConnectionInterface;
use React\Stream\WritableStreamInterface;
/**
* @event response
* @event drain
* @event error
* @event close
* @internal
*/
class ClientRequestStream extends EventEmitter implements WritableStreamInterface
{
const STATE_INIT = 0;
const STATE_WRITING_HEAD = 1;
const STATE_HEAD_WRITTEN = 2;
const STATE_END = 3;
/** @var ClientConnectionManager */
private $connectionManager;
/** @var RequestInterface */
private $request;
/** @var ?ConnectionInterface */
private $connection;
/** @var string */
private $buffer = '';
private $responseFactory;
private $state = self::STATE_INIT;
private $ended = false;
private $pendingWrites = '';
public function __construct(ClientConnectionManager $connectionManager, RequestInterface $request)
{
$this->connectionManager = $connectionManager;
$this->request = $request;
}
public function isWritable()
{
return self::STATE_END > $this->state && !$this->ended;
}
private function writeHead()
{
$this->state = self::STATE_WRITING_HEAD;
$expected = 0;
$headers = "{$this->request->getMethod()} {$this->request->getRequestTarget()} HTTP/{$this->request->getProtocolVersion()}\r\n";
foreach ($this->request->getHeaders() as $name => $values) {
if (\strpos($name, ':') !== false) {
$expected = -1;
break;
}
foreach ($values as $value) {
$headers .= "$name: $value\r\n";
++$expected;
}
}
/** @var array $m legacy PHP 5.3 only */
if (!\preg_match('#^\S+ \S+ HTTP/1\.[01]\r\n#m', $headers) || \substr_count($headers, "\n") !== ($expected + 1) || (\PHP_VERSION_ID >= 50400 ? \preg_match_all(AbstractMessage::REGEX_HEADERS, $headers) : \preg_match_all(AbstractMessage::REGEX_HEADERS, $headers, $m)) !== $expected) {
$this->closeError(new \InvalidArgumentException('Unable to send request with invalid request headers'));
return;
}
$connectionRef = &$this->connection;
$stateRef = &$this->state;
$pendingWrites = &$this->pendingWrites;
$that = $this;
$promise = $this->connectionManager->connect($this->request->getUri());
$promise->then(
function (ConnectionInterface $connection) use ($headers, &$connectionRef, &$stateRef, &$pendingWrites, $that) {
$connectionRef = $connection;
assert($connectionRef instanceof ConnectionInterface);
$connection->on('drain', array($that, 'handleDrain'));
$connection->on('data', array($that, 'handleData'));
$connection->on('end', array($that, 'handleEnd'));
$connection->on('error', array($that, 'handleError'));
$connection->on('close', array($that, 'close'));
$more = $connection->write($headers . "\r\n" . $pendingWrites);
assert($stateRef === ClientRequestStream::STATE_WRITING_HEAD);
$stateRef = ClientRequestStream::STATE_HEAD_WRITTEN;
// clear pending writes if non-empty
if ($pendingWrites !== '') {
$pendingWrites = '';
if ($more) {
$that->emit('drain');
}
}
},
array($this, 'closeError')
);
$this->on('close', function() use ($promise) {
$promise->cancel();
});
}
public function write($data)
{
if (!$this->isWritable()) {
return false;
}
// write directly to connection stream if already available
if (self::STATE_HEAD_WRITTEN <= $this->state) {
return $this->connection->write($data);
}
// otherwise buffer and try to establish connection
$this->pendingWrites .= $data;
if (self::STATE_WRITING_HEAD > $this->state) {
$this->writeHead();
}
return false;
}
public function end($data = null)
{
if (!$this->isWritable()) {
return;
}
if (null !== $data) {
$this->write($data);
} else if (self::STATE_WRITING_HEAD > $this->state) {
$this->writeHead();
}
$this->ended = true;
}
/** @internal */
public function handleDrain()
{
$this->emit('drain');
}
/** @internal */
public function handleData($data)
{
$this->buffer .= $data;
// buffer until double CRLF (or double LF for compatibility with legacy servers)
$eom = \strpos($this->buffer, "\r\n\r\n");
$eomLegacy = \strpos($this->buffer, "\n\n");
if ($eom !== false || $eomLegacy !== false) {
try {
if ($eom !== false && ($eomLegacy === false || $eom < $eomLegacy)) {
$response = Response::parseMessage(\substr($this->buffer, 0, $eom + 2));
$bodyChunk = (string) \substr($this->buffer, $eom + 4);
} else {
$response = Response::parseMessage(\substr($this->buffer, 0, $eomLegacy + 1));
$bodyChunk = (string) \substr($this->buffer, $eomLegacy + 2);
}
} catch (\InvalidArgumentException $exception) {
$this->closeError($exception);
return;
}
// response headers successfully received => remove listeners for connection events
$connection = $this->connection;
assert($connection instanceof ConnectionInterface);
$connection->removeListener('drain', array($this, 'handleDrain'));
$connection->removeListener('data', array($this, 'handleData'));
$connection->removeListener('end', array($this, 'handleEnd'));
$connection->removeListener('error', array($this, 'handleError'));
$connection->removeListener('close', array($this, 'close'));
$this->connection = null;
$this->buffer = '';
// take control over connection handling and check if we can reuse the connection once response body closes
$that = $this;
$request = $this->request;
$connectionManager = $this->connectionManager;
$successfulEndReceived = false;
$input = $body = new CloseProtectionStream($connection);
$input->on('close', function () use ($connection, $that, $connectionManager, $request, $response, &$successfulEndReceived) {
// only reuse connection after successful response and both request and response allow keep alive
if ($successfulEndReceived && $connection->isReadable() && $that->hasMessageKeepAliveEnabled($response) && $that->hasMessageKeepAliveEnabled($request)) {
$connectionManager->keepAlive($request->getUri(), $connection);
} else {
$connection->close();
}
$that->close();
});
// determine length of response body
$length = null;
$code = $response->getStatusCode();
if ($this->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');
}
$response = $response->withBody($body = new ReadableBodyStream($body, $length));
$body->on('end', function () use (&$successfulEndReceived) {
$successfulEndReceived = true;
});
// emit response with streaming response body (see `Sender`)
$this->emit('response', array($response, $body));
// re-emit HTTP response body to trigger body parsing if parts of it are buffered
if ($bodyChunk !== '') {
$input->handleData($bodyChunk);
} elseif ($length === 0) {
$input->handleEnd();
}
}
}
/** @internal */
public function handleEnd()
{
$this->closeError(new \RuntimeException(
"Connection ended before receiving response"
));
}
/** @internal */
public function handleError(\Exception $error)
{
$this->closeError(new \RuntimeException(
"An error occurred in the underlying stream",
0,
$error
));
}
/** @internal */
public function closeError(\Exception $error)
{
if (self::STATE_END <= $this->state) {
return;
}
$this->emit('error', array($error));
$this->close();
}
public function close()
{
if (self::STATE_END <= $this->state) {
return;
}
$this->state = self::STATE_END;
$this->pendingWrites = '';
$this->buffer = '';
if ($this->connection instanceof ConnectionInterface) {
$this->connection->close();
$this->connection = null;
}
$this->emit('close');
$this->removeAllListeners();
}
/**
* @internal
* @return bool
* @link https://www.rfc-editor.org/rfc/rfc9112#section-9.3
* @link https://www.rfc-editor.org/rfc/rfc7230#section-6.1
*/
public function hasMessageKeepAliveEnabled(MessageInterface $message)
{
// @link https://www.rfc-editor.org/rfc/rfc9110#section-7.6.1
$connectionOptions = \array_map('trim', \explode(',', \strtolower($message->getHeaderLine('Connection'))));
if (\in_array('close', $connectionOptions, true)) {
return false;
}
if ($message->getProtocolVersion() === '1.1') {
return true;
}
if (\in_array('keep-alive', $connectionOptions, true)) {
return true;
}
return false;
}
}

54
vendor/react/http/src/Io/Clock.php vendored Normal file
View File

@@ -0,0 +1,54 @@
<?php
namespace React\Http\Io;
use React\EventLoop\LoopInterface;
/**
* [internal] Clock source that returns current timestamp and memoize clock for same tick
*
* This is mostly used as an internal optimization to avoid unneeded syscalls to
* get the current system time multiple times within the same loop tick. For the
* purpose of the HTTP server, the clock is assumed to not change to a
* significant degree within the same loop tick. If you need a high precision
* clock source, you may want to use `\hrtime()` instead (PHP 7.3+).
*
* The API is modelled to resemble the PSR-20 `ClockInterface` (in draft at the
* time of writing this), but uses a `float` return value for performance
* reasons instead.
*
* Note that this is an internal class only and nothing you should usually care
* about for outside use.
*
* @internal
*/
class Clock
{
/** @var LoopInterface $loop */
private $loop;
/** @var ?float */
private $now;
public function __construct(LoopInterface $loop)
{
$this->loop = $loop;
}
/** @return float */
public function now()
{
if ($this->now === null) {
$this->now = \microtime(true);
// remember clock for current loop tick only and update on next tick
$now =& $this->now;
$this->loop->futureTick(function () use (&$now) {
assert($now !== null);
$now = null;
});
}
return $this->now;
}
}

View File

@@ -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));
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}
}

48
vendor/react/http/src/Io/IniUtil.php vendored Normal file
View File

@@ -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;
}
}

View File

@@ -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'));
}
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,345 @@
<?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;
/**
* Based on $maxInputVars and $maxFileUploads
*
* @var int
*/
private $maxMultipartBodyParts;
/**
* 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 $multipartBodyPartCount = 0;
private $postCount = 0;
private $filesCount = 0;
private $emptyCount = 0;
private $cursor = 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;
$this->maxMultipartBodyParts = $this->maxInputVars + $this->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->multipartBodyPartCount = 0;
$this->cursor = 0;
$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)
$this->cursor = \strpos($buffer, $boundary . "\r\n");
while ($this->cursor !== false) {
// search following boundary (preceded by newline)
// ignore last if not followed by boundary (SHOULD end with "--")
$this->cursor += $len + 2;
$end = \strpos($buffer, "\r\n" . $boundary, $this->cursor);
if ($end === false) {
break;
}
// parse one part and continue searching for next
$this->parsePart(\substr($buffer, $this->cursor, $end - $this->cursor));
$this->cursor = $end;
if (++$this->multipartBodyPartCount > $this->maxMultipartBodyParts) {
break;
}
}
}
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;
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -0,0 +1,179 @@
<?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;
/** @var Clock */
private $clock;
/** @var array<string|int,array<string,string>> */
private $connectionParams = array();
public function __construct(Clock $clock)
{
$this->clock = $clock;
}
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
);
} 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 ConnectionInterface $connection
* @return ServerRequestInterface
* @throws \InvalidArgumentException
* @internal
*/
public function parseRequest($headers, ConnectionInterface $connection)
{
// reuse same connection params for all server params for this connection
$cid = \PHP_VERSION_ID < 70200 ? \spl_object_hash($connection) : \spl_object_id($connection);
if (isset($this->connectionParams[$cid])) {
$serverParams = $this->connectionParams[$cid];
} else {
// assign new server params for new connection
$serverParams = array();
// scheme is `http` unless TLS is used
$localSocketUri = $connection->getLocalAddress();
$localParts = $localSocketUri === null ? array() : \parse_url($localSocketUri);
if (isset($localParts['scheme']) && $localParts['scheme'] === 'tls') {
$serverParams['HTTPS'] = 'on';
}
// 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'];
}
// apply REMOTE_ADDR and REMOTE_PORT if source address is known
// address should always be known, unless this is over Unix domain sockets (UDS)
$remoteSocketUri = $connection->getRemoteAddress();
if ($remoteSocketUri !== null) {
$remoteAddress = \parse_url($remoteSocketUri);
$serverParams['REMOTE_ADDR'] = $remoteAddress['host'];
$serverParams['REMOTE_PORT'] = $remoteAddress['port'];
}
// remember server params for all requests from this connection, reset on connection close
$this->connectionParams[$cid] = $serverParams;
$params =& $this->connectionParams;
$connection->on('close', function () use (&$params, $cid) {
assert(\is_array($params));
unset($params[$cid]);
});
}
// create new obj implementing ServerRequestInterface by preserving all
// previous properties and restoring original request-target
$serverParams['REQUEST_TIME'] = (int) ($now = $this->clock->now());
$serverParams['REQUEST_TIME_FLOAT'] = $now;
return ServerRequest::parseMessage($headers, $serverParams);
}
}

152
vendor/react/http/src/Io/Sender.php vendored Normal file
View File

@@ -0,0 +1,152 @@
<?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\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)
{
return new self(new HttpClient(new ClientConnectionManager($connector, $loop)));
}
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)
{
// support HTTP/1.1 and HTTP/1.0 only, ensured by `Browser` already
assert(\in_array($request->getProtocolVersion(), array('1.0', '1.1'), true));
$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 && $size !== 0 && $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;
}
// automatically add `Authorization: Basic …` request header if URL includes `user:pass@host`
if ($request->getUri()->getUserInfo() !== '' && !$request->hasHeader('Authorization')) {
$request = $request->withHeader('Authorization', 'Basic ' . \base64_encode($request->getUri()->getUserInfo()));
}
$requestStream = $this->http->request($request);
$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) use ($deferred, $request) {
$deferred->resolve($response);
});
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();
}
}

View File

@@ -0,0 +1,405 @@
<?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\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;
/** @var Clock */
private $clock;
/**
* 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->callback = $requestHandler;
$this->clock = new Clock($loop);
$this->parser = new RequestHeaderParser($this->clock);
$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
$connectionOnCloseResponseCancelerHandler = function () {};
if ($response instanceof PromiseInterface && \method_exists($response, 'cancel')) {
$connectionOnCloseResponseCanceler = function () use ($response) {
$response->cancel();
};
$connectionOnCloseResponseCancelerHandler = function () use ($connectionOnCloseResponseCanceler, $conn) {
if ($connectionOnCloseResponseCanceler !== null) {
$conn->removeListener('close', $connectionOnCloseResponseCanceler);
}
};
$conn->on('close', $connectionOnCloseResponseCanceler);
}
// 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);
}
)->then($connectionOnCloseResponseCancelerHandler, $connectionOnCloseResponseCancelerHandler);
}
/** @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', (int) $this->clock->now()) . ' 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 ($method === 'HEAD' && $response->hasHeader('Content-Length')) {
// HEAD Request: preserve explicit 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
$expected = 0;
$headers = "HTTP/" . $version . " " . $code . " " . $response->getReasonPhrase() . "\r\n";
foreach ($response->getHeaders() as $name => $values) {
if (\strpos($name, ':') !== false) {
$expected = -1;
break;
}
foreach ($values as $value) {
$headers .= $name . ": " . $value . "\r\n";
++$expected;
}
}
/** @var array $m legacy PHP 5.3 only */
if ($code < 100 || $code > 999 || \substr_count($headers, "\n") !== ($expected + 1) || (\PHP_VERSION_ID >= 50400 ? \preg_match_all(AbstractMessage::REGEX_HEADERS, $headers) : \preg_match_all(AbstractMessage::REGEX_HEADERS, $headers, $m)) !== $expected) {
$this->emit('error', array(new \InvalidArgumentException('Unable to send response with invalid response headers')));
$this->writeError($connection, Response::STATUS_INTERNAL_SERVER_ERROR, $request);
return;
}
// 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);
}
}
}

330
vendor/react/http/src/Io/Transaction.php vendored Normal file
View File

@@ -0,0 +1,330 @@
<?php
namespace React\Http\Io;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\UriInterface;
use React\EventLoop\LoopInterface;
use React\Http\Message\Response;
use React\Http\Message\ResponseException;
use React\Http\Message\Uri;
use React\Promise\Deferred;
use React\Promise\Promise;
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)
{
$state = new ClientRequestState();
$deferred = new Deferred(function () use ($state) {
if ($state->pending !== null) {
$state->pending->cancel();
$state->pending = null;
}
});
// 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, $state)->then(
function (ResponseInterface $response) use ($state, $deferred, $loop, &$timeout) {
if ($state->timeout !== null) {
$loop->cancelTimer($state->timeout);
$state->timeout = null;
}
$timeout = -1;
$deferred->resolve($response);
},
function ($e) use ($state, $deferred, $loop, &$timeout) {
if ($state->timeout !== null) {
$loop->cancelTimer($state->timeout);
$state->timeout = null;
}
$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, $state, &$timeout) {
if ($timeout >= 0) {
$that->applyTimeout($deferred, $state, $timeout);
}
});
} else {
$this->applyTimeout($deferred, $state, $timeout);
}
return $deferred->promise();
}
/**
* @internal
* @param number $timeout
* @return void
*/
public function applyTimeout(Deferred $deferred, ClientRequestState $state, $timeout)
{
$state->timeout = $this->loop->addTimer($timeout, function () use ($timeout, $deferred, $state) {
$deferred->reject(new \RuntimeException(
'Request timed out after ' . $timeout . ' seconds'
));
if ($state->pending !== null) {
$state->pending->cancel();
$state->pending = null;
}
});
}
private function next(RequestInterface $request, Deferred $deferred, ClientRequestState $state)
{
$this->progress('request', array($request));
$that = $this;
++$state->numRequests;
$promise = $this->sender->send($request);
if (!$this->streaming) {
$promise = $promise->then(function ($response) use ($deferred, $state, $that) {
return $that->bufferResponse($response, $deferred, $state);
});
}
$state->pending = $promise;
return $promise->then(
function (ResponseInterface $response) use ($request, $that, $deferred, $state) {
return $that->onResponse($response, $request, $deferred, $state);
}
);
}
/**
* @internal
* @return PromiseInterface Promise<ResponseInterface, Exception>
*/
public function bufferResponse(ResponseInterface $response, Deferred $deferred, ClientRequestState $state)
{
$body = $response->getBody();
$size = $body->getSize();
if ($size !== null && $size > $this->maximumSize) {
$body->close();
return \React\Promise\reject(new \OverflowException(
'Response body size of ' . $size . ' bytes exceeds maximum of ' . $this->maximumSize . ' bytes',
\defined('SOCKET_EMSGSIZE') ? \SOCKET_EMSGSIZE : 90
));
}
// body is not streaming => already buffered
if (!$body instanceof ReadableStreamInterface) {
return \React\Promise\resolve($response);
}
/** @var ?\Closure $closer */
$closer = null;
$maximumSize = $this->maximumSize;
return $state->pending = new Promise(function ($resolve, $reject) use ($body, $maximumSize, $response, &$closer) {
// resolve with current buffer when stream closes successfully
$buffer = '';
$body->on('close', $closer = function () use (&$buffer, $response, $maximumSize, $resolve, $reject) {
$resolve($response->withBody(new BufferedBody($buffer)));
});
// buffer response body data in memory
$body->on('data', function ($data) use (&$buffer, $maximumSize, $body, $closer, $reject) {
$buffer .= $data;
// close stream and reject promise if limit is exceeded
if (isset($buffer[$maximumSize])) {
$buffer = '';
assert($closer instanceof \Closure);
$body->removeListener('close', $closer);
$body->close();
$reject(new \OverflowException(
'Response body size exceeds maximum of ' . $maximumSize . ' bytes',
\defined('SOCKET_EMSGSIZE') ? \SOCKET_EMSGSIZE : 90
));
}
});
// reject buffering if body emits error
$body->on('error', function (\Exception $e) use ($reject) {
$reject(new \RuntimeException(
'Error while buffering response body: ' . $e->getMessage(),
$e->getCode(),
$e
));
});
}, function () use ($body, &$closer) {
// cancelled buffering: remove close handler to avoid resolving, then close and reject
assert($closer instanceof \Closure);
$body->removeListener('close', $closer);
$body->close();
throw new \RuntimeException('Cancelled buffering response body');
});
}
/**
* @internal
* @throws ResponseException
* @return ResponseInterface|PromiseInterface
*/
public function onResponse(ResponseInterface $response, RequestInterface $request, Deferred $deferred, ClientRequestState $state)
{
$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, $state);
}
// 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
* @param Deferred $deferred
* @param ClientRequestState $state
* @return PromiseInterface
* @throws \RuntimeException
*/
private function onResponseRedirect(ResponseInterface $response, RequestInterface $request, Deferred $deferred, ClientRequestState $state)
{
// resolve location relative to last request URI
$location = Uri::resolve($request->getUri(), new Uri($response->getHeaderLine('Location')));
$request = $this->makeRedirectRequest($request, $location, $response->getStatusCode());
$this->progress('redirect', array($request));
if ($state->numRequests >= $this->maxRedirects) {
throw new \RuntimeException('Maximum number of redirects (' . $this->maxRedirects . ') exceeded');
}
return $this->next($request, $deferred, $state);
}
/**
* @param RequestInterface $request
* @param UriInterface $location
* @param int $statusCode
* @return RequestInterface
* @throws \RuntimeException
*/
private function makeRedirectRequest(RequestInterface $request, UriInterface $location, $statusCode)
{
// Remove authorization if changing hostnames (but not if just changing ports or protocols).
$originalHost = $request->getUri()->getHost();
if ($location->getHost() !== $originalHost) {
$request = $request->withoutHeader('Authorization');
}
$request = $request->withoutHeader('Host')->withUri($location);
if ($statusCode === Response::STATUS_TEMPORARY_REDIRECT || $statusCode === Response::STATUS_PERMANENT_REDIRECT) {
if ($request->getBody() instanceof ReadableStreamInterface) {
throw new \RuntimeException('Unable to redirect request with streaming body');
}
} else {
$request = $request
->withMethod($request->getMethod() === 'HEAD' ? 'HEAD' : 'GET')
->withoutHeader('Content-Type')
->withoutHeader('Content-Length')
->withBody(new BufferedBody(''));
}
return $request;
}
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;
}
}

View File

@@ -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;
}
}