package com.eucalyptus.objectstorage.pipeline.handlers; import com.eucalyptus.http.MappingHttpRequest; import com.google.common.base.Charsets; import org.jboss.netty.buffer.ChannelBuffer; import org.jboss.netty.buffer.ChannelBuffers; import org.jboss.netty.buffer.CompositeChannelBuffer; import org.jboss.netty.handler.codec.http.DefaultHttpChunk; import org.jboss.netty.handler.codec.http.HttpChunk; import org.jboss.netty.handler.codec.http.HttpHeaders; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import static java.util.stream.Collectors.partitioningBy; import static org.bouncycastle.asn1.x500.style.RFC4519Style.c; /** * Represents a stream of AWS chunks possibly consisting of a stream of HTTP chunks. Only the current chunk is cached internally. */ public class AwsChunkStream { private static final int MAX_BUFFER_COMPONENTS = 1024; private static final byte EQUALS = "=".getBytes(Charsets.UTF_8)[0]; private static final byte SEMICOLON = ";".getBytes(Charsets.UTF_8)[0]; StreamingHttpRequest currentRequest = new StreamingHttpRequest(); /** * A streaming Http request that encapsulates one or more Http chunks necessary to complete one or more Aws chunks. A single Http chunk * may complete multiple Aws chunks or multiple Http chunks may be needed to complete a single Aws chunk. */ public static class StreamingHttpRequest { MappingHttpRequest initialRequest; List<HttpChunk> httpChunks = new ArrayList<>(); List<AwsChunk> awsChunks; StreamingHttpRequest() { awsChunks = new ArrayList<>(); } StreamingHttpRequest(HttpChunk httpChunk, List<AwsChunk> awsChunks) { httpChunks.add(httpChunk); this.awsChunks = awsChunks; } /** * Appends the httpChunk and returns true if the append causes an AwsChunk to be completed. */ boolean append(HttpChunk httpChunk) { httpChunks.add(httpChunk); AwsChunk currentChunk = getOrCreateCurrentAwsChunk(); currentChunk.appendContent(httpChunk.getContent().copy()); if (!currentChunk.isParsed()) currentChunk.parseChunkInfo(); boolean chunkCompleted = false; while (currentChunk.isComplete()) { chunkCompleted = true; if (currentChunk.isFinal()) { break; } else { currentChunk = new AwsChunk(currentChunk); awsChunks.add(currentChunk); currentChunk.parseChunkInfo(); } } return chunkCompleted; } private AwsChunk getOrCreateCurrentAwsChunk() { AwsChunk result; if (awsChunks.isEmpty()) { result = new AwsChunk(); awsChunks.add(result); } else { result = awsChunks.get(awsChunks.size() - 1); } return result; } void setInitialRequest(MappingHttpRequest initialRequest) { this.initialRequest = initialRequest; } } public static class AwsChunk { public String previousSignature; public int chunkSize = -1; public String chunkSizeHex; public String chunkSignature; public int payloadStartIndex; private ChannelBuffer contents; AwsChunk() { } AwsChunk(AwsChunk previousChunk) { previousSignature = previousChunk.chunkSignature; // Next chunk starts at payload end + CRLF int nextChunkStartIndex = previousChunk.payloadEndIndex() + 2; // Create contents as a slice from previous chunk int readableBytes = previousChunk.contents.readableBytes(); int sliceLen = readableBytes - nextChunkStartIndex; contents = readableBytes > nextChunkStartIndex ? previousChunk.contents.slice(nextChunkStartIndex, sliceLen) : null; } boolean isParsed() { return chunkSignature != null; } boolean isComplete() { return chunkSignature != null && contents.readableBytes() >= payloadEndIndex(); } boolean isFinal() { return chunkSize == 0; } int payloadEndIndex() { return payloadStartIndex + chunkSize; } public ByteBuffer getPayload() { return isComplete() ? contents.slice(payloadStartIndex, chunkSize).toByteBuffer() : null; } public HttpChunk toHttpChunk() { ChannelBuffer httpChunkContent = isComplete() ? contents.slice(payloadStartIndex, chunkSize) : null; return new DefaultHttpChunk(httpChunkContent); } public ChannelBuffer getContents() { return isComplete() ? contents.slice(0, payloadStartIndex + chunkSize) : null; } private void appendContent(ChannelBuffer newContent) { if (contents == null) { contents = newContent; } else if (contents instanceof CompositeChannelBuffer) { CompositeChannelBuffer composite = (CompositeChannelBuffer) contents; if (composite.numComponents() >= MAX_BUFFER_COMPONENTS) { contents = ChannelBuffers.wrappedBuffer(composite.copy(), newContent); } else { List<ChannelBuffer> decomposed = composite.decompose(0, composite.readableBytes()); ChannelBuffer[] buffers = decomposed.toArray(new ChannelBuffer[decomposed.size() + 1]); buffers[buffers.length - 1] = newContent; contents = ChannelBuffers.wrappedBuffer(buffers); } } else { contents = ChannelBuffers.wrappedBuffer(contents, newContent); } } /** * Parses chunk length and signature. */ private void parseChunkInfo() { if (contents != null) { int sigStartIndex = 0; byte lastByte = 0; for (int i = 0; i < contents.readableBytes(); i++) { byte b = contents.getByte(i); if (chunkSizeHex == null) { if (b == SEMICOLON) { byte[] size = new byte[i]; contents.getBytes(0, size, 0, size.length); chunkSizeHex = new String(size); chunkSize = (int) Long.parseLong(chunkSizeHex, 16); } } else { if (b == EQUALS) { sigStartIndex = i + 1; } else if (sigStartIndex != 0 && lastByte == '\r' && b == '\n') { byte[] signature = new byte[i - sigStartIndex - 1]; contents.getBytes(sigStartIndex, signature, 0, signature.length); chunkSignature = new String(signature); payloadStartIndex = i + 1; break; } lastByte = b; } } } } } /** * Appends the HttpChunk to the AwsChunkStream and returns a StreamingHttpRequest if the append caused one or more AwsChunks to be * completed, else returns null. */ public StreamingHttpRequest append(HttpChunk httpChunk) { if (currentRequest.append(httpChunk)) { HttpChunk incompleteHttpChunk = currentRequest.httpChunks.remove(currentRequest.httpChunks.size() - 1); Map<Boolean, List<AwsChunk>> awsChunkPartition = currentRequest.awsChunks.stream().collect(partitioningBy(AwsChunk::isComplete)); List<AwsChunk> completeAwsChunks = awsChunkPartition.get(Boolean.TRUE); List<AwsChunk> incompleteAwsChunks = awsChunkPartition.get(Boolean.FALSE); StreamingHttpRequest result = currentRequest; result.awsChunks = completeAwsChunks; currentRequest = new StreamingHttpRequest(incompleteHttpChunk, incompleteAwsChunks); return result; } else { return null; } } /** * Parses and appends an initial HttpRequest that contains content to the AwsChunkStream. */ public StreamingHttpRequest append(MappingHttpRequest initialRequest) { if (initialRequest.getContent().readableBytes() > 0) { StreamingHttpRequest streamingRequest = append(new DefaultHttpChunk(initialRequest.getContent())); if (streamingRequest != null) streamingRequest.setInitialRequest(initialRequest); initialRequest.setChunked(true); return streamingRequest; } return null; } }