/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2014 Red Hat, Inc., and individual contributors
 * as indicated by the @author tags.
 *
 * Licensed 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.undertow.conduits;

import static org.xnio.Bits.allAreClear;
import static org.xnio.Bits.anyAreSet;
import static org.xnio.Bits.longBitMask;

import java.io.IOException;
import java.nio.ByteBuffer;

import org.xnio.conduits.Conduit;
import io.undertow.UndertowMessages;
import io.undertow.util.Attachable;
import io.undertow.util.AttachmentKey;
import io.undertow.util.HeaderMap;
import io.undertow.util.HttpString;

Utility class for reading chunked streams.
Author:Stuart Douglas
/** * Utility class for reading chunked streams. * * @author Stuart Douglas */
class ChunkReader<T extends Conduit> { private static final long FLAG_FINISHED = 1L << 62L; private static final long FLAG_READING_LENGTH = 1L << 61L; private static final long FLAG_READING_TILL_END_OF_LINE = 1L << 60L; private static final long FLAG_READING_NEWLINE = 1L << 59L; private static final long FLAG_READING_AFTER_LAST = 1L << 58L; private static final long MASK_COUNT = longBitMask(0, 56); private static final long LIMIT = Long.MAX_VALUE >> 4; private long state; private final Attachable attachable; private final AttachmentKey<HeaderMap> trailerAttachmentKey;
The trailer parser that stores the trailer parse state. If this class is not null it means that we are in the middle of parsing trailers.
/** * The trailer parser that stores the trailer parse state. If this class is not null it means * that we are in the middle of parsing trailers. */
private TrailerParser trailerParser; private final T conduit; ChunkReader(final Attachable attachable, final AttachmentKey<HeaderMap> trailerAttachmentKey, T conduit) { this.attachable = attachable; this.trailerAttachmentKey = trailerAttachmentKey; this.conduit = conduit; this.state = FLAG_READING_LENGTH; } public long readChunk(final ByteBuffer buf) throws IOException { long oldVal = state; long chunkRemaining = state & MASK_COUNT; if (chunkRemaining > 0 && !anyAreSet(state, FLAG_READING_AFTER_LAST | FLAG_READING_LENGTH | FLAG_READING_NEWLINE | FLAG_READING_TILL_END_OF_LINE)) { return chunkRemaining; } long newVal = oldVal & ~MASK_COUNT; try { if (anyAreSet(oldVal, FLAG_READING_AFTER_LAST)) { int ret = handleChunkedRequestEnd(buf); if (ret == -1) { newVal |= FLAG_FINISHED & ~FLAG_READING_AFTER_LAST; return -1; } return 0; } while (anyAreSet(newVal, FLAG_READING_NEWLINE)) { while (buf.hasRemaining()) { byte b = buf.get(); if (b == '\n') { newVal = newVal & ~FLAG_READING_NEWLINE | FLAG_READING_LENGTH; break; } } if (anyAreSet(newVal, FLAG_READING_NEWLINE)) { return 0; } } while (anyAreSet(newVal, FLAG_READING_LENGTH)) { while (buf.hasRemaining()) { byte b = buf.get(); if ((b >= '0' && b <= '9') || (b >= 'a' && b <= 'f') || (b >= 'A' && b <= 'F')) { if (chunkRemaining > LIMIT) { throw UndertowMessages.MESSAGES.chunkSizeTooLarge(); } chunkRemaining <<= 4; //shift it 4 bytes and then add the next value to the end chunkRemaining += Character.digit((char) b, 16); } else { if (b == '\n') { newVal = newVal & ~FLAG_READING_LENGTH; } else { newVal = newVal & ~FLAG_READING_LENGTH | FLAG_READING_TILL_END_OF_LINE; } break; } } if (anyAreSet(newVal, FLAG_READING_LENGTH)) { return 0; } } while (anyAreSet(newVal, FLAG_READING_TILL_END_OF_LINE)) { while (buf.hasRemaining()) { if (buf.get() == '\n') { newVal = newVal & ~FLAG_READING_TILL_END_OF_LINE; break; } } if (anyAreSet(newVal, FLAG_READING_TILL_END_OF_LINE)) { return 0; } } //we have our chunk size, check to make sure it was not the last chunk if (allAreClear(newVal, FLAG_READING_NEWLINE | FLAG_READING_LENGTH | FLAG_READING_TILL_END_OF_LINE) && chunkRemaining == 0) { newVal |= FLAG_READING_AFTER_LAST; int ret = handleChunkedRequestEnd(buf); if (ret == -1) { newVal |= FLAG_FINISHED & ~FLAG_READING_AFTER_LAST; return -1; } return 0; } return chunkRemaining; } finally { state = newVal | chunkRemaining; } } public long getChunkRemaining() { if (anyAreSet(state, FLAG_FINISHED)) { return -1; } if (anyAreSet(state, FLAG_READING_LENGTH | FLAG_READING_TILL_END_OF_LINE | FLAG_READING_NEWLINE | FLAG_READING_AFTER_LAST)) { return 0; } return state & MASK_COUNT; } public void setChunkRemaining(final long remaining) { if (remaining < 0 || anyAreSet(state, FLAG_READING_LENGTH | FLAG_READING_TILL_END_OF_LINE | FLAG_READING_NEWLINE | FLAG_READING_AFTER_LAST)) { return; } long old = state; long oldRemaining = old & MASK_COUNT; if (remaining == 0 && oldRemaining != 0) { //if oldRemaining is zero it could be that no data has been read yet //and the correct state is READING_LENGTH old |= FLAG_READING_NEWLINE; } state = (old & ~MASK_COUNT) | remaining; } private int handleChunkedRequestEnd(ByteBuffer buffer) throws IOException { if (trailerParser != null) { return trailerParser.handle(buffer); } while (buffer.hasRemaining()) { byte b = buffer.get(); if (b == '\n') { return -1; } else if (b != '\r') { buffer.position(buffer.position() - 1); trailerParser = new TrailerParser(); return trailerParser.handle(buffer); } } return 0; }
Class that parses HTTP trailers. We don't just re-use the http parser code because it is complicated enough already, and this is not used very often so the performance benefits should not matter.
/** * Class that parses HTTP trailers. We don't just re-use the http parser code because it is complicated enough * already, and this is not used very often so the performance benefits should not matter. */
private final class TrailerParser { private HeaderMap headerMap = new HeaderMap(); private StringBuilder builder = new StringBuilder(); private HttpString httpString; int state = 0; private static final int STATE_TRAILER_NAME = 0; private static final int STATE_TRAILER_VALUE = 1; private static final int STATE_ENDING = 2; public int handle(ByteBuffer buf) throws IOException { while (buf.hasRemaining()) { final byte b = buf.get(); if (state == STATE_TRAILER_NAME) { if (b == '\r') { if (builder.length() == 0) { state = STATE_ENDING; } else { throw UndertowMessages.MESSAGES.couldNotDecodeTrailers(); } } else if (b == '\n') { if (builder.length() == 0) { attachable.putAttachment(trailerAttachmentKey, headerMap); return -1; } else { throw UndertowMessages.MESSAGES.couldNotDecodeTrailers(); } } else if (b == ':') { httpString = HttpString.tryFromString(builder.toString().trim()); state = STATE_TRAILER_VALUE; builder.setLength(0); } else { builder.append((char) b); } } else if (state == STATE_TRAILER_VALUE) { if (b == '\n') { headerMap.put(httpString, builder.toString().trim()); httpString = null; builder.setLength(0); state = STATE_TRAILER_NAME; } else if (b != '\r') { builder.append((char) b); } } else if (state == STATE_ENDING) { if (b == '\n') { if (attachable != null) { attachable.putAttachment(trailerAttachmentKey, headerMap); } return -1; } else { throw UndertowMessages.MESSAGES.couldNotDecodeTrailers(); } } else { throw new IllegalStateException(); } } return 0; } } }