changeset 1147:30d87b7d1d62

webserver - support multipart/form-data
author Franklin Schmidt <fschmidt@gmail.com>
date Thu, 01 Feb 2018 22:06:37 -0700
parents 2dda3c92a473
children 49fb4e83484f
files src/luan/webserver/Connection.java src/luan/webserver/Request.java src/luan/webserver/RequestParser.java src/luan/webserver/Util.java src/luan/webserver/examples/post_multipart.html
diffstat 5 files changed, 110 insertions(+), 6 deletions(-) [+]
line wrap: on
line diff
--- a/src/luan/webserver/Connection.java	Thu Feb 01 03:08:21 2018 -0700
+++ b/src/luan/webserver/Connection.java	Thu Feb 01 22:06:37 2018 -0700
@@ -76,8 +76,8 @@
 						}
 						size += n;
 					}
-					request.body = new String(body);
-//System.out.println(request.body);
+					request.body = body;
+//System.out.println(new String(request.body));
 				}
 
 				String contentType = (String)request.headers.get("Content-Type");
@@ -87,8 +87,10 @@
 					} else {
 						if( "application/x-www-form-urlencoded".equals(contentType) ) {
 							parser.parseUrlencoded();
+						} else if( contentType.startsWith("multipart/form-data;") ) {
+							parser.parseMultipart();
 						} else {
-							logger.warn("unknown content type: "+contentType);
+							logger.error("unknown content type: "+contentType);
 						}
 					}
 				}
--- a/src/luan/webserver/Request.java	Thu Feb 01 03:08:21 2018 -0700
+++ b/src/luan/webserver/Request.java	Thu Feb 01 22:06:37 2018 -0700
@@ -14,5 +14,19 @@
 	public final Map<String,Object> headers = Collections.synchronizedMap(new LinkedHashMap<String,Object>());
 	public final Map<String,Object> parameters = Collections.synchronizedMap(new LinkedHashMap<String,Object>());
 	public final Map<String,String> cookies = Collections.synchronizedMap(new LinkedHashMap<String,String>());
-	public volatile String body;
+	public volatile byte[] body;
+
+	public static final class MultipartFile {
+		public final String filename;
+		public final Object content;  // byte[] or String
+
+		MultipartFile(String filename,Object content) {
+			this.filename = filename;
+			this.content = content;
+		}
+
+		public String toString() {
+			return "{filename="+filename+", content="+content+"}";
+		}
+	}
 }
--- a/src/luan/webserver/RequestParser.java	Thu Feb 01 03:08:21 2018 -0700
+++ b/src/luan/webserver/RequestParser.java	Thu Feb 01 22:06:37 2018 -0700
@@ -15,7 +15,7 @@
 	}
 
 	void parseUrlencoded() throws ParseException {
-		this.parser = new Parser(request.body);
+		this.parser = new Parser(Util.toString(request.body));
 		parseQuery();
 		require( parser.endOfInput() );
 	}
@@ -195,4 +195,64 @@
 		}
 	}
 
+
+	private static final String contentTypeStart = "multipart/form-data; boundary=";
+
+	void parseMultipart() throws ParseException {
+		String contentType = (String)request.headers.get("Content-Type");
+		if( !contentType.startsWith(contentTypeStart) )
+			throw new RuntimeException(contentType);
+		String boundary = "--"+contentType.substring(contentTypeStart.length());
+		this.parser = new Parser(Util.toString(request.body));
+		require( parser.match(boundary) );
+		boundary = "\r\n" + boundary;
+		while( !parser.match("--\r\n") ) {
+			require( parser.match("\r\n") );
+			require( parser.match("Content-Disposition: form-data; name=") );
+			String name = quotedString();
+			String filename = null;
+			boolean isBinary = false;
+			if( parser.match("; filename=") ) {
+				filename = quotedString();
+				require( parser.match("\r\n") );
+				require( parser.match("Content-Type: ") );
+				if( parser.match("application/octet-stream") ) {
+					isBinary = true;
+				} else if( parser.match("text/plain") ) {
+					isBinary = false;
+				} else
+					throw new ParseException(parser,"bad file content-type");
+			}
+			require( parser.match("\r\n") );
+			require( parser.match("\r\n") );
+			int start = parser.currentIndex();
+			while( !parser.test(boundary) ) {
+				require( parser.anyChar() );
+			}
+			String value = parser.textFrom(start);
+			if( filename == null ) {
+				Util.add(request.parameters,name,value);
+			} else {
+				Object content = isBinary ? Util.toBytes(value) : value;
+				Request.MultipartFile mf = new Request.MultipartFile(filename,content);
+				Util.add(request.parameters,name,mf);
+			}
+			require( parser.match(boundary) );
+		}
+	}
+
+	private String quotedString() throws ParseException {
+		StringBuilder sb = new StringBuilder();
+		require( parser.match('"') );
+		while( !parser.match('"') ) {
+			if( parser.match("\\\"") ) {
+				sb.append('"');
+			} else {
+				require( parser.anyChar() );
+				sb.append( parser.lastChar() );
+			}
+		}
+		return sb.toString();
+	}
+
 }
--- a/src/luan/webserver/Util.java	Thu Feb 01 03:08:21 2018 -0700
+++ b/src/luan/webserver/Util.java	Thu Feb 01 22:06:37 2018 -0700
@@ -26,7 +26,7 @@
 		}
 	}
 
-	static void add(Map<String,Object> map,String name,String value) {
+	static void add(Map<String,Object> map,String name,Object value) {
 		Object current = map.get(name);
 		if( current == null ) {
 			map.put(name,value);
@@ -41,5 +41,22 @@
 		}
 	}
 
+	static String toString(byte[] a) {
+		char[] ac = new char[a.length];
+		for( int i=0; i<a.length; i++ ) {
+			ac[i] = (char)a[i];
+		}
+		return new String(ac);
+	}
+
+	static byte[] toBytes(String s) {
+		char[] ac = s.toCharArray();
+		byte[] a = new byte[ac.length];
+		for( int i=0; i<ac.length; i++ ) {
+			a[i] = (byte)ac[i];
+		}
+		return a;
+	}
+
 	private Util() {}  // never
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/webserver/examples/post_multipart.html	Thu Feb 01 22:06:37 2018 -0700
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+	<body>
+		<form action=/params method=post enctype="multipart/form-data">
+			<p>a <input name=a></p>
+			<p>b <input name=b></p>
+			<p><input type=file name=file></p>
+			<p><input type=submit></p>
+		</form>
+	</body>
+</html>