changeset 1347:643cf1c37723

move webserver to lib and bug fixes
author Franklin Schmidt <fschmidt@gmail.com>
date Mon, 25 Feb 2019 13:02:33 -0700
parents efd1c6380f2c
children af478a1bd23d
files conv.txt src/luan/LuanClosure.java src/luan/host/WebHandler.java src/luan/host/run.luan src/luan/lib/webserver/Connection.java src/luan/lib/webserver/Handler.java src/luan/lib/webserver/Request.java src/luan/lib/webserver/RequestParser.java src/luan/lib/webserver/Response.java src/luan/lib/webserver/ResponseOutputStream.java src/luan/lib/webserver/Server.java src/luan/lib/webserver/Status.java src/luan/lib/webserver/Util.java src/luan/lib/webserver/examples/Cookies.java src/luan/lib/webserver/examples/Example.java src/luan/lib/webserver/examples/Headers.java src/luan/lib/webserver/examples/Params.java src/luan/lib/webserver/examples/post.html src/luan/lib/webserver/examples/post_multipart.html src/luan/lib/webserver/handlers/ContentTypeHandler.java src/luan/lib/webserver/handlers/DirHandler.java src/luan/lib/webserver/handlers/DomainHandler.java src/luan/lib/webserver/handlers/FileHandler.java src/luan/lib/webserver/handlers/IndexHandler.java src/luan/lib/webserver/handlers/ListHandler.java src/luan/lib/webserver/handlers/LogHandler.java src/luan/lib/webserver/handlers/MapHandler.java src/luan/lib/webserver/handlers/SafeHandler.java src/luan/modules/Boot.luan src/luan/modules/http/Http.luan src/luan/modules/http/LuanDomainHandler.java src/luan/modules/http/LuanHandler.java src/luan/modules/http/NotFound.java src/luan/modules/http/Server.luan src/luan/modules/lucene/LuceneIndex.java src/luan/modules/url/MultipartClient.java src/luan/webserver/Connection.java src/luan/webserver/Handler.java src/luan/webserver/Request.java src/luan/webserver/RequestParser.java src/luan/webserver/Response.java src/luan/webserver/ResponseOutputStream.java src/luan/webserver/Server.java src/luan/webserver/Status.java src/luan/webserver/Util.java src/luan/webserver/examples/Cookies.java src/luan/webserver/examples/Example.java src/luan/webserver/examples/Headers.java src/luan/webserver/examples/Params.java src/luan/webserver/examples/post.html src/luan/webserver/examples/post_multipart.html src/luan/webserver/handlers/ContentTypeHandler.java src/luan/webserver/handlers/DirHandler.java src/luan/webserver/handlers/DomainHandler.java src/luan/webserver/handlers/FileHandler.java src/luan/webserver/handlers/IndexHandler.java src/luan/webserver/handlers/ListHandler.java src/luan/webserver/handlers/LogHandler.java src/luan/webserver/handlers/MapHandler.java src/luan/webserver/handlers/SafeHandler.java
diffstat 60 files changed, 1410 insertions(+), 1409 deletions(-) [+]
line wrap: on
line diff
diff -r efd1c6380f2c -r 643cf1c37723 conv.txt
--- a/conv.txt	Mon Feb 25 12:29:33 2019 -0700
+++ b/conv.txt	Mon Feb 25 13:02:33 2019 -0700
@@ -1,3 +1,4 @@
+luan.webserver
 indexed_only_field
 lucene search sort
 call
diff -r efd1c6380f2c -r 643cf1c37723 src/luan/LuanClosure.java
--- a/src/luan/LuanClosure.java	Mon Feb 25 12:29:33 2019 -0700
+++ b/src/luan/LuanClosure.java	Mon Feb 25 13:02:33 2019 -0700
@@ -27,7 +27,7 @@
 		try {
 			return doCall(luan,args);
 		} catch(StackOverflowError e) {
-			throw new LuanException( "stack overflow" );
+			throw new LuanException( "stack overflow", e );
 		} finally {
 			luan.pop();
 		}	
diff -r efd1c6380f2c -r 643cf1c37723 src/luan/host/WebHandler.java
--- a/src/luan/host/WebHandler.java	Mon Feb 25 12:29:33 2019 -0700
+++ b/src/luan/host/WebHandler.java	Mon Feb 25 13:02:33 2019 -0700
@@ -3,10 +3,10 @@
 import java.io.File;
 import luan.lib.logging.Logger;
 import luan.lib.logging.LoggerFactory;
-import luan.webserver.Handler;
-import luan.webserver.Request;
-import luan.webserver.Response;
-import luan.webserver.handlers.DomainHandler;
+import luan.lib.webserver.Handler;
+import luan.lib.webserver.Request;
+import luan.lib.webserver.Response;
+import luan.lib.webserver.handlers.DomainHandler;
 import luan.Luan;
 import luan.LuanException;
 import luan.LuanTable;
diff -r efd1c6380f2c -r 643cf1c37723 src/luan/host/run.luan
--- a/src/luan/host/run.luan	Mon Feb 25 12:29:33 2019 -0700
+++ b/src/luan/host/run.luan	Mon Feb 25 13:02:33 2019 -0700
@@ -19,10 +19,10 @@
 
 -- web server
 
-local Server = require "java:luan.webserver.Server"
-local IndexHandler = require "java:luan.webserver.handlers.IndexHandler"
-local ContentTypeHandler = require "java:luan.webserver.handlers.ContentTypeHandler"
-local SafeHandler = require "java:luan.webserver.handlers.SafeHandler"
+local Server = require "java:luan.lib.webserver.Server"
+local IndexHandler = require "java:luan.lib.webserver.handlers.IndexHandler"
+local ContentTypeHandler = require "java:luan.lib.webserver.handlers.ContentTypeHandler"
+local SafeHandler = require "java:luan.lib.webserver.handlers.SafeHandler"
 
 local webHandler = WebHandler.new(Hosting.sites_dir)
 local handler = webHandler
diff -r efd1c6380f2c -r 643cf1c37723 src/luan/lib/webserver/Connection.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/lib/webserver/Connection.java	Mon Feb 25 13:02:33 2019 -0700
@@ -0,0 +1,134 @@
+package luan.lib.webserver;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.IOException;
+import java.net.Socket;
+import luan.lib.logging.Logger;
+import luan.lib.logging.LoggerFactory;
+import luan.lib.parser.ParseException;
+
+
+final class Connection {
+	private static final Logger logger = LoggerFactory.getLogger(Connection.class);
+
+	static void handle(Server server,Socket socket) {
+		new Connection(server,socket).handle();
+	}
+
+	private final Server server;
+	private final Socket socket;
+
+	private Connection(Server server,Socket socket) {
+		this.server = server;
+		this.socket = socket;
+	}
+
+	private void handle() {
+		try {
+			Request request = new Request();
+			Response response;
+			try {
+				{
+					InputStream in = socket.getInputStream();
+					byte[] a = new byte[8192];
+					int endOfHeader;
+					int size = 0;
+					int left = a.length;
+					outer: while(true) {
+						int n = in.read(a,size,left);
+						if( n == -1 ) {
+							if( size == 0 ) {
+								socket.close();
+								return;
+							}
+							throw new IOException("unexpected end of input at "+size);
+						}
+						size += n;
+						for( int i=0; i<=size-4; i++ ) {
+							if( a[i]=='\r' && a[i+1]=='\n' && a[i+2]=='\r' && a[i+3]=='\n' ) {
+								endOfHeader = i + 4;
+								break outer;
+							}
+						}
+						left -= n;
+						if( left == 0 ) {
+							byte[] a2 = new byte[2*a.length];
+							System.arraycopy(a,0,a2,0,size);
+							a = a2;
+							left = a.length - size;
+						}
+					}
+					String rawHead = new String(a,0,endOfHeader);
+					//System.out.println(rawHead);
+					request.rawHead = rawHead;
+					RequestParser parser = new RequestParser(request);
+					parser.parseHead();
+		
+					String lenStr = (String)request.headers.get("content-length");
+					if( lenStr != null ) {
+						int len = Integer.parseInt(lenStr);
+						byte[] body = new byte[len];
+						size -= endOfHeader;
+						System.arraycopy(a,endOfHeader,body,0,size);
+						while( size < len ) {
+							int n = in.read(body,size,len-size);
+							if( n == -1 ) {
+								throw new IOException("unexpected end of input at "+size);
+							}
+							size += n;
+						}
+						request.body = body;
+						//System.out.println(new String(request.body));
+					}
+	
+					String contentType = (String)request.headers.get("content-type");
+					if( contentType != null ) {
+						contentType = contentType.toLowerCase();
+						if( "application/x-www-form-urlencoded".equals(contentType) ) {
+							parser.parseUrlencoded(null);
+						} else if( "application/x-www-form-urlencoded; charset=utf-8".equals(contentType) ) {
+							parser.parseUrlencoded("utf-8");
+						} else if( contentType.startsWith("multipart/form-data;") ) {
+							parser.parseMultipart();
+						} else if( contentType.equals("application/json; charset=utf-8") ) {
+							parser.parseJson();
+						} else {
+							logger.info("unknown request content-type: "+contentType);
+						}
+					}
+
+					String scheme = (String)request.headers.get("x-forwarded-proto");
+					if( scheme != null )
+						request.scheme = scheme;
+				}
+				response = server.handler.handle(request);
+			} catch(ParseException e) {
+				logger.warn("parse error\n"+request.rawHead.trim()+"\n",e);
+				response = Response.errorResponse(Status.BAD_REQUEST,e.toString());
+			}
+			response.headers.put("connection","close");
+			response.headers.put("content-length",Long.toString(response.body.length));
+			byte[] header = response.toHeaderString().getBytes();
+	
+			OutputStream out = socket.getOutputStream();
+			out.write(header);
+			copyAll(response.body.content,out);
+			out.close();
+			socket.close();
+		} catch(IOException e) {
+			logger.info("",e);
+		}
+	}
+
+	private static void copyAll(InputStream in,OutputStream out)
+		throws IOException
+	{
+		byte[] a = new byte[8192];
+		int n;
+		while( (n=in.read(a)) != -1 ) {
+			out.write(a,0,n);
+		}
+	}
+
+}
diff -r efd1c6380f2c -r 643cf1c37723 src/luan/lib/webserver/Handler.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/lib/webserver/Handler.java	Mon Feb 25 13:02:33 2019 -0700
@@ -0,0 +1,6 @@
+package luan.lib.webserver;
+
+
+public interface Handler {
+	public Response handle(Request request);
+}
diff -r efd1c6380f2c -r 643cf1c37723 src/luan/lib/webserver/Request.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/lib/webserver/Request.java	Mon Feb 25 13:02:33 2019 -0700
@@ -0,0 +1,35 @@
+package luan.lib.webserver;
+
+import java.util.Map;
+import java.util.LinkedHashMap;
+import java.util.Collections;
+
+
+public class Request {
+	public volatile String rawHead;
+	public volatile String method;
+	public volatile String rawPath;
+	public volatile String path;
+	public volatile String protocol;  // only HTTP/1.1 is accepted
+	public volatile String scheme;
+	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 byte[] body;
+
+	public static final class MultipartFile {
+		public final String filename;
+		public final String contentType;
+		public final Object content;  // byte[] or String
+
+		public MultipartFile(String filename,String contentType,Object content) {
+			this.filename = filename;
+			this.contentType = contentType;
+			this.content = content;
+		}
+
+		public String toString() {
+			return "{filename="+filename+", content="+content+"}";
+		}
+	}
+}
diff -r efd1c6380f2c -r 643cf1c37723 src/luan/lib/webserver/RequestParser.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/lib/webserver/RequestParser.java	Mon Feb 25 13:02:33 2019 -0700
@@ -0,0 +1,286 @@
+package luan.lib.webserver;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.util.List;
+import java.util.ArrayList;
+import luan.lib.logging.Logger;
+import luan.lib.logging.LoggerFactory;
+import luan.lib.parser.Parser;
+import luan.lib.parser.ParseException;
+
+
+final class RequestParser {
+	private static final Logger logger = LoggerFactory.getLogger(RequestParser.class);
+	private final Request request;
+	private Parser parser;
+
+	RequestParser(Request request) {
+		this.request = request;
+	}
+
+	void parseUrlencoded(String charset) throws ParseException, UnsupportedEncodingException {
+		if( request.body == null ) {
+			logger.warn("body is null\n"+request.rawHead);
+			return;
+		}
+		this.parser = new Parser(Util.toString(request.body,charset));
+		parseQuery();
+		require( parser.endOfInput() );
+	}
+
+	void parseHead() throws ParseException {
+		this.parser = new Parser(request.rawHead);
+		parseRequestLine();
+		while( !parser.match("\r\n") ) {
+			parserHeaderField();
+		}
+		parseCookies();
+	}
+
+	private void parseRequestLine() throws ParseException {
+		parseMethod();
+		require( parser.match(' ') );
+		parseRawPath();
+		require( parser.match(' ') );
+		parseProtocol();
+		require( parser.match("\r\n") );
+	}
+
+	private void parseMethod() throws ParseException {
+		int start = parser.currentIndex();
+		if( !methodChar() )
+			throw new ParseException(parser,"no method");
+		while( methodChar() );
+		request.method = parser.textFrom(start);
+	}
+
+	private boolean methodChar() {
+		return parser.inCharRange('A','Z');
+	}
+
+	private void parseRawPath() throws ParseException {
+		int start = parser.currentIndex();
+		parsePath();
+		if( parser.match('?') )
+			parseQuery();
+		request.rawPath = parser.textFrom(start);
+	}
+
+	private void parsePath() throws ParseException {
+		int start = parser.currentIndex();
+		if( !parser.match('/') )
+			throw new ParseException(parser,"bad path");
+		while( parser.noneOf(" ?#") );
+		request.path = urlDecode( parser.textFrom(start) );
+	}
+
+	private void parseQuery() throws ParseException {
+		do {
+			int start = parser.currentIndex();
+			while( queryChar() );
+			String name = urlDecode( parser.textFrom(start) );
+			String value = null;
+			if( parser.match('=') ) {
+				start = parser.currentIndex();
+				while( queryChar() || parser.match('=') );
+				value = urlDecode( parser.textFrom(start) );
+			}
+			if( name.length() > 0 || value != null ) {
+				if( value==null )
+					value = "";
+				Util.add(request.parameters,name,value);
+			}
+		} while( parser.match('&') );
+	}
+
+	private boolean queryChar() {
+		return parser.noneOf("=&# \t\n\f\r\u000b");
+	}
+
+	private void parseProtocol() throws ParseException {
+		int start = parser.currentIndex();
+		if( !(
+			parser.match("HTTP/")
+			&& parser.inCharRange('0','9')
+			&& parser.match('.')
+			&& parser.inCharRange('0','9')
+		) )
+			throw new ParseException(parser,"bad protocol");
+		request.protocol = parser.textFrom(start);
+		request.scheme = "http";
+	}
+
+
+	private void parserHeaderField() throws ParseException {
+		String name = parseName();
+		require( parser.match(':') );
+		while( parser.anyOf(" \t") );
+		String value = parseValue();
+		while( parser.anyOf(" \t") );
+		require( parser.match("\r\n") );
+		Util.add(request.headers,name,value);
+	}
+
+	private String parseName() throws ParseException {
+		int start = parser.currentIndex();
+		require( tokenChar() );
+		while( tokenChar() );
+		return parser.textFrom(start).toLowerCase();
+	}
+
+	private String parseValue() throws ParseException {
+		int start = parser.currentIndex();
+		while( !testEndOfValue() )
+			require( parser.anyChar() );
+		return parser.textFrom(start);
+	}
+
+	private boolean testEndOfValue() {
+		parser.begin();
+		while( parser.anyOf(" \t") );
+		boolean b = parser.endOfInput() || parser.anyOf("\r\n");
+		parser.failure();  // rollback
+		return b;
+	}
+
+	private void require(boolean b) throws ParseException {
+		if( !b )
+			throw new ParseException(parser,"failed");
+	}
+
+	boolean tokenChar() {
+		if( parser.endOfInput() )
+			return false;
+		char c = parser.currentChar();
+		if( 32 <= c && c <= 126 && "()<>@,;:\\\"/[]?={} \t\r\n".indexOf(c) == -1 ) {
+			parser.anyChar();
+			return true;
+		} else {
+			return false;
+		}
+	}
+
+
+	private void parseCookies() throws ParseException {
+		String text = (String)request.headers.get("cookie");
+		if( text == null )
+			return;
+		this.parser = new Parser(text);
+		while(true) {
+			int start = parser.currentIndex();
+			while( parser.noneOf("=;") );
+			String name = urlDecode( parser.textFrom(start) );
+			if( parser.match('=') ) {
+				start = parser.currentIndex();
+				while( parser.noneOf(";") );
+				String value = parser.textFrom(start);
+				int len = value.length();
+				if( value.charAt(0)=='"' && value.charAt(len-1)=='"' )
+					value = value.substring(1,len-1);
+				value = urlDecode(value);
+				request.cookies.put(name,value);
+			}
+			if( parser.endOfInput() )
+				return;
+			require( parser.match(';') );
+			parser.match(' ');  // optional for bad browsers
+		}
+	}
+
+
+	private static final String contentTypeStart = "multipart/form-data; boundary=";
+
+	void parseMultipart() throws ParseException, UnsupportedEncodingException {
+		if( request.body == null ) {
+			logger.warn("body is null\n"+request.rawHead);
+			return;
+		}
+		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,null));
+//System.out.println(this.parser.text);
+		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: ") );
+				int start = parser.currentIndex();
+				if( parser.match("application/") ) {
+					isBinary = true;
+				} else if( parser.match("image/") ) {
+					isBinary = true;
+				} else if( parser.match("text/") ) {
+					isBinary = false;
+				} else
+					throw new ParseException(parser,"bad file content-type");
+				while( parser.inCharRange('a','z') || parser.anyOf("-.") );
+				contentType = parser.textFrom(start);
+			}
+			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,contentType,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();
+	}
+
+	private String urlDecode(String s) throws ParseException {
+		try {
+			return URLDecoder.decode(s,"UTF-8");
+		} catch(UnsupportedEncodingException e) {
+			parser.rollback();
+			throw new ParseException(parser,e);
+		} catch(IllegalArgumentException e) {
+			parser.rollback();
+			throw new ParseException(parser,e);
+		}
+	}
+
+	// improve later
+	void parseJson() throws UnsupportedEncodingException {
+		if( request.body == null ) {
+			logger.warn("body is null\n"+request.rawHead);
+			return;
+		}
+		String contentType = (String)request.headers.get("content-type");
+		if( !contentType.equals("application/json; charset=utf-8") )
+			throw new RuntimeException(contentType);
+		String value = new String(request.body,"utf-8");
+		Util.add(request.parameters,"json",value);
+	}
+
+}
diff -r efd1c6380f2c -r 643cf1c37723 src/luan/lib/webserver/Response.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/lib/webserver/Response.java	Mon Feb 25 13:02:33 2019 -0700
@@ -0,0 +1,85 @@
+package luan.lib.webserver;
+
+import java.io.InputStream;
+import java.io.PrintWriter;
+import java.util.Map;
+import java.util.LinkedHashMap;
+import java.util.Collections;
+import java.util.List;
+
+
+public class Response {
+	public final String protocol = "HTTP/1.1";
+	public volatile Status status = Status.OK;
+	public final Map<String,Object> headers = Collections.synchronizedMap(new LinkedHashMap<String,Object>());
+	{
+		headers.put("server","Luan");
+	}
+	private static final Body empty = new Body(0,new InputStream(){
+		public int read() { return -1; }
+	});
+	public volatile Body body = empty;
+
+	public static class Body {
+		public final long length;
+		public final InputStream content;
+	
+		public Body(long length,InputStream content) {
+			this.length = length;
+			this.content = content;
+		}
+	}
+
+
+	public void addHeader(String name,String value) {
+		Util.add(headers,name,value);
+	}
+
+	public void setCookie(String name,String value,Map<String,String> attributes) {
+		StringBuilder buf = new StringBuilder();
+		buf.append( Util.urlEncode(name) );
+		buf.append( '=' );
+		buf.append( Util.urlEncode(value) );
+		for( Map.Entry<String,String> entry : attributes.entrySet() ) {
+			buf.append( "; " );
+			buf.append( entry.getKey() );
+			buf.append( '=' );
+			buf.append( entry.getValue() );
+		}
+		addHeader( "Set-Cookie", buf.toString() );
+	}
+
+
+	public String toHeaderString() {
+		StringBuilder sb = new StringBuilder();
+		sb.append( protocol )
+			.append( ' ' ).append( status.code )
+			.append( ' ' ).append( status.reason )
+			.append( "\r\n" )
+		;
+		for( Map.Entry<String,Object> entry : headers.entrySet() ) {
+			String name = entry.getKey();
+			Object value = entry.getValue();
+			if( value instanceof List ) {
+				for( Object v : (List)value ) {
+					sb.append( name ).append( ": " ).append( v ).append( "\r\n" );
+				}
+			} else {
+				sb.append( name ).append( ": " ).append( value ).append( "\r\n" );
+			}
+		}
+		sb.append( "\r\n" );
+		return sb.toString();
+	}
+
+
+	public static Response errorResponse(Status status,String text) {
+		Response response = new Response();
+		response.status = status;
+		response.headers.put( "content-type", "text/plain; charset=utf-8" );
+		PrintWriter writer = new PrintWriter( new ResponseOutputStream(response) );
+		writer.write( text );
+		writer.close();
+		return response;
+	}
+}
diff -r efd1c6380f2c -r 643cf1c37723 src/luan/lib/webserver/ResponseOutputStream.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/lib/webserver/ResponseOutputStream.java	Mon Feb 25 13:02:33 2019 -0700
@@ -0,0 +1,22 @@
+package luan.lib.webserver;
+
+import java.io.ByteArrayOutputStream;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+
+
+// plenty of room for improvement
+public class ResponseOutputStream extends ByteArrayOutputStream {
+	private final Response response;
+
+	public ResponseOutputStream(Response response) {
+		if(response==null) throw new NullPointerException();
+		this.response = response;
+	}
+
+	@Override public void close() throws IOException {
+		super.close();
+		int size = size();
+		response.body = new Response.Body( size, new ByteArrayInputStream(buf,0,size) );
+	}
+}
diff -r efd1c6380f2c -r 643cf1c37723 src/luan/lib/webserver/Server.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/lib/webserver/Server.java	Mon Feb 25 13:02:33 2019 -0700
@@ -0,0 +1,78 @@
+package luan.lib.webserver;
+
+import java.io.IOException;
+import java.net.Socket;
+import java.net.ServerSocket;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import luan.lib.logging.Logger;
+import luan.lib.logging.LoggerFactory;
+
+
+public class Server {
+	private static final Logger logger = LoggerFactory.getLogger(Server.class);
+
+	public final int port;
+	public final Handler handler;
+	public static final ThreadPoolExecutor threadPool = (ThreadPoolExecutor)Executors.newCachedThreadPool();
+
+	public Server(int port,Handler handler) {
+		this.port = port;
+		this.handler = handler;
+	}
+
+	protected ServerSocket newServerSocket() throws IOException {
+		return new ServerSocket(port);
+	}
+
+	public synchronized void start() throws IOException {
+		final ServerSocket ss = newServerSocket();
+		threadPool.execute(new Runnable(){public void run() {
+			try {
+				while(!threadPool.isShutdown()) {
+					final Socket socket = ss.accept();
+					threadPool.execute(new Runnable(){public void run() {
+						Connection.handle(Server.this,socket);
+					}});
+				}
+			} catch(IOException e) {
+				logger.error("",e);
+			}
+		}});
+		logger.info("started server on port "+port);
+	}
+
+	public synchronized boolean stop(long timeoutSeconds) {
+		try {
+			threadPool.shutdownNow();
+			boolean stopped = threadPool.awaitTermination(timeoutSeconds,TimeUnit.SECONDS);
+			if(stopped)
+				logger.info("stopped server on port "+port);
+			else
+				logger.warn("couldn't stop server on port "+port);
+			return stopped;
+		} catch(InterruptedException e) {
+			throw new RuntimeException(e);
+		}
+	}
+
+	public static class ForAddress extends Server {
+		private final InetAddress addr;
+
+		public ForAddress(InetAddress addr,int port,Handler handler) {
+			super(port,handler);
+			this.addr = addr;
+		}
+
+		public ForAddress(String addrName,int port,Handler handler) throws UnknownHostException {
+			this(InetAddress.getByName(addrName),port,handler);
+		}
+
+		protected ServerSocket newServerSocket() throws IOException {
+			return new ServerSocket(port,0,addr);
+		}
+	}
+}
diff -r efd1c6380f2c -r 643cf1c37723 src/luan/lib/webserver/Status.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/lib/webserver/Status.java	Mon Feb 25 13:02:33 2019 -0700
@@ -0,0 +1,43 @@
+package luan.lib.webserver;
+
+import java.util.Map;
+import java.util.HashMap;
+import luan.lib.logging.Logger;
+import luan.lib.logging.LoggerFactory;
+
+
+public class Status {
+	private static final Logger logger = LoggerFactory.getLogger(Status.class);
+
+	public final int code;
+	public final String reason;
+
+	public Status(int code,String reason) {
+		this.code = code;
+		this.reason = reason;
+	}
+
+	private static final Map<Integer,Status> map = new HashMap<Integer,Status>();
+
+	protected static Status newStatus(int code,String reason) {
+		Status status = new Status(code,reason);
+		map.put(code,status);
+		return status;
+	}
+
+	public static Status getStatus(int code) {
+		Status status = map.get(code);
+		if( status == null ) {
+			logger.warn("missing status "+code);
+			status = new Status(code,"");
+		}
+		return status;
+	}
+
+	public static final Status OK = newStatus(200,"OK");
+	public static final Status MOVED_PERMANENTLY = newStatus(301,"Moved Permanently");
+	public static final Status FOUND = newStatus(302,"Found");
+	public static final Status BAD_REQUEST = newStatus(400,"Bad Request");
+	public static final Status NOT_FOUND = newStatus(404,"Not Found");
+	public static final Status INTERNAL_SERVER_ERROR = newStatus(500,"Internal Server Error");
+}
diff -r efd1c6380f2c -r 643cf1c37723 src/luan/lib/webserver/Util.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/lib/webserver/Util.java	Mon Feb 25 13:02:33 2019 -0700
@@ -0,0 +1,55 @@
+package luan.lib.webserver;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.util.Map;
+import java.util.List;
+import java.util.ArrayList;
+
+
+final class Util {
+
+	static String urlEncode(String s) {
+		try {
+			return URLEncoder.encode(s,"UTF-8");
+		} catch(UnsupportedEncodingException e) {
+			throw new RuntimeException(e);
+		}
+	}
+
+	static void add(Map<String,Object> map,String name,Object value) {
+		Object current = map.get(name);
+		if( current == null ) {
+			map.put(name,value);
+		} else if( current instanceof List ) {
+			List list = (List)current;
+			list.add(value);
+		} else {
+			List list = new ArrayList();
+			list.add(current);
+			list.add(value);
+			map.put(name,list);
+		}
+	}
+
+	static String toString(byte[] a,String charset) throws UnsupportedEncodingException {
+		if( charset != null )
+			return new String(a,charset);
+		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
+}
diff -r efd1c6380f2c -r 643cf1c37723 src/luan/lib/webserver/examples/Cookies.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/lib/webserver/examples/Cookies.java	Mon Feb 25 13:02:33 2019 -0700
@@ -0,0 +1,42 @@
+package luan.lib.webserver.examples;
+
+import java.io.Writer;
+import java.io.OutputStreamWriter;
+import java.io.IOException;
+import java.util.Map;
+import java.util.HashMap;
+import luan.lib.webserver.Handler;
+import luan.lib.webserver.Request;
+import luan.lib.webserver.Response;
+import luan.lib.webserver.ResponseOutputStream;
+
+
+public final class Cookies implements Handler {
+
+	public Response handle(Request request) {
+		Response response = new Response();
+		String name = (String)request.parameters.get("name");
+		if( name != null ) {
+			Map<String,String> attributes = new HashMap<String,String>();
+			String value = (String)request.parameters.get("value");
+			if( value != null ) {
+				response.setCookie(name,value,attributes);
+			} else {
+				attributes.put("Max-Age","0");
+				response.setCookie(name,"delete",attributes);
+			}
+		}
+		response.headers.put( "content-type", "text/plain; charset=utf-8" );
+		try {
+			Writer writer = new OutputStreamWriter( new ResponseOutputStream(response) );
+			for( Map.Entry<String,String> entry : request.cookies.entrySet() ) {
+				writer.write(entry.getKey()+" = "+entry.getValue()+"\n");
+			}
+			writer.close();
+		} catch(IOException e) {
+			throw new RuntimeException(e);
+		}
+		return response;
+	}
+
+}
diff -r efd1c6380f2c -r 643cf1c37723 src/luan/lib/webserver/examples/Example.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/lib/webserver/examples/Example.java	Mon Feb 25 13:02:33 2019 -0700
@@ -0,0 +1,72 @@
+package luan.lib.webserver.examples;
+
+import java.io.Writer;
+import java.io.OutputStreamWriter;
+import java.io.IOException;
+import java.util.Map;
+import java.util.HashMap;
+import org.apache.log4j.EnhancedPatternLayout;
+import org.apache.log4j.ConsoleAppender;
+import org.apache.log4j.Logger;
+import luan.lib.webserver.Handler;
+import luan.lib.webserver.Request;
+import luan.lib.webserver.Response;
+import luan.lib.webserver.ResponseOutputStream;
+import luan.lib.webserver.Server;
+import luan.lib.webserver.handlers.MapHandler;
+import luan.lib.webserver.handlers.SafeHandler;
+import luan.lib.webserver.handlers.LogHandler;
+import luan.lib.webserver.handlers.FileHandler;
+import luan.lib.webserver.handlers.DirHandler;
+import luan.lib.webserver.handlers.ListHandler;
+import luan.lib.webserver.handlers.ContentTypeHandler;
+
+
+public class Example implements Handler {
+
+	public Response handle(Request request) {
+		Response response = new Response();
+		response.headers.put( "content-type", "text/plain; charset=utf-8" );
+		try {
+			Writer writer = new OutputStreamWriter( new ResponseOutputStream(response) );
+			writer.write("Hello World\n");
+			writer.close();
+		} catch(IOException e) {
+			throw new RuntimeException("shouldn't happen",e);
+		}
+		return response;
+	}
+
+	public static void simple() throws IOException {
+		Handler handler = new Example();
+		new Server(8080,handler).start();
+	}
+
+	public static void fancy() throws IOException {
+		Map<String,Handler> map = new HashMap<String,Handler>();
+		map.put( "/hello", new Example() );
+		map.put( "/headers", new Headers() );
+		map.put( "/params", new Params() );
+		map.put( "/cookies", new Cookies() );
+		Handler mapHandler = new MapHandler(map);
+		FileHandler fileHandler = new FileHandler();
+		Handler dirHandler = new DirHandler(fileHandler);
+		Handler handler = new ListHandler( mapHandler, fileHandler, dirHandler );
+		handler = new ContentTypeHandler(handler);
+		handler = new SafeHandler(handler);
+		handler = new LogHandler(handler);
+		new Server(8080,handler).start();
+	}
+
+	public static void initLogging() {
+//		Logger.getRootLogger().setLevel(Level.INFO);
+		EnhancedPatternLayout layout = new EnhancedPatternLayout("%d{HH:mm:ss} %-5p %c - %m%n");
+		ConsoleAppender appender = new ConsoleAppender(layout,"System.err");
+		Logger.getRootLogger().addAppender(appender);
+	}
+
+	public static void main(String[] args) throws Exception {
+		initLogging();
+		fancy();
+	}
+}
diff -r efd1c6380f2c -r 643cf1c37723 src/luan/lib/webserver/examples/Headers.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/lib/webserver/examples/Headers.java	Mon Feb 25 13:02:33 2019 -0700
@@ -0,0 +1,30 @@
+package luan.lib.webserver.examples;
+
+import java.io.Writer;
+import java.io.OutputStreamWriter;
+import java.io.IOException;
+import java.util.Map;
+import luan.lib.webserver.Handler;
+import luan.lib.webserver.Request;
+import luan.lib.webserver.Response;
+import luan.lib.webserver.ResponseOutputStream;
+
+
+public final class Headers implements Handler {
+
+	public Response handle(Request request) {
+		Response response = new Response();
+		response.headers.put( "content-type", "text/plain; charset=utf-8" );
+		try {
+			Writer writer = new OutputStreamWriter( new ResponseOutputStream(response) );
+			for( Map.Entry<String,Object> entry : request.headers.entrySet() ) {
+				writer.write(entry.getKey()+": "+entry.getValue()+"\n");
+			}
+			writer.close();
+		} catch(IOException e) {
+			throw new RuntimeException(e);
+		}
+		return response;
+	}
+
+}
diff -r efd1c6380f2c -r 643cf1c37723 src/luan/lib/webserver/examples/Params.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/lib/webserver/examples/Params.java	Mon Feb 25 13:02:33 2019 -0700
@@ -0,0 +1,30 @@
+package luan.lib.webserver.examples;
+
+import java.io.Writer;
+import java.io.OutputStreamWriter;
+import java.io.IOException;
+import java.util.Map;
+import luan.lib.webserver.Handler;
+import luan.lib.webserver.Request;
+import luan.lib.webserver.Response;
+import luan.lib.webserver.ResponseOutputStream;
+
+
+public final class Params implements Handler {
+
+	public Response handle(Request request) {
+		Response response = new Response();
+		response.headers.put( "content-type", "text/plain; charset=utf-8" );
+		try {
+			Writer writer = new OutputStreamWriter( new ResponseOutputStream(response) );
+			for( Map.Entry<String,Object> entry : request.parameters.entrySet() ) {
+				writer.write(entry.getKey()+" = "+entry.getValue()+"\n");
+			}
+			writer.close();
+		} catch(IOException e) {
+			throw new RuntimeException(e);
+		}
+		return response;
+	}
+
+}
diff -r efd1c6380f2c -r 643cf1c37723 src/luan/lib/webserver/examples/post.html
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/lib/webserver/examples/post.html	Mon Feb 25 13:02:33 2019 -0700
@@ -0,0 +1,10 @@
+<!doctype html>
+<html>
+	<body>
+		<form action=/params method=post>
+			<p>a <input name=a></p>
+			<p>b <input name=b></p>
+			<p><input type=submit></p>
+		</form>
+	</body>
+</html>
diff -r efd1c6380f2c -r 643cf1c37723 src/luan/lib/webserver/examples/post_multipart.html
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/lib/webserver/examples/post_multipart.html	Mon Feb 25 13:02:33 2019 -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>
diff -r efd1c6380f2c -r 643cf1c37723 src/luan/lib/webserver/handlers/ContentTypeHandler.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/lib/webserver/handlers/ContentTypeHandler.java	Mon Feb 25 13:02:33 2019 -0700
@@ -0,0 +1,54 @@
+package luan.lib.webserver.handlers;
+
+import java.util.Map;
+import java.util.HashMap;
+import luan.lib.webserver.Handler;
+import luan.lib.webserver.Request;
+import luan.lib.webserver.Response;
+
+
+public class ContentTypeHandler implements Handler {
+	private final Handler handler;
+
+	// maps extension to content-type
+	// key must be lower case
+	public final Map<String,String> map = new HashMap<String,String>();
+
+	// set to null for none
+	public String contentTypeForNoExtension;
+
+	public ContentTypeHandler(Handler handler) {
+		this(handler,"utf-8");
+	}
+
+	public ContentTypeHandler(Handler handler,String charset) {
+		this.handler = handler;
+		String attrs = charset== null ? "" : "; charset="+charset;
+		String htmlType = "text/html" + attrs;
+		String textType = "text/plain" + attrs;
+		contentTypeForNoExtension = htmlType;
+		map.put( "html", htmlType );
+		map.put( "txt", textType );
+		map.put( "css", "text/css" );
+		// add more as need
+	}
+
+	public Response handle(Request request) {
+		Response response = handler.handle(request);
+		if( response!=null && !response.headers.containsKey("content-type") ) {
+			String path = request.path;
+			int iSlash = path.lastIndexOf('/');
+			int iDot = path.lastIndexOf('.');
+			String type;
+			if( iDot < iSlash ) {  // no extension
+				type = contentTypeForNoExtension;
+			} else {  // extension
+				String extension = path.substring(iDot+1);
+				type = map.get( extension.toLowerCase() );
+			}
+			if( type != null )
+				response.headers.put("content-type",type);
+		}
+		return response;
+	}
+}
diff -r efd1c6380f2c -r 643cf1c37723 src/luan/lib/webserver/handlers/DirHandler.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/lib/webserver/handlers/DirHandler.java	Mon Feb 25 13:02:33 2019 -0700
@@ -0,0 +1,68 @@
+package luan.lib.webserver.handlers;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.Writer;
+import java.io.OutputStreamWriter;
+import java.io.IOException;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.Date;
+import luan.lib.webserver.Handler;
+import luan.lib.webserver.Request;
+import luan.lib.webserver.Response;
+import luan.lib.webserver.ResponseOutputStream;
+
+
+public final class DirHandler implements Handler {
+	private final FileHandler fileHandler;
+
+	public DirHandler(FileHandler fileHandler) {
+		this.fileHandler = fileHandler;
+	}
+
+	private static final Comparator<File> sorter = new Comparator<File>() {
+		public int compare(File f1, File f2) {
+			return f1.getName().compareTo(f2.getName());
+		}
+	};
+
+	public Response handle(Request request) {
+		try {
+			File file = fileHandler.file(request);
+			if( request.path.endsWith("/") && file.isDirectory() ) {
+				DateFormat fmt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss zzz");
+				Response response = new Response();
+				response.headers.put( "content-type", "text/html; charset=utf-8" );
+				Writer writer = new OutputStreamWriter( new ResponseOutputStream(response) );
+				writer.write( "<!doctype html><html>" );
+				writer.write( "<head><style>td{padding: 2px 8px}</style></head>" );
+				writer.write( "<body>" );
+				writer.write( "<h1>Directory: "+request.path+"</h1>" );
+				writer.write( "<table border=0>" );
+				File[] a = file.listFiles();
+				Arrays.sort(a,sorter);
+				for( File child : a ) {
+					String name = child.getName();
+					if( child.isDirectory() )
+						name += '/';
+					writer.write( "<tr>" );
+					writer.write( "<td><a href='"+name+"'>"+name+"</a></td>" );
+					writer.write( "<td>"+child.length()+" bytes</td>" );
+					writer.write( "<td>"+fmt.format(new Date(child.lastModified()))+"</td>" );
+					writer.write( "</tr>" );
+				}
+				writer.write( "</table>" );
+				writer.write( "</body>" );
+				writer.write( "</html>" );
+				writer.close();
+				return response;
+			}
+			return null;
+		} catch(IOException e) {
+			throw new RuntimeException(e);
+		}
+	}
+}
diff -r efd1c6380f2c -r 643cf1c37723 src/luan/lib/webserver/handlers/DomainHandler.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/lib/webserver/handlers/DomainHandler.java	Mon Feb 25 13:02:33 2019 -0700
@@ -0,0 +1,116 @@
+package luan.lib.webserver.handlers;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.lang.ref.Reference;
+import java.lang.ref.SoftReference;
+import java.lang.ref.ReferenceQueue;
+import java.util.Map;
+import java.util.HashMap;
+import luan.lib.logging.Logger;
+import luan.lib.logging.LoggerFactory;
+import luan.lib.webserver.Handler;
+import luan.lib.webserver.Request;
+import luan.lib.webserver.Response;
+
+
+public final class DomainHandler implements Handler {
+	private static final Logger logger = LoggerFactory.getLogger(DomainHandler.class);
+
+	public interface Factory {
+		public Handler newHandler(String domain);
+	}
+
+	private static class Ref {
+		private final Handler handler;
+
+		private Ref(Handler handler) {
+			this.handler = handler;
+		}
+	}
+
+	private final ReferenceQueue<Ref> queue = new ReferenceQueue<Ref>();
+
+	private class MyReference extends SoftReference<Ref> {
+		private Handler handler;
+
+		private MyReference(Ref r) {
+			super(r,queue);
+			this.handler = r.handler;
+		}
+	}
+
+	private static void close(Handler handler) {
+		if( handler instanceof Closeable ) {
+			try {
+				((Closeable)handler).close();
+			} catch(IOException e) {
+				logger.error(handler.toString(),e);
+			}
+		}
+	}
+
+	private void sweep() {
+		while(true) {
+			MyReference ref = (MyReference)queue.poll();
+			if( ref == null )
+				return;
+			//logger.info("sweep");
+			close(ref.handler);
+			ref.handler = null;
+		}
+	}
+
+	private final Map<String,MyReference> map = new HashMap<String,MyReference>();
+
+	private final Factory factory;
+
+	public DomainHandler(Factory factory) {
+		this.factory = factory;
+	}
+
+	public Response handle(Request request) {
+		String host = (String)request.headers.get("host");
+		if( host == null )
+			return null;
+		int i = host.indexOf(':');
+		String domain = i == -1 ? host : host.substring(0,i);
+		Handler handler = getHandler(domain);
+		return handler==null ? null : handler.handle(request);
+	}
+
+	public Handler getHandler(String domain) {
+		Ref r = getRef(domain);
+		return r==null ? null : r.handler;
+	}
+
+	public void removeHandler(String domain) {
+		domain = domain.toLowerCase();
+		synchronized(map) {
+			Reference<Ref> ref = map.remove(domain);
+			Ref r = ref==null ? null : ref.get();
+			if( r != null ) {
+				close(r.handler);
+			}
+		}
+	}
+
+	private Ref getRef(String domain) {
+		domain = domain.toLowerCase();
+		synchronized(map) {
+			Reference<Ref> ref = map.get(domain);
+			Ref r = ref==null ? null : ref.get();
+			if( r == null ) {
+				//if(ref!=null) logger.info("gc "+domain);
+				sweep();
+				Handler handler = factory.newHandler(domain);
+				if( handler == null )
+					return null;
+				r = new Ref(handler);
+				map.put(domain,new MyReference(r));
+			}
+			return r;
+		}
+	}
+
+}
diff -r efd1c6380f2c -r 643cf1c37723 src/luan/lib/webserver/handlers/FileHandler.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/lib/webserver/handlers/FileHandler.java	Mon Feb 25 13:02:33 2019 -0700
@@ -0,0 +1,51 @@
+package luan.lib.webserver.handlers;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.Writer;
+import java.io.OutputStreamWriter;
+import java.io.IOException;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import luan.lib.webserver.Handler;
+import luan.lib.webserver.Request;
+import luan.lib.webserver.Response;
+import luan.lib.webserver.ResponseOutputStream;
+
+
+public class FileHandler implements Handler {
+	final File dir;
+
+	public FileHandler() {
+		this(".");
+	}
+
+	public FileHandler(String pathname) {
+		this(new File(pathname));
+	}
+
+	public FileHandler(File dir) {
+		if( !dir.isDirectory() )
+			throw new RuntimeException("must be a directory");
+		this.dir = dir;
+	}
+
+	File file(Request request) {
+		return new File(dir,request.path);
+	}
+
+	public Response handle(Request request) {
+		try {
+			File file = file(request);
+			if( file.isFile() ) {
+				Response response = new Response();
+				response.body = new Response.Body( file.length(), new FileInputStream(file) );
+				return response;
+			}
+			return null;
+		} catch(IOException e) {
+			throw new RuntimeException(e);
+		}
+	}
+}
diff -r efd1c6380f2c -r 643cf1c37723 src/luan/lib/webserver/handlers/IndexHandler.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/lib/webserver/handlers/IndexHandler.java	Mon Feb 25 13:02:33 2019 -0700
@@ -0,0 +1,33 @@
+package luan.lib.webserver.handlers;
+
+import luan.lib.webserver.Handler;
+import luan.lib.webserver.Request;
+import luan.lib.webserver.Response;
+
+
+public final class IndexHandler implements Handler {
+	private final Handler handler;
+	private final String indexName;
+
+	public IndexHandler(Handler handler) {
+		this(handler,"index.html");
+	}
+
+	public IndexHandler(Handler handler,String indexName) {
+		this.handler = handler;
+		this.indexName = indexName;
+	}
+
+	public Response handle(Request request) {
+		if( request.path.endsWith("/") ) {
+			String path = request.path;
+			try {
+				request.path += indexName;
+				return handler.handle(request);
+			} finally {
+				request.path = path;
+			}
+		} else
+			return handler.handle(request);
+	}
+}
diff -r efd1c6380f2c -r 643cf1c37723 src/luan/lib/webserver/handlers/ListHandler.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/lib/webserver/handlers/ListHandler.java	Mon Feb 25 13:02:33 2019 -0700
@@ -0,0 +1,23 @@
+package luan.lib.webserver.handlers;
+
+import luan.lib.webserver.Handler;
+import luan.lib.webserver.Request;
+import luan.lib.webserver.Response;
+
+
+public final class ListHandler implements Handler {
+	private final Handler[] handlers;
+
+	public ListHandler(Handler... handlers) {
+		this.handlers = handlers;
+	}
+
+	public Response handle(Request request) {
+		for( Handler handler : handlers ) {
+			Response response = handler.handle(request);
+			if( response != null )
+				return response;
+		}
+		return null;
+	}
+}
diff -r efd1c6380f2c -r 643cf1c37723 src/luan/lib/webserver/handlers/LogHandler.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/lib/webserver/handlers/LogHandler.java	Mon Feb 25 13:02:33 2019 -0700
@@ -0,0 +1,24 @@
+package luan.lib.webserver.handlers;
+
+import luan.lib.logging.Logger;
+import luan.lib.logging.LoggerFactory;
+import luan.lib.webserver.Handler;
+import luan.lib.webserver.Request;
+import luan.lib.webserver.Response;
+
+
+public final class LogHandler implements Handler {
+	private static final Logger logger = LoggerFactory.getLogger("HTTP");
+
+	private final Handler handler;
+
+	public LogHandler(Handler handler) {
+		this.handler = handler;
+	}
+
+	public Response handle(Request request) {
+		Response response = handler.handle(request);
+		logger.info( request.method + " " + request.path + " " + response.status.code + " " + response.body.length );
+		return response;
+	}
+}
diff -r efd1c6380f2c -r 643cf1c37723 src/luan/lib/webserver/handlers/MapHandler.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/lib/webserver/handlers/MapHandler.java	Mon Feb 25 13:02:33 2019 -0700
@@ -0,0 +1,20 @@
+package luan.lib.webserver.handlers;
+
+import java.util.Map;
+import luan.lib.webserver.Handler;
+import luan.lib.webserver.Request;
+import luan.lib.webserver.Response;
+
+
+public final class MapHandler implements Handler {
+	private final Map<String,Handler> map;
+
+	public MapHandler(Map<String,Handler> map) {
+		this.map = map;
+	}
+
+	public Response handle(Request request) {
+		Handler handler = map.get(request.path);
+		return handler==null ? null : handler.handle(request);
+	}
+}
diff -r efd1c6380f2c -r 643cf1c37723 src/luan/lib/webserver/handlers/SafeHandler.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/lib/webserver/handlers/SafeHandler.java	Mon Feb 25 13:02:33 2019 -0700
@@ -0,0 +1,44 @@
+package luan.lib.webserver.handlers;
+
+import java.io.Writer;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.io.IOException;
+import luan.lib.logging.Logger;
+import luan.lib.logging.LoggerFactory;
+import luan.lib.webserver.Handler;
+import luan.lib.webserver.Request;
+import luan.lib.webserver.Response;
+import luan.lib.webserver.ResponseOutputStream;
+import luan.lib.webserver.Status;
+
+
+public final class SafeHandler implements Handler {
+	private static final Logger logger = LoggerFactory.getLogger(SafeHandler.class);
+
+	private final Handler handler;
+
+	public SafeHandler(Handler handler) {
+		this.handler = handler;
+	}
+
+	public Response handle(Request request) {
+		try {
+			Response response = handler.handle(request);
+			if( response != null )
+				return response;
+		} catch(RuntimeException e) {
+			logger.error("",e);
+			Response response = new Response();
+			response.status = Status.INTERNAL_SERVER_ERROR;
+			response.headers.put( "content-type", "text/plain; charset=utf-8" );
+			PrintWriter writer = new PrintWriter( new ResponseOutputStream(response) );
+			writer.write( "Internel Server Error\n\n" );
+			e.printStackTrace(writer);
+			writer.close();
+			return response;
+		}
+		return Response.errorResponse( Status.NOT_FOUND, request.path+" not found\n" );
+	}
+
+}
diff -r efd1c6380f2c -r 643cf1c37723 src/luan/modules/Boot.luan
--- a/src/luan/modules/Boot.luan	Mon Feb 25 12:29:33 2019 -0700
+++ b/src/luan/modules/Boot.luan	Mon Feb 25 13:02:33 2019 -0700
@@ -218,7 +218,7 @@
 	this.get_message = ex.getMessage
 	this.throw = ex.throwThis
 	this.get_stack_trace_string = ex.getLuanStackTraceString
-	this.get_java_stack_trace_string = ex.getLuanStackTraceString
+	this.get_java_stack_trace_string = ex.getJavaStackTraceString
 	return this
 end
 
diff -r efd1c6380f2c -r 643cf1c37723 src/luan/modules/http/Http.luan
--- a/src/luan/modules/http/Http.luan	Mon Feb 25 12:29:33 2019 -0700
+++ b/src/luan/modules/http/Http.luan	Mon Feb 25 13:02:33 2019 -0700
@@ -16,10 +16,10 @@
 local trim = String.trim or error()
 local Boot = require "luan:Boot.luan"
 local LuanJava = require "java:luan.Luan"
-local Request = require "java:luan.webserver.Request"
-local Response = require "java:luan.webserver.Response"
-local ResponseOutputStream = require "java:luan.webserver.ResponseOutputStream"
-local Status = require "java:luan.webserver.Status"
+local Request = require "java:luan.lib.webserver.Request"
+local Response = require "java:luan.lib.webserver.Response"
+local ResponseOutputStream = require "java:luan.lib.webserver.ResponseOutputStream"
+local Status = require "java:luan.lib.webserver.Status"
 local OutputStreamWriter = require "java:java.io.OutputStreamWriter"
 local HashMap = require "java:java.util.HashMap"
 local Logging = require "luan:logging/Logging.luan"
diff -r efd1c6380f2c -r 643cf1c37723 src/luan/modules/http/LuanDomainHandler.java
--- a/src/luan/modules/http/LuanDomainHandler.java	Mon Feb 25 12:29:33 2019 -0700
+++ b/src/luan/modules/http/LuanDomainHandler.java	Mon Feb 25 13:02:33 2019 -0700
@@ -1,9 +1,9 @@
 package luan.modules.http;
 
-import luan.webserver.Request;
-import luan.webserver.Response;
-import luan.webserver.Handler;
-import luan.webserver.handlers.DomainHandler;
+import luan.lib.webserver.Request;
+import luan.lib.webserver.Response;
+import luan.lib.webserver.Handler;
+import luan.lib.webserver.handlers.DomainHandler;
 import luan.Luan;
 import luan.LuanTable;
 import luan.LuanCloner;
diff -r efd1c6380f2c -r 643cf1c37723 src/luan/modules/http/LuanHandler.java
--- a/src/luan/modules/http/LuanHandler.java	Mon Feb 25 12:29:33 2019 -0700
+++ b/src/luan/modules/http/LuanHandler.java	Mon Feb 25 13:02:33 2019 -0700
@@ -13,12 +13,12 @@
 import java.util.concurrent.locks.ReadWriteLock;
 import java.util.concurrent.locks.ReentrantReadWriteLock;
 import luan.lib.logging.Logger;
-import luan.webserver.Request;
-import luan.webserver.Response;
-import luan.webserver.Status;
-import luan.webserver.Server;
-import luan.webserver.Handler;
-import luan.webserver.ResponseOutputStream;
+import luan.lib.webserver.Request;
+import luan.lib.webserver.Response;
+import luan.lib.webserver.Status;
+import luan.lib.webserver.Server;
+import luan.lib.webserver.Handler;
+import luan.lib.webserver.ResponseOutputStream;
 import luan.Luan;
 import luan.LuanTable;
 import luan.LuanFunction;
diff -r efd1c6380f2c -r 643cf1c37723 src/luan/modules/http/NotFound.java
--- a/src/luan/modules/http/NotFound.java	Mon Feb 25 12:29:33 2019 -0700
+++ b/src/luan/modules/http/NotFound.java	Mon Feb 25 13:02:33 2019 -0700
@@ -1,8 +1,8 @@
 package luan.modules.http;
 
-import luan.webserver.Request;
-import luan.webserver.Response;
-import luan.webserver.Handler;
+import luan.lib.webserver.Request;
+import luan.lib.webserver.Response;
+import luan.lib.webserver.Handler;
 
 
 public class NotFound implements Handler {
diff -r efd1c6380f2c -r 643cf1c37723 src/luan/modules/http/Server.luan
--- a/src/luan/modules/http/Server.luan	Mon Feb 25 12:29:33 2019 -0700
+++ b/src/luan/modules/http/Server.luan	Mon Feb 25 13:02:33 2019 -0700
@@ -15,14 +15,14 @@
 local logger = Logging.logger "http/Server"
 
 java()
-local JavaServer = require "java:luan.webserver.Server"
-local FileHandler = require "java:luan.webserver.handlers.FileHandler"
-local DirHandler = require "java:luan.webserver.handlers.DirHandler"
-local IndexHandler = require "java:luan.webserver.handlers.IndexHandler"
-local ContentTypeHandler = require "java:luan.webserver.handlers.ContentTypeHandler"
-local SafeHandler = require "java:luan.webserver.handlers.SafeHandler"
-local LogHandler = require "java:luan.webserver.handlers.LogHandler"
-local ListHandler = require "java:luan.webserver.handlers.ListHandler"
+local JavaServer = require "java:luan.lib.webserver.Server"
+local FileHandler = require "java:luan.lib.webserver.handlers.FileHandler"
+local DirHandler = require "java:luan.lib.webserver.handlers.DirHandler"
+local IndexHandler = require "java:luan.lib.webserver.handlers.IndexHandler"
+local ContentTypeHandler = require "java:luan.lib.webserver.handlers.ContentTypeHandler"
+local SafeHandler = require "java:luan.lib.webserver.handlers.SafeHandler"
+local LogHandler = require "java:luan.lib.webserver.handlers.LogHandler"
+local ListHandler = require "java:luan.lib.webserver.handlers.ListHandler"
 local LuanHandler = require "java:luan.modules.http.LuanHandler"
 local System = require "java:java.lang.System"
 
diff -r efd1c6380f2c -r 643cf1c37723 src/luan/modules/lucene/LuceneIndex.java
--- a/src/luan/modules/lucene/LuceneIndex.java	Mon Feb 25 12:29:33 2019 -0700
+++ b/src/luan/modules/lucene/LuceneIndex.java	Mon Feb 25 13:02:33 2019 -0700
@@ -578,7 +578,7 @@
 	}
 
 	private Document toLucene(LuanTable table,LuanTable boosts) throws LuanException {
-		return toLucene(table,boosts);
+		return toLucene(table.iterable(),boosts);
 	}
 
 	private Document toLucene(Iterable<Map.Entry> iterable,LuanTable boosts) throws LuanException {
diff -r efd1c6380f2c -r 643cf1c37723 src/luan/modules/url/MultipartClient.java
--- a/src/luan/modules/url/MultipartClient.java	Mon Feb 25 12:29:33 2019 -0700
+++ b/src/luan/modules/url/MultipartClient.java	Mon Feb 25 13:02:33 2019 -0700
@@ -9,7 +9,7 @@
 import java.util.HashMap;
 import luan.LuanTable;
 import luan.LuanException;
-import luan.webserver.Request;
+import luan.lib.webserver.Request;
 
 
 public final class MultipartClient {
diff -r efd1c6380f2c -r 643cf1c37723 src/luan/webserver/Connection.java
--- a/src/luan/webserver/Connection.java	Mon Feb 25 12:29:33 2019 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,134 +0,0 @@
-package luan.webserver;
-
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.io.IOException;
-import java.net.Socket;
-import luan.lib.logging.Logger;
-import luan.lib.logging.LoggerFactory;
-import luan.lib.parser.ParseException;
-
-
-final class Connection {
-	private static final Logger logger = LoggerFactory.getLogger(Connection.class);
-
-	static void handle(Server server,Socket socket) {
-		new Connection(server,socket).handle();
-	}
-
-	private final Server server;
-	private final Socket socket;
-
-	private Connection(Server server,Socket socket) {
-		this.server = server;
-		this.socket = socket;
-	}
-
-	private void handle() {
-		try {
-			Request request = new Request();
-			Response response;
-			try {
-				{
-					InputStream in = socket.getInputStream();
-					byte[] a = new byte[8192];
-					int endOfHeader;
-					int size = 0;
-					int left = a.length;
-					outer: while(true) {
-						int n = in.read(a,size,left);
-						if( n == -1 ) {
-							if( size == 0 ) {
-								socket.close();
-								return;
-							}
-							throw new IOException("unexpected end of input at "+size);
-						}
-						size += n;
-						for( int i=0; i<=size-4; i++ ) {
-							if( a[i]=='\r' && a[i+1]=='\n' && a[i+2]=='\r' && a[i+3]=='\n' ) {
-								endOfHeader = i + 4;
-								break outer;
-							}
-						}
-						left -= n;
-						if( left == 0 ) {
-							byte[] a2 = new byte[2*a.length];
-							System.arraycopy(a,0,a2,0,size);
-							a = a2;
-							left = a.length - size;
-						}
-					}
-					String rawHead = new String(a,0,endOfHeader);
-					//System.out.println(rawHead);
-					request.rawHead = rawHead;
-					RequestParser parser = new RequestParser(request);
-					parser.parseHead();
-		
-					String lenStr = (String)request.headers.get("content-length");
-					if( lenStr != null ) {
-						int len = Integer.parseInt(lenStr);
-						byte[] body = new byte[len];
-						size -= endOfHeader;
-						System.arraycopy(a,endOfHeader,body,0,size);
-						while( size < len ) {
-							int n = in.read(body,size,len-size);
-							if( n == -1 ) {
-								throw new IOException("unexpected end of input at "+size);
-							}
-							size += n;
-						}
-						request.body = body;
-						//System.out.println(new String(request.body));
-					}
-	
-					String contentType = (String)request.headers.get("content-type");
-					if( contentType != null ) {
-						contentType = contentType.toLowerCase();
-						if( "application/x-www-form-urlencoded".equals(contentType) ) {
-							parser.parseUrlencoded(null);
-						} else if( "application/x-www-form-urlencoded; charset=utf-8".equals(contentType) ) {
-							parser.parseUrlencoded("utf-8");
-						} else if( contentType.startsWith("multipart/form-data;") ) {
-							parser.parseMultipart();
-						} else if( contentType.equals("application/json; charset=utf-8") ) {
-							parser.parseJson();
-						} else {
-							logger.info("unknown request content-type: "+contentType);
-						}
-					}
-
-					String scheme = (String)request.headers.get("x-forwarded-proto");
-					if( scheme != null )
-						request.scheme = scheme;
-				}
-				response = server.handler.handle(request);
-			} catch(ParseException e) {
-				logger.warn("parse error\n"+request.rawHead.trim()+"\n",e);
-				response = Response.errorResponse(Status.BAD_REQUEST,e.toString());
-			}
-			response.headers.put("connection","close");
-			response.headers.put("content-length",Long.toString(response.body.length));
-			byte[] header = response.toHeaderString().getBytes();
-	
-			OutputStream out = socket.getOutputStream();
-			out.write(header);
-			copyAll(response.body.content,out);
-			out.close();
-			socket.close();
-		} catch(IOException e) {
-			logger.info("",e);
-		}
-	}
-
-	private static void copyAll(InputStream in,OutputStream out)
-		throws IOException
-	{
-		byte[] a = new byte[8192];
-		int n;
-		while( (n=in.read(a)) != -1 ) {
-			out.write(a,0,n);
-		}
-	}
-
-}
diff -r efd1c6380f2c -r 643cf1c37723 src/luan/webserver/Handler.java
--- a/src/luan/webserver/Handler.java	Mon Feb 25 12:29:33 2019 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,6 +0,0 @@
-package luan.webserver;
-
-
-public interface Handler {
-	public Response handle(Request request);
-}
diff -r efd1c6380f2c -r 643cf1c37723 src/luan/webserver/Request.java
--- a/src/luan/webserver/Request.java	Mon Feb 25 12:29:33 2019 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,35 +0,0 @@
-package luan.webserver;
-
-import java.util.Map;
-import java.util.LinkedHashMap;
-import java.util.Collections;
-
-
-public class Request {
-	public volatile String rawHead;
-	public volatile String method;
-	public volatile String rawPath;
-	public volatile String path;
-	public volatile String protocol;  // only HTTP/1.1 is accepted
-	public volatile String scheme;
-	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 byte[] body;
-
-	public static final class MultipartFile {
-		public final String filename;
-		public final String contentType;
-		public final Object content;  // byte[] or String
-
-		public MultipartFile(String filename,String contentType,Object content) {
-			this.filename = filename;
-			this.contentType = contentType;
-			this.content = content;
-		}
-
-		public String toString() {
-			return "{filename="+filename+", content="+content+"}";
-		}
-	}
-}
diff -r efd1c6380f2c -r 643cf1c37723 src/luan/webserver/RequestParser.java
--- a/src/luan/webserver/RequestParser.java	Mon Feb 25 12:29:33 2019 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,286 +0,0 @@
-package luan.webserver;
-
-import java.io.UnsupportedEncodingException;
-import java.net.URLDecoder;
-import java.util.List;
-import java.util.ArrayList;
-import luan.lib.logging.Logger;
-import luan.lib.logging.LoggerFactory;
-import luan.lib.parser.Parser;
-import luan.lib.parser.ParseException;
-
-
-final class RequestParser {
-	private static final Logger logger = LoggerFactory.getLogger(RequestParser.class);
-	private final Request request;
-	private Parser parser;
-
-	RequestParser(Request request) {
-		this.request = request;
-	}
-
-	void parseUrlencoded(String charset) throws ParseException, UnsupportedEncodingException {
-		if( request.body == null ) {
-			logger.warn("body is null\n"+request.rawHead);
-			return;
-		}
-		this.parser = new Parser(Util.toString(request.body,charset));
-		parseQuery();
-		require( parser.endOfInput() );
-	}
-
-	void parseHead() throws ParseException {
-		this.parser = new Parser(request.rawHead);
-		parseRequestLine();
-		while( !parser.match("\r\n") ) {
-			parserHeaderField();
-		}
-		parseCookies();
-	}
-
-	private void parseRequestLine() throws ParseException {
-		parseMethod();
-		require( parser.match(' ') );
-		parseRawPath();
-		require( parser.match(' ') );
-		parseProtocol();
-		require( parser.match("\r\n") );
-	}
-
-	private void parseMethod() throws ParseException {
-		int start = parser.currentIndex();
-		if( !methodChar() )
-			throw new ParseException(parser,"no method");
-		while( methodChar() );
-		request.method = parser.textFrom(start);
-	}
-
-	private boolean methodChar() {
-		return parser.inCharRange('A','Z');
-	}
-
-	private void parseRawPath() throws ParseException {
-		int start = parser.currentIndex();
-		parsePath();
-		if( parser.match('?') )
-			parseQuery();
-		request.rawPath = parser.textFrom(start);
-	}
-
-	private void parsePath() throws ParseException {
-		int start = parser.currentIndex();
-		if( !parser.match('/') )
-			throw new ParseException(parser,"bad path");
-		while( parser.noneOf(" ?#") );
-		request.path = urlDecode( parser.textFrom(start) );
-	}
-
-	private void parseQuery() throws ParseException {
-		do {
-			int start = parser.currentIndex();
-			while( queryChar() );
-			String name = urlDecode( parser.textFrom(start) );
-			String value = null;
-			if( parser.match('=') ) {
-				start = parser.currentIndex();
-				while( queryChar() || parser.match('=') );
-				value = urlDecode( parser.textFrom(start) );
-			}
-			if( name.length() > 0 || value != null ) {
-				if( value==null )
-					value = "";
-				Util.add(request.parameters,name,value);
-			}
-		} while( parser.match('&') );
-	}
-
-	private boolean queryChar() {
-		return parser.noneOf("=&# \t\n\f\r\u000b");
-	}
-
-	private void parseProtocol() throws ParseException {
-		int start = parser.currentIndex();
-		if( !(
-			parser.match("HTTP/")
-			&& parser.inCharRange('0','9')
-			&& parser.match('.')
-			&& parser.inCharRange('0','9')
-		) )
-			throw new ParseException(parser,"bad protocol");
-		request.protocol = parser.textFrom(start);
-		request.scheme = "http";
-	}
-
-
-	private void parserHeaderField() throws ParseException {
-		String name = parseName();
-		require( parser.match(':') );
-		while( parser.anyOf(" \t") );
-		String value = parseValue();
-		while( parser.anyOf(" \t") );
-		require( parser.match("\r\n") );
-		Util.add(request.headers,name,value);
-	}
-
-	private String parseName() throws ParseException {
-		int start = parser.currentIndex();
-		require( tokenChar() );
-		while( tokenChar() );
-		return parser.textFrom(start).toLowerCase();
-	}
-
-	private String parseValue() throws ParseException {
-		int start = parser.currentIndex();
-		while( !testEndOfValue() )
-			require( parser.anyChar() );
-		return parser.textFrom(start);
-	}
-
-	private boolean testEndOfValue() {
-		parser.begin();
-		while( parser.anyOf(" \t") );
-		boolean b = parser.endOfInput() || parser.anyOf("\r\n");
-		parser.failure();  // rollback
-		return b;
-	}
-
-	private void require(boolean b) throws ParseException {
-		if( !b )
-			throw new ParseException(parser,"failed");
-	}
-
-	boolean tokenChar() {
-		if( parser.endOfInput() )
-			return false;
-		char c = parser.currentChar();
-		if( 32 <= c && c <= 126 && "()<>@,;:\\\"/[]?={} \t\r\n".indexOf(c) == -1 ) {
-			parser.anyChar();
-			return true;
-		} else {
-			return false;
-		}
-	}
-
-
-	private void parseCookies() throws ParseException {
-		String text = (String)request.headers.get("cookie");
-		if( text == null )
-			return;
-		this.parser = new Parser(text);
-		while(true) {
-			int start = parser.currentIndex();
-			while( parser.noneOf("=;") );
-			String name = urlDecode( parser.textFrom(start) );
-			if( parser.match('=') ) {
-				start = parser.currentIndex();
-				while( parser.noneOf(";") );
-				String value = parser.textFrom(start);
-				int len = value.length();
-				if( value.charAt(0)=='"' && value.charAt(len-1)=='"' )
-					value = value.substring(1,len-1);
-				value = urlDecode(value);
-				request.cookies.put(name,value);
-			}
-			if( parser.endOfInput() )
-				return;
-			require( parser.match(';') );
-			parser.match(' ');  // optional for bad browsers
-		}
-	}
-
-
-	private static final String contentTypeStart = "multipart/form-data; boundary=";
-
-	void parseMultipart() throws ParseException, UnsupportedEncodingException {
-		if( request.body == null ) {
-			logger.warn("body is null\n"+request.rawHead);
-			return;
-		}
-		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,null));
-//System.out.println(this.parser.text);
-		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: ") );
-				int start = parser.currentIndex();
-				if( parser.match("application/") ) {
-					isBinary = true;
-				} else if( parser.match("image/") ) {
-					isBinary = true;
-				} else if( parser.match("text/") ) {
-					isBinary = false;
-				} else
-					throw new ParseException(parser,"bad file content-type");
-				while( parser.inCharRange('a','z') || parser.anyOf("-.") );
-				contentType = parser.textFrom(start);
-			}
-			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,contentType,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();
-	}
-
-	private String urlDecode(String s) throws ParseException {
-		try {
-			return URLDecoder.decode(s,"UTF-8");
-		} catch(UnsupportedEncodingException e) {
-			parser.rollback();
-			throw new ParseException(parser,e);
-		} catch(IllegalArgumentException e) {
-			parser.rollback();
-			throw new ParseException(parser,e);
-		}
-	}
-
-	// improve later
-	void parseJson() throws UnsupportedEncodingException {
-		if( request.body == null ) {
-			logger.warn("body is null\n"+request.rawHead);
-			return;
-		}
-		String contentType = (String)request.headers.get("content-type");
-		if( !contentType.equals("application/json; charset=utf-8") )
-			throw new RuntimeException(contentType);
-		String value = new String(request.body,"utf-8");
-		Util.add(request.parameters,"json",value);
-	}
-
-}
diff -r efd1c6380f2c -r 643cf1c37723 src/luan/webserver/Response.java
--- a/src/luan/webserver/Response.java	Mon Feb 25 12:29:33 2019 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,85 +0,0 @@
-package luan.webserver;
-
-import java.io.InputStream;
-import java.io.PrintWriter;
-import java.util.Map;
-import java.util.LinkedHashMap;
-import java.util.Collections;
-import java.util.List;
-
-
-public class Response {
-	public final String protocol = "HTTP/1.1";
-	public volatile Status status = Status.OK;
-	public final Map<String,Object> headers = Collections.synchronizedMap(new LinkedHashMap<String,Object>());
-	{
-		headers.put("server","Luan");
-	}
-	private static final Body empty = new Body(0,new InputStream(){
-		public int read() { return -1; }
-	});
-	public volatile Body body = empty;
-
-	public static class Body {
-		public final long length;
-		public final InputStream content;
-	
-		public Body(long length,InputStream content) {
-			this.length = length;
-			this.content = content;
-		}
-	}
-
-
-	public void addHeader(String name,String value) {
-		Util.add(headers,name,value);
-	}
-
-	public void setCookie(String name,String value,Map<String,String> attributes) {
-		StringBuilder buf = new StringBuilder();
-		buf.append( Util.urlEncode(name) );
-		buf.append( '=' );
-		buf.append( Util.urlEncode(value) );
-		for( Map.Entry<String,String> entry : attributes.entrySet() ) {
-			buf.append( "; " );
-			buf.append( entry.getKey() );
-			buf.append( '=' );
-			buf.append( entry.getValue() );
-		}
-		addHeader( "Set-Cookie", buf.toString() );
-	}
-
-
-	public String toHeaderString() {
-		StringBuilder sb = new StringBuilder();
-		sb.append( protocol )
-			.append( ' ' ).append( status.code )
-			.append( ' ' ).append( status.reason )
-			.append( "\r\n" )
-		;
-		for( Map.Entry<String,Object> entry : headers.entrySet() ) {
-			String name = entry.getKey();
-			Object value = entry.getValue();
-			if( value instanceof List ) {
-				for( Object v : (List)value ) {
-					sb.append( name ).append( ": " ).append( v ).append( "\r\n" );
-				}
-			} else {
-				sb.append( name ).append( ": " ).append( value ).append( "\r\n" );
-			}
-		}
-		sb.append( "\r\n" );
-		return sb.toString();
-	}
-
-
-	public static Response errorResponse(Status status,String text) {
-		Response response = new Response();
-		response.status = status;
-		response.headers.put( "content-type", "text/plain; charset=utf-8" );
-		PrintWriter writer = new PrintWriter( new ResponseOutputStream(response) );
-		writer.write( text );
-		writer.close();
-		return response;
-	}
-}
diff -r efd1c6380f2c -r 643cf1c37723 src/luan/webserver/ResponseOutputStream.java
--- a/src/luan/webserver/ResponseOutputStream.java	Mon Feb 25 12:29:33 2019 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,22 +0,0 @@
-package luan.webserver;
-
-import java.io.ByteArrayOutputStream;
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-
-
-// plenty of room for improvement
-public class ResponseOutputStream extends ByteArrayOutputStream {
-	private final Response response;
-
-	public ResponseOutputStream(Response response) {
-		if(response==null) throw new NullPointerException();
-		this.response = response;
-	}
-
-	@Override public void close() throws IOException {
-		super.close();
-		int size = size();
-		response.body = new Response.Body( size, new ByteArrayInputStream(buf,0,size) );
-	}
-}
diff -r efd1c6380f2c -r 643cf1c37723 src/luan/webserver/Server.java
--- a/src/luan/webserver/Server.java	Mon Feb 25 12:29:33 2019 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,78 +0,0 @@
-package luan.webserver;
-
-import java.io.IOException;
-import java.net.Socket;
-import java.net.ServerSocket;
-import java.net.InetAddress;
-import java.net.UnknownHostException;
-import java.util.concurrent.ThreadPoolExecutor;
-import java.util.concurrent.Executors;
-import java.util.concurrent.TimeUnit;
-import luan.lib.logging.Logger;
-import luan.lib.logging.LoggerFactory;
-
-
-public class Server {
-	private static final Logger logger = LoggerFactory.getLogger(Server.class);
-
-	public final int port;
-	public final Handler handler;
-	public static final ThreadPoolExecutor threadPool = (ThreadPoolExecutor)Executors.newCachedThreadPool();
-
-	public Server(int port,Handler handler) {
-		this.port = port;
-		this.handler = handler;
-	}
-
-	protected ServerSocket newServerSocket() throws IOException {
-		return new ServerSocket(port);
-	}
-
-	public synchronized void start() throws IOException {
-		final ServerSocket ss = newServerSocket();
-		threadPool.execute(new Runnable(){public void run() {
-			try {
-				while(!threadPool.isShutdown()) {
-					final Socket socket = ss.accept();
-					threadPool.execute(new Runnable(){public void run() {
-						Connection.handle(Server.this,socket);
-					}});
-				}
-			} catch(IOException e) {
-				logger.error("",e);
-			}
-		}});
-		logger.info("started server on port "+port);
-	}
-
-	public synchronized boolean stop(long timeoutSeconds) {
-		try {
-			threadPool.shutdownNow();
-			boolean stopped = threadPool.awaitTermination(timeoutSeconds,TimeUnit.SECONDS);
-			if(stopped)
-				logger.info("stopped server on port "+port);
-			else
-				logger.warn("couldn't stop server on port "+port);
-			return stopped;
-		} catch(InterruptedException e) {
-			throw new RuntimeException(e);
-		}
-	}
-
-	public static class ForAddress extends Server {
-		private final InetAddress addr;
-
-		public ForAddress(InetAddress addr,int port,Handler handler) {
-			super(port,handler);
-			this.addr = addr;
-		}
-
-		public ForAddress(String addrName,int port,Handler handler) throws UnknownHostException {
-			this(InetAddress.getByName(addrName),port,handler);
-		}
-
-		protected ServerSocket newServerSocket() throws IOException {
-			return new ServerSocket(port,0,addr);
-		}
-	}
-}
diff -r efd1c6380f2c -r 643cf1c37723 src/luan/webserver/Status.java
--- a/src/luan/webserver/Status.java	Mon Feb 25 12:29:33 2019 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,43 +0,0 @@
-package luan.webserver;
-
-import java.util.Map;
-import java.util.HashMap;
-import luan.lib.logging.Logger;
-import luan.lib.logging.LoggerFactory;
-
-
-public class Status {
-	private static final Logger logger = LoggerFactory.getLogger(Status.class);
-
-	public final int code;
-	public final String reason;
-
-	public Status(int code,String reason) {
-		this.code = code;
-		this.reason = reason;
-	}
-
-	private static final Map<Integer,Status> map = new HashMap<Integer,Status>();
-
-	protected static Status newStatus(int code,String reason) {
-		Status status = new Status(code,reason);
-		map.put(code,status);
-		return status;
-	}
-
-	public static Status getStatus(int code) {
-		Status status = map.get(code);
-		if( status == null ) {
-			logger.warn("missing status "+code);
-			status = new Status(code,"");
-		}
-		return status;
-	}
-
-	public static final Status OK = newStatus(200,"OK");
-	public static final Status MOVED_PERMANENTLY = newStatus(301,"Moved Permanently");
-	public static final Status FOUND = newStatus(302,"Found");
-	public static final Status BAD_REQUEST = newStatus(400,"Bad Request");
-	public static final Status NOT_FOUND = newStatus(404,"Not Found");
-	public static final Status INTERNAL_SERVER_ERROR = newStatus(500,"Internal Server Error");
-}
diff -r efd1c6380f2c -r 643cf1c37723 src/luan/webserver/Util.java
--- a/src/luan/webserver/Util.java	Mon Feb 25 12:29:33 2019 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,55 +0,0 @@
-package luan.webserver;
-
-import java.io.UnsupportedEncodingException;
-import java.net.URLEncoder;
-import java.util.Map;
-import java.util.List;
-import java.util.ArrayList;
-
-
-final class Util {
-
-	static String urlEncode(String s) {
-		try {
-			return URLEncoder.encode(s,"UTF-8");
-		} catch(UnsupportedEncodingException e) {
-			throw new RuntimeException(e);
-		}
-	}
-
-	static void add(Map<String,Object> map,String name,Object value) {
-		Object current = map.get(name);
-		if( current == null ) {
-			map.put(name,value);
-		} else if( current instanceof List ) {
-			List list = (List)current;
-			list.add(value);
-		} else {
-			List list = new ArrayList();
-			list.add(current);
-			list.add(value);
-			map.put(name,list);
-		}
-	}
-
-	static String toString(byte[] a,String charset) throws UnsupportedEncodingException {
-		if( charset != null )
-			return new String(a,charset);
-		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
-}
diff -r efd1c6380f2c -r 643cf1c37723 src/luan/webserver/examples/Cookies.java
--- a/src/luan/webserver/examples/Cookies.java	Mon Feb 25 12:29:33 2019 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,42 +0,0 @@
-package luan.webserver.examples;
-
-import java.io.Writer;
-import java.io.OutputStreamWriter;
-import java.io.IOException;
-import java.util.Map;
-import java.util.HashMap;
-import luan.webserver.Handler;
-import luan.webserver.Request;
-import luan.webserver.Response;
-import luan.webserver.ResponseOutputStream;
-
-
-public final class Cookies implements Handler {
-
-	public Response handle(Request request) {
-		Response response = new Response();
-		String name = (String)request.parameters.get("name");
-		if( name != null ) {
-			Map<String,String> attributes = new HashMap<String,String>();
-			String value = (String)request.parameters.get("value");
-			if( value != null ) {
-				response.setCookie(name,value,attributes);
-			} else {
-				attributes.put("Max-Age","0");
-				response.setCookie(name,"delete",attributes);
-			}
-		}
-		response.headers.put( "content-type", "text/plain; charset=utf-8" );
-		try {
-			Writer writer = new OutputStreamWriter( new ResponseOutputStream(response) );
-			for( Map.Entry<String,String> entry : request.cookies.entrySet() ) {
-				writer.write(entry.getKey()+" = "+entry.getValue()+"\n");
-			}
-			writer.close();
-		} catch(IOException e) {
-			throw new RuntimeException(e);
-		}
-		return response;
-	}
-
-}
diff -r efd1c6380f2c -r 643cf1c37723 src/luan/webserver/examples/Example.java
--- a/src/luan/webserver/examples/Example.java	Mon Feb 25 12:29:33 2019 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,72 +0,0 @@
-package luan.webserver.examples;
-
-import java.io.Writer;
-import java.io.OutputStreamWriter;
-import java.io.IOException;
-import java.util.Map;
-import java.util.HashMap;
-import org.apache.log4j.EnhancedPatternLayout;
-import org.apache.log4j.ConsoleAppender;
-import org.apache.log4j.Logger;
-import luan.webserver.Handler;
-import luan.webserver.Request;
-import luan.webserver.Response;
-import luan.webserver.ResponseOutputStream;
-import luan.webserver.Server;
-import luan.webserver.handlers.MapHandler;
-import luan.webserver.handlers.SafeHandler;
-import luan.webserver.handlers.LogHandler;
-import luan.webserver.handlers.FileHandler;
-import luan.webserver.handlers.DirHandler;
-import luan.webserver.handlers.ListHandler;
-import luan.webserver.handlers.ContentTypeHandler;
-
-
-public class Example implements Handler {
-
-	public Response handle(Request request) {
-		Response response = new Response();
-		response.headers.put( "content-type", "text/plain; charset=utf-8" );
-		try {
-			Writer writer = new OutputStreamWriter( new ResponseOutputStream(response) );
-			writer.write("Hello World\n");
-			writer.close();
-		} catch(IOException e) {
-			throw new RuntimeException("shouldn't happen",e);
-		}
-		return response;
-	}
-
-	public static void simple() throws IOException {
-		Handler handler = new Example();
-		new Server(8080,handler).start();
-	}
-
-	public static void fancy() throws IOException {
-		Map<String,Handler> map = new HashMap<String,Handler>();
-		map.put( "/hello", new Example() );
-		map.put( "/headers", new Headers() );
-		map.put( "/params", new Params() );
-		map.put( "/cookies", new Cookies() );
-		Handler mapHandler = new MapHandler(map);
-		FileHandler fileHandler = new FileHandler();
-		Handler dirHandler = new DirHandler(fileHandler);
-		Handler handler = new ListHandler( mapHandler, fileHandler, dirHandler );
-		handler = new ContentTypeHandler(handler);
-		handler = new SafeHandler(handler);
-		handler = new LogHandler(handler);
-		new Server(8080,handler).start();
-	}
-
-	public static void initLogging() {
-//		Logger.getRootLogger().setLevel(Level.INFO);
-		EnhancedPatternLayout layout = new EnhancedPatternLayout("%d{HH:mm:ss} %-5p %c - %m%n");
-		ConsoleAppender appender = new ConsoleAppender(layout,"System.err");
-		Logger.getRootLogger().addAppender(appender);
-	}
-
-	public static void main(String[] args) throws Exception {
-		initLogging();
-		fancy();
-	}
-}
diff -r efd1c6380f2c -r 643cf1c37723 src/luan/webserver/examples/Headers.java
--- a/src/luan/webserver/examples/Headers.java	Mon Feb 25 12:29:33 2019 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,30 +0,0 @@
-package luan.webserver.examples;
-
-import java.io.Writer;
-import java.io.OutputStreamWriter;
-import java.io.IOException;
-import java.util.Map;
-import luan.webserver.Handler;
-import luan.webserver.Request;
-import luan.webserver.Response;
-import luan.webserver.ResponseOutputStream;
-
-
-public final class Headers implements Handler {
-
-	public Response handle(Request request) {
-		Response response = new Response();
-		response.headers.put( "content-type", "text/plain; charset=utf-8" );
-		try {
-			Writer writer = new OutputStreamWriter( new ResponseOutputStream(response) );
-			for( Map.Entry<String,Object> entry : request.headers.entrySet() ) {
-				writer.write(entry.getKey()+": "+entry.getValue()+"\n");
-			}
-			writer.close();
-		} catch(IOException e) {
-			throw new RuntimeException(e);
-		}
-		return response;
-	}
-
-}
diff -r efd1c6380f2c -r 643cf1c37723 src/luan/webserver/examples/Params.java
--- a/src/luan/webserver/examples/Params.java	Mon Feb 25 12:29:33 2019 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,30 +0,0 @@
-package luan.webserver.examples;
-
-import java.io.Writer;
-import java.io.OutputStreamWriter;
-import java.io.IOException;
-import java.util.Map;
-import luan.webserver.Handler;
-import luan.webserver.Request;
-import luan.webserver.Response;
-import luan.webserver.ResponseOutputStream;
-
-
-public final class Params implements Handler {
-
-	public Response handle(Request request) {
-		Response response = new Response();
-		response.headers.put( "content-type", "text/plain; charset=utf-8" );
-		try {
-			Writer writer = new OutputStreamWriter( new ResponseOutputStream(response) );
-			for( Map.Entry<String,Object> entry : request.parameters.entrySet() ) {
-				writer.write(entry.getKey()+" = "+entry.getValue()+"\n");
-			}
-			writer.close();
-		} catch(IOException e) {
-			throw new RuntimeException(e);
-		}
-		return response;
-	}
-
-}
diff -r efd1c6380f2c -r 643cf1c37723 src/luan/webserver/examples/post.html
--- a/src/luan/webserver/examples/post.html	Mon Feb 25 12:29:33 2019 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,10 +0,0 @@
-<!doctype html>
-<html>
-	<body>
-		<form action=/params method=post>
-			<p>a <input name=a></p>
-			<p>b <input name=b></p>
-			<p><input type=submit></p>
-		</form>
-	</body>
-</html>
diff -r efd1c6380f2c -r 643cf1c37723 src/luan/webserver/examples/post_multipart.html
--- a/src/luan/webserver/examples/post_multipart.html	Mon Feb 25 12:29:33 2019 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,11 +0,0 @@
-<!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>
diff -r efd1c6380f2c -r 643cf1c37723 src/luan/webserver/handlers/ContentTypeHandler.java
--- a/src/luan/webserver/handlers/ContentTypeHandler.java	Mon Feb 25 12:29:33 2019 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,54 +0,0 @@
-package luan.webserver.handlers;
-
-import java.util.Map;
-import java.util.HashMap;
-import luan.webserver.Handler;
-import luan.webserver.Request;
-import luan.webserver.Response;
-
-
-public class ContentTypeHandler implements Handler {
-	private final Handler handler;
-
-	// maps extension to content-type
-	// key must be lower case
-	public final Map<String,String> map = new HashMap<String,String>();
-
-	// set to null for none
-	public String contentTypeForNoExtension;
-
-	public ContentTypeHandler(Handler handler) {
-		this(handler,"utf-8");
-	}
-
-	public ContentTypeHandler(Handler handler,String charset) {
-		this.handler = handler;
-		String attrs = charset== null ? "" : "; charset="+charset;
-		String htmlType = "text/html" + attrs;
-		String textType = "text/plain" + attrs;
-		contentTypeForNoExtension = htmlType;
-		map.put( "html", htmlType );
-		map.put( "txt", textType );
-		map.put( "css", "text/css" );
-		// add more as need
-	}
-
-	public Response handle(Request request) {
-		Response response = handler.handle(request);
-		if( response!=null && !response.headers.containsKey("content-type") ) {
-			String path = request.path;
-			int iSlash = path.lastIndexOf('/');
-			int iDot = path.lastIndexOf('.');
-			String type;
-			if( iDot < iSlash ) {  // no extension
-				type = contentTypeForNoExtension;
-			} else {  // extension
-				String extension = path.substring(iDot+1);
-				type = map.get( extension.toLowerCase() );
-			}
-			if( type != null )
-				response.headers.put("content-type",type);
-		}
-		return response;
-	}
-}
diff -r efd1c6380f2c -r 643cf1c37723 src/luan/webserver/handlers/DirHandler.java
--- a/src/luan/webserver/handlers/DirHandler.java	Mon Feb 25 12:29:33 2019 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,68 +0,0 @@
-package luan.webserver.handlers;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.Writer;
-import java.io.OutputStreamWriter;
-import java.io.IOException;
-import java.text.DateFormat;
-import java.text.SimpleDateFormat;
-import java.util.Arrays;
-import java.util.Comparator;
-import java.util.Date;
-import luan.webserver.Handler;
-import luan.webserver.Request;
-import luan.webserver.Response;
-import luan.webserver.ResponseOutputStream;
-
-
-public final class DirHandler implements Handler {
-	private final FileHandler fileHandler;
-
-	public DirHandler(FileHandler fileHandler) {
-		this.fileHandler = fileHandler;
-	}
-
-	private static final Comparator<File> sorter = new Comparator<File>() {
-		public int compare(File f1, File f2) {
-			return f1.getName().compareTo(f2.getName());
-		}
-	};
-
-	public Response handle(Request request) {
-		try {
-			File file = fileHandler.file(request);
-			if( request.path.endsWith("/") && file.isDirectory() ) {
-				DateFormat fmt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss zzz");
-				Response response = new Response();
-				response.headers.put( "content-type", "text/html; charset=utf-8" );
-				Writer writer = new OutputStreamWriter( new ResponseOutputStream(response) );
-				writer.write( "<!doctype html><html>" );
-				writer.write( "<head><style>td{padding: 2px 8px}</style></head>" );
-				writer.write( "<body>" );
-				writer.write( "<h1>Directory: "+request.path+"</h1>" );
-				writer.write( "<table border=0>" );
-				File[] a = file.listFiles();
-				Arrays.sort(a,sorter);
-				for( File child : a ) {
-					String name = child.getName();
-					if( child.isDirectory() )
-						name += '/';
-					writer.write( "<tr>" );
-					writer.write( "<td><a href='"+name+"'>"+name+"</a></td>" );
-					writer.write( "<td>"+child.length()+" bytes</td>" );
-					writer.write( "<td>"+fmt.format(new Date(child.lastModified()))+"</td>" );
-					writer.write( "</tr>" );
-				}
-				writer.write( "</table>" );
-				writer.write( "</body>" );
-				writer.write( "</html>" );
-				writer.close();
-				return response;
-			}
-			return null;
-		} catch(IOException e) {
-			throw new RuntimeException(e);
-		}
-	}
-}
diff -r efd1c6380f2c -r 643cf1c37723 src/luan/webserver/handlers/DomainHandler.java
--- a/src/luan/webserver/handlers/DomainHandler.java	Mon Feb 25 12:29:33 2019 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,116 +0,0 @@
-package luan.webserver.handlers;
-
-import java.io.Closeable;
-import java.io.IOException;
-import java.lang.ref.Reference;
-import java.lang.ref.SoftReference;
-import java.lang.ref.ReferenceQueue;
-import java.util.Map;
-import java.util.HashMap;
-import luan.lib.logging.Logger;
-import luan.lib.logging.LoggerFactory;
-import luan.webserver.Handler;
-import luan.webserver.Request;
-import luan.webserver.Response;
-
-
-public final class DomainHandler implements Handler {
-	private static final Logger logger = LoggerFactory.getLogger(DomainHandler.class);
-
-	public interface Factory {
-		public Handler newHandler(String domain);
-	}
-
-	private static class Ref {
-		private final Handler handler;
-
-		private Ref(Handler handler) {
-			this.handler = handler;
-		}
-	}
-
-	private final ReferenceQueue<Ref> queue = new ReferenceQueue<Ref>();
-
-	private class MyReference extends SoftReference<Ref> {
-		private Handler handler;
-
-		private MyReference(Ref r) {
-			super(r,queue);
-			this.handler = r.handler;
-		}
-	}
-
-	private static void close(Handler handler) {
-		if( handler instanceof Closeable ) {
-			try {
-				((Closeable)handler).close();
-			} catch(IOException e) {
-				logger.error(handler.toString(),e);
-			}
-		}
-	}
-
-	private void sweep() {
-		while(true) {
-			MyReference ref = (MyReference)queue.poll();
-			if( ref == null )
-				return;
-			//logger.info("sweep");
-			close(ref.handler);
-			ref.handler = null;
-		}
-	}
-
-	private final Map<String,MyReference> map = new HashMap<String,MyReference>();
-
-	private final Factory factory;
-
-	public DomainHandler(Factory factory) {
-		this.factory = factory;
-	}
-
-	public Response handle(Request request) {
-		String host = (String)request.headers.get("host");
-		if( host == null )
-			return null;
-		int i = host.indexOf(':');
-		String domain = i == -1 ? host : host.substring(0,i);
-		Handler handler = getHandler(domain);
-		return handler==null ? null : handler.handle(request);
-	}
-
-	public Handler getHandler(String domain) {
-		Ref r = getRef(domain);
-		return r==null ? null : r.handler;
-	}
-
-	public void removeHandler(String domain) {
-		domain = domain.toLowerCase();
-		synchronized(map) {
-			Reference<Ref> ref = map.remove(domain);
-			Ref r = ref==null ? null : ref.get();
-			if( r != null ) {
-				close(r.handler);
-			}
-		}
-	}
-
-	private Ref getRef(String domain) {
-		domain = domain.toLowerCase();
-		synchronized(map) {
-			Reference<Ref> ref = map.get(domain);
-			Ref r = ref==null ? null : ref.get();
-			if( r == null ) {
-				//if(ref!=null) logger.info("gc "+domain);
-				sweep();
-				Handler handler = factory.newHandler(domain);
-				if( handler == null )
-					return null;
-				r = new Ref(handler);
-				map.put(domain,new MyReference(r));
-			}
-			return r;
-		}
-	}
-
-}
diff -r efd1c6380f2c -r 643cf1c37723 src/luan/webserver/handlers/FileHandler.java
--- a/src/luan/webserver/handlers/FileHandler.java	Mon Feb 25 12:29:33 2019 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,51 +0,0 @@
-package luan.webserver.handlers;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.Writer;
-import java.io.OutputStreamWriter;
-import java.io.IOException;
-import java.text.DateFormat;
-import java.text.SimpleDateFormat;
-import java.util.Date;
-import luan.webserver.Handler;
-import luan.webserver.Request;
-import luan.webserver.Response;
-import luan.webserver.ResponseOutputStream;
-
-
-public class FileHandler implements Handler {
-	final File dir;
-
-	public FileHandler() {
-		this(".");
-	}
-
-	public FileHandler(String pathname) {
-		this(new File(pathname));
-	}
-
-	public FileHandler(File dir) {
-		if( !dir.isDirectory() )
-			throw new RuntimeException("must be a directory");
-		this.dir = dir;
-	}
-
-	File file(Request request) {
-		return new File(dir,request.path);
-	}
-
-	public Response handle(Request request) {
-		try {
-			File file = file(request);
-			if( file.isFile() ) {
-				Response response = new Response();
-				response.body = new Response.Body( file.length(), new FileInputStream(file) );
-				return response;
-			}
-			return null;
-		} catch(IOException e) {
-			throw new RuntimeException(e);
-		}
-	}
-}
diff -r efd1c6380f2c -r 643cf1c37723 src/luan/webserver/handlers/IndexHandler.java
--- a/src/luan/webserver/handlers/IndexHandler.java	Mon Feb 25 12:29:33 2019 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,33 +0,0 @@
-package luan.webserver.handlers;
-
-import luan.webserver.Handler;
-import luan.webserver.Request;
-import luan.webserver.Response;
-
-
-public final class IndexHandler implements Handler {
-	private final Handler handler;
-	private final String indexName;
-
-	public IndexHandler(Handler handler) {
-		this(handler,"index.html");
-	}
-
-	public IndexHandler(Handler handler,String indexName) {
-		this.handler = handler;
-		this.indexName = indexName;
-	}
-
-	public Response handle(Request request) {
-		if( request.path.endsWith("/") ) {
-			String path = request.path;
-			try {
-				request.path += indexName;
-				return handler.handle(request);
-			} finally {
-				request.path = path;
-			}
-		} else
-			return handler.handle(request);
-	}
-}
diff -r efd1c6380f2c -r 643cf1c37723 src/luan/webserver/handlers/ListHandler.java
--- a/src/luan/webserver/handlers/ListHandler.java	Mon Feb 25 12:29:33 2019 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-package luan.webserver.handlers;
-
-import luan.webserver.Handler;
-import luan.webserver.Request;
-import luan.webserver.Response;
-
-
-public final class ListHandler implements Handler {
-	private final Handler[] handlers;
-
-	public ListHandler(Handler... handlers) {
-		this.handlers = handlers;
-	}
-
-	public Response handle(Request request) {
-		for( Handler handler : handlers ) {
-			Response response = handler.handle(request);
-			if( response != null )
-				return response;
-		}
-		return null;
-	}
-}
diff -r efd1c6380f2c -r 643cf1c37723 src/luan/webserver/handlers/LogHandler.java
--- a/src/luan/webserver/handlers/LogHandler.java	Mon Feb 25 12:29:33 2019 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,24 +0,0 @@
-package luan.webserver.handlers;
-
-import luan.lib.logging.Logger;
-import luan.lib.logging.LoggerFactory;
-import luan.webserver.Handler;
-import luan.webserver.Request;
-import luan.webserver.Response;
-
-
-public final class LogHandler implements Handler {
-	private static final Logger logger = LoggerFactory.getLogger("HTTP");
-
-	private final Handler handler;
-
-	public LogHandler(Handler handler) {
-		this.handler = handler;
-	}
-
-	public Response handle(Request request) {
-		Response response = handler.handle(request);
-		logger.info( request.method + " " + request.path + " " + response.status.code + " " + response.body.length );
-		return response;
-	}
-}
diff -r efd1c6380f2c -r 643cf1c37723 src/luan/webserver/handlers/MapHandler.java
--- a/src/luan/webserver/handlers/MapHandler.java	Mon Feb 25 12:29:33 2019 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,20 +0,0 @@
-package luan.webserver.handlers;
-
-import java.util.Map;
-import luan.webserver.Handler;
-import luan.webserver.Request;
-import luan.webserver.Response;
-
-
-public final class MapHandler implements Handler {
-	private final Map<String,Handler> map;
-
-	public MapHandler(Map<String,Handler> map) {
-		this.map = map;
-	}
-
-	public Response handle(Request request) {
-		Handler handler = map.get(request.path);
-		return handler==null ? null : handler.handle(request);
-	}
-}
diff -r efd1c6380f2c -r 643cf1c37723 src/luan/webserver/handlers/SafeHandler.java
--- a/src/luan/webserver/handlers/SafeHandler.java	Mon Feb 25 12:29:33 2019 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,44 +0,0 @@
-package luan.webserver.handlers;
-
-import java.io.Writer;
-import java.io.OutputStreamWriter;
-import java.io.PrintWriter;
-import java.io.IOException;
-import luan.lib.logging.Logger;
-import luan.lib.logging.LoggerFactory;
-import luan.webserver.Handler;
-import luan.webserver.Request;
-import luan.webserver.Response;
-import luan.webserver.ResponseOutputStream;
-import luan.webserver.Status;
-
-
-public final class SafeHandler implements Handler {
-	private static final Logger logger = LoggerFactory.getLogger(SafeHandler.class);
-
-	private final Handler handler;
-
-	public SafeHandler(Handler handler) {
-		this.handler = handler;
-	}
-
-	public Response handle(Request request) {
-		try {
-			Response response = handler.handle(request);
-			if( response != null )
-				return response;
-		} catch(RuntimeException e) {
-			logger.error("",e);
-			Response response = new Response();
-			response.status = Status.INTERNAL_SERVER_ERROR;
-			response.headers.put( "content-type", "text/plain; charset=utf-8" );
-			PrintWriter writer = new PrintWriter( new ResponseOutputStream(response) );
-			writer.write( "Internel Server Error\n\n" );
-			e.printStackTrace(writer);
-			writer.close();
-			return response;
-		}
-		return Response.errorResponse( Status.NOT_FOUND, request.path+" not found\n" );
-	}
-
-}