/** * Retrieve the next frame from the internal buffer. * * @param string $buffer Binary data to feed to the parser. * @param int &$requiredBytes The minimum number of bytes that must be * read to produce the next frame. * * @return Frame|null The frame parsed from the start of the buffer. * @throws ProtocolException The incoming data does not conform to the AMQP * specification. */ public function feed(string $buffer, int &$requiredBytes = 0) { $this->buffer .= $buffer; $availableBytes = \strlen($this->buffer); // not enough bytes for a frame ... if ($availableBytes < $this->requiredBytes) { $requiredBytes = $this->requiredBytes; return null; // we're still looking for the header ... } elseif ($this->requiredBytes === self::MINIMUM_FRAME_SIZE) { // now that we know the payload size we can add that to the number // of required bytes ... $this->requiredBytes += \unpack('N', \substr($this->buffer, self::HEADER_TYPE_SIZE + self::HEADER_CHANNEL_SIZE, self::HEADER_PAYLOAD_LENGTH_SIZE))[1]; // taking the payload into account we still don't have enough bytes // for the frame ... if ($availableBytes < $this->requiredBytes) { $requiredBytes = $this->requiredBytes; return null; } } // we've got enough bytes, check that the last byte is the end marker ... if (\ord($this->buffer[$this->requiredBytes - 1]) !== Constants::FRAME_END) { throw ProtocolException::create(\sprintf('Frame end marker (0x%02x) is invalid.', \ord($this->buffer[$this->requiredBytes - 1]))); } // read the (t)ype and (c)hannel then discard the header ... $fields = \unpack('Ct/nc', $this->buffer); $this->buffer = \substr($this->buffer, self::HEADER_SIZE); $type = $fields['t']; // read the frame ... if ($type === Constants::FRAME_METHOD) { $frame = $this->parseMethodFrame(); } elseif ($type === Constants::FRAME_HEADER) { $frame = $this->parseHeaderFrame(); } elseif ($type === Constants::FRAME_BODY) { $length = $this->requiredBytes - self::MINIMUM_FRAME_SIZE; $frame = new BodyFrame(); $frame->content = \substr($this->buffer, 0, $length); $this->buffer = \substr($this->buffer, $length); } elseif ($type === Constants::FRAME_HEARTBEAT) { if (self::MINIMUM_FRAME_SIZE !== $this->requiredBytes) { throw ProtocolException::create(\sprintf('Heartbeat frame payload size (%d) is invalid, must be zero.', $this->requiredBytes - self::MINIMUM_FRAME_SIZE)); } $frame = new HeartbeatFrame(); } else { throw ProtocolException::create(\sprintf('Frame type (0x%02x) is invalid.', $type)); } // discard the end marker ... $this->buffer = \substr($this->buffer, 1); $consumedBytes = $availableBytes - \strlen($this->buffer); // the frame lied about its payload size ... if ($consumedBytes !== $this->requiredBytes) { throw ProtocolException::create(\sprintf('Mismatch between frame size (%s) and consumed bytes (%s).', $this->requiredBytes, $consumedBytes)); } $this->requiredBytes = $requiredBytes = self::MINIMUM_FRAME_SIZE; $frame->frameChannelId = $fields['c']; return $frame; }
/** * Parse a table or array field. * * @return integer|array|boolean|double|string|null */ private function parseField() { $type = $this->buffer[0]; $this->buffer = substr($this->buffer, 1); // @todo bench switch vs if vs method map switch ($type) { case 's': return $this->parseSignedInt16(); case 'l': return $this->parseSignedInt64(); case 'x': return $this->parseByteArray(); case 't': return $this->parseUnsignedInt8() !== 0; case 'b': return $this->parseSignedInt8(); case 'I': return $this->parseSignedInt32(); case 'f': return $this->parseFloat(); case 'd': return $this->parseDouble(); case 'D': return $this->parseDecimal(); case 'S': return $this->parseLongString(); case 'A': return $this->parseArray(); case 'T': return $this->parseUnsignedInt64(); case 'F': return $this->parseTable(); case 'V': return null; } throw ProtocolException::create(\sprintf('table value type (0x%02x) is invalid or unrecognised.', ord($type))); }
/** * Notify the channel of an incoming frame. * * @recoil-coroutine * * @param IncomingFrame $frame The received frame. * * @throws ProtocolException The incoming frame violates the expectations of the AMQP protocol. */ public function dispatch(IncomingFrame $frame) : Generator { assert($frame->frameChannelId === $this->channelId); // Handle frames during normal operation ... if ($this->state === ChannelState::OPEN) { // The broker has closed the channel ... if ($frame instanceof ChannelCloseFrame) { (yield $this->onCloseByBroker($frame)); // The broker has acknowledged a client-side open after already opened ... } elseif ($frame instanceof ChannelOpenOkFrame) { goto unexpectedFrame; // The broker has acknowledged a client-side close that was never made ... } elseif ($frame instanceof ChannelCloseOkFrame) { goto unexpectedFrame; // The broker has requested a client->broker flow start/stop ... } elseif ($frame instanceof ChannelFlowFrame) { // Not suppported (and unused by RabbitMQ). // The AMQP spec dictates that method frames may not be interleaved // with content frames, which makes it unclear how the client should // respond to a flow frame if it's in the middle of transmitting content // We're expecting a method and we got one ... } elseif ($this->expectedFrame === Constants::FRAME_METHOD && $frame instanceof MethodFrame) { // The frame will be followed by content ... if ($frame instanceof ContentPrecursorFrame) { $this->expectedFrame = Constants::FRAME_HEADER; } $waitMethod = $frame::METHOD_ID; // Check if there is a suspended strand waiting for this frame // type as a response ... ($callers =& $this->callers[$waitMethod]) ?? null; if ($callers) { foreach ($callers as $id => $strand) { $sentMethod = $this->callersByWaitMethod[$waitMethod][$id]; unset($this->callers[$waitMethod][$id], $this->callersBySentMethod[$sentMethod][$id], $this->callersByWaitMethod[$waitMethod][$id]); $strand->send($frame); break; } // Otherwise dispatch the frame to any listeners ... } elseif (isset($this->listeners[$waitMethod])) { assert(false, 'not implemented'); // foreach ($this->listeners[$waitMethod] as $callback) { // $callback(null, $frame); // } } // We're expecting a content header and we got one ... } elseif ($this->expectedFrame === Constants::FRAME_HEADER && $frame instanceof HeaderFrame) { // There are no body frames to follow ... if ($frame->contentLength === 0) { $this->expectedFrame = Constants::FRAME_METHOD; if ($this->onIncomingContent) { $callback = $this->onIncomingContent; $this->onIncomingContent = null; $callback(null, $frame, true); } // There are body frames to follow ... } else { $this->expectedFrame = Constants::FRAME_BODY; $this->incomingBytesRemaining = $frame->contentLength; if ($this->onIncomingContent) { $callback = $this->onIncomingContent; $callback(null, $frame, false); } } // We're expecting some content and we got some ... } elseif ($this->expectedFrame === Constants::FRAME_BODY && $frame instanceof BodyFrame) { $this->incomingBytesRemaining -= strlen($frame->content); if ($this->incomingBytesRemaining < 0) { throw ProtocolException::create(\sprintf('Expected content length was exceeded on channel #%d by %d byte(s).', $this->channelId, -$this->incomingBytesRemaining)); } elseif ($this->incomingBytesRemaining === 0) { $this->expectedFrame = Constants::FRAME_METHOD; if ($this->onIncomingContent) { $callback = $this->onIncomingContent; $this->onIncomingContent = null; $callback(null, $frame, true); } } elseif ($this->onIncomingContent) { $callback = $this->onIncomingContent; $callback(null, $frame, false); } // Otherwise, bail with a protocol exception ... } else { goto unexpectedFrame; } // The broker has acknowledged our open request ... } elseif ($this->state === ChannelState::OPENING && $frame instanceof ChannelOpenOkFrame) { $this->state = ChannelState::OPEN; $strand = $this->openingStrand; $this->openingStrand = null; (yield Recoil::resume($strand)); // We're waiting on a close acknowledgement, blackhole everything else ... } elseif ($this->state === ChannelState::CLOSING) { if ($frame instanceof ChannelCloseOkFrame) { (yield $this->onClose()); } } else { unexpectedFrame: throw ProtocolException::unexpectedFrame($frame, \sprintf('received on channel #%d, state: %d, expect: %d', $this->channelId, $this->state, $this->expectedFrame)); } }
private function parseHeaderFrame() { $fields = \unpack("na/nb/Jc/nd", $this->buffer); $this->buffer = \substr($this->buffer, 14); $class = $fields["a"]; $flags = $fields["d"]; // class "basic" if ($class === 60) { $frame = new Basic\BasicHeaderFrame(); $frame->contentLength = $fields["c"]; // consume "content-type" (shortstr) if ($flags & 32768) { $length = \ord($this->buffer); $frame->contentType = \substr($this->buffer, 1, $length); $this->buffer = \substr($this->buffer, 1 + $length); } // consume "content-encoding" (shortstr) if ($flags & 16384) { $length = \ord($this->buffer); $frame->contentEncoding = \substr($this->buffer, 1, $length); $this->buffer = \substr($this->buffer, 1 + $length); } // consume "headers" (table) if ($flags & 8192) { $frame->headers = $this->tableParser->parse($this->buffer); } // consume "delivery-mode" (octet) if ($flags & 4096) { list(, $frame->deliveryMode) = \unpack('c', $this->buffer); $this->buffer = \substr($this->buffer, 1); } // consume "priority" (octet) if ($flags & 2048) { list(, $frame->priority) = \unpack('c', $this->buffer); $this->buffer = \substr($this->buffer, 1); } // consume "correlation-id" (shortstr) if ($flags & 1024) { $length = \ord($this->buffer); $frame->correlationId = \substr($this->buffer, 1, $length); $this->buffer = \substr($this->buffer, 1 + $length); } // consume "reply-to" (shortstr) if ($flags & 512) { $length = \ord($this->buffer); $frame->replyTo = \substr($this->buffer, 1, $length); $this->buffer = \substr($this->buffer, 1 + $length); } // consume "expiration" (shortstr) if ($flags & 256) { $length = \ord($this->buffer); $frame->expiration = \substr($this->buffer, 1, $length); $this->buffer = \substr($this->buffer, 1 + $length); } // consume "message-id" (shortstr) if ($flags & 128) { $length = \ord($this->buffer); $frame->messageId = \substr($this->buffer, 1, $length); $this->buffer = \substr($this->buffer, 1 + $length); } // consume "timestamp" (timestamp) if ($flags & 64) { list(, $frame->timestamp) = \unpack('J', $this->buffer); $this->buffer = \substr($this->buffer, 8); } // consume "type" (shortstr) if ($flags & 32) { $length = \ord($this->buffer); $frame->type = \substr($this->buffer, 1, $length); $this->buffer = \substr($this->buffer, 1 + $length); } // consume "user-id" (shortstr) if ($flags & 16) { $length = \ord($this->buffer); $frame->userId = \substr($this->buffer, 1, $length); $this->buffer = \substr($this->buffer, 1 + $length); } // consume "app-id" (shortstr) if ($flags & 8) { $length = \ord($this->buffer); $frame->appId = \substr($this->buffer, 1, $length); $this->buffer = \substr($this->buffer, 1 + $length); } // consume "cluster-id" (shortstr) if ($flags & 4) { $length = \ord($this->buffer); $frame->clusterId = \substr($this->buffer, 1, $length); $this->buffer = \substr($this->buffer, 1 + $length); } return $frame; } throw ProtocolException::create("Frame class (" . $class . ") is invalid or does not support content frames."); }
private function parseMethodFrame() { list(, $class, $method) = \unpack("n2", $this->buffer); $this->buffer = \substr($this->buffer, 4); // class "connection" if ($class === 10) { // method "connection.start" if ($method === 10) { $frame = new Connection\ConnectionStartFrame(); // consume (a) "version-major" (octet) // consume (b) "version-minor" (octet) $fields = \unpack('ca/cb', $this->buffer); $this->buffer = \substr($this->buffer, 2); $frame->versionMajor = $fields["a"]; $frame->versionMinor = $fields["b"]; // consume "server-properties" (table) $frame->serverProperties = $this->tableParser->parse($this->buffer); // consume "mechanisms" (longstr) list(, $length) = \unpack("N", $this->buffer); $frame->mechanisms = \substr($this->buffer, 4, $length); $this->buffer = \substr($this->buffer, 4 + $length); // consume "locales" (longstr) list(, $length) = \unpack("N", $this->buffer); $frame->locales = \substr($this->buffer, 4, $length); $this->buffer = \substr($this->buffer, 4 + $length); return $frame; // method "connection.secure" } elseif ($method === 20) { $frame = new Connection\ConnectionSecureFrame(); // consume "challenge" (longstr) list(, $length) = \unpack("N", $this->buffer); $frame->challenge = \substr($this->buffer, 4, $length); $this->buffer = \substr($this->buffer, 4 + $length); return $frame; // method "connection.tune" } elseif ($method === 30) { $frame = new Connection\ConnectionTuneFrame(); // consume (a) "channel-max" (short) // consume (b) "frame-max" (long) // consume (c) "heartbeat" (short) $fields = \unpack('na/Nb/nc', $this->buffer); $this->buffer = \substr($this->buffer, 8); $frame->channelMax = $fields["a"]; $frame->frameMax = $fields["b"]; $frame->heartbeat = $fields["c"]; return $frame; // method "connection.open-ok" } elseif ($method === 41) { $frame = new Connection\ConnectionOpenOkFrame(); // consume "known-hosts" (shortstr) $length = \ord($this->buffer); $frame->knownHosts = \substr($this->buffer, 1, $length); $this->buffer = \substr($this->buffer, 1 + $length); return $frame; // method "connection.close" } elseif ($method === 50) { $frame = new Connection\ConnectionCloseFrame(); // consume "replyCode" (short) list(, $frame->replyCode) = \unpack('n', $this->buffer); $this->buffer = \substr($this->buffer, 2); // consume "reply-text" (shortstr) $length = \ord($this->buffer); $frame->replyText = \substr($this->buffer, 1, $length); $this->buffer = \substr($this->buffer, 1 + $length); // consume (a) "class-id" (short) // consume (b) "method-id" (short) $fields = \unpack('na/nb', $this->buffer); $this->buffer = \substr($this->buffer, 4); $frame->classId = $fields["a"]; $frame->methodId = $fields["b"]; return $frame; // method "connection.close-ok" } elseif ($method === 51) { return new Connection\ConnectionCloseOkFrame(); // method "connection.blocked" } elseif ($method === 60) { $frame = new Connection\ConnectionBlockedFrame(); // consume "reason" (shortstr) $length = \ord($this->buffer); $frame->reason = \substr($this->buffer, 1, $length); $this->buffer = \substr($this->buffer, 1 + $length); return $frame; // method "connection.unblocked" } elseif ($method === 61) { return new Connection\ConnectionUnblockedFrame(); } throw ProtocolException::create("Frame method (" . $method . ") is invalid for class \"connection\"."); // class "channel" } elseif ($class === 20) { // method "channel.open-ok" if ($method === 11) { $frame = new Channel\ChannelOpenOkFrame(); // consume "channel-id" (longstr) list(, $length) = \unpack("N", $this->buffer); $frame->channelId = \substr($this->buffer, 4, $length); $this->buffer = \substr($this->buffer, 4 + $length); return $frame; // method "channel.flow" } elseif ($method === 20) { $frame = new Channel\ChannelFlowFrame(); // consume "active" (bit) $frame->active = \ord($this->buffer) !== 0; $this->buffer = \substr($this->buffer, 1); return $frame; // method "channel.flow-ok" } elseif ($method === 21) { $frame = new Channel\ChannelFlowOkFrame(); // consume "active" (bit) $frame->active = \ord($this->buffer) !== 0; $this->buffer = \substr($this->buffer, 1); return $frame; // method "channel.close" } elseif ($method === 40) { $frame = new Channel\ChannelCloseFrame(); // consume "replyCode" (short) list(, $frame->replyCode) = \unpack('n', $this->buffer); $this->buffer = \substr($this->buffer, 2); // consume "reply-text" (shortstr) $length = \ord($this->buffer); $frame->replyText = \substr($this->buffer, 1, $length); $this->buffer = \substr($this->buffer, 1 + $length); // consume (a) "class-id" (short) // consume (b) "method-id" (short) $fields = \unpack('na/nb', $this->buffer); $this->buffer = \substr($this->buffer, 4); $frame->classId = $fields["a"]; $frame->methodId = $fields["b"]; return $frame; // method "channel.close-ok" } elseif ($method === 41) { return new Channel\ChannelCloseOkFrame(); } throw ProtocolException::create("Frame method (" . $method . ") is invalid for class \"channel\"."); // class "access" } elseif ($class === 30) { // method "access.request-ok" if ($method === 11) { $frame = new Access\AccessRequestOkFrame(); // consume "reserved1" (short) list(, $frame->reserved1) = \unpack('n', $this->buffer); $this->buffer = \substr($this->buffer, 2); return $frame; } throw ProtocolException::create("Frame method (" . $method . ") is invalid for class \"access\"."); // class "exchange" } elseif ($class === 40) { // method "exchange.declare-ok" if ($method === 11) { return new Exchange\ExchangeDeclareOkFrame(); // method "exchange.delete-ok" } elseif ($method === 21) { return new Exchange\ExchangeDeleteOkFrame(); // method "exchange.bind-ok" } elseif ($method === 31) { return new Exchange\ExchangeBindOkFrame(); // method "exchange.unbind-ok" } elseif ($method === 51) { return new Exchange\ExchangeUnbindOkFrame(); } throw ProtocolException::create("Frame method (" . $method . ") is invalid for class \"exchange\"."); // class "queue" } elseif ($class === 50) { // method "queue.declare-ok" if ($method === 11) { $frame = new Queue\QueueDeclareOkFrame(); // consume "queue" (shortstr) $length = \ord($this->buffer); $frame->queue = \substr($this->buffer, 1, $length); $this->buffer = \substr($this->buffer, 1 + $length); // consume (a) "message-count" (long) // consume (b) "consumer-count" (long) $fields = \unpack('Na/Nb', $this->buffer); $this->buffer = \substr($this->buffer, 8); $frame->messageCount = $fields["a"]; $frame->consumerCount = $fields["b"]; return $frame; // method "queue.bind-ok" } elseif ($method === 21) { return new Queue\QueueBindOkFrame(); // method "queue.purge-ok" } elseif ($method === 31) { $frame = new Queue\QueuePurgeOkFrame(); // consume "messageCount" (long) list(, $frame->messageCount) = \unpack('N', $this->buffer); $this->buffer = \substr($this->buffer, 4); return $frame; // method "queue.delete-ok" } elseif ($method === 41) { $frame = new Queue\QueueDeleteOkFrame(); // consume "messageCount" (long) list(, $frame->messageCount) = \unpack('N', $this->buffer); $this->buffer = \substr($this->buffer, 4); return $frame; // method "queue.unbind-ok" } elseif ($method === 51) { return new Queue\QueueUnbindOkFrame(); } throw ProtocolException::create("Frame method (" . $method . ") is invalid for class \"queue\"."); // class "basic" } elseif ($class === 60) { // method "basic.qos-ok" if ($method === 11) { return new Basic\BasicQosOkFrame(); // method "basic.consume-ok" } elseif ($method === 21) { $frame = new Basic\BasicConsumeOkFrame(); // consume "consumer-tag" (shortstr) $length = \ord($this->buffer); $frame->consumerTag = \substr($this->buffer, 1, $length); $this->buffer = \substr($this->buffer, 1 + $length); return $frame; // method "basic.cancel-ok" } elseif ($method === 31) { $frame = new Basic\BasicCancelOkFrame(); // consume "consumer-tag" (shortstr) $length = \ord($this->buffer); $frame->consumerTag = \substr($this->buffer, 1, $length); $this->buffer = \substr($this->buffer, 1 + $length); return $frame; // method "basic.return" } elseif ($method === 50) { $frame = new Basic\BasicReturnFrame(); // consume "replyCode" (short) list(, $frame->replyCode) = \unpack('n', $this->buffer); $this->buffer = \substr($this->buffer, 2); // consume "reply-text" (shortstr) $length = \ord($this->buffer); $frame->replyText = \substr($this->buffer, 1, $length); $this->buffer = \substr($this->buffer, 1 + $length); // consume "exchange" (shortstr) $length = \ord($this->buffer); $frame->exchange = \substr($this->buffer, 1, $length); $this->buffer = \substr($this->buffer, 1 + $length); // consume "routing-key" (shortstr) $length = \ord($this->buffer); $frame->routingKey = \substr($this->buffer, 1, $length); $this->buffer = \substr($this->buffer, 1 + $length); return $frame; // method "basic.deliver" } elseif ($method === 60) { $frame = new Basic\BasicDeliverFrame(); // consume "consumer-tag" (shortstr) $length = \ord($this->buffer); $frame->consumerTag = \substr($this->buffer, 1, $length); $this->buffer = \substr($this->buffer, 1 + $length); // consume "deliveryTag" (longlong) list(, $frame->deliveryTag) = \unpack('J', $this->buffer); $this->buffer = \substr($this->buffer, 8); // consume "redelivered" (bit) $frame->redelivered = \ord($this->buffer) !== 0; $this->buffer = \substr($this->buffer, 1); // consume "exchange" (shortstr) $length = \ord($this->buffer); $frame->exchange = \substr($this->buffer, 1, $length); $this->buffer = \substr($this->buffer, 1 + $length); // consume "routing-key" (shortstr) $length = \ord($this->buffer); $frame->routingKey = \substr($this->buffer, 1, $length); $this->buffer = \substr($this->buffer, 1 + $length); return $frame; // method "basic.get-ok" } elseif ($method === 71) { $frame = new Basic\BasicGetOkFrame(); // consume "deliveryTag" (longlong) list(, $frame->deliveryTag) = \unpack('J', $this->buffer); $this->buffer = \substr($this->buffer, 8); // consume "redelivered" (bit) $frame->redelivered = \ord($this->buffer) !== 0; $this->buffer = \substr($this->buffer, 1); // consume "exchange" (shortstr) $length = \ord($this->buffer); $frame->exchange = \substr($this->buffer, 1, $length); $this->buffer = \substr($this->buffer, 1 + $length); // consume "routing-key" (shortstr) $length = \ord($this->buffer); $frame->routingKey = \substr($this->buffer, 1, $length); $this->buffer = \substr($this->buffer, 1 + $length); // consume "messageCount" (long) list(, $frame->messageCount) = \unpack('N', $this->buffer); $this->buffer = \substr($this->buffer, 4); return $frame; // method "basic.get-empty" } elseif ($method === 72) { $frame = new Basic\BasicGetEmptyFrame(); // consume "cluster-id" (shortstr) $length = \ord($this->buffer); $frame->clusterId = \substr($this->buffer, 1, $length); $this->buffer = \substr($this->buffer, 1 + $length); return $frame; // method "basic.ack" } elseif ($method === 80) { $frame = new Basic\BasicAckFrame(); // consume "deliveryTag" (longlong) list(, $frame->deliveryTag) = \unpack('J', $this->buffer); $this->buffer = \substr($this->buffer, 8); // consume "multiple" (bit) $frame->multiple = \ord($this->buffer) !== 0; $this->buffer = \substr($this->buffer, 1); return $frame; // method "basic.recover-ok" } elseif ($method === 111) { return new Basic\BasicRecoverOkFrame(); // method "basic.nack" } elseif ($method === 120) { $frame = new Basic\BasicNackFrame(); // consume "deliveryTag" (longlong) list(, $frame->deliveryTag) = \unpack('J', $this->buffer); $this->buffer = \substr($this->buffer, 8); // consume "multiple" (bit) // consume "requeue" (bit) $octet = \ord($this->buffer); $this->buffer = \substr($this->buffer, 1); $frame->multiple = $octet & 1 !== 0; $frame->requeue = $octet & 2 !== 0; return $frame; } throw ProtocolException::create("Frame method (" . $method . ") is invalid for class \"basic\"."); // class "tx" } elseif ($class === 90) { // method "tx.select-ok" if ($method === 11) { return new Tx\TxSelectOkFrame(); // method "tx.commit-ok" } elseif ($method === 21) { return new Tx\TxCommitOkFrame(); // method "tx.rollback-ok" } elseif ($method === 31) { return new Tx\TxRollbackOkFrame(); } throw ProtocolException::create("Frame method (" . $method . ") is invalid for class \"tx\"."); // class "confirm" } elseif ($class === 85) { // method "confirm.select-ok" if ($method === 11) { return new Confirm\ConfirmSelectOkFrame(); } throw ProtocolException::create("Frame method (" . $method . ") is invalid for class \"confirm\"."); } throw ProtocolException::create("Frame class (" . $class . ") is invalid."); }