blob: 989280b667fd2f62c867ec1a75644d6511b3114d [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF 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
*
* 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 org.apache.chemistry.opencmis.server.impl.browser;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.apache.chemistry.opencmis.commons.exceptions.CmisBaseException;
import org.apache.chemistry.opencmis.commons.exceptions.CmisInvalidArgumentException;
import org.apache.chemistry.opencmis.commons.exceptions.CmisRuntimeException;
import org.apache.chemistry.opencmis.commons.impl.Constants;
import org.apache.chemistry.opencmis.commons.impl.IOUtils;
import org.apache.chemistry.opencmis.commons.impl.MimeHelper;
import org.apache.chemistry.opencmis.commons.server.TempStoreOutputStream;
import org.apache.chemistry.opencmis.server.shared.TempStoreOutputStreamFactory;
/**
* Simple multi-part parser, following all necessary standards for the CMIS
* browser binding.
*/
public class MultipartParser {
public static final String MULTIPART = "multipart/";
private static final String CHARSET_FIELD = "_charset_";
private static final int MAX_FIELD_BYTES = 10 * 1024 * 1024;
private static final int BUFFER_SIZE = 256 * 1024;
private static final byte CR = 0x0D;
private static final byte LF = 0x0A;
private static final byte DASH = 0x2D;
private static final byte[] BOUNDARY_PREFIX = { CR, LF, DASH, DASH };
private final HttpServletRequest request;
private final TempStoreOutputStreamFactory streamFactory;
private final InputStream requestStream;
private byte[] boundary;
private int[] badCharacters;
private int[] goodSuffixes;
private byte[] buffer;
private byte[] buffer2;
private int bufferPosition;
private int bufferCount;
private boolean eof;
private int fieldBytes;
private boolean hasContent;
private Map<String, String> headers;
private String filename;
private String contentType;
private BigInteger contentSize;
private InputStream contentStream;
private Map<String, String[]> fields;
private Map<String, byte[][]> rawFields;
private String charset = IOUtils.ISO_8859_1;
public MultipartParser(HttpServletRequest request, TempStoreOutputStreamFactory streamFactory) throws IOException {
this.request = request;
this.streamFactory = streamFactory;
this.requestStream = request.getInputStream();
extractBoundary();
buffer = new byte[BUFFER_SIZE + boundary.length];
buffer2 = new byte[buffer.length];
bufferPosition = 0;
bufferCount = 0;
eof = false;
hasContent = false;
fieldBytes = 0;
fields = new HashMap<String, String[]>();
rawFields = new HashMap<String, byte[][]>();
skipPreamble();
}
private void addField(String name, String value) {
String[] values = fields.get(name);
if (values == null) {
fields.put(name, new String[] { value });
} else {
String[] newValues = new String[values.length + 1];
System.arraycopy(values, 0, newValues, 0, values.length);
newValues[newValues.length - 1] = value;
fields.put(name, newValues);
}
}
private void addRawField(String name, byte[] value) {
byte[][] values = rawFields.get(name);
if (values == null) {
byte[][] newValue = new byte[][] { value };
rawFields.put(name, newValue);
} else {
byte[][] newValues = new byte[values.length + 1][];
System.arraycopy(values, 0, newValues, 0, values.length);
newValues[newValues.length - 1] = value;
rawFields.put(name, newValues);
}
}
private void extractBoundary() {
String requestContentType = request.getContentType();
// parse content type and extract boundary
byte[] extractedBoundary = MimeHelper.getBoundaryFromMultiPart(requestContentType);
if (extractedBoundary == null) {
throw new CmisInvalidArgumentException("Invalid multipart request!");
}
boundary = new byte[BOUNDARY_PREFIX.length + extractedBoundary.length];
System.arraycopy(BOUNDARY_PREFIX, 0, boundary, 0, BOUNDARY_PREFIX.length);
System.arraycopy(extractedBoundary, 0, boundary, BOUNDARY_PREFIX.length, extractedBoundary.length);
// prepare boundary search
int m = boundary.length;
badCharacters = new int[256];
Arrays.fill(badCharacters, -1);
for (int j = 0; j < m; j++) {
badCharacters[boundary[j] & 0xff] = j;
}
int[] f = new int[m + 1];
goodSuffixes = new int[m + 1];
int i = m;
int j = m + 1;
f[i] = j;
while (i > 0) {
while (j <= m && boundary[i - 1] != boundary[j - 1]) {
if (goodSuffixes[j] == 0) {
goodSuffixes[j] = j - i;
}
j = f[j];
}
i--;
j--;
f[i] = j;
}
j = f[0];
for (i = 0; i <= m; i++) {
if (goodSuffixes[i] == 0) {
goodSuffixes[i] = j;
}
if (i == j) {
j = f[j];
}
}
}
private int findBoundary() {
if (bufferCount < boundary.length) {
if (eof) {
throw new CmisInvalidArgumentException("Unexpected end of stream!");
} else {
return -1;
}
}
int m = boundary.length;
int i = 0;
while (i <= bufferCount - m) {
int j = m - 1;
while (j >= 0 && boundary[j] == buffer[i + j]) {
j--;
}
if (j < 0) {
return i;
} else {
i += Math.max(goodSuffixes[j + 1], j - badCharacters[buffer[i + j] & 0xff]);
}
}
return -1;
}
private void readBuffer() throws IOException {
if (bufferPosition < bufferCount) {
System.arraycopy(buffer, bufferPosition, buffer2, 0, bufferCount - bufferPosition);
bufferCount = bufferCount - bufferPosition;
byte[] tmpBuffer = buffer2;
buffer2 = buffer;
buffer = tmpBuffer;
} else {
bufferCount = 0;
}
bufferPosition = 0;
if (eof) {
return;
}
while (true) {
int r = requestStream.read(buffer, bufferCount, buffer.length - bufferCount);
if (r == -1) {
eof = true;
break;
}
bufferCount += r;
if (buffer.length == bufferCount) {
break;
}
}
}
private int nextByte() throws IOException {
if (bufferCount == 0) {
if (eof) {
return -1;
} else {
readBuffer();
return nextByte();
}
}
if (bufferCount > bufferPosition) {
return buffer[bufferPosition++] & 0xff;
}
readBuffer();
return nextByte();
}
private String readLine() throws IOException {
StringBuilder sb = new StringBuilder();
int r;
while ((r = nextByte()) > -1) {
if (r == CR) {
if (nextByte() != LF) {
throw new CmisInvalidArgumentException("Invalid multipart request!");
}
break;
}
sb.append((char) r);
}
return sb.toString();
}
private void readHeaders() throws IOException {
int b = nextByte();
if (b == -1) {
throw new CmisInvalidArgumentException("Unexpected end of stream!");
}
if (b == DASH) {
b = nextByte();
if (b == DASH) {
// expected end of stream
headers = null;
return;
}
} else if (b == CR) {
b = nextByte();
if (b == LF) {
parseHeaders();
return;
}
}
throw new CmisInvalidArgumentException("Invalid multipart request!");
}
private void parseHeaders() throws IOException {
headers = new HashMap<String, String>();
while (true) {
String line = readLine();
if (line.length() == 0) {
// empty line -> end of headers
break;
}
int x = line.indexOf(':');
if (x > 0) {
headers.put(line.substring(0, x).toLowerCase(Locale.ENGLISH).trim(), line.substring(x + 1).trim());
}
}
}
private byte[] readBodyBytes() throws IOException {
readBuffer();
int boundaryPosition = findBoundary();
if (boundaryPosition > -1) {
// the body bytes are completely in the buffer
int len = boundaryPosition - bufferPosition;
addFieldBytes(len);
byte[] body = new byte[len];
System.arraycopy(buffer, bufferPosition, body, 0, len);
bufferPosition = boundaryPosition + boundary.length;
return body;
}
// the body bytes are not completely in the buffer
// read all available bytes
int len = Math.min(BUFFER_SIZE, bufferCount) - bufferPosition;
addFieldBytes(len);
byte[] bodyBytes = new byte[len + BUFFER_SIZE];
int bodyBytesPos = len;
System.arraycopy(buffer, bufferPosition, bodyBytes, 0, len);
bufferPosition = bufferPosition + len;
// read next chunk
while (true) {
readBuffer();
boundaryPosition = findBoundary();
if (boundaryPosition > -1) {
// last chunk
len = boundaryPosition - bufferPosition;
addFieldBytes(len);
if (bodyBytesPos + len >= bodyBytes.length) {
byte[] newBodyBytes = new byte[bodyBytesPos + len];
System.arraycopy(bodyBytes, 0, newBodyBytes, 0, bodyBytesPos);
bodyBytes = newBodyBytes;
}
System.arraycopy(buffer, bufferPosition, bodyBytes, bodyBytesPos, len);
bodyBytesPos += len;
bufferPosition = boundaryPosition + boundary.length;
break;
} else {
// not the last chunk
len = Math.min(BUFFER_SIZE, bufferCount) - bufferPosition;
addFieldBytes(len);
if (bodyBytesPos + len >= bodyBytes.length) {
int newSize = bodyBytes.length << 1;
if (newSize < 0 || newSize > MAX_FIELD_BYTES) {
newSize = MAX_FIELD_BYTES;
}
if (newSize < bodyBytesPos + len) {
newSize = bodyBytesPos + BUFFER_SIZE;
}
byte[] newBodyBytes = new byte[newSize];
System.arraycopy(bodyBytes, 0, newBodyBytes, 0, bodyBytesPos);
bodyBytes = newBodyBytes;
}
System.arraycopy(buffer, bufferPosition, bodyBytes, bodyBytesPos, len);
bodyBytesPos += len;
bufferPosition = bufferPosition + len;
}
}
if (bodyBytes.length == bodyBytesPos) {
return bodyBytes;
}
byte[] returnBytes = new byte[bodyBytesPos];
System.arraycopy(bodyBytes, 0, returnBytes, 0, returnBytes.length);
return returnBytes;
}
private void addFieldBytes(int len) {
fieldBytes += len;
if (fieldBytes > MAX_FIELD_BYTES) {
throw new CmisInvalidArgumentException("Limit exceeded!");
}
}
private void readBodyAsStream(String contentType, String filename) throws IOException {
TempStoreOutputStream stream = streamFactory.newOutputStream();
stream.setMimeType(contentType);
stream.setFileName(filename);
try {
while (true) {
readBuffer();
int boundaryPosition = findBoundary();
if (boundaryPosition > -1) {
stream.write(buffer, bufferPosition, boundaryPosition - bufferPosition);
bufferPosition = boundaryPosition + boundary.length;
break;
} else {
int len = Math.min(BUFFER_SIZE, bufferCount) - bufferPosition;
stream.write(buffer, bufferPosition, len);
bufferPosition = bufferPosition + len;
}
}
stream.close();
contentSize = BigInteger.valueOf(stream.getLength());
contentStream = stream.getInputStream();
} catch (IOException e) {
// if something went wrong, make sure the temp file will
// be deleted
stream.destroy(e);
throw e;
}
}
private void readBody() throws IOException {
String contentDisposition = headers.get("content-disposition");
if (contentDisposition == null) {
throw new CmisInvalidArgumentException("Invalid multipart request!");
}
Map<String, String> params = new HashMap<String, String>();
MimeHelper.decodeContentDisposition(contentDisposition, params);
boolean isContent = params.containsKey(MimeHelper.DISPOSITION_FILENAME);
if (isContent) {
if (hasContent) {
throw new CmisInvalidArgumentException("Only one content expected!");
}
hasContent = true;
filename = params.get(MimeHelper.DISPOSITION_FILENAME);
if (filename != null) {
// if the browser sent the full path,
// extract the filename segment
int pathsep = filename.lastIndexOf('/');
if (pathsep > -1) {
filename = filename.substring(pathsep + 1);
}
pathsep = filename.lastIndexOf('\\');
if (pathsep > -1) {
filename = filename.substring(pathsep + 1);
}
filename = filename.trim();
}
contentType = headers.get("content-type");
if (contentType == null) {
contentType = Constants.MEDIATYPE_OCTETSTREAM;
}
readBodyAsStream(contentType, filename);
} else {
String name = params.get(MimeHelper.DISPOSITION_NAME);
byte[] rawValue = readBodyBytes();
if (CHARSET_FIELD.equalsIgnoreCase(name)) {
charset = new String(rawValue, IOUtils.ISO_8859_1);
return;
}
String fieldContentType = headers.get("content-type");
if (fieldContentType != null) {
String fieldCharset = MimeHelper.getCharsetFromContentType(fieldContentType);
if (fieldCharset != null) {
addField(name, new String(rawValue, fieldCharset));
return;
}
}
addRawField(name, rawValue);
}
}
private void skipPreamble() throws IOException {
readBuffer();
if (bufferCount < boundary.length - 2) {
throw new CmisInvalidArgumentException("Invalid multipart request!");
}
for (int i = 2; i < boundary.length; i++) {
if (boundary[i] != buffer[i - 2]) {
break;
}
if (i == boundary.length - 1) {
bufferPosition = boundary.length - 2;
readBuffer();
return;
}
}
while (true) {
int boundaryPosition = findBoundary();
if (boundaryPosition > -1) {
bufferPosition = boundaryPosition + boundary.length;
readBuffer();
break;
}
bufferPosition = BUFFER_SIZE + 1;
readBuffer();
}
}
private void skipEpilogue() {
try {
// read to the end of stream, but max 1 MB
int count = 0;
byte[] tmpBuf = new byte[4096];
int b;
while ((b = requestStream.read(tmpBuf)) > -1) {
count += b;
if (count >= 1024 * 1024) {
break;
}
}
} catch (IOException e) {
// ignore
}
}
private boolean readNext() throws IOException {
try {
readHeaders();
// no headers -> end of request
if (headers == null) {
skipEpilogue();
return false;
}
readBody();
return true;
} catch (IOException e) {
IOUtils.closeQuietly(contentStream);
skipEpilogue();
throw e;
}
}
public void parse() throws IOException {
try {
while (readNext()) {
// nothing to do here, just read
}
// apply charset
for (Map.Entry<String, byte[][]> e : rawFields.entrySet()) {
String[] otherValues = fields.get(e.getKey());
int index = (otherValues != null ? otherValues.length : 0);
String[] values = new String[e.getValue().length + index];
if (otherValues != null) {
System.arraycopy(otherValues, 0, values, 0, otherValues.length);
}
for (byte[] rawValue : e.getValue()) {
values[index++] = new String(rawValue, charset);
}
fields.put(e.getKey(), values);
}
} catch (Exception e) {
if (contentStream != null) {
IOUtils.closeQuietly(contentStream);
}
skipEpilogue();
fields = null;
if (e instanceof UnsupportedEncodingException) {
throw new CmisInvalidArgumentException("Encoding not supported!", e);
} else if (e instanceof CmisBaseException) {
throw (CmisBaseException) e;
} else if (e instanceof IOException) {
throw (IOException) e;
} else {
throw new CmisRuntimeException(e.getMessage(), e);
}
} finally {
rawFields = null;
}
}
public boolean hasContent() {
return hasContent;
}
public String getFilename() {
return filename;
}
public String getContentType() {
return contentType;
}
public BigInteger getSize() {
return contentSize;
}
public InputStream getStream() {
return contentStream;
}
public Map<String, String[]> getFields() {
return fields;
}
/**
* Returns if the request is a multi-part request
*/
public static final boolean isMultipartContent(HttpServletRequest request) {
String contentType = request.getContentType();
if (contentType != null && contentType.toLowerCase(Locale.ENGLISH).startsWith(MULTIPART)) {
return true;
}
return false;
}
}