/** * @coroutine * * @param string $data * @param float|int $timeout * @param bool $end * * @return \Generator * * @resolve int Number of bytes written to the stream. * * @throws \Icicle\Stream\Exception\UnwritableException If the stream is no longer writable. */ protected function send(string $data, float $timeout = 0, bool $end = false) : \Generator { if (!$this->isWritable()) { throw new UnwritableException('The stream is no longer writable.'); } $this->buffer->push($data); if (null !== $this->delayed && !$this->buffer->isEmpty()) { $delayed = $this->delayed; $this->delayed = null; $delayed->resolve($this->remove()); } if ($end) { if ($this->buffer->isEmpty()) { $this->free(); } else { $this->writable = false; } } if (0 !== $this->hwm && $this->buffer->getLength() > $this->hwm) { $awaitable = new Delayed($this->onCancelled = $this->onCancelled ?: function () { $this->free(); }); $this->queue->push($awaitable); if ($timeout) { $awaitable = $awaitable->timeout($timeout); } (yield $awaitable); } return strlen($data); }
/** * @coroutine * * Writes a value to the stream. * * The given value will be coerced to a string before being written. The resulting * string will be written to the internal buffer; if the buffer is full, the entire * buffer will be flushed to the stream. * * @param mixed $text A printable value that can be coerced to a string. * * @return \Generator * * @resolve int Number of bytes written to the buffer. * * @throws \Icicle\Awaitable\Exception\TimeoutException If the operation times out. * @throws \Icicle\Stream\Exception\UnwritableException If the stream is no longer writable. * @throws \Icicle\Stream\Exception\ClosedException If the stream is unexpectedly closed. */ public function write(string $text) : \Generator { $length = strlen($text); $this->buffer->push($text); if ($this->autoFlush || $this->buffer->getLength() > $this->bufferSize) { yield from $this->flush(); } return $length; }
/** * @coroutine * * @param int $maxSize Max frame size. * @param float|int $timeout * * @return \Generator * * @resolve \Icicle\WebSocket\Protocol\Rfc6455Frame * * @throws \Icicle\WebSocket\Exception\FrameException */ public function read(int $maxSize, float $timeout = 0) : \Generator { $buffer = new Buffer(); try { do { $buffer->push(yield from $this->socket->read(0, null, $timeout)); } while ($buffer->getLength() < 2); $bytes = unpack('Cflags/Clength', $buffer->shift(2)); $rsv = $bytes['flags'] & self::RSV_MASK; $opcode = $bytes['flags'] & self::OPCODE_MASK; $final = (bool) ($bytes['flags'] & self::FIN_MASK); $masked = (bool) ($bytes['length'] & self::MASK_FLAG_MASK); $size = $bytes['length'] & self::LENGTH_MASK; if ($masked === $this->masked) { throw new FrameException(sprintf('Received %s frame.', $masked ? 'masked' : 'unmasked')); } if ($size === self::TWO_BYTE_LENGTH_FLAG) { while ($buffer->getLength() < 2) { $buffer->push(yield from $this->socket->read(0, null, $timeout)); } $bytes = unpack('nlength', $buffer->shift(2)); $size = $bytes['length']; if ($size < self::TWO_BYTE_LENGTH_FLAG) { throw new FrameException('Frame format error.'); } } elseif ($size === self::EIGHT_BYTE_LENGTH_FLAG) { while ($buffer->getLength() < 8) { $buffer->push(yield from $this->socket->read(0, null, $timeout)); } $bytes = unpack('Nhigh/Nlow', $buffer->shift(8)); $size = $bytes['high'] << 32 | $bytes['low']; if ($size < self::TWO_BYTE_MAX_LENGTH) { throw new FrameException('Frame format error.'); } } if ($size > $maxSize) { throw new PolicyException('Frame size exceeded max allowed size.'); } if ($masked) { while ($buffer->getLength() < self::MASK_LENGTH) { $buffer->push(yield from $this->socket->read(0, null, $timeout)); } $mask = $buffer->shift(self::MASK_LENGTH); } while ($buffer->getLength() < $size) { $buffer->push(yield from $this->socket->read(0, null, $timeout)); } $data = $buffer->shift($size); if ($masked) { $data ^= str_repeat($mask, (int) (($size + self::MASK_LENGTH - 1) / self::MASK_LENGTH)); } } finally { if (!$buffer->isEmpty()) { $this->socket->unshift((string) $buffer); } } return new Frame($opcode, $data, $rsv, $final); }
/** * @coroutine * * Reads and parses characters from the stream according to a format. * * The format string is of the same format as `sscanf()`. * * @param string $format The parse format. * @param float|int $timeout Number of seconds until the returned promise is rejected with a TimeoutException * if no data is received. Use 0 for no timeout. * * @return \Generator * * @resolve array An array of parsed values. * * @throws \Icicle\Awaitable\Exception\TimeoutException If the operation times out. * @throws \Icicle\Stream\Exception\UnreadableException If the stream is no longer readable. * @throws \Icicle\Stream\Exception\ClosedException If the stream is unexpectedly closed. * * @see http://php.net/sscanf */ public function scan(string $format, float $timeout = 0) : \Generator { // Read from the stream chunk by chunk, attempting to satisfy the format // string each time until the format successfully parses or the end of // the stream is reached. while (true) { $result = sscanf((string) $this->buffer, $format . '%n'); $length = $result ? array_pop($result) : null; // If the format string was satisfied, consume the used characters and // return the parsed results. if ($length !== null && $length < $this->buffer->getLength()) { $this->buffer->shift($length); return $result; } // Read more into the buffer if possible. if ($this->stream->isReadable()) { $this->buffer->push(yield from $this->stream->read(0, null, $timeout)); } else { // Format string can't be satisfied. return []; } } }
/** * {@inheritdoc} * * @throws \Icicle\Http\Exception\MessageException If an invalid chunk length is found. */ protected function send(string $data, float $timeout = 0, bool $end = false) : \Generator { $this->buffer->push($data); $data = ''; while (!$this->buffer->isEmpty()) { if (0 === $this->length) { // Read chunk length. if (false === ($position = $this->buffer->search("\r\n"))) { return yield from parent::send($data, $timeout, $end); } $length = rtrim($this->buffer->remove($position + 2), "\r\n"); if ($position = strpos($length, ';')) { $length = substr($length, 0, $position); } if (!preg_match('/^[a-f0-9]+$/i', $length)) { yield from parent::send('', $timeout, true); throw new MessageException(Response::BAD_REQUEST, 'Invalid chunk length.'); } $this->length = hexdec($length) + 2; if (2 === $this->length) { // Termination chunk. $end = true; } } if (2 < $this->length) { // Read chunk. $buffer = $this->buffer->remove($this->length - 2); $this->length -= strlen($buffer); $data .= $buffer; } if (2 >= $this->length) { // Remove \r\n after chunk. $this->length -= strlen($this->buffer->remove($this->length)); } } return yield from parent::send($data, $timeout, $end); }
/** * @param \Icicle\Stream\Structures\Buffer $buffer * @param \Icicle\Socket\Socket $socket * @param float|int $timeout * * @return \Generator * * @throws \Icicle\Http\Exception\MessageException * @throws \Icicle\Http\Exception\ParseException */ protected function readHeaders(Buffer $buffer, Socket $socket, float $timeout = 0) : \Generator { $size = 0; $headers = []; do { while (false === ($position = $buffer->search("\r\n"))) { if ($buffer->getLength() >= $this->maxSize) { throw new MessageException(Response::REQUEST_HEADER_TOO_LARGE, sprintf('Message header exceeded maximum size of %d bytes.', $this->maxSize)); } $buffer->push(yield from $socket->read(0, null, $timeout)); } $length = $position + 2; $line = $buffer->shift($length); if (2 === $length) { return $headers; } $size += $length; $parts = explode(':', $line, 2); if (2 !== count($parts)) { throw new ParseException('Found header without colon.'); } list($name, $value) = $parts; $name = Message\decode($name); $value = Message\decode(trim($value)); // No check for case as Message class will automatically combine similarly named headers. if (!isset($headers[$name])) { $headers[$name] = [$value]; } else { $headers[$name][] = $value; } } while ($size < $this->maxSize); throw new MessageException(Response::REQUEST_HEADER_TOO_LARGE, sprintf('Message header exceeded maximum size of %d bytes.', $this->maxSize)); }
/** * {@inheritdoc} */ public function getLength() : int { return $this->buffer->getLength(); }