Mercurial Hosting > luan
changeset 1347:643cf1c37723
move webserver to lib and bug fixes
line wrap: on
line diff
--- 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
--- 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(); }
--- 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;
--- 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
--- /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); + } + } + +}
--- /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); +}
--- /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+"}"; + } + } +}
--- /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); + } + +}
--- /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; + } +}
--- /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) ); + } +}
--- /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); + } + } +}
--- /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"); +}
--- /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 +}
--- /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; + } + +}
--- /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(); + } +}
--- /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; + } + +}
--- /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; + } + +}
--- /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>
--- /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>
--- /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; + } +}
--- /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); + } + } +}
--- /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; + } + } + +}
--- /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); + } + } +}
--- /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); + } +}
--- /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; + } +}
--- /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; + } +}
--- /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); + } +}
--- /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" ); + } + +}
--- 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
--- 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"
--- 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;
--- 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;
--- 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 {
--- 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"
--- 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 {
--- 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 {
--- 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); - } - } - -}
--- 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); -}
--- 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+"}"; - } - } -}
--- 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); - } - -}
--- 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; - } -}
--- 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) ); - } -}
--- 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); - } - } -}
--- 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"); -}
--- 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 -}
--- 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; - } - -}
--- 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(); - } -}
--- 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; - } - -}
--- 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; - } - -}
--- 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>
--- 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>
--- 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; - } -}
--- 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); - } - } -}
--- 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; - } - } - -}
--- 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); - } - } -}
--- 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); - } -}
--- 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; - } -}
--- 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; - } -}
--- 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); - } -}
--- 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" ); - } - -}