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);
}
}
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) {
}
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));
+ }
}
public ServletConfig j2eeconfig() {
return(sc);
}
+
+ public RequestThread worker(Responder root, Request req, ThreadGroup tg, String name) {
+ return(new RequestThread(root, req, tg, name));
+ }
}
--- /dev/null
+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));
+ }
+ }
+}
--- /dev/null
+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<File, DSContext> contexts = new HashMap<File, DSContext>();
+ 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<String, String> 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();
+ }
+}
--- /dev/null
+package dolda.jsvc.scgi;
+
+public class InvalidRequestException extends java.io.IOException {
+ public InvalidRequestException(String message) {
+ super(message);
+ }
+}
--- /dev/null
+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);
+ }
+ }
+}
--- /dev/null
+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));
+ }
+ }
+ }
+}
--- /dev/null
+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<String, String> environ;
+ private final InputStream in;
+ private final String method, path;
+ private final URL url, context;
+ private MultiMap<String, String> params = null;
+ private MultiMap<String, String> inhead = new HeaderTreeMap();
+
+ public ScgiRequest(Socket sk, Map<String, String> environ) throws IOException {
+ this.sk = sk;
+ this.environ = environ;
+ for(Map.Entry<String, String> 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<String, String> 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<String, String> 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<String, String> 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));
+ }
+ }
+}
--- /dev/null
+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<String, String> readhead(InputStream in) throws IOException {
+ byte[] rawhead = readns(in);
+ String head = new String(rawhead, headcs);
+ Map<String, String> ret = new HashMap<String, String>();
+ 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<String, String> head) {
+ if(!head.containsKey("SCGI") || !head.get("SCGI").equals("1"))
+ return(false);
+ return(true);
+ }
+
+ protected abstract void handle(Map<String, String> head, Socket sk) throws Exception;
+
+ private void serve(Socket sk) {
+ try {
+ try {
+ InputStream in = sk.getInputStream();
+ Map<String, String> 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);
+ }
+ }
+}
--- /dev/null
+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));
+ }
+}
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<Integer, String> stext = new HashMap<Integer, String>();
static {
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) {
init();
}
- protected abstract void backflush();
+ protected abstract void backflush() throws IOException;
protected abstract OutputStream realoutput();
}