Added initial SCGI server and a handler for serving JARs from the filesystem.
authorFredrik Tolf <fredrik@dolda2000.com>
Tue, 7 Sep 2010 04:54:24 +0000 (06:54 +0200)
committerFredrik Tolf <fredrik@dolda2000.com>
Tue, 7 Sep 2010 04:54:24 +0000 (06:54 +0200)
There's quite some amount uglyness involved, but nothing that cannot be
fixed over time.

13 files changed:
src/dolda/jsvc/ServerContext.java
src/dolda/jsvc/ThreadContext.java
src/dolda/jsvc/j2ee/J2eeContext.java
src/dolda/jsvc/scgi/DSContext.java [new file with mode: 0644]
src/dolda/jsvc/scgi/DirServer.java [new file with mode: 0644]
src/dolda/jsvc/scgi/InvalidRequestException.java [new file with mode: 0644]
src/dolda/jsvc/scgi/LimitInputStream.java [new file with mode: 0644]
src/dolda/jsvc/scgi/ScgiReqThread.java [new file with mode: 0644]
src/dolda/jsvc/scgi/ScgiRequest.java [new file with mode: 0644]
src/dolda/jsvc/scgi/Server.java [new file with mode: 0644]
src/dolda/jsvc/util/JarContext.java [new file with mode: 0644]
src/dolda/jsvc/util/Misc.java
src/dolda/jsvc/util/ResponseBuffer.java

index 423deba..47d4d13 100644 (file)
@@ -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);
 }
index 27d90bf..d0e32fd 100644 (file)
@@ -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));
+    }
 }
index 84ce918..f687afd 100644 (file)
@@ -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 (file)
index 0000000..9103525
--- /dev/null
@@ -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 (file)
index 0000000..7bf064d
--- /dev/null
@@ -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<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();
+    }
+}
diff --git a/src/dolda/jsvc/scgi/InvalidRequestException.java b/src/dolda/jsvc/scgi/InvalidRequestException.java
new file mode 100644 (file)
index 0000000..bb8f965
--- /dev/null
@@ -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 (file)
index 0000000..7a37f25
--- /dev/null
@@ -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 (file)
index 0000000..1b1ad22
--- /dev/null
@@ -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 (file)
index 0000000..192d380
--- /dev/null
@@ -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<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));
+       }
+    }
+}
diff --git a/src/dolda/jsvc/scgi/Server.java b/src/dolda/jsvc/scgi/Server.java
new file mode 100644 (file)
index 0000000..f01eae9
--- /dev/null
@@ -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<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);
+       }
+    }
+}
diff --git a/src/dolda/jsvc/util/JarContext.java b/src/dolda/jsvc/util/JarContext.java
new file mode 100644 (file)
index 0000000..cb16d37
--- /dev/null
@@ -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));
+    }
+}
index 34e36f7..9b7f8e1 100644 (file)
@@ -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<Integer, String> stext = new HashMap<Integer, String>();
     
     static {
index c949464..96d4563 100644 (file)
@@ -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();
 }