2014-01-30 21:18:30 +01:00
|
|
|
/*
|
|
|
|
* Copyright 2014 The Netty Project
|
|
|
|
*
|
|
|
|
* The Netty Project 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 io.netty.handler.codec.stomp;
|
|
|
|
|
2019-12-16 21:00:32 +01:00
|
|
|
|
2014-01-30 21:18:30 +01:00
|
|
|
import io.netty.buffer.ByteBuf;
|
|
|
|
import io.netty.buffer.Unpooled;
|
|
|
|
import io.netty.channel.ChannelHandlerContext;
|
|
|
|
import io.netty.handler.codec.DecoderException;
|
|
|
|
import io.netty.handler.codec.DecoderResult;
|
|
|
|
import io.netty.handler.codec.ReplayingDecoder;
|
|
|
|
import io.netty.handler.codec.TooLongFrameException;
|
2014-06-04 09:39:50 +02:00
|
|
|
import io.netty.handler.codec.stomp.StompSubframeDecoder.State;
|
2019-11-06 12:07:38 +01:00
|
|
|
import io.netty.util.ByteProcessor;
|
2014-06-04 09:39:50 +02:00
|
|
|
import io.netty.util.internal.AppendableCharSequence;
|
2019-12-10 11:27:32 +01:00
|
|
|
import io.netty.util.internal.ObjectUtil;
|
2019-11-06 12:07:38 +01:00
|
|
|
import io.netty.util.internal.StringUtil;
|
2014-06-04 09:39:50 +02:00
|
|
|
|
2019-12-10 11:27:32 +01:00
|
|
|
import java.util.Objects;
|
2019-11-06 12:07:38 +01:00
|
|
|
|
|
|
|
import static io.netty.buffer.ByteBufUtil.*;
|
2014-01-30 21:18:30 +01:00
|
|
|
|
|
|
|
/**
|
2019-11-06 12:07:38 +01:00
|
|
|
* Decodes {@link ByteBuf}s into {@link StompHeadersSubframe}s and {@link StompContentSubframe}s.
|
2014-01-30 21:18:30 +01:00
|
|
|
*
|
|
|
|
* <h3>Parameters to control memory consumption: </h3>
|
2019-11-06 12:07:38 +01:00
|
|
|
* {@code maxLineLength} the maximum length of line - restricts length of command and header lines If the length of the
|
|
|
|
* initial line exceeds this value, a {@link TooLongFrameException} will be raised.
|
2014-01-30 21:18:30 +01:00
|
|
|
* <br>
|
2019-11-06 12:07:38 +01:00
|
|
|
* {@code maxChunkSize} The maximum length of the content or each chunk. If the content length (or the length of each
|
|
|
|
* chunk) exceeds this value, the content or chunk ill be split into multiple {@link StompContentSubframe}s whose length
|
|
|
|
* is {@code maxChunkSize} at maximum.
|
2014-01-30 21:18:30 +01:00
|
|
|
*
|
|
|
|
* <h3>Chunked Content</h3>
|
2019-11-06 12:07:38 +01:00
|
|
|
* <p>
|
|
|
|
* If the content of a stomp message is greater than {@code maxChunkSize} the transfer encoding of the HTTP message is
|
|
|
|
* 'chunked', this decoder generates multiple {@link StompContentSubframe} instances to avoid excessive memory
|
|
|
|
* consumption. Note, that every message, even with no content decodes with {@link LastStompContentSubframe} at the end
|
|
|
|
* to simplify upstream message parsing.
|
2014-01-30 21:18:30 +01:00
|
|
|
*/
|
2014-06-04 09:39:50 +02:00
|
|
|
public class StompSubframeDecoder extends ReplayingDecoder<State> {
|
|
|
|
|
|
|
|
private static final int DEFAULT_CHUNK_SIZE = 8132;
|
|
|
|
private static final int DEFAULT_MAX_LINE_LENGTH = 1024;
|
|
|
|
|
|
|
|
enum State {
|
|
|
|
SKIP_CONTROL_CHARACTERS,
|
|
|
|
READ_HEADERS,
|
|
|
|
READ_CONTENT,
|
|
|
|
FINALIZE_FRAME_READ,
|
|
|
|
BAD_FRAME,
|
|
|
|
INVALID_CHUNK
|
|
|
|
}
|
|
|
|
|
2019-11-06 12:07:38 +01:00
|
|
|
private final Utf8LineParser commandParser;
|
|
|
|
private final HeaderParser headerParser;
|
2014-06-04 09:39:50 +02:00
|
|
|
private final int maxChunkSize;
|
2014-01-30 21:18:30 +01:00
|
|
|
private int alreadyReadChunkSize;
|
2014-06-04 09:39:50 +02:00
|
|
|
private LastStompContentSubframe lastContent;
|
2015-09-25 14:29:11 +02:00
|
|
|
private long contentLength = -1;
|
2014-01-30 21:18:30 +01:00
|
|
|
|
2014-06-04 09:39:50 +02:00
|
|
|
public StompSubframeDecoder() {
|
2014-01-30 21:18:30 +01:00
|
|
|
this(DEFAULT_MAX_LINE_LENGTH, DEFAULT_CHUNK_SIZE);
|
|
|
|
}
|
|
|
|
|
2017-08-28 22:58:26 +02:00
|
|
|
public StompSubframeDecoder(boolean validateHeaders) {
|
|
|
|
this(DEFAULT_MAX_LINE_LENGTH, DEFAULT_CHUNK_SIZE, validateHeaders);
|
|
|
|
}
|
|
|
|
|
2014-06-04 09:39:50 +02:00
|
|
|
public StompSubframeDecoder(int maxLineLength, int maxChunkSize) {
|
2017-08-28 22:58:26 +02:00
|
|
|
this(maxLineLength, maxChunkSize, false);
|
|
|
|
}
|
|
|
|
|
|
|
|
public StompSubframeDecoder(int maxLineLength, int maxChunkSize, boolean validateHeaders) {
|
2014-01-30 21:18:30 +01:00
|
|
|
super(State.SKIP_CONTROL_CHARACTERS);
|
2019-12-10 11:27:32 +01:00
|
|
|
ObjectUtil.checkPositive(maxLineLength, "maxLineLength");
|
|
|
|
ObjectUtil.checkPositive(maxChunkSize, "maxChunkSize");
|
2014-01-30 21:18:30 +01:00
|
|
|
this.maxChunkSize = maxChunkSize;
|
2019-11-06 12:07:38 +01:00
|
|
|
commandParser = new Utf8LineParser(new AppendableCharSequence(16), maxLineLength);
|
|
|
|
headerParser = new HeaderParser(new AppendableCharSequence(128), maxLineLength, validateHeaders);
|
2014-01-30 21:18:30 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
2019-12-16 21:00:32 +01:00
|
|
|
protected void decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
|
2014-01-30 21:18:30 +01:00
|
|
|
switch (state()) {
|
|
|
|
case SKIP_CONTROL_CHARACTERS:
|
|
|
|
skipControlCharacters(in);
|
|
|
|
checkpoint(State.READ_HEADERS);
|
2014-06-04 09:39:50 +02:00
|
|
|
// Fall through.
|
2014-01-30 21:18:30 +01:00
|
|
|
case READ_HEADERS:
|
|
|
|
StompCommand command = StompCommand.UNKNOWN;
|
2014-06-04 09:39:50 +02:00
|
|
|
StompHeadersSubframe frame = null;
|
2014-01-30 21:18:30 +01:00
|
|
|
try {
|
|
|
|
command = readCommand(in);
|
2014-06-04 09:39:50 +02:00
|
|
|
frame = new DefaultStompHeadersSubframe(command);
|
2014-01-30 21:18:30 +01:00
|
|
|
checkpoint(readHeaders(in, frame.headers()));
|
2019-12-16 21:00:32 +01:00
|
|
|
ctx.fireChannelRead(frame);
|
2014-01-30 21:18:30 +01:00
|
|
|
} catch (Exception e) {
|
|
|
|
if (frame == null) {
|
2014-06-04 09:39:50 +02:00
|
|
|
frame = new DefaultStompHeadersSubframe(command);
|
2014-01-30 21:18:30 +01:00
|
|
|
}
|
|
|
|
frame.setDecoderResult(DecoderResult.failure(e));
|
2019-12-16 21:00:32 +01:00
|
|
|
ctx.fireChannelRead(frame);
|
2014-01-30 21:18:30 +01:00
|
|
|
checkpoint(State.BAD_FRAME);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case BAD_FRAME:
|
|
|
|
in.skipBytes(actualReadableBytes());
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
try {
|
|
|
|
switch (state()) {
|
|
|
|
case READ_CONTENT:
|
|
|
|
int toRead = in.readableBytes();
|
|
|
|
if (toRead == 0) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (toRead > maxChunkSize) {
|
|
|
|
toRead = maxChunkSize;
|
|
|
|
}
|
2017-08-28 22:58:26 +02:00
|
|
|
if (contentLength >= 0) {
|
2015-09-25 14:29:11 +02:00
|
|
|
int remainingLength = (int) (contentLength - alreadyReadChunkSize);
|
|
|
|
if (toRead > remainingLength) {
|
|
|
|
toRead = remainingLength;
|
|
|
|
}
|
|
|
|
ByteBuf chunkBuffer = readBytes(ctx.alloc(), in, toRead);
|
|
|
|
if ((alreadyReadChunkSize += toRead) >= contentLength) {
|
|
|
|
lastContent = new DefaultLastStompContentSubframe(chunkBuffer);
|
|
|
|
checkpoint(State.FINALIZE_FRAME_READ);
|
|
|
|
} else {
|
2019-12-16 21:00:32 +01:00
|
|
|
ctx.fireChannelRead(new DefaultStompContentSubframe(chunkBuffer));
|
2015-09-25 14:29:11 +02:00
|
|
|
return;
|
|
|
|
}
|
2014-01-30 21:18:30 +01:00
|
|
|
} else {
|
2015-09-25 14:29:11 +02:00
|
|
|
int nulIndex = indexOf(in, in.readerIndex(), in.writerIndex(), StompConstants.NUL);
|
|
|
|
if (nulIndex == in.readerIndex()) {
|
|
|
|
checkpoint(State.FINALIZE_FRAME_READ);
|
|
|
|
} else {
|
|
|
|
if (nulIndex > 0) {
|
|
|
|
toRead = nulIndex - in.readerIndex();
|
|
|
|
} else {
|
|
|
|
toRead = in.writerIndex() - in.readerIndex();
|
|
|
|
}
|
|
|
|
ByteBuf chunkBuffer = readBytes(ctx.alloc(), in, toRead);
|
|
|
|
alreadyReadChunkSize += toRead;
|
|
|
|
if (nulIndex > 0) {
|
|
|
|
lastContent = new DefaultLastStompContentSubframe(chunkBuffer);
|
|
|
|
checkpoint(State.FINALIZE_FRAME_READ);
|
|
|
|
} else {
|
2019-12-16 21:00:32 +01:00
|
|
|
ctx.fireChannelRead(new DefaultStompContentSubframe(chunkBuffer));
|
2015-09-25 14:29:11 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
2014-01-30 21:18:30 +01:00
|
|
|
}
|
2014-06-04 09:39:50 +02:00
|
|
|
// Fall through.
|
2014-01-30 21:18:30 +01:00
|
|
|
case FINALIZE_FRAME_READ:
|
|
|
|
skipNullCharacter(in);
|
|
|
|
if (lastContent == null) {
|
2014-06-04 09:39:50 +02:00
|
|
|
lastContent = LastStompContentSubframe.EMPTY_LAST_CONTENT;
|
2014-01-30 21:18:30 +01:00
|
|
|
}
|
2019-12-16 21:00:32 +01:00
|
|
|
ctx.fireChannelRead(lastContent);
|
2014-01-30 21:18:30 +01:00
|
|
|
resetDecoder();
|
|
|
|
}
|
|
|
|
} catch (Exception e) {
|
2014-06-04 09:39:50 +02:00
|
|
|
StompContentSubframe errorContent = new DefaultLastStompContentSubframe(Unpooled.EMPTY_BUFFER);
|
2014-01-30 21:18:30 +01:00
|
|
|
errorContent.setDecoderResult(DecoderResult.failure(e));
|
2019-12-16 21:00:32 +01:00
|
|
|
ctx.fireChannelRead(errorContent);
|
2014-01-30 21:18:30 +01:00
|
|
|
checkpoint(State.BAD_FRAME);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private StompCommand readCommand(ByteBuf in) {
|
2019-11-06 12:07:38 +01:00
|
|
|
CharSequence commandSequence = commandParser.parse(in);
|
|
|
|
if (commandSequence == null) {
|
|
|
|
throw new DecoderException("Failed to read command from channel");
|
|
|
|
}
|
|
|
|
String commandStr = commandSequence.toString();
|
2014-01-30 21:18:30 +01:00
|
|
|
try {
|
2019-11-06 12:07:38 +01:00
|
|
|
return StompCommand.valueOf(commandStr);
|
2014-01-30 21:18:30 +01:00
|
|
|
} catch (IllegalArgumentException iae) {
|
2019-11-06 12:07:38 +01:00
|
|
|
throw new DecoderException("Cannot to parse command " + commandStr);
|
2014-01-30 21:18:30 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private State readHeaders(ByteBuf buffer, StompHeaders headers) {
|
2014-06-04 09:39:50 +02:00
|
|
|
for (;;) {
|
2019-11-06 12:07:38 +01:00
|
|
|
boolean headerRead = headerParser.parseHeader(headers, buffer);
|
2018-01-03 13:08:48 +01:00
|
|
|
if (!headerRead) {
|
|
|
|
if (headers.contains(StompHeaders.CONTENT_LENGTH)) {
|
2019-11-06 12:07:38 +01:00
|
|
|
contentLength = getContentLength(headers);
|
2017-08-28 22:58:26 +02:00
|
|
|
if (contentLength == 0) {
|
2015-09-25 14:29:11 +02:00
|
|
|
return State.FINALIZE_FRAME_READ;
|
2014-01-30 21:18:30 +01:00
|
|
|
}
|
|
|
|
}
|
2015-09-25 14:29:11 +02:00
|
|
|
return State.READ_CONTENT;
|
2014-01-30 21:18:30 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-11-06 12:07:38 +01:00
|
|
|
private static long getContentLength(StompHeaders headers) {
|
|
|
|
long contentLength = headers.getLong(StompHeaders.CONTENT_LENGTH, 0L);
|
2015-09-25 14:29:11 +02:00
|
|
|
if (contentLength < 0) {
|
|
|
|
throw new DecoderException(StompHeaders.CONTENT_LENGTH + " must be non-negative");
|
|
|
|
}
|
|
|
|
return contentLength;
|
2014-06-04 09:39:50 +02:00
|
|
|
}
|
|
|
|
|
2014-01-30 21:18:30 +01:00
|
|
|
private static void skipNullCharacter(ByteBuf buffer) {
|
|
|
|
byte b = buffer.readByte();
|
2014-06-04 09:39:50 +02:00
|
|
|
if (b != StompConstants.NUL) {
|
2014-01-30 21:18:30 +01:00
|
|
|
throw new IllegalStateException("unexpected byte in buffer " + b + " while expecting NULL byte");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private static void skipControlCharacters(ByteBuf buffer) {
|
|
|
|
byte b;
|
2014-06-04 09:39:50 +02:00
|
|
|
for (;;) {
|
2014-01-30 21:18:30 +01:00
|
|
|
b = buffer.readByte();
|
|
|
|
if (b != StompConstants.CR && b != StompConstants.LF) {
|
|
|
|
buffer.readerIndex(buffer.readerIndex() - 1);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-11-06 12:07:38 +01:00
|
|
|
private void resetDecoder() {
|
|
|
|
checkpoint(State.SKIP_CONTROL_CHARACTERS);
|
|
|
|
contentLength = -1;
|
|
|
|
alreadyReadChunkSize = 0;
|
|
|
|
lastContent = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
private static class Utf8LineParser implements ByteProcessor {
|
|
|
|
|
|
|
|
private final AppendableCharSequence charSeq;
|
|
|
|
private final int maxLineLength;
|
|
|
|
|
|
|
|
private int lineLength;
|
|
|
|
private char interim;
|
|
|
|
private boolean nextRead;
|
|
|
|
|
|
|
|
Utf8LineParser(AppendableCharSequence charSeq, int maxLineLength) {
|
2019-12-10 11:27:32 +01:00
|
|
|
this.charSeq = Objects.requireNonNull(charSeq, "charSeq");
|
2019-11-06 12:07:38 +01:00
|
|
|
this.maxLineLength = maxLineLength;
|
|
|
|
}
|
|
|
|
|
|
|
|
AppendableCharSequence parse(ByteBuf byteBuf) {
|
|
|
|
reset();
|
|
|
|
int offset = byteBuf.forEachByte(this);
|
|
|
|
if (offset == -1) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
byteBuf.readerIndex(offset + 1);
|
|
|
|
return charSeq;
|
|
|
|
}
|
|
|
|
|
|
|
|
AppendableCharSequence charSequence() {
|
|
|
|
return charSeq;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public boolean process(byte nextByte) throws Exception {
|
2014-01-30 21:18:30 +01:00
|
|
|
if (nextByte == StompConstants.CR) {
|
2019-11-06 12:07:38 +01:00
|
|
|
++lineLength;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (nextByte == StompConstants.LF) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (++lineLength > maxLineLength) {
|
|
|
|
throw new TooLongFrameException("An STOMP line is larger than " + maxLineLength + " bytes.");
|
|
|
|
}
|
|
|
|
|
|
|
|
// 1 byte - 0xxxxxxx - 7 bits
|
|
|
|
// 2 byte - 110xxxxx 10xxxxxx - 11 bits
|
|
|
|
// 3 byte - 1110xxxx 10xxxxxx 10xxxxxx - 16 bits
|
|
|
|
if (nextRead) {
|
|
|
|
interim |= (nextByte & 0x3F) << 6;
|
|
|
|
nextRead = false;
|
|
|
|
} else if (interim != 0) { // flush 2 or 3 byte
|
|
|
|
charSeq.append((char) (interim | (nextByte & 0x3F)));
|
|
|
|
interim = 0;
|
|
|
|
} else if (nextByte >= 0) { // INITIAL BRANCH
|
|
|
|
// The first 128 characters (US-ASCII) need one byte.
|
|
|
|
charSeq.append((char) nextByte);
|
|
|
|
} else if ((nextByte & 0xE0) == 0xC0) {
|
|
|
|
// The next 1920 characters need two bytes and we can define
|
|
|
|
// a first byte by mask 110xxxxx.
|
|
|
|
interim = (char) ((nextByte & 0x1F) << 6);
|
2014-01-30 21:18:30 +01:00
|
|
|
} else {
|
2019-11-06 12:07:38 +01:00
|
|
|
// The rest of characters need three bytes.
|
|
|
|
interim = (char) ((nextByte & 0x0F) << 12);
|
|
|
|
nextRead = true;
|
2014-01-30 21:18:30 +01:00
|
|
|
}
|
2019-11-06 12:07:38 +01:00
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
protected void reset() {
|
|
|
|
charSeq.reset();
|
|
|
|
lineLength = 0;
|
|
|
|
interim = 0;
|
|
|
|
nextRead = false;
|
2014-01-30 21:18:30 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-11-06 12:07:38 +01:00
|
|
|
private static final class HeaderParser extends Utf8LineParser {
|
|
|
|
|
|
|
|
private final boolean validateHeaders;
|
|
|
|
|
|
|
|
private String name;
|
|
|
|
private boolean valid;
|
|
|
|
|
|
|
|
HeaderParser(AppendableCharSequence charSeq, int maxLineLength, boolean validateHeaders) {
|
|
|
|
super(charSeq, maxLineLength);
|
|
|
|
this.validateHeaders = validateHeaders;
|
|
|
|
}
|
|
|
|
|
|
|
|
boolean parseHeader(StompHeaders headers, ByteBuf buf) {
|
|
|
|
AppendableCharSequence value = super.parse(buf);
|
|
|
|
if (value == null || (name == null && value.length() == 0)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (valid) {
|
|
|
|
headers.add(name, value.toString());
|
|
|
|
} else if (validateHeaders) {
|
|
|
|
if (StringUtil.isNullOrEmpty(name)) {
|
|
|
|
throw new IllegalArgumentException("received an invalid header line '" + value.toString() + '\'');
|
2018-01-03 13:08:48 +01:00
|
|
|
}
|
2019-11-06 12:07:38 +01:00
|
|
|
String line = name + ':' + value.toString();
|
|
|
|
throw new IllegalArgumentException("a header value or name contains a prohibited character ':'"
|
|
|
|
+ ", " + line);
|
2018-01-03 13:08:48 +01:00
|
|
|
}
|
2019-11-06 12:07:38 +01:00
|
|
|
return true;
|
2018-01-03 13:08:48 +01:00
|
|
|
}
|
|
|
|
|
2019-11-06 12:07:38 +01:00
|
|
|
@Override
|
|
|
|
public boolean process(byte nextByte) throws Exception {
|
|
|
|
if (nextByte == StompConstants.COLON) {
|
|
|
|
if (name == null) {
|
|
|
|
AppendableCharSequence charSeq = charSequence();
|
|
|
|
if (charSeq.length() != 0) {
|
|
|
|
name = charSeq.substring(0, charSeq.length());
|
|
|
|
charSeq.reset();
|
|
|
|
valid = true;
|
|
|
|
return true;
|
|
|
|
} else {
|
|
|
|
name = StringUtil.EMPTY_STRING;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
valid = false;
|
|
|
|
}
|
|
|
|
}
|
2018-01-03 13:08:48 +01:00
|
|
|
|
2019-11-06 12:07:38 +01:00
|
|
|
return super.process(nextByte);
|
|
|
|
}
|
2018-01-03 13:08:48 +01:00
|
|
|
|
2019-11-06 12:07:38 +01:00
|
|
|
@Override
|
|
|
|
protected void reset() {
|
|
|
|
name = null;
|
|
|
|
valid = false;
|
|
|
|
super.reset();
|
|
|
|
}
|
2014-01-30 21:18:30 +01:00
|
|
|
}
|
|
|
|
}
|