rfc6455/src/Messaging/MessageBuffer.php
2016-02-08 22:21:56 -05:00

228 lines
6.3 KiB
PHP

<?php
namespace Ratchet\RFC6455\Messaging;
class MessageBuffer {
/**
* @var \Ratchet\RFC6455\Messaging\CloseFrameChecker
*/
private $closeFrameChecker;
/**
* @var callable
*/
private $exceptionFactory;
/**
* @var \Ratchet\RFC6455\Messaging\Message
*/
private $messageBuffer;
/**
* @var \Ratchet\RFC6455\Messaging\Frame
*/
private $frameBuffer;
/**
* @var callable
*/
private $onMessage;
/**
* @var callable
*/
private $onControl;
/**
* @var bool
*/
private $checkForMask;
function __construct(
CloseFrameChecker $frameChecker,
callable $onMessage,
callable $onControl = null,
$expectMask = true,
$exceptionFactory = null
) {
$this->closeFrameChecker = $frameChecker;
$this->checkForMask = (bool)$expectMask;
$this->exceptionFactory ?: $this->exceptionFactory = function($msg) {
return new \UnderflowException($msg);
};
$this->onMessage = $onMessage;
$this->onControl = $onControl ?: function() {};
}
/**
* @param string $data
* @return null
*/
public function onData($data) {
$this->messageBuffer ?: $this->messageBuffer = $this->newMessage();
$this->frameBuffer ?: $this->frameBuffer = $this->newFrame();
$this->frameBuffer->addBuffer($data);
if (!$this->frameBuffer->isCoalesced()) {
return;
}
$onMessage = $this->onMessage;
$onControl = $this->onControl;
$this->frameBuffer = $this->frameCheck($this->frameBuffer);
$overflow = $this->frameBuffer->extractOverflow();
$this->frameBuffer->unMaskPayload();
$opcode = $this->frameBuffer->getOpcode();
if ($opcode > 2) {
$onControl($this->frameBuffer);
if (Frame::OP_CLOSE === $opcode) {
return;
}
} else {
$this->messageBuffer->addFrame($this->frameBuffer);
}
$this->frameBuffer = null;
if ($this->messageBuffer->isCoalesced()) {
$msgCheck = $this->checkMessage($this->messageBuffer);
if (true !== $msgCheck) {
$onControl($this->newCloseFrame($msgCheck));
} else {
$onMessage($this->messageBuffer);
}
$this->messageBuffer = null;
}
if (strlen($overflow) > 0) {
$this->onData($overflow); // PHP doesn't do tail recursion :(
}
}
/**
* Check a frame to be added to the current message buffer
* @param \Ratchet\RFC6455\Messaging\FrameInterface|FrameInterface $frame
* @return \Ratchet\RFC6455\Messaging\FrameInterface|FrameInterface
*/
public function frameCheck(FrameInterface $frame) {
if (false !== $frame->getRsv1() ||
false !== $frame->getRsv2() ||
false !== $frame->getRsv3()
) {
return $this->newCloseFrame(Frame::CLOSE_PROTOCOL);
}
if ($this->checkForMask && !$frame->isMasked()) {
return $this->newCloseFrame(Frame::CLOSE_PROTOCOL);
}
$opcode = $frame->getOpcode();
if ($opcode > 2) {
if ($frame->getPayloadLength() > 125 || !$frame->isFinal()) {
return $this->newCloseFrame(Frame::CLOSE_PROTOCOL);
}
switch ($opcode) {
case Frame::OP_CLOSE:
$closeCode = 0;
$bin = $frame->getPayload();
if (empty($bin)) {
return $this->newCloseFrame(Frame::CLOSE_NORMAL);
}
if (strlen($bin) == 1) {
return $this->newCloseFrame(Frame::CLOSE_PROTOCOL);
}
if (strlen($bin) >= 2) {
list($closeCode) = array_merge(unpack('n*', substr($bin, 0, 2)));
}
$checker = $this->closeFrameChecker;
if (!$checker($closeCode)) {
return $this->newCloseFrame(Frame::CLOSE_PROTOCOL);
}
if (!$this->checkUtf8(substr($bin, 2))) {
return $this->newCloseFrame(Frame::CLOSE_BAD_PAYLOAD);
}
return $this->newCloseFrame(Frame::CLOSE_NORMAL);
break;
case Frame::OP_PING:
case Frame::OP_PONG:
break;
default:
return $this->newCloseFrame(Frame::CLOSE_PROTOCOL);
break;
}
return $frame;
}
if (Frame::OP_CONTINUE == $frame->getOpcode() && 0 == count($this->messageBuffer)) {
return $this->newCloseFrame(Frame::CLOSE_PROTOCOL);
}
if (count($this->messageBuffer) > 0 && Frame::OP_CONTINUE != $frame->getOpcode()) {
return $this->newCloseFrame(Frame::CLOSE_PROTOCOL);
}
return $frame;
}
/**
* Determine if a message is valid
* @param \Ratchet\RFC6455\Messaging\MessageInterface
* @return bool|int true if valid - false if incomplete - int of recommended close code
*/
public function checkMessage(MessageInterface $message) {
if (!$message->isBinary()) {
if (!$this->checkUtf8($message->getPayload())) {
return Frame::CLOSE_BAD_PAYLOAD;
}
}
return true;
}
private function checkUtf8($string) {
if (extension_loaded('mbstring')) {
return mb_check_encoding($string, 'UTF-8');
}
return preg_match('//u', $string);
}
/**
* @return \Ratchet\RFC6455\Messaging\MessageInterface
*/
public function newMessage() {
return new Message;
}
/**
* @param string|null $payload
* @param bool|null $final
* @param int|null $opcode
* @return \Ratchet\RFC6455\Messaging\FrameInterface
*/
public function newFrame($payload = null, $final = null, $opcode = null) {
return new Frame($payload, $final, $opcode, $this->exceptionFactory);
}
public function newCloseFrame($code) {
return $this->newFrame(pack('n', $code), true, Frame::OP_CLOSE);
}
}