/*
* 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
*/
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 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.
*/
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')) {
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.
*/
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;
}
}
}