This is an automated email from the ASF dual-hosted git repository.
rzo1 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/storm.git
The following commit(s) were added to refs/heads/master by this push:
new ee97546fb Decode netty server frames only after the handshake completes
ee97546fb is described below
commit ee97546fb2957982f93933ad236d9b248488a8e4
Author: Gianluca Graziadei <[email protected]>
AuthorDate: Sat Aug 22 13:47:38 2026 +0200
Decode netty server frames only after the handshake completes
---
.../storm/messaging/netty/MessageDecoder.java | 34 +++++
.../netty/StormServerPipelineFactory.java | 7 +-
.../storm/messaging/netty/MessageDecoderTest.java | 168 +++++++++++++++++++++
3 files changed, 206 insertions(+), 3 deletions(-)
diff --git
a/storm-client/src/jvm/org/apache/storm/messaging/netty/MessageDecoder.java
b/storm-client/src/jvm/org/apache/storm/messaging/netty/MessageDecoder.java
index a3b282a82..d063cf68f 100644
--- a/storm-client/src/jvm/org/apache/storm/messaging/netty/MessageDecoder.java
+++ b/storm-client/src/jvm/org/apache/storm/messaging/netty/MessageDecoder.java
@@ -25,10 +25,17 @@ import org.slf4j.LoggerFactory;
public class MessageDecoder extends ByteToMessageDecoder {
private static final Logger LOG =
LoggerFactory.getLogger(MessageDecoder.class);
+ private static final int MAX_UNAUTHENTICATED_SASL_TOKEN_BYTES = 64 * 1024;
private final KryoValuesDeserializer deser;
+ private final boolean serverAuthRequired;
public MessageDecoder(KryoValuesDeserializer deser) {
+ this(deser, false);
+ }
+
+ public MessageDecoder(KryoValuesDeserializer deser, boolean
serverAuthRequired) {
this.deser = deser;
+ this.serverAuthRequired = serverAuthRequired;
}
/*
@@ -86,6 +93,10 @@ public class MessageDecoder extends ByteToMessageDecoder {
// Read the length field.
int length = buf.readInt();
+ if (gateFrames(ctx) && length >
MAX_UNAUTHENTICATED_SASL_TOKEN_BYTES) {
+ discardAndClose(ctx, buf, "an oversized handshake frame");
+ return;
+ }
if (length <= 0) {
out.add(new SaslMessageToken(null));
return;
@@ -109,6 +120,10 @@ public class MessageDecoder extends ByteToMessageDecoder {
// case 3: BackPressureStatus
if (code == BackPressureStatus.IDENTIFIER) {
+ if (gateFrames(ctx)) {
+ discardAndClose(ctx, buf, "a status frame before the
handshake completed");
+ return;
+ }
available = buf.readableBytes();
if (available < 4) {
//Need more data
@@ -129,6 +144,11 @@ public class MessageDecoder extends ByteToMessageDecoder {
// case 4: task Message
+ if (gateFrames(ctx)) {
+ discardAndClose(ctx, buf, "a data frame before the handshake
completed");
+ return;
+ }
+
// Make sure that we have received at least an integer (length)
if (available < 4) {
// need more data
@@ -168,6 +188,20 @@ public class MessageDecoder extends ByteToMessageDecoder {
}
}
+ private boolean gateFrames(ChannelHandlerContext ctx) {
+ if (!serverAuthRequired) {
+ return false;
+ }
+ SaslNettyServer saslNettyServer =
ctx.channel().attr(SaslNettyServerState.SASL_NETTY_SERVER).get();
+ return saslNettyServer == null || !saslNettyServer.isComplete();
+ }
+
+ private static void discardAndClose(ChannelHandlerContext ctx, ByteBuf
buf, String what) {
+ LOG.warn("Channel {} sent {}; closing the connection", ctx.channel(),
what);
+ buf.skipBytes(buf.readableBytes());
+ ctx.close();
+ }
+
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
LOG.error("Exception thrown while decoding messages in channel {};
exception: ", ctx.channel(), cause);
diff --git
a/storm-client/src/jvm/org/apache/storm/messaging/netty/StormServerPipelineFactory.java
b/storm-client/src/jvm/org/apache/storm/messaging/netty/StormServerPipelineFactory.java
index 7a8b1c61b..02f838142 100644
---
a/storm-client/src/jvm/org/apache/storm/messaging/netty/StormServerPipelineFactory.java
+++
b/storm-client/src/jvm/org/apache/storm/messaging/netty/StormServerPipelineFactory.java
@@ -44,14 +44,15 @@ class StormServerPipelineFactory extends
ChannelInitializer<Channel> {
pipeline.addLast("ssl", sslContext.newHandler(ch.alloc()));
}
+ boolean isNettyAuth = (Boolean) topoConf
+ .get(Config.STORM_MESSAGING_NETTY_AUTHENTICATION);
+
// Decoder
- pipeline.addLast("decoder", new MessageDecoder(new
KryoValuesDeserializer(topoConf)));
+ pipeline.addLast("decoder", new MessageDecoder(new
KryoValuesDeserializer(topoConf), isNettyAuth));
// Encoders
pipeline.addLast("netty-serializable-encoder",
NettySerializableMessageEncoder.INSTANCE);
pipeline.addLast("backpressure-encoder", new
BackPressureStatusEncoder(new KryoValuesSerializer(topoConf)));
- boolean isNettyAuth = (Boolean) topoConf
- .get(Config.STORM_MESSAGING_NETTY_AUTHENTICATION);
if (isNettyAuth) {
// Authenticate: Removed after authentication completes
pipeline.addLast("saslServerHandler", new SaslStormServerHandler(
diff --git
a/storm-client/test/jvm/org/apache/storm/messaging/netty/MessageDecoderTest.java
b/storm-client/test/jvm/org/apache/storm/messaging/netty/MessageDecoderTest.java
new file mode 100644
index 000000000..969158823
--- /dev/null
+++
b/storm-client/test/jvm/org/apache/storm/messaging/netty/MessageDecoderTest.java
@@ -0,0 +1,168 @@
+/**
+ * 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.storm.messaging.netty;
+
+import java.util.List;
+import org.apache.storm.messaging.TaskMessage;
+import org.apache.storm.serialization.KryoValuesDeserializer;
+import org.apache.storm.shade.io.netty.buffer.ByteBuf;
+import org.apache.storm.shade.io.netty.buffer.Unpooled;
+import org.apache.storm.shade.io.netty.channel.embedded.EmbeddedChannel;
+import org.junit.jupiter.api.Test;
+
+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.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+
+public class MessageDecoderTest {
+
+ private static final short TASK_ID = 1;
+ private static final int HUGE_LENGTH = 0x7FFFFFFC;
+
+ private static ByteBuf backPressureFrame() {
+ ByteBuf buf = Unpooled.buffer();
+ buf.writeShort(BackPressureStatus.IDENTIFIER);
+ byte[] payload = { 1, 2, 3, 4 };
+ buf.writeInt(payload.length);
+ buf.writeBytes(payload);
+ return buf;
+ }
+
+ private static ByteBuf frameHeader(short code, int declaredLength) {
+ ByteBuf buf = Unpooled.buffer();
+ buf.writeShort(code);
+ buf.writeInt(declaredLength);
+ return buf;
+ }
+
+ private static ByteBuf taskMessageFrame(byte[] payload) {
+ ByteBuf buf = frameHeader(TASK_ID, payload.length);
+ buf.writeBytes(payload);
+ return buf;
+ }
+
+ private static ByteBuf saslTokenFrame(byte[] token) {
+ ByteBuf buf = frameHeader(SaslMessageToken.IDENTIFIER, token.length);
+ buf.writeBytes(token);
+ return buf;
+ }
+
+ private static EmbeddedChannel authenticatedChannel(KryoValuesDeserializer
deser) {
+ EmbeddedChannel channel = new EmbeddedChannel(new
MessageDecoder(deser, true));
+ SaslNettyServer saslNettyServer = mock(SaslNettyServer.class);
+ when(saslNettyServer.isComplete()).thenReturn(true);
+
channel.attr(SaslNettyServerState.SASL_NETTY_SERVER).set(saslNettyServer);
+ return channel;
+ }
+
+ @Test
+ public void backPressureFrameIsNotDeserializedBeforeAuthentication() {
+ KryoValuesDeserializer deser = mock(KryoValuesDeserializer.class);
+ EmbeddedChannel channel = new EmbeddedChannel(new
MessageDecoder(deser, true));
+
+ channel.writeInbound(backPressureFrame());
+
+ assertNull(channel.readInbound());
+ assertFalse(channel.isActive());
+ verifyNoInteractions(deser);
+ }
+
+ @Test
+ public void taskMessageFrameIsNotBufferedBeforeAuthentication() {
+ KryoValuesDeserializer deser = mock(KryoValuesDeserializer.class);
+ EmbeddedChannel channel = new EmbeddedChannel(new
MessageDecoder(deser, true));
+
+ channel.writeInbound(frameHeader(TASK_ID, HUGE_LENGTH));
+
+ assertNull(channel.readInbound());
+ assertFalse(channel.isActive());
+ }
+
+ @Test
+ public void oversizedSaslTokenFrameIsNotBufferedBeforeAuthentication() {
+ KryoValuesDeserializer deser = mock(KryoValuesDeserializer.class);
+ EmbeddedChannel channel = new EmbeddedChannel(new
MessageDecoder(deser, true));
+
+ channel.writeInbound(frameHeader(SaslMessageToken.IDENTIFIER,
HUGE_LENGTH));
+
+ assertNull(channel.readInbound());
+ assertFalse(channel.isActive());
+ }
+
+ @Test
+ public void saslHandshakeFramesAreAcceptedBeforeAuthentication() {
+ KryoValuesDeserializer deser = mock(KryoValuesDeserializer.class);
+ EmbeddedChannel channel = new EmbeddedChannel(new
MessageDecoder(deser, true));
+
+ ByteBuf request = Unpooled.buffer();
+ ControlMessage.SASL_TOKEN_MESSAGE_REQUEST.write(request);
+ channel.writeInbound(request);
+ assertSame(ControlMessage.SASL_TOKEN_MESSAGE_REQUEST,
channel.readInbound());
+
+ byte[] token = { 9, 8, 7 };
+ channel.writeInbound(saslTokenFrame(token));
+
+ SaslMessageToken decoded = channel.readInbound();
+ assertArrayEquals(token, decoded.getSaslToken());
+ assertTrue(channel.isActive());
+ }
+
+ @Test
+ public void backPressureFrameIsDeserializedAfterAuthentication() {
+ KryoValuesDeserializer deser = mock(KryoValuesDeserializer.class);
+ BackPressureStatus status = new BackPressureStatus();
+ when(deser.deserializeObject(any(byte[].class))).thenReturn(status);
+
+ EmbeddedChannel channel = authenticatedChannel(deser);
+
+ channel.writeInbound(backPressureFrame());
+
+ assertSame(status, channel.readInbound());
+ assertTrue(channel.isActive());
+ }
+
+ @Test
+ public void taskMessageFrameIsDecodedAfterAuthentication() {
+ KryoValuesDeserializer deser = mock(KryoValuesDeserializer.class);
+ EmbeddedChannel channel = authenticatedChannel(deser);
+ byte[] payload = { 4, 5, 6 };
+
+ channel.writeInbound(taskMessageFrame(payload));
+
+ List<Object> decoded = channel.readInbound();
+ assertEquals(1, decoded.size());
+ assertArrayEquals(payload, ((TaskMessage) decoded.get(0)).message());
+ assertTrue(channel.isActive());
+ }
+
+ @Test
+ public void framesAreDecodedWhenServerAuthenticationIsNotRequired() {
+ KryoValuesDeserializer deser = mock(KryoValuesDeserializer.class);
+ BackPressureStatus status = new BackPressureStatus();
+ when(deser.deserializeObject(any(byte[].class))).thenReturn(status);
+
+ EmbeddedChannel channel = new EmbeddedChannel(new
MessageDecoder(deser, false));
+
+ channel.writeInbound(backPressureFrame());
+
+ assertSame(status, channel.readInbound());
+ assertTrue(channel.isActive());
+ }
+}