changeset 1160:4beabb087be6

add http/impl
author Franklin Schmidt <fschmidt@gmail.com>
date Mon, 05 Feb 2018 22:33:59 -0700
parents 3ef883468fd0
children 6baccd0c85a7
files src/luan/modules/http/Http_test.luan src/luan/modules/http/Implementation.luan src/luan/modules/http/impl/Http.luan src/luan/modules/http/impl/HttpServicer.java src/luan/modules/http/impl/LuanHandler.java src/luan/modules/http/impl/NotFound.java src/luan/modules/http/impl/Server.luan src/luan/modules/http/jetty/Http.luan src/luan/modules/http/tools/Dump_mod.luan src/luan/webserver/Connection.java src/luan/webserver/Request.java src/luan/webserver/Response.java src/luan/webserver/ResponseOutputStream.java src/luan/webserver/Status.java
diffstat 14 files changed, 511 insertions(+), 15 deletions(-) [+]
line wrap: on
line diff
--- a/src/luan/modules/http/Http_test.luan	Mon Feb 05 12:37:59 2018 -0700
+++ b/src/luan/modules/http/Http_test.luan	Mon Feb 05 22:33:59 2018 -0700
@@ -40,13 +40,16 @@
 end
 
 function Http_test.init()
-	Http.request = Http.new_request{}
+	Http.request = Http.new_request()
 	Http.request.cookies = Http_test.cookies
 
-	Http.response = Http.new_response{
+	Http.response = {
+
+		headers = {}
+
+		status = Http.STATUS.OK
 
 		text_writer = function()
-			Http.sent_headers(Http.response.headers)
 			Http_test.result = Io.uri "string:"
 			Http_test.text_writer = Http_test.result.text_writer()
 			return Http_test.text_writer
--- a/src/luan/modules/http/Implementation.luan	Mon Feb 05 12:37:59 2018 -0700
+++ b/src/luan/modules/http/Implementation.luan	Mon Feb 05 22:33:59 2018 -0700
@@ -1,3 +1,4 @@
 return {
 	luan = "luan:http/jetty/"
+--	luan = "luan:http/impl/"
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/http/impl/Http.luan	Mon Feb 05 22:33:59 2018 -0700
@@ -0,0 +1,121 @@
+java()
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local ipairs = Luan.ipairs or error()
+local pairs = Luan.pairs or error()
+local type = Luan.type or error()
+local Io = require "luan:Io.luan"
+local Html = require "luan:Html.luan"
+local url_encode = Html.url_encode or error()
+local Table = require "luan:Table.luan"
+local clear = Table.clear or error()
+local Package = require "luan:Package.luan"
+local String = require "luan:String.luan"
+local matches = String.matches or error()
+local HttpServicer = require "java:luan.modules.http.impl.HttpServicer"
+local IoLuan = require "java:luan.modules.IoLuan"
+local LuanJava = require "java:luan.Luan"
+local ResponseOutputStream = require "java:luan.webserver.ResponseOutputStream"
+local OutputStreamWriter = require "java:java.io.OutputStreamWriter"
+
+
+local Http = {}
+
+function Http.new_request(java)
+	local this = {}
+	Http.request = this
+	if java == nil then
+		this.port = 80
+		this.method = "GET"
+		this.headers = {}
+		this.parameters = {}
+		this.cookies = {}
+	else
+		this.java = java
+		this.port = java.port or error()
+		this.method = java.method or error()
+		this.raw_path = java.rawPath or error()
+		this.path = java.path or error()
+		this.protocol = java.protocol or error()
+		this.headers = LuanJava.toLuan(java.headers)
+		this.parameters = LuanJava.toLuan(java.parameters)
+		this.cookies = LuanJava.toLuan(java.cookies)
+	end
+	this.scheme = "http"
+
+	function this.full_path()  -- compatible with jetty
+		return this.raw_path or this.path
+	end
+
+	function this.url()
+		return this.scheme.."://"..this.headers["host"]..this.raw_path
+	end
+
+	return this
+end
+
+local STATUS = {
+	OK = 200
+	MOVED_PERMANENTLY = 301
+	FOUND = 302
+	-- add more as needed
+}
+Http.STATUS = STATUS
+
+function Http.new_response(java)
+	java or error()
+	local this = {}
+	Http.response = this
+	this.java = java
+
+	this.headers = {}
+
+	this.status = STATUS.OK
+
+	function this.send_redirect(location)
+		this.status = STATUS.FOUND
+		this.headers["location"] = location
+	end
+
+	function this.set_cookie(name,value,attributes)
+		HttpServicer.setCookie(this.java,name,value,attributes)
+	end
+
+	function this.set_persistent_cookie(name,value,attributes)
+		attributes = attributes or {}
+		attributes["Max-Age"] = "10000000"
+		this.set_cookie(name,value,attributes)
+	end
+
+	function this.remove_cookie(name,attributes)
+		attributes = attributes or {}
+		attributes["Max-Age"] = "0"
+		this.set_cookie(name,"delete",attributes)
+	end
+
+	function this.text_writer()
+		this.writer and error "writer already set"
+		this.writer = ResponseOutputStream.new(this.java)
+		this.writer = OutputStreamWriter.new(this.writer)
+		return IoLuan.textWriter(this.writer)
+	end
+
+	function this.binary_writer()
+		this.writer and error "writer already set"
+		this.writer = ResponseOutputStream.new(this.java)
+		return IoLuan.binaryWriter(this.writer)
+	end
+
+	return this
+end
+
+
+function Http.uncache_site()
+	for k in pairs(Table.copy(Package.loaded)) do
+		if matches(k,"^site:") then
+			Package.loaded[k] = nil
+		end
+	end
+end
+
+return Http
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/http/impl/HttpServicer.java	Mon Feb 05 22:33:59 2018 -0700
@@ -0,0 +1,98 @@
+package luan.modules.http.impl;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.Map;
+import java.util.HashMap;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import luan.webserver.Request;
+import luan.webserver.Response;
+import luan.webserver.Status;
+import luan.Luan;
+import luan.LuanState;
+import luan.LuanFunction;
+import luan.LuanException;
+import luan.LuanTable;
+import luan.LuanCloner;
+import luan.modules.PackageLuan;
+
+
+public final class HttpServicer {
+	private static final Logger logger = LoggerFactory.getLogger(HttpServicer.class);
+
+	public static Response service(LuanState luan,Request request)
+		throws LuanException
+	{
+		if( request.path.endsWith("/") )
+			return null;
+		LuanFunction fn;
+		synchronized(luan) {
+			String modName = "site:" + request.path +".luan";
+			PackageLuan.enableLoad(luan,"luan:http/Http.luan",modName);
+			LuanTable module = (LuanTable)PackageLuan.require(luan,"luan:http/Http.luan");
+			Object mod = PackageLuan.load(luan,modName);
+			if( mod.equals(Boolean.FALSE) )
+				return null;
+			if( !(mod instanceof LuanFunction) )
+				throw new LuanException( "module '"+modName+"' must return a function" );
+			LuanCloner cloner = new LuanCloner(LuanCloner.Type.INCREMENTAL);
+			luan = (LuanState)cloner.clone(luan);
+			fn = (LuanFunction)cloner.get(mod);
+		}
+
+		LuanTable module = (LuanTable)PackageLuan.require(luan,"luan:http/Http.luan");
+
+		// request
+		LuanFunction newRequestFn = (LuanFunction)module.rawGet("new_request");
+		newRequestFn.call( luan, new Object[]{request} );
+
+		// response
+		Response response = new Response();
+		LuanFunction newResponseFn = (LuanFunction)module.rawGet("new_response");
+		LuanTable responseTbl = (LuanTable)newResponseFn.call( luan, new Object[]{response} );
+
+		fn.call(luan);
+
+		response.status = Status.getStatus( Luan.asInteger(responseTbl.rawGet("status")) );
+		LuanTable headersTbl = (LuanTable)responseTbl.rawGet("headers");
+		if( !headersTbl.rawIsEmpty() ) {
+			Map headers = (Map)Luan.toJava(headersTbl);
+			for( Object obj : headers.entrySet() ) {
+				Map.Entry entry = (Map.Entry)obj;
+				String name = (String)entry.getKey();
+				Object value = entry.getValue();
+				response.headers.put(name,value);
+			}
+		}
+		Closeable writer = (Closeable)responseTbl.rawGet("writer");
+		if( writer != null ) {
+			try {
+				((Closeable)writer).close();
+			} catch(IOException e) {
+				throw new RuntimeException(e);
+			}
+		}
+
+		return response;
+	}
+
+	public static void setCookie(LuanState luan,Response response,String name,String value,LuanTable attributesTbl)
+		throws LuanException
+	{
+		Map<String,String> attributes = new HashMap<String,String>();
+		if( attributesTbl != null ) {
+			for( Map.Entry entry : attributesTbl.iterable(luan) ) {
+				String key = (String)entry.getKey();
+				if( !(key instanceof String) )
+					throw new LuanException("cookie attribute name must be string");
+				String val = (String)entry.getValue();
+				if( !(val instanceof String) )
+					throw new LuanException("cookie attribute value must be string");
+				attributes.put(key,val);
+			}
+		}
+		response.setCookie(name,value,attributes);
+	}
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/http/impl/LuanHandler.java	Mon Feb 05 22:33:59 2018 -0700
@@ -0,0 +1,168 @@
+package luan.modules.http.impl;
+
+import java.io.Writer;
+import java.io.PrintWriter;
+import java.io.IOException;
+import java.lang.reflect.Method;
+import java.net.BindException;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import luan.webserver.Request;
+import luan.webserver.Response;
+import luan.webserver.Server;
+import luan.webserver.Handler;
+import luan.webserver.Status;
+import luan.webserver.ResponseOutputStream;
+import luan.Luan;
+import luan.LuanState;
+import luan.LuanTable;
+import luan.LuanFunction;
+import luan.LuanJavaFunction;
+import luan.LuanCloner;
+import luan.LuanException;
+import luan.modules.PackageLuan;
+
+
+public class LuanHandler implements Handler {
+	private final LuanState luanInit;
+	private final Logger logger;
+	private final ReadWriteLock lock = new ReentrantReadWriteLock();
+	private LuanState luan;
+
+	private static final Method resetLuanMethod;
+	static {
+		try {
+			resetLuanMethod = LuanHandler.class.getMethod("reset_luan");
+		} catch(NoSuchMethodException e) {
+			throw new RuntimeException(e);
+		}
+	}
+
+	public LuanHandler(LuanState luan,String loggerRoot) {
+		this.luanInit = luan;
+		if( loggerRoot==null )
+			loggerRoot = "";
+		logger = LoggerFactory.getLogger(loggerRoot+LuanHandler.class.getName());
+		try {
+			LuanTable Http = (LuanTable)PackageLuan.require(luanInit,"luan:http/Http.luan");
+			Http.rawPut( "reset_luan", new LuanJavaFunction(resetLuanMethod,this) );
+		} catch(LuanException e) {
+			throw new RuntimeException(e);
+		}
+		setLuan();
+	}
+
+	@Override public Response handle(Request request) {
+		Thread thread = Thread.currentThread();
+		String oldName = thread.getName();
+		thread.setName(request.headers.get("host")+request.path);
+		lock.readLock().lock();
+		try {
+			Response response = HttpServicer.service(luan,request);
+			return response;
+		} catch(LuanException e) {
+//e.printStackTrace();
+			String err = e.getLuanStackTraceString();
+			logger.error(err);
+			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" );
+			writer.write( err );
+			writer.close();
+			return response;
+		} finally {
+			lock.readLock().unlock();
+			thread.setName(oldName);
+		}
+	}
+/*
+	@Override protected void doStart() throws Exception {
+//		Thread.dumpStack();
+//System.out.println("qqqqqqqqqqqqqqqqqqqq doStart "+this);
+		setLuan();
+		super.doStart();
+	}
+
+	@Override protected void doStop() throws Exception {
+		synchronized(luan) {
+			luan.close();
+		}
+//System.out.println("qqqqqqqqqqqqqqqqqqqq doStop "+this);
+		super.doStop();
+	}
+*/
+	public Object call_rpc(String fnName,Object... args) throws LuanException {
+		lock.readLock().lock();
+		try {
+			LuanFunction fn;
+			LuanState luan = this.luan;
+			synchronized(luan) {
+				PackageLuan.enableLoad(luan,"luan:Rpc.luan");
+				LuanTable rpc = (LuanTable)PackageLuan.require(luan,"luan:Rpc.luan");
+				LuanTable fns = (LuanTable)rpc.get(luan,"functions");
+				fn = (LuanFunction)fns.get(luan,fnName);
+				if( fn == null )
+					throw new LuanException( "function not found: " + fnName );
+				LuanCloner cloner = new LuanCloner(LuanCloner.Type.INCREMENTAL);
+				luan = (LuanState)cloner.clone(luan);
+				fn = (LuanFunction)cloner.get(fn);
+			}
+			return fn.call(luan,args);
+		} finally {
+			lock.readLock().unlock();
+		}
+	}
+
+	public void reset_luan() {
+		new Thread() {
+			public void run() {
+				lock.writeLock().lock();
+				try {
+					synchronized(luan) {
+						luan.close();
+						setLuan();
+					}
+				} catch(IOException e) {
+					logger.error("reset_luan failed",e);
+				} finally {
+					lock.writeLock().unlock();
+				}
+			}
+		}.start();
+	}
+
+	private void setLuan() {
+		LuanCloner cloner = new LuanCloner(LuanCloner.Type.COMPLETE);
+		luan = (LuanState)cloner.clone(luanInit);
+		try {
+			PackageLuan.load(luan,"site:/init.luan");
+		} catch(LuanException e) {
+			String err = e.getLuanStackTraceString();
+			logger.error(err);
+		}
+	}
+
+	public Object runLuan(String sourceText,String sourceName) throws LuanException {
+		LuanFunction fn = Luan.load(sourceText,sourceName);
+		synchronized(luan) {
+			LuanCloner cloner = new LuanCloner(LuanCloner.Type.INCREMENTAL);
+			LuanState luan = (LuanState)cloner.clone(this.luan);
+			return fn.call(luan);
+		}
+	}
+
+	public static void start(Server server) throws Exception {
+		try {
+			server.start();
+		} catch(BindException e) {
+			throw new LuanException(e.toString());
+		}
+	}
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/http/impl/NotFound.java	Mon Feb 05 22:33:59 2018 -0700
@@ -0,0 +1,29 @@
+package luan.modules.http.impl;
+
+import luan.webserver.Request;
+import luan.webserver.Response;
+import luan.webserver.Handler;
+
+
+public class NotFound implements Handler {
+	private final Handler handler;
+
+	public NotFound(Handler handler) {
+		this.handler = handler;
+	}
+
+	@Override public Response handle(Request request) {
+		Response response = handler.handle(request);
+		if( response == null ) {
+			String path = request.path;
+			try {
+				request.path = "/not_found";
+				response = handler.handle(request);
+			} finally {
+				request.path = path;
+			}
+		}
+		return response;
+	}
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/http/impl/Server.luan	Mon Feb 05 22:33:59 2018 -0700
@@ -0,0 +1,69 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local String = require "luan:String.luan"
+local gsub = String.gsub or error()
+local matches = String.matches or error()
+local Io = require "luan:Io.luan"
+local Package = require "luan:Package.luan"
+local Rpc = require "luan:Rpc.luan"
+local Thread = require "luan:Thread.luan"
+--local Http = require "luan:http/Http.luan"
+require "luan:logging/init.luan"  -- initialize logging
+local Logging = require "luan:logging/Logging.luan"
+local logger = Logging.logger "http/Server"
+
+java()
+local JavaServer = require "java:luan.webserver.Server"
+local FileHandler = require "java:luan.webserver.handlers.FileHandler"
+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 LuanHandler = require "java:luan.modules.http.impl.LuanHandler"
+local NotFound = require "java:luan.modules.http.impl.NotFound"
+
+
+local Server = {}
+
+Server.port = 8080
+
+function Server.init(dir)
+	matches(dir,"^file:") or error "server dir must be scheme 'file:'"
+	dir = gsub(dir,"/$","")  -- remove trailing '/' if any
+--	Http.dir = dir
+	function Io.schemes.site(path)
+		return Io.uri( dir..path )
+	end
+	local file_dir = Io.uri(dir).to_string()
+	local handler = FileHandler.new(file_dir)
+	local luan_handler = LuanHandler.new()
+	handler = ListHandler.new( luan_handler, handler )
+	handler = IndexHandler.new(handler)
+	handler = NotFound.new(handler)
+	handler = ContentTypeHandler.new(handler)
+	handler = SafeHandler.new(handler)
+	handler = LogHandler.new(handler)
+--	Server.handlers.addHandler(NotFound.new(Server.luan_handler))
+	Server.server = JavaServer.new(Server.port,handler)
+end
+
+function Server.start()
+	LuanHandler.start(Server.server)
+end
+
+function Server.start_rpc()
+	function Rpc.functions.call(domain,fn_name,...)
+		return Server.luan_handler.call_rpc(fn_name,...)
+	end
+
+	Thread.fork(Rpc.serve)
+end
+
+function Server.serve(dir)
+	Server.init(dir)
+	Server.start_rpc()
+	Server.start()
+end
+
+return Server
--- a/src/luan/modules/http/jetty/Http.luan	Mon Feb 05 12:37:59 2018 -0700
+++ b/src/luan/modules/http/jetty/Http.luan	Mon Feb 05 22:33:59 2018 -0700
@@ -67,15 +67,19 @@
 		return s ~= "" and s or nil
 	end
 
-	function this.url()
-		local url = this.scheme.."://"..this.headers["host"]..this.path
+	function this.full_path()  -- compatible with impl
+		local path = this.path
 		if this.method ~= "POST" then
 			local query = this.query_string()
 			if query ~= nil then
-				url = url.."?"..query
+				path = path.."?"..query
 			end
 		end
-		return url
+		return path
+	end
+
+	function this.url()
+		return this.scheme.."://"..this.headers["host"]..this.full_path()
 	end
 
 	return this
--- a/src/luan/modules/http/tools/Dump_mod.luan	Mon Feb 05 12:37:59 2018 -0700
+++ b/src/luan/modules/http/tools/Dump_mod.luan	Mon Feb 05 22:33:59 2018 -0700
@@ -13,11 +13,7 @@
 	Io.stdout = Http.response.text_writer()
 
 	local method = Http.request.method
-	local path = Http.request.path
-	local query = Http.request.query_string()
-	if method ~= "POST" and query ~= nil then
-		path = path.."?"..query
-	end
+	local path = Http.request.full_path()
 %>
 <%=method%> <%=path%> <%=Http.request.protocol%> 
 <%
@@ -25,9 +21,9 @@
 %>
 
 <%
-	if method == "POST" and query ~= nil then
+	if method == "POST" then
 %>
-<%=query%>
+<%=Io.repr(Http.request.parameters)%>
 <%
 	end
 end
--- a/src/luan/webserver/Connection.java	Mon Feb 05 12:37:59 2018 -0700
+++ b/src/luan/webserver/Connection.java	Mon Feb 05 22:33:59 2018 -0700
@@ -27,6 +27,7 @@
 	private void handle() {
 		try {
 			Request request = new Request();
+			request.port = server.port;
 			{
 				InputStream in = socket.getInputStream();
 				byte[] a = new byte[8192];
--- a/src/luan/webserver/Request.java	Mon Feb 05 12:37:59 2018 -0700
+++ b/src/luan/webserver/Request.java	Mon Feb 05 22:33:59 2018 -0700
@@ -6,6 +6,7 @@
 
 
 public class Request {
+	public volatile int port;
 	public volatile String rawHead;
 	public volatile String method;
 	public volatile String rawPath;
--- a/src/luan/webserver/Response.java	Mon Feb 05 12:37:59 2018 -0700
+++ b/src/luan/webserver/Response.java	Mon Feb 05 22:33:59 2018 -0700
@@ -14,7 +14,10 @@
 	{
 		headers.put("server","Luan");
 	}
-	public volatile Body body;
+	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;
--- a/src/luan/webserver/ResponseOutputStream.java	Mon Feb 05 12:37:59 2018 -0700
+++ b/src/luan/webserver/ResponseOutputStream.java	Mon Feb 05 22:33:59 2018 -0700
@@ -10,6 +10,7 @@
 	private final Response response;
 
 	public ResponseOutputStream(Response response) {
+		if(response==null) throw new NullPointerException();
 		this.response = response;
 	}
 
--- a/src/luan/webserver/Status.java	Mon Feb 05 12:37:59 2018 -0700
+++ b/src/luan/webserver/Status.java	Mon Feb 05 22:33:59 2018 -0700
@@ -36,6 +36,7 @@
 
 	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 NOT_FOUND = newStatus(404,"Not Found");
 	public static final Status INTERNAL_SERVER_ERROR = newStatus(500,"Internal Server Error");
 }