Mercurial Hosting > luan
changeset 1137:c123ee15f99b
add webserver
author | Franklin Schmidt <fschmidt@gmail.com> |
---|---|
date | Mon, 29 Jan 2018 18:49:59 -0700 |
parents | d30d400fd43d |
children | 4189027691b7 |
files | src/luan/webserver/Connection.java src/luan/webserver/Handler.java src/luan/webserver/Request.java src/luan/webserver/RequestHeadParser.java src/luan/webserver/Response.java src/luan/webserver/ResponseOutputStream.java src/luan/webserver/Server.java src/luan/webserver/Status.java src/luan/webserver/examples/Example.java src/luan/webserver/handlers/ContentTypeHandler.java src/luan/webserver/handlers/IndexHandler.java src/luan/webserver/handlers/ListHandler.java src/luan/webserver/handlers/MapHandler.java src/luan/webserver/handlers/SafeHandler.java |
diffstat | 14 files changed, 615 insertions(+), 0 deletions(-) [+] |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/luan/webserver/Connection.java Mon Jan 29 18:49:59 2018 -0700 @@ -0,0 +1,82 @@ +package luan.webserver; + +import java.io.InputStream; +import java.io.OutputStream; +import java.io.IOException; +import java.net.Socket; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import luan.lib.parser.ParseException; + + +final class Connection { + private static final Logger logger = LoggerFactory.getLogger(Connection.class); + + private final Server server; + private final Socket socket; + + Connection(Server server,Socket socket) { + this.server = server; + this.socket = socket; + handle(); + } + + private void handle() { + 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 ) + 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 rawRequest = new String(a,0,endOfHeader); +//System.out.println(rawRequest); + Request request = RequestHeadParser.parse(rawRequest); +//System.out.println(request.headers); + + Response response = server.handler.handle(request); + 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); + } catch(ParseException 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/webserver/Handler.java Mon Jan 29 18:49:59 2018 -0700 @@ -0,0 +1,6 @@ +package luan.webserver; + + +public interface Handler { + public Response handle(Request request); +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/luan/webserver/Request.java Mon Jan 29 18:49:59 2018 -0700 @@ -0,0 +1,15 @@ +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 path; + public volatile String protocol; // only HTTP/1.1 is accepted + public final Map<String,String> headers = Collections.synchronizedMap(new LinkedHashMap<String,String>()); + public final Map<String,Object> parameters = Collections.synchronizedMap(new LinkedHashMap<String,Object>()); +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/luan/webserver/RequestHeadParser.java Mon Jan 29 18:49:59 2018 -0700 @@ -0,0 +1,138 @@ +package luan.webserver; + +import luan.lib.parser.Parser; +import luan.lib.parser.ParseException; + + +final class RequestHeadParser { + + static Request parse(String text) throws ParseException { + RequestHeadParser rhp = new RequestHeadParser(text); + rhp.parse(); + return rhp.request; + } + + private final Request request = new Request(); + private final Parser parser; + + private RequestHeadParser(String text) { + this.parser = new Parser(text); + request.rawHead = text; + } + + private void parse() throws ParseException { + parseRequestLine(); + while( !parser.match("\r\n") ) { + parserHeaderField(); + } + } + + + private void parseRequestLine() throws ParseException { + parseMethod(); + require( parser.match(' ') ); + parsePath(); + 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 parsePath() throws ParseException { + int start = parser.currentIndex(); + if( !parser.match('/') ) + throw new ParseException(parser,"bad path"); + while( + parser.inCharRange('A','Z') + || parser.inCharRange('a','z') + || parser.inCharRange('0','9') + || parser.anyOf("-._~:/?#[]@!$&'()*+,;=`.") + ); + request.path = parser.textFrom(start); + } + + 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); + } + + + 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") ); + request.headers.put(name,value); + } + + private String parseName() throws ParseException { + StringBuilder buf = new StringBuilder(); + boolean cap = true; + require( tokenChar() ); + do { + char c = parser.lastChar(); + if( c == '-' ) { + cap = true; + } else if( cap ) { + c = Character.toUpperCase(c); + cap = false; + } else { + c = Character.toLowerCase(c); + } + buf.append(c); + } while( tokenChar() ); + return buf.toString(); + } + + private String parseValue() { + int start = parser.currentIndex(); + while( !testEndOfValue() ) + 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; + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/luan/webserver/Response.java Mon Jan 29 18:49:59 2018 -0700 @@ -0,0 +1,44 @@ +package luan.webserver; + +import java.io.InputStream; +import java.util.Map; +import java.util.LinkedHashMap; +import java.util.Collections; + + +public class Response { + public final String protocol = "HTTP/1.1"; + public volatile Status status = Status.OK; + public final Map<String,String> headers = Collections.synchronizedMap(new LinkedHashMap<String,String>()); + { + headers.put("Server","ThreeBody"); + } + public volatile Body body; + + 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 String toHeaderString() { + StringBuilder sb = new StringBuilder(); + sb.append( protocol ) + .append( ' ' ).append( status.code ) + .append( ' ' ).append( status.reason ) + .append( "\r\n" ) + ; + for( Map.Entry<String,String> entry : headers.entrySet() ) { + String name = entry.getKey(); + String value = entry.getValue(); + sb.append( name ).append( ": " ).append( value ).append( "\r\n" ); + } + sb.append( "\r\n" ); + return sb.toString(); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/luan/webserver/ResponseOutputStream.java Mon Jan 29 18:49:59 2018 -0700 @@ -0,0 +1,21 @@ +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) { + 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/webserver/Server.java Mon Jan 29 18:49:59 2018 -0700 @@ -0,0 +1,56 @@ +package luan.webserver; + +import java.io.IOException; +import java.net.Socket; +import java.net.ServerSocket; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +public class Server { + private static final Logger logger = LoggerFactory.getLogger(Server.class); + + public final int port; + public final Handler handler; + private volatile boolean isRunning = false; + private Thread thread; + + 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 { + isRunning = true; + final ServerSocket ss = newServerSocket(); + thread = new Thread("threebody.http.Server") { + public void run() { + try { + while(isRunning) { + Socket socket = ss.accept(); + new Connection(Server.this,socket); + } + } catch(IOException e) { + logger.error("",e); + } + } + }; + thread.start(); + logger.info("started server on port "+port); + } + + public synchronized void stop() { + isRunning = false; + try { + thread.join(); + } catch(InterruptedException e) { + throw new RuntimeException(e); + } + logger.info("stopped server on port "+port); + } + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/luan/webserver/Status.java Mon Jan 29 18:49:59 2018 -0700 @@ -0,0 +1,16 @@ +package luan.webserver; + + +public class Status { + public final int code; + public final String reason; + + public Status(int code,String reason) { + this.code = code; + this.reason = reason; + } + + public static final Status OK = new Status(200,"OK"); + public static final Status NOT_FOUND = new Status(404,"Not Found"); + public static final Status INTERNAL_SERVER_ERROR = new Status(500,"Internal Server Error"); +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/luan/webserver/examples/Example.java Mon Jan 29 18:49:59 2018 -0700 @@ -0,0 +1,51 @@ +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.BasicConfigurator; +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; + + +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 { + Handler handler = new Example(); + Map<String,Handler> map = new HashMap<String,Handler>(); + map.put("/hello",handler); + handler = new MapHandler(map); + handler = new SafeHandler(handler); + new Server(8080,handler).start(); + } + + public static void main(String[] args) throws Exception { + BasicConfigurator.configure(); + fancy(); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/luan/webserver/handlers/ContentTypeHandler.java Mon Jan 29 18:49:59 2018 -0700 @@ -0,0 +1,54 @@ +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 { + public final static String CONTENT_TYPE = "Content-Type"; + + 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 htmlType = "text/html; charset=" + charset; + String textType = "text/plain; charset=" + charset; + contentTypeForNoExtension = htmlType; + map.put( "html", htmlType ); + map.put( "txt", textType ); + // 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/webserver/handlers/IndexHandler.java Mon Jan 29 18:49:59 2018 -0700 @@ -0,0 +1,35 @@ +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; + Response response = handler.handle(request); + if( response != null ) + return response; + } finally { + request.path = path; + } + } + return handler.handle(request); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/luan/webserver/handlers/ListHandler.java Mon Jan 29 18:49:59 2018 -0700 @@ -0,0 +1,23 @@ +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; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/luan/webserver/handlers/MapHandler.java Mon Jan 29 18:49:59 2018 -0700 @@ -0,0 +1,20 @@ +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); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/luan/webserver/handlers/SafeHandler.java Mon Jan 29 18:49:59 2018 -0700 @@ -0,0 +1,54 @@ +package luan.webserver.handlers; + +import java.io.Writer; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.IOException; +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 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) { + + 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; + + } + + Response response = new Response(); + response.status = Status.NOT_FOUND; + response.headers.put( "Content-Type", "text/plain; charset=UTF-8" ); + try { + Writer writer = new OutputStreamWriter( new ResponseOutputStream(response) ); + writer.write( request.path+" not found\n" ); + writer.close(); + } catch(IOException e) { + throw new RuntimeException(e); + } + return response; + } + +}