All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.hbase.thirdparty.io.netty.handler.codec.stomp.StompSubframeDecoder Maven / Gradle / Ivy

/*
 * 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:
 *
 *   https://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.hbase.thirdparty.io.netty.handler.codec.stomp;

import org.apache.hbase.thirdparty.io.netty.buffer.ByteBuf;
import org.apache.hbase.thirdparty.io.netty.buffer.Unpooled;
import org.apache.hbase.thirdparty.io.netty.channel.ChannelHandlerContext;
import org.apache.hbase.thirdparty.io.netty.handler.codec.DecoderException;
import org.apache.hbase.thirdparty.io.netty.handler.codec.DecoderResult;
import org.apache.hbase.thirdparty.io.netty.handler.codec.ReplayingDecoder;
import org.apache.hbase.thirdparty.io.netty.handler.codec.TooLongFrameException;
import org.apache.hbase.thirdparty.io.netty.handler.codec.stomp.StompSubframeDecoder.State;
import org.apache.hbase.thirdparty.io.netty.util.ByteProcessor;
import org.apache.hbase.thirdparty.io.netty.util.internal.AppendableCharSequence;
import org.apache.hbase.thirdparty.io.netty.util.internal.StringUtil;
import org.apache.hbase.thirdparty.io.netty.util.internal.UnstableApi;

import java.util.List;

import static org.apache.hbase.thirdparty.io.netty.buffer.ByteBufUtil.*;
import static org.apache.hbase.thirdparty.io.netty.util.internal.ObjectUtil.*;

/**
 * Decodes {@link ByteBuf}s into {@link StompHeadersSubframe}s and {@link StompContentSubframe}s.
 *
 * 

Parameters to control memory consumption:

* {@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. *
* {@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. * *

Chunked Content

*

* 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. */ public class StompSubframeDecoder extends ReplayingDecoder { private static final int DEFAULT_CHUNK_SIZE = 8132; private static final int DEFAULT_MAX_LINE_LENGTH = 1024; @UnstableApi public enum State { SKIP_CONTROL_CHARACTERS, READ_HEADERS, READ_CONTENT, FINALIZE_FRAME_READ, BAD_FRAME, INVALID_CHUNK } private final Utf8LineParser commandParser; private final HeaderParser headerParser; private final int maxChunkSize; private int alreadyReadChunkSize; private LastStompContentSubframe lastContent; private long contentLength = -1; public StompSubframeDecoder() { this(DEFAULT_MAX_LINE_LENGTH, DEFAULT_CHUNK_SIZE); } public StompSubframeDecoder(boolean validateHeaders) { this(DEFAULT_MAX_LINE_LENGTH, DEFAULT_CHUNK_SIZE, validateHeaders); } public StompSubframeDecoder(int maxLineLength, int maxChunkSize) { this(maxLineLength, maxChunkSize, false); } public StompSubframeDecoder(int maxLineLength, int maxChunkSize, boolean validateHeaders) { super(State.SKIP_CONTROL_CHARACTERS); checkPositive(maxLineLength, "maxLineLength"); checkPositive(maxChunkSize, "maxChunkSize"); this.maxChunkSize = maxChunkSize; commandParser = new Utf8LineParser(new AppendableCharSequence(16), maxLineLength); headerParser = new HeaderParser(new AppendableCharSequence(128), maxLineLength, validateHeaders); } @Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception { switch (state()) { case SKIP_CONTROL_CHARACTERS: skipControlCharacters(in); checkpoint(State.READ_HEADERS); // Fall through. case READ_HEADERS: StompCommand command = StompCommand.UNKNOWN; StompHeadersSubframe frame = null; try { command = readCommand(in); frame = new DefaultStompHeadersSubframe(command); checkpoint(readHeaders(in, frame)); out.add(frame); } catch (Exception e) { if (frame == null) { frame = new DefaultStompHeadersSubframe(command); } frame.setDecoderResult(DecoderResult.failure(e)); out.add(frame); 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; } if (contentLength >= 0) { 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 { out.add(new DefaultStompContentSubframe(chunkBuffer)); return; } } else { 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 { out.add(new DefaultStompContentSubframe(chunkBuffer)); return; } } } // Fall through. case FINALIZE_FRAME_READ: skipNullCharacter(in); if (lastContent == null) { lastContent = LastStompContentSubframe.EMPTY_LAST_CONTENT; } out.add(lastContent); resetDecoder(); } } catch (Exception e) { if (lastContent != null) { lastContent.release(); lastContent = null; } StompContentSubframe errorContent = new DefaultLastStompContentSubframe(Unpooled.EMPTY_BUFFER); errorContent.setDecoderResult(DecoderResult.failure(e)); out.add(errorContent); checkpoint(State.BAD_FRAME); } } private StompCommand readCommand(ByteBuf in) { CharSequence commandSequence = commandParser.parse(in); if (commandSequence == null) { throw new DecoderException("Failed to read command from channel"); } String commandStr = commandSequence.toString(); try { return StompCommand.valueOf(commandStr); } catch (IllegalArgumentException iae) { throw new DecoderException("Cannot to parse command " + commandStr); } } private State readHeaders(ByteBuf buffer, StompHeadersSubframe headersSubframe) { StompHeaders headers = headersSubframe.headers(); for (;;) { boolean headerRead = headerParser.parseHeader(headersSubframe, buffer); if (!headerRead) { if (headers.contains(StompHeaders.CONTENT_LENGTH)) { contentLength = getContentLength(headers); if (contentLength == 0) { return State.FINALIZE_FRAME_READ; } } return State.READ_CONTENT; } } } private static long getContentLength(StompHeaders headers) { long contentLength = headers.getLong(StompHeaders.CONTENT_LENGTH, 0L); if (contentLength < 0) { throw new DecoderException(StompHeaders.CONTENT_LENGTH + " must be non-negative"); } return contentLength; } private static void skipNullCharacter(ByteBuf buffer) { byte b = buffer.readByte(); if (b != StompConstants.NUL) { throw new IllegalStateException("unexpected byte in buffer " + b + " while expecting NULL byte"); } } private static void skipControlCharacters(ByteBuf buffer) { byte b; for (;;) { b = buffer.readByte(); if (b != StompConstants.CR && b != StompConstants.LF) { buffer.readerIndex(buffer.readerIndex() - 1); break; } } } 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) { this.charSeq = checkNotNull(charSeq, "charSeq"); 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 { if (nextByte == StompConstants.CR) { ++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 appendTo(charSeq, (char) (interim | (nextByte & 0x3F))); interim = 0; } else if (nextByte >= 0) { // INITIAL BRANCH // The first 128 characters (US-ASCII) need one byte. appendTo(charSeq, (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); } else { // The rest of characters need three bytes. interim = (char) ((nextByte & 0x0F) << 12); nextRead = true; } return true; } protected void appendTo(AppendableCharSequence charSeq, char chr) { charSeq.append(chr); } protected void reset() { charSeq.reset(); lineLength = 0; interim = 0; nextRead = false; } } private static final class HeaderParser extends Utf8LineParser { private final boolean validateHeaders; private String name; private boolean valid; private boolean shouldUnescape; private boolean unescapeInProgress; HeaderParser(AppendableCharSequence charSeq, int maxLineLength, boolean validateHeaders) { super(charSeq, maxLineLength); this.validateHeaders = validateHeaders; } boolean parseHeader(StompHeadersSubframe headersSubframe, ByteBuf buf) { shouldUnescape = shouldUnescape(headersSubframe.command()); AppendableCharSequence value = super.parse(buf); if (value == null || (name == null && value.length() == 0)) { return false; } if (valid) { headersSubframe.headers().add(name, value.toString()); } else if (validateHeaders) { if (StringUtil.isNullOrEmpty(name)) { throw new IllegalArgumentException("received an invalid header line '" + value + '\''); } String line = name + ':' + value; throw new IllegalArgumentException("a header value or name contains a prohibited character ':'" + ", " + line); } return true; } @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; } } return super.process(nextByte); } @Override protected void appendTo(AppendableCharSequence charSeq, char chr) { if (!shouldUnescape) { super.appendTo(charSeq, chr); return; } if (chr == '\\') { if (unescapeInProgress) { super.appendTo(charSeq, chr); unescapeInProgress = false; } else { unescapeInProgress = true; } return; } if (unescapeInProgress) { if (chr == 'c') { charSeq.append(':'); } else if (chr == 'r') { charSeq.append('\r'); } else if (chr == 'n') { charSeq.append('\n'); } else { charSeq.append('\\').append(chr); throw new IllegalArgumentException("received an invalid escape header sequence '" + charSeq + '\''); } unescapeInProgress = false; return; } super.appendTo(charSeq, chr); } @Override protected void reset() { name = null; valid = false; unescapeInProgress = false; super.reset(); } private static boolean shouldUnescape(StompCommand command) { return command != StompCommand.CONNECT && command != StompCommand.CONNECTED; } } }