From: Fredrik Tolf Date: Tue, 7 Sep 2010 04:54:24 +0000 (+0200) Subject: Added initial SCGI server and a handler for serving JARs from the filesystem. X-Git-Url: http://git.dolda2000.com/gitweb/?a=commitdiff_plain;h=13e578b10b388cc0dea88e05b79265c21416e3a0;p=jsvc.git Added initial SCGI server and a handler for serving JARs from the filesystem. There's quite some amount uglyness involved, but nothing that cannot be fixed over time. --- diff --git a/src/dolda/jsvc/ServerContext.java b/src/dolda/jsvc/ServerContext.java index 423deba..47d4d13 100644 --- a/src/dolda/jsvc/ServerContext.java +++ b/src/dolda/jsvc/ServerContext.java @@ -5,4 +5,5 @@ public interface ServerContext { public String sysconfig(String key, String def); public String libconfig(String key, String def); public String name(); + public RequestThread worker(Responder root, Request req, ThreadGroup tg, String name); } diff --git a/src/dolda/jsvc/ThreadContext.java b/src/dolda/jsvc/ThreadContext.java index 27d90bf..d0e32fd 100644 --- a/src/dolda/jsvc/ThreadContext.java +++ b/src/dolda/jsvc/ThreadContext.java @@ -137,7 +137,7 @@ public class ThreadContext extends ThreadGroup { } public RequestThread respond(Request req) { - return(new RequestThread(root, req, workers, "Worker thread " + reqs++)); + return(ctx.worker(root, req, workers, "Worker thread " + reqs++)); } private Responder bootstrap(final Class bootclass) { @@ -190,4 +190,31 @@ public class ThreadContext extends ThreadGroup { } return(null); } + + public static class CreateException extends Exception { + public CreateException(String message) { + super(message); + } + + public CreateException(String message, Throwable cause) { + super(message, cause); + } + } + + public static ThreadContext create(ServerContext ctx, ClassLoader cl) throws CreateException { + String nm = "JSvc Service"; + if(ctx.name() != null) + nm = "JSvc Service for " + ctx.name(); + + String clnm = ctx.libconfig("jsvc.bootstrap", null); + if(clnm == null) + throw(new CreateException("No JSvc bootstrapper specified")); + Class bc; + try { + bc = cl.loadClass(clnm); + } catch(ClassNotFoundException e) { + throw(new CreateException("Invalid JSvc bootstrapper specified", e)); + } + return(new ThreadContext(null, nm, ctx, bc)); + } } diff --git a/src/dolda/jsvc/j2ee/J2eeContext.java b/src/dolda/jsvc/j2ee/J2eeContext.java index 84ce918..f687afd 100644 --- a/src/dolda/jsvc/j2ee/J2eeContext.java +++ b/src/dolda/jsvc/j2ee/J2eeContext.java @@ -43,4 +43,8 @@ public abstract class J2eeContext implements ServerContext { public ServletConfig j2eeconfig() { return(sc); } + + public RequestThread worker(Responder root, Request req, ThreadGroup tg, String name) { + return(new RequestThread(root, req, tg, name)); + } } diff --git a/src/dolda/jsvc/scgi/DSContext.java b/src/dolda/jsvc/scgi/DSContext.java new file mode 100644 index 0000000..9103525 --- /dev/null +++ b/src/dolda/jsvc/scgi/DSContext.java @@ -0,0 +1,48 @@ +package dolda.jsvc.scgi; + +import java.io.*; +import dolda.jsvc.*; +import dolda.jsvc.util.*; + +public class DSContext extends JarContext { + public final long mtime; + private final File datroot; + public final ThreadContext tg; + + public DSContext(File jar, File datroot) throws ThreadContext.CreateException { + super(jar); + this.mtime = jar.lastModified(); + this.datroot = datroot; + loadconfig(); + this.tg = ThreadContext.create(this, loader); + } + + private void loadconfig() { + if(datroot != null) { + File sroot = new File(new File(datroot, "store"), name()); + sysconfig.put("jsvc.storage", "file:" + sroot.getPath()); + File conf = new File(datroot, "jsvc.properties"); + if(conf.exists()) { + try { + InputStream in = new FileInputStream(conf); + try { + sysconfig.load(in); + } finally { + in.close(); + } + } catch(IOException e) { + throw(new RuntimeException(e)); + } + } + } + } + + public RequestThread worker(Responder root, Request req, ThreadGroup tg, String name) { + java.net.Socket sk = ((ScgiRequest)req).sk; + if(req.path().equals("")) { + return(new ScgiReqThread(new RootRedirect(""), req, tg, name, sk)); + } else { + return(new ScgiReqThread(root, RequestWrap.chpath(req, req.path().substring(1)), tg, name, sk)); + } + } +} diff --git a/src/dolda/jsvc/scgi/DirServer.java b/src/dolda/jsvc/scgi/DirServer.java new file mode 100644 index 0000000..7bf064d --- /dev/null +++ b/src/dolda/jsvc/scgi/DirServer.java @@ -0,0 +1,103 @@ +package dolda.jsvc.scgi; + +import java.io.*; +import java.net.*; +import java.util.*; +import dolda.jsvc.*; +import dolda.jsvc.util.*; +import dolda.jsvc.j2ee.PosixArgs; + +public class DirServer extends Server { + private final Map contexts = new HashMap(); + private final File datroot; + + public DirServer(ServerSocket sk, File datroot) { + super(sk); + this.datroot = datroot; + } + + private DSContext context(File file) throws ThreadContext.CreateException { + synchronized(contexts) { + DSContext ctx = contexts.get(file); + if(ctx != null) { + if(ctx.mtime < file.lastModified()) { + ctx.tg.destroy(); + contexts.remove(file); + ctx = null; + } + } + if(ctx == null) { + ctx = new DSContext(file, datroot); + contexts.put(file, ctx); + } + return(ctx); + } + } + + public void handle(Map head, Socket sk) throws Exception { + String filename = head.get("SCRIPT_FILENAME"); + if(filename == null) + throw(new Exception("Request for DirServer must contain SCRIPT_FILENAME")); + File file = new File(filename); + if(!file.exists() || !file.canRead()) + throw(new Exception("Cannot access the requested JSvc file " + file.toString())); + DSContext ctx = context(file); + Request req = new ScgiRequest(sk, head); + RequestThread w = ctx.tg.respond(req); + w.start(); + } + + private static void usage(PrintStream out) { + out.println("usage: dolda.jsvc.scgi.DirServer [-h] [-e CHARSET] [-d DATADIR] PORT"); + } + + public static void main(String[] args) { + PosixArgs opt = PosixArgs.getopt(args, "h"); + if(opt == null) { + usage(System.err); + System.exit(1); + } + String charset = null; + File datroot = null; + for(char c : opt.parsed()) { + switch(c) { + case 'e': + charset = opt.arg; + break; + case 'd': + datroot = new File(opt.arg); + if(!datroot.exists() || !datroot.isDirectory()) { + System.err.println(opt.arg + ": no such directory"); + System.exit(1); + } + break; + case 'h': + usage(System.out); + return; + } + } + if(opt.rest.length < 1) { + usage(System.err); + System.exit(1); + } + if(datroot == null) { + datroot = new File(System.getProperty("user.home"), ".jsvc"); + if(!datroot.exists() || !datroot.isDirectory()) + datroot = null; + } + int port = Integer.parseInt(opt.rest[0]); + ServerSocket sk; + try { + sk = new ServerSocket(port); + } catch(IOException e) { + System.err.println("could not bind to port " + port + ": " + e.getMessage()); + System.exit(1); + return; /* Because javac is stupid. :-/ */ + } + DirServer s = new DirServer(sk, datroot); + if(charset != null) + s.headcs = charset; + + new Thread(s, "SCGI server thread").start(); + } +} diff --git a/src/dolda/jsvc/scgi/InvalidRequestException.java b/src/dolda/jsvc/scgi/InvalidRequestException.java new file mode 100644 index 0000000..bb8f965 --- /dev/null +++ b/src/dolda/jsvc/scgi/InvalidRequestException.java @@ -0,0 +1,7 @@ +package dolda.jsvc.scgi; + +public class InvalidRequestException extends java.io.IOException { + public InvalidRequestException(String message) { + super(message); + } +} diff --git a/src/dolda/jsvc/scgi/LimitInputStream.java b/src/dolda/jsvc/scgi/LimitInputStream.java new file mode 100644 index 0000000..7a37f25 --- /dev/null +++ b/src/dolda/jsvc/scgi/LimitInputStream.java @@ -0,0 +1,65 @@ +package dolda.jsvc.scgi; + +import java.io.*; + +public class LimitInputStream extends InputStream { + private final InputStream bk; + private final long limit; + private long read; + + public LimitInputStream(InputStream bk, long limit) { + this.bk = bk; + this.limit = limit; + } + + public void close() throws IOException { + bk.close(); + } + + public int available() throws IOException { + int av = bk.available(); + synchronized(this) { + if(av > limit - read) + av = (int)(limit - read); + return(av); + } + } + + public int read() throws IOException { + synchronized(this) { + if(read >= limit) + return(-1); + int ret = bk.read(); + if(ret >= 0) + read++; + return(ret); + } + } + + public int read(byte[] b) throws IOException { + return(read(b, 0, b.length)); + } + + public int read(byte[] b, int off, int len) throws IOException { + synchronized(this) { + if(read >= limit) + return(-1); + if(len > limit - read) + len = (int)(limit - read); + int ret = bk.read(b, off, len); + if(ret > 0) + read += ret; + return(ret); + } + } + + public long skip(long n) throws IOException { + synchronized(this) { + if(n > limit - read) + n = limit - read; + long ret = bk.skip(n); + read += ret; + return(ret); + } + } +} diff --git a/src/dolda/jsvc/scgi/ScgiReqThread.java b/src/dolda/jsvc/scgi/ScgiReqThread.java new file mode 100644 index 0000000..1b1ad22 --- /dev/null +++ b/src/dolda/jsvc/scgi/ScgiReqThread.java @@ -0,0 +1,26 @@ +package dolda.jsvc.scgi; + +import java.io.*; +import java.net.*; +import dolda.jsvc.*; + +public class ScgiReqThread extends RequestThread { + protected final Socket sk; + + public ScgiReqThread(Responder root, Request req, ThreadGroup tg, String name, Socket sk) { + super(root, req, tg, name); + this.sk = sk; + } + + public void run() { + try { + super.run(); + } finally { + try { + sk.close(); + } catch(IOException e) { + throw(new RuntimeException(e)); + } + } + } +} diff --git a/src/dolda/jsvc/scgi/ScgiRequest.java b/src/dolda/jsvc/scgi/ScgiRequest.java new file mode 100644 index 0000000..192d380 --- /dev/null +++ b/src/dolda/jsvc/scgi/ScgiRequest.java @@ -0,0 +1,217 @@ +package dolda.jsvc.scgi; + +import java.io.*; +import java.net.*; +import java.util.*; +import dolda.jsvc.*; +import dolda.jsvc.util.*; + +public class ScgiRequest extends ResponseBuffer { + final Socket sk; + private final Map environ; + private final InputStream in; + private final String method, path; + private final URL url, context; + private MultiMap params = null; + private MultiMap inhead = new HeaderTreeMap(); + + public ScgiRequest(Socket sk, Map environ) throws IOException { + this.sk = sk; + this.environ = environ; + for(Map.Entry var : environ.entrySet()) { + String k = var.getKey(); + if((k.length() > 5) && k.substring(0, 5).equals("HTTP_")) { + StringBuilder buf = new StringBuilder(); + boolean f = true; + for(int i = 5; i < k.length(); i++) { + char c = k.charAt(i); + if(c == '_') { + buf.append('-'); + f = true; + } else if(f) { + buf.append(Character.toUpperCase(c)); + f = false; + } else { + buf.append(Character.toLowerCase(c)); + } + } + inhead.add(buf.toString(), var.getValue()); + } + } + long len; + { + String h = environ.get("CONTENT_LENGTH"); + if(h == null) { + len = 0; + } else { + try { + len = Long.parseLong(h); + } catch(NumberFormatException e) { + throw(new InvalidRequestException("Invalid Content-Length header: " + h)); + } + } + } + this.in = new LimitInputStream(sk.getInputStream(), len); + path = environ.get("PATH_INFO"); + if(path == null) + throw(new InvalidRequestException("Missing PATH_INFO")); + method = environ.get("REQUEST_METHOD"); + if(method == null) + throw(new InvalidRequestException("Missing REQUEST_METHOD")); + { + /* Ewwww, this is disgusting! */ + String scheme = "http"; + if(environ.get("HTTPS") != null) + scheme = "https"; + int port = -1; + String host = environ.get("HTTP_HOST"); + if((host == null) || (host.length() < 1)) { + if((host = environ.get("SERVER_NAME")) == null) + throw(new InvalidRequestException("Both HTTP_HOST and SERVER name are missing")); + String portnum = environ.get("SERVER_PORT"); + if(portnum == null) + throw(new InvalidRequestException("Missing SERVER_PORT")); + try { + port = Integer.parseInt(portnum); + } catch(NumberFormatException e) { + throw(new InvalidRequestException("Bad SERVER_PORT: " + portnum)); + } + if((port == 80) && scheme.equals("http")) + port = -1; + else if((port == 443) && scheme.equals("https")) + port = -1; + } else { + int p; + if((host.charAt(0) == '[') && ((p = host.indexOf(']', 1)) > 1)) { + String newhost = host.substring(1, p); + if((p = host.indexOf(':', p + 1)) >= 0) { + try { + port = Integer.parseInt(host.substring(p + 1)); + } catch(NumberFormatException e) {} + } + host = newhost; + } else if((p = host.indexOf(':')) >= 0) { + try { + port = Integer.parseInt(host.substring(p + 1)); + host = host.substring(0, p); + } catch(NumberFormatException e) {} + } + } + String nm = environ.get("SCRIPT_NAME"); + if(nm == null) + throw(new InvalidRequestException("Missing SCRIPT_NAME")); + String q = environ.get("QUERY_STRING"); + if(q != null) + q = "?" + q; + else + q = ""; + try { + url = new URL(scheme, host, port, nm + path + q); + if(nm.charAt(nm.length() - 1) != '/') + nm += "/"; /* XXX? */ + context = new URL(scheme, host, port, nm); + } catch(MalformedURLException e) { + throw(new Error(e)); + } + } + } + + public MultiMap inheaders() { + return(inhead); + } + + public ServerContext ctx() { + return(ThreadContext.current().server()); + } + + public InputStream input() { + return(in); + } + + public URL url() { + return(url); + } + + public URL rooturl() { + return(context); + } + + public String path() { + return(path); + } + + public String method() { + return(method); + } + + public MultiMap params() { + if(params == null) + params = Params.stdparams(this); + return(params); + } + + public SocketAddress localaddr() { + String portnum = environ.get("SERVER_PORT"); + int port = -1; + try { + if(portnum != null) + port = Integer.parseInt(portnum); + } catch(NumberFormatException e) {} + if(port < 0) + return(null); /* XXX? */ + String addr; + addr = environ.get("X_ASH_SERVER_ADDRESS"); + if(addr == null) + return(new InetSocketAddress(port)); /* XXX? */ + else + return(new InetSocketAddress(addr, port)); + } + + public SocketAddress remoteaddr() { + String addr; + String portnum; + addr = environ.get("REMOTE_ADDR"); + portnum = environ.get("X_ASH_PORT"); + int port = -1; + try { + if(portnum != null) + port = Integer.parseInt(portnum); + } catch(NumberFormatException e) {} + if((addr != null) && (port >= 0)) + return(new InetSocketAddress(addr, port)); + return(null); /* XXX? */ + } + + private void checkstring(String s) { + for(int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if((c < 32) || (c >= 128)) + throw(new RuntimeException("Invalid header string: " + s)); + } + } + + protected void backflush() throws IOException { + Writer out = new OutputStreamWriter(realoutput(), Misc.ascii); + out.write(String.format("Status: %d %s\n", respcode, resptext)); + for(Map.Entry e : outheaders().entrySet()) { + String k = e.getKey(); + String v = e.getValue(); + checkstring(k); + checkstring(v); + out.write(String.format("%s: %s\n", k, v)); + } + out.write("\n"); + out.flush(); + } + + protected OutputStream realoutput() { + try { + return(sk.getOutputStream()); + } catch(IOException e) { + /* It is not obvious why this would happen, so I'll wait + * until I know whatever might happen to try and implement + * meaningful behavior. */ + throw(new RuntimeException(e)); + } + } +} diff --git a/src/dolda/jsvc/scgi/Server.java b/src/dolda/jsvc/scgi/Server.java new file mode 100644 index 0000000..f01eae9 --- /dev/null +++ b/src/dolda/jsvc/scgi/Server.java @@ -0,0 +1,111 @@ +package dolda.jsvc.scgi; + +import java.util.logging.*; +import java.io.*; +import java.net.*; +import java.util.*; + +public abstract class Server implements Runnable { + private final ServerSocket sk; + private final Logger logger = Logger.getLogger("dolda.jsvc.scgi"); + public String headcs = "UTF-8"; + + public Server(ServerSocket sk) { + this.sk = sk; + } + + private static int readnslen(InputStream in) throws IOException { + int ret = 0; + while(true) { + int c = in.read(); + if(c == ':') + return(ret); + else if((c >= '0') && (c <= '9')) + ret = (ret * 10) + (c - '0'); + else + throw(new InvalidRequestException("Malformed netstring length")); + } + } + + private static byte[] readns(InputStream in) throws IOException { + byte[] buf = new byte[readnslen(in)]; + int off = 0; + while(off < buf.length) { + int ret = in.read(buf, off, buf.length - off); + if(ret < 0) + throw(new InvalidRequestException("Unexpected EOS in netstring")); + off += ret; + } + if(in.read() != ',') + throw(new InvalidRequestException("Unterminated netstring")); + return(buf); + } + + private Map readhead(InputStream in) throws IOException { + byte[] rawhead = readns(in); + String head = new String(rawhead, headcs); + Map ret = new HashMap(); + int p = 0; + while(true) { + int p2 = head.indexOf(0, p); + if(p2 < 0) { + if(p == head.length()) + return(ret); + throw(new InvalidRequestException("Malformed headers")); + } + String key = head.substring(p, p2); + int p3 = head.indexOf(0, p2 + 1); + if(p3 < 0) + throw(new InvalidRequestException("Malformed headers")); + String val = head.substring(p2 + 1, p3); + ret.put(key, val); + p = p3 + 1; + } + } + + private boolean checkhead(Map head) { + if(!head.containsKey("SCGI") || !head.get("SCGI").equals("1")) + return(false); + return(true); + } + + protected abstract void handle(Map head, Socket sk) throws Exception; + + private void serve(Socket sk) { + try { + try { + InputStream in = sk.getInputStream(); + Map head = readhead(in); + if(!checkhead(head)) + return; + try { + handle(head, sk); + } catch(Exception e) { + logger.log(Level.WARNING, "Could not handle request", e); + return; + } + sk = null; + } finally { + if(sk != null) + sk.close(); + } + } catch(IOException e) { + logger.log(Level.WARNING, "I/O error encountered while serving SCGI request", e); + } + } + + public void run() { + try { + try { + while(true) { + Socket nsk = sk.accept(); + serve(nsk); + } + } finally { + sk.close(); + } + } catch(IOException e) { + logger.log(Level.SEVERE, "SCGI server encountered I/O error", e); + } + } +} diff --git a/src/dolda/jsvc/util/JarContext.java b/src/dolda/jsvc/util/JarContext.java new file mode 100644 index 0000000..cb16d37 --- /dev/null +++ b/src/dolda/jsvc/util/JarContext.java @@ -0,0 +1,93 @@ +package dolda.jsvc.util; + +import java.io.*; +import java.util.*; +import java.net.*; +import dolda.jsvc.*; + +public class JarContext implements ServerContext { + private final long ctime; + private final String name; + public final ClassLoader loader; + protected final Properties sysconfig, libconfig; + + private static String mangle(File f) { + String ret = f.getName(); + int p = ret.lastIndexOf('.'); + if(p > 0) + ret = ret.substring(0, p); + for(f = f.getParentFile(); f != null; f = f.getParentFile()) + ret = f.getName() + "/" + ret; + return(ret); + } + + private void loadconfig() { + try { + InputStream pi = loader.getResourceAsStream("jsvc.properties"); + if(pi != null) { + try { + libconfig.load(pi); + } finally { + pi.close(); + } + } + } catch(IOException e) { + throw(new Error(e)); + } + } + + public Class findboot() { + String clnm = libconfig("jsvc.bootstrap", null); + if(clnm == null) + return(null); + Class bc; + try { + bc = loader.loadClass(clnm); + } catch(ClassNotFoundException e) { + return(null); + } + return(bc); + } + + public JarContext(ClassLoader cl, String name) { + this.ctime = System.currentTimeMillis(); + this.name = name; + this.loader = cl; + sysconfig = new Properties(); + libconfig = new Properties(); + + loadconfig(); + } + + private static URL makingmewanttokilljavac(File jar) { + try { + return(jar.toURI().toURL()); + } catch(MalformedURLException e) { + throw(new RuntimeException(e)); + } + } + + public JarContext(File jar) { + this(new URLClassLoader(new URL[] {makingmewanttokilljavac(jar)}, JarContext.class.getClassLoader()), mangle(jar)); + } + + public long starttime() { + return(ctime); + } + + public String name() { + return(name); + } + + public String sysconfig(String key, String def) { + return(sysconfig.getProperty(key, def)); + } + + public String libconfig(String key, String def) { + return(libconfig.getProperty(key, def)); + } + + public RequestThread worker(Responder root, Request req, ThreadGroup tg, String name) { + return(new RequestThread(root, req, tg, name)); + } +} diff --git a/src/dolda/jsvc/util/Misc.java b/src/dolda/jsvc/util/Misc.java index 34e36f7..9b7f8e1 100644 --- a/src/dolda/jsvc/util/Misc.java +++ b/src/dolda/jsvc/util/Misc.java @@ -6,6 +6,7 @@ import java.io.*; public class Misc { public static final java.nio.charset.Charset utf8 = java.nio.charset.Charset.forName("UTF-8"); + public static final java.nio.charset.Charset ascii = java.nio.charset.Charset.forName("US-ASCII"); private static Map stext = new HashMap(); static { diff --git a/src/dolda/jsvc/util/ResponseBuffer.java b/src/dolda/jsvc/util/ResponseBuffer.java index c949464..96d4563 100644 --- a/src/dolda/jsvc/util/ResponseBuffer.java +++ b/src/dolda/jsvc/util/ResponseBuffer.java @@ -31,7 +31,7 @@ public abstract class ResponseBuffer implements ResettableRequest { throw(new IllegalStateException("Response has been flushed; header information cannot be modified")); } - private void flush() { + private void flush() throws IOException { if(flushed) return; if(respcode < 0) { @@ -100,6 +100,6 @@ public abstract class ResponseBuffer implements ResettableRequest { init(); } - protected abstract void backflush(); + protected abstract void backflush() throws IOException; protected abstract OutputStream realoutput(); }