| /* |
| * 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; |
| } |
| } |