diff --git a/src/zutil/net/http/multipart/MultipartField.java b/src/zutil/net/http/multipart/MultipartField.java old mode 100644 new mode 100755 index 00c7302..4dd0379 --- a/src/zutil/net/http/multipart/MultipartField.java +++ b/src/zutil/net/http/multipart/MultipartField.java @@ -25,46 +25,22 @@ package zutil.net.http.multipart; +import java.io.InputStream; + /** - * A class for handling multipart field + * A interface representing a single field in a multipart message. * * @author Ziver */ -public class MultipartField{ - protected long received; - protected long length; - protected String contentType; - - protected String fieldname; - protected String value; - - - protected MultipartField(){ - - } +public interface MultipartField{ + /** + * @return the amount of data received for this field. Might only be available when all data has been processed + */ + public long getLength(); /** - * @return the amount of data received for this field + * @return the name of the field. */ - public long getReceivedBytes(){ - return received; - } - - /** - * @return the fieldname - */ - public String getFieldname(){ - return fieldname; - } - - /** - * @return the value of the field - */ - public String getValue(){ - return value; - } - - protected void parse(){ - - } + public String getName(); + } diff --git a/src/zutil/net/http/multipart/MultipartFile.java b/src/zutil/net/http/multipart/MultipartFileField.java old mode 100644 new mode 100755 similarity index 56% rename from src/zutil/net/http/multipart/MultipartFile.java rename to src/zutil/net/http/multipart/MultipartFileField.java index a9f9bcf..7f584c3 --- a/src/zutil/net/http/multipart/MultipartFile.java +++ b/src/zutil/net/http/multipart/MultipartFileField.java @@ -1,84 +1,102 @@ -/* - * The MIT License (MIT) - * - * Copyright (c) 2015 Ziver Koc - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -package zutil.net.http.multipart; - -import java.io.File; - - -/** - * A class for handling multipart files - * - * @author Ziver - */ -public class MultipartFile extends MultipartField{ - protected String filename; - protected File file; - - - protected MultipartFile(File tempFile){ - this.file = tempFile; - } - - /** - * @return the amount of data received for this field - */ - public long getReceivedBytes(){ - return received; - } - - /** - * @return the value of the field - */ - public String getValue(){ - return null; - } - - /** - * @return the filename - */ - public String getFilename(){ - return filename; - } - - /** - * @return the File class that points to the received file - */ - public File getFile(){ - return file; - } - - /** - * Moves this file - * - * @param new_file is the new location to move the file to - * @return if the move was successful - */ - public boolean moveFile(File new_file){ - boolean success = file.renameTo(new_file); - if(success) - file = new_file; - return success; - } -} +/* + * The MIT License (MIT) + * + * Copyright (c) 2015 Ziver Koc + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package zutil.net.http.multipart; + +import java.io.File; +import java.io.IOException; + + +/** + * A class for handling multipart files + * + * @author Ziver + */ +public class MultipartFileField implements MultipartField{ + /** This is the temporary directory for the received files */ + private File tempDir; + protected String filename; + protected File file; + + + protected MultipartFileField() throws IOException { + this.file = createTempFile(); + } + + /** + * @return the amount of data received for this field + */ + public long getLength(){ + return 0; //TODO: + } + + /** + * @return the specific file name for this field. + */ + public String getName(){ + return filename; + } + + /** + * @return the File class that points to the received file + */ + public File getFile(){ + return file; + } + + /** + * Moves the received file. + * + * @param newFile is the new location to move the file to + * @return if the move was successful + */ + public boolean moveFile(File newFile){ + boolean success = file.renameTo(newFile); + if(success) + file = newFile; + return success; + } + + /** + * Creates a temporary file in either the system + * temporary folder or by the setTempDir() function + * + * @return the temporary file + */ + protected File createTempFile() throws IOException { + if(tempDir != null) + return File.createTempFile("upload", ".part", tempDir.getAbsoluteFile()); + else + return File.createTempFile("upload", ".part"); + } + + + public void setTempDir(File dir){ + if(!dir.isDirectory()) + throw new RuntimeException("\""+dir.getAbsolutePath()+"\" is not a directory!"); + if(!dir.canWrite()) + throw new RuntimeException("\""+dir.getAbsolutePath()+"\" is not writable!"); + tempDir = dir; + } +} diff --git a/src/zutil/net/http/multipart/MultipartParser.java b/src/zutil/net/http/multipart/MultipartParser.java index caabd74..3d95273 100755 --- a/src/zutil/net/http/multipart/MultipartParser.java +++ b/src/zutil/net/http/multipart/MultipartParser.java @@ -25,14 +25,18 @@ package zutil.net.http.multipart; import zutil.ProgressListener; +import zutil.log.LogUtil; import zutil.net.http.HttpHeader; +import zutil.net.http.HttpHeaderParser; import javax.servlet.http.HttpServletRequest; -import java.io.BufferedReader; -import java.io.File; -import java.io.IOException; +import java.io.*; import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; /** * Parses a multipart/form-data http request, @@ -43,103 +47,140 @@ import java.util.List; * @author Ziver * */ -public class MultipartParser { - /** This is the temporary directory for the received files */ - private File tempDir; +public class MultipartParser implements Iterable{ + private static final Logger logger = LogUtil.getLogger(); + private static final String HEADER_CONTENT_DISPOSITION = "Content-Disposition"; + private static final String HEADER_CONTENT_TYPE = "Content-Type"; + /** This is the delimiter that will separate the fields */ private String delimiter; /** The length of the HTTP Body */ private long contentLength; /** This is the input stream */ private BufferedReader in; - - /** This is the listener that will listen on the progress */ - private ProgressListener listener; - - - + + private MultiPartIterator iterator; + + + public MultipartParser(BufferedReader in, String delimiter, long length){ + this.in = in; + this.delimiter = delimiter; + this.contentLength = length; + } public MultipartParser(BufferedReader in, HttpHeader header){ - this.in = in; - - String cotype = header.getHeader("Content-type"); - cotype = cotype.split(" *; *")[1]; - delimiter = cotype.split(" *= *")[1]; - - contentLength = Long.parseLong( header.getHeader("Content-Length") ); + this(in, + parseDelimiter(header.getHeader("Content-type")), + Long.parseLong( header.getHeader("Content-Length"))); } - - public MultipartParser(BufferedReader in, HttpServletRequest req){ - this.in = in; - - String cotype = req.getHeader("Content-type"); - cotype = cotype.split(" *; *")[1]; - delimiter = cotype.split(" *= *")[1]; - - contentLength = req.getContentLength(); - } - - public MultipartParser(BufferedReader in, String delimiter, long length){ - this.in = in; - this.delimiter = delimiter; - this.contentLength = length; - } - - - /** - * @param listener is the listener that will be called for progress - */ - public void setListener(ProgressListener listener){ - this.listener = listener; - } - - /** - * Parses the HTTP Body and returns a list of fields - * - * @return A list of FormField - */ - public List parse() throws IOException{ - ArrayList list = new ArrayList(); - parse(list, delimiter); - return list; + public MultipartParser(HttpServletRequest req) throws IOException { + this(req.getReader(), + parseDelimiter(req.getHeader("Content-type")), + req.getContentLength()); } - - private void parse(List list, String delimiter) throws IOException{ - // TODO: - } + private static String parseDelimiter(String contentTypeHeader){ + String delimiter = contentTypeHeader.split(" *; *")[1]; + delimiter = delimiter.split(" *= *")[1]; + return delimiter; + } - /** - * Creates a temporary file in either the system - * temporary folder or by the setTempDir() function - * - * @return the temporary file - */ - protected File createTempFile() throws IOException{ - if(tempDir != null) - return File.createTempFile("upload", ".part", tempDir.getAbsoluteFile()); - else - return File.createTempFile("upload", ".part"); - } - - /** - * Sets the initial delimiter - * - * @param delimiter is the new delimiter - */ - public void setDelimiter(String delimiter){ - this.delimiter = delimiter; - } - - public void setTempDir(File dir){ - if(!dir.isDirectory()) - throw new RuntimeException("\""+dir.getAbsolutePath()+"\" is not a directory!"); - if(!dir.canWrite()) - throw new RuntimeException("\""+dir.getAbsolutePath()+"\" is not writable!"); - tempDir = dir; - } - + public long getContentLength(){ return contentLength; } + + + + @Override + public Iterator iterator() { + if (iterator == null) + iterator = new MultiPartIterator(); + return iterator; + } + + + protected class MultiPartIterator implements Iterator{ + private String currentLine; + private boolean endOfStream; + private HttpHeaderParser parser; + + + protected MultiPartIterator(){ + this.endOfStream = false; + this.parser = new HttpHeaderParser(in); + this.parser.setReadStatusLine(false); + } + + + @Override + public boolean hasNext() { + try{ + findDelimiter(); + if (endOfStream) + return false; + return true; + } catch (IOException e){ + logger.log(Level.SEVERE, null, e); + endOfStream = true; + } + return false; + } + private void findDelimiter() throws IOException { + if (endOfStream || matchesDelimiter(currentLine)) + return; + while ((currentLine=in.readLine()) != null) { + if (matchesDelimiter(currentLine)) + break; + } + } + private boolean matchesDelimiter(String match){ + if (match == null) + return false; + else if (match.equals("--" + delimiter + "--")) { + endOfStream = true; + return true; + } + else if (match.equals("--" + delimiter)) + return true; + return false; + } + + @Override + public MultipartField next() { + if (!hasNext()) + return null; + try { + HttpHeader header = parser.read(); + String disposition = header.getHeader(HEADER_CONTENT_DISPOSITION); + if (disposition != null){ + HashMap map = new HashMap<>(); + HttpHeaderParser.parseHeaderValue(map, disposition); + if (map.containsKey("form-data")){ + if (map.containsKey("filename")){ + MultipartFileField field = new MultipartFileField(); + return field; + } + else{ + MultipartStringField field = new MultipartStringField(); + return field; + } + } + else { + logger.warning("Only form-data is supported"); + return this.next(); // find next field + } + } + } catch (IOException e) { + logger.log(Level.SEVERE, null, e); + } + return null; + } + + + @Override + public void remove() { + throw new UnsupportedOperationException("Remove not supported in read only stream."); + } + } } diff --git a/src/zutil/net/http/multipart/MultipartStringField.java b/src/zutil/net/http/multipart/MultipartStringField.java new file mode 100755 index 0000000..f86076c --- /dev/null +++ b/src/zutil/net/http/multipart/MultipartStringField.java @@ -0,0 +1,24 @@ +package zutil.net.http.multipart; + +import java.io.InputStream; + +/** + * Created by ezivkoc on 2016-07-06. + */ +public class MultipartStringField implements MultipartField { + private String value; + + @Override + public long getLength() { + return 0; + } + + @Override + public String getName() { + return null; + } + + public String getValue() { + return value; + } +} diff --git a/test/zutil/net/http/multipart/MultipartParserTest.java b/test/zutil/net/http/multipart/MultipartParserTest.java new file mode 100755 index 0000000..998fbf3 --- /dev/null +++ b/test/zutil/net/http/multipart/MultipartParserTest.java @@ -0,0 +1,96 @@ +package zutil.net.http.multipart; + +import org.junit.Test; + +import java.io.BufferedReader; +import java.io.StringReader; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +/** + * JUnit test for MultipartParser class + * + * Created by Ziver on 2016-07-06. + */ +public class MultipartParserTest { + + @Test + public void singleFormDataField(){ + String input = + "------------------------------83ff53821b7c\n" + + "Content-Disposition: form-data; name=\"foo\"\n" + + "\n" + + "bar\n" + + "------------------------------83ff53821b7c--"; + MultipartParser parser = new MultipartParser( + new BufferedReader(new StringReader(input)), + "----------------------------83ff53821b7c", + input.length()); + + int count = 0; + for(MultipartField field : parser){ + assertEquals(0, count); + if (field instanceof MultipartStringField) { + MultipartStringField stringField = (MultipartStringField)field; + assertEquals("foo", stringField.getName()); + assertEquals("bar", stringField.getValue()); + } + else fail("Field is not an instance of "+MultipartStringField.class); + ++count; + } + } + + //@Test + public void singleFileUpload(){ + String input = + "------------------------------83ff53821b7c\n" + + "Content-Disposition: form-data; name=\"img\"; filename=\"a.png\"\n" + + "Content-Type: application/octet-stream\n" + + "\n" + + "?PNG\n" + + "\n" + + "IHD?wS??iCCPICC Profilex?T?kA?6n??Zk?x?\"IY?hE?6?bk\n" + + "Y?<ߡ)??????9Nyx?+=?Y\"|@5-?\u007FM?S?%?@?H8??qR>?\u05CB??inf???O?????b??N?????~N??>?!?\n" + + "??V?J?p?8?da?sZHO?Ln?}&???wVQ?y?g????E??0\n" + + " ??\n" + + " IDAc????????-IEND?B`?\n" + + "------------------------------83ff53821b7c--"; + MultipartParser parser = new MultipartParser( + new BufferedReader(new StringReader(input)), + "----------------------------83ff53821b7c", + input.length()); + + // TODO + } + + //Test + public void multiFileUpload(){ + String input = + " --AaB03x\n" + + " Content-Disposition: form-data; name=\"submit-name\"\n" + + "\n" + + " Larry\n" + + " --AaB03x\n" + + " Content-Disposition: form-data; name=\"files\"\n" + + " Content-Type: multipart/mixed; boundary=BbC04y\n" + + "\n" + + " --BbC04y\n" + + " Content-Disposition: file; filename=\"file1.txt\"\n" + + " Content-Type: text/plain\n" + + "\n" + + " ... contents of file1.txt ...\n" + + " --BbC04y\n" + + " Content-Disposition: file; filename=\"file2.gif\"\n" + + " Content-Type: image/gif\n" + + " Content-Transfer-Encoding: binary\n" + + "\n" + + " ...contents of file2.gif...\n" + + " --BbC04y--\n" + + " --AaB03x--"; + MultipartParser parser = new MultipartParser( + new BufferedReader(new StringReader(input)), + "AaB03x", + input.length()); + } +}