blob: b7183953c1db24f8c348094eb3202a9fed4f283e [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.netbeans.lib.uihandler;
/*
* MultipartHandler:
* A utility class to handle content of multipart/form-data type used in form uploads.
*
* Parses and provides accessor functions to extract the form fields and the uploaded
* file content parts separated by a boundary string.
* See http://www.ietf.org/rfc/rfc1867.txt.
*/
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.SocketTimeoutException;
import java.nio.charset.StandardCharsets;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.HashMap;
import java.util.Vector;
class MultiPartHandler {
public interface InputFacade {
public int readLine(byte[] arr, int off, int len) throws IOException;
public InputStream getInputStream();
}
public interface RequestFacade {
public int getContentLength();
public String getContentType();
public InputFacade getInput() throws IOException;
}
private static final int DEFAULT_MAX_UPLOAD_SIZE = 1024 * 1024; // 1Mb
protected Hashtable<String,Vector<String>> formFields = new Hashtable<String,Vector<String>>();
Hashtable<String,OneUpload> uploadFiles = new Hashtable<String,OneUpload>();
/** servlet request */
private RequestFacade req;
/** input stream to read parts from */
private InputFacade in;
/** MIME boundary that delimits parts */
private String boundary;
/** buffer for readLine method */
private byte[] buf = new byte[8 * 1024];
/** upload directory */
private File uploadDir;
/** encoding used for the from fields */
private String fieldEncoding = "ISO-8859-1";
// i18n StringManager
/*private static StringManager localStrings =
StringManager.getManager( OneUpload.class );*/
/**
* Instantiate a new multipart handler with default
*/
public MultiPartHandler(RequestFacade request,
String tmpDirectory) throws IOException {
this(request, tmpDirectory, DEFAULT_MAX_UPLOAD_SIZE, "ISO-8859-1");
}
public MultiPartHandler(RequestFacade request,
String tmpDirectory,
int maxUploadSize) throws IOException {
this(request, tmpDirectory, maxUploadSize, "ISO-8859-1");
}
/**
* Instantiate a new OneUpload to handle the given request,
* saving any uploaded files to the given directory and limiting the
* upload size to maxUploadSize.
*
* An IOException is thrown when the request content-type doesn't match
* with multipart/form-data or if the upload size exceeds the given limit.
*
* call parseMultipartUpload() to parse various parts of the posted data and then
* call getParameter(), getParameters(), getParameterNames() and getParameterValues()
* functions to access form field names and their values.
* call getFile(), getFileType() to access uploaded file and its content-type.
*/
public MultiPartHandler(RequestFacade request,
String tmpDirectory,
int maxUploadSize,
String fieldEncoding) throws IOException {
// Ensure we are passed legal arguments
if (request == null) {
//String msg = localStrings.getString( "admin.server.gui.servlet.request_cannot_be_null" );
throw new IllegalArgumentException( "request is null" );
}
if (tmpDirectory == null) {
//String msg = localStrings.getString( "admin.server.gui.servlet.tmpdirectory_cannot_be_null" );
throw new IllegalArgumentException( "tmp Dir is null" );
}
if (maxUploadSize <= 0) {
//String msg = localStrings.getString( "admin.server.gui.servlet.maxpostsize_must_be_positive" );
throw new IllegalArgumentException( "Max size is < 0" );
}
// Ensure that the directory exists and is writable (this should be a temp directory)
uploadDir = new File(tmpDirectory);
if (!uploadDir.isDirectory()) {
//String msg = localStrings.getString( "admin.server.gui.servlet.not_directory", tmpDirectory );
throw new IllegalArgumentException( "Not a Directory" );
}
if (!uploadDir.canWrite()) {
//String msg = localStrings.getString( "admin.server.gui.servlet.not_writable", tmpDirectory );
throw new IllegalArgumentException("write protected" );
}
/*
int length = request.getContentLength();
//commented this code to remove the restriction on the file upload size.
/*if (length > maxUploadSize) {
//String msg = localStrings.getString( "admin.server.gui.servlet.posted_content_length_exceeds_limit", new Integer(length), new Integer(maxUploadSize) );
throw new IOException( msg );
}*/
// Check the content type to make sure it's "multipart/form-data"
String type = request.getContentType();
if (type == null ||
!type.toLowerCase().startsWith("multipart/form-data")) {
//String msg = localStrings.getString( "admin.server.gui.servlet.posted_content_type_not_multipart" );
throw new IOException( "type null" );
}
// Check the content length to prevent denial of service attacks
this.fieldEncoding = fieldEncoding;
this.req = request;
}
/* parseMultipartUpload:
*
* This function parses the multipart/form-data and throws an IOException
* if there's any problem reading or parsing the request or if the posted
* content is larger than the maximum permissible size.
*/
public void parseMultipartUpload()
throws IOException {
// setup the initial buffered input stream, boundary string that separates
// various parts in the stream.
startMultipartParse();
HashMap partHeaders = parsePartHeaders();
while (partHeaders != null) {
String fieldName = (String)partHeaders.get("fieldName");
String fileName = (String)partHeaders.get("fileName");
if (fileName != null) {
// This is a file upload part
if (fileName.equals("")) {
fileName = null; // empty filename, probably an "empty" file param
}
if (fileName != null) {
// a filename was actually specified
String content = (String)partHeaders.get("content-type");
fileName = saveUploadFile(fileName, content);
uploadFiles.put(fieldName,
new OneUpload(
uploadDir.toString(),
fileName,
content
)
);
}
else {
uploadFiles.put(fieldName, new OneUpload(null, null, null));
}
}
else {
// this is a parameters list part
byte[] valueBytes = parseFormFieldBytes();
String value = new String(valueBytes, fieldEncoding);
Vector<String> existingValues = formFields.get(fieldName);
if (existingValues == null) {
existingValues = new Vector<String>();
formFields.put(fieldName, existingValues);
}
existingValues.addElement(value);
}
partHeaders.clear();
partHeaders = parsePartHeaders();
}
}
private void startMultipartParse()
throws IOException {
// Get the boundary string; it's included in the content type.
// Should look something like "------------------------12012133613061"
String boundary = parseBoundary(req.getContentType());
if (boundary == null) {
//String msg = localStrings.getString( "admin.server.gui.servlet.separation_boundary_not_specified" );
throw new IOException( "boundary is nul" );
}
this.in = req.getInput();
this.boundary = boundary;
// Read the first line, should be the first boundary
String line = readLine();
if (line == null) {
//String msg = localStrings.getString( "admin.server.gui.servlet.corrupt_form_data_premature_ending" );
throw new IOException( "line is null" );
}
// Verify that the line is the boundary
if (!line.startsWith(boundary)) {
//String msg = localStrings.getString( "admin.server.gui.servlet.corrupt_form_data_no_leading_boundary", line, boundary );
throw new IOException( "not start with boundary" );
}
}
/**
* parse the headers of the individual part; they look like this:
* Content-Disposition: form-data; name="field1"; filename="file1.txt"
* Content-Type: type/subtype
* Content-Transfer-Encoding: binary
*/
private HashMap parsePartHeaders() throws IOException {
HashMap<String,String> partHeaders = new HashMap<String,String>();
Vector<String> headers = new Vector<String>();
String line = readLine();
if (line == null) {
// No parts left, we're done
return null;
}
else if (line.length() == 0) {
// IE4 on Mac sends an empty line at the end; treat that as the end.
return null;
}
headers.addElement(line);
// Read the following header lines we hit an empty line
while ((line = readLine()) != null && (line.length() > 0)) {
headers.addElement(line);
}
// If we got a null above, it's the end
if (line == null) {
return null;
}
// default part content type (rfc1867)
partHeaders.put("content-type", "text/plain");
Enumeration ee = headers.elements();
while (ee.hasMoreElements()) {
String headerline = (String) ee.nextElement();
if (headerline.toLowerCase().startsWith("content-disposition:")) {
// Parse the content-disposition line
parseContentDisposition(headerline, partHeaders);
}
else if (headerline.toLowerCase().startsWith("content-type:")) {
// Get the content type, or null if none specified
parseContentType(headerline, partHeaders);
}
}
return partHeaders;
}
/**
* parses and returns the boundary token from a line.
*/
private String parseBoundary(String line) {
// Use lastIndexOf() because IE 4.01 on Win98 has been known to send the
// "boundary=" string multiple times.
int index = line.lastIndexOf("boundary=");
if (index == -1) {
return null;
}
String boundary = line.substring(index + 9); // 9 for "boundary="
if (boundary.charAt(0) == '"') {
// The boundary is enclosed in quotes, strip them
index = boundary.lastIndexOf('"');
boundary = boundary.substring(1, index);
}
// The real boundary is always preceeded by an extra "--"
boundary = "--" + boundary;
return boundary;
}
/**
* parses and returns content-disposition header and stores the values
* in the partHeaders.
*
* throws IOException if the line is malformatted.
*/
private void parseContentDisposition(String line, HashMap<String,String> partHeaders)
throws IOException {
// Convert the line to a lowercase string without the ending \r\n
// Keep the original line for error messages and for variable names.
String origline = line;
line = origline.toLowerCase();
// Get the content disposition, should be "form-data"
int start = line.indexOf("content-disposition: ");
int end = line.indexOf(";");
if (start == -1 || end == -1) {
//String msg = localStrings.getString( "admin.server.gui.servlet.content_disposition_corrupt", origline );
throw new IOException( "end reached" );
}
String disposition = line.substring(start + 21, end);
if (!disposition.equals("form-data")) {
//String msg = localStrings.getString( "admin.server.gui.servlet.invalid_content_disposition", disposition );
throw new IOException( "fome-data not match" );
}
// Get the field name
start = line.indexOf("name=\"", end); // start at last semicolon
end = line.indexOf("\"", start + 7); // skip name=\"
if (start == -1 || end == -1) {
//String msg = localStrings.getString( "admin.server.gui.servlet.content_disposition_corrupt", origline );
throw new IOException( "data corrupt" );
}
String name = origline.substring(start + 6, end);
// Get the fileName, if given
String fileName = null;
String origFileName = null;
start = line.indexOf("filename=\"", end + 2); // start after name
end = line.indexOf("\"", start + 10); // skip filename=\"
if (start != -1 && end != -1) { // note the !=
fileName = origline.substring(start + 10, end);
origFileName = fileName;
// The filename may contain a full path. Cut to just the filename.
int slash =
Math.max(fileName.lastIndexOf('/'), fileName.lastIndexOf('\\'));
if (slash > -1) {
fileName = fileName.substring(slash + 1); // past last slash
}
}
// fill in the part parameters map: disposition, name, filename
// empty fileName denotes no file posted!
partHeaders.put("disposition", disposition);
partHeaders.put("fieldName", name);
partHeaders.put("fileName", fileName);
partHeaders.put("filePath", origFileName);
}
/**
* parse and returns the content type from a line, or null if the
* line was empty.
*/
private void parseContentType(String line, HashMap<String,String> partHeaders)
throws IOException {
String contentType = null;
// Convert the line to a lowercase string
String origline = line;
line = origline.toLowerCase();
// Get the content type, if any
if (line.startsWith("content-type")) {
int start = line.indexOf(" ");
if (start == -1) {
//String msg = localStrings.getString( "admin.server.gui.servlet.corrupt_content_type", origline );
throw new IOException( "no start" );
}
contentType = line.substring(start + 1);
partHeaders.put("content-type", contentType);
}
else if (line.length() != 0) { // no content type, so should be empty
//String msg = localStrings.getString( "admin.server.gui.servlet.malformed_line_after_disposition", origline );
throw new IOException( "length 0" );
}
}
/** parse contents of a form field parameter; uses the encoding set by the user
*/
private byte[] parseFormFieldBytes() throws IOException {
// Copy the part's contents into a byte array
MultipartInputStream pis = new MultipartInputStream(in, boundary);
ByteArrayOutputStream baos = new ByteArrayOutputStream(512);
byte[] buf = new byte[128];
int read;
while ((read = pis.read(buf)) != -1) {
baos.write(buf, 0, read);
}
pis.close();
baos.close();
// get the value bytes
return baos.toByteArray();
}
/**
* Read the next line of input.
*
* @return a String containing the next line of input from the stream,
* or null to indicate the end of the stream.
* @exception IOException if an input or output exception has occurred.
*/
private String readLine() throws IOException {
StringBuffer sbuf = new StringBuffer();
int result;
String line;
do {
result = in.readLine(buf, 0, buf.length); // does +=
if (result != -1) {
sbuf.append(new String(buf, 0, result, StandardCharsets.ISO_8859_1));
}
} while (result == buf.length); // loop only if the buffer was filled
if (sbuf.length() == 0) {
return null; // nothing read, must be at the end of stream
}
// Cut off the trailing \n or \r\n
// It should always be \r\n but IE5 sometimes does just \n
int len = sbuf.length();
if (len >= 2 && sbuf.charAt(len - 2) == '\r') {
sbuf.setLength(len - 2); // cut \r\n
}
else {
sbuf.setLength(len - 1); // cut \n
}
return sbuf.toString();
}
/**
* Write this file part to the specified directory.
*/
private String saveUploadFile(String fileName, String content)
throws IOException {
long written = 0;
OutputStream fileOut = null;
File file = new File(uploadDir, fileName);
try {
// Only do something if this part contains a file
for (int i = 0; file.exists(); i++) {
if (!file.exists()) {
break;
}
file = new File(uploadDir, fileName + "." + i);
}
fileName = file.getName();
fileOut = new BufferedOutputStream(new FileOutputStream(file));
int numBytes;
byte[] buf = new byte[8 * 1024];
InputStream partInput;
boolean canCloseStream = true;
if (content.equals("x-application/gzip")) { // NOI18N
// sending from NetBeans UI Gestures Collector
partInput = in.getInputStream();
canCloseStream = false;
} else {
/** input stream containing file data */
partInput = new MultipartInputStream(in, boundary);
}
while((numBytes = partInput.read(buf)) != -1) {
fileOut.write(buf, 0, numBytes);
written += numBytes;
}
if (canCloseStream){
partInput.close();
}
} catch (SocketTimeoutException ste) {
if (file.exists()){
file.delete();
}
throw ste;
} finally {
if (fileOut != null) fileOut.close();
}
return fileName;
}
/**
* Returns the names of all the parameters as an Enumeration of
* Strings. It returns an empty Enumeration if there are no parameters.
*
*/
public Enumeration getParameterNames() {
return formFields.keys();
}
/**
* Returns the names of all the uploaded files as an Enumeration of
* Strings. It returns an empty Enumeration if there are no uploaded
* files. Each file name is the name specified by the form, not by
* the user.
*
*/
public Enumeration getFileNames() {
return uploadFiles.keys();
}
/**
* Returns the value of the named parameter as a String, or null if
* the parameter was not sent or was sent without a value. The value
* is guaranteed to be in its normal, decoded form. If the parameter
* has multiple values, only the last one is returned (for backward
* compatibility). For parameters with multiple values, it's possible
* the last "value" may be null.
*
*/
public String getParameter(String name) {
try {
Vector values = (Vector)formFields.get(name);
if (values == null || values.size() == 0) {
return null;
}
String value = (String)values.elementAt(values.size() - 1);
return value;
}
catch (Exception e) {
return null;
}
}
/**
* Returns the values of the named parameter as a String array, or null if
* the parameter was not sent. The array has one entry for each parameter
* field sent. If any field was sent without a value that entry is stored
* in the array as a null. The values are guaranteed to be in their
* normal, decoded form. A single value is returned as a one-element array.
*
*/
public String[] getParameterValues(String name) {
try {
Vector values = (Vector)formFields.get(name);
if (values == null || values.size() == 0) {
return null;
}
String[] valuesArray = new String[values.size()];
values.copyInto(valuesArray);
return valuesArray;
}
catch (Exception e) {
return null;
}
}
/**
* Returns the filesystem name of the specified file, or null if the
* file was not included in the upload. A filesystem name is the name
* specified by the user. It is also the name under which the file is
* actually saved.
*
*/
public String getFileName(String name) {
try {
OneUpload file = uploadFiles.get(name);
return file.getFileName(); // may be null
}
catch (Exception e) {
return null;
}
}
/**
* Returns the content type of the specified file (as supplied by the
* client browser), or null if the file was not included in the upload.
*
*/
public String getFileType(String name) {
try {
OneUpload file = uploadFiles.get(name);
return file.getFileType(); // may be null
}
catch (Exception e) {
return null;
}
}
/**
* Returns a File object for the specified file saved on the server's
* filesystem, or null if the file was not included in the upload.
*
*/
public File getFile(String name) {
try {
OneUpload file = uploadFiles.get(name);
return file.getFile(); // may be null
}
catch (Exception e) {
return null;
}
}
/**
* close the multi-part form handler
*/
public void close() throws IOException {
req = null;
in = null;
boundary = null;
buf = null;
uploadDir = null;
}
/** A class to hold information about an uploaded file. */
private static class OneUpload {
private String dir;
private String filename;
private String type;
OneUpload(String dir, String filename, String type) {
this.dir = dir;
this.filename = filename;
this.type = type;
}
public String getFileType() {
return type;
}
public String getFileName() {
return filename;
}
public File getFile() {
if (dir == null || filename == null) {
return null;
} else {
return new File(dir + File.separator + filename);
}
}
}
/*
* providing access to a single MIME part contained with in which ends with
* the boundary specified. It uses buffering to provide maximum performance.
*
*/
private static class MultipartInputStream extends FilterInputStream {
/** boundary which "ends" the stream */
private String boundary;
/** our buffer */
private byte [] buf = new byte[64*1024]; // 64k
/** number of bytes we've read into the buffer */
private int count;
/** current position in the buffer */
private int pos;
/** flag that indicates if we have encountered the boundary */
private boolean eof;
/** associated facade */
private MultiPartHandler.InputFacade facade;
// i18n StringManager
/*private static StringManager localStrings =
StringManager.getManager( MultipartInputStream.class );*/
/**
* Instantiate a MultipartInputStream which stops at the specified
* boundary from an underlying ServletInputStream.
*
*/
MultipartInputStream(MultiPartHandler.InputFacade in,
String boundary) throws IOException {
super(in.getInputStream());
this.boundary = boundary;
this.facade = in;
}
/**
* Fill up our buffer from the underlying input stream, and check for the
* boundary that signifies end-of-file. Users of this method must ensure
* that they leave exactly 2 characters in the buffer before calling this
* method (except the first time), so that we may only use these characters
* if a boundary is not found in the first line read.
*
* @exception IOException if an I/O error occurs.
*/
private void fill() throws IOException
{
if (eof)
return;
// as long as we are not just starting up
if (count > 0)
{
// if the caller left the requisite amount spare in the buffer
if (count - pos == 2) {
// copy it back to the start of the buffer
System.arraycopy(buf, pos, buf, 0, count - pos);
count -= pos;
pos = 0;
} else {
// should never happen, but just in case
//String msg = localStrings.getString( "admin.server.gui.servlet.fill_detected_illegal_buffer_state" );
throw new IllegalStateException( "should never happen" );
}
}
// try and fill the entire buffer, starting at count, line by line
// but never read so close to the end that we might split a boundary
int read = 0;
int maxRead = buf.length - boundary.length();
while (count < maxRead) {
// read a line
read = facade.readLine(buf, count, buf.length - count);
// check for eof and boundary
if (read == -1) {
//String msg = localStrings.getString( "admin.server.gui.servlet.unexpected_end_part" );
throw new IOException( "read is -1" );
} else {
if (read >= boundary.length()) {
eof = true;
for (int i=0; i < boundary.length(); i++) {
if (boundary.charAt(i) != buf[count + i]) {
// Not the boundary!
eof = false;
break;
}
}
if (eof) {
break;
}
}
}
// success
count += read;
}
}
/**
* See the general contract of the read method of InputStream.
* Returns -1 (end of file) when the MIME boundary of this part is encountered.
*
* throws IOException if an I/O error occurs.
*/
public int read() throws IOException {
if (count - pos <= 2) {
fill();
if (count - pos <= 2) {
return -1;
}
}
return buf[pos++] & 0xff;
}
/**
* See the general contract of the read method of InputStream.
*
* Returns -1 (end of file) when the MIME boundary of this part
* is encountered.
*
* throws IOException if an I/O error occurs.
*/
public int read(byte b[]) throws IOException {
return read(b, 0, b.length);
}
/**
* See the general contract of the read method of InputStream.
*
* Returns -1 (end of file) when the MIME boundary of this part is encountered.
*
* throws IOException if an I/O error occurs.
*/
public int read(byte b[], int off, int len) throws IOException
{
int total = 0;
if (len == 0) {
return 0;
}
int avail = count - pos - 2;
if (avail <= 0) {
fill();
avail = count - pos - 2;
if(avail <= 0) {
return -1;
}
}
int copy = Math.min(len, avail);
System.arraycopy(buf, pos, b, off, copy);
pos += copy;
total += copy;
while (total < len) {
fill();
avail = count - pos - 2;
if(avail <= 0) {
return total;
}
copy = Math.min(len - total, avail);
System.arraycopy(buf, pos, b, off + total, copy);
pos += copy;
total += copy;
}
return total;
}
/**
* Returns the number of bytes that can be read from this input stream
* without blocking. This is a standard InputStream idiom
* to deal with buffering gracefully, and is not same as the length of the
* part arriving in this stream.
*
* throws IOException if an I/O error occurs.
*/
public int available() throws IOException {
int avail = (count - pos - 2) + in.available();
// Never return a negative value
return (avail < 0 ? 0 : avail);
}
/**
* Closes this input stream and releases any system resources
* associated with the stream. This method will read any unread data
* in the MIME part so that the next part starts an an expected place in
* the parent InputStream.
*
* throws IOException if an I/O error occurs.
*/
public void close() throws IOException {
if (!eof) {
while (read(buf, 0, buf.length) != -1)
; // do nothing
}
}
}
}