/* * Copyright 2009-2014 Eucalyptus Systems, Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see http://www.gnu.org/licenses/. * * Please contact Eucalyptus Systems, Inc., 6755 Hollister Ave., Goleta * CA 93117, USA or visit http://www.eucalyptus.com/licenses/ if you need * additional information or have any questions. */ package com.eucalyptus.objectstorage.pipeline.handlers; import org.apache.log4j.Logger; import org.jboss.netty.buffer.ChannelBuffer; import org.jboss.netty.buffer.ChannelBufferIndexFinder; import org.jboss.netty.buffer.ChannelBuffers; import org.jboss.netty.channel.Channel; import org.jboss.netty.channel.ChannelHandlerContext; import org.jboss.netty.channel.Channels; import org.jboss.netty.channel.MessageEvent; import com.eucalyptus.http.MappingHttpRequest; import com.eucalyptus.objectstorage.exceptions.s3.InternalErrorException; import com.eucalyptus.objectstorage.exceptions.s3.MalformedPOSTRequestException; import com.eucalyptus.objectstorage.util.OSGUtil; import com.eucalyptus.objectstorage.util.ObjectStorageProperties; import com.eucalyptus.records.Logs; import com.eucalyptus.util.LogUtil; import com.google.common.primitives.Bytes; /** * Extension of the ObjectStoragePUTAggregator that scans the input data for form-field delimiter to ensure that only the file/data is included and * not trailing form fields */ public class FormPOSTFilteringAggregator extends ObjectStoragePUTAggregator { private static final Logger LOG = Logger.getLogger(FormPOSTFilteringAggregator.class); // The boundary delimiter to use to filter the chunks. private byte[] boundaryBytes; private OSGUtil.ByteMatcherBeginningIndexFinder boundaryFinder; private long bytesSent; // Potentially a fully buffered previous chunk because partial boundary match may require next chunk to determine. private ChannelBuffer lastBufferedChunk; private int trailingBytesFound = 0; // Set to true once the end of the file data is reached and no more bytes should be sent through private boolean truncateRemaining; protected void setupBoundary(byte[] boundary) throws Exception { Logs.extreme().debug("Initialized boundary for form field filtering: '" + new String(boundary) + "'"); // Add the leading \r\n to make matching easy this.boundaryBytes = Bytes.concat(MultipartFormPartParser.PART_LINE_DELIMITER_BYTES, boundary); this.truncateRemaining = false; boundaryFinder = new OSGUtil.ByteMatcherBeginningIndexFinder(this.boundaryBytes); this.bytesSent = 0L; } @Override public void messageReceived(ChannelHandlerContext ctx, MessageEvent event) throws Exception { Logs.extreme().debug(LogUtil.dumpObject(event)); if (event.getMessage() instanceof MappingHttpRequest) { MappingHttpRequest httpRequest = (MappingHttpRequest) event.getMessage(); byte[] boundary = (byte[]) (httpRequest.getFormFields().get(ObjectStorageProperties.FormField.x_ignore_formboundary.toString())); if (boundary == null) { // Error LOG.error("No boundary found in the form. Cannot filter content. Failing POST form upload"); Channels.fireExceptionCaught(ctx, new InternalErrorException(null, "Error processing POST form content")); return; } else { setupBoundary(boundary); } } Logs.extreme().debug("Sending event to PUT aggregator"); super.messageReceived(ctx, event); } /** * Returns a channel buffer that is sanitized for form fields, will remove any subsequent fields found and set isLast = true * * @param data * @return */ protected ChannelBuffer scanForFormBoundary(final ChannelBuffer data) throws Exception { if (this.truncateRemaining) { this.trailingBytesFound += data.readableBytes(); return ChannelBuffers.wrappedBuffer(new byte[0]); } ChannelBuffer dataToScan = data; if (this.lastBufferedChunk != null) { dataToScan = ChannelBuffers.wrappedBuffer(this.lastBufferedChunk, data); } int lfIndex = 0; int endOffset = -1; int bufferSize = dataToScan.readableBytes(); // Scan through looking at all carriage returns to determine if beginning of boundary or end of data do { lfIndex = dataToScan.indexOf(lfIndex, dataToScan.readableBytes(), ChannelBufferIndexFinder.CR); if (lfIndex >= 0) { // Scan the rest starting there if (lfIndex + this.boundaryBytes.length > bufferSize) { // Can't be found here not enough space to check, save the slice in the last chunk this.lastBufferedChunk = dataToScan.copy(lfIndex, bufferSize - lfIndex); // Return what we know is okay. endOffset = lfIndex; } else { // May be able to find the delimiter, look for it in the next N bytes where N=boundary size endOffset = dataToScan.indexOf(lfIndex, lfIndex + this.boundaryBytes.length, this.boundaryFinder); if (endOffset >= lfIndex) { // Boundary found right after the crlf, send everything before the crlf this.lastBufferedChunk = null; this.truncateRemaining = true; this.trailingBytesFound = bufferSize - endOffset; if (this.trailingBytesFound > this.boundaryBytes.length + 4) { // boundary + --\r\n // If more left in message than this.lastBufferedChunk = null; throw new MalformedPOSTRequestException(null, "Must not have any trailing form parts after file content"); } } else { // didn't find it. continue lfIndex++; endOffset = -1; } } } else { this.lastBufferedChunk = null; endOffset = bufferSize; } } while (endOffset < 0); if (endOffset == 0) { return ChannelBuffers.wrappedBuffer(new byte[0]); } else if (endOffset == -1 || endOffset == dataToScan.readableBytes()) { return dataToScan; } else { return ChannelBuffers.copiedBuffer(dataToScan.slice(0, endOffset)); } } @Override protected void appendChunk(ChannelBuffer input, Channel channel) throws Exception { ChannelBuffer data = scanForFormBoundary(input); this.bytesSent += data.readableBytes(); super.appendChunk(data, channel); } }