Mercurial Hosting > luan
changeset 1135:707a5d874f3e
add luan.host
author | Franklin Schmidt <fschmidt@gmail.com> |
---|---|
date | Sun, 28 Jan 2018 21:36:58 -0700 |
parents | e54ae41e9501 |
children | d30d400fd43d |
files | src/luan/host/Backup.java src/luan/host/Init.luan src/luan/host/Log4j.java src/luan/host/WebHandler.java src/luan/host/main.luan src/luan/host/run.luan |
diffstat | 6 files changed, 693 insertions(+), 0 deletions(-) [+] |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/luan/host/Backup.java Sun Jan 28 21:36:58 2018 -0700 @@ -0,0 +1,111 @@ +package luan.host; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.Map; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.apache.lucene.index.SnapshotDeletionPolicy; +import org.apache.lucene.index.IndexCommit; +import org.apache.lucene.store.FSDirectory; +import luan.LuanState; +import luan.LuanTable; +import luan.LuanException; +import luan.modules.PackageLuan; +import luan.modules.lucene.LuceneIndex; + + +public final class Backup { + private static final Logger logger = LoggerFactory.getLogger(Backup.class); + + private Backup() {} // never + + private static void mkdir(File dir) { + if( !dir.mkdirs() ) + throw new RuntimeException("couldn't make "+dir); + } + + private static void link(File from,File to) throws IOException { + Files.createLink( to.toPath(), from.toPath() ); + } + + private static void backupNonlocal(File from,File to) throws IOException { + mkdir(to); + for( File fromChild : from.listFiles() ) { + File toChild = new File( to, fromChild.getName() ); + if( fromChild.isDirectory() ) { + if( !fromChild.getName().equals("local") ) + backupNonlocal( fromChild, toChild ); + } else if( fromChild.isFile() ) { + link( fromChild, toChild ); + } else { + throw new RuntimeException(fromChild+" isn't dir or file"); + } + } + } + + private static final String getLucenes = + "local Lucene = require 'luan:lucene/Lucene.luan'\n" + +"local Table = require 'luan:Table.luan'\n" + +"return Table.copy(Lucene.instances)\n" + ; + + private static void backupLucene(File from,File to) throws IOException { + if( !new File(from,"site/init.luan").exists() ) { + return; + } + String fromPath = from.getCanonicalPath() + "/"; + LuanTable luceneInstances; + LuanState luan = null; + try { + if( WebHandler.isServing() ) { + luceneInstances = (LuanTable)WebHandler.runLuan( from.getName(), getLucenes, "getLucenes" ); + } else { + luan = new LuanState(); + WebHandler.initLuan( luan, from.toString(), from.getName() ); + PackageLuan.load(luan,"site:/init.luan"); + luceneInstances = (LuanTable)luan.eval(getLucenes); + } + } catch(LuanException e) { + throw new RuntimeException(e); + } + for( Map.Entry entry : luceneInstances.rawIterable() ) { + LuanTable tbl = (LuanTable)entry.getKey(); + LuceneIndex li = (LuceneIndex)tbl.rawGet("java"); + SnapshotDeletionPolicy snapshotDeletionPolicy = li.snapshotDeletionPolicy(); + IndexCommit ic = snapshotDeletionPolicy.snapshot(); + try { + FSDirectory fsdir = (FSDirectory)ic.getDirectory(); + File dir = fsdir.getDirectory(); + String dirPath = dir.toString(); + if( !dirPath.startsWith(fromPath) ) + throw new RuntimeException(fromPath+" "+dirPath); + File toDir = new File( to, dirPath.substring(fromPath.length()) ); + mkdir(toDir); + for( String name : ic.getFileNames() ) { + link( new File(dir,name), new File(toDir,name) ); + } + } finally { + snapshotDeletionPolicy.release(ic); + } + } + if( luan != null ) + luan.close(); + } + + public static void backup(File sitesDir,File backupDir) throws IOException { + mkdir(backupDir); + for( File siteDir : sitesDir.listFiles() ) { + File to = new File( backupDir, siteDir.getName() ); + backupNonlocal( siteDir, to ); + backupLucene( siteDir, to ); + } + } + + public static void main(String[] args) throws Exception { + Log4j.initForConsole(); + backup( new File(args[0]), new File(args[1]) ); + System.exit(0); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/luan/host/Init.luan Sun Jan 28 21:36:58 2018 -0700 @@ -0,0 +1,93 @@ +local Luan = require "luan:Luan.luan" +local error = Luan.error +local String = require "luan:String.luan" +local gsub = String.gsub or error() +local Io = require "luan:Io.luan" +local Http = require "luan:http/Http.luan" +local Hosting = require "luan:host/Hosting.luan" +local Mail = require "luan:mail/Mail.luan" + + +local Init = {} + +local dir, domain = ... + +Init.password = Io.schemes.file(dir).child("password").read_text() + +Http.dir = "file:"..dir.."/site" + +function Io.schemes.site(path,loading) + return Io.uri( Http.dir..path, loading ) +end + +Hosting.domain = domain +Io.password = Init.password + + +-- logging + +java() +local Logger = require "java:org.apache.log4j.Logger" +local Logging = require "luan:logging/Logging.luan" + +local root = gsub(domain,"\.",":") + +Logging.layout = "%d %-5p %c{-1} - %m%n" +Logging.file = dir.."/site/private/local/logs/luan.log" +Logging.max_file_size = "1MB" +local one_mb = 1048576 + +local log_dir = dir.."/site/private/local/logs/" +Logging.appenders = { + [log_dir.."error.log"] = "ERROR" + [log_dir.."warn.log"] = "WARN" + [log_dir.."info.log"] = "INFO" +} + +Logging.log4j_root_logger = Logger.getLogger(root) +Logging.log4j_root_logger.setAdditivity(false) + +local old_log_to_file = Logging.log_to_file + +function Logging.log_to_file(file,logger_name) + Io.schemes.file(file) -- security check + logger_name = logger_name and root .. "." .. logger_name + local appender = old_log_to_file(file,logger_name) + appender.getMaximumFileSize() <= one_mb or error "Logging.max_file_size is too big" + return appender +end + +local old_init = Logging.init + +function Logging.init() + Logging.appenders["System.err"] = nil + Logging.appenders["System.out"] = nil + old_init() +end + +Logging.init() + +local old_logger = Logging.logger + +function Logging.root_logger() + return old_logger(root) +end + +function Logging.logger(name) + return old_logger( root .. "." .. name ) +end + +Init.logger_root = root.."." + + +-- mail - fix later + +Hosting.send_mail = Mail.Sender{ + host = "smtpcorp.com"; + username = "smtp@luanhost.com"; -- ? + password = "luanhost"; + port = 2525; +}.send + + +return Init
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/luan/host/Log4j.java Sun Jan 28 21:36:58 2018 -0700 @@ -0,0 +1,18 @@ +package luan.host; + +import org.apache.log4j.Logger; +import org.apache.log4j.Level; +import org.apache.log4j.PatternLayout; +import org.apache.log4j.ConsoleAppender; + + +public final class Log4j { + + public static void initForConsole() { + Logger.getRootLogger().setLevel(Level.INFO); + PatternLayout layout = new PatternLayout("%d %-5p %c - %m%n"); + ConsoleAppender appender = new ConsoleAppender(layout,"System.err"); + Logger.getRootLogger().addAppender(appender); + } + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/luan/host/WebHandler.java Sun Jan 28 21:36:58 2018 -0700 @@ -0,0 +1,229 @@ +package luan.host; + +import java.io.File; +import java.io.IOException; +import java.util.Map; +import java.util.HashMap; +import java.util.TimeZone; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.NCSARequestLog; +import org.eclipse.jetty.server.handler.AbstractHandler; +import org.eclipse.jetty.server.handler.ResourceHandler; +import org.eclipse.jetty.server.handler.HandlerList; +import org.eclipse.jetty.server.handler.RequestLogHandler; +import org.eclipse.jetty.server.handler.DefaultHandler; +import org.eclipse.jetty.server.handler.HandlerCollection; +import luan.Luan; +import luan.LuanState; +import luan.LuanException; +import luan.LuanTable; +import luan.LuanFunction; +import luan.modules.IoLuan; +import luan.modules.JavaLuan; +import luan.modules.PackageLuan; +import luan.modules.http.LuanHandler; +import luan.modules.http.AuthenticationHandler; +import luan.modules.http.NotFound; + + +public class WebHandler extends AbstractHandler { + private static final Logger logger = LoggerFactory.getLogger(WebHandler.class); + + private static class Site { + final Handler handler; + final LuanHandler luanHandler; + + Site(Handler handler,LuanHandler luanHandler) { + this.handler = handler; + this.luanHandler = luanHandler; + } + } + + public static String allowJavaFileName = "allow_java"; // change for security + private static final String tz = TimeZone.getDefault().getID(); + private static final Map<String,Site> siteMap = new HashMap<String,Site>(); + private static String sitesDir = null; + private static Server server = null; + + public static boolean isServing() { + return sitesDir != null; + } + + public WebHandler(String dir,Server server) { + if( sitesDir != null ) + throw new RuntimeException("already set"); + if( !new File(dir).exists() ) + throw new RuntimeException(); + this.sitesDir = dir; + this.server = server; + } + + public void handle(String target,Request baseRequest,HttpServletRequest request,HttpServletResponse response) + throws IOException, ServletException + { + String domain = baseRequest.getServerName(); +// System.out.println("handle "+domain); + Site site = getSite(domain); + if( site != null ) { + site.handler.handle(target,baseRequest,request,response); + } + } + + public static Object runLuan(String domain,String sourceText,String sourceName) throws LuanException { + return getSite(domain).luanHandler.runLuan(sourceText,sourceName); + } + + public static Object callSite(String domain,String fnName,Object... args) throws LuanException { + return getSite(domain).luanHandler.call_rpc(fnName,args); + } + + private static Site getSite(String domain) { + synchronized(siteMap) { + Site site = siteMap.get(domain); + if( site == null ) { + if( sitesDir==null ) + throw new NullPointerException("sitesDir"); + File dir = new File(sitesDir,domain); + if( !dir.exists() /* && !recover(dir) */ ) + return null; + site = newSite(dir.toString(),domain); + siteMap.put(domain,site); + } + return site; + } + } +/* + private static boolean recover(File dir) { + File backups = new File(dir.getParentFile().getParentFile(),"backups"); + if( !backups.exists() ) + return false; + String name = dir.getName(); + File from = null; + for( File backup : backups.listFiles() ) { + File d = new File(backup,"current/"+name); + if( d.exists() && (from==null || from.lastModified() < d.lastModified()) ) + from = d; + } + if( from == null ) + return false; + if( !from.renameTo(dir) ) + throw new RuntimeException("couldn't rename "+from+" to "+dir); + logger.info("recovered "+name+" from "+from); + return true; + } +*/ + static LuanTable initLuan(LuanState luan,String dir,String domain) { + LuanTable init; + try { + init = (LuanTable)luan.eval( + "local Luan = require 'luan:Luan.luan'\n" + +"local f = Luan.load_file 'classpath:luan/host/Init.luan'\n" + +"return f('"+dir+"','"+domain+"')\n" + ); + } catch(LuanException e) { + throw new RuntimeException(e); + } + File allowJavaFile = new File(dir,"site/private/"+allowJavaFileName); + if( !allowJavaFile.exists() ) { + JavaLuan.setSecurity( luan, javaSecurity ); + IoLuan.setSecurity( luan, ioSecurity(dir) ); + } + return init; + } + + private static Site newSite(String dir,String domain) { + LuanState luan = new LuanState(); + LuanTable init = initLuan(luan,dir,domain); + String password = (String)init.rawGet("password"); + + AuthenticationHandler authenticationHandler = new AuthenticationHandler("/private/"); + authenticationHandler.setPassword(password); + String loggerRoot = (String)init.rawGet("logger_root"); + LuanHandler luanHandler = new LuanHandler(luan,loggerRoot); + + ResourceHandler resourceHandler = new ResourceHandler(); + resourceHandler.setResourceBase(dir+"/site"); + resourceHandler.setDirectoriesListed(true); + resourceHandler.setAliases(true); + + NotFound notFoundHandler = new NotFound(luanHandler); + DefaultHandler defaultHandler = new DefaultHandler(); + + HandlerList handlers = new HandlerList(); + handlers.setHandlers(new Handler[]{authenticationHandler,luanHandler,resourceHandler,notFoundHandler,defaultHandler}); + + String logDir = dir+"/site/private/local/logs/web"; + new File(logDir).mkdirs(); + NCSARequestLog log = new NCSARequestLog(logDir+"/yyyy_mm_dd.log"); + log.setExtended(false); + log.setLogTimeZone(tz); + RequestLogHandler logHandler = new RequestLogHandler(); + logHandler.setRequestLog(log); + + HandlerCollection hc = new HandlerCollection(); + hc.setHandlers(new Handler[]{handlers,logHandler}); +// hc.setServer(getServer()); + + try { + hc.start(); + } catch(Exception e) { + throw new RuntimeException(e); + } + return new Site(hc,luanHandler); + } + + public static void removeHandler(String domain) throws Exception { + synchronized(siteMap) { + Site site = siteMap.remove(domain); + if( site != null ) { + site.handler.stop(); + site.handler.destroy(); + } + } + } + + public static void loadHandler(String domain) { + getSite(domain); + } + + public static Server server() { + return server; + } + + private static final IoLuan.Security ioSecurity(String dir) { + final String siteDir = dir + "/site/"; + return new IoLuan.Security() { + public void check(LuanState luan,String name) throws LuanException { + if( name.startsWith("file:") ) { + if( name.contains("..") ) + throw new LuanException("Security violation - '"+name+"' contains '..'"); + if( !name.startsWith("file:"+siteDir) ) + throw new LuanException("Security violation - '"+name+"' outside of site dir"); + } + else if( name.startsWith("classpath:luan/host/") ) { + throw new LuanException("Security violation"); + } + else if( name.startsWith("os:") || name.startsWith("bash:") ) { + throw new LuanException("Security violation"); + } + } + }; + } + + private static final JavaLuan.Security javaSecurity = new JavaLuan.Security() { + public void check(LuanState luan,String name) throws LuanException { + if( !name.startsWith("luan:") ) + throw new LuanException("Security violation - only luan:* modules can load Java"); + if( name.equals("luan:logging/Logging") ) + throw new LuanException("Security violation - cannot reload Logging"); + } + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/luan/host/main.luan Sun Jan 28 21:36:58 2018 -0700 @@ -0,0 +1,187 @@ +java() +local Luan = require "luan:Luan.luan" +local error = Luan.error +local assert_string = Luan.assert_string or error() +local ipairs = Luan.ipairs or error() +local try = Luan.try or error() +local Io = require "luan:Io.luan" +local print = Io.print or error() +local Rpc = require "luan:Rpc.luan" +local Thread = require "luan:Thread.luan" +local String = require "luan:String.luan" +local literal = String.literal or error() +local lower = String.lower or error() +local matches = String.matches or error() +local Hosting = require "luan:host/Hosting.luan" +local Logging = require "luan:logging/Logging.luan" +local logger = Logging.logger "main" +local WebHandler = require "java:luan.host.WebHandler" + + +local sites_dir = Io.schemes.file(Hosting.sites_dir) + +sites_dir.mkdir() + +local function delete_unused(file) + if file.is_directory() then + if file.name() == "local" then + return false + end + local all_deleted = true + for _,child in ipairs(file.children()) do + all_deleted = delete_unused(child) and all_deleted + end + if not all_deleted then + return false + end + end + file.delete() + return true +end + + +local fns = Rpc.functions + +local function get_dir(domain,password) + assert_string(domain) + assert_string(password) + domain = lower(domain) + local dir = sites_dir.child(domain) + if dir.exists() then + local pwd = dir.child("password").read_text() + if pwd ~= password then + error "wrong password" + end + return dir.child("site") + else + return nil + end +end + +function fns.get(domain,password) + local site_dir = get_dir(domain,password) + if site_dir == nil then + return nil + end + + local children, file_info + + function children(dir) + if dir.name() == "local" then + return {} + end + local rtn = {} + for _,child in ipairs(dir.children()) do + local info = file_info(child) + if info ~= nil then + rtn[info.name] = info + end + end + return rtn + end + + function file_info(file) + local info = { name = file.name(), path = file.to_string() } + if file.is_directory() then + info.children = children(file) + elseif file.is_file() then + info.checksum = file.checksum() + else + return nil + end + return info + end + + return file_info(site_dir) +end + +function fns.create(domain,password) + assert_string(domain) + assert_string(password) + domain = lower(domain) + local dir = sites_dir.child(domain) + dir.exists() and error "already exists" + dir.mkdir() + dir.child("password").write(password) + dir = dir.child("site") + dir.mkdir() + return { name = dir.name(), path = dir.to_string(), children = {} } +end + +local function security(site_dir,file) + matches( file.to_string(), "^"..literal(site_dir.to_string()) ) or error "security violation" +end + +function fns.copy_file(domain,password,dir,name,content) + local site_dir = get_dir(domain,password) + site_dir or error "domain not found" + local file = Io.schemes.file(dir).child(name) + security(site_dir,file) + file.delete() + file.write(content) +end + +function fns.mkdir(domain,password,dir,name) + local site_dir = get_dir(domain,password) + site_dir or error "domain not found" + local file = Io.schemes.file(dir).child(name) + security(site_dir,file) + file.mkdir() + return { name = file.name(), path = file.to_string(), children = {} } +end + +function fns.delete_unused(domain,password,path) + local site_dir = get_dir(domain,password) + site_dir or error "domain not found" + local file = Io.schemes.file(path) + security(site_dir,file) + return delete_unused(file) +end + +function fns.update_handler(domain,password) + local site_dir = get_dir(domain,password) + site_dir or error "domain not found" + domain = lower(domain) + WebHandler.removeHandler(domain) + WebHandler.loadHandler(domain) +end + +function fns.delete(domain,password) + local site_dir = get_dir(domain,password) + site_dir or error "domain not found" + site_dir.parent().delete() + domain = lower(domain) + WebHandler.removeHandler(domain) +end + +function fns.exists(domain) + assert_string(domain) + domain = lower(domain) + return sites_dir.child(domain).exists() +end + +function fns.change_domain(old_domain,new_domain,password) + local old_dir = get_dir(old_domain,password) + old_dir or error "domain not found" + old_dir = old_dir.parent() + assert_string(new_domain) + new_domain = lower(new_domain) + local new_dir = sites_dir.child(new_domain) + new_dir.exists() and error "new_domain already exists" + WebHandler.removeHandler(old_domain) + old_dir.rename_to(new_dir.to_string()) + WebHandler.removeHandler(old_domain) + WebHandler.loadHandler(new_domain) +end + +function fns.change_password(domain,old_password,new_password) + local site_dir = get_dir(domain,old_password) + site_dir or error "domain not found" + site_dir.parent().child("password").write(new_password) + WebHandler.removeHandler(domain) + WebHandler.loadHandler(domain) +end + +fns.call = WebHandler.callSite + +Thread.fork(Rpc.serve)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/luan/host/run.luan Sun Jan 28 21:36:58 2018 -0700 @@ -0,0 +1,55 @@ +local Luan = require "luan:Luan.luan" +local error = Luan.error +local do_file = Luan.do_file or error() +local ipairs = Luan.ipairs or error() +local Io = require "luan:Io.luan" +local print = Io.print or error() +local String = require "luan:String.luan" +local Hosting = require "luan:host/Hosting.luan" +require "luan:logging/init.luan" -- initialize logging + + +local here = Io.schemes.file(".").canonical().to_string() +Hosting.sites_dir = here.."/sites/" +do_file "classpath:luan/host/main.luan" + + + +-- web server + +java() +local Server = require "java:org.eclipse.jetty.server.Server" +local DefaultHandler = require "java:org.eclipse.jetty.server.handler.DefaultHandler" +local HandlerCollection = require "java:org.eclipse.jetty.server.handler.HandlerCollection" +local SessionHandler = require "java:org.eclipse.jetty.server.session.SessionHandler" +local SslSelectChannelConnector = require "java:org.eclipse.jetty.server.ssl.SslSelectChannelConnector" +local WebHandler = require "java:luan.host.WebHandler" + +local server = Server.new(8080) + +local handlers = HandlerCollection.new() +handlers.setHandlers { + SessionHandler.new(), + WebHandler.new(Hosting.sites_dir,server), + DefaultHandler.new() +} +server.setHandler(handlers); + +server.start() + + +--[[ +local tp = server.getThreadPool() +print(tp) +print(tp.getClass()) +print("max "..tp.getMaxThreads()) +print("getMaxQueued "..tp.getMaxQueued()) + +for _, c in ipairs(server.getConnectors()) do + print(c) + tp = c.getThreadPool() + print(tp) +end + +print "done" +]]