You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@qpid.apache.org by da...@apache.org on 2026/09/20 01:12:14 UTC
(qpid-broker-j) 01/05: NO-JIRA: [Broker-J] AMQP 0-8 / 0-9 / 0-9-1 validation improvements
This is an automated email from the ASF dual-hosted git repository.
dakirily pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/qpid-broker-j.git
commit 0db16e4115b0c922926dbd461216508cd030bbde
Author: Daniil Kirilyuk <da...@apache.org>
AuthorDate: Sat Sep 19 19:37:22 2026 +0200
NO-JIRA: [Broker-J] AMQP 0-8 / 0-9 / 0-9-1 validation improvements
---
.../apache/qpid/server/protocol/ErrorCodes.java | 5 +
.../NonBlockingConnectionExceptionTest.java | 121 +++
.../qpid/server/protocol/v0_8/AMQChannel.java | 146 ++-
.../qpid/server/protocol/v0_8/AMQDecoder.java | 66 +-
...on.java => AMQFatalFrameDecodingException.java} | 27 +-
.../protocol/v0_8/AMQFrameDecodingException.java | 57 +-
.../server/protocol/v0_8/AMQPConnection_0_8.java | 11 +
.../protocol/v0_8/AMQPConnection_0_8Impl.java | 174 ++-
.../apache/qpid/server/protocol/v0_8/AMQType.java | 47 +-
.../qpid/server/protocol/v0_8/AMQTypedValue.java | 21 +-
...xception.java => AMQValueNestingException.java} | 26 +-
.../qpid/server/protocol/v0_8/BrokerDecoder.java | 9 +-
.../qpid/server/protocol/v0_8/EncodingUtils.java | 65 +-
.../qpid/server/protocol/v0_8/FieldArray.java | 13 +-
.../qpid/server/protocol/v0_8/FieldTable.java | 260 ++++-
.../server/protocol/v0_8/FieldTableFactory.java | 7 +
.../protocol/v0_8/FieldValueNestingValidator.java | 154 +++
...ameBoundaryValidatingServerMethodProcessor.java | 610 +++++++++++
.../qpid/server/protocol/v0_8/IncomingMessage.java | 50 +-
.../qpid/server/protocol/v0_8/ServerDecoder.java | 501 +++++----
.../protocol/v0_8/transport/BasicConsumeBody.java | 32 +-
.../transport/BasicContentHeaderProperties.java | 58 +-
.../protocol/v0_8/transport/ChannelAlertBody.java | 14 +-
.../v0_8/transport/ConnectionStartBody.java | 25 +-
.../v0_8/transport/ConnectionStartOkBody.java | 35 +-
.../protocol/v0_8/transport/ContentHeaderBody.java | 94 +-
.../v0_8/transport/ExchangeDeclareBody.java | 33 +-
.../protocol/v0_8/transport/HeartbeatBody.java | 30 +-
.../protocol/v0_8/transport/MethodProcessor.java | 8 +
.../protocol/v0_8/transport/QueueBindBody.java | 26 +-
.../protocol/v0_8/transport/QueueDeclareBody.java | 26 +-
.../protocol/v0_8/transport/QueueUnbindBody.java | 26 +-
.../transport/ServerChannelMethodProcessor.java | 10 +
.../qpid/server/protocol/v0_8/AMQChannelTest.java | 192 ++++
.../protocol/v0_8/AMQPConnection_0_8Test.java | 985 ++++++++++++++++-
.../server/protocol/v0_8/EncodingUtilsTest.java | 192 +++-
.../v0_8/FieldTableNestingDecoderTest.java | 354 +++++++
.../qpid/server/protocol/v0_8/FieldTableTest.java | 369 ++++++-
.../server/protocol/v0_8/IncomingMessageTest.java | 125 +++
.../server/protocol/v0_8/ServerDecoderTest.java | 1103 ++++++++++++++++++++
.../BasicContentHeaderPropertiesDisposalTest.java | 460 ++++++++
.../BasicContentHeaderPropertiesTest.java | 39 +-
.../ClientFieldTableBodyDisposalTest.java | 106 ++
.../v0_8/transport/ConnectionStartOkBodyTest.java | 126 +++
.../ServerFieldTableBodyDisposalTest.java | 160 +++
.../qpid/tests/protocol/v0_8/ConnectionTest.java | 135 ++-
.../protocol/v0_8/ContentBodyFrameLimitTest.java | 108 ++
.../v0_8/IncompleteContentSequenceTest.java | 186 ++++
.../tests/protocol/v0_8/MalformedFrameTest.java | 244 +++++
.../v0_8/extension/basic/MalformedMessageTest.java | 38 +-
50 files changed, 7114 insertions(+), 595 deletions(-)
diff --git a/broker-core/src/main/java/org/apache/qpid/server/protocol/ErrorCodes.java b/broker-core/src/main/java/org/apache/qpid/server/protocol/ErrorCodes.java
index 88616c1593..cc729d0c54 100644
--- a/broker-core/src/main/java/org/apache/qpid/server/protocol/ErrorCodes.java
+++ b/broker-core/src/main/java/org/apache/qpid/server/protocol/ErrorCodes.java
@@ -105,6 +105,11 @@ public interface ErrorCodes
*/
int CHANNEL_ERROR = 504;
+ /**
+ * The peer sent a frame that was not expected, usually because content frames were sent out of sequence.
+ */
+ int UNEXPECTED_FRAME = 505;
+
/**
* The server could not complete the method because it lacked sufficient resources. This may be due to the client
* creating too many of some type of entity.
diff --git a/broker-core/src/test/java/org/apache/qpid/server/transport/NonBlockingConnectionExceptionTest.java b/broker-core/src/test/java/org/apache/qpid/server/transport/NonBlockingConnectionExceptionTest.java
new file mode 100644
index 0000000000..3532a38769
--- /dev/null
+++ b/broker-core/src/test/java/org/apache/qpid/server/transport/NonBlockingConnectionExceptionTest.java
@@ -0,0 +1,121 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ */
+
+package org.apache.qpid.server.transport;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.nio.ByteBuffer;
+import java.nio.channels.SocketChannel;
+import java.util.Collections;
+import java.util.Set;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import org.apache.qpid.server.bytebuffer.QpidByteBuffer;
+import org.apache.qpid.server.logging.EventLogger;
+import org.apache.qpid.server.model.Broker;
+import org.apache.qpid.server.model.port.AmqpPort;
+import org.apache.qpid.server.transport.network.TransportEncryption;
+import org.apache.qpid.server.util.ConnectionScopedRuntimeException;
+import org.apache.qpid.test.utils.UnitTestBase;
+
+class NonBlockingConnectionExceptionTest extends UnitTestBase
+{
+ private AmqpPort<?> _port;
+ private NetworkConnectionScheduler _scheduler;
+
+ @BeforeEach
+ void setUp()
+ {
+ final Broker<?> broker = mock(Broker.class);
+ when(broker.getEventLogger()).thenReturn(mock(EventLogger.class));
+ _port = mock(AmqpPort.class);
+ doReturn(broker).when(_port).getParent();
+ when(_port.getNetworkBufferSize()).thenReturn(128);
+ when(_port.getContextValue(Integer.class, AmqpPort.FINAL_WRITE_THRESHOLD)).thenReturn(100);
+ when(_port.getContextValue(Long.class, AmqpPort.FINAL_WRITE_TIMEOUT)).thenReturn(100L);
+ when(_port.getContextValue(Integer.class, AmqpPort.MAX_GATHERING_WRITE_BUFFERS)).thenReturn(1024);
+ _scheduler = mock(NetworkConnectionScheduler.class);
+ }
+
+ @Test
+ void testContainedParsingFailureClosesOnlyItsConnection() throws Exception
+ {
+ final ProtocolEngine failingEngine = mock(ProtocolEngine.class);
+ final ProtocolEngine healthyEngine = mock(ProtocolEngine.class);
+ final SocketChannel failingChannel = mock(SocketChannel.class);
+ final SocketChannel healthyChannel = mock(SocketChannel.class);
+ final NonBlockingConnection failing = createConnection(failingEngine, failingChannel);
+ final NonBlockingConnection healthy = createConnection(healthyEngine, healthyChannel);
+ final ArithmeticException failure = new ArithmeticException("Injected decoding failure");
+ doThrow(new ConnectionScopedRuntimeException(failure)).when(failingEngine).received(any(QpidByteBuffer.class));
+
+ try
+ {
+ assertTrue(failing.doWork());
+ verify(failingEngine).closed();
+ verify(failingChannel).close();
+ verify(_scheduler).removeConnection(failing);
+
+ assertFalse(healthy.doWork());
+ verify(healthyEngine).received(any(QpidByteBuffer.class));
+ }
+ finally
+ {
+ failing.close();
+ failing.doWork();
+ healthy.close();
+ healthy.doWork();
+ }
+ }
+
+ private NonBlockingConnection createConnection(final ProtocolEngine engine, final SocketChannel channel)
+ throws Exception
+ {
+ final Socket socket = mock(Socket.class);
+ when(socket.getRemoteSocketAddress()).thenReturn(new InetSocketAddress("localhost", 1000));
+ when(socket.getLocalSocketAddress()).thenReturn(new InetSocketAddress("localhost", 5672));
+ when(channel.socket()).thenReturn(socket);
+ when(channel.read(any(ByteBuffer.class))).thenAnswer(invocation ->
+ {
+ final ByteBuffer input = invocation.getArgument(0);
+ input.put((byte) 0);
+ return 1;
+ });
+ when(engine.getAggregateTicker()).thenReturn(new AggregateTicker());
+ when(engine.processPendingIterator()).thenAnswer(invocation -> Collections.emptyIterator());
+ final NonBlockingConnection connection = new NonBlockingConnection(channel, engine,
+ Set.of(TransportEncryption.NONE), mock(Runnable.class), _scheduler, _port);
+ connection.setSelectionTask(mock(SelectorThread.SelectionTask.class));
+ return connection;
+ }
+}
diff --git a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQChannel.java b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQChannel.java
index 0b2fba51e9..b76ba29e75 100644
--- a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQChannel.java
+++ b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQChannel.java
@@ -155,6 +155,7 @@ public class AMQChannel extends AbstractAMQPSession<AMQChannel, ConsumerTarget_0
private final CachedFrame _txCommitOkFrame;
private final long _blockingTimeout;
+ private final int _maxContentBodyFramesPerMessage;
/**
* The delivery tag is unique per channel. This is pre-incremented before putting into the deliver frame so that
@@ -222,6 +223,11 @@ public class AMQChannel extends AbstractAMQPSession<AMQChannel, ConsumerTarget_0
_forceMessageValidation = connection.getContextValue(Boolean.class, AMQPConnection_0_8.FORCE_MESSAGE_VALIDATION);
+ final Integer maxContentBodyFramesPerMessage = connection.getContextValue(
+ Integer.class, AMQPConnection_0_8.CONNECTION_MAX_CONTENT_BODY_FRAMES_PER_MESSAGE);
+ _maxContentBodyFramesPerMessage = Math.max(1, maxContentBodyFramesPerMessage == null
+ ? AMQPConnection_0_8.DEFAULT_MAX_CONTENT_BODY_FRAMES_PER_MESSAGE
+ : maxContentBodyFramesPerMessage);
}
private void message(final LogMessage message)
@@ -288,6 +294,41 @@ public class AMQChannel extends AbstractAMQPSession<AMQChannel, ConsumerTarget_0
_currentMessage.setMessageDestination(e);
}
+ private void rejectIncompleteMessage(final String message)
+ {
+ final int errorCode = ProtocolVersion.v0_91.equals(_connection.getProtocolVersion())
+ ? ErrorCodes.UNEXPECTED_FRAME
+ : ErrorCodes.FRAME_ERROR;
+ disposeCurrentMessageAndCloseConnection(errorCode, message);
+ }
+
+ @Override
+ public boolean rejectMethodFrameIfContentIncomplete()
+ {
+ if (hasCurrentMessage())
+ {
+ rejectIncompleteMessage("Method frame received before completing the previous message");
+ return true;
+ }
+ return false;
+ }
+
+ private void disposeCurrentMessageAndCloseConnection(final int errorCode, final String message)
+ {
+ disposeCurrentMessage();
+ _connection.sendConnectionClose(errorCode, message, _channelId);
+ }
+
+ private void disposeCurrentMessage()
+ {
+ final IncomingMessage currentMessage = _currentMessage;
+ _currentMessage = null;
+ if (currentMessage != null)
+ {
+ currentMessage.dispose();
+ }
+ }
+
private void publishContentHeader(ContentHeaderBody contentHeaderBody)
{
if (LOGGER.isDebugEnabled())
@@ -333,13 +374,13 @@ public class AMQChannel extends AbstractAMQPSession<AMQChannel, ConsumerTarget_0
getConnection().getLastReadTime());
final MessageHandle<MessageMetaData> handle = _messageStore.addMessage(messageMetaData);
- int bodyCount = _currentMessage.getBodyCount();
- if (bodyCount > 0)
+ final int contentChunkCount = _currentMessage.getContentChunkCount();
+ if (contentChunkCount > 0)
{
- for (int i = 0; i < bodyCount; i++)
+ for (int i = 0; i < contentChunkCount; i++)
{
- ContentBody contentChunk = _currentMessage.getContentChunk(i);
- handle.addContent(contentChunk.getPayload());
+ final QpidByteBuffer contentChunk = _currentMessage.getContentChunk(i);
+ handle.addContent(contentChunk);
contentChunk.dispose();
}
}
@@ -487,21 +528,26 @@ public class AMQChannel extends AbstractAMQPSession<AMQChannel, ConsumerTarget_0
}
- private void publishContentBody(ContentBody contentBody)
+ private void publishContentBody(final QpidByteBuffer contentBody)
{
if (LOGGER.isDebugEnabled())
{
LOGGER.debug(debugIdentity() + " content body received on channel " + _channelId);
}
+ if (_currentMessage.getContentBodyFrameCount() >= _maxContentBodyFramesPerMessage)
+ {
+ disposeCurrentMessageAndCloseConnection(ErrorCodes.RESOURCE_ERROR,"Message exceeds maximum number of " +
+ "content body frames (" + _maxContentBodyFramesPerMessage + ")");
+ return;
+ }
+
try
{
- long currentSize = _currentMessage.addContentBodyFrame(contentBody);
- if(currentSize > _currentMessage.getSize())
+ if (!_currentMessage.addContentBodyFrame(contentBody))
{
- _connection.sendConnectionClose(ErrorCodes.FRAME_ERROR,
- "More message data received than content header defined",
- _channelId);
+ disposeCurrentMessageAndCloseConnection(ErrorCodes.FRAME_ERROR, "More message data received than " +
+ "content header defined");
}
else
{
@@ -512,7 +558,7 @@ public class AMQChannel extends AbstractAMQPSession<AMQChannel, ConsumerTarget_0
{
// we want to make sure we don't keep a reference to the message in the
// event of an error
- _currentMessage = null;
+ disposeCurrentMessage();
throw e;
}
}
@@ -1928,9 +1974,7 @@ public class AMQChannel extends AbstractAMQPSession<AMQChannel, ConsumerTarget_0
" immediate: " + immediate + " ]");
}
-
-
- NamedAddressSpace vHost = _connection.getAddressSpace();
+ final NamedAddressSpace vHost = _connection.getAddressSpace();
if(blockingTimeoutExceeded())
{
@@ -1940,7 +1984,7 @@ public class AMQChannel extends AbstractAMQPSession<AMQChannel, ConsumerTarget_0
}
else
{
- MessageDestination destination;
+ final MessageDestination destination;
if (isDefaultExchange(exchangeName))
{
@@ -1959,10 +2003,10 @@ public class AMQChannel extends AbstractAMQPSession<AMQChannel, ConsumerTarget_0
else
{
- MessagePublishInfo info = new MessagePublishInfo(exchangeName,
- immediate,
- mandatory,
- routingKey);
+ final MessagePublishInfo info = new MessagePublishInfo(exchangeName,
+ immediate,
+ mandatory,
+ routingKey);
try
{
@@ -2153,13 +2197,19 @@ public class AMQChannel extends AbstractAMQPSession<AMQChannel, ConsumerTarget_0
if(hasCurrentMessage())
{
- publishContentBody(new ContentBody(data));
+ if (_currentMessage.getContentHeader() == null)
+ {
+ rejectIncompleteMessage("Attempt to send a content body before sending a content header");
+ }
+ else
+ {
+ publishContentBody(data);
+ }
}
else
{
- _connection.sendConnectionClose(ErrorCodes.COMMAND_INVALID,
- "Attempt to send a content header without first sending a publish frame",
- _channelId);
+ _connection.sendConnectionClose(ErrorCodes.COMMAND_INVALID, "Attempt to send a content body without " +
+ "first sending a publish frame", _channelId);
}
}
@@ -2173,7 +2223,12 @@ public class AMQChannel extends AbstractAMQPSession<AMQChannel, ConsumerTarget_0
if(hasCurrentMessage())
{
- if(bodySize > _connection.getMaxMessageSize())
+ if (_currentMessage.getContentHeader() != null)
+ {
+ properties.dispose();
+ rejectIncompleteMessage("Attempt to send a duplicate content header");
+ }
+ else if (bodySize > _connection.getMaxMessageSize())
{
properties.dispose();
closeChannel(ErrorCodes.MESSAGE_TOO_LARGE,
@@ -2203,6 +2258,27 @@ public class AMQChannel extends AbstractAMQPSession<AMQChannel, ConsumerTarget_0
}
}
+ void receiveOversizedMessageHeader(final long bodySize)
+ {
+ if (hasCurrentMessage())
+ {
+ if (_currentMessage.getContentHeader() != null)
+ {
+ rejectIncompleteMessage("Attempt to send a duplicate content header");
+ }
+ else
+ {
+ closeChannel(ErrorCodes.MESSAGE_TOO_LARGE, "Content body size " + Long.toUnsignedString(bodySize) +
+ " exceeds the supported range");
+ }
+ }
+ else
+ {
+ _connection.sendConnectionClose(ErrorCodes.COMMAND_INVALID, "Attempt to send a content header without " +
+ "first sending a publish frame", _channelId);
+ }
+ }
+
@Override
public boolean ignoreAllButCloseOk()
{
@@ -3517,24 +3593,6 @@ public class AMQChannel extends AbstractAMQPSession<AMQChannel, ConsumerTarget_0
public void dispose()
{
_txCommitOkFrame.dispose();
- final IncomingMessage currentMessage = _currentMessage;
- if (currentMessage != null)
- {
- _currentMessage = null;
- final ContentHeaderBody contentHeader = currentMessage.getContentHeader();
- if (contentHeader != null)
- {
- contentHeader.dispose();
- }
-
- int bodyCount = currentMessage.getBodyCount();
- if (bodyCount > 0)
- {
- for (int i = 0; i < bodyCount; i++)
- {
- currentMessage.getContentChunk(i).dispose();
- }
- }
- }
+ disposeCurrentMessage();
}
}
diff --git a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQDecoder.java b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQDecoder.java
index b2b56d407f..af0d85a243 100644
--- a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQDecoder.java
+++ b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQDecoder.java
@@ -45,13 +45,14 @@ import org.apache.qpid.server.protocol.v0_8.transport.ProtocolInitiation;
* TODO If protocol initiation decoder not needed, then don't create it. Probably not a big deal, but it adds to the
* per-session overhead.
*/
-public abstract class AMQDecoder<T extends MethodProcessor>
+public abstract class AMQDecoder<T extends MethodProcessor<?>>
{
private static final Logger LOGGER = LoggerFactory.getLogger(AMQDecoder.class);
public static final int FRAME_HEADER_SIZE = 7;
public static final int FRAME_MIN_SIZE = 4096;
private final T _methodProcessor;
+ private final int _maxNestedObjects;
/** Holds the protocol initiation decoder. */
private final ProtocolInitiation.Decoder _piDecoder = new ProtocolInitiation.Decoder();
@@ -62,16 +63,29 @@ public abstract class AMQDecoder<T extends MethodProcessor>
private boolean _firstRead = true;
private int _maxFrameSize = FRAME_MIN_SIZE;
+ private long _frameBytesToDiscard;
/**
* Creates a new AMQP decoder.
* @param expectProtocolInitiation <code>true</code> if this decoder needs to handle protocol initiation.
* @param methodProcessor method processor
*/
- protected AMQDecoder(boolean expectProtocolInitiation, T methodProcessor)
+ protected AMQDecoder(final boolean expectProtocolInitiation, final T methodProcessor)
{
+ this(expectProtocolInitiation, methodProcessor, AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS);
+ }
+
+ protected AMQDecoder(final boolean expectProtocolInitiation,
+ final T methodProcessor,
+ final int maxNestedObjects)
+ {
+ if (maxNestedObjects < 0)
+ {
+ throw new IllegalArgumentException("Maximum nested objects must not be negative: " + maxNestedObjects);
+ }
_expectProtocolInitiation = expectProtocolInitiation;
_methodProcessor = methodProcessor;
+ _maxNestedObjects = maxNestedObjects;
}
@@ -97,6 +111,11 @@ public abstract class AMQDecoder<T extends MethodProcessor>
return _methodProcessor;
}
+ protected final int getMaxNestedObjects()
+ {
+ return _maxNestedObjects;
+ }
+
protected final int decode(final QpidByteBuffer buf) throws AMQFrameDecodingException
{
// If this is the first read then we may be getting a protocol initiation back if we tried to negotiate
@@ -142,6 +161,11 @@ public abstract class AMQDecoder<T extends MethodProcessor>
protected int decodable(final QpidByteBuffer in) throws AMQFrameDecodingException
{
+ if (_frameBytesToDiscard != 0 && !discardFrame(in))
+ {
+ return 1;
+ }
+
final int remainingAfterAttributes = in.remaining() - FRAME_HEADER_SIZE;
// type, channel, body length and end byte
if (remainingAfterAttributes < 0)
@@ -154,6 +178,8 @@ public abstract class AMQDecoder<T extends MethodProcessor>
final long bodySize = ((long)in.getInt(in.position()+3)) & 0xffffffffL;
if (bodySize > _maxFrameSize)
{
+ _frameBytesToDiscard = bodySize + 1;
+ in.position(in.position() + FRAME_HEADER_SIZE);
throw new AMQFrameDecodingException(
"Incoming frame size of "
+ bodySize
@@ -166,6 +192,18 @@ public abstract class AMQDecoder<T extends MethodProcessor>
}
+ private boolean discardFrame(final QpidByteBuffer in) throws AMQFatalFrameDecodingException
+ {
+ final int discarded = (int) Math.min(_frameBytesToDiscard, in.remaining());
+ in.position(in.position() + discarded);
+ _frameBytesToDiscard -= discarded;
+ if (_frameBytesToDiscard == 0 && (in.get(in.position() - 1) & 0xFF) != 0xCE)
+ {
+ throw new AMQFatalFrameDecodingException("End of frame marker not found after discarded frame");
+ }
+ return _frameBytesToDiscard == 0;
+ }
+
protected void processInput(final QpidByteBuffer in)
throws AMQFrameDecodingException, AMQProtocolVersionException
{
@@ -182,16 +220,28 @@ public abstract class AMQDecoder<T extends MethodProcessor>
+ " bodySize = " + bodySize);
}
- processFrame(channel, type, bodySize, in);
-
- byte marker = in.get();
+ final int frameEnd = in.position() + (int) bodySize + 1;
+ final byte marker = in.get(frameEnd - 1);
if ((marker & 0xFF) != 0xCE)
{
- throw new AMQFrameDecodingException(
+ throw new AMQFatalFrameDecodingException(
"End of frame marker not found. Read " + marker + " length=" + bodySize
+ " type=" + type);
}
+ try (final QpidByteBuffer frameBody = in.view(0, (int) bodySize))
+ {
+ processFrame(channel, type, bodySize, frameBody);
+ if (frameBody.hasRemaining())
+ {
+ throw new AMQFrameDecodingException("Frame body was not fully consumed: type=" + type +
+ ", channel=" + channel + ", bodySize=" + bodySize + ", remaining=" + frameBody.remaining());
+ }
+ }
+ finally
+ {
+ in.position(frameEnd);
+ }
}
protected void processFrame(final int channel, final byte type, final long bodySize, final QpidByteBuffer in)
@@ -203,7 +253,7 @@ public abstract class AMQDecoder<T extends MethodProcessor>
processMethod(channel, in);
break;
case 2:
- ContentHeaderBody.process(in, _methodProcessor.getChannelMethodProcessor(channel), bodySize);
+ ContentHeaderBody.process(in, _methodProcessor, channel, bodySize, _maxNestedObjects);
break;
case 3:
ContentBody.process(in, _methodProcessor.getChannelMethodProcessor(channel), bodySize);
@@ -212,7 +262,7 @@ public abstract class AMQDecoder<T extends MethodProcessor>
HeartbeatBody.process(channel, in, _methodProcessor, bodySize);
break;
default:
- throw new AMQFrameDecodingException("Unsupported frame type: " + type);
+ throw new AMQFatalFrameDecodingException("Unsupported frame type: " + type);
}
}
diff --git a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQFrameDecodingException.java b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQFatalFrameDecodingException.java
similarity index 55%
copy from broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQFrameDecodingException.java
copy to broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQFatalFrameDecodingException.java
index c8a7db57ae..945f7da5e2 100644
--- a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQFrameDecodingException.java
+++ b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQFatalFrameDecodingException.java
@@ -20,29 +20,16 @@
*/
package org.apache.qpid.server.protocol.v0_8;
-import org.apache.qpid.server.AMQException;
-import org.apache.qpid.server.protocol.ErrorCodes;
+import java.io.Serial;
-/**
- * AMQFrameDecodingException indicates that an AMQP frame cannot be decoded because it does not have the correct
- * format as defined by the protocol.
- */
-public class AMQFrameDecodingException extends AMQException
+/** Indicates framing corruption for which AMQP prohibits sending any further data on the connection */
+final class AMQFatalFrameDecodingException extends AMQFrameDecodingException
{
- public AMQFrameDecodingException(String message, Throwable cause)
- {
- super(ErrorCodes.FRAME_ERROR, message, cause);
- }
-
- public AMQFrameDecodingException(String message)
- {
- super(ErrorCodes.FRAME_ERROR, message, null);
- }
+ @Serial
+ private static final long serialVersionUID = 1L;
-
- public AMQFrameDecodingException(int errorCode, String message, final Throwable cause)
+ AMQFatalFrameDecodingException(final String message)
{
- super(errorCode, message, cause);
+ super(message);
}
-
}
diff --git a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQFrameDecodingException.java b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQFrameDecodingException.java
index c8a7db57ae..246c68badb 100644
--- a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQFrameDecodingException.java
+++ b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQFrameDecodingException.java
@@ -23,26 +23,73 @@ package org.apache.qpid.server.protocol.v0_8;
import org.apache.qpid.server.AMQException;
import org.apache.qpid.server.protocol.ErrorCodes;
+import java.io.Serial;
+
/**
* AMQFrameDecodingException indicates that an AMQP frame cannot be decoded because it does not have the correct
* format as defined by the protocol.
*/
public class AMQFrameDecodingException extends AMQException
{
- public AMQFrameDecodingException(String message, Throwable cause)
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ private final int _classId;
+ private final int _methodId;
+
+ public AMQFrameDecodingException(final String message, final Throwable cause)
{
- super(ErrorCodes.FRAME_ERROR, message, cause);
+ this(ErrorCodes.FRAME_ERROR, message, 0, 0, cause);
}
- public AMQFrameDecodingException(String message)
+ public AMQFrameDecodingException(final String message)
{
- super(ErrorCodes.FRAME_ERROR, message, null);
+ this(ErrorCodes.FRAME_ERROR, message, 0, 0, null);
}
- public AMQFrameDecodingException(int errorCode, String message, final Throwable cause)
+ public AMQFrameDecodingException(final int errorCode, final String message, final Throwable cause)
+ {
+ this(errorCode, message, 0, 0, cause);
+ }
+
+ AMQFrameDecodingException(final int errorCode,
+ final String message,
+ final int classId,
+ final int methodId,
+ final Throwable cause)
{
super(errorCode, message, cause);
+ _classId = classId;
+ _methodId = methodId;
+ }
+
+ public static AMQFrameDecodingException forDecodingFailure(final String context,
+ final RuntimeException cause)
+ {
+ return forDecodingFailure(context, 0, 0, cause);
}
+ static AMQFrameDecodingException forDecodingFailure(final String context,
+ final int classId,
+ final int methodId,
+ final RuntimeException cause)
+ {
+ final int errorCode = cause instanceof AMQValueNestingException
+ ? ErrorCodes.RESOURCE_ERROR
+ : ErrorCodes.FRAME_ERROR;
+ final String detail = cause.getMessage();
+ final String message = detail == null || detail.isEmpty() ? context : context + ": " + detail;
+ return new AMQFrameDecodingException(errorCode, message, classId, methodId, cause);
+ }
+
+ int getClassId()
+ {
+ return _classId;
+ }
+
+ int getMethodId()
+ {
+ return _methodId;
+ }
}
diff --git a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQPConnection_0_8.java b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQPConnection_0_8.java
index 1943dfaa0d..cb7cb3d47b 100644
--- a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQPConnection_0_8.java
+++ b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQPConnection_0_8.java
@@ -56,6 +56,17 @@ public interface AMQPConnection_0_8<C extends AMQPConnection_0_8<C>> extends AMQ
@ManagedContextDefault(name= FORCE_MESSAGE_VALIDATION)
boolean DEFAULT_FORCE_MESSAGE_VALIDATION = false;
+ String CODEC_MAX_NESTED_OBJECTS = "amqp0x.codec.maxNestedObjects";
+ @ManagedContextDefault(name = CODEC_MAX_NESTED_OBJECTS,
+ description = "Maximum nesting depth of AMQP 0-x field tables and arrays")
+ int DEFAULT_CODEC_MAX_NESTED_OBJECTS = 50;
+
+ String CONNECTION_MAX_CONTENT_BODY_FRAMES_PER_MESSAGE = "connection.maxContentBodyFramesPerMessage";
+ @ManagedContextDefault(name = CONNECTION_MAX_CONTENT_BODY_FRAMES_PER_MESSAGE,
+ description = "Maximum number of content body frames accepted for one message. The default preserves " +
+ "the default maximum message size for peers using the AMQP minimum frame size.")
+ int DEFAULT_MAX_CONTENT_BODY_FRAMES_PER_MESSAGE = 256 * 1024;
+
@DerivedAttribute(description = "The actual negotiated value of heartbeat delay.")
int getHeartbeatDelay();
diff --git a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQPConnection_0_8Impl.java b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQPConnection_0_8Impl.java
index f706ccf705..6a4ca494fc 100644
--- a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQPConnection_0_8Impl.java
+++ b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQPConnection_0_8Impl.java
@@ -24,10 +24,10 @@ import static java.nio.charset.StandardCharsets.US_ASCII;
import java.io.IOException;
import java.lang.reflect.Proxy;
-import java.nio.BufferUnderflowException;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
+import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
@@ -40,7 +40,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Pattern;
-import org.apache.qpid.server.security.limit.ConnectionLimitException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -79,6 +78,7 @@ import org.apache.qpid.server.security.AccessDeniedException;
import org.apache.qpid.server.security.SubjectCreator;
import org.apache.qpid.server.security.auth.SubjectAuthenticationResult;
import org.apache.qpid.server.security.auth.sasl.SaslNegotiator;
+import org.apache.qpid.server.security.limit.ConnectionLimitException;
import org.apache.qpid.server.session.AMQPSession;
import org.apache.qpid.server.transport.AbstractAMQPConnection;
import org.apache.qpid.server.transport.AggregateTicker;
@@ -90,6 +90,7 @@ import org.apache.qpid.server.txn.LocalTransaction;
import org.apache.qpid.server.txn.ServerTransaction;
import org.apache.qpid.server.util.Action;
import org.apache.qpid.server.util.ConnectionScopedRuntimeException;
+import org.apache.qpid.server.util.ServerScopedRuntimeException;
import org.apache.qpid.server.virtualhost.VirtualHostUnavailableException;
public class AMQPConnection_0_8Impl
@@ -129,6 +130,7 @@ public class AMQPConnection_0_8Impl
private final ServerDecoder _decoder;
+ private List<String> _advertisedSaslMechanisms = Collections.emptyList();
private volatile SaslNegotiator _saslNegotiator;
private volatile int _maxNoOfChannels;
@@ -168,6 +170,7 @@ public class AMQPConnection_0_8Impl
private volatile int _currentMethodId;
private final int _binaryDataLimit;
private volatile boolean _transportBlockedForWriting;
+ private volatile boolean _inputClosed;
private volatile SubjectAuthenticationResult _successfulAuthenticationResult;
private final Set<AMQPSession<?,?>> _sessionsWithWork =
@@ -255,16 +258,88 @@ public class AMQPConnection_0_8Impl
@Override
protected void onReceive(final QpidByteBuffer msg)
{
- try
+ if (_inputClosed)
{
- _decoder.decodeBuffer(msg);
- receivedCompleteAllChannels();
+ msg.position(msg.limit());
+ return;
}
- catch (AMQFrameDecodingException | IOException | AMQPInvalidClassException
- | IllegalArgumentException | IllegalStateException | BufferUnderflowException e)
+
+ while (true)
{
- LOGGER.warn("Unexpected exception", e);
- throw new ConnectionScopedRuntimeException(e);
+ final int position = msg.position();
+ try
+ {
+ _decoder.decodeBuffer(msg);
+ receivedCompleteAllChannels();
+ if (_inputClosed)
+ {
+ msg.position(msg.limit());
+ }
+ return;
+ }
+ catch (final AMQValueNestingException e)
+ {
+ LOGGER.debug("Field-value nesting limit exceeded", e);
+ handleProtocolError(ErrorCodes.RESOURCE_ERROR, e.getMessage());
+ }
+ catch (final AMQFatalFrameDecodingException e)
+ {
+ LOGGER.warn("Fatal frame decoding error", e);
+ msg.position(msg.limit());
+ closeNetworkConnection();
+ return;
+ }
+ catch (final AMQFrameDecodingException e)
+ {
+ LOGGER.debug("Frame decoding error", e);
+ handleProtocolError(e.getErrorCode(), e.getMessage(), e.getClassId(), e.getMethodId());
+ }
+ catch (final ConnectionScopedRuntimeException | ServerScopedRuntimeException e)
+ {
+ throw e;
+ }
+ catch (final IOException | RuntimeException e)
+ {
+ LOGGER.warn("Unexpected exception", e);
+ _inputClosed = true;
+ msg.position(msg.limit());
+ throw new ConnectionScopedRuntimeException(e);
+ }
+
+ if (_inputClosed || msg.position() <= position)
+ {
+ msg.position(msg.limit());
+ if (!_inputClosed)
+ {
+ closeNetworkConnection();
+ }
+ return;
+ }
+
+ if (!isClosing() || !msg.hasRemaining())
+ {
+ return;
+ }
+ }
+ }
+
+ private void handleProtocolError(final int errorCode, final String message)
+ {
+ handleProtocolError(errorCode, message, 0, 0);
+ }
+
+ private void handleProtocolError(final int errorCode,
+ final String message,
+ final int classId,
+ final int methodId)
+ {
+ if (_state == ConnectionState.OPEN)
+ {
+ sendConnectionClose(errorCode, message, 0, classId, methodId);
+ }
+ else
+ {
+ closeNetworkConnection();
}
}
@@ -303,27 +378,21 @@ public class AMQPConnection_0_8Impl
_channelsForCurrentMessage.add(amqChannel);
}
- private synchronized void protocolInitiationReceived(ProtocolInitiation pi)
+ private synchronized void protocolInitiationReceived(final ProtocolInitiation pi)
{
// this ensures the codec never checks for a PI message again
_decoder.setExpectProtocolInitiation(false);
try
{
- ProtocolVersion pv = pi.checkVersion(); // Fails if not correct
+ final ProtocolVersion pv = pi.checkVersion(); // Fails if not correct
setProtocolVersion(pv);
- StringBuilder mechanismBuilder = new StringBuilder();
- for(String mechanismName : getPort().getAuthenticationProvider().getAvailableMechanisms(getTransport().isSecure()))
- {
- if(mechanismBuilder.length() != 0)
- {
- mechanismBuilder.append(' ');
- }
- mechanismBuilder.append(mechanismName);
- }
- String mechanisms = mechanismBuilder.toString();
+ final List<String> advertisedSaslMechanisms = List.copyOf(getPort().getAuthenticationProvider()
+ .getAvailableMechanisms(getTransport().isSecure()));
+ _advertisedSaslMechanisms = advertisedSaslMechanisms;
+ final String mechanisms = String.join(" ", advertisedSaslMechanisms);
- String locales = "en_US";
+ final String locales = "en_US";
Map<String,Object> props = Map.of();
for(ConnectionPropertyEnricher enricher : getPort().getConnectionPropertyEnrichers())
@@ -517,13 +586,23 @@ public class AMQPConnection_0_8Impl
}
@Override
- public void sendConnectionClose(int errorCode,
- String message, int channelId)
+ public void sendConnectionClose(final int errorCode, final String message, final int channelId)
{
- sendConnectionClose(channelId, new AMQFrame(0, new ConnectionCloseBody(getProtocolVersion(), errorCode, AMQShortString.validValueOf(message), _currentClassId, _currentMethodId)));
+ sendConnectionClose(errorCode, message, channelId, _currentClassId, _currentMethodId);
}
- private void sendConnectionClose(int channelId, AMQFrame frame)
+ protected void sendConnectionClose(final int errorCode,
+ final String message,
+ final int channelId,
+ final int classId,
+ final int methodId)
+ {
+ final ConnectionCloseBody connectionCloseBody = new ConnectionCloseBody(getProtocolVersion(), errorCode,
+ AMQShortString.validValueOf(message), classId, methodId);
+ sendConnectionClose(channelId, new AMQFrame(0, connectionCloseBody));
+ }
+
+ private void sendConnectionClose(final int channelId, final AMQFrame frame)
{
if (_orderlyClose.compareAndSet(false, true))
{
@@ -553,6 +632,7 @@ public class AMQPConnection_0_8Impl
public void closeNetworkConnection()
{
+ _inputClosed = true;
getNetwork().close();
}
@@ -653,6 +733,7 @@ public class AMQPConnection_0_8Impl
@Override
public void closed()
{
+ _inputClosed = true;
try
{
try
@@ -1092,14 +1173,24 @@ public class AMQPConnection_0_8Impl
LOGGER.debug("SASL Mechanism selected: {} Locale : {}", mechanism, locale);
+ final List<String> advertisedSaslMechanisms = _advertisedSaslMechanisms;
+ _advertisedSaslMechanisms = Collections.emptyList();
+
if (mechanism == null || mechanism.length() == 0)
{
- sendConnectionClose(ErrorCodes.CONNECTION_FORCED, "No Sasl mechanism was specified", 0);
+ closeNetworkConnection();
return;
}
- SubjectCreator subjectCreator = getSubjectCreator();
- _saslNegotiator = subjectCreator.createSaslNegotiator(String.valueOf(mechanism), this);
+ final String mechanismName = mechanism.toString();
+ if (!advertisedSaslMechanisms.contains(mechanismName))
+ {
+ closeNetworkConnection();
+ return;
+ }
+
+ final SubjectCreator subjectCreator = getSubjectCreator();
+ _saslNegotiator = subjectCreator.createSaslNegotiator(mechanismName, this);
if (_saslNegotiator == null)
{
sendConnectionClose(ErrorCodes.CONNECTION_FORCED, "No SaslServer could be created for mechanism: " + mechanism, 0);
@@ -1308,12 +1399,37 @@ public class AMQPConnection_0_8Impl
{
return channelAwaitingClosure(channelId);
}
+ else if (method.getName().equals("rejectMethodFrameIfContentIncomplete"))
+ {
+ return false;
+ }
return null;
});
}
return channelMethodProcessor;
}
+ @Override
+ public void receiveOversizedMessageHeader(final int channelId, final long bodySize)
+ throws AMQFrameDecodingException
+ {
+ if (_state != ConnectionState.OPEN)
+ {
+ throw new AMQFrameDecodingException("Content body size " + Long.toUnsignedString(bodySize) +
+ " exceeds the supported range", null);
+ }
+
+ final AMQChannel channel = getChannel(channelId);
+ if (channel != null)
+ {
+ channel.receiveOversizedMessageHeader(bodySize);
+ }
+ else if (!channelAwaitingClosure(channelId))
+ {
+ sendConnectionClose(ErrorCodes.CHANNEL_ERROR, "Unknown channel id: " + channelId, channelId);
+ }
+ }
+
@Override
public void receiveHeartbeat()
{
diff --git a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQType.java b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQType.java
index 442a2c5c1d..ccf2125bae 100644
--- a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQType.java
+++ b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQType.java
@@ -302,9 +302,36 @@ public enum AMQType
* @return An instance of the type.
*/
@Override
- public Object readValueFromBuffer(QpidByteBuffer buffer)
+ public Object readValueFromBuffer(final QpidByteBuffer buffer)
{
- return EncodingUtils.readFieldTable(buffer);
+ return readValueFromBuffer(buffer, AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS);
+ }
+
+ @Override
+ public Object readValueFromBuffer(final QpidByteBuffer buffer, final int maxNestedObjects)
+ {
+ final FieldTable fieldTable = EncodingUtils.readFieldTableValue(buffer, maxNestedObjects);
+ if (fieldTable == null)
+ {
+ return null;
+ }
+
+ if (!fieldTable.isDirect())
+ {
+ return fieldTable;
+ }
+
+ try
+ {
+ try (final QpidByteBuffer encodedForm = QpidByteBuffer.wrap(fieldTable.getDataAsBytes()))
+ {
+ return FieldTableFactory.createFieldTable(encodedForm, encodedForm.remaining(), maxNestedObjects);
+ }
+ }
+ finally
+ {
+ fieldTable.dispose();
+ }
}
@Override
@@ -367,11 +394,16 @@ public enum AMQType
* @return An instance of the type.
*/
@Override
- public Object readValueFromBuffer(QpidByteBuffer buffer)
+ public Object readValueFromBuffer(final QpidByteBuffer buffer)
{
- // Read size of field table then all name/value pairs.
- return FieldArray.readFromBuffer(buffer);
+ return readValueFromBuffer(buffer, AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS);
+ }
+ @Override
+ public Object readValueFromBuffer(final QpidByteBuffer buffer, final int maxNestedObjects)
+ {
+ // read size of field array then all values
+ return FieldArray.readFromBuffer(buffer, maxNestedObjects);
}
@Override
@@ -1105,5 +1137,10 @@ public enum AMQType
*/
abstract Object readValueFromBuffer(QpidByteBuffer buffer);
+ Object readValueFromBuffer(final QpidByteBuffer buffer, final int maxNestedObjects)
+ {
+ return readValueFromBuffer(buffer);
+ }
+
abstract void skip(QpidByteBuffer buffer);
}
diff --git a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQTypedValue.java b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQTypedValue.java
index 6836c3eb89..eb902c41a3 100644
--- a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQTypedValue.java
+++ b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQTypedValue.java
@@ -64,10 +64,12 @@ public abstract class AMQTypedValue
_value = type.toNativeValue(value);
}
- private GenericTypedValue(AMQType type, QpidByteBuffer buffer)
+ private GenericTypedValue(final AMQType type,
+ final QpidByteBuffer buffer,
+ final int maxNestedObjects)
{
_type = type;
- _value = type.readValueFromBuffer(buffer);
+ _value = type.readValueFromBuffer(buffer, maxNestedObjects);
}
@@ -211,21 +213,26 @@ public abstract class AMQTypedValue
}
- public static AMQTypedValue readFromBuffer(QpidByteBuffer buffer)
+ public static AMQTypedValue readFromBuffer(final QpidByteBuffer buffer)
{
- AMQType type = AMQTypeMap.getType(buffer.get());
+ return readFromBuffer(buffer, AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS);
+ }
- switch(type)
+ static AMQTypedValue readFromBuffer(final QpidByteBuffer buffer, final int maxNestedObjects)
+ {
+ final AMQType type = AMQTypeMap.getType(buffer.get());
+
+ switch (type)
{
case LONG:
return new LongTypedValue(buffer);
case INT:
- int value = buffer.getInt();
+ final int value = buffer.getInt();
return createAMQTypedValue(value);
default:
- return new GenericTypedValue(type, buffer);
+ return new GenericTypedValue(type, buffer, maxNestedObjects);
}
}
diff --git a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQFrameDecodingException.java b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQValueNestingException.java
similarity index 55%
copy from broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQFrameDecodingException.java
copy to broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQValueNestingException.java
index c8a7db57ae..f33c237068 100644
--- a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQFrameDecodingException.java
+++ b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQValueNestingException.java
@@ -20,29 +20,15 @@
*/
package org.apache.qpid.server.protocol.v0_8;
-import org.apache.qpid.server.AMQException;
-import org.apache.qpid.server.protocol.ErrorCodes;
+import java.io.Serial;
-/**
- * AMQFrameDecodingException indicates that an AMQP frame cannot be decoded because it does not have the correct
- * format as defined by the protocol.
- */
-public class AMQFrameDecodingException extends AMQException
+final class AMQValueNestingException extends IllegalArgumentException
{
- public AMQFrameDecodingException(String message, Throwable cause)
- {
- super(ErrorCodes.FRAME_ERROR, message, cause);
- }
-
- public AMQFrameDecodingException(String message)
- {
- super(ErrorCodes.FRAME_ERROR, message, null);
- }
+ @Serial
+ private static final long serialVersionUID = 1L;
-
- public AMQFrameDecodingException(int errorCode, String message, final Throwable cause)
+ AMQValueNestingException(final String message)
{
- super(errorCode, message, cause);
+ super(message);
}
-
}
diff --git a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/BrokerDecoder.java b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/BrokerDecoder.java
index 5dccce7dbb..90a9c6e37e 100644
--- a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/BrokerDecoder.java
+++ b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/BrokerDecoder.java
@@ -38,10 +38,17 @@ public class BrokerDecoder extends ServerDecoder
*/
public BrokerDecoder(final AMQPConnection_0_8Impl connection)
{
- super(connection);
+ super(connection, getMaxNestedObjects(connection));
_connection = connection;
}
+ private static int getMaxNestedObjects(final AMQPConnection_0_8Impl connection)
+ {
+ final Integer configured = connection.getContextValue(Integer.class,
+ AMQPConnection_0_8.CODEC_MAX_NESTED_OBJECTS);
+ return configured == null ? AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS : configured;
+ }
+
@Override
protected void processFrame(final int channelId, final byte type, final long bodySize, final QpidByteBuffer in)
throws AMQFrameDecodingException
diff --git a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/EncodingUtils.java b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/EncodingUtils.java
index 0b9ff41923..0fd92a006b 100644
--- a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/EncodingUtils.java
+++ b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/EncodingUtils.java
@@ -20,6 +20,8 @@
*/
package org.apache.qpid.server.protocol.v0_8;
+import java.nio.ByteBuffer;
+import java.nio.charset.CharacterCodingException;
import java.nio.charset.StandardCharsets;
import org.slf4j.Logger;
@@ -35,6 +37,16 @@ public class EncodingUtils
{
}
+ static int checkLength(final long length, final QpidByteBuffer buffer)
+ {
+ if (length > buffer.remaining())
+ {
+ throw new IllegalArgumentException("Declared field length " + length +
+ " is invalid; buffer has " + buffer.remaining() + " byte(s) remaining");
+ }
+ return (int) length;
+ }
+
public static int encodedShortStringLength(String s)
{
if (s == null)
@@ -206,14 +218,38 @@ public class EncodingUtils
public static FieldTable readFieldTable(QpidByteBuffer input)
{
- long length = input.getUnsignedInt();
+ return readFieldTable(input, AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS);
+ }
+
+ public static FieldTable readFieldTable(final QpidByteBuffer input, final int maxNestedObjects)
+ {
+ FieldValueNestingValidator.validateMaximum(maxNestedObjects);
+ final FieldTable fieldTable = readFieldTableValue(input, maxNestedObjects);
+ if (fieldTable != null)
+ {
+ try
+ {
+ fieldTable.validate(maxNestedObjects);
+ }
+ catch (RuntimeException e)
+ {
+ fieldTable.dispose();
+ throw e;
+ }
+ }
+ return fieldTable;
+ }
+
+ static FieldTable readFieldTableValue(final QpidByteBuffer input, final int maxNestedObjects)
+ {
+ final long length = input.getUnsignedInt();
if (length == 0)
{
return null;
}
else
{
- return FieldTableFactory.createFieldTable(input, (int) length);
+ return FieldTableFactory.createFieldTable(input, checkLength(length, input), maxNestedObjects);
}
}
@@ -222,24 +258,31 @@ public class EncodingUtils
long length = buffer.getUnsignedInt();
if (length > 0)
{
- buffer.position(buffer.position() + (int)length);
+ buffer.position(buffer.position() + checkLength(length, buffer));
}
}
- public static String readLongString(QpidByteBuffer buffer)
+ public static String readLongString(final QpidByteBuffer buffer)
{
- long length = ((long)(buffer.getInt())) & 0xFFFFFFFFL;
+ final long length = ((long)(buffer.getInt())) & 0xFFFFFFFFL;
if (length == 0)
{
return "";
}
else
{
- byte[] stringBytes = new byte[(int) length];
- buffer.get(stringBytes, 0, (int) length);
+ final byte[] stringBytes = new byte[checkLength(length, buffer)];
+ buffer.get(stringBytes, 0, stringBytes.length);
- return new String(stringBytes, StandardCharsets.UTF_8);
+ try
+ {
+ return StandardCharsets.UTF_8.newDecoder().decode(ByteBuffer.wrap(stringBytes)).toString();
+ }
+ catch (CharacterCodingException e)
+ {
+ throw new IllegalArgumentException("Cannot decode long string as UTF-8", e);
+ }
}
}
@@ -249,7 +292,7 @@ public class EncodingUtils
long length = buffer.getUnsignedInt();
if (length > 0)
{
- buffer.position(buffer.position() + (int)length);
+ buffer.position(buffer.position() + checkLength(length, buffer));
}
}
@@ -262,7 +305,7 @@ public class EncodingUtils
}
else
{
- byte[] result = new byte[(int) length];
+ final byte[] result = new byte[checkLength(length, buffer)];
buffer.get(result);
return result;
@@ -322,7 +365,7 @@ public class EncodingUtils
}
else
{
- byte[] dataBytes = new byte[(int)length];
+ final byte[] dataBytes = new byte[checkLength(length, buffer)];
buffer.get(dataBytes);
return dataBytes;
diff --git a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/FieldArray.java b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/FieldArray.java
index 8b55a5cd04..419ef2197a 100644
--- a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/FieldArray.java
+++ b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/FieldArray.java
@@ -115,14 +115,19 @@ public class FieldArray<T> extends AbstractCollection<T>
public static FieldArray<?> readFromBuffer(final QpidByteBuffer buffer)
{
- ArrayList<Object> result = new ArrayList<>();
- int size = buffer.getInt();
+ return readFromBuffer(buffer, AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS);
+ }
+
+ static FieldArray<?> readFromBuffer(final QpidByteBuffer buffer, final int maxNestedObjects)
+ {
+ final ArrayList<Object> result = new ArrayList<>();
+ final int size = EncodingUtils.checkLength(buffer.getUnsignedInt(), buffer);
try (QpidByteBuffer slicedBuffer = buffer.view(0, size))
{
buffer.position(buffer.position() + size);
while (slicedBuffer.hasRemaining())
{
- result.add(AMQTypedValue.readFromBuffer(slicedBuffer).getValue());
+ result.add(AMQTypedValue.readFromBuffer(slicedBuffer, maxNestedObjects).getValue());
}
}
return new FieldArray<>(result);
@@ -130,7 +135,7 @@ public class FieldArray<T> extends AbstractCollection<T>
public static void skipFieldArray(final QpidByteBuffer buffer)
{
- int size = buffer.getInt();
+ final int size = EncodingUtils.checkLength(buffer.getUnsignedInt(), buffer);
if (size > 0)
{
buffer.position(buffer.position() + size);
diff --git a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/FieldTable.java b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/FieldTable.java
index b5ad4db480..35876132ea 100644
--- a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/FieldTable.java
+++ b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/FieldTable.java
@@ -47,20 +47,29 @@ public class FieldTable
static boolean _strictAMQP = Boolean.valueOf(System.getProperty(STRICT_AMQP_NAME, "false"));
private final FieldTableSupport _fieldTableSupport;
+ private final int _maxNestedObjects;
- FieldTable(QpidByteBuffer input, int len)
+ FieldTable(final QpidByteBuffer input, final int len)
{
+ this(input, len, AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS);
+ }
+
+ FieldTable(final QpidByteBuffer input, final int len, final int maxNestedObjects)
+ {
+ FieldValueNestingValidator.validateMaximum(maxNestedObjects);
final QpidByteBuffer encodedForm = input.view(0, len);
input.position(input.position() + len);
_fieldTableSupport = new ByteBufferFieldTableSupport(encodedForm);
+ _maxNestedObjects = maxNestedObjects;
}
- FieldTable(QpidByteBuffer buffer)
+ FieldTable(final QpidByteBuffer buffer)
{
+ _maxNestedObjects = AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS;
_fieldTableSupport = new ByteBufferFieldTableSupport(buffer.duplicate());
}
- FieldTable(Map<String, Object> properties)
+ FieldTable(final Map<String, Object> properties)
{
final Map<String, AMQTypedValue> m;
if (properties != null && !properties.isEmpty())
@@ -79,11 +88,19 @@ public class FieldTable
}
_fieldTableSupport = new MapFieldTableSupport(m);
+ _maxNestedObjects = AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS;
+ }
+
+ FieldTable(final FieldTableSupport fieldTableSupport)
+ {
+ this(fieldTableSupport, AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS);
}
- FieldTable(FieldTableSupport fieldTableSupport)
+ FieldTable(final FieldTableSupport fieldTableSupport, final int maxNestedObjects)
{
- _fieldTableSupport = new MapFieldTableSupport(fieldTableSupport.getAsMap());
+ FieldValueNestingValidator.validateMaximum(maxNestedObjects);
+ _fieldTableSupport = new MapFieldTableSupport(fieldTableSupport.getAsMap(maxNestedObjects));
+ _maxNestedObjects = maxNestedObjects;
}
private static AMQTypedValue getAMQTypeValue(final Object object) throws AMQPInvalidClassException
@@ -179,10 +196,10 @@ public class FieldTable
@Override
public String toString()
{
- return _fieldTableSupport.toString();
+ return getProperties().toString();
}
- private static void checkPropertyName(String propertyName)
+ static void checkPropertyName(final String propertyName)
{
if (propertyName == null)
{
@@ -266,14 +283,43 @@ public class FieldTable
}
public static Map<String, Object> convertToMap(final FieldTable fieldTable)
+ {
+ final int maxNestedObjects = fieldTable == null
+ ? AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS
+ : fieldTable._maxNestedObjects;
+ return convertToMap(fieldTable, maxNestedObjects);
+ }
+
+ public static Map<String, Object> convertToMap(final FieldTable fieldTable, final int maxNestedObjects)
+ {
+ if (maxNestedObjects < 0)
+ {
+ throw new IllegalArgumentException("Maximum nested objects must not be negative: " + maxNestedObjects);
+ }
+ if (fieldTable != null)
+ {
+ fieldTable.validateNesting(maxNestedObjects);
+ }
+ return convertToMap(fieldTable, 1, maxNestedObjects);
+ }
+
+ private static Map<String, Object> convertToMap(final FieldTable fieldTable,
+ final int depth,
+ final int maxNestedObjects)
{
if (fieldTable != null)
{
+ if (depth > maxNestedObjects)
+ {
+ throw new AMQValueNestingException("Maximum field-value nesting depth (" +
+ maxNestedObjects + ") exceeded");
+ }
+
final Map<String, Object> map = new LinkedHashMap<>();
- Map<String, AMQTypedValue> properties = fieldTable.getProperties();
+ final Map<String, AMQTypedValue> properties = fieldTable.getProperties();
if (properties != null)
{
- for (Map.Entry<String, AMQTypedValue> e : properties.entrySet())
+ for (final Map.Entry<String, AMQTypedValue> e : properties.entrySet())
{
Object val = e.getValue().getValue();
if (val instanceof AMQShortString)
@@ -282,7 +328,12 @@ public class FieldTable
}
else if (val instanceof FieldTable)
{
- val = FieldTable.convertToMap((FieldTable) val);
+ if (depth >= maxNestedObjects)
+ {
+ throw new AMQValueNestingException("Maximum field-value nesting depth (" +
+ maxNestedObjects + ") exceeded");
+ }
+ val = convertToMap((FieldTable) val, depth + 1, maxNestedObjects);
}
map.put(e.getKey(), val);
}
@@ -307,9 +358,9 @@ public class FieldTable
return getEncodedSize() > 0;
}
- public boolean containsKey(String key)
+ public boolean containsKey(final String key)
{
- return _fieldTableSupport.containsKey(key);
+ return _fieldTableSupport.containsKey(key, _maxNestedObjects);
}
public Set<String> keys()
@@ -317,10 +368,10 @@ public class FieldTable
return new LinkedHashSet<>(getProperties().keySet());
}
- public Object get(String key)
+ public Object get(final String key)
{
checkPropertyName(key);
- return _fieldTableSupport.get(key);
+ return _fieldTableSupport.get(key, _maxNestedObjects);
}
@Override
@@ -348,7 +399,7 @@ public class FieldTable
private Map<String, AMQTypedValue> getProperties()
{
- return _fieldTableSupport.getAsMap();
+ return _fieldTableSupport.getAsMap(_maxNestedObjects);
}
public static FieldTable convertToFieldTable(Map<String, Object> map)
@@ -370,31 +421,88 @@ public class FieldTable
return null;
}
- return new FieldTable(fieldTable._fieldTableSupport);
+ fieldTable.validate();
+ if (fieldTable.isDirect())
+ {
+ try (final QpidByteBuffer encodedForm = QpidByteBuffer.wrap(fieldTable.getDataAsBytes()))
+ {
+ final FieldTable detachedTable = new FieldTable(encodedForm, encodedForm.remaining(),
+ fieldTable._maxNestedObjects);
+ try
+ {
+ detachedTable._fieldTableSupport.markValidated(fieldTable._maxNestedObjects);
+ return new FieldTable(detachedTable._fieldTableSupport, fieldTable._maxNestedObjects);
+ }
+ finally
+ {
+ detachedTable.dispose();
+ }
+ }
+ }
+
+ return new FieldTable(fieldTable._fieldTableSupport, fieldTable._maxNestedObjects);
+ }
+
+ boolean isDirect()
+ {
+ return _fieldTableSupport.isDirect();
+ }
+
+ private static void markValueValidated(final Object value, final int maxNestedObjects)
+ {
+ if (value instanceof FieldTable)
+ {
+ ((FieldTable) value)._fieldTableSupport.markValidated(maxNestedObjects);
+ }
+ else if (value instanceof Collection)
+ {
+ for (final Object element : (Collection<?>) value)
+ {
+ markValueValidated(element, maxNestedObjects);
+ }
+ }
}
public void validate()
{
- _fieldTableSupport.validate();
+ validate(_maxNestedObjects);
+ }
+
+ public void validate(final int maxNestedObjects)
+ {
+ FieldValueNestingValidator.validateMaximum(maxNestedObjects);
+ _fieldTableSupport.validate(maxNestedObjects);
+ }
+
+ void validateNesting(final int maxNestedObjects)
+ {
+ FieldValueNestingValidator.validateMaximum(maxNestedObjects);
+ _fieldTableSupport.validateNesting(maxNestedObjects);
}
interface FieldTableSupport
{
- Object get(String key);
+ Object get(final String key, final int maxNestedObjects);
- boolean containsKey(String key);
+ boolean containsKey(final String key, final int maxNestedObjects);
long getEncodedSize();
- void writeToBuffer(QpidByteBuffer buffer);
+ void writeToBuffer(final QpidByteBuffer buffer);
byte[] getAsBytes();
- Map<String, AMQTypedValue> getAsMap();
+ Map<String, AMQTypedValue> getAsMap(final int maxNestedObjects);
void dispose();
- void validate();
+ void validate(final int maxNestedObjects);
+
+ boolean isDirect();
+
+ void markValidated(final int maxNestedObjects);
+
+ void validateNesting(final int maxNestedObjects);
}
static class ByteBufferFieldTableSupport implements FieldTableSupport
@@ -403,6 +511,7 @@ public class FieldTable
private final QpidByteBuffer _encodedForm;
private volatile SoftReference<Map<String, AMQTypedValue>> _cache;
+ private volatile int _validatedMaxNestedObjects = -1;
ByteBufferFieldTableSupport(final QpidByteBuffer encodedForm)
{
@@ -417,9 +526,10 @@ public class FieldTable
}
@Override
- public synchronized Object get(final String key)
+ public synchronized Object get(final String key, final int maxNestedObjects)
{
- final AMQTypedValue value = getValue(key);
+ validateEncodedForm(maxNestedObjects);
+ final AMQTypedValue value = getValue(key, maxNestedObjects);
if (value != null && value != NOT_PRESENT)
{
return value.getValue();
@@ -431,9 +541,10 @@ public class FieldTable
}
@Override
- public boolean containsKey(final String key)
+ public boolean containsKey(final String key, final int maxNestedObjects)
{
- final AMQTypedValue value = getValue(key);
+ validateEncodedForm(maxNestedObjects);
+ final AMQTypedValue value = getValue(key, maxNestedObjects);
return value != null && value != NOT_PRESENT;
}
@@ -454,9 +565,9 @@ public class FieldTable
}
@Override
- public Map<String, AMQTypedValue> getAsMap()
+ public Map<String, AMQTypedValue> getAsMap(final int maxNestedObjects)
{
- return decode();
+ return decode(maxNestedObjects);
}
@Override
@@ -470,9 +581,36 @@ public class FieldTable
}
@Override
- public void validate()
+ public void validate(final int maxNestedObjects)
+ {
+ validateEncodedForm(maxNestedObjects);
+ }
+
+ @Override
+ public boolean isDirect()
+ {
+ return _encodedForm.isDirect();
+ }
+
+ @Override
+ public synchronized void markValidated(final int maxNestedObjects)
+ {
+ FieldValueNestingValidator.validateMaximum(maxNestedObjects);
+ _validatedMaxNestedObjects = _validatedMaxNestedObjects < 0
+ ? maxNestedObjects
+ : Math.min(_validatedMaxNestedObjects, maxNestedObjects);
+ }
+
+ @Override
+ public synchronized void validateNesting(final int maxNestedObjects)
{
- decode();
+ if (_validatedMaxNestedObjects < 0 || maxNestedObjects < _validatedMaxNestedObjects)
+ {
+ if (FieldValueNestingValidator.mayExceedMaximumDepth(_encodedForm.remaining(), maxNestedObjects))
+ {
+ validateEncodedForm(maxNestedObjects);
+ }
+ }
}
@Override
@@ -498,14 +636,7 @@ public class FieldTable
return _encodedForm.hashCode();
}
- @Override
- public String toString()
- {
- return getAsMap().toString();
- }
-
-
- private synchronized AMQTypedValue getValue(final String key)
+ private synchronized AMQTypedValue getValue(final String key, final int maxNestedObjects)
{
AMQTypedValue value = null;
Map<String, AMQTypedValue> properties = _cache.get();
@@ -520,7 +651,7 @@ public class FieldTable
}
if (value == null)
{
- value = findValueForKey(key);
+ value = findValueForKey(key, maxNestedObjects);
if (value == null)
{
value = NOT_PRESENT;
@@ -533,9 +664,9 @@ public class FieldTable
return value;
}
- private AMQTypedValue findValueForKey(String key)
+ private AMQTypedValue findValueForKey(final String key, final int maxNestedObjects)
{
- byte[] keyBytes = key.getBytes(StandardCharsets.UTF_8);
+ final byte[] keyBytes = key.getBytes(StandardCharsets.UTF_8);
_encodedForm.mark();
try
{
@@ -544,11 +675,13 @@ public class FieldTable
final byte[] bytes = AMQShortString.readAMQShortStringAsBytes(_encodedForm);
if (Arrays.equals(keyBytes, bytes))
{
- return AMQTypedValue.readFromBuffer(_encodedForm);
+ final AMQTypedValue value = AMQTypedValue.readFromBuffer(_encodedForm, maxNestedObjects);
+ markValueValidated(value.getValue(), _validatedMaxNestedObjects);
+ return value;
}
else
{
- AMQType type = AMQTypeMap.getType(_encodedForm.get());
+ final AMQType type = AMQTypeMap.getType(_encodedForm.get());
type.skip(_encodedForm);
}
}
@@ -560,8 +693,9 @@ public class FieldTable
return null;
}
- private synchronized Map<String, AMQTypedValue> decode()
+ private synchronized Map<String, AMQTypedValue> decode(final int maxNestedObjects)
{
+ validateEncodedForm(maxNestedObjects);
final Map<String, AMQTypedValue> properties = new HashMap<>();
final long encodedSize = getEncodedSize();
if (encodedSize > 0)
@@ -574,7 +708,8 @@ public class FieldTable
final String key = AMQShortString.readAMQShortStringAsString(_encodedForm);
checkPropertyName(key);
- AMQTypedValue value = AMQTypedValue.readFromBuffer(_encodedForm);
+ final AMQTypedValue value = AMQTypedValue.readFromBuffer(_encodedForm, maxNestedObjects);
+ markValueValidated(value.getValue(), _validatedMaxNestedObjects);
properties.put(key, value);
}
while (_encodedForm.hasRemaining());
@@ -595,6 +730,15 @@ public class FieldTable
}
return properties;
}
+
+ private synchronized void validateEncodedForm(final int maxNestedObjects)
+ {
+ if (_validatedMaxNestedObjects < 0 || maxNestedObjects < _validatedMaxNestedObjects)
+ {
+ FieldValueNestingValidator.validateTable(_encodedForm, maxNestedObjects);
+ markValidated(maxNestedObjects);
+ }
+ }
}
static class MapFieldTableSupport implements FieldTableSupport
@@ -615,7 +759,7 @@ public class FieldTable
}
@Override
- public Object get(final String key)
+ public Object get(final String key, final int maxNestedObjects)
{
final AMQTypedValue value = _properties.get(key);
if (value == null)
@@ -626,7 +770,7 @@ public class FieldTable
}
@Override
- public boolean containsKey(final String key)
+ public boolean containsKey(final String key, final int maxNestedObjects)
{
return _properties.containsKey(key);
}
@@ -651,7 +795,7 @@ public class FieldTable
}
@Override
- public Map<String, AMQTypedValue> getAsMap()
+ public Map<String, AMQTypedValue> getAsMap(final int maxNestedObjects)
{
return _properties;
}
@@ -663,7 +807,25 @@ public class FieldTable
}
@Override
- public void validate()
+ public void validate(final int maxNestedObjects)
+ {
+ // noop
+ }
+
+ @Override
+ public boolean isDirect()
+ {
+ return false;
+ }
+
+ @Override
+ public void markValidated(final int maxNestedObjects)
+ {
+ // noop
+ }
+
+ @Override
+ public void validateNesting(final int maxNestedObjects)
{
// noop
}
diff --git a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/FieldTableFactory.java b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/FieldTableFactory.java
index b0a53dcdff..15af1c1aae 100644
--- a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/FieldTableFactory.java
+++ b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/FieldTableFactory.java
@@ -40,6 +40,13 @@ public class FieldTableFactory
return new FieldTable(qpidByteBuffer, length);
}
+ static FieldTable createFieldTable(final QpidByteBuffer qpidByteBuffer,
+ final int length,
+ final int maxNestedObjects)
+ {
+ return new FieldTable(qpidByteBuffer, length, maxNestedObjects);
+ }
+
public static FieldTable createFieldTable(final Map<String, Object> map)
{
return new FieldTable(map);
diff --git a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/FieldValueNestingValidator.java b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/FieldValueNestingValidator.java
new file mode 100644
index 0000000000..42df6843f0
--- /dev/null
+++ b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/FieldValueNestingValidator.java
@@ -0,0 +1,154 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ */
+package org.apache.qpid.server.protocol.v0_8;
+
+import java.nio.charset.StandardCharsets;
+
+import org.apache.qpid.server.bytebuffer.QpidByteBuffer;
+
+final class FieldValueNestingValidator
+{
+ private static final int MINIMUM_FIELD_NAME_ENCODING_SIZE = 2 * Byte.BYTES;
+ private static final int COMPOUND_VALUE_HEADER_SIZE = Byte.BYTES + Integer.BYTES;
+ private static final int FIRST_NESTED_VALUE_SIZE = MINIMUM_FIELD_NAME_ENCODING_SIZE + COMPOUND_VALUE_HEADER_SIZE;
+ private static final int SUBSEQUENT_NESTED_VALUE_SIZE = COMPOUND_VALUE_HEADER_SIZE;
+
+ private FieldValueNestingValidator()
+ {
+ // utility has no public constructor
+ }
+
+ static void validateTable(final QpidByteBuffer encodedTable, final int maxNestedObjects)
+ {
+ validateMaximum(maxNestedObjects);
+
+ try (final QpidByteBuffer table = encodedTable.duplicate())
+ {
+ checkDepth(1, maxNestedObjects);
+ validateTableContents(table, 1, maxNestedObjects);
+ }
+ }
+
+ static boolean mayExceedMaximumDepth(final int encodedSize, final int maxNestedObjects)
+ {
+ validateMaximum(maxNestedObjects);
+ if (maxNestedObjects == 0)
+ {
+ return true;
+ }
+
+ final long minimumExcessiveSize = FIRST_NESTED_VALUE_SIZE + (long) SUBSEQUENT_NESTED_VALUE_SIZE *
+ (maxNestedObjects - 1);
+ return encodedSize >= minimumExcessiveSize;
+ }
+
+ static void validateMaximum(final int maxNestedObjects)
+ {
+ if (maxNestedObjects < 0)
+ {
+ throw new IllegalArgumentException("Maximum nested objects must not be negative: " + maxNestedObjects);
+ }
+ }
+
+ private static void validateTableContents(final QpidByteBuffer table,
+ final int depth,
+ final int maxNestedObjects)
+ {
+ while (table.hasRemaining())
+ {
+ validateFieldName(table);
+ validateValue(table, depth, maxNestedObjects);
+ }
+ }
+
+ private static void validateArrayContents(final QpidByteBuffer array,
+ final int depth,
+ final int maxNestedObjects)
+ {
+ while (array.hasRemaining())
+ {
+ validateValue(array, depth, maxNestedObjects);
+ }
+ }
+
+ private static void validateFieldName(final QpidByteBuffer table)
+ {
+ final int length = table.getUnsignedByte();
+ if (length == 0)
+ {
+ throw new IllegalArgumentException("Field-table property name must not be empty");
+ }
+
+ EncodingUtils.checkLength(length, table);
+ if (FieldTable._strictAMQP)
+ {
+ final byte[] bytes = new byte[length];
+ table.get(bytes);
+ FieldTable.checkPropertyName(new String(bytes, StandardCharsets.UTF_8));
+ }
+ else
+ {
+ table.position(table.position() + length);
+ }
+ }
+
+ private static void validateValue(final QpidByteBuffer buffer,
+ final int depth,
+ final int maxNestedObjects)
+ {
+ final AMQType type = AMQTypeMap.getType(buffer.get());
+ if (type == AMQType.FIELD_TABLE || type == AMQType.FIELD_ARRAY)
+ {
+ final int nestedDepth = depth + 1;
+ checkDepth(nestedDepth, maxNestedObjects);
+
+ final int length = EncodingUtils.checkLength(buffer.getUnsignedInt(), buffer);
+ try (final QpidByteBuffer nested = buffer.view(0, length))
+ {
+ buffer.position(buffer.position() + length);
+ if (type == AMQType.FIELD_TABLE)
+ {
+ validateTableContents(nested, nestedDepth, maxNestedObjects);
+ }
+ else
+ {
+ validateArrayContents(nested, nestedDepth, maxNestedObjects);
+ }
+ }
+ }
+ else if (type == AMQType.LONG_STRING || type == AMQType.ASCII_STRING || type == AMQType.WIDE_STRING)
+ {
+ EncodingUtils.readLongString(buffer);
+ }
+ else
+ {
+ type.skip(buffer);
+ }
+ }
+
+ private static void checkDepth(final int depth, final int maxNestedObjects)
+ {
+ if (depth > maxNestedObjects)
+ {
+ throw new AMQValueNestingException("Maximum field-value nesting depth (" + maxNestedObjects + ") exceeded");
+ }
+ }
+}
diff --git a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/FrameBoundaryValidatingServerMethodProcessor.java b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/FrameBoundaryValidatingServerMethodProcessor.java
new file mode 100644
index 0000000000..433fe4304d
--- /dev/null
+++ b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/FrameBoundaryValidatingServerMethodProcessor.java
@@ -0,0 +1,610 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ */
+package org.apache.qpid.server.protocol.v0_8;
+
+import java.util.Objects;
+
+import org.apache.qpid.server.bytebuffer.QpidByteBuffer;
+import org.apache.qpid.server.protocol.ProtocolVersion;
+import org.apache.qpid.server.protocol.v0_8.transport.BasicContentHeaderProperties;
+import org.apache.qpid.server.protocol.v0_8.transport.ProtocolInitiation;
+import org.apache.qpid.server.protocol.v0_8.transport.ServerChannelMethodProcessor;
+import org.apache.qpid.server.protocol.v0_8.transport.ServerMethodProcessor;
+import org.apache.qpid.server.util.ConnectionScopedRuntimeException;
+import org.apache.qpid.server.util.ServerScopedRuntimeException;
+
+final class FrameBoundaryValidatingServerMethodProcessor
+ implements ServerMethodProcessor<ServerChannelMethodProcessor>, ServerChannelMethodProcessor
+{
+ private static final int NO_CHANNEL = -1;
+
+ private final ServerMethodProcessor<? extends ServerChannelMethodProcessor> _delegate;
+
+ private QpidByteBuffer _frameBody;
+ private int _channelId = NO_CHANNEL;
+ private ServerChannelMethodProcessor _channelDelegate;
+ private int _classId;
+ private int _methodId;
+ private boolean _currentMethodRecorded;
+ private boolean _currentMethodPublished;
+ private boolean _channelMethodValidationPerformed;
+ private boolean _channelMethodRejected;
+
+ FrameBoundaryValidatingServerMethodProcessor(final ServerMethodProcessor<? extends ServerChannelMethodProcessor> delegate)
+ {
+ _delegate = Objects.requireNonNull(delegate);
+ }
+
+ void begin(final QpidByteBuffer frameBody)
+ {
+ if (_frameBody != null)
+ {
+ throw new IllegalStateException("Method frame validation is already active");
+ }
+ _frameBody = Objects.requireNonNull(frameBody);
+ _channelId = NO_CHANNEL;
+ _channelDelegate = null;
+ _classId = 0;
+ _methodId = 0;
+ _currentMethodRecorded = false;
+ _currentMethodPublished = false;
+ _channelMethodValidationPerformed = false;
+ _channelMethodRejected = false;
+ }
+
+ void end()
+ {
+ _frameBody = null;
+ _channelId = NO_CHANNEL;
+ _channelDelegate = null;
+ _classId = 0;
+ _methodId = 0;
+ _currentMethodRecorded = false;
+ _currentMethodPublished = false;
+ _channelMethodValidationPerformed = false;
+ _channelMethodRejected = false;
+ }
+
+ void publishCurrentMethodIfFrameBodyConsumed()
+ {
+ ensureActive();
+ if (!_frameBody.hasRemaining())
+ {
+ publishCurrentMethod();
+ }
+ }
+
+ boolean isMethodBodyDecoded()
+ {
+ return _currentMethodPublished;
+ }
+
+ @Override
+ public ProtocolVersion getProtocolVersion()
+ {
+ try
+ {
+ return _delegate.getProtocolVersion();
+ }
+ catch (final ConnectionScopedRuntimeException | ServerScopedRuntimeException e)
+ {
+ throw e;
+ }
+ catch (RuntimeException e)
+ {
+ throw new ConnectionScopedRuntimeException(e);
+ }
+ }
+
+ @Override
+ public ServerChannelMethodProcessor getChannelMethodProcessor(final int channelId)
+ {
+ ensureActive();
+ if (_channelId == NO_CHANNEL)
+ {
+ _channelId = channelId;
+ }
+ else if (_channelId != channelId)
+ {
+ throw new IllegalStateException("Multiple channels referenced by one method frame");
+ }
+ return this;
+ }
+
+ @Override
+ public void receiveConnectionStartOk(final FieldTable clientProperties,
+ final AMQShortString mechanism,
+ final byte[] response,
+ final AMQShortString locale)
+ {
+ if (isFrameBodyConsumed())
+ {
+ _delegate.receiveConnectionStartOk(clientProperties, mechanism, response, locale);
+ }
+ }
+
+ @Override
+ public void receiveConnectionSecureOk(final byte[] response)
+ {
+ if (isFrameBodyConsumed())
+ {
+ _delegate.receiveConnectionSecureOk(response);
+ }
+ }
+
+ @Override
+ public void receiveConnectionTuneOk(final int channelMax, final long frameMax, final int heartbeat)
+ {
+ if (isFrameBodyConsumed())
+ {
+ _delegate.receiveConnectionTuneOk(channelMax, frameMax, heartbeat);
+ }
+ }
+
+ @Override
+ public void receiveConnectionOpen(final AMQShortString virtualHost,
+ final AMQShortString capabilities,
+ final boolean insist)
+ {
+ if (isFrameBodyConsumed())
+ {
+ _delegate.receiveConnectionOpen(virtualHost, capabilities, insist);
+ }
+ }
+
+ @Override
+ public void receiveChannelOpen(final int channelId)
+ {
+ if (isFrameBodyConsumed())
+ {
+ _delegate.receiveChannelOpen(channelId);
+ }
+ }
+
+ @Override
+ public void receiveConnectionClose(final int replyCode,
+ final AMQShortString replyText,
+ final int classId,
+ final int methodId)
+ {
+ if (isFrameBodyConsumed())
+ {
+ _delegate.receiveConnectionClose(replyCode, replyText, classId, methodId);
+ }
+ }
+
+ @Override
+ public void receiveConnectionCloseOk()
+ {
+ if (isFrameBodyConsumed())
+ {
+ _delegate.receiveConnectionCloseOk();
+ }
+ }
+
+ @Override
+ public void receiveHeartbeat()
+ {
+ if (isFrameBodyConsumed())
+ {
+ _delegate.receiveHeartbeat();
+ }
+ }
+
+ @Override
+ public void receiveProtocolHeader(final ProtocolInitiation protocolInitiation)
+ {
+ if (isFrameBodyConsumed())
+ {
+ _delegate.receiveProtocolHeader(protocolInitiation);
+ }
+ }
+
+ @Override
+ public void setCurrentMethod(final int classId, final int methodId)
+ {
+ ensureActive();
+ if (_currentMethodRecorded)
+ {
+ throw new IllegalStateException("Current method is already recorded for this frame");
+ }
+ _classId = classId;
+ _methodId = methodId;
+ _currentMethodRecorded = true;
+ }
+
+ @Override
+ public boolean ignoreAllButCloseOk()
+ {
+ if (_channelId == NO_CHANNEL)
+ {
+ return !isFrameBodyConsumed() || _delegate.ignoreAllButCloseOk();
+ }
+ return !isChannelMethodDispatchAllowed() || getChannelDelegate().ignoreAllButCloseOk();
+ }
+
+ @Override
+ public void receiveAccessRequest(final AMQShortString realm,
+ final boolean exclusive,
+ final boolean passive,
+ final boolean active,
+ final boolean write,
+ final boolean read)
+ {
+ if (isChannelMethodDispatchAllowed())
+ {
+ getChannelDelegate().receiveAccessRequest(realm, exclusive, passive, active, write, read);
+ }
+ }
+
+ @Override
+ public void receiveExchangeDeclare(final AMQShortString exchange,
+ final AMQShortString type,
+ final boolean passive,
+ final boolean durable,
+ final boolean autoDelete,
+ final boolean internal,
+ final boolean nowait,
+ final FieldTable arguments)
+ {
+ if (isChannelMethodDispatchAllowed())
+ {
+ getChannelDelegate().receiveExchangeDeclare(exchange, type, passive, durable, autoDelete, internal,
+ nowait, arguments);
+ }
+ }
+
+ @Override
+ public void receiveExchangeDelete(final AMQShortString exchange, final boolean ifUnused, final boolean nowait)
+ {
+ if (isChannelMethodDispatchAllowed())
+ {
+ getChannelDelegate().receiveExchangeDelete(exchange, ifUnused, nowait);
+ }
+ }
+
+ @Override
+ public void receiveExchangeBound(final AMQShortString exchange,
+ final AMQShortString routingKey,
+ final AMQShortString queue)
+ {
+ if (isChannelMethodDispatchAllowed())
+ {
+ getChannelDelegate().receiveExchangeBound(exchange, routingKey, queue);
+ }
+ }
+
+ @Override
+ public void receiveQueueDeclare(final AMQShortString queue,
+ final boolean passive,
+ final boolean durable,
+ final boolean exclusive,
+ final boolean autoDelete,
+ final boolean nowait,
+ final FieldTable arguments)
+ {
+ if (isChannelMethodDispatchAllowed())
+ {
+ getChannelDelegate().receiveQueueDeclare(queue, passive, durable, exclusive, autoDelete, nowait, arguments);
+ }
+ }
+
+ @Override
+ public void receiveQueueBind(final AMQShortString queue,
+ final AMQShortString exchange,
+ final AMQShortString bindingKey,
+ final boolean nowait,
+ final FieldTable arguments)
+ {
+ if (isChannelMethodDispatchAllowed())
+ {
+ getChannelDelegate().receiveQueueBind(queue, exchange, bindingKey, nowait, arguments);
+ }
+ }
+
+ @Override
+ public void receiveQueuePurge(final AMQShortString queue, final boolean nowait)
+ {
+ if (isChannelMethodDispatchAllowed())
+ {
+ getChannelDelegate().receiveQueuePurge(queue, nowait);
+ }
+ }
+
+ @Override
+ public void receiveQueueDelete(final AMQShortString queue,
+ final boolean ifUnused,
+ final boolean ifEmpty,
+ final boolean nowait)
+ {
+ if (isChannelMethodDispatchAllowed())
+ {
+ getChannelDelegate().receiveQueueDelete(queue, ifUnused, ifEmpty, nowait);
+ }
+ }
+
+ @Override
+ public void receiveQueueUnbind(final AMQShortString queue,
+ final AMQShortString exchange,
+ final AMQShortString bindingKey,
+ final FieldTable arguments)
+ {
+ if (isChannelMethodDispatchAllowed())
+ {
+ getChannelDelegate().receiveQueueUnbind(queue, exchange, bindingKey, arguments);
+ }
+ }
+
+ @Override
+ public void receiveBasicRecover(final boolean requeue, final boolean sync)
+ {
+ if (isChannelMethodDispatchAllowed())
+ {
+ getChannelDelegate().receiveBasicRecover(requeue, sync);
+ }
+ }
+
+ @Override
+ public void receiveBasicQos(final long prefetchSize, final int prefetchCount, final boolean global)
+ {
+ if (isChannelMethodDispatchAllowed())
+ {
+ getChannelDelegate().receiveBasicQos(prefetchSize, prefetchCount, global);
+ }
+ }
+
+ @Override
+ public void receiveBasicConsume(final AMQShortString queue,
+ final AMQShortString consumerTag,
+ final boolean noLocal,
+ final boolean noAck,
+ final boolean exclusive,
+ final boolean nowait,
+ final FieldTable arguments)
+ {
+ if (isChannelMethodDispatchAllowed())
+ {
+ getChannelDelegate().receiveBasicConsume(queue, consumerTag, noLocal, noAck, exclusive, nowait, arguments);
+ }
+ }
+
+ @Override
+ public void receiveBasicCancel(final AMQShortString consumerTag, final boolean noWait)
+ {
+ if (isChannelMethodDispatchAllowed())
+ {
+ getChannelDelegate().receiveBasicCancel(consumerTag, noWait);
+ }
+ }
+
+ @Override
+ public void receiveBasicPublish(final AMQShortString exchange,
+ final AMQShortString routingKey,
+ final boolean mandatory,
+ final boolean immediate)
+ {
+ if (isChannelMethodDispatchAllowed())
+ {
+ getChannelDelegate().receiveBasicPublish(exchange, routingKey, mandatory, immediate);
+ }
+ }
+
+ @Override
+ public void receiveBasicGet(final AMQShortString queue, final boolean noAck)
+ {
+ if (isChannelMethodDispatchAllowed())
+ {
+ getChannelDelegate().receiveBasicGet(queue, noAck);
+ }
+ }
+
+ @Override
+ public void receiveBasicAck(final long deliveryTag, final boolean multiple)
+ {
+ if (isChannelMethodDispatchAllowed())
+ {
+ getChannelDelegate().receiveBasicAck(deliveryTag, multiple);
+ }
+ }
+
+ @Override
+ public void receiveBasicReject(final long deliveryTag, final boolean requeue)
+ {
+ if (isChannelMethodDispatchAllowed())
+ {
+ getChannelDelegate().receiveBasicReject(deliveryTag, requeue);
+ }
+ }
+
+ @Override
+ public void receiveTxSelect()
+ {
+ if (isChannelMethodDispatchAllowed())
+ {
+ getChannelDelegate().receiveTxSelect();
+ }
+ }
+
+ @Override
+ public void receiveTxCommit()
+ {
+ if (isChannelMethodDispatchAllowed())
+ {
+ getChannelDelegate().receiveTxCommit();
+ }
+ }
+
+ @Override
+ public void receiveTxRollback()
+ {
+ if (isChannelMethodDispatchAllowed())
+ {
+ getChannelDelegate().receiveTxRollback();
+ }
+ }
+
+ @Override
+ public void receiveConfirmSelect(final boolean nowait)
+ {
+ if (isChannelMethodDispatchAllowed())
+ {
+ getChannelDelegate().receiveConfirmSelect(nowait);
+ }
+ }
+
+ @Override
+ public void receiveChannelFlow(final boolean active)
+ {
+ if (isChannelMethodDispatchAllowed())
+ {
+ getChannelDelegate().receiveChannelFlow(active);
+ }
+ }
+
+ @Override
+ public void receiveChannelFlowOk(final boolean active)
+ {
+ if (isChannelMethodDispatchAllowed())
+ {
+ getChannelDelegate().receiveChannelFlowOk(active);
+ }
+ }
+
+ @Override
+ public void receiveChannelClose(final int replyCode,
+ final AMQShortString replyText,
+ final int classId,
+ final int methodId)
+ {
+ if (isChannelMethodDispatchAllowed())
+ {
+ getChannelDelegate().receiveChannelClose(replyCode, replyText, classId, methodId);
+ }
+ }
+
+ @Override
+ public void receiveChannelCloseOk()
+ {
+ if (isChannelMethodDispatchAllowed())
+ {
+ getChannelDelegate().receiveChannelCloseOk();
+ }
+ }
+
+ @Override
+ public void receiveMessageContent(final QpidByteBuffer data)
+ {
+ if (isFrameBodyConsumed())
+ {
+ getChannelDelegate().receiveMessageContent(data);
+ }
+ }
+
+ @Override
+ public void receiveMessageHeader(final BasicContentHeaderProperties properties, final long bodySize)
+ {
+ boolean propertiesTransferred = false;
+ try
+ {
+ if (isFrameBodyConsumed())
+ {
+ final ServerChannelMethodProcessor channelDelegate = getChannelDelegate();
+ propertiesTransferred = true;
+ channelDelegate.receiveMessageHeader(properties, bodySize);
+ }
+ }
+ finally
+ {
+ if (!propertiesTransferred)
+ {
+ properties.dispose();
+ }
+ }
+ }
+
+ @Override
+ public void receiveBasicNack(final long deliveryTag, final boolean multiple, final boolean requeue)
+ {
+ if (isChannelMethodDispatchAllowed())
+ {
+ getChannelDelegate().receiveBasicNack(deliveryTag, multiple, requeue);
+ }
+ }
+
+ private boolean isFrameBodyConsumed()
+ {
+ ensureActive();
+ if (_frameBody.hasRemaining())
+ {
+ return false;
+ }
+ publishCurrentMethod();
+ return true;
+ }
+
+ private boolean isChannelMethodDispatchAllowed()
+ {
+ if (!isFrameBodyConsumed())
+ {
+ return false;
+ }
+ if (!_channelMethodValidationPerformed)
+ {
+ _channelMethodRejected = getChannelDelegate().rejectMethodFrameIfContentIncomplete();
+ _channelMethodValidationPerformed = true;
+ }
+ return !_channelMethodRejected;
+ }
+
+ private void publishCurrentMethod()
+ {
+ if (!_currentMethodRecorded)
+ {
+ throw new IllegalStateException("No current method was recorded for this frame");
+ }
+ if (!_currentMethodPublished)
+ {
+ _currentMethodPublished = true;
+ _delegate.setCurrentMethod(_classId, _methodId);
+ }
+ }
+
+ private void ensureActive()
+ {
+ if (_frameBody == null)
+ {
+ throw new IllegalStateException("Method frame validation is not active");
+ }
+ }
+
+ private ServerChannelMethodProcessor getChannelDelegate()
+ {
+ if (_channelId == NO_CHANNEL)
+ {
+ throw new IllegalStateException("No channel was selected for the method frame");
+ }
+ if (_channelDelegate == null)
+ {
+ _channelDelegate = _delegate.getChannelMethodProcessor(_channelId);
+ }
+ return _channelDelegate;
+ }
+}
diff --git a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/IncomingMessage.java b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/IncomingMessage.java
index fc6d831bf9..590c0701f2 100644
--- a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/IncomingMessage.java
+++ b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/IncomingMessage.java
@@ -23,19 +23,20 @@ package org.apache.qpid.server.protocol.v0_8;
import java.util.ArrayList;
import java.util.List;
-import org.apache.qpid.server.protocol.v0_8.transport.ContentBody;
+import org.apache.qpid.server.bytebuffer.QpidByteBuffer;
+import org.apache.qpid.server.message.MessageDestination;
import org.apache.qpid.server.protocol.v0_8.transport.ContentHeaderBody;
import org.apache.qpid.server.protocol.v0_8.transport.MessagePublishInfo;
-import org.apache.qpid.server.message.MessageDestination;
public class IncomingMessage
{
private final MessagePublishInfo _messagePublishInfo;
- private final List<ContentBody> _contentChunks = new ArrayList<>();
+ private final List<QpidByteBuffer> _contentChunks = new ArrayList<>();
private ContentHeaderBody _contentHeaderBody;
private MessageDestination _messageDestination;
+ private int _contentBodyFrameCount;
/**
* Keeps a track of how many bytes we have received in body frames
@@ -57,11 +58,21 @@ public class IncomingMessage
return _messagePublishInfo;
}
- public long addContentBodyFrame(final ContentBody contentChunk)
+ public boolean addContentBodyFrame(final QpidByteBuffer contentChunk)
{
- _bodyLengthReceived += contentChunk.getSize();
- _contentChunks.add(contentChunk);
- return _bodyLengthReceived;
+ final int contentSize = contentChunk.remaining();
+ if (contentSize > getSize() - _bodyLengthReceived)
+ {
+ return false;
+ }
+
+ if (contentSize > 0)
+ {
+ _contentChunks.add(contentChunk.duplicate());
+ }
+ _bodyLengthReceived += contentSize;
+ _contentBodyFrameCount++;
+ return true;
}
public boolean allContentReceived()
@@ -94,14 +105,35 @@ public class IncomingMessage
_messageDestination = e;
}
- public int getBodyCount()
+ public int getContentBodyFrameCount()
+ {
+ return _contentBodyFrameCount;
+ }
+
+ public int getContentChunkCount()
{
return _contentChunks.size();
}
- public ContentBody getContentChunk(int index)
+ public QpidByteBuffer getContentChunk(final int index)
{
return _contentChunks.get(index);
}
+ public void dispose()
+ {
+ final ContentHeaderBody contentHeaderBody = _contentHeaderBody;
+ _contentHeaderBody = null;
+ if (contentHeaderBody != null)
+ {
+ contentHeaderBody.dispose();
+ }
+
+ for (final QpidByteBuffer contentChunk : _contentChunks)
+ {
+ contentChunk.dispose();
+ }
+ _contentChunks.clear();
+ }
+
}
diff --git a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/ServerDecoder.java b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/ServerDecoder.java
index 59d498572e..68bb87a91e 100644
--- a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/ServerDecoder.java
+++ b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/ServerDecoder.java
@@ -23,11 +23,46 @@ package org.apache.qpid.server.protocol.v0_8;
import java.io.IOException;
import org.apache.qpid.server.bytebuffer.QpidByteBuffer;
+import org.apache.qpid.server.protocol.ErrorCodes;
import org.apache.qpid.server.protocol.ProtocolVersion;
-import org.apache.qpid.server.protocol.v0_8.transport.*;
+import org.apache.qpid.server.protocol.v0_8.transport.AMQProtocolVersionException;
+import org.apache.qpid.server.protocol.v0_8.transport.AccessRequestBody;
+import org.apache.qpid.server.protocol.v0_8.transport.BasicAckBody;
+import org.apache.qpid.server.protocol.v0_8.transport.BasicCancelBody;
+import org.apache.qpid.server.protocol.v0_8.transport.BasicConsumeBody;
+import org.apache.qpid.server.protocol.v0_8.transport.BasicGetBody;
+import org.apache.qpid.server.protocol.v0_8.transport.BasicNackBody;
+import org.apache.qpid.server.protocol.v0_8.transport.BasicPublishBody;
+import org.apache.qpid.server.protocol.v0_8.transport.BasicQosBody;
+import org.apache.qpid.server.protocol.v0_8.transport.BasicRecoverBody;
+import org.apache.qpid.server.protocol.v0_8.transport.BasicRecoverSyncBody;
+import org.apache.qpid.server.protocol.v0_8.transport.BasicRejectBody;
+import org.apache.qpid.server.protocol.v0_8.transport.ChannelCloseBody;
+import org.apache.qpid.server.protocol.v0_8.transport.ChannelFlowBody;
+import org.apache.qpid.server.protocol.v0_8.transport.ChannelFlowOkBody;
+import org.apache.qpid.server.protocol.v0_8.transport.ChannelOpenBody;
+import org.apache.qpid.server.protocol.v0_8.transport.ConfirmSelectBody;
+import org.apache.qpid.server.protocol.v0_8.transport.ConnectionCloseBody;
+import org.apache.qpid.server.protocol.v0_8.transport.ConnectionOpenBody;
+import org.apache.qpid.server.protocol.v0_8.transport.ConnectionSecureOkBody;
+import org.apache.qpid.server.protocol.v0_8.transport.ConnectionStartOkBody;
+import org.apache.qpid.server.protocol.v0_8.transport.ConnectionTuneOkBody;
+import org.apache.qpid.server.protocol.v0_8.transport.ExchangeBoundBody;
+import org.apache.qpid.server.protocol.v0_8.transport.ExchangeDeclareBody;
+import org.apache.qpid.server.protocol.v0_8.transport.ExchangeDeleteBody;
+import org.apache.qpid.server.protocol.v0_8.transport.QueueBindBody;
+import org.apache.qpid.server.protocol.v0_8.transport.QueueDeclareBody;
+import org.apache.qpid.server.protocol.v0_8.transport.QueueDeleteBody;
+import org.apache.qpid.server.protocol.v0_8.transport.QueuePurgeBody;
+import org.apache.qpid.server.protocol.v0_8.transport.QueueUnbindBody;
+import org.apache.qpid.server.protocol.v0_8.transport.ServerChannelMethodProcessor;
+import org.apache.qpid.server.protocol.v0_8.transport.ServerMethodProcessor;
+import org.apache.qpid.server.util.ConnectionScopedRuntimeException;
+import org.apache.qpid.server.util.ServerScopedRuntimeException;
public class ServerDecoder extends AMQDecoder<ServerMethodProcessor<? extends ServerChannelMethodProcessor>>
{
+ private final FrameBoundaryValidatingServerMethodProcessor _validatingMethodProcessor;
/**
* Creates a new AMQP decoder.
@@ -36,218 +71,296 @@ public class ServerDecoder extends AMQDecoder<ServerMethodProcessor<? extends Se
*/
public ServerDecoder(final ServerMethodProcessor<? extends ServerChannelMethodProcessor> methodProcessor)
{
- super(true, methodProcessor);
+ this(methodProcessor, AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS);
}
- public void decodeBuffer(QpidByteBuffer buf) throws AMQFrameDecodingException, AMQProtocolVersionException, IOException
+ ServerDecoder(final ServerMethodProcessor<? extends ServerChannelMethodProcessor> methodProcessor,
+ final int maxNestedObjects)
+ {
+ super(true, methodProcessor, maxNestedObjects);
+ _validatingMethodProcessor = new FrameBoundaryValidatingServerMethodProcessor(methodProcessor);
+ }
+
+ public void decodeBuffer(final QpidByteBuffer buf)
+ throws AMQFrameDecodingException, AMQProtocolVersionException, IOException
{
decode(buf);
}
@Override
- protected void processMethod(int channelId,
- QpidByteBuffer in)
+ protected void processMethod(final int channelId,
+ final QpidByteBuffer in)
throws AMQFrameDecodingException
{
- ServerMethodProcessor<? extends ServerChannelMethodProcessor> methodProcessor = getMethodProcessor();
- final int classAndMethod = in.getInt();
- int classId = classAndMethod >> 16;
- int methodId = classAndMethod & 0xFFFF;
- methodProcessor.setCurrentMethod(classId, methodId);
+ final ServerMethodProcessor<? extends ServerChannelMethodProcessor> actualMethodProcessor =
+ getMethodProcessor();
+ final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor = _validatingMethodProcessor;
+ _validatingMethodProcessor.begin(in);
try
{
- switch (classAndMethod)
+ if (in.remaining() < Integer.BYTES)
+ {
+ throw new AMQFrameDecodingException("Method frame does not contain a class and method identifier");
+ }
+ final int classAndMethod = in.getInt();
+ final int classId = classAndMethod >> 16;
+ final int methodId = classAndMethod & 0xFFFF;
+ try
{
- //CONNECTION_CLASS:
- case 0x000a000b:
- ConnectionStartOkBody.process(in, methodProcessor);
- break;
- case 0x000a0015:
- ConnectionSecureOkBody.process(in, methodProcessor);
- break;
- case 0x000a001f:
- ConnectionTuneOkBody.process(in, methodProcessor);
- break;
- case 0x000a0028:
- ConnectionOpenBody.process(in, methodProcessor);
- break;
- case 0x000a0032:
- if (methodProcessor.getProtocolVersion().equals(ProtocolVersion.v0_8))
- {
- throw newUnknownMethodException(classId, methodId,
- methodProcessor.getProtocolVersion());
- }
- else
- {
- ConnectionCloseBody.process(in, methodProcessor);
- }
- break;
- case 0x000a0033:
- if (methodProcessor.getProtocolVersion().equals(ProtocolVersion.v0_8))
- {
- throw newUnknownMethodException(classId, methodId,
- methodProcessor.getProtocolVersion());
- }
- else
- {
- methodProcessor.receiveConnectionCloseOk();
- }
- break;
- case 0x000a003c:
- if (methodProcessor.getProtocolVersion().equals(ProtocolVersion.v0_8))
- {
- ConnectionCloseBody.process(in, methodProcessor);
- }
- else
- {
- throw newUnknownMethodException(classId, methodId,
- methodProcessor.getProtocolVersion());
- }
- break;
- case 0x000a003d:
- if (methodProcessor.getProtocolVersion().equals(ProtocolVersion.v0_8))
- {
- methodProcessor.receiveConnectionCloseOk();
- }
- else
- {
- throw newUnknownMethodException(classId, methodId,
- methodProcessor.getProtocolVersion());
- }
- break;
-
- // CHANNEL_CLASS:
-
- case 0x0014000a:
- ChannelOpenBody.process(channelId, in, methodProcessor);
- break;
- case 0x00140014:
- ChannelFlowBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
- break;
- case 0x00140015:
- ChannelFlowOkBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
- break;
- case 0x00140028:
- ChannelCloseBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
- break;
- case 0x00140029:
- methodProcessor.getChannelMethodProcessor(channelId).receiveChannelCloseOk();
- break;
-
- // ACCESS_CLASS:
-
- case 0x001e000a:
- AccessRequestBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
- break;
-
- // EXCHANGE_CLASS:
-
- case 0x0028000a:
- ExchangeDeclareBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
- break;
- case 0x00280014:
- ExchangeDeleteBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
- break;
- case 0x00280016:
- ExchangeBoundBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
- break;
-
-
- // QUEUE_CLASS:
-
- case 0x0032000a:
- QueueDeclareBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
- break;
- case 0x00320014:
- QueueBindBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
- break;
- case 0x0032001e:
- QueuePurgeBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
- break;
- case 0x00320028:
- QueueDeleteBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
- break;
- case 0x00320032:
- QueueUnbindBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
- break;
-
-
- // BASIC_CLASS:
-
- case 0x003c000a:
- BasicQosBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
- break;
- case 0x003c0014:
- BasicConsumeBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
- break;
- case 0x003c001e:
- BasicCancelBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
- break;
- case 0x003c0028:
- BasicPublishBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
- break;
- case 0x003c0046:
- BasicGetBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
- break;
- case 0x003c0050:
- BasicAckBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
- break;
- case 0x003c005a:
- BasicRejectBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
- break;
- case 0x003c0064:
- BasicRecoverBody.process(in, methodProcessor.getProtocolVersion(),
- methodProcessor.getChannelMethodProcessor(channelId));
- break;
- case 0x003c0066:
- BasicRecoverSyncBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
- break;
- case 0x003c006e:
- BasicRecoverSyncBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
- break;
- case 0x003c0078:
- BasicNackBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
- break;
-
- // CONFIRM CLASS:
-
- case 0x0055000a:
- ConfirmSelectBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
- break;
-
- // TX_CLASS:
-
- case 0x005a000a:
- if(!methodProcessor.getChannelMethodProcessor(channelId).ignoreAllButCloseOk())
- {
- methodProcessor.getChannelMethodProcessor(channelId).receiveTxSelect();
- }
- break;
- case 0x005a0014:
- if(!methodProcessor.getChannelMethodProcessor(channelId).ignoreAllButCloseOk())
- {
- methodProcessor.getChannelMethodProcessor(channelId).receiveTxCommit();
- }
- break;
- case 0x005a001e:
- if(!methodProcessor.getChannelMethodProcessor(channelId).ignoreAllButCloseOk())
- {
- methodProcessor.getChannelMethodProcessor(channelId).receiveTxRollback();
- }
- break;
-
-
- default:
- throw newUnknownMethodException(classId, methodId,
- methodProcessor.getProtocolVersion());
+ if (classId == ConnectionStartOkBody.CLASS_ID && channelId != 0)
+ {
+ throw new AMQFrameDecodingException(ErrorCodes.COMMAND_INVALID, "Connection class method " +
+ methodId + " must use channel 0, received channel " + channelId, null);
+ }
+ methodProcessor.setCurrentMethod(classId, methodId);
+ switch (classAndMethod)
+ {
+ //CONNECTION_CLASS:
+ case 0x000a000b:
+ ConnectionStartOkBody.process(in, methodProcessor, getMaxNestedObjects());
+ break;
+ case 0x000a0015:
+ ConnectionSecureOkBody.process(in, methodProcessor);
+ break;
+ case 0x000a001f:
+ ConnectionTuneOkBody.process(in, methodProcessor);
+ break;
+ case 0x000a0028:
+ ConnectionOpenBody.process(in, methodProcessor);
+ break;
+ case 0x000a0032:
+ if (methodProcessor.getProtocolVersion().equals(ProtocolVersion.v0_8))
+ {
+ throw newValidatedUnknownMethodException(classId, methodId,
+ methodProcessor.getProtocolVersion());
+ }
+ else
+ {
+ ConnectionCloseBody.process(in, methodProcessor);
+ }
+ break;
+ case 0x000a0033:
+ if (methodProcessor.getProtocolVersion().equals(ProtocolVersion.v0_8))
+ {
+ throw newValidatedUnknownMethodException(classId, methodId,
+ methodProcessor.getProtocolVersion());
+ }
+ else
+ {
+ methodProcessor.receiveConnectionCloseOk();
+ }
+ break;
+ case 0x000a003c:
+ if (methodProcessor.getProtocolVersion().equals(ProtocolVersion.v0_8))
+ {
+ ConnectionCloseBody.process(in, methodProcessor);
+ }
+ else
+ {
+ throw newValidatedUnknownMethodException(classId, methodId,
+ methodProcessor.getProtocolVersion());
+ }
+ break;
+ case 0x000a003d:
+ if (methodProcessor.getProtocolVersion().equals(ProtocolVersion.v0_8))
+ {
+ methodProcessor.receiveConnectionCloseOk();
+ }
+ else
+ {
+ throw newValidatedUnknownMethodException(classId, methodId,
+ methodProcessor.getProtocolVersion());
+ }
+ break;
+
+ // CHANNEL_CLASS:
+
+ case 0x0014000a:
+ ChannelOpenBody.process(channelId, in, methodProcessor);
+ break;
+ case 0x00140014:
+ ChannelFlowBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
+ break;
+ case 0x00140015:
+ ChannelFlowOkBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
+ break;
+ case 0x00140028:
+ ChannelCloseBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
+ break;
+ case 0x00140029:
+ methodProcessor.getChannelMethodProcessor(channelId).receiveChannelCloseOk();
+ break;
+
+ // ACCESS_CLASS:
+
+ case 0x001e000a:
+ AccessRequestBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
+ break;
+
+ // EXCHANGE_CLASS:
+
+ case 0x0028000a:
+ ExchangeDeclareBody.process(in, methodProcessor.getChannelMethodProcessor(channelId),
+ getMaxNestedObjects());
+ break;
+ case 0x00280014:
+ ExchangeDeleteBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
+ break;
+ case 0x00280016:
+ ExchangeBoundBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
+ break;
+
+
+ // QUEUE_CLASS:
+
+ case 0x0032000a:
+ QueueDeclareBody.process(in, methodProcessor.getChannelMethodProcessor(channelId),
+ getMaxNestedObjects());
+ break;
+ case 0x00320014:
+ QueueBindBody.process(in, methodProcessor.getChannelMethodProcessor(channelId),
+ getMaxNestedObjects());
+ break;
+ case 0x0032001e:
+ QueuePurgeBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
+ break;
+ case 0x00320028:
+ QueueDeleteBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
+ break;
+ case 0x00320032:
+ QueueUnbindBody.process(in, methodProcessor.getChannelMethodProcessor(channelId),
+ getMaxNestedObjects());
+ break;
+
+
+ // BASIC_CLASS:
+
+ case 0x003c000a:
+ BasicQosBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
+ break;
+ case 0x003c0014:
+ BasicConsumeBody.process(in, methodProcessor.getChannelMethodProcessor(channelId),
+ getMaxNestedObjects());
+ break;
+ case 0x003c001e:
+ BasicCancelBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
+ break;
+ case 0x003c0028:
+ BasicPublishBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
+ break;
+ case 0x003c0046:
+ BasicGetBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
+ break;
+ case 0x003c0050:
+ BasicAckBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
+ break;
+ case 0x003c005a:
+ BasicRejectBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
+ break;
+ case 0x003c0064:
+ BasicRecoverBody.process(in, methodProcessor.getProtocolVersion(),
+ methodProcessor.getChannelMethodProcessor(channelId));
+ break;
+ case 0x003c0066:
+ BasicRecoverSyncBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
+ break;
+ case 0x003c006e:
+ BasicRecoverSyncBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
+ break;
+ case 0x003c0078:
+ BasicNackBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
+ break;
+
+ // CONFIRM CLASS:
+
+ case 0x0055000a:
+ ConfirmSelectBody.process(in, methodProcessor.getChannelMethodProcessor(channelId));
+ break;
+
+ // TX_CLASS:
+ case 0x005a000a:
+ if(!methodProcessor.getChannelMethodProcessor(channelId).ignoreAllButCloseOk())
+ {
+ methodProcessor.getChannelMethodProcessor(channelId).receiveTxSelect();
+ }
+ break;
+ case 0x005a0014:
+ if(!methodProcessor.getChannelMethodProcessor(channelId).ignoreAllButCloseOk())
+ {
+ methodProcessor.getChannelMethodProcessor(channelId).receiveTxCommit();
+ }
+ break;
+ case 0x005a001e:
+ if(!methodProcessor.getChannelMethodProcessor(channelId).ignoreAllButCloseOk())
+ {
+ methodProcessor.getChannelMethodProcessor(channelId).receiveTxRollback();
+ }
+ break;
+
+
+ default:
+ throw newValidatedUnknownMethodException(classId, methodId,
+ methodProcessor.getProtocolVersion());
+ }
+
+ if (in.hasRemaining())
+ {
+ throw new AMQFrameDecodingException("Method frame body was not fully consumed: classId=" +
+ classId + ", methodId=" + methodId + ", remaining=" + in.remaining());
+ }
+ }
+ catch (AMQValueNestingException e)
+ {
+ if (isMethodProcessorFailure())
+ {
+ throw new ConnectionScopedRuntimeException(e);
+ }
+ throw AMQFrameDecodingException.forDecodingFailure("Could not decode method " + classId + ':' +
+ methodId, classId, methodId, e);
+ }
+ catch (AMQFrameDecodingException e)
+ {
+ throw new AMQFrameDecodingException(e.getErrorCode(), e.getMessage(), classId, methodId, e);
+ }
+ catch (final ConnectionScopedRuntimeException | ServerScopedRuntimeException e)
+ {
+ throw e;
+ }
+ catch (final RuntimeException e)
+ {
+ if (isMethodProcessorFailure())
+ {
+ throw e;
+ }
+ throw AMQFrameDecodingException.forDecodingFailure("Could not decode method " + classId + ':' +
+ methodId, classId, methodId, e);
}
}
finally
{
- methodProcessor.setCurrentMethod(0, 0);
+ try
+ {
+ actualMethodProcessor.setCurrentMethod(0, 0);
+ }
+ finally
+ {
+ _validatingMethodProcessor.end();
+ }
}
}
+ private boolean isMethodProcessorFailure()
+ {
+ return _validatingMethodProcessor.isMethodBodyDecoded();
+ }
+
+ private AMQFrameDecodingException newValidatedUnknownMethodException(final int classId,
+ final int methodId,
+ final ProtocolVersion protocolVersion)
+ {
+ _validatingMethodProcessor.publishCurrentMethodIfFrameBodyConsumed();
+ return newUnknownMethodException(classId, methodId, protocolVersion);
+ }
}
diff --git a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/BasicConsumeBody.java b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/BasicConsumeBody.java
index a8dfa27b4f..f398a73309 100644
--- a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/BasicConsumeBody.java
+++ b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/BasicConsumeBody.java
@@ -30,6 +30,7 @@ package org.apache.qpid.server.protocol.v0_8.transport;
import org.apache.qpid.server.QpidException;
import org.apache.qpid.server.bytebuffer.QpidByteBuffer;
import org.apache.qpid.server.protocol.v0_8.AMQFrameDecodingException;
+import org.apache.qpid.server.protocol.v0_8.AMQPConnection_0_8;
import org.apache.qpid.server.protocol.v0_8.AMQShortString;
import org.apache.qpid.server.protocol.v0_8.EncodingUtils;
import org.apache.qpid.server.protocol.v0_8.FieldTable;
@@ -192,6 +193,14 @@ public class BasicConsumeBody extends AMQMethodBodyImpl implements EncodableAMQD
final ServerChannelMethodProcessor dispatcher)
throws AMQFrameDecodingException
{
+ process(buffer, dispatcher, AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS);
+ }
+
+ public static void process(final QpidByteBuffer buffer,
+ final ServerChannelMethodProcessor dispatcher,
+ final int maxNestedObjects)
+ throws AMQFrameDecodingException
+ {
int ticket = buffer.getUnsignedShort();
AMQShortString queue = AMQShortString.readAMQShortString(buffer);
@@ -202,20 +211,21 @@ public class BasicConsumeBody extends AMQMethodBodyImpl implements EncodableAMQD
boolean noAck = (bitfield & 0x02) == 0x02;
boolean exclusive = (bitfield & 0x04) == 0x04;
boolean nowait = (bitfield & 0x08) == 0x08;
- FieldTable arguments = EncodingUtils.readFieldTable(buffer);
- if (!dispatcher.ignoreAllButCloseOk())
+ final FieldTable arguments = EncodingUtils.readFieldTable(buffer, maxNestedObjects);
+ try
{
- dispatcher.receiveBasicConsume(queue,
- consumerTag,
- noLocal,
- noAck,
- exclusive,
- nowait,
- FieldTable.convertToDecodedFieldTable(arguments));
+ if (!dispatcher.ignoreAllButCloseOk())
+ {
+ dispatcher.receiveBasicConsume(queue, consumerTag, noLocal, noAck, exclusive, nowait,
+ FieldTable.convertToDecodedFieldTable(arguments));
+ }
}
- if (arguments != null)
+ finally
{
- arguments.dispose();
+ if (arguments != null)
+ {
+ arguments.dispose();
+ }
}
}
}
diff --git a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/BasicContentHeaderProperties.java b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/BasicContentHeaderProperties.java
index d07132f800..514a9e9a5c 100644
--- a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/BasicContentHeaderProperties.java
+++ b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/BasicContentHeaderProperties.java
@@ -28,6 +28,7 @@ import org.slf4j.LoggerFactory;
import org.apache.qpid.server.bytebuffer.QpidByteBuffer;
import org.apache.qpid.server.protocol.v0_8.AMQFrameDecodingException;
+import org.apache.qpid.server.protocol.v0_8.AMQPConnection_0_8;
import org.apache.qpid.server.protocol.v0_8.AMQShortString;
import org.apache.qpid.server.protocol.v0_8.EncodingUtils;
import org.apache.qpid.server.protocol.v0_8.FieldTable;
@@ -75,12 +76,10 @@ public class BasicContentHeaderProperties
private int _propertyFlags = 0;
private static final int CONTENT_TYPE_MASK = 1 << 15;
private static final int ENCODING_MASK = 1 << 14;
- private static final int HEADERS_MASK = 1 << 13;
private static final int DELIVERY_MODE_MASK = 1 << 12;
private static final int PRIORITY_MASK = 1 << 11;
private static final int CORRELATION_ID_MASK = 1 << 10;
private static final int REPLY_TO_MASK = 1 << 9;
- private static final int EXPIRATION_MASK = 1 << 8;
private static final int MESSAGE_ID_MASK = 1 << 7;
private static final int TIMESTAMP_MASK = 1 << 6;
private static final int TYPE_MASK = 1 << 5;
@@ -88,11 +87,15 @@ public class BasicContentHeaderProperties
private static final int APPLICATION_ID_MASK = 1 << 3;
private static final int CLUSTER_ID_MASK = 1 << 2;
+ private final int _maxNestedObjects;
private volatile QpidByteBuffer _encodedForm;
+ static final int HEADERS_MASK = 1 << 13;
+ static final int EXPIRATION_MASK = 1 << 8;
- public BasicContentHeaderProperties(BasicContentHeaderProperties other)
+ public BasicContentHeaderProperties(final BasicContentHeaderProperties other)
{
+ _maxNestedObjects = other._maxNestedObjects;
_headers = FieldTableFactory.createFieldTable(other.getHeadersAsMap());
_contentType = other._contentType;
@@ -125,7 +128,8 @@ public class BasicContentHeaderProperties
}
public BasicContentHeaderProperties()
- {
+ {
+ _maxNestedObjects = AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS;
}
public synchronized int getPropertyListSize()
@@ -346,10 +350,36 @@ public class BasicContentHeaderProperties
}
- public BasicContentHeaderProperties(QpidByteBuffer buffer, int propertyFlags, int size) throws AMQFrameDecodingException
+ public BasicContentHeaderProperties(final QpidByteBuffer buffer,
+ final int propertyFlags,
+ final int size)
+ throws AMQFrameDecodingException
+ {
+ this(buffer, propertyFlags, size, AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS);
+ }
+
+ BasicContentHeaderProperties(final QpidByteBuffer buffer,
+ final int propertyFlags,
+ final int size,
+ final int maxNestedObjects)
+ throws AMQFrameDecodingException
{
+ if (maxNestedObjects < 0)
+ {
+ throw new IllegalArgumentException("Maximum nested objects must not be negative: " + maxNestedObjects);
+ }
+ _maxNestedObjects = maxNestedObjects;
_propertyFlags = propertyFlags;
+ final int supportedPropertyFlags = CONTENT_TYPE_MASK | ENCODING_MASK | HEADERS_MASK | DELIVERY_MODE_MASK |
+ PRIORITY_MASK | CORRELATION_ID_MASK | REPLY_TO_MASK | EXPIRATION_MASK | MESSAGE_ID_MASK |
+ TIMESTAMP_MASK | TYPE_MASK | USER_ID_MASK | APPLICATION_ID_MASK | CLUSTER_ID_MASK;
+ if ((_propertyFlags & ~supportedPropertyFlags) != 0)
+ {
+ throw new AMQFrameDecodingException("Unsupported content header property flags: 0x" +
+ Integer.toHexString(_propertyFlags));
+ }
+
if (LOGGER.isDebugEnabled())
{
LOGGER.debug("Property flags: " + _propertyFlags);
@@ -359,6 +389,16 @@ public class BasicContentHeaderProperties
try (QpidByteBuffer byteBuffer = _encodedForm.slice())
{
decode(byteBuffer);
+ if (byteBuffer.hasRemaining())
+ {
+ throw new AMQFrameDecodingException("Content header property list was not fully consumed: remaining=" +
+ byteBuffer.remaining());
+ }
+ }
+ catch (AMQFrameDecodingException | RuntimeException e)
+ {
+ dispose();
+ throw e;
}
buffer.position(buffer.position()+size);
@@ -378,7 +418,7 @@ public class BasicContentHeaderProperties
if ((_propertyFlags & HEADERS_MASK) != 0)
{
- _headers = EncodingUtils.readFieldTable(buffer);
+ _headers = EncodingUtils.readFieldTable(buffer, _maxNestedObjects);
}
if ((_propertyFlags & DELIVERY_MODE_MASK) != 0)
@@ -516,7 +556,7 @@ public class BasicContentHeaderProperties
public synchronized Map<String, Object> getHeadersAsMap()
{
- return FieldTable.convertToMap(_headers);
+ return FieldTable.convertToMap(_headers, _maxNestedObjects);
}
public synchronized void setHeaders(FieldTable headers)
@@ -880,7 +920,7 @@ public class BasicContentHeaderProperties
if ((_propertyFlags & HEADERS_MASK) != 0)
{
- _headers = EncodingUtils.readFieldTable(byteBuffer);
+ _headers = EncodingUtils.readFieldTable(byteBuffer, _maxNestedObjects);
}
}
}
@@ -889,7 +929,7 @@ public class BasicContentHeaderProperties
{
if (_headers != null)
{
- _headers.validate();
+ _headers.validate(_maxNestedObjects);
}
}
diff --git a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/ChannelAlertBody.java b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/ChannelAlertBody.java
index 0964d0e59e..a8946d39bb 100644
--- a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/ChannelAlertBody.java
+++ b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/ChannelAlertBody.java
@@ -128,13 +128,19 @@ public class ChannelAlertBody extends AMQMethodBodyImpl implements EncodableAMQD
int replyCode = buffer.getUnsignedShort();
AMQShortString replyText = AMQShortString.readAMQShortString(buffer);
FieldTable details = EncodingUtils.readFieldTable(buffer);
- if(!dispatcher.ignoreAllButCloseOk())
+ try
{
- dispatcher.receiveChannelAlert(replyCode, replyText, FieldTable.convertToDecodedFieldTable(details));
+ if (!dispatcher.ignoreAllButCloseOk())
+ {
+ dispatcher.receiveChannelAlert(replyCode, replyText, FieldTable.convertToDecodedFieldTable(details));
+ }
}
- if (details != null)
+ finally
{
- details.dispose();
+ if (details != null)
+ {
+ details.dispose();
+ }
}
}
}
diff --git a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/ConnectionStartBody.java b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/ConnectionStartBody.java
index a5ad7721d0..bfca7b03b7 100644
--- a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/ConnectionStartBody.java
+++ b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/ConnectionStartBody.java
@@ -147,20 +147,23 @@ public class ConnectionStartBody extends AMQMethodBodyImpl implements EncodableA
short versionMajor = in.getUnsignedByte();
short versionMinor = in.getUnsignedByte();
FieldTable serverProperties = EncodingUtils.readFieldTable(in);
- byte[] mechanisms = EncodingUtils.readBytes(in);
- byte[] locales = EncodingUtils.readBytes(in);
-
- if(!dispatcher.ignoreAllButCloseOk())
+ try
{
- dispatcher.receiveConnectionStart(versionMajor,
- versionMinor,
- FieldTable.convertToDecodedFieldTable(serverProperties),
- mechanisms,
- locales);
+ final byte[] mechanisms = EncodingUtils.readBytes(in);
+ final byte[] locales = EncodingUtils.readBytes(in);
+
+ if (!dispatcher.ignoreAllButCloseOk())
+ {
+ dispatcher.receiveConnectionStart(versionMajor, versionMinor,
+ FieldTable.convertToDecodedFieldTable(serverProperties), mechanisms, locales);
+ }
}
- if (serverProperties != null)
+ finally
{
- serverProperties.dispose();
+ if (serverProperties != null)
+ {
+ serverProperties.dispose();
+ }
}
}
}
diff --git a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/ConnectionStartOkBody.java b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/ConnectionStartOkBody.java
index 2a6b7fbae8..f5335b8328 100644
--- a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/ConnectionStartOkBody.java
+++ b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/ConnectionStartOkBody.java
@@ -30,6 +30,7 @@ package org.apache.qpid.server.protocol.v0_8.transport;
import org.apache.qpid.server.QpidException;
import org.apache.qpid.server.bytebuffer.QpidByteBuffer;
import org.apache.qpid.server.protocol.v0_8.AMQFrameDecodingException;
+import org.apache.qpid.server.protocol.v0_8.AMQPConnection_0_8;
import org.apache.qpid.server.protocol.v0_8.AMQShortString;
import org.apache.qpid.server.protocol.v0_8.EncodingUtils;
import org.apache.qpid.server.protocol.v0_8.FieldTable;
@@ -136,21 +137,33 @@ public class ConnectionStartOkBody extends AMQMethodBodyImpl implements Encodabl
public static void process(final QpidByteBuffer in, final ServerMethodProcessor dispatcher)
throws AMQFrameDecodingException
{
+ process(in, dispatcher, AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS);
+ }
+
+ public static void process(final QpidByteBuffer in,
+ final ServerMethodProcessor dispatcher,
+ final int maxNestedObjects)
+ throws AMQFrameDecodingException
+ {
- FieldTable clientProperties = EncodingUtils.readFieldTable(in);
- AMQShortString mechanism = AMQShortString.readAMQShortString(in);
- byte[] response = EncodingUtils.readBytes(in);
- AMQShortString locale = AMQShortString.readAMQShortString(in);
- if (!dispatcher.ignoreAllButCloseOk())
+ final FieldTable clientProperties = EncodingUtils.readFieldTable(in, maxNestedObjects);
+ try
{
- dispatcher.receiveConnectionStartOk(FieldTable.convertToDecodedFieldTable(clientProperties),
- mechanism,
- response,
- locale);
+ final AMQShortString mechanism = AMQShortString.readAMQShortString(in);
+ final byte[] response = EncodingUtils.readBytes(in);
+ final AMQShortString locale = AMQShortString.readAMQShortString(in);
+ if (!dispatcher.ignoreAllButCloseOk())
+ {
+ dispatcher.receiveConnectionStartOk(FieldTable.convertToDecodedFieldTable(clientProperties),
+ mechanism, response, locale);
+ }
}
- if (clientProperties != null)
+ finally
{
- clientProperties.dispose();
+ if (clientProperties != null)
+ {
+ clientProperties.dispose();
+ }
}
}
}
diff --git a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/ContentHeaderBody.java b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/ContentHeaderBody.java
index 6adbce5571..85da68a17a 100644
--- a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/ContentHeaderBody.java
+++ b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/ContentHeaderBody.java
@@ -20,9 +20,14 @@
*/
package org.apache.qpid.server.protocol.v0_8.transport;
+import java.nio.BufferUnderflowException;
+
import org.apache.qpid.server.QpidException;
import org.apache.qpid.server.bytebuffer.QpidByteBuffer;
+import org.apache.qpid.server.protocol.ErrorCodes;
+import org.apache.qpid.server.protocol.ProtocolVersion;
import org.apache.qpid.server.protocol.v0_8.AMQFrameDecodingException;
+import org.apache.qpid.server.protocol.v0_8.AMQPConnection_0_8;
import org.apache.qpid.server.transport.ByteBufferSender;
public class ContentHeaderBody implements AMQBody
@@ -165,29 +170,94 @@ public class ContentHeaderBody implements AMQBody
}
public static void process(final QpidByteBuffer buffer,
- final ChannelMethodProcessor methodProcessor, final long size)
+ final MethodProcessor<? extends ChannelMethodProcessor> methodProcessor,
+ final int channelId,
+ final long size)
throws AMQFrameDecodingException
{
- int classId = buffer.getUnsignedShort();
- buffer.getUnsignedShort();
- long bodySize = buffer.getLong();
- int propertyFlags = buffer.getUnsignedShort();
-
- BasicContentHeaderProperties properties;
+ process(buffer, methodProcessor, channelId, size, AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS);
+ }
+ public static void process(final QpidByteBuffer buffer,
+ final MethodProcessor<? extends ChannelMethodProcessor> methodProcessor,
+ final int channelId,
+ final long size,
+ final int maxNestedObjects)
+ throws AMQFrameDecodingException
+ {
+ final int classId;
+ final int weight;
+ final long bodySize;
+ final int propertyFlags;
+ try
+ {
+ classId = buffer.getUnsignedShort();
+ weight = buffer.getUnsignedShort();
+ bodySize = buffer.getLong();
+ propertyFlags = buffer.getUnsignedShort();
+ }
+ catch (BufferUnderflowException | IllegalArgumentException | IllegalStateException e)
+ {
+ throw AMQFrameDecodingException.forDecodingFailure("Could not decode content header", e);
+ }
if (classId != CLASS_ID)
{
throw new AMQFrameDecodingException("Unsupported content header class id: " + classId, null);
}
- properties = new BasicContentHeaderProperties(buffer, propertyFlags, (int)(size-14));
- if(!methodProcessor.ignoreAllButCloseOk())
+ validateWeight(weight, methodProcessor);
+
+ if (bodySize < 0)
{
- methodProcessor.receiveMessageHeader(properties, bodySize);
+ buffer.position(buffer.limit());
+ methodProcessor.receiveOversizedMessageHeader(channelId, bodySize);
+ return;
}
- else
+
+ final BasicContentHeaderProperties properties;
+ try
+ {
+ properties = new BasicContentHeaderProperties(buffer, propertyFlags, (int) (size - HEADER_SIZE),
+ maxNestedObjects);
+ }
+ catch (BufferUnderflowException | IllegalArgumentException | IllegalStateException e)
+ {
+ throw AMQFrameDecodingException.forDecodingFailure("Could not decode content header properties", e);
+ }
+ boolean propertiesTransferred = false;
+ try
+ {
+ final ChannelMethodProcessor channelMethodProcessor = methodProcessor.getChannelMethodProcessor(channelId);
+
+ if (!channelMethodProcessor.ignoreAllButCloseOk())
+ {
+ propertiesTransferred = true;
+ channelMethodProcessor.receiveMessageHeader(properties, bodySize);
+ }
+ }
+ finally
+ {
+ if (!propertiesTransferred)
+ {
+ properties.dispose();
+ }
+ }
+ }
+
+ private static void validateWeight(final int weight,
+ final MethodProcessor<? extends ChannelMethodProcessor> methodProcessor)
+ throws AMQFrameDecodingException
+ {
+ if (weight != 0)
{
- properties.dispose();
+ // AMQP 0-8 uses weight for structured content; later versions require the field to be zero
+ final ProtocolVersion protocolVersion = methodProcessor.getProtocolVersion();
+ final boolean structuredContent = ProtocolVersion.v0_8.equals(protocolVersion);
+ final int errorCode = structuredContent ? ErrorCodes.NOT_IMPLEMENTED : ErrorCodes.FRAME_ERROR;
+ final String message = structuredContent
+ ? "AMQP 0-8 structured content is not supported (content weight " + weight + ")"
+ : "Content weight must be zero for AMQP " + protocolVersion + " (received " + weight + ")";
+ throw new AMQFrameDecodingException(errorCode, message, null);
}
}
diff --git a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/ExchangeDeclareBody.java b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/ExchangeDeclareBody.java
index 8f3265d2ce..252d72e74b 100644
--- a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/ExchangeDeclareBody.java
+++ b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/ExchangeDeclareBody.java
@@ -30,6 +30,7 @@ package org.apache.qpid.server.protocol.v0_8.transport;
import org.apache.qpid.server.QpidException;
import org.apache.qpid.server.bytebuffer.QpidByteBuffer;
import org.apache.qpid.server.protocol.v0_8.AMQFrameDecodingException;
+import org.apache.qpid.server.protocol.v0_8.AMQPConnection_0_8;
import org.apache.qpid.server.protocol.v0_8.AMQShortString;
import org.apache.qpid.server.protocol.v0_8.EncodingUtils;
import org.apache.qpid.server.protocol.v0_8.FieldTable;
@@ -204,6 +205,14 @@ public class ExchangeDeclareBody extends AMQMethodBodyImpl implements EncodableA
public static void process(final QpidByteBuffer buffer,
final ServerChannelMethodProcessor dispatcher) throws AMQFrameDecodingException
{
+ process(buffer, dispatcher, AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS);
+ }
+
+ public static void process(final QpidByteBuffer buffer,
+ final ServerChannelMethodProcessor dispatcher,
+ final int maxNestedObjects)
+ throws AMQFrameDecodingException
+ {
int ticket = buffer.getUnsignedShort();
AMQShortString exchange = AMQShortString.readAMQShortString(buffer);
@@ -214,21 +223,21 @@ public class ExchangeDeclareBody extends AMQMethodBodyImpl implements EncodableA
boolean autoDelete = (bitfield & 0x4) == 0x4;
boolean internal = (bitfield & 0x8) == 0x8;
boolean nowait = (bitfield & 0x10) == 0x10;
- FieldTable arguments = EncodingUtils.readFieldTable(buffer);
- if(!dispatcher.ignoreAllButCloseOk())
+ final FieldTable arguments = EncodingUtils.readFieldTable(buffer, maxNestedObjects);
+ try
{
- dispatcher.receiveExchangeDeclare(exchange,
- type,
- passive,
- durable,
- autoDelete,
- internal,
- nowait,
- FieldTable.convertToDecodedFieldTable(arguments));
+ if (!dispatcher.ignoreAllButCloseOk())
+ {
+ dispatcher.receiveExchangeDeclare(exchange, type, passive, durable, autoDelete, internal, nowait,
+ FieldTable.convertToDecodedFieldTable(arguments));
+ }
}
- if (arguments != null)
+ finally
{
- arguments.dispose();
+ if (arguments != null)
+ {
+ arguments.dispose();
+ }
}
}
}
diff --git a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/HeartbeatBody.java b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/HeartbeatBody.java
index 1aa1ab2ace..6e338b00b7 100644
--- a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/HeartbeatBody.java
+++ b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/HeartbeatBody.java
@@ -25,6 +25,8 @@ import java.io.IOException;
import org.apache.qpid.server.QpidException;
import org.apache.qpid.server.bytebuffer.QpidByteBuffer;
+import org.apache.qpid.server.protocol.ErrorCodes;
+import org.apache.qpid.server.protocol.ProtocolVersion;
import org.apache.qpid.server.protocol.v0_8.AMQFrameDecodingException;
import org.apache.qpid.server.transport.ByteBufferSender;
@@ -87,14 +89,30 @@ public class HeartbeatBody implements AMQBody
}
public static void process(final int channel,
- final QpidByteBuffer in,
- final MethodProcessor processor,
- final long bodySize)
+ final QpidByteBuffer in,
+ final MethodProcessor<?> processor,
+ final long bodySize)
+ throws AMQFrameDecodingException
{
-
- if(bodySize > 0)
+ if (channel != 0)
+ {
+ // AMQP 0-8 and 0-9 define this as a framing error. AMQP 0-9-1's specific
+ // heartbeat-channel rule requires command-invalid
+ final int errorCode = ProtocolVersion.v0_91.equals(processor.getProtocolVersion())
+ ? ErrorCodes.COMMAND_INVALID
+ : ErrorCodes.FRAME_ERROR;
+ throw new AMQFrameDecodingException(errorCode, "Heartbeat frame must use channel 0, received channel " +
+ channel, null);
+ }
+ if (bodySize > 0)
{
- in.position(in.position()+(int)bodySize);
+ if (ProtocolVersion.v0_91.equals(processor.getProtocolVersion()))
+ {
+ throw new AMQFrameDecodingException("AMQP 0-9-1 heartbeat frame must have an empty payload, " +
+ "received " + bodySize + " bytes");
+ }
+ // AMQP 0-8 and 0-9 did not define an empty-payload requirement
+ in.position(in.position() + (int) bodySize);
}
processor.receiveHeartbeat();
}
diff --git a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/MethodProcessor.java b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/MethodProcessor.java
index e7b71a7ef5..b8bd74ae04 100644
--- a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/MethodProcessor.java
+++ b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/MethodProcessor.java
@@ -21,6 +21,7 @@
package org.apache.qpid.server.protocol.v0_8.transport;
import org.apache.qpid.server.protocol.ProtocolVersion;
+import org.apache.qpid.server.protocol.v0_8.AMQFrameDecodingException;
import org.apache.qpid.server.protocol.v0_8.AMQShortString;
public interface MethodProcessor<T extends ChannelMethodProcessor>
@@ -29,6 +30,13 @@ public interface MethodProcessor<T extends ChannelMethodProcessor>
T getChannelMethodProcessor(int channelId);
+ default void receiveOversizedMessageHeader(final int channelId, final long bodySize)
+ throws AMQFrameDecodingException
+ {
+ throw new AMQFrameDecodingException("Content body size " + Long.toUnsignedString(bodySize) +
+ " exceeds the supported range", null);
+ }
+
void receiveConnectionClose(int replyCode, AMQShortString replyText, int classId, int methodId);
void receiveConnectionCloseOk();
diff --git a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/QueueBindBody.java b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/QueueBindBody.java
index bafcf9cc61..b49abe0584 100644
--- a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/QueueBindBody.java
+++ b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/QueueBindBody.java
@@ -30,6 +30,7 @@ package org.apache.qpid.server.protocol.v0_8.transport;
import org.apache.qpid.server.QpidException;
import org.apache.qpid.server.bytebuffer.QpidByteBuffer;
import org.apache.qpid.server.protocol.v0_8.AMQFrameDecodingException;
+import org.apache.qpid.server.protocol.v0_8.AMQPConnection_0_8;
import org.apache.qpid.server.protocol.v0_8.AMQShortString;
import org.apache.qpid.server.protocol.v0_8.EncodingUtils;
import org.apache.qpid.server.protocol.v0_8.FieldTable;
@@ -164,20 +165,35 @@ public class QueueBindBody extends AMQMethodBodyImpl implements EncodableAMQData
public static void process(final QpidByteBuffer buffer,
final ServerChannelMethodProcessor dispatcher) throws AMQFrameDecodingException
{
+ process(buffer, dispatcher, AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS);
+ }
+
+ public static void process(final QpidByteBuffer buffer,
+ final ServerChannelMethodProcessor dispatcher,
+ final int maxNestedObjects)
+ throws AMQFrameDecodingException
+ {
int ticket = buffer.getUnsignedShort();
AMQShortString queue = AMQShortString.readAMQShortString(buffer);
AMQShortString exchange = AMQShortString.readAMQShortString(buffer);
AMQShortString bindingKey = AMQShortString.readAMQShortString(buffer);
boolean nowait = (buffer.get() & 0x01) == 0x01;
- FieldTable arguments = EncodingUtils.readFieldTable(buffer);
- if(!dispatcher.ignoreAllButCloseOk())
+ final FieldTable arguments = EncodingUtils.readFieldTable(buffer, maxNestedObjects);
+ try
{
- dispatcher.receiveQueueBind(queue, exchange, bindingKey, nowait, FieldTable.convertToDecodedFieldTable(arguments));
+ if (!dispatcher.ignoreAllButCloseOk())
+ {
+ final FieldTable decoded = FieldTable.convertToDecodedFieldTable(arguments);
+ dispatcher.receiveQueueBind(queue, exchange, bindingKey, nowait, decoded);
+ }
}
- if (arguments != null)
+ finally
{
- arguments.dispose();
+ if (arguments != null)
+ {
+ arguments.dispose();
+ }
}
}
}
diff --git a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/QueueDeclareBody.java b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/QueueDeclareBody.java
index 9df4634bf8..1d76469902 100644
--- a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/QueueDeclareBody.java
+++ b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/QueueDeclareBody.java
@@ -30,6 +30,7 @@ package org.apache.qpid.server.protocol.v0_8.transport;
import org.apache.qpid.server.QpidException;
import org.apache.qpid.server.bytebuffer.QpidByteBuffer;
import org.apache.qpid.server.protocol.v0_8.AMQFrameDecodingException;
+import org.apache.qpid.server.protocol.v0_8.AMQPConnection_0_8;
import org.apache.qpid.server.protocol.v0_8.AMQShortString;
import org.apache.qpid.server.protocol.v0_8.EncodingUtils;
import org.apache.qpid.server.protocol.v0_8.FieldTable;
@@ -192,6 +193,14 @@ public class QueueDeclareBody extends AMQMethodBodyImpl implements EncodableAMQD
public static void process(final QpidByteBuffer buffer,
final ServerChannelMethodProcessor dispatcher) throws AMQFrameDecodingException
{
+ process(buffer, dispatcher, AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS);
+ }
+
+ public static void process(final QpidByteBuffer buffer,
+ final ServerChannelMethodProcessor dispatcher,
+ final int maxNestedObjects)
+ throws AMQFrameDecodingException
+ {
int ticket = buffer.getUnsignedShort();
AMQShortString queue = AMQShortString.readAMQShortString(buffer);
@@ -202,14 +211,21 @@ public class QueueDeclareBody extends AMQMethodBodyImpl implements EncodableAMQD
boolean exclusive = (bitfield & 0x04 ) == 0x04;
boolean autoDelete = (bitfield & 0x08 ) == 0x08;
boolean nowait = (bitfield & 0x010 ) == 0x010;
- FieldTable arguments = EncodingUtils.readFieldTable(buffer);
- if(!dispatcher.ignoreAllButCloseOk())
+ final FieldTable arguments = EncodingUtils.readFieldTable(buffer, maxNestedObjects);
+ try
{
- dispatcher.receiveQueueDeclare(queue, passive, durable, exclusive, autoDelete, nowait, FieldTable.convertToDecodedFieldTable(arguments));
+ if (!dispatcher.ignoreAllButCloseOk())
+ {
+ final FieldTable decoded = FieldTable.convertToDecodedFieldTable(arguments);
+ dispatcher.receiveQueueDeclare(queue, passive, durable, exclusive, autoDelete, nowait, decoded);
+ }
}
- if (arguments != null)
+ finally
{
- arguments.dispose();
+ if (arguments != null)
+ {
+ arguments.dispose();
+ }
}
}
}
diff --git a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/QueueUnbindBody.java b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/QueueUnbindBody.java
index 0cba587ab4..59bf07626f 100644
--- a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/QueueUnbindBody.java
+++ b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/QueueUnbindBody.java
@@ -30,6 +30,7 @@ package org.apache.qpid.server.protocol.v0_8.transport;
import org.apache.qpid.server.QpidException;
import org.apache.qpid.server.bytebuffer.QpidByteBuffer;
import org.apache.qpid.server.protocol.v0_8.AMQFrameDecodingException;
+import org.apache.qpid.server.protocol.v0_8.AMQPConnection_0_8;
import org.apache.qpid.server.protocol.v0_8.AMQShortString;
import org.apache.qpid.server.protocol.v0_8.EncodingUtils;
import org.apache.qpid.server.protocol.v0_8.FieldTable;
@@ -147,19 +148,34 @@ public class QueueUnbindBody extends AMQMethodBodyImpl implements EncodableAMQDa
public static void process(final QpidByteBuffer buffer,
final ServerChannelMethodProcessor dispatcher) throws AMQFrameDecodingException
{
+ process(buffer, dispatcher, AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS);
+ }
+
+ public static void process(final QpidByteBuffer buffer,
+ final ServerChannelMethodProcessor dispatcher,
+ final int maxNestedObjects)
+ throws AMQFrameDecodingException
+ {
int ticket = buffer.getUnsignedShort();
AMQShortString queue = AMQShortString.readAMQShortString(buffer);
AMQShortString exchange = AMQShortString.readAMQShortString(buffer);
AMQShortString routingKey = AMQShortString.readAMQShortString(buffer);
- FieldTable arguments = EncodingUtils.readFieldTable(buffer);
- if(!dispatcher.ignoreAllButCloseOk())
+ final FieldTable arguments = EncodingUtils.readFieldTable(buffer, maxNestedObjects);
+ try
{
- dispatcher.receiveQueueUnbind(queue, exchange, routingKey, FieldTable.convertToDecodedFieldTable(arguments));
+ if (!dispatcher.ignoreAllButCloseOk())
+ {
+ final FieldTable decoded = FieldTable.convertToDecodedFieldTable(arguments);
+ dispatcher.receiveQueueUnbind(queue, exchange, routingKey, decoded);
+ }
}
- if (arguments != null)
+ finally
{
- arguments.dispose();
+ if (arguments != null)
+ {
+ arguments.dispose();
+ }
}
}
diff --git a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/ServerChannelMethodProcessor.java b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/ServerChannelMethodProcessor.java
index ac081a27e7..96c712e782 100644
--- a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/ServerChannelMethodProcessor.java
+++ b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/transport/ServerChannelMethodProcessor.java
@@ -25,6 +25,16 @@ import org.apache.qpid.server.protocol.v0_8.FieldTable;
public interface ServerChannelMethodProcessor extends ChannelMethodProcessor
{
+ /**
+ * Rejects the current method frame if a content sequence is awaiting its remaining content frames
+ *
+ * @return true when the method frame was rejected and must not be dispatched, false otherwise
+ */
+ default boolean rejectMethodFrameIfContentIncomplete()
+ {
+ return false;
+ }
+
void receiveAccessRequest(AMQShortString realm,
boolean exclusive,
boolean passive,
diff --git a/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/AMQChannelTest.java b/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/AMQChannelTest.java
index 282ce12a4c..57cbf43ba2 100644
--- a/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/AMQChannelTest.java
+++ b/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/AMQChannelTest.java
@@ -20,6 +20,7 @@
*/
package org.apache.qpid.server.protocol.v0_8;
+import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -32,6 +33,7 @@ import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
@@ -46,6 +48,7 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
+import org.apache.qpid.server.bytebuffer.QpidByteBuffer;
import org.apache.qpid.server.configuration.updater.TaskExecutor;
import org.apache.qpid.server.connection.SessionPrincipal;
import org.apache.qpid.server.logging.EventLogger;
@@ -119,6 +122,7 @@ class AMQChannelTest extends UnitTestBase
when(_amqConnection.getSubject()).thenReturn(authenticatedSubject);
when(_amqConnection.getAuthorizedPrincipal()).thenReturn(authenticatedPrincipal);
when(_amqConnection.getAddressSpace()).thenReturn(_virtualHost);
+ when(_amqConnection.getProtocolVersion()).thenReturn(ProtocolVersion.v0_9);
when(_amqConnection.getProtocolOutputConverter()).thenReturn(protocolOutputConverter);
when(_amqConnection.getBroker()).thenReturn(broker);
when(_amqConnection.getMethodRegistry()).thenReturn(new MethodRegistry(ProtocolVersion.v0_9));
@@ -127,6 +131,8 @@ class AMQChannelTest extends UnitTestBase
when(_amqConnection.getContextValue(Integer.class, Session.PRODUCER_AUTH_CACHE_SIZE)).thenReturn(Session.PRODUCER_AUTH_CACHE_SIZE_DEFAULT);
when(_amqConnection.getContextValue(Long.class, Connection.MAX_UNCOMMITTED_IN_MEMORY_SIZE)).thenReturn(Connection.DEFAULT_MAX_UNCOMMITTED_IN_MEMORY_SIZE);
when(_amqConnection.getContextValue(Boolean.class, AMQPConnection_0_8.FORCE_MESSAGE_VALIDATION)).thenReturn(true);
+ when(_amqConnection.getContextValue(Integer.class, AMQPConnection_0_8.CONNECTION_MAX_CONTENT_BODY_FRAMES_PER_MESSAGE))
+ .thenReturn(AMQPConnection_0_8.DEFAULT_MAX_CONTENT_BODY_FRAMES_PER_MESSAGE);
when(_amqConnection.getTaskExecutor()).thenReturn(taskExecutor);
when(_amqConnection.getChildExecutor()).thenReturn(taskExecutor);
when(_amqConnection.getModel()).thenReturn(BrokerModel.getInstance());
@@ -209,6 +215,192 @@ class AMQChannelTest extends UnitTestBase
}
+ @Test
+ void unsignedOversizedMessageClosesChannel()
+ {
+ when(_virtualHost.getDefaultDestination()).thenReturn(_messageDestination);
+
+ final AMQChannel channel = new AMQChannel(_amqConnection, 1, _messageStore);
+ channel.receiveBasicPublish(AMQShortString.EMPTY_STRING, AMQShortString.EMPTY_STRING, false, false);
+
+ channel.receiveOversizedMessageHeader(Long.MIN_VALUE);
+
+ verify(_amqConnection).closeChannelAndWriteFrame(channel, ErrorCodes.MESSAGE_TOO_LARGE,
+ "Content body size 9223372036854775808 exceeds the supported range");
+ }
+
+ @Test
+ void unsignedOversizedMessageWithoutPublishClosesConnection()
+ {
+ final int channelId = 1;
+ final AMQChannel channel = new AMQChannel(_amqConnection, channelId, _messageStore);
+
+ channel.receiveOversizedMessageHeader(Long.MIN_VALUE);
+
+ verify(_amqConnection).sendConnectionClose(ErrorCodes.COMMAND_INVALID,
+ "Attempt to send a content header without first sending a publish frame", channelId);
+ }
+
+ @Test
+ void unsignedOversizedDuplicateHeaderUsesContentSequenceError()
+ {
+ when(_amqConnection.getProtocolVersion()).thenReturn(ProtocolVersion.v0_91);
+ when(_amqConnection.getMaxMessageSize()).thenReturn(1L);
+ when(_virtualHost.getDefaultDestination()).thenReturn(_messageDestination);
+ final BasicContentHeaderProperties properties = mock(BasicContentHeaderProperties.class);
+ when(properties.checkValid()).thenReturn(true);
+
+ final int channelId = 1;
+ final AMQChannel channel = new AMQChannel(_amqConnection, channelId, _messageStore);
+ channel.receiveBasicPublish(AMQShortString.EMPTY_STRING, ROUTING_KEY, false, false);
+ channel.receiveMessageHeader(properties, 1L);
+
+ channel.receiveOversizedMessageHeader(Long.MIN_VALUE);
+
+ verify(properties).dispose();
+ verify(_amqConnection).sendConnectionClose(ErrorCodes.UNEXPECTED_FRAME,
+ "Attempt to send a duplicate content header", channelId);
+ }
+
+ @Test
+ void contentBodyFrameLimitDisposesIncompleteMessageAndClosesConnection()
+ {
+ final int maximumContentBodyFrames = 2;
+ when(_amqConnection.getContextValue(Integer.class,
+ AMQPConnection_0_8.CONNECTION_MAX_CONTENT_BODY_FRAMES_PER_MESSAGE))
+ .thenReturn(maximumContentBodyFrames);
+ when(_amqConnection.getMaxMessageSize()).thenReturn(3L);
+ when(_virtualHost.getDefaultDestination()).thenReturn(_messageDestination);
+
+ final BasicContentHeaderProperties properties = mock(BasicContentHeaderProperties.class);
+ when(properties.checkValid()).thenReturn(true);
+ final QpidByteBuffer content = mock(QpidByteBuffer.class);
+ final QpidByteBuffer firstRetainedContent = mock(QpidByteBuffer.class);
+ final QpidByteBuffer secondRetainedContent = mock(QpidByteBuffer.class);
+ when(content.remaining()).thenReturn(1);
+ when(content.duplicate()).thenReturn(firstRetainedContent, secondRetainedContent);
+
+ final int channelId = 1;
+ final AMQChannel channel = new AMQChannel(_amqConnection, channelId, _messageStore);
+ channel.receiveBasicPublish(AMQShortString.EMPTY_STRING, ROUTING_KEY, false, false);
+ channel.receiveMessageHeader(properties, 3L);
+
+ channel.receiveMessageContent(content);
+ channel.receiveMessageContent(content);
+ channel.receiveMessageContent(content);
+
+ verify(properties).dispose();
+ verify(firstRetainedContent).dispose();
+ verify(secondRetainedContent).dispose();
+ verify(_amqConnection).sendConnectionClose(ErrorCodes.RESOURCE_ERROR,
+ "Message exceeds maximum number of content body frames (" + maximumContentBodyFrames + ")", channelId);
+ channel.dispose();
+ }
+
+ @Test
+ void excessiveContentBodyDisposesIncompleteMessageAndClosesConnection()
+ {
+ when(_amqConnection.getMaxMessageSize()).thenReturn(2L);
+ when(_virtualHost.getDefaultDestination()).thenReturn(_messageDestination);
+
+ final BasicContentHeaderProperties properties = mock(BasicContentHeaderProperties.class);
+ when(properties.checkValid()).thenReturn(true);
+ final QpidByteBuffer content = mock(QpidByteBuffer.class);
+ final QpidByteBuffer retainedContent = mock(QpidByteBuffer.class);
+ when(content.remaining()).thenReturn(1);
+ when(content.duplicate()).thenReturn(retainedContent);
+ final QpidByteBuffer excessiveContent = mock(QpidByteBuffer.class);
+ when(excessiveContent.remaining()).thenReturn(2);
+
+ final int channelId = 1;
+ final AMQChannel channel = new AMQChannel(_amqConnection, channelId, _messageStore);
+ channel.receiveBasicPublish(AMQShortString.EMPTY_STRING, ROUTING_KEY, false, false);
+ channel.receiveMessageHeader(properties, 2L);
+ channel.receiveMessageContent(content);
+
+ channel.receiveMessageContent(excessiveContent);
+
+ verify(properties).dispose();
+ verify(retainedContent).dispose();
+ verify(excessiveContent, never()).duplicate();
+ verify(_amqConnection).sendConnectionClose(ErrorCodes.FRAME_ERROR,
+ "More message data received than content header defined", channelId);
+ channel.dispose();
+ }
+
+ @Test
+ void contentBodyBeforeHeaderClosesConnection()
+ {
+ when(_virtualHost.getDefaultDestination()).thenReturn(_messageDestination);
+
+ final int channelId = 1;
+ final AMQChannel channel = new AMQChannel(_amqConnection, channelId, _messageStore);
+ channel.receiveBasicPublish(AMQShortString.EMPTY_STRING, ROUTING_KEY, false, false);
+
+ try (final QpidByteBuffer emptyContent = QpidByteBuffer.wrap(new byte[0]))
+ {
+ channel.receiveMessageContent(emptyContent);
+ }
+
+ verify(_amqConnection).sendConnectionClose(ErrorCodes.FRAME_ERROR,
+ "Attempt to send a content body before sending a content header", channelId);
+ channel.dispose();
+ }
+
+ @Test
+ void methodFrameRejectionDisposesIncompleteMessageAndClosesConnection()
+ {
+ when(_amqConnection.getMaxMessageSize()).thenReturn(2L);
+ when(_virtualHost.getDefaultDestination()).thenReturn(_messageDestination);
+
+ final BasicContentHeaderProperties properties = mock(BasicContentHeaderProperties.class);
+ when(properties.checkValid()).thenReturn(true);
+ final QpidByteBuffer content = mock(QpidByteBuffer.class);
+ final QpidByteBuffer retainedContent = mock(QpidByteBuffer.class);
+ when(content.remaining()).thenReturn(1);
+ when(content.duplicate()).thenReturn(retainedContent);
+
+ final int channelId = 1;
+ final AMQChannel channel = new AMQChannel(_amqConnection, channelId, _messageStore);
+ assertFalse(channel.rejectMethodFrameIfContentIncomplete());
+ channel.receiveBasicPublish(AMQShortString.EMPTY_STRING, ROUTING_KEY, false, false);
+ channel.receiveMessageHeader(properties, 2L);
+ channel.receiveMessageContent(content);
+
+ assertTrue(channel.rejectMethodFrameIfContentIncomplete());
+
+ verify(properties).dispose();
+ verify(retainedContent).dispose();
+ verify(_amqConnection).sendConnectionClose(ErrorCodes.FRAME_ERROR,
+ "Method frame received before completing the previous message", channelId);
+ channel.dispose();
+ }
+
+ @Test
+ void duplicateContentHeaderDisposesBothHeadersAndClosesConnection()
+ {
+ when(_amqConnection.getProtocolVersion()).thenReturn(ProtocolVersion.v0_91);
+ when(_amqConnection.getMaxMessageSize()).thenReturn(1L);
+ when(_virtualHost.getDefaultDestination()).thenReturn(_messageDestination);
+
+ final BasicContentHeaderProperties firstProperties = mock(BasicContentHeaderProperties.class);
+ when(firstProperties.checkValid()).thenReturn(true);
+ final BasicContentHeaderProperties secondProperties = mock(BasicContentHeaderProperties.class);
+
+ final int channelId = 1;
+ final AMQChannel channel = new AMQChannel(_amqConnection, channelId, _messageStore);
+ channel.receiveBasicPublish(AMQShortString.EMPTY_STRING, ROUTING_KEY, false, false);
+ channel.receiveMessageHeader(firstProperties, 1L);
+
+ channel.receiveMessageHeader(secondProperties, 1L);
+
+ verify(firstProperties).dispose();
+ verify(secondProperties).dispose();
+ verify(_amqConnection).sendConnectionClose(ErrorCodes.UNEXPECTED_FRAME,
+ "Attempt to send a duplicate content header", channelId);
+ channel.dispose();
+ }
+
@Test
void publishContentHeaderWhenMessageAuthorizationFails()
{
diff --git a/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/AMQPConnection_0_8Test.java b/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/AMQPConnection_0_8Test.java
index a7912a3b72..9f69eaf5b3 100644
--- a/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/AMQPConnection_0_8Test.java
+++ b/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/AMQPConnection_0_8Test.java
@@ -20,28 +20,63 @@
*/
package org.apache.qpid.server.protocol.v0_8;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockConstruction;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
+import java.io.IOException;
import java.net.InetSocketAddress;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
import java.util.List;
import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Supplier;
+import java.util.stream.Stream;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLEngineResult;
+import javax.net.ssl.SSLSession;
import javax.security.auth.Subject;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.junit.jupiter.params.provider.NullAndEmptySource;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.mockito.MockedConstruction;
+import org.mockito.MockedStatic;
+import org.apache.qpid.server.bytebuffer.QpidByteBuffer;
import org.apache.qpid.server.configuration.updater.TaskExecutorImpl;
import org.apache.qpid.server.logging.EventLogger;
+import org.apache.qpid.server.message.MessageDestination;
import org.apache.qpid.server.model.AuthenticationProvider;
import org.apache.qpid.server.model.Broker;
import org.apache.qpid.server.model.BrokerModel;
@@ -56,8 +91,21 @@ import org.apache.qpid.server.model.VirtualHost;
import org.apache.qpid.server.model.VirtualHostNode;
import org.apache.qpid.server.model.port.AmqpPort;
import org.apache.qpid.server.properties.ConnectionStartProperties;
+import org.apache.qpid.server.protocol.ErrorCodes;
import org.apache.qpid.server.protocol.ProtocolVersion;
+import org.apache.qpid.server.protocol.v0_8.transport.AMQDataBlock;
+import org.apache.qpid.server.protocol.v0_8.transport.AMQFrame;
+import org.apache.qpid.server.protocol.v0_8.transport.BasicQosBody;
+import org.apache.qpid.server.protocol.v0_8.transport.ConnectionCloseBody;
+import org.apache.qpid.server.protocol.v0_8.transport.ConnectionCloseOkBody;
+import org.apache.qpid.server.protocol.v0_8.transport.ConnectionStartOkBody;
+import org.apache.qpid.server.protocol.v0_8.transport.ConnectionTuneOkBody;
+import org.apache.qpid.server.protocol.v0_8.transport.ContentBody;
+import org.apache.qpid.server.protocol.v0_8.transport.ContentHeaderBody;
+import org.apache.qpid.server.protocol.v0_8.transport.FrameCreatingMethodProcessor;
+import org.apache.qpid.server.protocol.v0_8.transport.HeartbeatBody;
import org.apache.qpid.server.protocol.v0_8.transport.ProtocolInitiation;
+import org.apache.qpid.server.protocol.v0_8.transport.QueueDeclareBody;
import org.apache.qpid.server.security.SubjectCreator;
import org.apache.qpid.server.security.auth.AuthenticatedPrincipal;
import org.apache.qpid.server.security.auth.AuthenticationResult;
@@ -65,10 +113,17 @@ import org.apache.qpid.server.security.auth.SubjectAuthenticationResult;
import org.apache.qpid.server.security.auth.UsernamePrincipal;
import org.apache.qpid.server.security.auth.sasl.SaslNegotiator;
import org.apache.qpid.server.security.auth.sasl.SaslSettings;
+import org.apache.qpid.server.store.StoreException;
import org.apache.qpid.server.transport.AMQPConnection;
import org.apache.qpid.server.transport.AggregateTicker;
import org.apache.qpid.server.transport.ByteBufferSender;
+import org.apache.qpid.server.transport.NonBlockingConnection;
+import org.apache.qpid.server.transport.NonBlockingConnectionPlainDelegate;
+import org.apache.qpid.server.transport.NonBlockingConnectionTLSDelegate;
import org.apache.qpid.server.transport.ServerNetworkConnection;
+import org.apache.qpid.server.transport.network.security.ssl.SSLUtil;
+import org.apache.qpid.server.util.ConnectionScopedRuntimeException;
+import org.apache.qpid.server.util.ServerScopedRuntimeException;
import org.apache.qpid.server.virtualhost.QueueManagingVirtualHost;
import org.apache.qpid.server.virtualhost.VirtualHostPrincipal;
import org.apache.qpid.test.utils.UnitTestBase;
@@ -80,6 +135,7 @@ class AMQPConnection_0_8Test extends UnitTestBase
private static final byte[] SASL_RESPONSE = "response".getBytes();
private static final AMQShortString LOCALE = AMQShortString.createAMQShortString("en_US");
private static final AMQShortString SASL_MECH = AMQShortString.createAMQShortString("MECH");
+ private static final byte FRAME_END = (byte) 0xCE;
private TaskExecutorImpl _taskExecutor;
private Broker _broker;
@@ -89,6 +145,9 @@ class AMQPConnection_0_8Test extends UnitTestBase
private Transport _transport;
private Protocol _protocol;
private AggregateTicker _ticker;
+ private AuthenticationProvider _authenticationProvider;
+ private SubjectCreator _subjectCreator;
+ private ByteBufferSender _sender;
@BeforeEach
void setUp() throws Exception
@@ -135,31 +194,32 @@ class AMQPConnection_0_8Test extends UnitTestBase
when(_virtualHost.authoriseCreateConnection(any(AMQPConnection.class))).thenReturn(true);
when(_virtualHost.getEventLogger()).thenReturn(value);
- final SubjectCreator subjectCreator = mock(SubjectCreator.class);
+ _subjectCreator = mock(SubjectCreator.class);
- final SaslNegotiator saslNegotiator = mock(SaslNegotiator.class);
- when(subjectCreator.createSaslNegotiator(eq(SASL_MECH.toString()), any(SaslSettings.class))).thenReturn(saslNegotiator);
- when(subjectCreator.authenticate(saslNegotiator, SASL_RESPONSE)).thenReturn(new SubjectAuthenticationResult(
+ final SaslNegotiator _saslNegotiator = mock(SaslNegotiator.class);
+ when(_subjectCreator.createSaslNegotiator(eq(SASL_MECH.toString()), any(SaslSettings.class)))
+ .thenReturn(_saslNegotiator);
+ when(_subjectCreator.authenticate(_saslNegotiator, SASL_RESPONSE)).thenReturn(new SubjectAuthenticationResult(
new AuthenticationResult(new AuthenticatedPrincipal(new UsernamePrincipal("username", null))), new Subject()));
- final AuthenticationProvider authenticationProvider = mock(AuthenticationProvider.class);
- when(authenticationProvider.getAvailableMechanisms(anyBoolean())).thenReturn(List.of(SASL_MECH.toString()));
+ _authenticationProvider = mock(AuthenticationProvider.class);
+ when(_authenticationProvider.getAvailableMechanisms(anyBoolean())).thenReturn(List.of(SASL_MECH.toString()));
_port = mock(AmqpPort.class);
when(_port.getParent()).thenReturn(_broker);
when(_port.getCategoryClass()).thenReturn(Port.class);
when(_port.getChildExecutor()).thenReturn(_taskExecutor);
when(_port.getModel()).thenReturn(model);
- when(_port.getAuthenticationProvider()).thenReturn(authenticationProvider);
+ when(_port.getAuthenticationProvider()).thenReturn(_authenticationProvider);
when(_port.getAddressSpace(VIRTUAL_HOST_NAME)).thenReturn(_virtualHost);
when(_port.getContextValue(Long.class, Port.CONNECTION_MAXIMUM_AUTHENTICATION_DELAY)).thenReturn(2500L);
when(_port.getContextValue(Integer.class, Connection.MAX_MESSAGE_SIZE)).thenReturn(Connection.DEFAULT_MAX_MESSAGE_SIZE);
- when(_port.getSubjectCreator(eq(false), anyString())).thenReturn(subjectCreator);
+ when(_port.getSubjectCreator(eq(false), anyString())).thenReturn(_subjectCreator);
- final ByteBufferSender sender = mock(ByteBufferSender.class);
+ _sender = mock(ByteBufferSender.class);
_network = mock(ServerNetworkConnection.class);
- when(_network.getSender()).thenReturn(sender);
+ when(_network.getSender()).thenReturn(_sender);
when(_network.getLocalAddress()).thenReturn(new InetSocketAddress("localhost", 12345));
when(_network.getSelectedHost()).thenReturn("localhost");
@@ -174,12 +234,64 @@ class AMQPConnection_0_8Test extends UnitTestBase
_taskExecutor.stopImmediately();
}
+ @ParameterizedTest
+ @ValueSource(strings = {"UNKNOWN", "DISABLED", "SECURE_ONLY"})
+ void testUnadvertisedSaslMechanismRejected(final String mechanismName)
+ {
+ final AMQPConnection_0_8Impl connection = createConnectionAwaitingStartOk();
+ clearInvocations(_network, _sender);
+
+ connection.receiveConnectionStartOk(FieldTableFactory.createFieldTable(Map.of()),
+ AMQShortString.createAMQShortString(mechanismName), SASL_RESPONSE, LOCALE);
+
+ verify(_network).close();
+ verifyNoInteractions(_sender);
+ verify(_subjectCreator, never()).createSaslNegotiator(eq(mechanismName), any(SaslSettings.class));
+ }
+
+ @ParameterizedTest
+ @NullAndEmptySource
+ void testMissingSaslMechanismClosesNetworkWithoutResponse(final String mechanismName)
+ {
+ final AMQPConnection_0_8Impl connection = createConnectionAwaitingStartOk();
+ clearInvocations(_network, _sender);
+ final AMQShortString mechanism = mechanismName == null
+ ? null
+ : AMQShortString.createAMQShortString(mechanismName);
+
+ connection.receiveConnectionStartOk(FieldTableFactory.createFieldTable(Map.of()), mechanism,
+ SASL_RESPONSE, LOCALE);
+
+ verify(_network).close();
+ verifyNoInteractions(_sender);
+ verify(_subjectCreator, never()).createSaslNegotiator(anyString(), any(SaslSettings.class));
+ }
+
+ @Test
+ void testMechanismAddedAfterAdvertisementRejected()
+ {
+ final String mechanismName = "NEWLY_ENABLED";
+ final List<String> availableMechanisms = new ArrayList<>(List.of(SASL_MECH.toString()));
+ when(_authenticationProvider.getAvailableMechanisms(false)).thenReturn(availableMechanisms);
+
+ final AMQPConnection_0_8Impl connection = createConnectionAwaitingStartOk();
+ availableMechanisms.add(mechanismName);
+ clearInvocations(_network, _sender);
+
+ connection.receiveConnectionStartOk(FieldTableFactory.createFieldTable(Map.of()),
+ AMQShortString.createAMQShortString(mechanismName), SASL_RESPONSE, LOCALE);
+
+ verify(_network).close();
+ verifyNoInteractions(_sender);
+ verify(_subjectCreator, never()).createSaslNegotiator(eq(mechanismName), any(SaslSettings.class));
+ }
+
@Test
void closeOnNoRoute()
{
{
- final AMQPConnection_0_8Impl
- conn = new AMQPConnection_0_8Impl(_broker, _network, _port, _transport, _protocol, 0, _ticker);
+ final AMQPConnection_0_8Impl conn =
+ new AMQPConnection_0_8Impl(_broker, _network, _port, _transport, _protocol, 0, _ticker);
conn.create();
conn.receiveProtocolHeader(new ProtocolInitiation(ProtocolVersion.v0_8));
@@ -206,8 +318,8 @@ class AMQPConnection_0_8Test extends UnitTestBase
@Test
void connectionEnforcesMaxSessions()
{
- final AMQPConnection_0_8Impl
- conn = new AMQPConnection_0_8Impl(_broker, _network, _port, _transport, _protocol, 0, _ticker);
+ final AMQPConnection_0_8Impl conn = new AMQPConnection_0_8Impl(_broker, _network, _port, _transport,
+ _protocol, 0, _ticker);
conn.create();
conn.receiveProtocolHeader(new ProtocolInitiation(ProtocolVersion.v0_8));
@@ -235,11 +347,560 @@ class AMQPConnection_0_8Test extends UnitTestBase
assertTrue(conn.isClosing(), "Connection should be closed after opening " + (maxChannels + 1) + " channels");
}
+ @Test
+ void nestingViolationBeforeOpenClosesNetworkWithoutResponse()
+ {
+ final RecordingAMQPConnection connection = new RecordingAMQPConnection(_broker, _network, _port, _transport,
+ _protocol, _ticker);
+ connection.create();
+ connection.receiveProtocolHeader(new ProtocolInitiation(ProtocolVersion.v0_8));
+ clearInvocations(_network, _sender);
+
+ try (final QpidByteBuffer buffer = createConnectionStartOkFrame(
+ buildNestedTable(AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS + 1),
+ SASL_MECH.toString(), SASL_RESPONSE, LOCALE.toString()))
+ {
+ assertDoesNotThrow(() -> connection.received(buffer));
+ }
+
+ assertEquals(0, connection.getCloseCount());
+ verify(_network).close();
+ verifyNoInteractions(_sender);
+ }
+
+ @Test
+ void nestingViolationAfterOpenProducesResourceError()
+ {
+ final RecordingAMQPConnection connection = createOpenRecordingConnection(ProtocolVersion.v0_8,
+ Protocol.AMQP_0_8);
+ clearInvocations(_network, _sender);
+
+ try (final QpidByteBuffer frame = createQueueDeclareFrame(
+ buildNestedTable(AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS + 1)))
+ {
+ assertDoesNotThrow(() -> connection.received(frame));
+ }
+
+ assertEquals(ErrorCodes.RESOURCE_ERROR, connection.getCloseErrorCode());
+ assertEquals(0, connection.getCloseChannelId());
+ assertEquals(QueueDeclareBody.CLASS_ID, connection.getCloseClassId());
+ assertEquals(QueueDeclareBody.METHOD_ID, connection.getCloseMethodId());
+ assertEquals(1, connection.getCloseCount());
+ verify(_network, never()).close();
+ verifyNoInteractions(_sender);
+ }
+
+ @Test
+ void codedDecodingErrorAfterOpenProducesConnectionClose()
+ {
+ final RecordingAMQPConnection connection = createOpenRecordingConnection(ProtocolVersion.v0_8,
+ Protocol.AMQP_0_8);
+ clearInvocations(_network, _sender);
+
+ try (final QpidByteBuffer frame = createMethodFrame(1, ConnectionTuneOkBody.CLASS_ID,
+ ConnectionTuneOkBody.METHOD_ID))
+ {
+ assertDoesNotThrow(() -> connection.received(frame));
+ }
+
+ assertEquals(ErrorCodes.COMMAND_INVALID, connection.getCloseErrorCode());
+ assertEquals(ConnectionTuneOkBody.CLASS_ID, connection.getCloseClassId());
+ assertEquals(ConnectionTuneOkBody.METHOD_ID, connection.getCloseMethodId());
+ assertEquals(1, connection.getCloseCount());
+ verify(_network, never()).close();
+ verifyNoInteractions(_sender);
+ }
+
+ @Test
+ void truncatedMethodAfterOpenProducesFrameErrorWithMethodIdentifiers()
+ {
+ final RecordingAMQPConnection connection = createOpenRecordingConnection(ProtocolVersion.v0_91,
+ Protocol.AMQP_0_9_1);
+ clearInvocations(_network, _sender);
+
+ try (final QpidByteBuffer frame = createMethodFrame(1, BasicQosBody.CLASS_ID, BasicQosBody.METHOD_ID))
+ {
+ assertDoesNotThrow(() -> connection.received(frame));
+ }
+
+ assertEquals(ErrorCodes.FRAME_ERROR, connection.getCloseErrorCode());
+ assertEquals(BasicQosBody.CLASS_ID, connection.getCloseClassId());
+ assertEquals(BasicQosBody.METHOD_ID, connection.getCloseMethodId());
+ assertEquals(1, connection.getCloseCount());
+ verify(_network, never()).close();
+ verifyNoInteractions(_sender);
+ }
+
+ @ParameterizedTest
+ @CsvSource({"false, false", "false, true", "true, false", "true, true"})
+ void testDecodingFailureWithoutProgressClosesTransport(final boolean direct, final boolean initialProgress)
+ throws Exception
+ {
+ final AtomicInteger decodeCount = new AtomicInteger();
+ try (final MockedConstruction<BrokerDecoder> construction = mockConstruction(BrokerDecoder.class,
+ (decoder, context) -> doAnswer(invocation ->
+ {
+ final QpidByteBuffer input = invocation.getArgument(0);
+ if (decodeCount.getAndIncrement() == 0 && initialProgress)
+ {
+ input.position(input.position() + Integer.BYTES);
+ }
+ throw new AMQFrameDecodingException("Frame header validation failed");
+ }).when(decoder).decodeBuffer(any(QpidByteBuffer.class))))
+ {
+ final AMQPConnection_0_8Impl connection = new AMQPConnection_0_8Impl(_broker, _network, _port, _transport,
+ _protocol, 0, _ticker);
+ openConnection(connection, ProtocolVersion.v0_8);
+ clearInvocations(_network, _sender);
+
+ try (final QpidByteBuffer input = QpidByteBuffer.allocate(direct, 16))
+ {
+ connection.received(input);
+
+ assertFalse(input.hasRemaining(), "Failed input must be discarded before shutdown");
+ verify(_network).close();
+
+ input.clear();
+ connection.received(input);
+
+ assertFalse(input.hasRemaining(), "Input arriving during transport shutdown must be discarded");
+ verify(construction.constructed().get(0), times(initialProgress ? 2 : 1))
+ .decodeBuffer(any(QpidByteBuffer.class));
+ }
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("terminalDecodingFailures")
+ void testTerminalDecodingExceptionDiscardsPendingInput(final Exception failure, final boolean direct)
+ throws Exception
+ {
+ try (final MockedConstruction<BrokerDecoder> construction = mockConstruction(BrokerDecoder.class,
+ (decoder, context) -> doThrow(failure).when(decoder).decodeBuffer(any(QpidByteBuffer.class))))
+ {
+ final AMQPConnection_0_8Impl connection = new AMQPConnection_0_8Impl(_broker, _network, _port, _transport,
+ _protocol, 0, _ticker);
+ openConnection(connection, ProtocolVersion.v0_8);
+ try (final QpidByteBuffer input = QpidByteBuffer.allocate(direct, 16))
+ {
+ final ConnectionScopedRuntimeException exception = assertThrows(ConnectionScopedRuntimeException.class,
+ () -> connection.received(input));
+ assertSame(failure, exception.getCause());
+ assertFalse(input.hasRemaining());
+
+ input.clear();
+ connection.received(input);
+ assertFalse(input.hasRemaining());
+ verify(construction.constructed().get(0)).decodeBuffer(any(QpidByteBuffer.class));
+ }
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
+ void testCloseHandshakeDiscardsPendingInput(final boolean direct)
+ {
+ final AMQPConnection_0_8Impl connection = new AMQPConnection_0_8Impl(_broker, _network, _port, _transport,
+ _protocol, 0, _ticker);
+ openConnection(connection, ProtocolVersion.v0_8);
+ connection.sendConnectionClose(ErrorCodes.FRAME_ERROR, "Closing connection", 0);
+ clearInvocations(_network);
+
+ try (final QpidByteBuffer reply = createFrame(connection.getMethodRegistry().createConnectionCloseOkBody()
+ .generateFrame(0));
+ final QpidByteBuffer input = QpidByteBuffer.allocate(direct, reply.remaining() + 3))
+ {
+ input.put(reply);
+ input.put(new byte[3]);
+ input.flip();
+ connection.received(input);
+ assertFalse(input.hasRemaining());
+ }
+ verify(_network).close();
+ }
+
+ @ParameterizedTest
+ @MethodSource("rejectedFrameCloseReadVariants")
+ void testRejectedFrameCleanupAllowsOrderlyClose(final ProtocolVersion protocolVersion,
+ final Protocol protocol,
+ final int split,
+ final boolean peerCloses,
+ final boolean oversized) throws Exception
+ {
+ final AMQPConnection_0_8Impl connection = new AMQPConnection_0_8Impl(_broker, _network, _port, _transport,
+ protocol, 0, _ticker);
+ openConnection(connection, protocolVersion);
+ connection.setMaxFrameSize(AMQDecoder.FRAME_MIN_SIZE);
+ clearInvocations(_network, _sender);
+ final ByteBuffer output = ByteBuffer.allocate(1024);
+ doAnswer(invocation ->
+ {
+ final QpidByteBuffer buffer = invocation.getArgument(0);
+ buffer.copyTo(output);
+ return null;
+ }).when(_sender).send(any(QpidByteBuffer.class));
+
+ final AMQFrame response = peerCloses
+ ? new ConnectionCloseBody(protocolVersion, ErrorCodes.REPLY_SUCCESS, AMQShortString.EMPTY_STRING,
+ 0, 0).generateFrame(0)
+ : connection.getMethodRegistry().createConnectionCloseOkBody().generateFrame(0);
+ try (final QpidByteBuffer rejected = oversized ?
+ createFrame(ContentBody.TYPE, 1, new byte[AMQDecoder.FRAME_MIN_SIZE + 1], FRAME_END) :
+ createMethodFrame(1, BasicQosBody.CLASS_ID, BasicQosBody.METHOD_ID);
+ final QpidByteBuffer reply = createFrame(response);
+ final QpidByteBuffer buffer = QpidByteBuffer.allocate(rejected.remaining() + reply.remaining()))
+ {
+ final int rejectedFrameSize = rejected.remaining();
+ final byte[] input = new byte[buffer.capacity()];
+ rejected.get(input, 0, rejectedFrameSize);
+ reply.get(input, rejectedFrameSize, reply.remaining());
+ final int firstReadLength = split < 0 ? input.length : split;
+ buffer.put(input, 0, firstReadLength);
+ buffer.flip();
+ connection.received(buffer);
+
+ final int rejectionThreshold = oversized ? AMQDecoder.FRAME_HEADER_SIZE : rejectedFrameSize;
+ if (firstReadLength < rejectionThreshold)
+ {
+ assertEquals(0, buffer.position());
+ assertFalse(connection.isClosing());
+ }
+ else
+ {
+ assertTrue(connection.isClosing());
+ }
+
+ if (oversized && firstReadLength >= rejectionThreshold && firstReadLength <= rejectedFrameSize)
+ {
+ assertFalse(buffer.hasRemaining(), "Rejected payload must not be retained for the next read");
+ }
+
+ if (firstReadLength < input.length)
+ {
+ verify(_network, never()).close();
+ buffer.compact();
+ buffer.put(input, firstReadLength, input.length - firstReadLength);
+ buffer.flip();
+ connection.received(buffer);
+ }
+
+ assertFalse(buffer.hasRemaining());
+ assertTrue(connection.isClosing());
+ verify(_network).close();
+ }
+
+ output.flip();
+ final FrameCreatingMethodProcessor methodProcessor = new FrameCreatingMethodProcessor(protocolVersion);
+ new ClientDecoder(methodProcessor).decodeBuffer(output);
+ final List<AMQDataBlock> frames = methodProcessor.getProcessedMethods();
+ assertEquals(peerCloses ? 2 : 1, frames.size());
+ final ConnectionCloseBody close = assertInstanceOf(ConnectionCloseBody.class,
+ ((AMQFrame) frames.get(0)).getBodyFrame());
+ assertEquals(ErrorCodes.FRAME_ERROR, close.getReplyCode());
+ assertEquals(oversized ? 0 : BasicQosBody.CLASS_ID, close.getClassId());
+ assertEquals(oversized ? 0 : BasicQosBody.METHOD_ID, close.getMethodId());
+ if (peerCloses)
+ {
+ assertInstanceOf(ConnectionCloseOkBody.class, ((AMQFrame) frames.get(1)).getBodyFrame());
+ }
+ }
+
+ @Test
+ void testIncompleteDiscardRetainsCloseTimeout()
+ {
+ final AMQPConnection_0_8Impl connection = new AMQPConnection_0_8Impl(_broker, _network, _port, _transport,
+ _protocol, 0, _ticker);
+ openConnection(connection, ProtocolVersion.v0_8);
+ connection.setMaxFrameSize(AMQDecoder.FRAME_MIN_SIZE);
+ clearInvocations(_network);
+
+ try (final QpidByteBuffer frame = createFrame(ContentBody.TYPE, 1, new byte[AMQDecoder.FRAME_MIN_SIZE + 1],
+ FRAME_END))
+ {
+ frame.limit(AMQDecoder.FRAME_HEADER_SIZE);
+ connection.received(frame);
+ assertTrue(connection.isClosing());
+ assertFalse(frame.hasRemaining());
+ verify(_network, never()).close();
+
+ final long closeTimeout = connection.getContextValue(Long.class, Connection.CLOSE_RESPONSE_TIMEOUT);
+ _ticker.tick(System.currentTimeMillis() + closeTimeout + 1);
+ verify(_network).close();
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
+ void testRejectedFrameKeepsTransportBuffersBounded(final boolean tls) throws Exception
+ {
+ final int networkBufferSize = 128;
+ QpidByteBuffer.deinitialisePool();
+ QpidByteBuffer.initialisePool(networkBufferSize, 16, 1.0);
+ try (final MockedStatic<SSLUtil> sslUtil = mockStatic(SSLUtil.class))
+ {
+ sslUtil.when(() -> SSLUtil.isSufficientToDetermineClientSNIHost(any(QpidByteBuffer.class)))
+ .thenReturn(true);
+ final AMQPConnection_0_8Impl connection =
+ new AMQPConnection_0_8Impl(_broker, _network, _port, _transport, _protocol, 0, _ticker);
+ openConnection(connection, ProtocolVersion.v0_8);
+ connection.setMaxFrameSize(AMQDecoder.FRAME_MIN_SIZE);
+ clearInvocations(_network, _sender);
+ final NonBlockingConnection network = mock(NonBlockingConnection.class);
+ when(_port.getNetworkBufferSize()).thenReturn(networkBufferSize);
+ doAnswer(invocation ->
+ {
+ final QpidByteBuffer input = invocation.getArgument(0);
+ assertTrue(input.capacity() <= networkBufferSize, "Application buffer grew during shutdown");
+ connection.received(input);
+ assertFalse(input.hasRemaining(), "Shutdown retained rejected input");
+ return null;
+ }).when(network).processAmqpData(any(QpidByteBuffer.class));
+
+ final Supplier<QpidByteBuffer> inputBuffer;
+ final Callable<Boolean> processInput;
+ final Runnable close;
+ if (tls)
+ {
+ final NonBlockingConnectionTLSDelegate delegate = createTlsDelegate(network, networkBufferSize);
+ inputBuffer = delegate::getNetInputBuffer;
+ processInput = delegate::processData;
+ close = () ->
+ {
+ delegate.shutdownInput();
+ delegate.shutdownOutput();
+ };
+ }
+ else
+ {
+ final NonBlockingConnectionPlainDelegate delegate =
+ new NonBlockingConnectionPlainDelegate(network, _port);
+ inputBuffer = delegate::getNetInputBuffer;
+ processInput = delegate::processData;
+ close = () ->
+ {
+ delegate.shutdownInput();
+ delegate.shutdownOutput();
+ };
+ }
+ try
+ {
+ try (final QpidByteBuffer rejected = createFrame(ContentBody.TYPE, 1,
+ new byte[AMQDecoder.FRAME_MIN_SIZE + 1], FRAME_END);
+ final QpidByteBuffer reply = createFrame(connection.getMethodRegistry()
+ .createConnectionCloseOkBody().generateFrame(0)))
+ {
+ final byte[] input = new byte[rejected.remaining() + reply.remaining()];
+ final int rejectedSize = rejected.remaining();
+ rejected.get(input, 0, rejectedSize);
+ reply.get(input, rejectedSize, reply.remaining());
+ for (int offset = 0; offset < input.length;)
+ {
+ final int length = Math.min(networkBufferSize, input.length - offset);
+ inputBuffer.get().put(input, offset, length);
+ processInput.call();
+ assertTrue(inputBuffer.get().capacity() <= networkBufferSize);
+ offset += length;
+ }
+ }
+
+ verify(_network).close();
+ inputBuffer.get().put(new byte[inputBuffer.get().remaining()]);
+ processInput.call();
+ assertEquals(networkBufferSize, inputBuffer.get().capacity());
+ }
+ finally
+ {
+ close.run();
+ }
+ assertEquals(0, QpidByteBuffer.getAllocatedDirectMemorySize(), "Shutdown retained pooled direct memory");
+ }
+ finally
+ {
+ QpidByteBuffer.deinitialisePool();
+ }
+ }
+
+ private NonBlockingConnectionTLSDelegate createTlsDelegate(final NonBlockingConnection network,
+ final int networkBufferSize) throws Exception
+ {
+ final SSLContext sslContext = mock(SSLContext.class);
+ final SSLEngine sslEngine = mock(SSLEngine.class);
+ final SSLSession session = mock(SSLSession.class);
+ when(_port.getSSLContext()).thenReturn(sslContext);
+ when(_port.getContextValue(Boolean.class, AmqpPort.PORT_DIAGNOSIS_OF_SSL_ENGINE_LOOPING)).thenReturn(false);
+ when(_port.getContextValue(Integer.class, AmqpPort.PORT_DIAGNOSIS_OF_SSL_ENGINE_LOOPING_WARN_THRESHOLD))
+ .thenReturn(0);
+ when(_port.getContextValue(Integer.class, AmqpPort.PORT_DIAGNOSIS_OF_SSL_ENGINE_LOOPING_BREAK_THRESHOLD))
+ .thenReturn(0);
+ when(sslContext.createSSLEngine()).thenReturn(sslEngine);
+ when(sslEngine.getSession()).thenReturn(session);
+ when(session.getPacketBufferSize()).thenReturn(networkBufferSize);
+ when(session.getApplicationBufferSize()).thenReturn(networkBufferSize);
+
+ when(sslEngine.unwrap(any(ByteBuffer.class), any(ByteBuffer.class))).thenAnswer(invocation ->
+ {
+ final ByteBuffer source = invocation.getArgument(0);
+ final ByteBuffer target = invocation.getArgument(1);
+ final int length = source.remaining();
+ target.put(source);
+ return new SSLEngineResult(SSLEngineResult.Status.OK, SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING,
+ length, length);
+ });
+ return new NonBlockingConnectionTLSDelegate(network, _port);
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
+ void testFatalDecodingFailureDiscardsPendingInput(final boolean direct)
+ {
+ final AMQPConnection_0_8Impl connection = new AMQPConnection_0_8Impl(_broker, _network, _port, _transport,
+ _protocol, 0, _ticker);
+ openConnection(connection, ProtocolVersion.v0_8);
+ clearInvocations(_network, _sender);
+
+ try (final QpidByteBuffer frame = createFrame(HeartbeatBody.TYPE, 0, new byte[0], (byte) 0);
+ final QpidByteBuffer input = QpidByteBuffer.allocate(direct, frame.remaining() + 3))
+ {
+ input.put(frame);
+ input.put(new byte[3]);
+ input.flip();
+ connection.received(input);
+
+ assertFalse(input.hasRemaining());
+ input.clear();
+ connection.received(input);
+ assertFalse(input.hasRemaining());
+ }
+ verify(_network).close();
+ verifyNoInteractions(_sender);
+ }
+
+ @Test
+ void malformedHeartbeatAfterOpenProducesConnectionClose()
+ {
+ final RecordingAMQPConnection connection = createOpenRecordingConnection(ProtocolVersion.v0_91,
+ Protocol.AMQP_0_9_1);
+ clearInvocations(_network, _sender);
+
+ try (final QpidByteBuffer frame = createFrame(HeartbeatBody.TYPE, 0, new byte[] { 1 }, FRAME_END))
+ {
+ assertDoesNotThrow(() -> connection.received(frame));
+ }
+
+ assertEquals(ErrorCodes.FRAME_ERROR, connection.getCloseErrorCode());
+ assertEquals(1, connection.getCloseCount());
+ verify(_network, never()).close();
+ verifyNoInteractions(_sender);
+ }
+
+ @Test
+ void unsignedContentBodySizeAfterOpenProducesChannelClose()
+ {
+ when(_virtualHost.getDefaultDestination()).thenReturn(mock(MessageDestination.class));
+ final RecordingAMQPConnection connection = createOpenRecordingConnection(ProtocolVersion.v0_91,
+ Protocol.AMQP_0_9_1);
+ connection.receiveChannelOpen(1);
+ final AMQChannel channel = connection.getChannel(1);
+ channel.receiveBasicPublish(AMQShortString.EMPTY_STRING, AMQShortString.EMPTY_STRING, false, false);
+ clearInvocations(_network, _sender);
+
+ try (final QpidByteBuffer frame = createContentHeaderFrame(1, Long.MIN_VALUE, new byte[] { (byte) 0xFF }))
+ {
+ assertDoesNotThrow(() -> connection.received(frame));
+ }
+
+ assertEquals(0, connection.getCloseCount());
+ assertEquals(1, connection.getChannelCloseCount());
+ assertSame(channel, connection.getClosedChannel());
+ assertEquals(ErrorCodes.MESSAGE_TOO_LARGE, connection.getChannelCloseErrorCode());
+ assertEquals("Content body size 9223372036854775808 exceeds the supported range",
+ connection.getChannelCloseMessage());
+ verify(_network, never()).close();
+ }
+
+ @Test
+ void unsignedContentBodySizeOnUnknownChannelProducesChannelError()
+ {
+ final RecordingAMQPConnection connection = createOpenRecordingConnection(ProtocolVersion.v0_91,
+ Protocol.AMQP_0_9_1);
+ clearInvocations(_network, _sender);
+
+ try (final QpidByteBuffer frame = createContentHeaderFrame(1, Long.MIN_VALUE, new byte[0]))
+ {
+ assertDoesNotThrow(() -> connection.received(frame));
+ }
+
+ assertEquals(ErrorCodes.CHANNEL_ERROR, connection.getCloseErrorCode());
+ assertEquals(1, connection.getCloseChannelId());
+ assertEquals(1, connection.getCloseCount());
+ assertEquals(0, connection.getChannelCloseCount());
+ verify(_network, never()).close();
+ verifyNoInteractions(_sender);
+ }
+
+ @Test
+ void unsignedContentBodySizeBeforeOpenClosesNetworkWithoutResponse()
+ {
+ final RecordingAMQPConnection connection = new RecordingAMQPConnection(_broker, _network, _port, _transport,
+ _protocol, _ticker);
+ connection.create();
+ connection.receiveProtocolHeader(new ProtocolInitiation(ProtocolVersion.v0_8));
+ clearInvocations(_network, _sender);
+
+ try (final QpidByteBuffer frame = createContentHeaderFrame(1, Long.MIN_VALUE, new byte[0]))
+ {
+ assertDoesNotThrow(() -> connection.received(frame));
+ }
+
+ assertEquals(0, connection.getCloseCount());
+ assertEquals(0, connection.getChannelCloseCount());
+ verify(_network).close();
+ verifyNoInteractions(_sender);
+ }
+
+ @Test
+ void invalidFrameEndAfterOpenClosesNetworkWithoutResponse()
+ {
+ final RecordingAMQPConnection connection = createOpenRecordingConnection(ProtocolVersion.v0_8,
+ Protocol.AMQP_0_8);
+ clearInvocations(_network, _sender);
+
+ try (final QpidByteBuffer frame = createFrame(HeartbeatBody.TYPE, 0, new byte[0], (byte) 0))
+ {
+ assertDoesNotThrow(() -> connection.received(frame));
+ }
+
+ assertEquals(0, connection.getCloseCount());
+ verify(_network).close();
+ verifyNoInteractions(_sender);
+ }
+
+ @Test
+ void unsupportedFrameTypeAfterOpenClosesNetworkWithoutResponse()
+ {
+ final RecordingAMQPConnection connection = createOpenRecordingConnection(ProtocolVersion.v0_8,
+ Protocol.AMQP_0_8);
+ try (final QpidByteBuffer heartbeat = createFrame(HeartbeatBody.TYPE, 0, new byte[0], FRAME_END))
+ {
+ assertDoesNotThrow(() -> connection.received(heartbeat));
+ }
+ clearInvocations(_network, _sender);
+
+ try (final QpidByteBuffer frame = createFrame(Byte.MAX_VALUE, 0, new byte[0], FRAME_END))
+ {
+ assertDoesNotThrow(() -> connection.received(frame));
+ }
+
+ assertEquals(0, connection.getCloseCount());
+ verify(_network).close();
+ verifyNoInteractions(_sender);
+ }
+
@Test
void resetStatistics()
{
- final AMQPConnection_0_8Impl connection =
- new AMQPConnection_0_8Impl(_broker, _network, _port, _transport, _protocol, 0, _ticker);
+ final AMQPConnection_0_8Impl connection = new AMQPConnection_0_8Impl(_broker, _network, _port, _transport,
+ _protocol, 0, _ticker);
connection.create();
connection.setAddressSpace(_virtualHost);
connection.registerMessageReceived(100L);
@@ -265,4 +926,296 @@ class AMQPConnection_0_8Test extends UnitTestBase
assertEquals(0L, statisticsAfterReset.get("transactedMessagesIn"));
assertEquals(0L, statisticsAfterReset.get("transactedMessagesOut"));
}
+
+ private static QpidByteBuffer createConnectionStartOkFrame(final byte[] fieldTable,
+ final String mechanism,
+ final byte[] response,
+ final String locale)
+ {
+ final byte[] mechanismBytes = mechanism.getBytes(StandardCharsets.UTF_8);
+ final byte[] localeBytes = locale.getBytes(StandardCharsets.UTF_8);
+ final int bodySize = Integer.BYTES + Integer.BYTES + fieldTable.length + 1 + mechanismBytes.length +
+ Integer.BYTES + response.length + 1 + localeBytes.length;
+ final QpidByteBuffer frame = QpidByteBuffer.allocate(AMQDecoder.FRAME_HEADER_SIZE + bodySize + 1);
+ frame.put((byte) 1);
+ frame.putUnsignedShort(0);
+ frame.putUnsignedInt(bodySize);
+ frame.putInt((ConnectionStartOkBody.CLASS_ID << 16) | ConnectionStartOkBody.METHOD_ID);
+ frame.putUnsignedInt(fieldTable.length);
+ frame.put(fieldTable);
+ frame.put((byte) mechanismBytes.length);
+ frame.put(mechanismBytes);
+ frame.putUnsignedInt(response.length);
+ frame.put(response);
+ frame.put((byte) localeBytes.length);
+ frame.put(localeBytes);
+ frame.put(FRAME_END);
+ frame.flip();
+ return frame;
+ }
+
+ private static QpidByteBuffer createQueueDeclareFrame(final byte[] arguments)
+ {
+ final int bodySize = Integer.BYTES + Short.BYTES + 2 + Integer.BYTES + arguments.length;
+ final QpidByteBuffer frame = QpidByteBuffer.allocate(AMQDecoder.FRAME_HEADER_SIZE + bodySize + 1);
+ frame.put((byte) 1);
+ frame.putUnsignedShort(1);
+ frame.putUnsignedInt(bodySize);
+ frame.putInt((QueueDeclareBody.CLASS_ID << 16) | QueueDeclareBody.METHOD_ID);
+ frame.putUnsignedShort(0);
+ frame.put((byte) 0);
+ frame.put((byte) 0);
+ frame.putUnsignedInt(arguments.length);
+ frame.put(arguments);
+ frame.put(FRAME_END);
+ frame.flip();
+ return frame;
+ }
+
+ private static QpidByteBuffer createMethodFrame(final int channelId,
+ final int classId,
+ final int methodId)
+ {
+ final ByteBuffer body = ByteBuffer.allocate(Integer.BYTES);
+ body.putInt((classId << 16) | methodId);
+ return createFrame((byte) 1, channelId, body.array(), FRAME_END);
+ }
+
+ private static QpidByteBuffer createContentHeaderFrame(final int channelId,
+ final long bodySize,
+ final byte[] properties)
+ {
+ final ByteBuffer body = ByteBuffer.allocate(Short.BYTES * 3 + Long.BYTES + properties.length);
+ body.putShort((short) ContentHeaderBody.CLASS_ID);
+ body.putShort((short) 0);
+ body.putLong(bodySize);
+ body.putShort((short) 0);
+ body.put(properties);
+ return createFrame(ContentHeaderBody.TYPE, channelId, body.array(), FRAME_END);
+ }
+
+ private static QpidByteBuffer createFrame(final AMQFrame frame)
+ {
+ final QpidByteBuffer buffer = QpidByteBuffer.allocate((int) frame.getSize());
+ final ByteBufferSender sender = mock(ByteBufferSender.class);
+ doAnswer(invocation ->
+ {
+ final QpidByteBuffer payload = invocation.getArgument(0);
+ buffer.put(payload);
+ return null;
+ }).when(sender).send(any(QpidByteBuffer.class));
+ frame.writePayload(sender);
+ buffer.flip();
+ return buffer;
+ }
+
+ @ParameterizedTest
+ @MethodSource("scopedAndFatalFailures")
+ void testReceivePreservesScopedAndFatalFailures(final Throwable failure) throws Exception
+ {
+ try (final MockedConstruction<BrokerDecoder> construction = mockConstruction(BrokerDecoder.class,
+ (decoder, context) -> doThrow(failure).when(decoder).decodeBuffer(any(QpidByteBuffer.class))))
+ {
+ final AMQPConnection_0_8Impl connection = new AMQPConnection_0_8Impl(_broker, _network, _port, _transport,
+ _protocol, 0, _ticker);
+ try (final QpidByteBuffer input = QpidByteBuffer.allocate(16))
+ {
+ assertSame(failure, assertThrows(failure.getClass(), () -> connection.onReceive(input)));
+ verify(construction.constructed().get(0)).decodeBuffer(input);
+ }
+ }
+ }
+
+ private static Stream<Arguments> terminalDecodingFailures()
+ {
+ return Stream.of(new IOException("Injected decoding failure"),
+ new ArithmeticException("Injected decoding failure"), new RuntimeException("Injected decoding failure"))
+ .flatMap(failure -> Stream.of(false, true).map(direct -> Arguments.of(failure, direct)));
+ }
+
+ private static Stream<Throwable> scopedAndFatalFailures()
+ {
+ return Stream.of(new ConnectionScopedRuntimeException("Injected connection failure"),
+ new ServerScopedRuntimeException("Injected server failure"),
+ new StoreException("Injected store failure"), new InternalError("Injected JVM failure"));
+ }
+
+ private static Stream<Arguments> rejectedFrameCloseReadVariants()
+ {
+ return Stream.of(Arguments.of(ProtocolVersion.v0_8, Protocol.AMQP_0_8),
+ Arguments.of(ProtocolVersion.v0_9, Protocol.AMQP_0_9),
+ Arguments.of(ProtocolVersion.v0_91, Protocol.AMQP_0_9_1))
+ .flatMap(arguments -> Stream.of(3, 7, 11, 12, 15, -1)
+ .flatMap(split -> Stream.of(false, true)
+ .flatMap(peerCloses -> Stream.of(false, true)
+ .map(oversized -> Arguments.of(arguments.get()[0], arguments.get()[1],
+ split, peerCloses, oversized)))));
+ }
+
+ private static QpidByteBuffer createFrame(final byte type,
+ final int channelId,
+ final byte[] body,
+ final byte frameEnd)
+ {
+ final QpidByteBuffer frame = QpidByteBuffer.allocate(AMQDecoder.FRAME_HEADER_SIZE + body.length + 1);
+ frame.put(type);
+ frame.putUnsignedShort(channelId);
+ frame.putUnsignedInt(body.length);
+ frame.put(body);
+ frame.put(frameEnd);
+ frame.flip();
+ return frame;
+ }
+
+ private static byte[] buildNestedTable(final int depth)
+ {
+ final ByteBuffer buffer = ByteBuffer.allocate(7 * depth);
+ for (int remainingDepth = depth; remainingDepth > 1; remainingDepth--)
+ {
+ buffer.put((byte) 1);
+ buffer.put((byte) 'n');
+ buffer.put(AMQType.FIELD_TABLE.identifier());
+ buffer.putInt(7 * (remainingDepth - 1));
+ }
+ buffer.put((byte) 1);
+ buffer.put((byte) 'v');
+ buffer.put(AMQType.INT.identifier());
+ buffer.putInt(42);
+ return buffer.array();
+ }
+
+ private static final class RecordingAMQPConnection extends AMQPConnection_0_8Impl
+ {
+ private int _closeErrorCode;
+ private int _closeChannelId;
+ private int _closeClassId;
+ private int _closeMethodId;
+ private int _closeCount;
+ private AMQChannel _closedChannel;
+ private int _channelCloseErrorCode;
+ private String _channelCloseMessage;
+ private int _channelCloseCount;
+
+ private RecordingAMQPConnection(final Broker broker,
+ final ServerNetworkConnection network,
+ final AmqpPort port,
+ final Transport transport,
+ final Protocol protocol,
+ final AggregateTicker ticker)
+ {
+ super(broker, network, port, transport, protocol, 0, ticker);
+ }
+
+ @Override
+ public void sendConnectionClose(final int errorCode, final String message, final int channelId)
+ {
+ recordConnectionClose(errorCode, channelId, 0, 0);
+ }
+
+ @Override
+ protected void sendConnectionClose(final int errorCode,
+ final String message,
+ final int channelId,
+ final int classId,
+ final int methodId)
+ {
+ recordConnectionClose(errorCode, channelId, classId, methodId);
+ }
+
+ @Override
+ public void closeChannelAndWriteFrame(final AMQChannel channel, final int cause, final String message)
+ {
+ _closedChannel = channel;
+ _channelCloseErrorCode = cause;
+ _channelCloseMessage = message;
+ _channelCloseCount++;
+ super.closeChannelAndWriteFrame(channel, cause, message);
+ }
+
+ private void recordConnectionClose(final int errorCode,
+ final int channelId,
+ final int classId,
+ final int methodId)
+ {
+ _closeErrorCode = errorCode;
+ _closeChannelId = channelId;
+ _closeClassId = classId;
+ _closeMethodId = methodId;
+ _closeCount++;
+ }
+
+ private int getCloseErrorCode()
+ {
+ return _closeErrorCode;
+ }
+
+ private int getCloseChannelId()
+ {
+ return _closeChannelId;
+ }
+
+ private int getCloseClassId()
+ {
+ return _closeClassId;
+ }
+
+ private int getCloseMethodId()
+ {
+ return _closeMethodId;
+ }
+
+ private int getCloseCount()
+ {
+ return _closeCount;
+ }
+
+ private AMQChannel getClosedChannel()
+ {
+ return _closedChannel;
+ }
+
+ private int getChannelCloseErrorCode()
+ {
+ return _channelCloseErrorCode;
+ }
+
+ private String getChannelCloseMessage()
+ {
+ return _channelCloseMessage;
+ }
+
+ private int getChannelCloseCount()
+ {
+ return _channelCloseCount;
+ }
+ }
+
+ private AMQPConnection_0_8Impl createConnectionAwaitingStartOk()
+ {
+ final AMQPConnection_0_8Impl connection = new AMQPConnection_0_8Impl(_broker, _network, _port, _transport,
+ _protocol, 0, _ticker);
+ connection.create();
+ connection.receiveProtocolHeader(new ProtocolInitiation(ProtocolVersion.v0_8));
+ return connection;
+ }
+
+ private RecordingAMQPConnection createOpenRecordingConnection(final ProtocolVersion protocolVersion,
+ final Protocol protocol)
+ {
+ final RecordingAMQPConnection connection = new RecordingAMQPConnection(_broker, _network, _port, _transport,
+ protocol, _ticker);
+ openConnection(connection, protocolVersion);
+ return connection;
+ }
+
+ private void openConnection(final AMQPConnection_0_8Impl connection, final ProtocolVersion protocolVersion)
+ {
+ connection.create();
+ connection.receiveProtocolHeader(new ProtocolInitiation(protocolVersion));
+ connection.receiveConnectionStartOk(FieldTableFactory.createFieldTable(Map.of()), SASL_MECH, SASL_RESPONSE,
+ LOCALE);
+ connection.receiveConnectionTuneOk(10, 65535, 0);
+ connection.receiveConnectionOpen(AMQShortString.createAMQShortString(VIRTUAL_HOST_NAME),
+ AMQShortString.EMPTY_STRING, false);
+ }
}
diff --git a/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/EncodingUtilsTest.java b/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/EncodingUtilsTest.java
index 8fd305f4df..2bd32f3e32 100644
--- a/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/EncodingUtilsTest.java
+++ b/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/EncodingUtilsTest.java
@@ -22,6 +22,11 @@ package org.apache.qpid.server.protocol.v0_8;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
@@ -71,8 +76,189 @@ class EncodingUtilsTest extends UnitTestBase
_buffer.put((byte)'1');
_buffer.put((byte)'a');
_buffer.flip();
- assertThrows(AMQFrameDecodingException.class,
- () -> EncodingUtils.readLongAsShortString(_buffer),
+ assertThrows(AMQFrameDecodingException.class, () -> EncodingUtils.readLongAsShortString(_buffer),
"Exception is expected");
}
-}
\ No newline at end of file
+
+ @Test
+ void readLongStringReadsValidValue()
+ {
+ _buffer.putInt(2);
+ _buffer.put((byte) 'h');
+ _buffer.put((byte) 'i');
+ _buffer.flip();
+ assertEquals("hi", EncodingUtils.readLongString(_buffer), "Unexpected result");
+ }
+
+ @Test
+ void readLongStringRejectsUnsignedLengthLargerThanRemaining()
+ {
+ _buffer.putInt(0x80000000);
+ _buffer.put((byte) 'x');
+ _buffer.flip();
+ assertThrows(IllegalArgumentException.class, () -> EncodingUtils.readLongString(_buffer));
+ }
+
+ @Test
+ void readLongStringRejectsOversizedLength()
+ {
+ _buffer.putInt(0x20000000);
+ _buffer.put((byte) 'x');
+ _buffer.flip();
+ assertThrows(IllegalArgumentException.class, () -> EncodingUtils.readLongString(_buffer));
+ }
+
+ @Test
+ void readLongstrRejectsUnsignedLengthLargerThanRemaining()
+ {
+ _buffer.putInt(0x80000000);
+ _buffer.put((byte) 1);
+ _buffer.flip();
+ assertThrows(IllegalArgumentException.class, () -> EncodingUtils.readLongstr(_buffer));
+ }
+
+ @Test
+ void readBytesRejectsOversizedLength()
+ {
+ _buffer.putInt(0x20000000);
+ _buffer.put((byte) 1);
+ _buffer.flip();
+ assertThrows(IllegalArgumentException.class, () -> EncodingUtils.readBytes(_buffer));
+ }
+
+ @Test
+ void fieldArrayRejectsUnsignedLengthLargerThanRemaining()
+ {
+ _buffer.putInt(0x80000000);
+ _buffer.put((byte) 1);
+ _buffer.flip();
+
+ assertThrows(IllegalArgumentException.class, () -> FieldArray.readFromBuffer(_buffer));
+ }
+
+ @Test
+ void readFieldTableRejectsOversizedLengthBeforeCreatingView()
+ {
+ final QpidByteBuffer input = mock(QpidByteBuffer.class);
+ when(input.getUnsignedInt()).thenReturn(1L);
+ when(input.remaining()).thenReturn(0);
+
+ assertThrows(IllegalArgumentException.class, () -> EncodingUtils.readFieldTable(input));
+
+ verify(input, never()).view(anyInt(), anyInt());
+ verify(input, never()).position(anyInt());
+ }
+
+ @Test
+ void fieldTableWithOversizedLongStringValueIsRejected()
+ {
+ _buffer.putUnsignedByte((short) 1); // key length
+ _buffer.put((byte) 's'); // key "s"
+ _buffer.put(AMQType.LONG_STRING.identifier()); // value type 'S'
+ _buffer.putInt(0x80000000); // long-string length
+ _buffer.flip();
+ final int length = _buffer.remaining();
+ final FieldTable fieldTable = FieldTableFactory.createFieldTable(_buffer, length);
+ try
+ {
+ assertThrows(IllegalArgumentException.class, fieldTable::validate);
+ }
+ finally
+ {
+ fieldTable.dispose();
+ }
+ }
+
+ @Test
+ void decodedFieldTableDetachesNestedTableFromPooledBuffer()
+ {
+ final long baseline = QpidByteBuffer.getAllocatedDirectMemorySize();
+ QpidByteBuffer input = QpidByteBuffer.allocateDirect(60);
+ FieldTable encodedTable = null;
+ FieldTable decodedTable = null;
+ FieldTable nestedTable = null;
+ try
+ {
+ input.putInt(14); // outer field-table length
+ input.putUnsignedByte((short) 1);
+ input.put((byte) 'n');
+ input.put(AMQType.FIELD_TABLE.identifier());
+ input.putInt(7); // nested field-table length
+ input.putUnsignedByte((short) 1);
+ input.put((byte) 'x');
+ input.put(AMQType.INT.identifier());
+ input.putInt(42);
+ input.flip();
+
+ encodedTable = EncodingUtils.readFieldTable(input);
+ decodedTable = FieldTable.convertToDecodedFieldTable(encodedTable);
+ nestedTable = (FieldTable) decodedTable.get("n");
+
+ encodedTable.dispose();
+ encodedTable = null;
+ input.dispose();
+ input = null;
+
+ assertEquals(baseline, QpidByteBuffer.getAllocatedDirectMemorySize(),
+ "Decoded nested field table retained a pooled direct buffer");
+ assertEquals(42, nestedTable.get("x"));
+ }
+ finally
+ {
+ if (nestedTable != null)
+ {
+ nestedTable.dispose();
+ }
+ if (decodedTable != null)
+ {
+ decodedTable.dispose();
+ }
+ if (encodedTable != null)
+ {
+ encodedTable.dispose();
+ }
+ if (input != null)
+ {
+ input.dispose();
+ }
+ }
+ }
+
+ @Test
+ void malformedFieldArrayAfterNestedTableDoesNotLeakPooledBuffer()
+ {
+ final long baseline = QpidByteBuffer.getAllocatedDirectMemorySize();
+ QpidByteBuffer input = QpidByteBuffer.allocateDirect(60);
+ try
+ {
+ input.putInt(20); // outer field-table length
+ input.putUnsignedByte((short) 1);
+ input.put((byte) 'a');
+ input.put(AMQType.FIELD_ARRAY.identifier());
+ input.putInt(13); // field-array payload length
+ input.put(AMQType.FIELD_TABLE.identifier());
+ input.putInt(3); // nested field-table length
+ input.putUnsignedByte((short) 1);
+ input.put((byte) 'x');
+ input.put(AMQType.VOID.identifier());
+ input.put(AMQType.LONG_STRING.identifier());
+ input.putInt(0x80000000);
+ input.flip();
+
+ final QpidByteBuffer tableToDecode = input;
+ assertThrows(IllegalArgumentException.class, () -> EncodingUtils.readFieldTable(tableToDecode));
+ input.dispose();
+ input = null;
+
+ assertEquals(baseline, QpidByteBuffer.getAllocatedDirectMemorySize(),
+ "Malformed field array leaked a nested field-table view");
+ }
+ finally
+ {
+ if (input != null)
+ {
+ input.dispose();
+ }
+ }
+ }
+}
diff --git a/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/FieldTableNestingDecoderTest.java b/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/FieldTableNestingDecoderTest.java
new file mode 100644
index 0000000000..21620952f0
--- /dev/null
+++ b/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/FieldTableNestingDecoderTest.java
@@ -0,0 +1,354 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ */
+package org.apache.qpid.server.protocol.v0_8;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.nio.ByteBuffer;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import org.mockito.ArgumentCaptor;
+
+import org.apache.qpid.server.bytebuffer.QpidByteBuffer;
+import org.apache.qpid.server.protocol.ErrorCodes;
+import org.apache.qpid.server.protocol.ProtocolVersion;
+import org.apache.qpid.server.protocol.v0_8.transport.BasicConsumeBody;
+import org.apache.qpid.server.protocol.v0_8.transport.ConnectionStartOkBody;
+import org.apache.qpid.server.protocol.v0_8.transport.ContentHeaderBody;
+import org.apache.qpid.server.protocol.v0_8.transport.ExchangeDeclareBody;
+import org.apache.qpid.server.protocol.v0_8.transport.QueueBindBody;
+import org.apache.qpid.server.protocol.v0_8.transport.QueueDeclareBody;
+import org.apache.qpid.server.protocol.v0_8.transport.ServerChannelMethodProcessor;
+import org.apache.qpid.server.protocol.v0_8.transport.ServerMethodProcessor;
+import org.apache.qpid.test.utils.UnitTestBase;
+
+class FieldTableNestingDecoderTest extends UnitTestBase
+{
+ private static final byte FRAME_END = (byte) 0xCE;
+ private static final byte METHOD_FRAME = 1;
+ private static final int CHANNEL_ID = 1;
+ private static final int CONTENT_HEADERS_PROPERTY_FLAG = 1 << 13;
+ private static final int CONFIGURED_MAXIMUM = 4;
+ private static final int CONFIGURED_MAXIMUM_ABOVE_DEFAULT = AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS + 10;
+
+ @ParameterizedTest(name = "{0} accepts {1} arguments at the nesting limit")
+ @MethodSource("protocolVersionsAndMethods")
+ void methodArgumentsAtLimitAreAccepted(final ProtocolVersion protocolVersion,
+ final FieldTableMethod method)
+ throws Exception
+ {
+ final ServerChannelMethodProcessor channelMethodProcessor = mock(ServerChannelMethodProcessor.class);
+ when(channelMethodProcessor.ignoreAllButCloseOk()).thenReturn(true);
+ final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor =
+ createMethodProcessor(protocolVersion, channelMethodProcessor);
+ final ServerDecoder decoder = new ServerDecoder(methodProcessor, CONFIGURED_MAXIMUM);
+ decoder.setExpectProtocolInitiation(false);
+
+ try (final QpidByteBuffer frame = createMethodFrame(method, buildNestedTable(CONFIGURED_MAXIMUM)))
+ {
+ decoder.decodeBuffer(frame);
+ }
+
+ verify(channelMethodProcessor).ignoreAllButCloseOk();
+ }
+
+ @ParameterizedTest(name = "{0} preserves a configured limit above the default after dispatch")
+ @MethodSource("protocolVersions")
+ void configuredLimitAboveDefaultIsPreservedAfterDispatch(final ProtocolVersion protocolVersion)
+ throws Exception
+ {
+ final ServerChannelMethodProcessor channelMethodProcessor = mock(ServerChannelMethodProcessor.class);
+ final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor =
+ createMethodProcessor(protocolVersion, channelMethodProcessor);
+ final ServerDecoder decoder = new ServerDecoder(methodProcessor, CONFIGURED_MAXIMUM_ABOVE_DEFAULT);
+ decoder.setExpectProtocolInitiation(false);
+
+ try (final QpidByteBuffer frame = createMethodFrame(FieldTableMethod.QUEUE_DECLARE,
+ buildNestedTable(CONFIGURED_MAXIMUM_ABOVE_DEFAULT)))
+ {
+ decoder.decodeBuffer(frame);
+ }
+
+ final ArgumentCaptor<FieldTable> argumentsCaptor = ArgumentCaptor.forClass(FieldTable.class);
+ verify(channelMethodProcessor).receiveQueueDeclare(any(), eq(false), eq(false), eq(false), eq(false),
+ eq(false), argumentsCaptor.capture());
+ final FieldTable arguments = argumentsCaptor.getValue();
+ try
+ {
+ final FieldTable nested = (FieldTable) arguments.get("n");
+ try
+ {
+ assertDoesNotThrow(() -> FieldTable.convertToMap(nested));
+ }
+ finally
+ {
+ nested.dispose();
+ }
+ }
+ finally
+ {
+ arguments.dispose();
+ }
+ }
+
+ @ParameterizedTest(name = "{0} rejects over-limit {1} arguments")
+ @MethodSource("protocolVersionsAndMethods")
+ void methodArgumentsOverLimitAreRejectedBeforeDispatch(final ProtocolVersion protocolVersion,
+ final FieldTableMethod method)
+ {
+ final ServerChannelMethodProcessor channelMethodProcessor = mock(ServerChannelMethodProcessor.class);
+ final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor =
+ createMethodProcessor(protocolVersion, channelMethodProcessor);
+ final ServerDecoder decoder = new ServerDecoder(methodProcessor, CONFIGURED_MAXIMUM);
+ decoder.setExpectProtocolInitiation(false);
+ final AMQFrameDecodingException exception;
+
+ try (final QpidByteBuffer frame = createMethodFrame(method, buildNestedTable(CONFIGURED_MAXIMUM + 1)))
+ {
+ exception = assertThrows(AMQFrameDecodingException.class, () -> decoder.decodeBuffer(frame));
+ }
+
+ final int classAndMethod = method.getClassAndMethod();
+ assertEquals(ErrorCodes.RESOURCE_ERROR, exception.getErrorCode());
+ assertEquals(classAndMethod >> 16, exception.getClassId());
+ assertEquals(classAndMethod & 0xFFFF, exception.getMethodId());
+ verify(channelMethodProcessor, never()).ignoreAllButCloseOk();
+ }
+
+ @ParameterizedTest(name = "{0} accepts content headers at the nesting limit")
+ @MethodSource("protocolVersions")
+ void contentHeadersAtLimitAreAccepted(final ProtocolVersion protocolVersion) throws Exception
+ {
+ final ServerChannelMethodProcessor channelMethodProcessor = mock(ServerChannelMethodProcessor.class);
+ when(channelMethodProcessor.ignoreAllButCloseOk()).thenReturn(true);
+ final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor =
+ createMethodProcessor(protocolVersion, channelMethodProcessor);
+ final ServerDecoder decoder = new ServerDecoder(methodProcessor, CONFIGURED_MAXIMUM);
+ decoder.setExpectProtocolInitiation(false);
+
+ try (final QpidByteBuffer frame = createContentHeaderFrame(buildNestedTable(CONFIGURED_MAXIMUM)))
+ {
+ decoder.decodeBuffer(frame);
+ }
+
+ verify(channelMethodProcessor).ignoreAllButCloseOk();
+ }
+
+ @ParameterizedTest(name = "{0} rejects over-limit content headers")
+ @MethodSource("protocolVersions")
+ void contentHeadersOverLimitAreRejectedBeforeDispatch(final ProtocolVersion protocolVersion)
+ {
+ final ServerChannelMethodProcessor channelMethodProcessor = mock(ServerChannelMethodProcessor.class);
+ final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor =
+ createMethodProcessor(protocolVersion, channelMethodProcessor);
+ final ServerDecoder decoder = new ServerDecoder(methodProcessor, CONFIGURED_MAXIMUM);
+ decoder.setExpectProtocolInitiation(false);
+ final AMQFrameDecodingException exception;
+
+ try (final QpidByteBuffer frame = createContentHeaderFrame(buildNestedTable(CONFIGURED_MAXIMUM + 1)))
+ {
+ exception = assertThrows(AMQFrameDecodingException.class, () -> decoder.decodeBuffer(frame));
+ }
+
+ assertEquals(ErrorCodes.RESOURCE_ERROR, exception.getErrorCode());
+ assertEquals(0, exception.getClassId());
+ assertEquals(0, exception.getMethodId());
+ verify(channelMethodProcessor, never()).ignoreAllButCloseOk();
+ }
+
+ @Test
+ void brokerDecoderUsesProtocolSpecificNestingContext()
+ {
+ final AMQPConnection_0_8Impl connection = mock(AMQPConnection_0_8Impl.class);
+ when(connection.getContextValue(Integer.class, AMQPConnection_0_8.CODEC_MAX_NESTED_OBJECTS))
+ .thenReturn(CONFIGURED_MAXIMUM);
+ final BrokerDecoder decoder = new BrokerDecoder(connection);
+ decoder.setExpectProtocolInitiation(false);
+ final AMQFrameDecodingException exception;
+
+ try (final QpidByteBuffer frame = createConnectionStartOkFrame(buildNestedTable(CONFIGURED_MAXIMUM + 1)))
+ {
+ exception = assertThrows(AMQFrameDecodingException.class, () -> decoder.decodeBuffer(frame));
+ }
+
+ assertEquals(ErrorCodes.RESOURCE_ERROR, exception.getErrorCode());
+ assertEquals(ConnectionStartOkBody.CLASS_ID, exception.getClassId());
+ assertEquals(ConnectionStartOkBody.METHOD_ID, exception.getMethodId());
+ verify(connection).getContextValue(Integer.class, AMQPConnection_0_8.CODEC_MAX_NESTED_OBJECTS);
+ verify(connection, never()).receiveConnectionStartOk(any(), any(), any(), any());
+ }
+
+ @Test
+ void negativeProtocolSpecificNestingContextIsRejected()
+ {
+ final AMQPConnection_0_8Impl connection = mock(AMQPConnection_0_8Impl.class);
+ when(connection.getContextValue(Integer.class, AMQPConnection_0_8.CODEC_MAX_NESTED_OBJECTS)).thenReturn(-1);
+
+ assertThrows(IllegalArgumentException.class, () -> new BrokerDecoder(connection));
+ }
+
+ private static Stream<Arguments> protocolVersionsAndMethods()
+ {
+ return protocolVersions().flatMap(protocolVersion -> Stream.of(FieldTableMethod.values())
+ .map(method -> Arguments.of(protocolVersion, method)));
+ }
+
+ private static Stream<ProtocolVersion> protocolVersions()
+ {
+ return Stream.of(ProtocolVersion.v0_8, ProtocolVersion.v0_9, ProtocolVersion.v0_91);
+ }
+
+ private static ServerMethodProcessor<ServerChannelMethodProcessor> createMethodProcessor(
+ final ProtocolVersion protocolVersion,
+ final ServerChannelMethodProcessor channelMethodProcessor)
+ {
+ final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor =
+ mock(ServerMethodProcessor.class);
+ when(methodProcessor.getProtocolVersion()).thenReturn(protocolVersion);
+ when(methodProcessor.getChannelMethodProcessor(CHANNEL_ID)).thenReturn(channelMethodProcessor);
+ return methodProcessor;
+ }
+
+ private static QpidByteBuffer createMethodFrame(final FieldTableMethod method, final byte[] fieldTable)
+ {
+ final int argumentsSize = switch (method)
+ {
+ case QUEUE_DECLARE -> Short.BYTES + 1 + 1;
+ case QUEUE_BIND -> Short.BYTES + 1 + 1 + 1 + 1;
+ case EXCHANGE_DECLARE, BASIC_CONSUME -> Short.BYTES + 1 + 1 + 1;
+ };
+
+ final int bodySize = Integer.BYTES + argumentsSize + Integer.BYTES + fieldTable.length;
+ final QpidByteBuffer frame = QpidByteBuffer.allocate(AMQDecoder.FRAME_HEADER_SIZE + bodySize + 1);
+ writeFrameHeader(frame, METHOD_FRAME, CHANNEL_ID, bodySize);
+ frame.putInt(method.getClassAndMethod());
+ frame.putUnsignedShort(0);
+ frame.put((byte) 0);
+ if (method != FieldTableMethod.QUEUE_DECLARE)
+ {
+ frame.put((byte) 0);
+ }
+ if (method == FieldTableMethod.QUEUE_BIND)
+ {
+ frame.put((byte) 0);
+ }
+ frame.put((byte) 0);
+ writeFieldTable(frame, fieldTable);
+ frame.put(FRAME_END);
+ frame.flip();
+ return frame;
+ }
+
+ private static QpidByteBuffer createContentHeaderFrame(final byte[] fieldTable)
+ {
+ final int bodySize = 2 * Short.BYTES + Long.BYTES + Short.BYTES + Integer.BYTES + fieldTable.length;
+ final QpidByteBuffer frame = QpidByteBuffer.allocate(AMQDecoder.FRAME_HEADER_SIZE + bodySize + 1);
+ writeFrameHeader(frame, ContentHeaderBody.TYPE, CHANNEL_ID, bodySize);
+ frame.putUnsignedShort(ContentHeaderBody.CLASS_ID);
+ frame.putUnsignedShort(0);
+ frame.putLong(0);
+ frame.putUnsignedShort(CONTENT_HEADERS_PROPERTY_FLAG);
+ writeFieldTable(frame, fieldTable);
+ frame.put(FRAME_END);
+ frame.flip();
+ return frame;
+ }
+
+ private static QpidByteBuffer createConnectionStartOkFrame(final byte[] fieldTable)
+ {
+ final int bodySize = Integer.BYTES + Integer.BYTES + fieldTable.length + 1 + Integer.BYTES + 1;
+ final QpidByteBuffer frame = QpidByteBuffer.allocate(AMQDecoder.FRAME_HEADER_SIZE + bodySize + 1);
+ writeFrameHeader(frame, METHOD_FRAME, 0, bodySize);
+ frame.putInt((ConnectionStartOkBody.CLASS_ID << 16) | ConnectionStartOkBody.METHOD_ID);
+ writeFieldTable(frame, fieldTable);
+ frame.put((byte) 0);
+ frame.putUnsignedInt(0);
+ frame.put((byte) 0);
+ frame.put(FRAME_END);
+ frame.flip();
+ return frame;
+ }
+
+ private static void writeFrameHeader(final QpidByteBuffer buffer,
+ final byte type,
+ final int channel,
+ final long bodySize)
+ {
+ buffer.put(type);
+ buffer.putUnsignedShort(channel);
+ buffer.putUnsignedInt(bodySize);
+ }
+
+ private static void writeFieldTable(final QpidByteBuffer buffer, final byte[] fieldTable)
+ {
+ buffer.putUnsignedInt(fieldTable.length);
+ buffer.put(fieldTable);
+ }
+
+ private static byte[] buildNestedTable(final int depth)
+ {
+ final ByteBuffer buffer = ByteBuffer.allocate(7 * depth);
+ for (int remainingDepth = depth; remainingDepth > 1; remainingDepth--)
+ {
+ buffer.put((byte) 1);
+ buffer.put((byte) 'n');
+ buffer.put(AMQType.FIELD_TABLE.identifier());
+ buffer.putInt(7 * (remainingDepth - 1));
+ }
+ buffer.put((byte) 1);
+ buffer.put((byte) 'v');
+ buffer.put(AMQType.INT.identifier());
+ buffer.putInt(42);
+ return buffer.array();
+ }
+
+ private enum FieldTableMethod
+ {
+ QUEUE_DECLARE,
+ QUEUE_BIND,
+ EXCHANGE_DECLARE,
+ BASIC_CONSUME;
+
+ int getClassAndMethod()
+ {
+ return switch (this)
+ {
+ case QUEUE_DECLARE -> (QueueDeclareBody.CLASS_ID << 16) | QueueDeclareBody.METHOD_ID;
+ case QUEUE_BIND -> (QueueBindBody.CLASS_ID << 16) | QueueBindBody.METHOD_ID;
+ case EXCHANGE_DECLARE -> (ExchangeDeclareBody.CLASS_ID << 16) | ExchangeDeclareBody.METHOD_ID;
+ case BASIC_CONSUME -> (BasicConsumeBody.CLASS_ID << 16) | BasicConsumeBody.METHOD_ID;
+ };
+ }
+ }
+}
diff --git a/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/FieldTableTest.java b/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/FieldTableTest.java
index 6298fcacc6..03f3bbf999 100644
--- a/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/FieldTableTest.java
+++ b/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/FieldTableTest.java
@@ -27,6 +27,7 @@ import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import java.nio.ByteBuffer;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
@@ -35,6 +36,8 @@ import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
import org.apache.qpid.server.bytebuffer.QpidByteBuffer;
import org.apache.qpid.test.utils.UnitTestBase;
@@ -414,8 +417,7 @@ class FieldTableTest extends UnitTestBase
void checkPropertyNameIsNull()
{
final Map<String, Object> map = Collections.singletonMap(null, "String");
- assertThrows(IllegalArgumentException.class,
- () -> FieldTableFactory.createFieldTable(map),
+ assertThrows(IllegalArgumentException.class, () -> FieldTableFactory.createFieldTable(map),
"Null property name is not allowed");
}
@@ -426,8 +428,7 @@ class FieldTableTest extends UnitTestBase
void checkPropertyNameIsEmptyString()
{
final Map<String, Object> map =Map.of("", "String");
- assertThrows(IllegalArgumentException.class,
- () -> FieldTableFactory.createFieldTable(map),
+ assertThrows(IllegalArgumentException.class, () -> FieldTableFactory.createFieldTable(map),
"Empty property name is not allowed");
}
@@ -441,9 +442,8 @@ class FieldTableTest extends UnitTestBase
final boolean strictAMQP = FieldTable._strictAMQP;
final Map<String, Object> map = Map.of("x".repeat(129), "String");
FieldTable._strictAMQP = true;
- assertThrows(IllegalArgumentException.class,
- () -> new FieldTable(map),
- "property name must be < 128 characters");
+ assertThrows(IllegalArgumentException.class, () -> new FieldTable(map),
+ "property name must be < 128 characters");
FieldTable._strictAMQP = strictAMQP;
}
@@ -458,9 +458,8 @@ class FieldTableTest extends UnitTestBase
// Try a name that starts with a number
FieldTable._strictAMQP = true;
final Map<String, Object> map = Map.of("1", "String");
- assertThrows(IllegalArgumentException.class,
- () -> new FieldTable(map),
- "property name must start with a letter");
+ assertThrows(IllegalArgumentException.class, () -> new FieldTable(map),
+ "property name must start with a letter");
FieldTable._strictAMQP = strictAMQP;
}
@@ -501,6 +500,356 @@ class FieldTableTest extends UnitTestBase
assertTrue(fieldTable.containsKey("testKey"), "Expected key is not found");
}
+ @Test
+ void malformedLongStringInNestedTableIsRejectedDuringRead()
+ {
+ final byte[] malformedNestedTable = tableEntryWithLongStringLength(0x80000000);
+ final byte[] outerTable = tableEntryWithCompoundValue(AMQType.FIELD_TABLE, malformedNestedTable);
+
+ assertThrows(IllegalArgumentException.class, () -> readFieldTable(outerTable));
+ }
+
+ @Test
+ void malformedLongStringInTableNestedInArrayIsRejectedDuringRead()
+ {
+ final byte[] malformedNestedTable = tableEntryWithLongStringLength(0x80000000);
+ final byte[] array = compoundValue(AMQType.FIELD_TABLE, malformedNestedTable);
+ final byte[] outerTable = tableEntryWithCompoundValue(AMQType.FIELD_ARRAY, array);
+
+ assertThrows(IllegalArgumentException.class, () -> readFieldTable(outerTable));
+ }
+
+ @Test
+ void maximumNestingDepthCanBeDetachedAndTraversed()
+ {
+ final FieldTable encoded =
+ readFieldTable(buildNestedTable(AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS));
+ final FieldTable decoded = FieldTable.convertToDecodedFieldTable(encoded);
+ try
+ {
+ FieldTable current = decoded;
+ for (int i = 1; i < AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS; i++)
+ {
+ current = (FieldTable) current.get("n");
+ }
+ assertEquals(42, current.get("v"));
+ }
+ finally
+ {
+ encoded.dispose();
+ decoded.dispose();
+ }
+ }
+
+ @Test
+ void maximumNestingDepthCanBeConverted()
+ {
+ final FieldTable fieldTable =
+ readFieldTable(buildNestedTable(AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS));
+ try
+ {
+ assertDoesNotThrow(() -> FieldTable.convertToMap(fieldTable));
+ }
+ finally
+ {
+ fieldTable.dispose();
+ }
+ }
+
+ @Test
+ void excessiveNestingIsRejectedDuringReadWithoutStackOverflow()
+ {
+ assertThrows(AMQValueNestingException.class, () -> readFieldTable(buildNestedTable(
+ AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS + 1)));
+ assertThrows(AMQValueNestingException.class, () -> readFieldTable(buildNestedTable(10_000)));
+ }
+
+ @Test
+ void excessiveProgrammaticNestingIsRejectedDuringConversionWithoutStackOverflow()
+ {
+ final FieldTable fieldTable = buildProgrammaticNestedTable(10_000);
+
+ assertThrows(AMQValueNestingException.class, () -> FieldTable.convertToMap(fieldTable));
+ }
+
+ @Test
+ void maximumArrayNestingDepthCanBeDecoded()
+ {
+ final int arrayDepth = AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS - 1;
+ final FieldTable fieldTable = readFieldTable(buildNestedArrayTable(arrayDepth));
+ try
+ {
+ Object current = fieldTable.get("n");
+ for (int i = 1; i < arrayDepth; i++)
+ {
+ current = ((Collection<?>) current).iterator().next();
+ }
+ assertEquals(42, ((Collection<?>) current).iterator().next());
+ }
+ finally
+ {
+ fieldTable.dispose();
+ }
+ }
+
+ @Test
+ void excessiveArrayNestingIsRejectedDuringReadWithoutStackOverflow()
+ {
+ assertThrows(AMQValueNestingException.class, () -> readFieldTable(buildNestedArrayTable(
+ AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS)));
+ assertThrows(AMQValueNestingException.class, () -> readFieldTable(buildNestedArrayTable(10_000)));
+ }
+
+ @Test
+ void configuredNestingDepthAppliesToTablesAndArrays()
+ {
+ final int configuredLimit = 2;
+ FieldTable table = null;
+ FieldTable array = null;
+ try
+ {
+ table = readFieldTable(buildNestedTable(configuredLimit), configuredLimit);
+ array = readFieldTable(buildNestedArrayTable(configuredLimit - 1), configuredLimit);
+ assertNotNull(table);
+ assertNotNull(array);
+ }
+ finally
+ {
+ if (table != null)
+ {
+ table.dispose();
+ }
+ if (array != null)
+ {
+ array.dispose();
+ }
+ }
+
+ assertThrows(AMQValueNestingException.class, () ->
+ readFieldTable(buildNestedTable(configuredLimit + 1), configuredLimit));
+ assertThrows(AMQValueNestingException.class, () ->
+ readFieldTable(buildNestedArrayTable(configuredLimit), configuredLimit));
+ }
+
+ @Test
+ void configuredLimitAboveDefaultIsPreservedDuringConversion()
+ {
+ final int configuredLimit = AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS + 10;
+ final FieldTable encoded = readFieldTable(buildNestedTable(configuredLimit), configuredLimit);
+ final FieldTable decoded = FieldTable.convertToDecodedFieldTable(encoded);
+ try
+ {
+ assertDoesNotThrow(() -> encoded.validate());
+ assertDoesNotThrow(() -> FieldTable.convertToMap(decoded));
+ final FieldTable nested = (FieldTable) decoded.get("n");
+ try
+ {
+ assertThrows(AMQValueNestingException.class, () ->
+ FieldTable.convertToMap(nested, AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS));
+ assertDoesNotThrow(() -> FieldTable.convertToMap(nested));
+ }
+ finally
+ {
+ nested.dispose();
+ }
+ }
+ finally
+ {
+ decoded.dispose();
+ encoded.dispose();
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
+ void configuredLimitAboveDefaultIsPreservedByLazyNestedTable(final boolean direct)
+ {
+ final int configuredLimit = AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS + 10;
+ final FieldTable fieldTable = readFieldTable(buildNestedTable(configuredLimit), configuredLimit, direct);
+ try
+ {
+ final FieldTable nested = (FieldTable) fieldTable.get("n");
+ try
+ {
+ assertThrows(AMQValueNestingException.class, () ->
+ FieldTable.convertToMap(nested, AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS));
+ assertDoesNotThrow(() -> FieldTable.convertToMap(nested));
+ }
+ finally
+ {
+ nested.dispose();
+ }
+ }
+ finally
+ {
+ fieldTable.dispose();
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
+ void configuredLimitAboveDefaultIsPreservedByTableNestedInArray(final boolean direct)
+ {
+ final int configuredLimit = AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS + 10;
+ final byte[] payload = buildNestedTableInArray(configuredLimit - 2);
+ final FieldTable fieldTable = readFieldTable(payload, configuredLimit, direct);
+ try
+ {
+ final Collection<?> array = (Collection<?>) fieldTable.get("n");
+ final FieldTable nested = (FieldTable) array.iterator().next();
+ try
+ {
+ assertThrows(AMQValueNestingException.class, () ->
+ FieldTable.convertToMap(nested, AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS));
+ assertDoesNotThrow(() -> FieldTable.convertToMap(nested));
+ }
+ finally
+ {
+ nested.dispose();
+ }
+ }
+ finally
+ {
+ fieldTable.dispose();
+ }
+ }
+
+ @Test
+ void stricterValidationIsAppliedAfterSuccessfulRead()
+ {
+ final FieldTable fieldTable = readFieldTable(buildNestedTable(3), 3);
+ try
+ {
+ assertThrows(AMQValueNestingException.class, () -> fieldTable.validate(2));
+ }
+ finally
+ {
+ fieldTable.dispose();
+ }
+ }
+
+ @Test
+ void malformedLongValueLengthIsRejectedDuringNestingValidation()
+ {
+ for (final AMQType type : List.of(AMQType.LONG_STRING, AMQType.BINARY, AMQType.ASCII_STRING, AMQType.WIDE_STRING))
+ {
+ final ByteBuffer buffer = ByteBuffer.allocate(7);
+ buffer.put((byte) 1);
+ buffer.put((byte) 'v');
+ buffer.put(type.identifier());
+ buffer.putInt(-1);
+
+ assertThrows(IllegalArgumentException.class, () -> readFieldTable(buffer.array(), 1), type.name());
+ }
+ }
+
+ private static FieldTable readFieldTable(final byte[] payload)
+ {
+ return readFieldTable(payload, AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS);
+ }
+
+ private static FieldTable readFieldTable(final byte[] payload, final int maxNestedObjects)
+ {
+ return readFieldTable(payload, maxNestedObjects, false);
+ }
+
+ private static FieldTable readFieldTable(final byte[] payload,
+ final int maxNestedObjects,
+ final boolean direct)
+ {
+ try (final QpidByteBuffer buffer = QpidByteBuffer.allocate(direct, Integer.BYTES + payload.length))
+ {
+ buffer.putUnsignedInt(payload.length);
+ buffer.put(payload);
+ buffer.flip();
+ return EncodingUtils.readFieldTable(buffer, maxNestedObjects);
+ }
+ }
+
+ private static byte[] buildNestedTable(final int depth)
+ {
+ final ByteBuffer buffer = ByteBuffer.allocate(7 * depth);
+ for (int remainingDepth = depth; remainingDepth > 1; remainingDepth--)
+ {
+ buffer.put((byte) 1);
+ buffer.put((byte) 'n');
+ buffer.put(AMQType.FIELD_TABLE.identifier());
+ buffer.putInt(7 * (remainingDepth - 1));
+ }
+ buffer.put((byte) 1);
+ buffer.put((byte) 'v');
+ buffer.put(AMQType.INT.identifier());
+ buffer.putInt(42);
+ return buffer.array();
+ }
+
+ private static byte[] buildNestedArrayTable(final int arrayDepth)
+ {
+ final ByteBuffer buffer = ByteBuffer.allocate(7 + 5 * arrayDepth);
+ buffer.put((byte) 1);
+ buffer.put((byte) 'n');
+ buffer.put(AMQType.FIELD_ARRAY.identifier());
+ buffer.putInt(5 * arrayDepth);
+ for (int remainingDepth = arrayDepth; remainingDepth > 1; remainingDepth--)
+ {
+ buffer.put(AMQType.FIELD_ARRAY.identifier());
+ buffer.putInt(5 * (remainingDepth - 1));
+ }
+ buffer.put(AMQType.INT.identifier());
+ buffer.putInt(42);
+ return buffer.array();
+ }
+
+ private static byte[] buildNestedTableInArray(final int tableDepth)
+ {
+ final byte[] nestedTable = buildNestedTable(tableDepth);
+ final ByteBuffer array = ByteBuffer.allocate(Byte.BYTES + Integer.BYTES + nestedTable.length);
+ array.put(AMQType.FIELD_TABLE.identifier());
+ array.putInt(nestedTable.length);
+ array.put(nestedTable);
+ return tableEntryWithCompoundValue(AMQType.FIELD_ARRAY, array.array());
+ }
+
+ private static byte[] tableEntryWithLongStringLength(final int length)
+ {
+ return ByteBuffer.allocate(7)
+ .put((byte) 1)
+ .put((byte) 's')
+ .put(AMQType.LONG_STRING.identifier())
+ .putInt(length)
+ .array();
+ }
+
+ private static byte[] tableEntryWithCompoundValue(final AMQType type, final byte[] value)
+ {
+ return ByteBuffer.allocate(7 + value.length)
+ .put((byte) 1)
+ .put((byte) 'n')
+ .put(type.identifier())
+ .putInt(value.length)
+ .put(value)
+ .array();
+ }
+
+ private static byte[] compoundValue(final AMQType type, final byte[] value)
+ {
+ return ByteBuffer.allocate(5 + value.length)
+ .put(type.identifier())
+ .putInt(value.length)
+ .put(value)
+ .array();
+ }
+
+ private static FieldTable buildProgrammaticNestedTable(final int depth)
+ {
+ FieldTable fieldTable = FieldTableFactory.createFieldTable(Map.of("v", 42));
+ for (int currentDepth = 1; currentDepth < depth; currentDepth++)
+ {
+ fieldTable = FieldTableFactory.createFieldTable(Map.of("n", fieldTable));
+ }
+ return fieldTable;
+ }
+
private FieldTable buildMalformedFieldTable()
{
final QpidByteBuffer buf = QpidByteBuffer.allocate(1);
diff --git a/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/IncomingMessageTest.java b/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/IncomingMessageTest.java
new file mode 100644
index 0000000000..4aab09ee73
--- /dev/null
+++ b/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/IncomingMessageTest.java
@@ -0,0 +1,125 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ */
+package org.apache.qpid.server.protocol.v0_8;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import org.junit.jupiter.api.Test;
+
+import org.apache.qpid.server.bytebuffer.QpidByteBuffer;
+import org.apache.qpid.server.protocol.v0_8.transport.BasicContentHeaderProperties;
+import org.apache.qpid.server.protocol.v0_8.transport.ContentHeaderBody;
+import org.apache.qpid.server.protocol.v0_8.transport.MessagePublishInfo;
+import org.apache.qpid.test.utils.UnitTestBase;
+
+class IncomingMessageTest extends UnitTestBase
+{
+ @Test
+ void emptyContentBodyFramesAreCountedButNotRetained()
+ {
+ final IncomingMessage message = createMessage(1L);
+
+ try (final QpidByteBuffer emptyContent = QpidByteBuffer.wrap(new byte[0]))
+ {
+ for (int i = 0; i < 100_000; i++)
+ {
+ message.addContentBodyFrame(emptyContent);
+ }
+ }
+
+ assertEquals(100_000, message.getContentBodyFrameCount());
+ assertEquals(0, message.getContentChunkCount());
+ assertFalse(message.allContentReceived());
+ message.getContentHeader().dispose();
+ }
+
+ @Test
+ void nonEmptyContentBodyFrameIsRetainedOnce()
+ {
+ final IncomingMessage message = createMessage(1L);
+
+ try (final QpidByteBuffer content = QpidByteBuffer.wrap(new byte[]{42}))
+ {
+ assertTrue(message.addContentBodyFrame(content));
+ }
+
+ assertEquals(1, message.getContentBodyFrameCount());
+ assertEquals(1, message.getContentChunkCount());
+ assertTrue(message.allContentReceived());
+
+ final QpidByteBuffer retainedContent = message.getContentChunk(0);
+ assertEquals((byte) 42, retainedContent.get());
+ retainedContent.dispose();
+ message.getContentHeader().dispose();
+ }
+
+ @Test
+ void contentBodyFrameExceedingDeclaredSizeIsNotRetained()
+ {
+ final IncomingMessage message = createMessage(1L);
+
+ try (final QpidByteBuffer content = QpidByteBuffer.wrap(new byte[2]))
+ {
+ assertFalse(message.addContentBodyFrame(content));
+ }
+
+ assertEquals(0, message.getContentBodyFrameCount());
+ assertEquals(0, message.getContentChunkCount());
+ assertFalse(message.allContentReceived());
+ message.getContentHeader().dispose();
+ }
+
+ @Test
+ void disposeReleasesHeaderAndRetainedContent()
+ {
+ final IncomingMessage message = new IncomingMessage(mock(MessagePublishInfo.class));
+ final ContentHeaderBody contentHeader = mock(ContentHeaderBody.class);
+ when(contentHeader.getBodySize()).thenReturn(1L);
+ message.setContentHeaderBody(contentHeader);
+ final QpidByteBuffer content = mock(QpidByteBuffer.class);
+ final QpidByteBuffer retainedContent = mock(QpidByteBuffer.class);
+ when(content.remaining()).thenReturn(1);
+ when(content.duplicate()).thenReturn(retainedContent);
+
+ assertTrue(message.addContentBodyFrame(content));
+
+ message.dispose();
+ message.dispose();
+
+ verify(contentHeader).dispose();
+ verify(retainedContent).dispose();
+ assertNull(message.getContentHeader());
+ assertEquals(0, message.getContentChunkCount());
+ }
+
+ private IncomingMessage createMessage(final long bodySize)
+ {
+ final IncomingMessage message = new IncomingMessage(mock(MessagePublishInfo.class));
+ message.setContentHeaderBody(new ContentHeaderBody(new BasicContentHeaderProperties(), bodySize));
+ return message;
+ }
+}
diff --git a/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/ServerDecoderTest.java b/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/ServerDecoderTest.java
new file mode 100644
index 0000000000..261dbcd886
--- /dev/null
+++ b/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/ServerDecoderTest.java
@@ -0,0 +1,1103 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ */
+package org.apache.qpid.server.protocol.v0_8;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InOrder;
+
+import org.apache.qpid.server.bytebuffer.QpidByteBuffer;
+import org.apache.qpid.server.protocol.ErrorCodes;
+import org.apache.qpid.server.protocol.ProtocolVersion;
+import org.apache.qpid.server.protocol.v0_8.transport.BasicAckBody;
+import org.apache.qpid.server.protocol.v0_8.transport.BasicPublishBody;
+import org.apache.qpid.server.protocol.v0_8.transport.BasicQosBody;
+import org.apache.qpid.server.protocol.v0_8.transport.ConnectionSecureOkBody;
+import org.apache.qpid.server.protocol.v0_8.transport.ConnectionStartOkBody;
+import org.apache.qpid.server.protocol.v0_8.transport.ConnectionTuneOkBody;
+import org.apache.qpid.server.protocol.v0_8.transport.ContentBody;
+import org.apache.qpid.server.protocol.v0_8.transport.HeartbeatBody;
+import org.apache.qpid.server.protocol.v0_8.transport.QueueDeclareBody;
+import org.apache.qpid.server.protocol.v0_8.transport.ServerChannelMethodProcessor;
+import org.apache.qpid.server.protocol.v0_8.transport.ServerMethodProcessor;
+import org.apache.qpid.server.protocol.v0_8.transport.TxSelectBody;
+import org.apache.qpid.server.store.StoreException;
+import org.apache.qpid.server.util.ConnectionScopedRuntimeException;
+import org.apache.qpid.server.util.ServerScopedRuntimeException;
+import org.apache.qpid.test.utils.UnitTestBase;
+
+class ServerDecoderTest extends UnitTestBase
+{
+ private static final byte FRAME_END = (byte) 0xCE;
+ private static final int CHANNEL_ID = 1;
+
+ @ParameterizedTest
+ @MethodSource("rejectedFrameReadVariants")
+ void testRejectedFrameIsConsumedBeforeCloseOk(final ProtocolVersion protocolVersion,
+ final int classAndMethod,
+ final int firstReadLength,
+ final boolean direct) throws Exception
+ {
+ final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor = createMethodProcessor();
+ when(methodProcessor.getProtocolVersion()).thenReturn(protocolVersion);
+ final ServerDecoder decoder = createRejectingDecoder(methodProcessor);
+ decoder.setExpectProtocolInitiation(false);
+ final int frameSize = AMQDecoder.FRAME_HEADER_SIZE + Integer.BYTES + 1;
+
+ try (final QpidByteBuffer frames = QpidByteBuffer.allocate(frameSize * 2);
+ final QpidByteBuffer buffer = QpidByteBuffer.allocate(direct, frameSize * 2))
+ {
+ writeFrameHeader(frames, (byte) 1, CHANNEL_ID, Integer.BYTES);
+ frames.putInt((TxSelectBody.CLASS_ID << 16) | TxSelectBody.METHOD_ID);
+ frames.put(FRAME_END);
+ writeFrameHeader(frames, (byte) 1, 0, Integer.BYTES);
+ frames.putInt(classAndMethod);
+ frames.put(FRAME_END);
+ frames.flip();
+ final byte[] input = new byte[frames.remaining()];
+ frames.get(input);
+
+ buffer.put(input, 0, firstReadLength);
+ buffer.flip();
+ if (firstReadLength < frameSize)
+ {
+ decoder.decodeBuffer(buffer);
+ assertEquals(0, buffer.position(), "Incomplete frames must remain buffered");
+ buffer.compact();
+ buffer.put(input, firstReadLength, input.length - firstReadLength);
+ buffer.flip();
+ }
+
+ assertThrows(AMQFrameDecodingException.class, () -> decoder.decodeBuffer(buffer));
+ assertEquals(frameSize, buffer.position(), "Rejected frame must be completely consumed");
+ when(methodProcessor.ignoreAllButCloseOk()).thenReturn(true);
+
+ if (firstReadLength >= frameSize && firstReadLength < input.length)
+ {
+ buffer.compact();
+ buffer.put(input, firstReadLength, input.length - firstReadLength);
+ buffer.flip();
+ }
+
+ decoder.decodeBuffer(buffer);
+ assertFalse(buffer.hasRemaining());
+ verify(methodProcessor).receiveConnectionCloseOk();
+ }
+ }
+
+ @Test
+ void fieldLengthCannotConsumeFollowingFrame() throws Exception
+ {
+ final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor = createMethodProcessor();
+ final ServerDecoder decoder = new ServerDecoder(methodProcessor);
+ decoder.setExpectProtocolInitiation(false);
+ final AMQFrameDecodingException exception;
+
+ try (final QpidByteBuffer buffer = QpidByteBuffer.allocate(24))
+ {
+ writeFrameHeader(buffer, (byte) 1, 0, 8);
+ buffer.putInt((ConnectionSecureOkBody.CLASS_ID << 16) | ConnectionSecureOkBody.METHOD_ID);
+ buffer.putUnsignedInt(4L);
+ buffer.put(FRAME_END);
+
+ writeFrameHeader(buffer, HeartbeatBody.TYPE, 0, 0);
+ buffer.put(FRAME_END);
+ buffer.flip();
+
+ exception = assertThrows(AMQFrameDecodingException.class, () -> decoder.decodeBuffer(buffer));
+ }
+
+ assertEquals(ErrorCodes.FRAME_ERROR, exception.getErrorCode());
+ assertEquals(ConnectionSecureOkBody.CLASS_ID, exception.getClassId());
+ assertEquals(ConnectionSecureOkBody.METHOD_ID, exception.getMethodId());
+ verify(methodProcessor, never()).receiveConnectionSecureOk(any(byte[].class));
+ }
+
+ @Test
+ void connectionClassMethodOnNonZeroChannelIsRejectedBeforeDispatch() throws Exception
+ {
+ final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor = createMethodProcessor();
+ final ServerDecoder decoder = new ServerDecoder(methodProcessor);
+ decoder.setExpectProtocolInitiation(false);
+ final int bodySize = Integer.BYTES + Short.BYTES + Integer.BYTES + Short.BYTES;
+ final AMQFrameDecodingException exception;
+
+ try (final QpidByteBuffer buffer = QpidByteBuffer.allocate(AMQDecoder.FRAME_HEADER_SIZE + bodySize + 1))
+ {
+ writeFrameHeader(buffer, (byte) 1, CHANNEL_ID, bodySize);
+ buffer.putInt((ConnectionTuneOkBody.CLASS_ID << 16) | ConnectionTuneOkBody.METHOD_ID);
+ buffer.putUnsignedShort(0);
+ buffer.putUnsignedInt(AMQDecoder.FRAME_MIN_SIZE);
+ buffer.putUnsignedShort(0);
+ buffer.put(FRAME_END);
+ buffer.flip();
+
+ exception = assertThrows(AMQFrameDecodingException.class, () -> decoder.decodeBuffer(buffer));
+ }
+
+ assertEquals(ErrorCodes.COMMAND_INVALID, exception.getErrorCode());
+ assertEquals(ConnectionTuneOkBody.CLASS_ID, exception.getClassId());
+ assertEquals(ConnectionTuneOkBody.METHOD_ID, exception.getMethodId());
+ verify(methodProcessor, never()).receiveConnectionTuneOk(anyInt(), anyLong(), anyInt());
+ verify(methodProcessor, never()).setCurrentMethod(ConnectionTuneOkBody.CLASS_ID, ConnectionTuneOkBody.METHOD_ID);
+ verify(methodProcessor).setCurrentMethod(0, 0);
+ verify(methodProcessor, times(1)).setCurrentMethod(anyInt(), anyInt());
+ }
+
+ @Test
+ void truncatedMethodBodyIsReportedWithFailingMethodIdentifiers() throws Exception
+ {
+ final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor = createMethodProcessor();
+ final ServerDecoder decoder = new ServerDecoder(methodProcessor);
+ decoder.setExpectProtocolInitiation(false);
+ final AMQFrameDecodingException exception;
+
+ try (final QpidByteBuffer buffer = QpidByteBuffer.allocate(AMQDecoder.FRAME_HEADER_SIZE + Integer.BYTES + 1))
+ {
+ writeFrameHeader(buffer, (byte) 1, CHANNEL_ID, Integer.BYTES);
+ buffer.putInt((BasicQosBody.CLASS_ID << 16) | BasicQosBody.METHOD_ID);
+ buffer.put(FRAME_END);
+ buffer.flip();
+
+ exception = assertThrows(AMQFrameDecodingException.class, () -> decoder.decodeBuffer(buffer));
+ }
+
+ assertEquals(ErrorCodes.FRAME_ERROR, exception.getErrorCode());
+ assertEquals(BasicQosBody.CLASS_ID, exception.getClassId());
+ assertEquals(BasicQosBody.METHOD_ID, exception.getMethodId());
+ verify(methodProcessor, never()).getChannelMethodProcessor(CHANNEL_ID);
+ }
+
+ @ParameterizedTest
+ @MethodSource("heartbeatChannelErrorCodes")
+ void heartbeatOnNonZeroChannelIsRejectedBeforeDispatch(final ProtocolVersion protocolVersion,
+ final int expectedErrorCode) throws Exception
+ {
+ final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor = createMethodProcessor();
+ when(methodProcessor.getProtocolVersion()).thenReturn(protocolVersion);
+ final ServerDecoder decoder = new ServerDecoder(methodProcessor);
+ decoder.setExpectProtocolInitiation(false);
+ final AMQFrameDecodingException exception;
+
+ try (final QpidByteBuffer buffer = QpidByteBuffer.allocate(AMQDecoder.FRAME_HEADER_SIZE + 1))
+ {
+ writeHeartbeatFrame(buffer, CHANNEL_ID, new byte[0]);
+ buffer.flip();
+
+ exception = assertThrows(AMQFrameDecodingException.class, () -> decoder.decodeBuffer(buffer));
+ }
+
+ assertEquals(expectedErrorCode, exception.getErrorCode());
+ verify(methodProcessor, never()).receiveHeartbeat();
+ verify(methodProcessor).getProtocolVersion();
+ }
+
+ @Test
+ void amqp091HeartbeatWithPayloadIsRejectedBeforeDispatch() throws Exception
+ {
+ final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor = createMethodProcessor();
+ when(methodProcessor.getProtocolVersion()).thenReturn(ProtocolVersion.v0_91);
+ final ServerDecoder decoder = new ServerDecoder(methodProcessor);
+ decoder.setExpectProtocolInitiation(false);
+ final AMQFrameDecodingException exception;
+
+ try (final QpidByteBuffer buffer = QpidByteBuffer.allocate(AMQDecoder.FRAME_HEADER_SIZE + 2))
+ {
+ writeHeartbeatFrame(buffer, 0, new byte[] { 1 });
+ buffer.flip();
+
+ exception = assertThrows(AMQFrameDecodingException.class, () -> decoder.decodeBuffer(buffer));
+ }
+
+ assertEquals(ErrorCodes.FRAME_ERROR, exception.getErrorCode());
+ verify(methodProcessor, never()).receiveHeartbeat();
+ }
+
+ @ParameterizedTest
+ @MethodSource("legacyProtocolVersions")
+ void legacyHeartbeatWithPayloadRemainsCompatible(final ProtocolVersion protocolVersion) throws Exception
+ {
+ final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor = createMethodProcessor();
+ when(methodProcessor.getProtocolVersion()).thenReturn(protocolVersion);
+ final ServerDecoder decoder = new ServerDecoder(methodProcessor);
+ decoder.setExpectProtocolInitiation(false);
+
+ try (final QpidByteBuffer buffer = QpidByteBuffer.allocate(AMQDecoder.FRAME_HEADER_SIZE + 2))
+ {
+ writeHeartbeatFrame(buffer, 0, new byte[] { 1 });
+ buffer.flip();
+
+ decoder.decodeBuffer(buffer);
+ }
+
+ verify(methodProcessor).receiveHeartbeat();
+ }
+
+ @Test
+ void validHeartbeatDoesNotRequireProtocolVersionLookup() throws Exception
+ {
+ final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor = createMethodProcessor();
+ final ServerDecoder decoder = new ServerDecoder(methodProcessor);
+ decoder.setExpectProtocolInitiation(false);
+
+ try (final QpidByteBuffer buffer = QpidByteBuffer.allocate(AMQDecoder.FRAME_HEADER_SIZE + 1))
+ {
+ writeHeartbeatFrame(buffer, 0, new byte[0]);
+ buffer.flip();
+
+ decoder.decodeBuffer(buffer);
+ }
+
+ verify(methodProcessor).receiveHeartbeat();
+ verify(methodProcessor, never()).getProtocolVersion();
+ }
+
+ @Test
+ void malformedNestedFieldTableIsRejectedBeforeLiveConnectionAccess() throws Exception
+ {
+ final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor = createMethodProcessor();
+ final ServerDecoder decoder = new ServerDecoder(methodProcessor);
+ decoder.setExpectProtocolInitiation(false);
+ final byte[] fieldTable = malformedNestedFieldTable();
+ final int bodySize = Integer.BYTES + Integer.BYTES + fieldTable.length + 6 + Integer.BYTES + 6;
+ final AMQFrameDecodingException exception;
+
+ try (final QpidByteBuffer buffer = QpidByteBuffer.allocate(AMQDecoder.FRAME_HEADER_SIZE + bodySize + 1))
+ {
+ writeFrameHeader(buffer, (byte) 1, 0, bodySize);
+ buffer.putInt((ConnectionStartOkBody.CLASS_ID << 16) | ConnectionStartOkBody.METHOD_ID);
+ buffer.putUnsignedInt(fieldTable.length);
+ buffer.put(fieldTable);
+ writeShortString(buffer, "PLAIN");
+ buffer.putUnsignedInt(0);
+ writeShortString(buffer, "en_US");
+ buffer.put(FRAME_END);
+ buffer.flip();
+
+ exception = assertThrows(AMQFrameDecodingException.class, () -> decoder.decodeBuffer(buffer));
+ }
+
+ assertEquals(ErrorCodes.FRAME_ERROR, exception.getErrorCode());
+ assertEquals(ConnectionStartOkBody.CLASS_ID, exception.getClassId());
+ assertEquals(ConnectionStartOkBody.METHOD_ID, exception.getMethodId());
+ verify(methodProcessor, never()).ignoreAllButCloseOk();
+ verify(methodProcessor, never()).receiveConnectionStartOk(any(), any(), any(), any());
+ verify(methodProcessor, never()).setCurrentMethod(ConnectionStartOkBody.CLASS_ID, ConnectionStartOkBody.METHOD_ID);
+ verify(methodProcessor).setCurrentMethod(0, 0);
+ verify(methodProcessor, times(1)).setCurrentMethod(anyInt(), anyInt());
+ }
+
+ @Test
+ void malformedNestedQueueArgumentsAreRejectedBeforeChannelLookup() throws Exception
+ {
+ final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor = createMethodProcessor();
+ final ServerDecoder decoder = new ServerDecoder(methodProcessor);
+ decoder.setExpectProtocolInitiation(false);
+ final byte[] fieldTable = malformedNestedFieldTable();
+ final int bodySize = Integer.BYTES + Short.BYTES + 1 + 1 + Integer.BYTES + fieldTable.length;
+ final AMQFrameDecodingException exception;
+
+ try (final QpidByteBuffer buffer = QpidByteBuffer.allocate(AMQDecoder.FRAME_HEADER_SIZE + bodySize + 1))
+ {
+ writeFrameHeader(buffer, (byte) 1, CHANNEL_ID, bodySize);
+ buffer.putInt((QueueDeclareBody.CLASS_ID << 16) | QueueDeclareBody.METHOD_ID);
+ buffer.putUnsignedShort(0);
+ writeShortString(buffer, "");
+ buffer.put((byte) 1);
+ buffer.putUnsignedInt(fieldTable.length);
+ buffer.put(fieldTable);
+ buffer.put(FRAME_END);
+ buffer.flip();
+
+ exception = assertThrows(AMQFrameDecodingException.class, () -> decoder.decodeBuffer(buffer));
+ }
+
+ assertEquals(ErrorCodes.FRAME_ERROR, exception.getErrorCode());
+ assertEquals(QueueDeclareBody.CLASS_ID, exception.getClassId());
+ assertEquals(QueueDeclareBody.METHOD_ID, exception.getMethodId());
+ verify(methodProcessor, never()).getChannelMethodProcessor(CHANNEL_ID);
+ verify(methodProcessor, never()).setCurrentMethod(QueueDeclareBody.CLASS_ID, QueueDeclareBody.METHOD_ID);
+ verify(methodProcessor).setCurrentMethod(0, 0);
+ verify(methodProcessor, times(1)).setCurrentMethod(anyInt(), anyInt());
+ }
+
+ @Test
+ void configuredNestingDepthRejectsMethodBeforeDispatch() throws Exception
+ {
+ final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor = createMethodProcessor();
+ final ServerDecoder decoder = new ServerDecoder(methodProcessor, 1);
+ decoder.setExpectProtocolInitiation(false);
+ final byte[] fieldTable = nestedFieldTable(2);
+ final int bodySize = 24 + fieldTable.length;
+ final AMQFrameDecodingException exception;
+
+ try (final QpidByteBuffer buffer = QpidByteBuffer.allocate(AMQDecoder.FRAME_HEADER_SIZE + bodySize + 1))
+ {
+ writeConnectionStartOkFrame(buffer, fieldTable, bodySize);
+ buffer.flip();
+
+ exception = assertThrows(AMQFrameDecodingException.class, () -> decoder.decodeBuffer(buffer));
+ }
+
+ assertEquals(ErrorCodes.RESOURCE_ERROR, exception.getErrorCode());
+ assertEquals(ConnectionStartOkBody.CLASS_ID, exception.getClassId());
+ assertEquals(ConnectionStartOkBody.METHOD_ID, exception.getMethodId());
+ verify(methodProcessor, never()).receiveConnectionStartOk(any(), any(), any(), any());
+ verify(methodProcessor, never()).setCurrentMethod(ConnectionStartOkBody.CLASS_ID,
+ ConnectionStartOkBody.METHOD_ID);
+ verify(methodProcessor).setCurrentMethod(0, 0);
+ }
+
+ @Test
+ void configuredNestingDepthAcceptsMethodAtLimit() throws Exception
+ {
+ final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor = createMethodProcessor();
+ final ServerDecoder decoder = new ServerDecoder(methodProcessor, 2);
+ decoder.setExpectProtocolInitiation(false);
+ final byte[] fieldTable = nestedFieldTable(2);
+ final int bodySize = 24 + fieldTable.length;
+
+ try (final QpidByteBuffer buffer = QpidByteBuffer.allocate(AMQDecoder.FRAME_HEADER_SIZE + bodySize + 1))
+ {
+ writeConnectionStartOkFrame(buffer, fieldTable, bodySize);
+ buffer.flip();
+
+ decoder.decodeBuffer(buffer);
+ }
+
+ verify(methodProcessor).receiveConnectionStartOk(any(), any(), any(), any());
+ verify(methodProcessor).setCurrentMethod(ConnectionStartOkBody.CLASS_ID, ConnectionStartOkBody.METHOD_ID);
+ verify(methodProcessor).setCurrentMethod(0, 0);
+ }
+
+ @Test
+ void brokerDecoderUsesProtocolSpecificNestingContext() throws Exception
+ {
+ final AMQPConnection_0_8Impl connection = mock(AMQPConnection_0_8Impl.class);
+ when(connection.getContextValue(Integer.class, AMQPConnection_0_8.CODEC_MAX_NESTED_OBJECTS))
+ .thenReturn(1);
+ final BrokerDecoder decoder = new BrokerDecoder(connection);
+ decoder.setExpectProtocolInitiation(false);
+ final byte[] fieldTable = nestedFieldTable(2);
+ final int bodySize = 24 + fieldTable.length;
+ final AMQFrameDecodingException exception;
+
+ try (final QpidByteBuffer buffer = QpidByteBuffer.allocate(AMQDecoder.FRAME_HEADER_SIZE + bodySize + 1))
+ {
+ writeConnectionStartOkFrame(buffer, fieldTable, bodySize);
+ buffer.flip();
+
+ exception = assertThrows(AMQFrameDecodingException.class, () -> decoder.decodeBuffer(buffer));
+ }
+
+ assertEquals(ErrorCodes.RESOURCE_ERROR, exception.getErrorCode());
+ assertEquals(ConnectionStartOkBody.CLASS_ID, exception.getClassId());
+ assertEquals(ConnectionStartOkBody.METHOD_ID, exception.getMethodId());
+ verify(connection).getContextValue(Integer.class, AMQPConnection_0_8.CODEC_MAX_NESTED_OBJECTS);
+ verify(connection, never()).receiveConnectionStartOk(any(), any(), any(), any());
+ }
+
+ @Test
+ void negativeProtocolSpecificNestingContextIsRejected()
+ {
+ final AMQPConnection_0_8Impl connection = mock(AMQPConnection_0_8Impl.class);
+ when(connection.getContextValue(Integer.class, AMQPConnection_0_8.CODEC_MAX_NESTED_OBJECTS))
+ .thenReturn(-1);
+
+ assertThrows(IllegalArgumentException.class, () -> new BrokerDecoder(connection));
+ }
+
+ @Test
+ void methodFrameWithTrailingBodyBytesIsRejected() throws Exception
+ {
+ final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor = createMethodProcessor();
+ final ServerChannelMethodProcessor channelMethodProcessor = mock(ServerChannelMethodProcessor.class);
+ when(methodProcessor.getChannelMethodProcessor(CHANNEL_ID)).thenReturn(channelMethodProcessor);
+
+ final ServerDecoder decoder = new ServerDecoder(methodProcessor);
+ decoder.setExpectProtocolInitiation(false);
+
+ try (final QpidByteBuffer buffer = QpidByteBuffer.allocate(13))
+ {
+ writeFrameHeader(buffer, (byte) 1, CHANNEL_ID, 5);
+ buffer.putInt((TxSelectBody.CLASS_ID << 16) | TxSelectBody.METHOD_ID);
+ buffer.put(FRAME_END);
+ buffer.put(FRAME_END);
+ buffer.flip();
+
+ assertThrows(AMQFrameDecodingException.class, () -> decoder.decodeBuffer(buffer));
+ }
+
+ verify(channelMethodProcessor, never()).receiveTxSelect();
+ verify(methodProcessor, never()).getChannelMethodProcessor(CHANNEL_ID);
+ verify(methodProcessor, never()).setCurrentMethod(TxSelectBody.CLASS_ID, TxSelectBody.METHOD_ID);
+ verify(methodProcessor).setCurrentMethod(0, 0);
+ verify(methodProcessor, times(1)).setCurrentMethod(anyInt(), anyInt());
+ }
+
+ @Test
+ void methodFrameWithInvalidEndMarkerIsRejectedBeforeDispatch() throws Exception
+ {
+ final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor = createMethodProcessor();
+ final ServerChannelMethodProcessor channelMethodProcessor = mock(ServerChannelMethodProcessor.class);
+ when(methodProcessor.getChannelMethodProcessor(CHANNEL_ID)).thenReturn(channelMethodProcessor);
+
+ final ServerDecoder decoder = new ServerDecoder(methodProcessor);
+ decoder.setExpectProtocolInitiation(false);
+
+ try (final QpidByteBuffer buffer = QpidByteBuffer.allocate(12))
+ {
+ writeFrameHeader(buffer, (byte) 1, CHANNEL_ID, 4);
+ buffer.putInt((TxSelectBody.CLASS_ID << 16) | TxSelectBody.METHOD_ID);
+ buffer.put((byte) 0);
+ buffer.flip();
+
+ assertThrows(AMQFatalFrameDecodingException.class, () -> decoder.decodeBuffer(buffer));
+ }
+
+ verify(channelMethodProcessor, never()).receiveTxSelect();
+ verify(methodProcessor, never()).getChannelMethodProcessor(CHANNEL_ID);
+ }
+
+ @Test
+ void unsupportedFrameTypeIsFatal() throws Exception
+ {
+ final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor = createMethodProcessor();
+ final ServerDecoder decoder = new ServerDecoder(methodProcessor);
+ decoder.setExpectProtocolInitiation(false);
+
+ try (final QpidByteBuffer buffer = QpidByteBuffer.allocate(2 * (AMQDecoder.FRAME_HEADER_SIZE + 1)))
+ {
+ writeFrameHeader(buffer, HeartbeatBody.TYPE, 0, 0);
+ buffer.put(FRAME_END);
+ writeFrameHeader(buffer, Byte.MAX_VALUE, 0, 0);
+ buffer.put(FRAME_END);
+ buffer.flip();
+
+ assertThrows(AMQFatalFrameDecodingException.class, () -> decoder.decodeBuffer(buffer));
+ }
+
+ verify(methodProcessor).receiveHeartbeat();
+ }
+
+ @Test
+ void validMethodFrameIsDispatchedAfterValidation() throws Exception
+ {
+ final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor = createMethodProcessor();
+ final ServerChannelMethodProcessor channelMethodProcessor = mock(ServerChannelMethodProcessor.class);
+ when(methodProcessor.getChannelMethodProcessor(CHANNEL_ID)).thenReturn(channelMethodProcessor);
+
+ final ServerDecoder decoder = new ServerDecoder(methodProcessor);
+ decoder.setExpectProtocolInitiation(false);
+
+ try (final QpidByteBuffer buffer = QpidByteBuffer.allocate(12))
+ {
+ writeFrameHeader(buffer, (byte) 1, CHANNEL_ID, 4);
+ buffer.putInt((TxSelectBody.CLASS_ID << 16) | TxSelectBody.METHOD_ID);
+ buffer.put(FRAME_END);
+ buffer.flip();
+
+ decoder.decodeBuffer(buffer);
+ }
+
+ final InOrder inOrder = inOrder(methodProcessor, channelMethodProcessor);
+ inOrder.verify(methodProcessor).setCurrentMethod(TxSelectBody.CLASS_ID, TxSelectBody.METHOD_ID);
+ inOrder.verify(methodProcessor).getChannelMethodProcessor(CHANNEL_ID);
+ inOrder.verify(channelMethodProcessor).rejectMethodFrameIfContentIncomplete();
+ inOrder.verify(channelMethodProcessor).receiveTxSelect();
+ inOrder.verify(methodProcessor).setCurrentMethod(0, 0);
+ }
+
+ @Test
+ void methodFrameIsNotDispatchedWhenContentSequenceIsIncomplete() throws Exception
+ {
+ final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor = createMethodProcessor();
+ final ServerChannelMethodProcessor channelMethodProcessor = mock(ServerChannelMethodProcessor.class);
+ when(channelMethodProcessor.rejectMethodFrameIfContentIncomplete()).thenReturn(true);
+ when(methodProcessor.getChannelMethodProcessor(CHANNEL_ID)).thenReturn(channelMethodProcessor);
+
+ final ServerDecoder decoder = new ServerDecoder(methodProcessor);
+ decoder.setExpectProtocolInitiation(false);
+
+ final int bodySize = Integer.BYTES + Short.BYTES + 1 + 1 + Integer.BYTES;
+ try (final QpidByteBuffer buffer = QpidByteBuffer.allocate(AMQDecoder.FRAME_HEADER_SIZE + bodySize + 1))
+ {
+ writeFrameHeader(buffer, (byte) 1, CHANNEL_ID, bodySize);
+ buffer.putInt((QueueDeclareBody.CLASS_ID << 16) | QueueDeclareBody.METHOD_ID);
+ buffer.putUnsignedShort(0);
+ writeShortString(buffer, "");
+ buffer.put((byte) 0);
+ buffer.putUnsignedInt(0);
+ buffer.put(FRAME_END);
+ buffer.flip();
+
+ decoder.decodeBuffer(buffer);
+ }
+
+ final InOrder inOrder = inOrder(methodProcessor, channelMethodProcessor);
+ inOrder.verify(methodProcessor).setCurrentMethod(QueueDeclareBody.CLASS_ID, QueueDeclareBody.METHOD_ID);
+ inOrder.verify(methodProcessor).getChannelMethodProcessor(CHANNEL_ID);
+ inOrder.verify(channelMethodProcessor).rejectMethodFrameIfContentIncomplete();
+ inOrder.verify(methodProcessor).setCurrentMethod(0, 0);
+ verify(channelMethodProcessor, never()).receiveQueueDeclare(any(), anyBoolean(), anyBoolean(), anyBoolean(),
+ anyBoolean(), anyBoolean(), any());
+ }
+
+ @Test
+ void generatedChannelMethodWithTrailingBodyBytesIsRejectedBeforeChannelLookup() throws Exception
+ {
+ final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor = createMethodProcessor();
+ final ServerDecoder decoder = new ServerDecoder(methodProcessor);
+ decoder.setExpectProtocolInitiation(false);
+
+ try (final QpidByteBuffer buffer = QpidByteBuffer.allocate(22))
+ {
+ writeFrameHeader(buffer, (byte) 1, CHANNEL_ID, 14);
+ buffer.putInt((BasicAckBody.CLASS_ID << 16) | BasicAckBody.METHOD_ID);
+ buffer.putLong(42L);
+ buffer.put((byte) 0);
+ buffer.put((byte) 1);
+ buffer.put(FRAME_END);
+ buffer.flip();
+
+ assertThrows(AMQFrameDecodingException.class, () -> decoder.decodeBuffer(buffer));
+ }
+
+ verify(methodProcessor, never()).getChannelMethodProcessor(CHANNEL_ID);
+ }
+
+ @Test
+ void directConnectionMethodWithTrailingBodyBytesIsRejectedBeforeDispatch() throws Exception
+ {
+ final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor = createMethodProcessor();
+ when(methodProcessor.getProtocolVersion()).thenReturn(ProtocolVersion.v0_91);
+ final ServerDecoder decoder = new ServerDecoder(methodProcessor);
+ decoder.setExpectProtocolInitiation(false);
+
+ try (final QpidByteBuffer buffer = QpidByteBuffer.allocate(13))
+ {
+ writeFrameHeader(buffer, (byte) 1, 0, 5);
+ buffer.putInt(0x000a0033);
+ buffer.put((byte) 1);
+ buffer.put(FRAME_END);
+ buffer.flip();
+
+ assertThrows(AMQFrameDecodingException.class, () -> decoder.decodeBuffer(buffer));
+ }
+
+ verify(methodProcessor, never()).receiveConnectionCloseOk();
+ }
+
+ @Test
+ void validUnknownMethodPublishesMetadataForCloseReporting() throws Exception
+ {
+ final int classId = 0x7FFF;
+ final int methodId = 1;
+ final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor = createMethodProcessor();
+ final ServerDecoder decoder = new ServerDecoder(methodProcessor);
+ decoder.setExpectProtocolInitiation(false);
+ final AMQFrameDecodingException exception;
+
+ try (final QpidByteBuffer buffer = QpidByteBuffer.allocate(12))
+ {
+ writeFrameHeader(buffer, (byte) 1, 0, Integer.BYTES);
+ buffer.putInt((classId << 16) | methodId);
+ buffer.put(FRAME_END);
+ buffer.flip();
+
+ exception = assertThrows(AMQFrameDecodingException.class, () -> decoder.decodeBuffer(buffer));
+ }
+
+ assertEquals(ErrorCodes.COMMAND_INVALID, exception.getErrorCode());
+ assertEquals(classId, exception.getClassId());
+ assertEquals(methodId, exception.getMethodId());
+ final InOrder inOrder = inOrder(methodProcessor);
+ inOrder.verify(methodProcessor).setCurrentMethod(classId, methodId);
+ inOrder.verify(methodProcessor).setCurrentMethod(0, 0);
+ }
+
+ @Test
+ void callbackFailureStillResetsCurrentMethod() throws Exception
+ {
+ final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor = createMethodProcessor();
+ final ServerChannelMethodProcessor channelMethodProcessor = mock(ServerChannelMethodProcessor.class);
+ when(methodProcessor.getChannelMethodProcessor(CHANNEL_ID)).thenReturn(channelMethodProcessor);
+ doThrow(new IllegalStateException("callback failed")).when(channelMethodProcessor).receiveTxSelect();
+ final ServerDecoder decoder = new ServerDecoder(methodProcessor);
+ decoder.setExpectProtocolInitiation(false);
+
+ try (final QpidByteBuffer buffer = QpidByteBuffer.allocate(12))
+ {
+ writeFrameHeader(buffer, (byte) 1, CHANNEL_ID, Integer.BYTES);
+ buffer.putInt((TxSelectBody.CLASS_ID << 16) | TxSelectBody.METHOD_ID);
+ buffer.put(FRAME_END);
+ buffer.flip();
+
+ assertThrows(IllegalStateException.class, () -> decoder.decodeBuffer(buffer));
+ assertFalse(buffer.hasRemaining());
+ }
+
+ verify(methodProcessor).setCurrentMethod(TxSelectBody.CLASS_ID, TxSelectBody.METHOD_ID);
+ verify(methodProcessor).setCurrentMethod(0, 0);
+ }
+
+ @ParameterizedTest
+ @MethodSource("decodingRuntimeFailures")
+ void runtimeFailureFromCallbackIsNotReportedAsMalformedWireData(final RuntimeException failure) throws Exception
+ {
+ final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor = createMethodProcessor();
+ final ServerChannelMethodProcessor channelMethodProcessor = mock(ServerChannelMethodProcessor.class);
+ when(methodProcessor.getChannelMethodProcessor(CHANNEL_ID)).thenReturn(channelMethodProcessor);
+ doThrow(failure).when(channelMethodProcessor).receiveTxSelect();
+ final ServerDecoder decoder = new ServerDecoder(methodProcessor);
+ decoder.setExpectProtocolInitiation(false);
+
+ try (final QpidByteBuffer buffer = QpidByteBuffer.allocate(12))
+ {
+ writeFrameHeader(buffer, (byte) 1, CHANNEL_ID, Integer.BYTES);
+ buffer.putInt((TxSelectBody.CLASS_ID << 16) | TxSelectBody.METHOD_ID);
+ buffer.put(FRAME_END);
+ buffer.flip();
+
+ assertSame(failure, assertThrows(failure.getClass(), () -> decoder.decodeBuffer(buffer)));
+ assertFalse(buffer.hasRemaining());
+ }
+
+ verify(methodProcessor).setCurrentMethod(TxSelectBody.CLASS_ID, TxSelectBody.METHOD_ID);
+ verify(methodProcessor).setCurrentMethod(0, 0);
+ }
+
+ @Test
+ void runtimeFailureFromProtocolVersionLookupIsNotReportedAsMalformedWireData() throws Exception
+ {
+ final int classId = 0x7FFF;
+ final int methodId = 1;
+ final IllegalArgumentException failure = new IllegalArgumentException("Protocol version unavailable");
+ final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor = createMethodProcessor();
+ when(methodProcessor.getProtocolVersion()).thenThrow(failure);
+ final ServerDecoder decoder = new ServerDecoder(methodProcessor);
+ decoder.setExpectProtocolInitiation(false);
+
+ try (final QpidByteBuffer buffer = QpidByteBuffer.allocate(12))
+ {
+ writeFrameHeader(buffer, (byte) 1, 0, Integer.BYTES);
+ buffer.putInt((classId << 16) | methodId);
+ buffer.put(FRAME_END);
+ buffer.flip();
+
+ final ConnectionScopedRuntimeException exception = assertThrows(ConnectionScopedRuntimeException.class,
+ () -> decoder.decodeBuffer(buffer));
+ assertSame(failure, exception.getCause());
+ assertFalse(buffer.hasRemaining());
+ }
+
+ verify(methodProcessor).setCurrentMethod(0, 0);
+ verify(methodProcessor, times(1)).setCurrentMethod(anyInt(), anyInt());
+ }
+
+ @ParameterizedTest
+ @MethodSource("protocolVersionsAndConnectionCloseOkMethods")
+ void validVersionSpecificConnectionCloseOkIsDispatched(final ProtocolVersion protocolVersion,
+ final int classAndMethod) throws Exception
+ {
+ final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor = createMethodProcessor();
+ when(methodProcessor.getProtocolVersion()).thenReturn(protocolVersion);
+ final ServerDecoder decoder = new ServerDecoder(methodProcessor);
+ decoder.setExpectProtocolInitiation(false);
+
+ try (final QpidByteBuffer buffer = QpidByteBuffer.allocate(12))
+ {
+ writeFrameHeader(buffer, (byte) 1, 0, 4);
+ buffer.putInt(classAndMethod);
+ buffer.put(FRAME_END);
+ buffer.flip();
+
+ decoder.decodeBuffer(buffer);
+ }
+
+ verify(methodProcessor).receiveConnectionCloseOk();
+ verify(methodProcessor).setCurrentMethod(classAndMethod >> 16, classAndMethod & 0xFFFF);
+ verify(methodProcessor).setCurrentMethod(0, 0);
+ }
+
+ @Test
+ void validBasicPublishIsDispatchedWithDecodedArguments() throws Exception
+ {
+ final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor = createMethodProcessor();
+ final ServerChannelMethodProcessor channelMethodProcessor = mock(ServerChannelMethodProcessor.class);
+ when(methodProcessor.getChannelMethodProcessor(CHANNEL_ID)).thenReturn(channelMethodProcessor);
+
+ final ServerDecoder decoder = new ServerDecoder(methodProcessor);
+ decoder.setExpectProtocolInitiation(false);
+
+ try (final QpidByteBuffer buffer = QpidByteBuffer.allocate(21))
+ {
+ writeFrameHeader(buffer, (byte) 1, CHANNEL_ID, 13);
+ buffer.putInt((BasicPublishBody.CLASS_ID << 16) | BasicPublishBody.METHOD_ID);
+ buffer.putUnsignedShort(0);
+ writeShortString(buffer, "ex");
+ writeShortString(buffer, "rk");
+ buffer.put((byte) 1);
+ buffer.put(FRAME_END);
+ buffer.flip();
+
+ decoder.decodeBuffer(buffer);
+ }
+
+ verify(channelMethodProcessor).receiveBasicPublish(AMQShortString.valueOf("ex"),
+ AMQShortString.valueOf("rk"), true, false);
+ verify(channelMethodProcessor).rejectMethodFrameIfContentIncomplete();
+ verify(methodProcessor, times(1)).getChannelMethodProcessor(CHANNEL_ID);
+ }
+
+ @Test
+ void validConnectionMethodRetainsDecodedArgumentsUntilDispatch() throws Exception
+ {
+ final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor = createMethodProcessor();
+ final ServerDecoder decoder = new ServerDecoder(methodProcessor);
+ decoder.setExpectProtocolInitiation(false);
+
+ try (final QpidByteBuffer buffer = QpidByteBuffer.allocate(18))
+ {
+ writeFrameHeader(buffer, (byte) 1, 0, 10);
+ buffer.putInt((ConnectionSecureOkBody.CLASS_ID << 16) | ConnectionSecureOkBody.METHOD_ID);
+ buffer.putUnsignedInt(2);
+ buffer.put((byte) 1);
+ buffer.put((byte) 2);
+ buffer.put(FRAME_END);
+ buffer.flip();
+
+ decoder.decodeBuffer(buffer);
+ }
+
+ final ArgumentCaptor<byte[]> response = ArgumentCaptor.forClass(byte[].class);
+ verify(methodProcessor).receiveConnectionSecureOk(response.capture());
+ assertArrayEquals(new byte[]{1, 2}, response.getValue());
+ }
+
+ @Test
+ void deferredMethodHonoursCloseOnlyMode() throws Exception
+ {
+ final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor = createMethodProcessor();
+ final ServerChannelMethodProcessor channelMethodProcessor = mock(ServerChannelMethodProcessor.class);
+ when(channelMethodProcessor.ignoreAllButCloseOk()).thenReturn(true);
+ when(methodProcessor.getChannelMethodProcessor(CHANNEL_ID)).thenReturn(channelMethodProcessor);
+
+ final ServerDecoder decoder = new ServerDecoder(methodProcessor);
+ decoder.setExpectProtocolInitiation(false);
+
+ try (final QpidByteBuffer buffer = QpidByteBuffer.allocate(12))
+ {
+ writeFrameHeader(buffer, (byte) 1, CHANNEL_ID, 4);
+ buffer.putInt((TxSelectBody.CLASS_ID << 16) | TxSelectBody.METHOD_ID);
+ buffer.put(FRAME_END);
+ buffer.flip();
+
+ decoder.decodeBuffer(buffer);
+ }
+
+ verify(channelMethodProcessor, never()).receiveTxSelect();
+ }
+
+ @ParameterizedTest
+ @ValueSource(longs = {2147483648L, 4294967295L})
+ void testDiscardPreservesUnsignedFrameSize(final long bodySize) throws Exception
+ {
+ final ServerDecoder decoder = new ServerDecoder(createMethodProcessor());
+ decoder.setExpectProtocolInitiation(false);
+ try (final QpidByteBuffer buffer = QpidByteBuffer.allocate(32))
+ {
+ writeFrameHeader(buffer, ContentBody.TYPE, CHANNEL_ID, bodySize);
+ buffer.flip();
+
+ assertThrows(AMQFrameDecodingException.class, () -> decoder.decodeBuffer(buffer));
+ assertFalse(buffer.hasRemaining());
+
+ buffer.clear();
+ buffer.put(new byte[buffer.remaining()]);
+ buffer.flip();
+ decoder.decodeBuffer(buffer);
+
+ assertFalse(buffer.hasRemaining(), "Discard must consume the fragment without retaining its payload");
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
+ void testDiscardedFrameEndIsValidated(final boolean fragmented) throws Exception
+ {
+ final ServerDecoder decoder = new ServerDecoder(createMethodProcessor());
+ decoder.setExpectProtocolInitiation(false);
+ decoder.setMaxFrameSize(16);
+ final int bodySize = 17;
+ try (final QpidByteBuffer buffer = QpidByteBuffer.allocate(AMQDecoder.FRAME_HEADER_SIZE + bodySize + 1))
+ {
+ writeFrameHeader(buffer, ContentBody.TYPE, CHANNEL_ID, bodySize);
+ buffer.put(new byte[bodySize]);
+ buffer.put((byte) 0);
+ buffer.flip();
+
+ assertThrows(AMQFrameDecodingException.class, () -> decoder.decodeBuffer(buffer));
+ if (fragmented)
+ {
+ buffer.limit(buffer.limit() - 1);
+ decoder.decodeBuffer(buffer);
+ assertFalse(buffer.hasRemaining());
+ buffer.limit(buffer.limit() + 1);
+ }
+ assertThrows(AMQFatalFrameDecodingException.class, () -> decoder.decodeBuffer(buffer));
+ assertFalse(buffer.hasRemaining());
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("decodingRuntimeFailures")
+ void testRuntimeDecodingFailureRetainsMethodContext(final RuntimeException failure) throws Exception
+ {
+ final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor = createMethodProcessor();
+ final ServerChannelMethodProcessor channelMethodProcessor = mock(ServerChannelMethodProcessor.class);
+ when(methodProcessor.getChannelMethodProcessor(CHANNEL_ID)).thenReturn(channelMethodProcessor);
+ final ServerDecoder decoder = new ServerDecoder(methodProcessor);
+ final QpidByteBuffer input = createFailingMethodBody(failure);
+
+ final AMQFrameDecodingException exception =
+ assertThrows(AMQFrameDecodingException.class, () -> decoder.processMethod(CHANNEL_ID, input));
+ assertSame(failure, exception.getCause());
+ assertEquals(ErrorCodes.FRAME_ERROR, exception.getErrorCode());
+ assertEquals(BasicQosBody.CLASS_ID, exception.getClassId());
+ assertEquals(BasicQosBody.METHOD_ID, exception.getMethodId());
+ verify(methodProcessor).setCurrentMethod(0, 0);
+
+ decoder.setExpectProtocolInitiation(false);
+ try (final QpidByteBuffer buffer = QpidByteBuffer.allocate(12))
+ {
+ writeFrameHeader(buffer, (byte) 1, CHANNEL_ID, Integer.BYTES);
+ buffer.putInt((TxSelectBody.CLASS_ID << 16) | TxSelectBody.METHOD_ID);
+ buffer.put(FRAME_END);
+ buffer.flip();
+ decoder.decodeBuffer(buffer);
+ assertFalse(buffer.hasRemaining());
+ }
+ verify(channelMethodProcessor).receiveTxSelect();
+ }
+
+ @ParameterizedTest
+ @MethodSource("scopedAndFatalFailures")
+ void testDecodingPreservesScopedAndFatalFailures(final Throwable failure)
+ {
+ final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor = createMethodProcessor();
+ final ServerDecoder decoder = new ServerDecoder(methodProcessor);
+ final QpidByteBuffer input = createFailingMethodBody(failure);
+
+ assertSame(failure, assertThrows(failure.getClass(), () -> decoder.processMethod(CHANNEL_ID, input)));
+ verify(methodProcessor).setCurrentMethod(0, 0);
+ }
+
+ @ParameterizedTest
+ @MethodSource("scopedAndFatalFailures")
+ void testProtocolVersionLookupPreservesScopedAndFatalFailures(final Throwable failure)
+ {
+ final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor = createMethodProcessor();
+ when(methodProcessor.getProtocolVersion()).thenThrow(failure);
+ final FrameBoundaryValidatingServerMethodProcessor validatingProcessor =
+ new FrameBoundaryValidatingServerMethodProcessor(methodProcessor);
+
+ assertSame(failure, assertThrows(failure.getClass(), validatingProcessor::getProtocolVersion));
+ }
+
+ private static QpidByteBuffer createFailingMethodBody(final Throwable failure)
+ {
+ final QpidByteBuffer input = mock(QpidByteBuffer.class);
+ when(input.remaining()).thenReturn(Integer.BYTES + Long.BYTES);
+ when(input.getInt()).thenReturn((BasicQosBody.CLASS_ID << 16) | BasicQosBody.METHOD_ID);
+ when(input.getUnsignedInt()).thenThrow(failure);
+ return input;
+ }
+
+ private static Stream<RuntimeException> decodingRuntimeFailures()
+ {
+ return Stream.of(new BufferUnderflowException(), new ArithmeticException("Injected decoding failure"),
+ new ClassCastException("Injected decoding failure"), new RuntimeException("Injected decoding failure"));
+ }
+
+ private static Stream<Throwable> scopedAndFatalFailures()
+ {
+ return Stream.of(new ConnectionScopedRuntimeException("Injected connection failure"),
+ new ServerScopedRuntimeException("Injected server failure"),
+ new StoreException("Injected store failure"), new InternalError("Injected JVM failure"));
+ }
+
+ private static ServerMethodProcessor<ServerChannelMethodProcessor> createMethodProcessor()
+ {
+ return mock(ServerMethodProcessor.class);
+ }
+
+ private static ServerDecoder createRejectingDecoder(
+ final ServerMethodProcessor<ServerChannelMethodProcessor> methodProcessor)
+ {
+ return new ServerDecoder(methodProcessor)
+ {
+ private boolean _rejectFirstFrame = true;
+
+ @Override
+ protected void processFrame(final int channel,
+ final byte type,
+ final long bodySize,
+ final QpidByteBuffer body) throws AMQFrameDecodingException
+ {
+ if (_rejectFirstFrame)
+ {
+ _rejectFirstFrame = false;
+ body.getUnsignedShort();
+ throw new AMQFrameDecodingException("Frame processing failed");
+ }
+ super.processFrame(channel, type, bodySize, body);
+ }
+ };
+ }
+
+ private static Stream<Arguments> rejectedFrameReadVariants()
+ {
+ return protocolVersionsAndConnectionCloseOkMethods()
+ .flatMap(arguments -> Stream.of(3, 7, 11, 12, 15, 23, 24)
+ .flatMap(firstReadLength -> Stream.of(false, true)
+ .map(direct -> Arguments.of(arguments.get()[0], arguments.get()[1],
+ firstReadLength, direct))));
+ }
+
+ private static Stream<Arguments> protocolVersionsAndConnectionCloseOkMethods()
+ {
+ return Stream.of(Arguments.of(ProtocolVersion.v0_8, 0x000a003d),
+ Arguments.of(ProtocolVersion.v0_9, 0x000a0033), Arguments.of(ProtocolVersion.v0_91, 0x000a0033));
+ }
+
+ private static Stream<Arguments> heartbeatChannelErrorCodes()
+ {
+ return Stream.of(Arguments.of(ProtocolVersion.v0_8, ErrorCodes.FRAME_ERROR),
+ Arguments.of(ProtocolVersion.v0_9, ErrorCodes.FRAME_ERROR),
+ Arguments.of(ProtocolVersion.v0_91, ErrorCodes.COMMAND_INVALID));
+ }
+
+ private static Stream<ProtocolVersion> legacyProtocolVersions()
+ {
+ return Stream.of(ProtocolVersion.v0_8, ProtocolVersion.v0_9);
+ }
+
+ private static void writeFrameHeader(final QpidByteBuffer buffer,
+ final byte type,
+ final int channel,
+ final long bodySize)
+ {
+ buffer.put(type);
+ buffer.putUnsignedShort(channel);
+ buffer.putUnsignedInt(bodySize);
+ }
+
+ private static void writeHeartbeatFrame(final QpidByteBuffer buffer,
+ final int channel,
+ final byte[] payload)
+ {
+ writeFrameHeader(buffer, HeartbeatBody.TYPE, channel, payload.length);
+ buffer.put(payload);
+ buffer.put(FRAME_END);
+ }
+
+ private static void writeShortString(final QpidByteBuffer buffer, final String value)
+ {
+ final byte[] encoded = value.getBytes(UTF_8);
+ buffer.put((byte) encoded.length);
+ buffer.put(encoded);
+ }
+
+ private static void writeConnectionStartOkFrame(final QpidByteBuffer buffer,
+ final byte[] fieldTable,
+ final int bodySize)
+ {
+ writeFrameHeader(buffer, (byte) 1, 0, bodySize);
+ buffer.putInt((ConnectionStartOkBody.CLASS_ID << 16) | ConnectionStartOkBody.METHOD_ID);
+ buffer.putUnsignedInt(fieldTable.length);
+ buffer.put(fieldTable);
+ writeShortString(buffer, "PLAIN");
+ buffer.putUnsignedInt(0);
+ writeShortString(buffer, "en_US");
+ buffer.put(FRAME_END);
+ }
+
+ private static byte[] nestedFieldTable(final int depth)
+ {
+ final ByteBuffer buffer = ByteBuffer.allocate(7 * depth);
+ for (int remainingDepth = depth; remainingDepth > 1; remainingDepth--)
+ {
+ buffer.put((byte) 1);
+ buffer.put((byte) 'n');
+ buffer.put(AMQType.FIELD_TABLE.identifier());
+ buffer.putInt(7 * (remainingDepth - 1));
+ }
+ buffer.put((byte) 1);
+ buffer.put((byte) 'v');
+ buffer.put(AMQType.INT.identifier());
+ buffer.putInt(42);
+ return buffer.array();
+ }
+
+ private static byte[] malformedNestedFieldTable()
+ {
+ final byte[] malformedNestedTable = ByteBuffer.allocate(7)
+ .put((byte) 1)
+ .put((byte) 's')
+ .put(AMQType.LONG_STRING.identifier())
+ .putInt(0x80000000)
+ .array();
+ return ByteBuffer.allocate(13 + malformedNestedTable.length)
+ .put((byte) 7)
+ .put("ignored".getBytes(UTF_8))
+ .put(AMQType.FIELD_TABLE.identifier())
+ .putInt(malformedNestedTable.length)
+ .put(malformedNestedTable)
+ .array();
+ }
+}
diff --git a/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/transport/BasicContentHeaderPropertiesDisposalTest.java b/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/transport/BasicContentHeaderPropertiesDisposalTest.java
new file mode 100644
index 0000000000..3658d5aaf5
--- /dev/null
+++ b/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/transport/BasicContentHeaderPropertiesDisposalTest.java
@@ -0,0 +1,460 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ */
+package org.apache.qpid.server.protocol.v0_8.transport;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.nio.ByteBuffer;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.mockito.ArgumentCaptor;
+
+import org.apache.qpid.server.bytebuffer.QpidByteBuffer;
+import org.apache.qpid.server.protocol.ErrorCodes;
+import org.apache.qpid.server.protocol.ProtocolVersion;
+import org.apache.qpid.server.protocol.v0_8.AMQFrameDecodingException;
+import org.apache.qpid.server.protocol.v0_8.AMQType;
+import org.apache.qpid.test.utils.UnitTestBase;
+
+class BasicContentHeaderPropertiesDisposalTest extends UnitTestBase
+{
+ private static final int BUFFER_SIZE = 64;
+ private static final int CHANNEL_ID = 1;
+
+ @BeforeEach
+ void setUp()
+ {
+ QpidByteBuffer.deinitialisePool();
+ QpidByteBuffer.initialisePool(BUFFER_SIZE, 20, 1.0);
+ }
+
+ @AfterEach
+ void tearDown()
+ {
+ QpidByteBuffer.deinitialisePool();
+ }
+
+ @Test
+ void constructorDisposesEncodedFormWhenHeaderTableDecodeFails()
+ {
+ final long baseline = QpidByteBuffer.getAllocatedDirectMemorySize();
+
+ final QpidByteBuffer in = QpidByteBuffer.allocateDirect(BUFFER_SIZE);
+ in.putInt(0x20000000); // headers field-table length
+ in.put((byte) 0);
+ in.flip();
+ final int size = in.remaining();
+
+ assertThrows(IllegalArgumentException.class, () -> new BasicContentHeaderProperties(in,
+ BasicContentHeaderProperties.HEADERS_MASK, size));
+
+ in.dispose();
+ assertEquals(baseline, QpidByteBuffer.getAllocatedDirectMemorySize(), "Malformed content-header headers " +
+ "leaked a pooled direct buffer");
+ }
+
+ @Test
+ void constructorDisposesHeadersWhenLaterPropertyDecodeFails()
+ {
+ final long baseline = QpidByteBuffer.getAllocatedDirectMemorySize();
+
+ final QpidByteBuffer in = QpidByteBuffer.allocateDirect(BUFFER_SIZE);
+ in.putInt(3); // headers field-table length
+ in.putUnsignedByte((short) 1);
+ in.put((byte) 'x');
+ in.put(AMQType.VOID.identifier());
+ in.putUnsignedByte((short) 1); // expiration string length
+ in.put((byte) 'x'); // invalid numeric digit
+ in.flip();
+ final int size = in.remaining();
+
+ assertThrows(AMQFrameDecodingException.class, () -> new BasicContentHeaderProperties(in,
+ BasicContentHeaderProperties.HEADERS_MASK | BasicContentHeaderProperties.EXPIRATION_MASK, size));
+
+ in.dispose();
+ assertEquals(baseline, QpidByteBuffer.getAllocatedDirectMemorySize(),
+ "A later property failure leaked decoded content-header headers");
+ }
+
+ @Test
+ void nestedMalformedHeaderIsRejectedBeforeDispatch()
+ {
+ final long baseline = QpidByteBuffer.getAllocatedDirectMemorySize();
+ final MethodProcessor<ChannelMethodProcessor> methodProcessor = mockMethodProcessor();
+ final byte[] fieldTable = malformedNestedFieldTable();
+ final QpidByteBuffer in = QpidByteBuffer.allocateDirect(BUFFER_SIZE);
+ in.putUnsignedShort(ContentHeaderBody.CLASS_ID);
+ in.putUnsignedShort(0);
+ in.putLong(0);
+ in.putUnsignedShort(BasicContentHeaderProperties.HEADERS_MASK);
+ in.putUnsignedInt(fieldTable.length);
+ in.put(fieldTable);
+ in.flip();
+ final int size = in.remaining();
+
+ final AMQFrameDecodingException exception = assertThrows(AMQFrameDecodingException.class, () ->
+ ContentHeaderBody.process(in, methodProcessor, CHANNEL_ID, size));
+
+ assertEquals(ErrorCodes.FRAME_ERROR, exception.getErrorCode());
+ verify(methodProcessor, never()).getChannelMethodProcessor(anyInt());
+ in.dispose();
+ assertEquals(baseline, QpidByteBuffer.getAllocatedDirectMemorySize(),
+ "Malformed nested content headers retained pooled direct memory");
+ }
+
+ @Test
+ void configuredNestingDepthRejectsContentHeaderBeforeDispatch()
+ {
+ final long baseline = QpidByteBuffer.getAllocatedDirectMemorySize();
+ final MethodProcessor<ChannelMethodProcessor> methodProcessor = mockMethodProcessor();
+ final byte[] fieldTable = nestedFieldTable(2);
+ final QpidByteBuffer in = QpidByteBuffer.allocateDirect(BUFFER_SIZE);
+ try
+ {
+ in.putUnsignedShort(ContentHeaderBody.CLASS_ID);
+ in.putUnsignedShort(0);
+ in.putLong(0);
+ in.putUnsignedShort(BasicContentHeaderProperties.HEADERS_MASK);
+ in.putUnsignedInt(fieldTable.length);
+ in.put(fieldTable);
+ in.flip();
+ final int size = in.remaining();
+
+ final AMQFrameDecodingException exception = assertThrows(AMQFrameDecodingException.class, () ->
+ ContentHeaderBody.process(in, methodProcessor, CHANNEL_ID, size, 1));
+
+ assertEquals(ErrorCodes.RESOURCE_ERROR, exception.getErrorCode());
+ verify(methodProcessor, never()).getChannelMethodProcessor(anyInt());
+ }
+ finally
+ {
+ in.dispose();
+ }
+ assertEquals(baseline, QpidByteBuffer.getAllocatedDirectMemorySize(),
+ "Content headers rejected by configured nesting depth retained pooled direct memory");
+ }
+
+ @Test
+ void propertyPaddingIsRejectedBeforeDispatch()
+ {
+ assertContentHeaderRejectedBeforeDispatch(0, 0, 0, new byte[] { (byte) 0xFF }, null);
+ }
+
+ @ParameterizedTest
+ @ValueSource(ints = { 0x0001, 0x0002 })
+ void unsupportedPropertyFlagsAreRejectedBeforeDispatch(final int propertyFlags)
+ {
+ assertContentHeaderRejectedBeforeDispatch(0, 0, propertyFlags, new byte[0], null);
+ }
+
+ @Test
+ void unsignedBodySizeOutsideSignedRangeIsRejectedWithoutDecodingProperties() throws AMQFrameDecodingException
+ {
+ final long baseline = QpidByteBuffer.getAllocatedDirectMemorySize();
+ final MethodProcessor<ChannelMethodProcessor> methodProcessor = mockMethodProcessor();
+ final QpidByteBuffer in = QpidByteBuffer.allocateDirect(BUFFER_SIZE);
+ try
+ {
+ in.putUnsignedShort(ContentHeaderBody.CLASS_ID);
+ in.putUnsignedShort(0);
+ in.putLong(Long.MIN_VALUE);
+ in.putUnsignedShort(0);
+ in.put((byte) 0xFF);
+ in.flip();
+
+ ContentHeaderBody.process(in, methodProcessor, CHANNEL_ID, in.remaining());
+
+ assertFalse(in.hasRemaining());
+ verify(methodProcessor).receiveOversizedMessageHeader(CHANNEL_ID, Long.MIN_VALUE);
+ verify(methodProcessor, never()).getChannelMethodProcessor(anyInt());
+ verify(methodProcessor, never()).getProtocolVersion();
+ }
+ finally
+ {
+ in.dispose();
+ }
+ assertEquals(baseline, QpidByteBuffer.getAllocatedDirectMemorySize(),
+ "Oversized content header retained pooled direct memory");
+ }
+
+ @Test
+ void amqp08StructuredContentIsRejectedAsNotImplementedBeforeDispatch()
+ {
+ final AMQFrameDecodingException exception =
+ assertContentHeaderRejectedBeforeDispatch(1, 0, 0, new byte[0], ProtocolVersion.v0_8);
+
+ assertEquals(ErrorCodes.NOT_IMPLEMENTED, exception.getErrorCode());
+ }
+
+ @Test
+ void amqp09ReservedContentWeightIsRejectedAsFrameErrorBeforeDispatch()
+ {
+ assertReservedContentWeightRejected(ProtocolVersion.v0_9);
+ }
+
+ @Test
+ void amqp091NonZeroContentWeightIsRejectedAsFrameErrorBeforeDispatch()
+ {
+ assertReservedContentWeightRejected(ProtocolVersion.v0_91);
+ }
+
+ @Test
+ void zeroContentWeightDispatchesWithoutProtocolVersionLookup() throws AMQFrameDecodingException
+ {
+ final MethodProcessor<ChannelMethodProcessor> methodProcessor = mockMethodProcessor();
+ final ChannelMethodProcessor channelMethodProcessor = mock(ChannelMethodProcessor.class);
+ when(methodProcessor.getChannelMethodProcessor(CHANNEL_ID)).thenReturn(channelMethodProcessor);
+
+ final QpidByteBuffer in = QpidByteBuffer.allocateDirect(BUFFER_SIZE);
+ in.putUnsignedShort(ContentHeaderBody.CLASS_ID);
+ in.putUnsignedShort(0);
+ in.putLong(0);
+ in.putUnsignedShort(0);
+ in.flip();
+
+ final ArgumentCaptor<BasicContentHeaderProperties> propertiesCaptor =
+ ArgumentCaptor.forClass(BasicContentHeaderProperties.class);
+ try
+ {
+ ContentHeaderBody.process(in, methodProcessor, CHANNEL_ID, in.remaining());
+
+ verify(methodProcessor, never()).getProtocolVersion();
+ verify(channelMethodProcessor).receiveMessageHeader(propertiesCaptor.capture(), eq(0L));
+ }
+ finally
+ {
+ if (!propertiesCaptor.getAllValues().isEmpty())
+ {
+ propertiesCaptor.getValue().dispose();
+ }
+ in.dispose();
+ }
+ }
+
+ @Test
+ void channelLookupFailureDisposesContentHeaderProperties()
+ {
+ final MethodProcessor<ChannelMethodProcessor> methodProcessor = mockMethodProcessor();
+ when(methodProcessor.getChannelMethodProcessor(CHANNEL_ID))
+ .thenThrow(new IllegalStateException("Connection is not open"));
+
+ assertProcessorFailureDoesNotRetainContentHeaderProperties(methodProcessor);
+ }
+
+ @Test
+ void channelDispatchFailurePreservesTransferredContentHeaderProperties()
+ {
+ final long baseline = QpidByteBuffer.getAllocatedDirectMemorySize();
+ final MethodProcessor<ChannelMethodProcessor> methodProcessor = mockMethodProcessor();
+ final ChannelMethodProcessor channelMethodProcessor = mock(ChannelMethodProcessor.class);
+ when(methodProcessor.getChannelMethodProcessor(CHANNEL_ID)).thenReturn(channelMethodProcessor);
+ final AtomicReference<BasicContentHeaderProperties> retainedProperties = new AtomicReference<>();
+ final IllegalStateException failure = new IllegalStateException("Dispatch failed after retaining properties");
+ doAnswer(invocation ->
+ {
+ retainedProperties.set(invocation.getArgument(0));
+ throw failure;
+ })
+ .when(channelMethodProcessor)
+ .receiveMessageHeader(any(BasicContentHeaderProperties.class), anyLong());
+
+ final QpidByteBuffer in = QpidByteBuffer.allocateDirect(BUFFER_SIZE);
+ try
+ {
+ in.putUnsignedShort(ContentHeaderBody.CLASS_ID);
+ in.putUnsignedShort(0);
+ in.putLong(0);
+ in.putUnsignedShort(BasicContentHeaderProperties.HEADERS_MASK);
+ in.putUnsignedInt(7);
+ in.putUnsignedByte((short) 1);
+ in.put((byte) 'v');
+ in.put(AMQType.INT.identifier());
+ in.putInt(42);
+ in.flip();
+
+ final IllegalStateException thrown = assertThrows(IllegalStateException.class, () ->
+ ContentHeaderBody.process(in, methodProcessor, CHANNEL_ID, in.remaining()));
+ assertSame(failure, thrown);
+ }
+ finally
+ {
+ in.dispose();
+ }
+
+ final BasicContentHeaderProperties properties = retainedProperties.get();
+ assertNotNull(properties, "Content-header properties were not retained by the receiver");
+ try
+ {
+ assertEquals(42, properties.getHeader("v"), "Transferred field-table buffer was released by the parser");
+ }
+ finally
+ {
+ properties.dispose();
+ }
+ assertEquals(baseline, QpidByteBuffer.getAllocatedDirectMemorySize(),
+ "Transferred content-header properties retained pooled direct memory after disposal");
+ }
+
+ @Test
+ void channelDispatchFailureDoesNotLeakPropertiesDisposedByReceiver()
+ {
+ final MethodProcessor<ChannelMethodProcessor> methodProcessor = mockMethodProcessor();
+ final ChannelMethodProcessor channelMethodProcessor = mock(ChannelMethodProcessor.class);
+ when(methodProcessor.getChannelMethodProcessor(CHANNEL_ID)).thenReturn(channelMethodProcessor);
+ doAnswer(invocation ->
+ {
+ final BasicContentHeaderProperties properties = invocation.getArgument(0);
+ properties.dispose();
+ throw new IllegalStateException("Dispatch rejected properties");
+ })
+ .when(channelMethodProcessor)
+ .receiveMessageHeader(any(BasicContentHeaderProperties.class), anyLong());
+
+ assertProcessorFailureDoesNotRetainContentHeaderProperties(methodProcessor);
+ }
+
+ private void assertProcessorFailureDoesNotRetainContentHeaderProperties(
+ final MethodProcessor<ChannelMethodProcessor> methodProcessor)
+ {
+ final long baseline = QpidByteBuffer.getAllocatedDirectMemorySize();
+ final QpidByteBuffer in = QpidByteBuffer.allocateDirect(BUFFER_SIZE);
+ try
+ {
+ in.putUnsignedShort(ContentHeaderBody.CLASS_ID);
+ in.putUnsignedShort(0);
+ in.putLong(0);
+ in.putUnsignedShort(0);
+ in.flip();
+
+ assertThrows(IllegalStateException.class, () ->
+ ContentHeaderBody.process(in, methodProcessor, CHANNEL_ID, in.remaining()));
+ }
+ finally
+ {
+ in.dispose();
+ }
+ assertEquals(baseline, QpidByteBuffer.getAllocatedDirectMemorySize(),
+ "Processor failure retained content-header properties");
+ }
+
+ private void assertReservedContentWeightRejected(final ProtocolVersion protocolVersion)
+ {
+ final AMQFrameDecodingException exception =
+ assertContentHeaderRejectedBeforeDispatch(1, 0, 0, new byte[0], protocolVersion);
+
+ assertEquals(ErrorCodes.FRAME_ERROR, exception.getErrorCode());
+ }
+
+ private AMQFrameDecodingException assertContentHeaderRejectedBeforeDispatch(final int weight,
+ final long bodySize,
+ final int propertyFlags,
+ final byte[] propertyList,
+ final ProtocolVersion protocolVersion)
+ {
+ final long baseline = QpidByteBuffer.getAllocatedDirectMemorySize();
+ final MethodProcessor<ChannelMethodProcessor> methodProcessor = mockMethodProcessor();
+ if (protocolVersion != null)
+ {
+ when(methodProcessor.getProtocolVersion()).thenReturn(protocolVersion);
+ }
+ final QpidByteBuffer in = QpidByteBuffer.allocateDirect(BUFFER_SIZE);
+ final AMQFrameDecodingException exception;
+ try
+ {
+ in.putUnsignedShort(ContentHeaderBody.CLASS_ID);
+ in.putUnsignedShort(weight);
+ in.putLong(bodySize);
+ in.putUnsignedShort(propertyFlags);
+ in.put(propertyList);
+ in.flip();
+ final int size = in.remaining();
+
+ exception = assertThrows(AMQFrameDecodingException.class, () ->
+ ContentHeaderBody.process(in, methodProcessor, CHANNEL_ID, size));
+
+ verify(methodProcessor, never()).getChannelMethodProcessor(anyInt());
+ }
+ finally
+ {
+ in.dispose();
+ }
+ assertEquals(baseline, QpidByteBuffer.getAllocatedDirectMemorySize(),
+ "Rejected content-header properties retained pooled direct memory");
+ return exception;
+ }
+
+ private static MethodProcessor<ChannelMethodProcessor> mockMethodProcessor()
+ {
+ return mock(MethodProcessor.class);
+ }
+
+ private static byte[] malformedNestedFieldTable()
+ {
+ final byte[] malformedNestedTable = ByteBuffer.allocate(7)
+ .put((byte) 1)
+ .put((byte) 's')
+ .put(AMQType.LONG_STRING.identifier())
+ .putInt(0x80000000)
+ .array();
+ return ByteBuffer.allocate(7 + malformedNestedTable.length)
+ .put((byte) 1)
+ .put((byte) 'n')
+ .put(AMQType.FIELD_TABLE.identifier())
+ .putInt(malformedNestedTable.length)
+ .put(malformedNestedTable)
+ .array();
+ }
+
+ private static byte[] nestedFieldTable(final int depth)
+ {
+ final ByteBuffer buffer = ByteBuffer.allocate(7 * depth);
+ for (int remainingDepth = depth; remainingDepth > 1; remainingDepth--)
+ {
+ buffer.put((byte) 1);
+ buffer.put((byte) 'n');
+ buffer.put(AMQType.FIELD_TABLE.identifier());
+ buffer.putInt(7 * (remainingDepth - 1));
+ }
+ buffer.put((byte) 1);
+ buffer.put((byte) 'v');
+ buffer.put(AMQType.INT.identifier());
+ buffer.putInt(42);
+ return buffer.array();
+ }
+}
diff --git a/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/transport/BasicContentHeaderPropertiesTest.java b/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/transport/BasicContentHeaderPropertiesTest.java
index 47d0a59762..6043b3dfaa 100644
--- a/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/transport/BasicContentHeaderPropertiesTest.java
+++ b/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/transport/BasicContentHeaderPropertiesTest.java
@@ -91,11 +91,15 @@ class BasicContentHeaderPropertiesTest extends UnitTestBase
}
@Test
- void populatePropertiesFromBuffer() throws Exception
+ void populateEmptyPropertiesFromBuffer() throws Exception
{
- final QpidByteBuffer buf = QpidByteBuffer.wrap(new byte[300]);
- _testProperties.dispose();
- _testProperties = new BasicContentHeaderProperties(buf, 99, 99);
+ try (final QpidByteBuffer buf = QpidByteBuffer.wrap(new byte[0]))
+ {
+ _testProperties.dispose();
+ _testProperties = new BasicContentHeaderProperties(buf, 0, 0);
+ assertEquals(0, _testProperties.getPropertyFlags());
+ _testProperties.dispose();
+ }
}
@Test
@@ -238,16 +242,23 @@ class BasicContentHeaderPropertiesTest extends UnitTestBase
propertiesBuffer.flip();
final BasicContentHeaderProperties testProperties = new BasicContentHeaderProperties(propertiesBuffer, flags, propertyListSize);
- final Map<String, Object> headersBeforeReallocation = testProperties.getHeadersAsMap();
- assertEquals(headers, headersBeforeReallocation, "Unexpected headers");
-
- buffer.dispose();
-
- assertTrue(propertiesBuffer.isSparse(), "Properties buffer should be sparse");
- testProperties.reallocate();
-
- final Map<String, Object> headersAfterReallocation = testProperties.getHeadersAsMap();
- assertEquals(headers, headersAfterReallocation, "Unexpected headers after re-allocation");
+ try
+ {
+ final Map<String, Object> headersBeforeReallocation = testProperties.getHeadersAsMap();
+ assertEquals(headers, headersBeforeReallocation, "Unexpected headers");
+
+ buffer.dispose();
+
+ assertTrue(propertiesBuffer.isSparse(), "Properties buffer should be sparse");
+ testProperties.reallocate();
+
+ final Map<String, Object> headersAfterReallocation = testProperties.getHeadersAsMap();
+ assertEquals(headers, headersAfterReallocation, "Unexpected headers after re-allocation");
+ }
+ finally
+ {
+ testProperties.dispose();
+ }
}
}
}
diff --git a/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/transport/ClientFieldTableBodyDisposalTest.java b/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/transport/ClientFieldTableBodyDisposalTest.java
new file mode 100644
index 0000000000..25cb803d0d
--- /dev/null
+++ b/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/transport/ClientFieldTableBodyDisposalTest.java
@@ -0,0 +1,106 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ */
+package org.apache.qpid.server.protocol.v0_8.transport;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import org.apache.qpid.server.bytebuffer.QpidByteBuffer;
+import org.apache.qpid.server.protocol.v0_8.AMQShortString;
+import org.apache.qpid.server.protocol.v0_8.AMQType;
+import org.apache.qpid.server.protocol.v0_8.FieldTable;
+import org.apache.qpid.test.utils.UnitTestBase;
+
+class ClientFieldTableBodyDisposalTest extends UnitTestBase
+{
+ private static final int BUFFER_SIZE = 64;
+
+ @BeforeEach
+ void setUp()
+ {
+ QpidByteBuffer.deinitialisePool();
+ QpidByteBuffer.initialisePool(BUFFER_SIZE, 20, 1.0);
+ }
+
+ @AfterEach
+ void tearDown()
+ {
+ QpidByteBuffer.deinitialisePool();
+ }
+
+ @Test
+ void connectionStartDisposesServerPropertiesWhenLaterFieldDecodeFails()
+ {
+ final ClientMethodProcessor<ClientChannelMethodProcessor> dispatcher = mock(ClientMethodProcessor.class);
+ final long baseline = QpidByteBuffer.getAllocatedDirectMemorySize();
+ final QpidByteBuffer in = QpidByteBuffer.allocateDirect(BUFFER_SIZE);
+ in.putUnsignedByte((short) 0); // version major
+ in.putUnsignedByte((short) 9); // version minor
+ writeValidFieldTable(in);
+ in.putInt(0x20000000); // mechanisms length
+ in.flip();
+
+ assertThrows(IllegalArgumentException.class, () -> ConnectionStartBody.process(in, dispatcher));
+
+ in.dispose();
+ assertEquals(baseline, QpidByteBuffer.getAllocatedDirectMemorySize(),
+ "Malformed connection.start leaked server properties");
+ }
+
+ @Test
+ void channelAlertDisposesDetailsWhenProcessorFails()
+ {
+ final ClientChannelMethodProcessor dispatcher = mock(ClientChannelMethodProcessor.class);
+ doThrow(new IllegalStateException("processor failure"))
+ .when(dispatcher)
+ .receiveChannelAlert(anyInt(), any(AMQShortString.class), any(FieldTable.class));
+
+ final long baseline = QpidByteBuffer.getAllocatedDirectMemorySize();
+ final QpidByteBuffer in = QpidByteBuffer.allocateDirect(BUFFER_SIZE);
+ in.putUnsignedShort(500);
+ in.putUnsignedByte((short) 1);
+ in.put((byte) 'x');
+ writeValidFieldTable(in);
+ in.flip();
+
+ assertThrows(IllegalStateException.class, () -> ChannelAlertBody.process(in, dispatcher));
+
+ in.dispose();
+ assertEquals(baseline, QpidByteBuffer.getAllocatedDirectMemorySize(),
+ "A channel.alert processor failure leaked details");
+ }
+
+ private static void writeValidFieldTable(final QpidByteBuffer buffer)
+ {
+ buffer.putInt(3);
+ buffer.putUnsignedByte((short) 1);
+ buffer.put((byte) 'x');
+ buffer.put(AMQType.VOID.identifier());
+ }
+}
diff --git a/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/transport/ConnectionStartOkBodyTest.java b/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/transport/ConnectionStartOkBodyTest.java
new file mode 100644
index 0000000000..94bd40f1c5
--- /dev/null
+++ b/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/transport/ConnectionStartOkBodyTest.java
@@ -0,0 +1,126 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ */
+package org.apache.qpid.server.protocol.v0_8.transport;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.mock;
+
+import java.nio.charset.StandardCharsets;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import org.apache.qpid.server.bytebuffer.QpidByteBuffer;
+import org.apache.qpid.test.utils.UnitTestBase;
+
+class ConnectionStartOkBodyTest extends UnitTestBase
+{
+ private static final int BUFFER_SIZE = 64;
+ private static final int POOL_SIZE = 20;
+ private static final double SPARSITY_FRACTION = 1.0;
+
+ @BeforeEach
+ void setUp()
+ {
+ QpidByteBuffer.deinitialisePool();
+ QpidByteBuffer.initialisePool(BUFFER_SIZE, POOL_SIZE, SPARSITY_FRACTION);
+ }
+
+ @AfterEach
+ void tearDown()
+ {
+ QpidByteBuffer.deinitialisePool();
+ }
+
+ @Test
+ void processDisposesClientPropertiesWhenFieldTableDecodeFails()
+ {
+ final ServerMethodProcessor dispatcher = mock(ServerMethodProcessor.class);
+
+ final long baseline = QpidByteBuffer.getAllocatedDirectMemorySize();
+
+ final QpidByteBuffer in = QpidByteBuffer.allocateDirect(BUFFER_SIZE);
+ // client-properties field table (length 7): entry key "s", value type 'S', long-string length 0x80000000
+ in.putInt(7);
+ in.putUnsignedByte((short) 1);
+ in.put((byte) 's');
+ in.put((byte) 'S');
+ in.putInt(0x80000000);
+ // mechanism "PLAIN" (short string)
+ in.putUnsignedByte((short) 5);
+ in.put("PLAIN".getBytes(StandardCharsets.US_ASCII));
+ // response "guest" (long bytes)
+ in.putInt(5);
+ in.put("guest".getBytes(StandardCharsets.US_ASCII));
+ // locale "en_US" (short string)
+ in.putUnsignedByte((short) 5);
+ in.put("en_US".getBytes(StandardCharsets.US_ASCII));
+ in.flip();
+
+ assertThrows(IllegalArgumentException.class, () -> ConnectionStartOkBody.process(in, dispatcher));
+
+ in.dispose();
+
+ assertEquals(baseline, QpidByteBuffer.getAllocatedDirectMemorySize(),
+ "Malformed start-ok leaked a pooled direct buffer");
+ }
+
+ @Test
+ void processDisposesNestedClientPropertyBeforeLaterDecodeFailure()
+ {
+ final ServerMethodProcessor dispatcher = mock(ServerMethodProcessor.class);
+ final long baseline = QpidByteBuffer.getAllocatedDirectMemorySize();
+ final QpidByteBuffer in = QpidByteBuffer.allocateDirect(BUFFER_SIZE);
+
+ // client-properties field table: a valid nested table followed by an invalid long-string length
+ in.putInt(17);
+ in.putUnsignedByte((short) 1);
+ in.put((byte) 'n');
+ in.put((byte) 'F');
+ in.putInt(3);
+ in.putUnsignedByte((short) 1);
+ in.put((byte) 'x');
+ in.put((byte) 'V');
+ in.putUnsignedByte((short) 1);
+ in.put((byte) 's');
+ in.put((byte) 'S');
+ in.putInt(0x80000000);
+ // mechanism "PLAIN" (short string)
+ in.putUnsignedByte((short) 5);
+ in.put("PLAIN".getBytes(StandardCharsets.US_ASCII));
+ // response "guest" (long bytes)
+ in.putInt(5);
+ in.put("guest".getBytes(StandardCharsets.US_ASCII));
+ // locale "en_US" (short string)
+ in.putUnsignedByte((short) 5);
+ in.put("en_US".getBytes(StandardCharsets.US_ASCII));
+ in.flip();
+
+ assertThrows(IllegalArgumentException.class, () -> ConnectionStartOkBody.process(in, dispatcher));
+
+ in.dispose();
+
+ assertEquals(baseline, QpidByteBuffer.getAllocatedDirectMemorySize(),
+ "Malformed start-ok leaked a nested client-properties buffer");
+ }
+}
diff --git a/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/transport/ServerFieldTableBodyDisposalTest.java b/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/transport/ServerFieldTableBodyDisposalTest.java
new file mode 100644
index 0000000000..2a9a32b5f2
--- /dev/null
+++ b/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/transport/ServerFieldTableBodyDisposalTest.java
@@ -0,0 +1,160 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ */
+package org.apache.qpid.server.protocol.v0_8.transport;
+
+import static java.nio.charset.StandardCharsets.US_ASCII;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.mock;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import org.apache.qpid.server.bytebuffer.QpidByteBuffer;
+import org.apache.qpid.test.utils.UnitTestBase;
+
+class ServerFieldTableBodyDisposalTest extends UnitTestBase
+{
+ private static final int BUFFER_SIZE = 64;
+
+ @FunctionalInterface
+ private interface Decoder
+ {
+ void process(final QpidByteBuffer buffer, final ServerChannelMethodProcessor dispatcher) throws Exception;
+ }
+
+ @FunctionalInterface
+ private interface ArgWriter
+ {
+ void write(final QpidByteBuffer buffer);
+ }
+
+ @BeforeEach
+ void setUp()
+ {
+ QpidByteBuffer.deinitialisePool();
+ QpidByteBuffer.initialisePool(BUFFER_SIZE, 20, 1.0);
+ }
+
+ @AfterEach
+ void tearDown()
+ {
+ QpidByteBuffer.deinitialisePool();
+ }
+
+ @Test
+ void basicConsumeDisposesArgumentsOnMalformedTable()
+ {
+ assertNoLeak(buffer ->
+ {
+ buffer.putShort((short) 0); // ticket
+ writeShortString(buffer, "q"); // queue
+ writeShortString(buffer, "tag"); // consumerTag
+ buffer.put((byte) 0); // bitfield
+ writeMalformedFieldTable(buffer);
+ }, BasicConsumeBody::process);
+ }
+
+ @Test
+ void exchangeDeclareDisposesArgumentsOnMalformedTable()
+ {
+ assertNoLeak(buffer ->
+ {
+ buffer.putShort((short) 0); // ticket
+ writeShortString(buffer, "ex"); // exchange
+ writeShortString(buffer, "direct"); // type
+ buffer.put((byte) 0); // bitfield
+ writeMalformedFieldTable(buffer);
+ }, ExchangeDeclareBody::process);
+ }
+
+ @Test
+ void queueDeclareDisposesArgumentsOnMalformedTable()
+ {
+ assertNoLeak(buffer ->
+ {
+ buffer.putShort((short) 0); // ticket
+ writeShortString(buffer, "q"); // queue
+ buffer.put((byte) 0); // bitfield
+ writeMalformedFieldTable(buffer);
+ }, QueueDeclareBody::process);
+ }
+
+ @Test
+ void queueBindDisposesArgumentsOnMalformedTable()
+ {
+ assertNoLeak(buffer ->
+ {
+ buffer.putShort((short) 0); // ticket
+ writeShortString(buffer, "q"); // queue
+ writeShortString(buffer, "ex"); // exchange
+ writeShortString(buffer, "rk"); // bindingKey
+ buffer.put((byte) 0); // nowait
+ writeMalformedFieldTable(buffer);
+ }, QueueBindBody::process);
+ }
+
+ @Test
+ void queueUnbindDisposesArgumentsOnMalformedTable()
+ {
+ assertNoLeak(buffer ->
+ {
+ buffer.putShort((short) 0); // ticket
+ writeShortString(buffer, "q"); // queue
+ writeShortString(buffer, "ex"); // exchange
+ writeShortString(buffer, "rk"); // routingKey
+ writeMalformedFieldTable(buffer);
+ }, QueueUnbindBody::process);
+ }
+
+ private void assertNoLeak(final ArgWriter argWriter, final Decoder decoder)
+ {
+ final ServerChannelMethodProcessor dispatcher = mock(ServerChannelMethodProcessor.class);
+ final long baseline = QpidByteBuffer.getAllocatedDirectMemorySize();
+ final QpidByteBuffer in = QpidByteBuffer.allocateDirect(BUFFER_SIZE);
+ argWriter.write(in);
+ in.flip();
+
+ assertThrows(IllegalArgumentException.class, () -> decoder.process(in, dispatcher));
+
+ in.dispose();
+ assertEquals(baseline, QpidByteBuffer.getAllocatedDirectMemorySize(),
+ "Malformed field-table argument leaked a pooled direct buffer");
+ }
+
+ private static void writeShortString(final QpidByteBuffer buffer, final String value)
+ {
+ final byte[] bytes = value.getBytes(US_ASCII);
+ buffer.putUnsignedByte((short) bytes.length);
+ buffer.put(bytes);
+ }
+
+ // field table (length 7): entry key "s", value type 'S', long-string length 0x80000000
+ private static void writeMalformedFieldTable(final QpidByteBuffer buffer)
+ {
+ buffer.putInt(7);
+ buffer.putUnsignedByte((short) 1);
+ buffer.put((byte) 's');
+ buffer.put((byte) 'S');
+ buffer.putInt(0x80000000);
+ }
+}
diff --git a/systests/protocol-tests-amqp-0-8/src/test/java/org/apache/qpid/tests/protocol/v0_8/ConnectionTest.java b/systests/protocol-tests-amqp-0-8/src/test/java/org/apache/qpid/tests/protocol/v0_8/ConnectionTest.java
index bdc68b1f23..f213ee67f3 100644
--- a/systests/protocol-tests-amqp-0-8/src/test/java/org/apache/qpid/tests/protocol/v0_8/ConnectionTest.java
+++ b/systests/protocol-tests-amqp-0-8/src/test/java/org/apache/qpid/tests/protocol/v0_8/ConnectionTest.java
@@ -33,14 +33,18 @@ import static org.hamcrest.Matchers.lessThanOrEqualTo;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
-import java.io.IOException;
+import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
-import java.util.concurrent.ExecutionException;
import org.junit.jupiter.api.Test;
+import org.apache.qpid.server.bytebuffer.QpidByteBuffer;
import org.apache.qpid.server.protocol.ErrorCodes;
+import org.apache.qpid.server.protocol.ProtocolVersion;
+import org.apache.qpid.server.protocol.v0_8.AMQDecoder;
+import org.apache.qpid.server.protocol.v0_8.transport.AMQDataBlock;
+import org.apache.qpid.server.protocol.v0_8.transport.BasicQosBody;
import org.apache.qpid.server.protocol.v0_8.transport.ChannelOpenOkBody;
import org.apache.qpid.server.protocol.v0_8.transport.ConnectionCloseBody;
import org.apache.qpid.server.protocol.v0_8.transport.ConnectionCloseOkBody;
@@ -49,6 +53,7 @@ import org.apache.qpid.server.protocol.v0_8.transport.ConnectionSecureBody;
import org.apache.qpid.server.protocol.v0_8.transport.ConnectionStartBody;
import org.apache.qpid.server.protocol.v0_8.transport.ConnectionTuneBody;
import org.apache.qpid.server.protocol.v0_8.transport.HeartbeatBody;
+import org.apache.qpid.server.transport.ByteBufferSender;
import org.apache.qpid.tests.protocol.ChannelClosedResponse;
import org.apache.qpid.tests.protocol.Response;
import org.apache.qpid.tests.protocol.SpecificationTest;
@@ -60,6 +65,9 @@ public class ConnectionTest extends BrokerAdminUsingTestBase
private static final String ANONYMOUS = "ANONYMOUS";
private static final String PLAIN = "PLAIN";
private static final String CRAM_MD5 = "CRAM-MD5";
+ private static final byte FRAME_END = (byte) 0xCE;
+ private static final int CONNECTION_CLOSE_METHOD_ID_0_8 = 60;
+ private static final int CONNECTION_CLOSE_METHOD_ID_0_9 = 50;
@Test
@SpecificationTest(section = "1.4.2.1", description = "start connection negotiation")
@@ -104,10 +112,8 @@ public class ConnectionTest extends BrokerAdminUsingTestBase
.connection().startOkMechanism("NOT-A-MECHANISM")
.startOk();
- final ConnectionCloseBody res = interaction.consumeResponse()
- .getLatestResponse(ConnectionCloseBody.class);
- assertThat(res.getReplyCode(), is(equalTo(ErrorCodes.CONNECTION_FORCED)));
-
+ interaction.sync();
+ transport.assertNoMoreResponsesAndChannelClosed();
}
}
@@ -301,39 +307,64 @@ public class ConnectionTest extends BrokerAdminUsingTestBase
final int overlyLargeFrameBodySize = (int) (frameMax + 1); // Should be frameMax - 8 + 1.
final byte[] bodyBytes = new byte[overlyLargeFrameBodySize];
- interaction.connection()
- .tuneOkChannelMax(response.getChannelMax())
- .tuneOkFrameMax(frameMax)
- .tuneOkHeartbeat(response.getHeartbeat())
- .tuneOk()
- .connection().open()
- .consumeResponse(ConnectionOpenOkBody.class)
- .channel().open()
- .consumeResponse(ChannelOpenOkBody.class)
- .basic().publish()
- .basic().contentHeader(bodyBytes.length)
- .basic().contentBody(bodyBytes);
+ final ConnectionCloseBody close = interaction.connection()
+ .tuneOkChannelMax(response.getChannelMax())
+ .tuneOkFrameMax(frameMax)
+ .tuneOkHeartbeat(response.getHeartbeat())
+ .tuneOk()
+ .connection().open()
+ .consumeResponse(ConnectionOpenOkBody.class)
+ .channel().open()
+ .consumeResponse(ChannelOpenOkBody.class)
+ .basic().publish()
+ .basic().contentHeader(bodyBytes.length)
+ .basic().contentBody(bodyBytes)
+ .consumeResponse()
+ .getLatestResponse(ConnectionCloseBody.class);
+
+ assertThat(close.getReplyCode(), is(equalTo(ErrorCodes.FRAME_ERROR)));
+ }
+ }
- // Spec requires:
- //assertThat(res.getReplyCode(), CoreMatchers.is(CoreMatchers.equalTo(ErrorCodes.COMMAND_INVALID)));
+ @Test
+ @SpecificationTest(section = "4.2.3",
+ description = "A Connection-class method on a non-zero channel must produce reply code 503.")
+ public void connectionMethodOnNonZeroChannelSignalsConnectionException() throws Exception
+ {
+ try (final FrameTransport transport = new FrameTransport(getBrokerAdmin()).connect())
+ {
+ final ConnectionCloseBody close = transport.newInteraction()
+ .negotiateOpen()
+ .channelId(1)
+ .connection().close()
+ .consumeResponse()
+ .getLatestResponse(ConnectionCloseBody.class);
+
+ assertThat(close.getReplyCode(), is(equalTo(ErrorCodes.COMMAND_INVALID)));
+ assertThat(close.getClassId(), is(equalTo(ConnectionCloseBody.CLASS_ID)));
+ final int expectedMethodId = ProtocolVersion.v0_8.equals(transport.getProtocolVersion())
+ ? CONNECTION_CLOSE_METHOD_ID_0_8
+ : CONNECTION_CLOSE_METHOD_ID_0_9;
+ assertThat(close.getMethodId(), is(equalTo(expectedMethodId)));
+ }
+ }
- // Server actually abruptly closes the connection. We might see a graceful TCP/IP close or a broken pipe.
- try
- {
- interaction.consumeResponse().getLatestResponse(ChannelClosedResponse.class);
- }
- catch (ExecutionException e)
- {
- Throwable original = e.getCause();
- if (original instanceof IOException)
- {
- // PASS
- }
- else
- {
- throw new RuntimeException(original);
- }
- }
+ @Test
+ @SpecificationTest(section = "4.2.3",
+ description = "A malformed method frame must produce reply code 501 and identify the failing method.")
+ public void truncatedMethodSignalsConnectionExceptionWithMethodIdentifiers() throws Exception
+ {
+ try (final FrameTransport transport = new FrameTransport(getBrokerAdmin()).connect())
+ {
+ final ConnectionCloseBody close = transport.newInteraction()
+ .negotiateOpen()
+ .sendPerformative(truncatedBasicQosFrame())
+ .consumeResponse()
+ .getLatestResponse(ConnectionCloseBody.class);
+
+ assertThat(close.getReplyCode(), is(equalTo(ErrorCodes.FRAME_ERROR)));
+ assertThat(close.getClassId(), is(equalTo(BasicQosBody.CLASS_ID)));
+ assertThat(close.getMethodId(), is(equalTo(BasicQosBody.METHOD_ID)));
}
}
@@ -517,4 +548,34 @@ public class ConnectionTest extends BrokerAdminUsingTestBase
assertThat(String.valueOf(close.getReplyText()).toLowerCase(), containsString("unknown virtual host"));
}
}
+
+ private static AMQDataBlock truncatedBasicQosFrame()
+ {
+ final ByteBuffer buffer = ByteBuffer.allocate(AMQDecoder.FRAME_HEADER_SIZE + Integer.BYTES + 1);
+ buffer.put((byte) 1);
+ buffer.putShort((short) 1);
+ buffer.putInt(Integer.BYTES);
+ buffer.putInt((BasicQosBody.CLASS_ID << 16) | BasicQosBody.METHOD_ID);
+ buffer.put(FRAME_END);
+ return rawFrame(buffer.array());
+ }
+
+ private static AMQDataBlock rawFrame(final byte[] bytes)
+ {
+ return new AMQDataBlock()
+ {
+ @Override
+ public long getSize()
+ {
+ return bytes.length;
+ }
+
+ @Override
+ public long writePayload(final ByteBufferSender sender)
+ {
+ sender.send(QpidByteBuffer.wrap(bytes));
+ return bytes.length;
+ }
+ };
+ }
}
diff --git a/systests/protocol-tests-amqp-0-8/src/test/java/org/apache/qpid/tests/protocol/v0_8/ContentBodyFrameLimitTest.java b/systests/protocol-tests-amqp-0-8/src/test/java/org/apache/qpid/tests/protocol/v0_8/ContentBodyFrameLimitTest.java
new file mode 100644
index 0000000000..581e3dd874
--- /dev/null
+++ b/systests/protocol-tests-amqp-0-8/src/test/java/org/apache/qpid/tests/protocol/v0_8/ContentBodyFrameLimitTest.java
@@ -0,0 +1,108 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ */
+package org.apache.qpid.tests.protocol.v0_8;
+
+import static org.apache.qpid.tests.utils.BrokerAdmin.KIND_BROKER_J;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.is;
+
+import java.util.List;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import org.apache.qpid.server.model.Protocol;
+import org.apache.qpid.server.protocol.ErrorCodes;
+import org.apache.qpid.server.protocol.v0_8.transport.ChannelCloseOkBody;
+import org.apache.qpid.server.protocol.v0_8.transport.ChannelOpenOkBody;
+import org.apache.qpid.server.protocol.v0_8.transport.ConnectionCloseBody;
+import org.apache.qpid.tests.utils.BrokerAdmin;
+import org.apache.qpid.tests.utils.BrokerAdminUsingTestBase;
+import org.apache.qpid.tests.utils.BrokerSpecific;
+import org.apache.qpid.tests.utils.ConfigItem;
+
+@BrokerSpecific(kind = KIND_BROKER_J)
+@ConfigItem(name = "connection.maxContentBodyFramesPerMessage", value = "2")
+public class ContentBodyFrameLimitTest extends BrokerAdminUsingTestBase
+{
+ private static final List<Protocol> PROTOCOLS = List.of(Protocol.AMQP_0_8, Protocol.AMQP_0_9, Protocol.AMQP_0_9_1);
+
+ @BeforeEach
+ public void setUp()
+ {
+ getBrokerAdmin().createQueue(BrokerAdmin.TEST_QUEUE_NAME);
+ }
+
+ @Test
+ public void contentBodyFrameLimitExceededClosesConnection() throws Exception
+ {
+ for (final Protocol protocol : PROTOCOLS)
+ {
+ try (final FrameTransport transport = new FrameTransport(getBrokerAdmin(), getBrokerAdmin()
+ .getPreferredPortType(), protocol).connect())
+ {
+ final Interaction interaction = openChannel(transport);
+ interaction.basic().publishExchange("")
+ .publishRoutingKey(BrokerAdmin.TEST_QUEUE_NAME)
+ .publish()
+ .basic().contentHeader(1)
+ .basic().contentBody(new byte[0])
+ .basic().contentBody(new byte[0])
+ .basic().contentBody(new byte[0]);
+
+ final ConnectionCloseBody close = interaction.consumeResponse()
+ .getLatestResponse(ConnectionCloseBody.class);
+ assertThat(protocol.toString(), close.getReplyCode(), is(equalTo(ErrorCodes.RESOURCE_ERROR)));
+ }
+ }
+ }
+
+ @Test
+ public void messageAtExactContentBodyFrameLimitIsAccepted() throws Exception
+ {
+ for (final Protocol protocol : PROTOCOLS)
+ {
+ try (final FrameTransport transport = new FrameTransport(getBrokerAdmin(), getBrokerAdmin()
+ .getPreferredPortType(), protocol).connect())
+ {
+ final Interaction interaction = openChannel(transport);
+ interaction.basic().publishExchange("")
+ .publishRoutingKey(BrokerAdmin.TEST_QUEUE_NAME)
+ .publish()
+ .basic().contentHeader(1)
+ .basic().contentBody(new byte[0])
+ .basic().contentBody(new byte[]{42})
+ .channel().close()
+ .consumeResponse(ChannelCloseOkBody.class);
+ }
+ }
+
+ assertThat(getBrokerAdmin().getQueueDepthMessages(BrokerAdmin.TEST_QUEUE_NAME), is(equalTo(PROTOCOLS.size())));
+ }
+
+ private Interaction openChannel(final FrameTransport transport) throws Exception
+ {
+ return transport.newInteraction()
+ .negotiateOpen()
+ .channel().open().consumeResponse(ChannelOpenOkBody.class);
+ }
+}
diff --git a/systests/protocol-tests-amqp-0-8/src/test/java/org/apache/qpid/tests/protocol/v0_8/IncompleteContentSequenceTest.java b/systests/protocol-tests-amqp-0-8/src/test/java/org/apache/qpid/tests/protocol/v0_8/IncompleteContentSequenceTest.java
new file mode 100644
index 0000000000..ab2c0cbf42
--- /dev/null
+++ b/systests/protocol-tests-amqp-0-8/src/test/java/org/apache/qpid/tests/protocol/v0_8/IncompleteContentSequenceTest.java
@@ -0,0 +1,186 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ */
+package org.apache.qpid.tests.protocol.v0_8;
+
+import static org.apache.qpid.tests.utils.BrokerAdmin.KIND_BROKER_J;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.is;
+
+import java.util.List;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import org.apache.qpid.server.model.Protocol;
+import org.apache.qpid.server.protocol.ErrorCodes;
+import org.apache.qpid.server.protocol.v0_8.transport.ChannelOpenOkBody;
+import org.apache.qpid.server.protocol.v0_8.transport.ConnectionCloseBody;
+import org.apache.qpid.server.protocol.v0_8.transport.ConnectionCloseOkBody;
+import org.apache.qpid.server.protocol.v0_8.transport.QueueDeclareOkBody;
+import org.apache.qpid.tests.utils.BrokerAdmin;
+import org.apache.qpid.tests.utils.BrokerAdminUsingTestBase;
+import org.apache.qpid.tests.utils.BrokerSpecific;
+
+@BrokerSpecific(kind = KIND_BROKER_J)
+public class IncompleteContentSequenceTest extends BrokerAdminUsingTestBase
+{
+ private static final List<Protocol> PROTOCOLS = List.of(Protocol.AMQP_0_8, Protocol.AMQP_0_9, Protocol.AMQP_0_9_1);
+
+ @BeforeEach
+ public void setUp()
+ {
+ getBrokerAdmin().createQueue(BrokerAdmin.TEST_QUEUE_NAME);
+ }
+
+ @Test
+ public void secondPublishBeforeContentCompletionClosesConnection() throws Exception
+ {
+ for (final Protocol protocol : PROTOCOLS)
+ {
+ try (final FrameTransport transport = new FrameTransport(getBrokerAdmin(), getBrokerAdmin()
+ .getPreferredPortType(), protocol).connect())
+ {
+ final Interaction interaction = openChannel(transport);
+ interaction.basic().publishExchange("")
+ .publishRoutingKey(BrokerAdmin.TEST_QUEUE_NAME)
+ .publish()
+ .basic().contentHeader(2)
+ .basic().contentBody(new byte[]{42})
+ .basic().publishExchange("")
+ .publishRoutingKey(BrokerAdmin.TEST_QUEUE_NAME)
+ .publish();
+
+ assertIncompleteContentClose(interaction, protocol);
+ }
+ }
+ }
+
+ @Test
+ public void methodBeforeContentCompletionClosesConnection() throws Exception
+ {
+ for (final Protocol protocol : PROTOCOLS)
+ {
+ try (final FrameTransport transport = new FrameTransport(getBrokerAdmin(), getBrokerAdmin()
+ .getPreferredPortType(), protocol).connect())
+ {
+ final Interaction interaction = openChannel(transport);
+ interaction.basic().publishExchange("")
+ .publishRoutingKey(BrokerAdmin.TEST_QUEUE_NAME)
+ .publish()
+ .basic().contentHeader(2)
+ .basic().contentBody(new byte[]{42})
+ .queue().declarePassive(true)
+ .declareName(BrokerAdmin.TEST_QUEUE_NAME)
+ .declare();
+
+ assertIncompleteContentClose(interaction, protocol);
+ }
+ }
+ }
+
+ @Test
+ public void methodOnDifferentChannelIsAllowedDuringContentSequence() throws Exception
+ {
+ for (final Protocol protocol : PROTOCOLS)
+ {
+ try (final FrameTransport transport = new FrameTransport(getBrokerAdmin(), getBrokerAdmin()
+ .getPreferredPortType(), protocol).connect())
+ {
+ final Interaction interaction = openChannel(transport)
+ .channelId(2).channel().open().consumeResponse(ChannelOpenOkBody.class);
+ interaction.channelId(1)
+ .basic().publishExchange("")
+ .publishRoutingKey(BrokerAdmin.TEST_QUEUE_NAME)
+ .publish()
+ .basic().contentHeader(2)
+ .basic().contentBody(new byte[]{42});
+
+ interaction.channelId(2)
+ .queue().declarePassive(true)
+ .declareName(BrokerAdmin.TEST_QUEUE_NAME)
+ .declare()
+ .consumeResponse(QueueDeclareOkBody.class);
+
+ interaction.channelId(1).basic().contentBody(new byte[]{43})
+ .channelId(0).connection().close()
+ .consumeResponse(ConnectionCloseOkBody.class);
+ }
+ }
+ }
+
+ @Test
+ public void duplicateContentHeaderClosesConnection() throws Exception
+ {
+ for (final Protocol protocol : PROTOCOLS)
+ {
+ try (final FrameTransport transport = new FrameTransport(getBrokerAdmin(), getBrokerAdmin()
+ .getPreferredPortType(), protocol).connect())
+ {
+ final Interaction interaction = openChannel(transport);
+ interaction.basic().publishExchange("")
+ .publishRoutingKey(BrokerAdmin.TEST_QUEUE_NAME)
+ .publish()
+ .basic().contentHeader(1)
+ .basic().contentHeader(1);
+
+ assertIncompleteContentClose(interaction, protocol);
+ }
+ }
+ }
+
+ @Test
+ public void contentBodyBeforeHeaderClosesConnection() throws Exception
+ {
+ for (final Protocol protocol : PROTOCOLS)
+ {
+ try (final FrameTransport transport = new FrameTransport(getBrokerAdmin(), getBrokerAdmin()
+ .getPreferredPortType(), protocol).connect())
+ {
+ final Interaction interaction = openChannel(transport);
+ interaction.basic().publishExchange("")
+ .publishRoutingKey(BrokerAdmin.TEST_QUEUE_NAME)
+ .publish()
+ .basic().contentBody(new byte[0]);
+
+ assertIncompleteContentClose(interaction, protocol);
+ }
+ }
+ }
+
+ private void assertIncompleteContentClose(final Interaction interaction, final Protocol protocol)
+ throws Exception
+ {
+ final ConnectionCloseBody close = interaction.consumeResponse().getLatestResponse(ConnectionCloseBody.class);
+ final int expectedReplyCode = protocol == Protocol.AMQP_0_9_1
+ ? ErrorCodes.UNEXPECTED_FRAME
+ : ErrorCodes.FRAME_ERROR;
+ assertThat(protocol.toString(), close.getReplyCode(), is(equalTo(expectedReplyCode)));
+ }
+
+ private Interaction openChannel(final FrameTransport transport) throws Exception
+ {
+ return transport.newInteraction()
+ .negotiateOpen()
+ .channelId(1)
+ .channel().open().consumeResponse(ChannelOpenOkBody.class);
+ }
+}
diff --git a/systests/protocol-tests-amqp-0-8/src/test/java/org/apache/qpid/tests/protocol/v0_8/MalformedFrameTest.java b/systests/protocol-tests-amqp-0-8/src/test/java/org/apache/qpid/tests/protocol/v0_8/MalformedFrameTest.java
new file mode 100644
index 0000000000..8204ed93e0
--- /dev/null
+++ b/systests/protocol-tests-amqp-0-8/src/test/java/org/apache/qpid/tests/protocol/v0_8/MalformedFrameTest.java
@@ -0,0 +1,244 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ */
+package org.apache.qpid.tests.protocol.v0_8;
+
+import static java.nio.charset.StandardCharsets.US_ASCII;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.is;
+
+import org.junit.jupiter.api.Test;
+
+import org.apache.qpid.server.bytebuffer.QpidByteBuffer;
+import org.apache.qpid.server.protocol.v0_8.AMQPConnection_0_8;
+import org.apache.qpid.server.protocol.v0_8.transport.AMQDataBlock;
+import org.apache.qpid.server.protocol.v0_8.transport.ConnectionStartBody;
+import org.apache.qpid.server.transport.ByteBufferSender;
+import org.apache.qpid.tests.protocol.ChannelClosedResponse;
+import org.apache.qpid.tests.utils.BrokerAdminUsingTestBase;
+
+public class MalformedFrameTest extends BrokerAdminUsingTestBase
+{
+ private static final byte FRAME_END = (byte) 0xCE;
+ private static final int MAX_NESTED_OBJECTS = Integer.getInteger(AMQPConnection_0_8.CODEC_MAX_NESTED_OBJECTS,
+ AMQPConnection_0_8.DEFAULT_CODEC_MAX_NESTED_OBJECTS);
+
+ @Test
+ public void secureOkWithOversizedResponseDoesNotCrashBroker() throws Exception
+ {
+ assertMalformedFrameClosesOnlyConnection(secureOkWithOversizedResponse());
+ }
+
+ @Test
+ public void startOkWithOversizedLongStringDoesNotCrashBroker() throws Exception
+ {
+ assertMalformedFrameClosesOnlyConnection(startOkWithOversizedLongString());
+ }
+
+ @Test
+ public void startOkWithOversizedLongStringInNestedTableDoesNotCrashBroker() throws Exception
+ {
+ assertMalformedFrameClosesOnlyConnection(startOkWithOversizedLongStringInNestedTable());
+ }
+
+ @Test
+ public void startOkExceedingNestingLimitClosesWithoutResponse() throws Exception
+ {
+ assertMalformedFrameClosesOnlyConnection(startOkWithExcessiveNesting());
+ }
+
+ private void assertMalformedFrameClosesOnlyConnection(final byte[] malformedFrame) throws Exception
+ {
+ try (final FrameTransport transport = new FrameTransport(getBrokerAdmin()).connect())
+ {
+ final Interaction interaction = transport.newInteraction();
+ interaction.negotiateProtocol()
+ .consumeResponse(ConnectionStartBody.class)
+ .sendPerformative(rawFrame(malformedFrame))
+ .consumeResponse()
+ .getLatestResponse(ChannelClosedResponse.class);
+ }
+
+ try (final FrameTransport transport = new FrameTransport(getBrokerAdmin()).connect())
+ {
+ final Interaction interaction = transport.newInteraction();
+ final ConnectionStartBody start = interaction.negotiateProtocol()
+ .consumeResponse()
+ .getLatestResponse(ConnectionStartBody.class);
+ assertThat(start.getVersionMajor(), is(equalTo((short) transport.getProtocolVersion().getMajorVersion())));
+ }
+ }
+
+ private static AMQDataBlock rawFrame(final byte[] bytes)
+ {
+ return new AMQDataBlock()
+ {
+ @Override
+ public long getSize()
+ {
+ return bytes.length;
+ }
+
+ @Override
+ public long writePayload(final ByteBufferSender sender)
+ {
+ sender.send(QpidByteBuffer.wrap(bytes));
+ return bytes.length;
+ }
+ };
+ }
+
+ private static byte[] secureOkWithOversizedResponse()
+ {
+ try (final QpidByteBuffer buffer = QpidByteBuffer.allocate(18))
+ {
+ writeFrameHeader(buffer, 10);
+ buffer.putInt(0x000A0015); // connection.secure-ok
+ buffer.putUnsignedInt(0x20000000L); // response length
+ buffer.putUnsignedShort(4); // insufficient response bytes
+ buffer.put(FRAME_END);
+ return readBytes(buffer);
+ }
+ }
+
+ private static byte[] startOkWithOversizedLongString()
+ {
+ try (final QpidByteBuffer buffer = QpidByteBuffer.allocate(44))
+ {
+ writeFrameHeader(buffer, 36);
+ buffer.putInt(0x000A000B); // connection.start-ok
+
+ buffer.putUnsignedInt(7); // client-properties field-table length
+ buffer.putUnsignedByte((short) 1);
+ buffer.put((byte) 's');
+ buffer.put((byte) 'S'); // long-string field value
+ buffer.putUnsignedInt(0x80000000L); // field value length
+
+ writeShortString(buffer, "PLAIN"); // mechanism
+ writeLongString(buffer, "guest"); // response
+ writeShortString(buffer, "en_US"); // locale
+ buffer.put(FRAME_END);
+ return readBytes(buffer);
+ }
+ }
+
+ private static byte[] startOkWithOversizedLongStringInNestedTable()
+ {
+ try (final QpidByteBuffer buffer = QpidByteBuffer.allocate(57))
+ {
+ writeFrameHeader(buffer, 49);
+ buffer.putInt(0x000A000B); // connection.start-ok
+
+ buffer.putUnsignedInt(20); // client-properties field-table length
+ buffer.putUnsignedByte((short) 7);
+ buffer.put("ignored".getBytes(US_ASCII));
+ buffer.put((byte) 'F'); // nested field table
+ buffer.putUnsignedInt(7);
+ buffer.putUnsignedByte((short) 1);
+ buffer.put((byte) 's');
+ buffer.put((byte) 'S'); // long-string field value
+ buffer.putUnsignedInt(0x80000000L); // field value length
+
+ writeShortString(buffer, "PLAIN"); // mechanism
+ writeLongString(buffer, "guest"); // response
+ writeShortString(buffer, "en_US"); // locale
+ buffer.put(FRAME_END);
+ return readBytes(buffer);
+ }
+ }
+
+ private static byte[] startOkWithExcessiveNesting()
+ {
+ final byte[] clientProperties = nestedFieldTable(MAX_NESTED_OBJECTS + 1);
+ final int bodySize = Integer.BYTES + Integer.BYTES + clientProperties.length + shortStringSize("PLAIN") +
+ longStringSize("guest") + shortStringSize("en_US");
+ try (final QpidByteBuffer buffer = QpidByteBuffer.allocate(8 + bodySize))
+ {
+ writeFrameHeader(buffer, bodySize);
+ buffer.putInt(0x000A000B); // connection.start-ok
+ buffer.putUnsignedInt(clientProperties.length);
+ buffer.put(clientProperties);
+ writeShortString(buffer, "PLAIN");
+ writeLongString(buffer, "guest");
+ writeShortString(buffer, "en_US");
+ buffer.put(FRAME_END);
+ return readBytes(buffer);
+ }
+ }
+
+ private static byte[] nestedFieldTable(final int depth)
+ {
+ try (final QpidByteBuffer buffer = QpidByteBuffer.allocate(7 * depth))
+ {
+ for (int remainingDepth = depth; remainingDepth > 1; remainingDepth--)
+ {
+ buffer.putUnsignedByte((short) 1);
+ buffer.put((byte) 'n');
+ buffer.put((byte) 'F');
+ buffer.putUnsignedInt(7L * (remainingDepth - 1));
+ }
+ buffer.putUnsignedByte((short) 1);
+ buffer.put((byte) 'v');
+ buffer.put((byte) 'I');
+ buffer.putInt(42);
+ return readBytes(buffer);
+ }
+ }
+
+ private static void writeFrameHeader(final QpidByteBuffer buffer, final long bodySize)
+ {
+ buffer.put((byte) 1);
+ buffer.putUnsignedShort(0); // connection methods use channel zero
+ buffer.putUnsignedInt(bodySize);
+ }
+
+ private static void writeShortString(final QpidByteBuffer buffer, final String value)
+ {
+ final byte[] bytes = value.getBytes(US_ASCII);
+ buffer.putUnsignedByte((short) bytes.length);
+ buffer.put(bytes);
+ }
+
+ private static void writeLongString(final QpidByteBuffer buffer, final String value)
+ {
+ final byte[] bytes = value.getBytes(US_ASCII);
+ buffer.putUnsignedInt(bytes.length);
+ buffer.put(bytes);
+ }
+
+ private static int shortStringSize(final String value)
+ {
+ return Byte.BYTES + value.getBytes(US_ASCII).length;
+ }
+
+ private static int longStringSize(final String value)
+ {
+ return Integer.BYTES + value.getBytes(US_ASCII).length;
+ }
+
+ private static byte[] readBytes(final QpidByteBuffer buffer)
+ {
+ buffer.flip();
+ final byte[] bytes = new byte[buffer.remaining()];
+ buffer.get(bytes);
+ return bytes;
+ }
+}
diff --git a/systests/protocol-tests-amqp-0-8/src/test/java/org/apache/qpid/tests/protocol/v0_8/extension/basic/MalformedMessageTest.java b/systests/protocol-tests-amqp-0-8/src/test/java/org/apache/qpid/tests/protocol/v0_8/extension/basic/MalformedMessageTest.java
index 1a50ee4460..080841809d 100644
--- a/systests/protocol-tests-amqp-0-8/src/test/java/org/apache/qpid/tests/protocol/v0_8/extension/basic/MalformedMessageTest.java
+++ b/systests/protocol-tests-amqp-0-8/src/test/java/org/apache/qpid/tests/protocol/v0_8/extension/basic/MalformedMessageTest.java
@@ -38,6 +38,7 @@ import org.junit.jupiter.api.Test;
import org.apache.qpid.server.bytebuffer.QpidByteBuffer;
import org.apache.qpid.server.exchange.ExchangeDefaults;
import org.apache.qpid.server.filter.AMQPFilterTypes;
+import org.apache.qpid.server.protocol.ErrorCodes;
import org.apache.qpid.server.protocol.v0_8.AMQShortString;
import org.apache.qpid.server.protocol.v0_8.AMQType;
import org.apache.qpid.server.protocol.v0_8.EncodingUtils;
@@ -50,11 +51,11 @@ import org.apache.qpid.server.protocol.v0_8.transport.BasicQosOkBody;
import org.apache.qpid.server.protocol.v0_8.transport.ChannelCloseOkBody;
import org.apache.qpid.server.protocol.v0_8.transport.ChannelFlowOkBody;
import org.apache.qpid.server.protocol.v0_8.transport.ChannelOpenOkBody;
+import org.apache.qpid.server.protocol.v0_8.transport.ConnectionCloseBody;
import org.apache.qpid.server.protocol.v0_8.transport.ContentBody;
import org.apache.qpid.server.protocol.v0_8.transport.ContentHeaderBody;
import org.apache.qpid.server.protocol.v0_8.transport.QueueBindOkBody;
import org.apache.qpid.server.protocol.v0_8.transport.TxSelectOkBody;
-import org.apache.qpid.tests.protocol.ChannelClosedResponse;
import org.apache.qpid.tests.protocol.v0_8.FrameTransport;
import org.apache.qpid.tests.protocol.v0_8.Interaction;
import org.apache.qpid.tests.utils.BrokerAdmin;
@@ -119,9 +120,8 @@ public class MalformedMessageTest extends BrokerAdminUsingTestBase
.publishRoutingKey(BrokerAdmin.TEST_QUEUE_NAME)
.contentHeaderPropertiesHeaders(malformedHeader)
.content(contentBytes)
- .publishMessage()
- .consumeResponse()
- .getLatestResponse(ChannelClosedResponse.class);
+ .publishMessage();
+ assertConnectionRejected(transport, interaction);
assertThat(getBrokerAdmin().getQueueDepthMessages(BrokerAdmin.TEST_QUEUE_NAME), is(equalTo(0)));
}
@@ -150,20 +150,20 @@ public class MalformedMessageTest extends BrokerAdminUsingTestBase
.publishRoutingKey(BrokerAdmin.TEST_QUEUE_NAME)
.contentHeaderPropertiesHeaders(malformedHeader)
.content(contentBytes)
- .publishMessage()
- .tx().commit()
- .consumeResponse()
- .getLatestResponse(ChannelClosedResponse.class);
+ .publishMessage();
+ assertConnectionRejected(transport, interaction);
assertThat(getBrokerAdmin().getQueueDepthMessages(BrokerAdmin.TEST_QUEUE_NAME), is(equalTo(0)));
}
}
@Test
- public void consumeMalformedMessage() throws Exception
+ public void consumeValidMessageAfterMalformedMessageRejected() throws Exception
{
final FieldTable malformedHeader = createHeadersWithMalformedLongString();
final byte[] contentBytes = CONTENT_TEXT.getBytes(StandardCharsets.UTF_8);
+ publishMalformedMessage(malformedHeader, contentBytes);
+ assertThat(getBrokerAdmin().getQueueDepthMessages(BrokerAdmin.TEST_QUEUE_NAME), is(equalTo(0)));
final String content2 = "message2";
final byte[] content2Bytes = content2.getBytes(StandardCharsets.UTF_8);
@@ -185,12 +185,6 @@ public class MalformedMessageTest extends BrokerAdminUsingTestBase
.channel().flow(true)
.consumeResponse(ChannelFlowOkBody.class)
- .basic().publishExchange("")
- .publishRoutingKey(BrokerAdmin.TEST_QUEUE_NAME)
- .contentHeaderPropertiesHeaders(malformedHeader)
- .content(contentBytes)
- .publishMessage()
-
.basic().publishExchange("")
.publishRoutingKey(BrokerAdmin.TEST_QUEUE_NAME)
.contentHeaderPropertiesContentType("text/plain")
@@ -236,12 +230,20 @@ public class MalformedMessageTest extends BrokerAdminUsingTestBase
.publishRoutingKey(BrokerAdmin.TEST_QUEUE_NAME)
.contentHeaderPropertiesHeaders(malformedHeader)
.content(contentBytes)
- .publishMessage()
- .channel().close()
- .consumeResponse(ChannelCloseOkBody.class);
+ .publishMessage();
+ assertConnectionRejected(transport, interaction);
}
}
+ private void assertConnectionRejected(final FrameTransport transport, final Interaction interaction) throws Exception
+ {
+ final ConnectionCloseBody close = interaction.consumeResponse(ConnectionCloseBody.class)
+ .getLatestResponse(ConnectionCloseBody.class);
+ assertThat(close.getReplyCode(), is(equalTo(ErrorCodes.FRAME_ERROR)));
+ interaction.connection().closeOk();
+ transport.assertNoMoreResponsesAndChannelClosed();
+ }
+
private static FieldTable createMalformedHeaders()
{
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@qpid.apache.org
For additional commands, e-mail: commits-help@qpid.apache.org