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;
+	}
+
+}