Initial commit.
authorFredrik Tolf <fredrik@dolda2000.com>
Thu, 10 Feb 2022 16:48:46 +0000 (17:48 +0100)
committerFredrik Tolf <fredrik@dolda2000.com>
Thu, 10 Feb 2022 16:48:46 +0000 (17:48 +0100)
12 files changed:
.gitignore [new file with mode: 0644]
build.xml [new file with mode: 0644]
src/jagi/PosixArgs.java [new file with mode: 0644]
src/jagi/Utils.java [new file with mode: 0644]
src/jagi/fs/Bootstrap.java [new file with mode: 0644]
src/jagi/fs/Compiler.java [new file with mode: 0644]
src/jagi/fs/Handler.java [new file with mode: 0644]
src/jagi/fs/JavaHandler.java [new file with mode: 0644]
src/jagi/scgi/Bootstrap.java [new file with mode: 0644]
src/jagi/scgi/Jagi.java [new file with mode: 0644]
src/jagi/scgi/Scgi.java [new file with mode: 0644]
src/jagi/scgi/SimpleServer.java [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..796b96d
--- /dev/null
@@ -0,0 +1 @@
+/build
diff --git a/build.xml b/build.xml
new file mode 100644 (file)
index 0000000..e805cd6
--- /dev/null
+++ b/build.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0"?>
+
+<project name="jagi" default="jar">
+  
+  <property environment="env" />
+  
+  <target name="build-env">
+    <mkdir dir="build" />
+    <mkdir dir="build/bin" />
+  </target>
+
+  <target name="classes" depends="build-env">
+    <javac srcdir="src" destdir="build/bin" debug="on"
+          source="1.8" target="1.8" includeantruntime="no">
+      <compilerarg value="-Xlint:unchecked" />
+    </javac>
+  </target>
+  
+  <target name="jar" depends="build-env, classes">
+    <jar destfile="build/jagi.jar" basedir="build/bin">
+      <manifest>
+       <attribute name="Main-Class" value="jagi.scgi.Bootstrap" />
+      </manifest>
+    </jar>
+  </target>
+  
+  <target name="clean">
+    <delete dir="build" />
+  </target>
+</project>
diff --git a/src/jagi/PosixArgs.java b/src/jagi/PosixArgs.java
new file mode 100644 (file)
index 0000000..fa6d28b
--- /dev/null
@@ -0,0 +1,100 @@
+package jagi;
+
+import java.util.*;
+
+public class PosixArgs {
+    private List<Arg> parsed;
+    public String[] rest;
+    public String arg = null;
+    
+    private static class Arg {
+       private char ch;
+       private String arg;
+       
+       private Arg(char ch, String arg) {
+           this.ch = ch;
+           this.arg = arg;
+       }
+    }
+    
+    private PosixArgs() {
+       parsed = new ArrayList<Arg>();
+    }
+
+    public static PosixArgs getopt(String[] argv, int start, String desc) {
+       PosixArgs ret = new PosixArgs();
+       List<Character> fl = new ArrayList<Character>(), fla = new ArrayList<Character>();
+       List<String> rest = new ArrayList<String>();
+       for(int i = 0; i < desc.length();) {
+           char ch = desc.charAt(i++);
+           if((i < desc.length()) && (desc.charAt(i) == ':')) {
+               i++;
+               fla.add(ch);
+           } else {
+               fl.add(ch);
+           }
+       }
+       boolean acc = true;
+       for(int i = start; i < argv.length;) {
+           String arg = argv[i++];
+           if(acc && arg.equals("--")) {
+               acc = false;
+           } else if(acc && (arg.charAt(0) == '-') && (arg.length() > 1)) {
+               for(int o = 1; o < arg.length();) {
+                   char ch = arg.charAt(o++);
+                   if(fl.contains(ch)) {
+                       ret.parsed.add(new Arg(ch, null));
+                   } else if(fla.contains(ch)) {
+                       if(o < arg.length()) {
+                           ret.parsed.add(new Arg(ch, arg.substring(o)));
+                           break;
+                       } else if(i < argv.length) {
+                           ret.parsed.add(new Arg(ch, argv[i++]));
+                           break;
+                       } else {
+                           System.err.println("option requires an argument -- '" + ch + "'");
+                           return(null);
+                       }
+                   } else {
+                       System.err.println("invalid option -- '" + ch + "'");
+                       return(null);
+                   }
+               }
+           } else {
+               rest.add(arg);
+           }
+       }
+       ret.rest = rest.toArray(new String[0]);
+       return(ret);
+    }
+    
+    public static PosixArgs getopt(String[] argv, String desc) {
+       return(getopt(argv, 0, desc));
+    }
+    
+    public Iterable<Character> parsed() {
+       return(new Iterable<Character>() {
+               public Iterator<Character> iterator() {
+                   return(new Iterator<Character>() {
+                           private int i = 0;
+                           
+                           public boolean hasNext() {
+                               return(i < parsed.size());
+                           }
+                           
+                           public Character next() {
+                               if(i >= parsed.size())
+                                   throw(new NoSuchElementException());
+                               Arg a = parsed.get(i++);
+                               arg = a.arg;
+                               return(a.ch);
+                           }
+                           
+                           public void remove() {
+                               throw(new UnsupportedOperationException());
+                           }
+                       });
+               }
+           });
+    }
+}
diff --git a/src/jagi/Utils.java b/src/jagi/Utils.java
new file mode 100644 (file)
index 0000000..ef4e02a
--- /dev/null
@@ -0,0 +1,85 @@
+package jagi;
+
+import java.util.*;
+import java.io.*;
+import java.nio.*;
+import java.nio.channels.*;
+
+public class Utils {
+    public static final java.nio.charset.Charset UTF8 = java.nio.charset.Charset.forName("UTF-8");
+    public static final java.nio.charset.Charset LATIN1 = java.nio.charset.Charset.forName("ISO-8859-1");
+    public static final java.nio.charset.Charset ASCII = java.nio.charset.Charset.forName("US-ASCII");
+
+    public static int read(ReadableByteChannel ch) throws IOException {
+       ByteBuffer buf = ByteBuffer.allocate(1);
+       while(true) {
+           int rv = ch.read(buf);
+           if(rv < 0)
+               return(-1);
+           else if(rv == 1)
+               return(buf.get(0) & 0xff);
+           else if(rv > 1)
+               throw(new AssertionError());
+       }
+    }
+
+    public static ByteBuffer readall(ReadableByteChannel ch, ByteBuffer dst) throws IOException {
+       while(dst.remaining() > 0)
+           ch.read(dst);
+       return(dst);
+    }
+
+    public static void writeall(WritableByteChannel ch, ByteBuffer src) throws IOException {
+       while(src.remaining() > 0)
+           ch.write(src);
+    }
+
+    public static void transfer(WritableByteChannel dst, ReadableByteChannel src) throws IOException {
+       ByteBuffer buf = ByteBuffer.allocate(65536);
+       while(true) {
+           buf.clear();
+           if(src.read(buf) < 0)
+               break;
+           buf.flip();
+           while(buf.remaining() > 0)
+               dst.write(buf);
+       }
+    }
+
+    public static String htmlquote(CharSequence text) {
+       StringBuilder buf = new StringBuilder();
+       for(int i = 0; i < text.length(); i++) {
+           char c = text.charAt(i);
+           switch(c) {
+           case '&': buf.append("&amp;"); break;
+           case '<': buf.append("&lt;"); break;
+           case '>': buf.append("&gt;"); break;
+           case '"': buf.append("&quot;"); break;
+           default: buf.append(c); break;
+           }
+       }
+       return(buf.toString());
+    }
+
+    public static Map<Object, Object> simpleerror(int code, CharSequence title, CharSequence msg) {
+       StringBuilder buf = new StringBuilder();
+       buf.append("<?xml version=\"1.0\" encoding=\"US-ASCII\"?>\n");
+       buf.append("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.1//EN\" \"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd\">\n");
+       buf.append("<html xmlns=\"http://www.w3.org/1999/xhtml\" xml:lang=\"en-US\">\n");
+       buf.append("<head>\n");
+       buf.append("<title>" + title + "</title>\n");
+       buf.append("</head>\n");
+       buf.append("<body>\n");
+       buf.append("<h1>" + title + "</h1>\n");
+       buf.append("<p>" + htmlquote(msg) + "</p>\n");
+       buf.append("</body>\n");
+       buf.append("</html>\n");
+       ByteBuffer out = ASCII.encode(CharBuffer.wrap(buf));
+       Map<Object, Object> resp = new HashMap<>();
+       resp.put("http.status", code + " " + title);
+       resp.put("http.Content-Type", "text/html; charset=us-ascii");
+       resp.put("http.Content-Length", Integer.toString(out.remaining()));
+       resp.put("jagi.output", out);
+       return(resp);
+    }
+}
diff --git a/src/jagi/fs/Bootstrap.java b/src/jagi/fs/Bootstrap.java
new file mode 100644 (file)
index 0000000..d24a258
--- /dev/null
@@ -0,0 +1,29 @@
+package jagi.fs;
+
+import java.util.function.*;
+import java.io.*;
+import jagi.*;
+
+public class Bootstrap {
+    private static void usage(PrintStream out) {
+       out.println("usage: jagi-fs [-h]");
+    }
+
+    public static Function wmain(String[] argv) {
+       PosixArgs opt = PosixArgs.getopt(argv, "h");
+       if(opt == null) {
+           usage(System.err);
+           System.exit(1);
+           return(null);
+       }
+       for(char c : opt.parsed()) {
+           switch(c) {
+           case 'h':
+               usage(System.out);
+               System.exit(0);
+               break;
+           }
+       }
+       return(new Handler());
+    }
+}
diff --git a/src/jagi/fs/Compiler.java b/src/jagi/fs/Compiler.java
new file mode 100644 (file)
index 0000000..dcfee03
--- /dev/null
@@ -0,0 +1,195 @@
+package jagi.fs;
+
+import java.util.*;
+import java.util.regex.*;
+import java.nio.file.*;
+import java.nio.file.attribute.*;
+import java.io.*;
+import java.net.*;
+import javax.tools.*;
+
+public class Compiler {
+    private final Map<Path, Module> modules = new HashMap<>();
+
+    public static class FilePart extends SimpleJavaFileObject {
+       public final Path file;
+       public final String clnm;
+       public final CharSequence src;
+
+       private static URI dummyuri(Path file, String clnm) {
+           String clp = clnm.replace('.', '/') + Kind.SOURCE.extension;
+           return(URI.create(file.toUri().toString() + "!/" + clp));
+       }
+
+       public FilePart(Path file, String clnm, CharSequence src) {
+           super(dummyuri(file, clnm), Kind.SOURCE);
+           this.file = file;
+           this.clnm = clnm;
+           this.src = src;
+       }
+
+       public CharSequence getCharContent(boolean ice) {
+           return(src);
+       }
+
+       private static final Pattern classpat = Pattern.compile("^((public|abstract)\\s+)*(class|interface)\\s+(\\S+)");
+       public static Collection<FilePart> split(Path file) throws IOException {
+           Collection<FilePart> ret = new ArrayList<>();
+           StringBuilder head = new StringBuilder();
+           StringBuilder cur = null;
+           String clnm = null;
+           try(BufferedReader fp = Files.newBufferedReader(file)) {
+               for(String ln = fp.readLine(); ln != null; ln = fp.readLine()) {
+                   Matcher m = classpat.matcher(ln);
+                   if(m.find()) {
+                       if(cur != null)
+                           ret.add(new FilePart(file, clnm, cur));
+                       clnm = m.group(4);
+                       cur = new StringBuilder();
+                       cur.append(head);
+                   }
+                   if(cur != null) {
+                       cur.append(ln); cur.append('\n');
+                   } else {
+                       head.append(ln); head.append('\n');
+                   }
+               }
+               if(cur != null)
+                   ret.add(new FilePart(file, clnm, cur));
+           }
+           return(ret);
+       }
+    }
+
+    public static class ClassOutput extends SimpleJavaFileObject {
+       public final String name;
+       private final ByteArrayOutputStream buf = new ByteArrayOutputStream();
+
+       public ClassOutput(String name) {
+           super(URI.create("mem://" + name), Kind.CLASS);
+           this.name = name;
+       }
+
+       public OutputStream openOutputStream() {
+           return(buf);
+       }
+    }
+
+    public static class FileContext extends ForwardingJavaFileManager<JavaFileManager> {
+       public final Collection<ClassOutput> output = new ArrayList<>();
+
+       public FileContext(JavaCompiler javac) {
+           super(javac.getStandardFileManager(null, null, null));
+       }
+
+       public JavaFileObject getJavaFileForOutput(Location location, String name, JavaFileObject.Kind kind, FileObject sibling) {
+           ClassOutput cl = new ClassOutput(name);
+           output.add(cl);
+           return(cl);
+       }
+    }
+
+    public static class CompilationException extends RuntimeException {
+       public final Path file;
+       private final List<Diagnostic<? extends JavaFileObject>> messages;
+
+       public CompilationException(Path file, List<Diagnostic<? extends JavaFileObject>> messages) {
+           this.file = file;
+           this.messages = messages;
+       }
+
+       public String getMessage() {
+           return(file + ": compilation failed");
+       }
+
+       public String messages() {
+           StringBuilder buf = new StringBuilder();
+           for(Diagnostic msg : messages)
+               buf.append(msg.toString() + "\n");
+           return(buf.toString());
+       }
+
+       public void printStackTrace(PrintStream out) {
+           out.print(messages());
+           super.printStackTrace(out);
+       }
+    }
+
+    public static Collection<ClassOutput> compile(Path file) throws IOException {
+       List<String> opt = Arrays.asList();
+       JavaCompiler javac = ToolProvider.getSystemJavaCompiler();
+       if(javac == null)
+           throw(new RuntimeException("no javac present"));
+       Collection<FilePart> files;
+       files = FilePart.split(file);
+       DiagnosticCollector<JavaFileObject> out = new DiagnosticCollector<>();
+       FileContext fs = new FileContext(javac);
+       JavaCompiler.CompilationTask job = javac.getTask(null, fs, out, opt, null, files);
+       if(!job.call())
+           throw(new CompilationException(file, out.getDiagnostics()));
+       return(fs.output);
+    }
+
+    public static class BufferedClassLoader extends ClassLoader {
+       public final Map<String, byte[]> contents;
+
+       public BufferedClassLoader(Collection<ClassOutput> contents) {
+           this.contents = new HashMap<>();
+           for(ClassOutput clc : contents)
+               this.contents.put(clc.name, clc.buf.toByteArray());
+       }
+
+       public Class<?> findClass(String name) throws ClassNotFoundException {
+           byte[] c = contents.get(name);
+           if(c == null)
+               throw(new ClassNotFoundException(name));
+           return(defineClass(name, c, 0, c.length));
+       }
+    }
+
+    public static class Module {
+       public final Path file;
+       private FileTime mtime = null;
+       private ClassLoader code = null;
+
+       private Module(Path file) {
+           this.file = file;
+       }
+
+       public void update() throws IOException {
+           synchronized(this) {
+               FileTime mtime = Files.getLastModifiedTime(file);
+               if((this.mtime == null) || (this.mtime.compareTo(mtime) < 0)) {
+                   code = new BufferedClassLoader(compile(file));
+                   this.mtime = mtime;
+               }
+           }
+       }
+
+       public ClassLoader code() {
+           if(code == null)
+               throw(new RuntimeException("module has not yet been updated"));
+           return(code);
+       }
+    }
+
+    public Module module(Path file) {
+       synchronized(modules) {
+           Module ret = modules.get(file);
+           if(ret == null)
+               modules.put(file, ret = new Module(file));
+           return(ret);
+       }
+    }
+
+    private static Compiler global = null;
+    public static Compiler get() {
+       if(global == null) {
+           synchronized(Compiler.class) {
+               if(global == null)
+                   global = new Compiler();
+           }
+       }
+       return(global);
+    }
+}
diff --git a/src/jagi/fs/Handler.java b/src/jagi/fs/Handler.java
new file mode 100644 (file)
index 0000000..0f61ecd
--- /dev/null
@@ -0,0 +1,112 @@
+package jagi.fs;
+
+import java.util.*;
+import java.util.function.*;
+import java.util.logging.*;
+import java.lang.reflect.*;
+import java.io.*;
+import java.nio.file.*;
+import jagi.*;
+
+public class Handler implements Function<Map<Object, Object>, Map<Object, Object>> {
+    private static final Logger log = Logger.getLogger("jagi-fs");
+    private Map<String, Function<Map<Object, Object>, Map<Object, Object>>> handlers = new HashMap<>();
+    private Map<String, Function<Map<Object, Object>, Map<Object, Object>>> exts = new HashMap<>();
+
+    @SuppressWarnings("unchecked")
+    private static Function<Map<Object, Object>, Map<Object, Object>> resolve(ClassLoader loader, String nm) {
+       Class<?> cl;
+       try {
+           cl = loader.loadClass(nm);
+       } catch(ClassNotFoundException e) {
+           try {
+               cl = loader.loadClass(nm + ".Bootstrap");
+           } catch(ClassNotFoundException e2) {
+               throw(new RuntimeException("could not find handler class or package: " + nm, e2));
+           }
+       }
+       Method wmain;
+       try {
+           wmain = cl.getDeclaredMethod("wmain", String[].class);
+           int mod = wmain.getModifiers();
+           if(((mod & Modifier.STATIC) == 0) || ((mod & Modifier.PUBLIC) == 0))
+               throw(new NoSuchMethodException());
+       } catch(NoSuchMethodException e) {
+           throw(new RuntimeException("could not find wmain method in " + cl.getName(), e));
+       }
+       Object handler;
+       try {
+           handler = wmain.invoke(null, new Object[] {new String[] {}});
+       } catch(IllegalAccessException e) {
+           throw(new RuntimeException("could not call wmain in " + cl.getName(), e));
+       } catch(InvocationTargetException e) {
+           throw(new RuntimeException("wmain in " + cl.getName() + " failed", e.getCause()));
+       }
+       if(!(handler instanceof Function))
+           throw(new RuntimeException("wmain in " + cl.getName() + " returned " + ((handler == null) ? "null" : ("a " + handler.getClass()))));
+       return((Function<Map<Object, Object>, Map<Object, Object>>)handler);
+    }
+
+    public Function<Map<Object, Object>, Map<Object, Object>> resolve(String nm) {
+       synchronized(handlers) {
+           Function<Map<Object, Object>, Map<Object, Object>> handler = handlers.get(nm);
+           if(handler == null)
+               handlers.put(nm, handler = resolve(Thread.currentThread().getContextClassLoader(), nm));
+           return(handler);
+       }
+    }
+
+    public void addext(String ext, String name) {
+       addext(ext, resolve(name));
+    }
+
+    public void addext(String ext, Function<Map<Object, Object>, Map<Object, Object>> handler) {
+       Map<String, Function<Map<Object, Object>, Map<Object, Object>>> exts = new HashMap<>(this.exts);
+       synchronized(exts) {
+           exts.put(ext, handler);
+       }
+       this.exts = exts;
+    }
+
+    {
+       addext("jagi", new JavaHandler());
+    }
+
+    public Map<Object, Object> apply(Map<Object, Object> req) {
+       String filename = (String)req.get("SCRIPT_FILENAME");
+       if(filename == null) {
+           log.warning("jagi-fs called without SCRIPT_FILENAME set");
+           return(Utils.simpleerror(500, "Internal Error", "The server is erroneously configured"));
+       }
+       Path path = Paths.get(filename);
+       if(!Files.isReadable(path)) {
+           log.warning(path + ": not readable");
+           return(Utils.simpleerror(500, "Internal Error", "The server is erroneously configured"));
+       }
+       String hname = (String)req.get("HTTP_X_ASH_JAVA_HANDLER");
+       if(hname != null) {
+           Function<Map<Object, Object>, Map<Object, Object>> handler;
+           try {
+               handler = resolve(hname);
+           } catch(Exception e) {
+               log.log(Level.WARNING, "could not load handler " + hname, e);
+               return(Utils.simpleerror(500, "Internal Error", "The server is erroneously configured"));
+           }
+           return(handler.apply(req));
+       } else {
+           String base = path.getFileName().toString();
+           int p = base.lastIndexOf('.');
+           if(p < 0) {
+               log.warning(path + ": no file extension");
+               return(Utils.simpleerror(500, "Internal Error", "The server is erroneously configured"));
+           }
+           String ext = base.substring(p + 1);
+           Function<Map<Object, Object>, Map<Object, Object>> handler = exts.get(ext);
+           if(handler == null) {
+               log.warning("non-registered file extension: " + ext);
+               return(Utils.simpleerror(500, "Internal Error", "The server is erroneously configured"));
+           }
+           return(handler.apply(req));
+       }
+    }
+}
diff --git a/src/jagi/fs/JavaHandler.java b/src/jagi/fs/JavaHandler.java
new file mode 100644 (file)
index 0000000..111264f
--- /dev/null
@@ -0,0 +1,102 @@
+package jagi.fs;
+
+import jagi.*;
+import java.util.*;
+import java.util.function.*;
+import java.util.logging.*;
+import java.lang.reflect.*;
+import java.nio.file.*;
+
+public class JavaHandler implements Function<Map<Object, Object>, Map<Object, Object>> {
+    private static final Logger log = Logger.getLogger("jagi-fs");
+    private final Map<ClassLoader, Function<Map<Object, Object>, Map<Object, Object>>> handlers = new WeakHashMap<>();
+
+    public static class HandlerException extends RuntimeException {
+       public final Path file;
+
+       public HandlerException(Path file, String msg, Throwable cause) {
+           super(msg, cause);
+           this.file = file;
+       }
+       public HandlerException(Path file, String msg) {
+           this(file, msg, null);
+       }
+
+       public String getMessage() {
+           return(file + ": " + super.getMessage());
+       }
+    }
+
+    @SuppressWarnings("unchecked")
+    private static Function<Map<Object, Object>, Map<Object, Object>> makehandler(Compiler.Module mod) {
+       Class<?> main;
+       try {
+           main = mod.code().loadClass("Main");
+       } catch(ClassNotFoundException e) {
+           throw(new HandlerException(mod.file, "no Main class"));
+       }
+       try {
+           Method wmain = main.getDeclaredMethod("wmain", String[].class);
+           int attr = wmain.getModifiers();
+           if(((attr & Modifier.STATIC) == 0) || ((attr & Modifier.PUBLIC) == 0))
+               throw(new NoSuchMethodException());
+           Object handler = wmain.invoke(null, new Object[] {new String[] {}});
+           if(!(handler instanceof Function))
+               throw(new HandlerException(mod.file, "wmain in " + main.getName() + " returned " + ((handler == null) ? "null" : ("a " + handler.getClass()))));
+           return((Function<Map<Object, Object>, Map<Object, Object>>)handler);
+       } catch(IllegalAccessException e) {
+           throw(new HandlerException(mod.file, "could not call wmain", e));
+       } catch(InvocationTargetException e) {
+           throw(new HandlerException(mod.file, "wmain failed", e.getCause()));
+       } catch(NoSuchMethodException e) {
+       }
+       if(Function.class.isAssignableFrom(main)) {
+           try {
+               Constructor<? extends Function> cons = main.asSubclass(Function.class).getConstructor();
+               Function handler = cons.newInstance();
+               return((Function<Map<Object, Object>, Map<Object, Object>>)handler);
+           } catch(NoSuchMethodException e) {
+           } catch(InvocationTargetException e) {
+               throw(new HandlerException(mod.file, "constructor failed", e.getCause()));
+           } catch(ReflectiveOperationException e) {
+               throw(new HandlerException(mod.file, "could not construct Main", e));
+           }
+       }
+       throw(new HandlerException(mod.file, "no wmain and not directly applicable"));
+    }
+
+    private Function<Map<Object, Object>, Map<Object, Object>> gethandler(Compiler.Module mod) {
+       ClassLoader code = mod.code();
+       synchronized(handlers) {
+           Function<Map<Object, Object>, Map<Object, Object>> ret = handlers.get(code);
+           if(ret == null)
+               handlers.put(code, ret = makehandler(mod));
+           return(ret);
+       }
+    }
+
+    public Map<Object, Object> apply(Map<Object, Object> req) {
+       Compiler.Module mod = Compiler.get().module(Paths.get((String)req.get("SCRIPT_FILENAME")));
+       try {
+           mod.update();
+       } catch(Compiler.CompilationException e) {
+           log.warning(String.format("Could not compile %s:\n%s", mod.file, e.messages()));
+           return(Utils.simpleerror(500, "Internal Error", "Could not load JAGI handler"));
+       } catch(Exception e) {
+           log.log(Level.WARNING, String.format("Error occurred when loading %s", mod.file), e);
+           return(Utils.simpleerror(500, "Internal Error", "Could not load JAGI handler"));
+       }
+       Function<Map<Object, Object>, Map<Object, Object>> handler;
+       try {
+           handler = gethandler(mod);
+       } catch(HandlerException e) {
+           Throwable cause = e.getCause();
+           if(cause != null)
+               log.log(Level.WARNING, cause, e::getMessage);
+           else
+               log.log(Level.WARNING, e::getMessage);
+           return(Utils.simpleerror(500, "Internal Error", "Invalid JAGI handler"));
+       }
+       return(handler.apply(req));
+    }
+}
diff --git a/src/jagi/scgi/Bootstrap.java b/src/jagi/scgi/Bootstrap.java
new file mode 100644 (file)
index 0000000..accac96
--- /dev/null
@@ -0,0 +1,172 @@
+package jagi.scgi;
+
+import jagi.*;
+import java.lang.reflect.*;
+import java.util.*;
+import java.util.function.*;
+import java.io.*;
+import java.net.*;
+import java.nio.channels.*;
+
+public class Bootstrap {
+    private static InetSocketAddress resolveinaddr(String spec) {
+       int p = spec.indexOf(':');
+       SocketAddress bind;
+       if(p >= 0) {
+           InetAddress host;
+           try {
+               if(spec.charAt(0) == '[') {
+                   p = spec.indexOf(']');
+                   if((p < 0) || (spec.charAt(p + 1) != ':'))
+                       throw(new IllegalArgumentException("invalid address syntax: " + spec));
+                   host = InetAddress.getByName(spec.substring(1, p));
+                   p++;
+               } else {
+                   host = InetAddress.getByName(spec.substring(0, p));
+               }
+           } catch(UnknownHostException e) {
+               throw(new IllegalArgumentException("could not resolve inet host: " + spec, e));
+           }
+           try {
+               return(new InetSocketAddress(host, Integer.parseInt(spec.substring(p + 1))));
+           } catch(NumberFormatException e) {
+               throw(new IllegalArgumentException("not a valid port number: " + spec.substring(p + 1), e));
+           }
+       } else {
+           try {
+               return(new InetSocketAddress(Integer.parseInt(spec)));
+           } catch(NumberFormatException e) {
+               throw(new IllegalArgumentException("not a valid port number: " + spec, e));
+           }
+       }
+    }
+
+    private static ServerSocketChannel tcplisten(String spec) {
+       SocketAddress bind;
+       try {
+           bind = resolveinaddr(spec);
+       } catch(IllegalArgumentException e) {
+           System.err.println("scgi-jagi: " + e.getMessage());
+           System.exit(1);
+           return(null);
+       }
+       try {
+           ServerSocketChannel sk = ServerSocketChannel.open();
+           sk.bind(bind);
+           return(sk);
+       } catch(IOException e) {
+           System.err.println("scgi-jagi: could not create TCP socket: " + e.getMessage());
+           System.exit(1);
+           return(null);
+       }
+    }
+
+    private static ServerSocketChannel getstdin() {
+       Channel stdin;
+       try {
+           stdin = System.inheritedChannel();
+       } catch(IOException e) {
+           System.err.println("scgi-jagi: could not get stdin channel: " + e.getMessage());
+           System.exit(1);
+           return(null);
+       }
+       if(!(stdin instanceof ServerSocketChannel)) {
+           System.err.println("scgi-jagi: stdin is not a listening socket");
+           System.exit(1);
+           return(null);
+       }
+       return((ServerSocketChannel)stdin);
+    }
+
+    private static Function gethandler(ClassLoader loader, String nm, String... args) {
+       Class<?> cl;
+       try {
+           cl = loader.loadClass(nm);
+       } catch(ClassNotFoundException e) {
+           try {
+               cl = loader.loadClass(nm + ".Bootstrap");
+           } catch(ClassNotFoundException e2) {
+               System.err.println("scgi-jagi: could not find handler class or package: " + nm);
+               System.exit(1);
+               return(null);
+           }
+       }
+       Method wmain;
+       try {
+           wmain = cl.getDeclaredMethod("wmain", String[].class);
+           int mod = wmain.getModifiers();
+           if(((mod & Modifier.STATIC) == 0) || ((mod & Modifier.PUBLIC) == 0))
+               throw(new NoSuchMethodException());
+       } catch(NoSuchMethodException e) {
+           System.err.println("scgi-jagi: could not find wmain method in " + cl.getName());
+           System.exit(1);
+           return(null);
+       }
+       Object handler;
+       try {
+           handler = wmain.invoke(null, new Object[] {args});
+       } catch(IllegalAccessException e) {
+           System.err.println("scgi-jagi: could not call wmain in " + cl.getName());
+           System.exit(1);
+           return(null);
+       } catch(InvocationTargetException e) {
+           System.err.println("scgi-jagi: wmain in " + cl.getName() + " failed");
+           e.printStackTrace(System.err);
+           System.exit(1);
+           return(null);
+       }
+       if(!(handler instanceof Function)) {
+           System.err.println("scgi-jagi: wmain in " + cl.getName() + " returned " + ((handler == null) ? "null" : ("a " + handler.getClass())));
+           System.exit(1);
+           return(null);
+       }
+       return((Function)handler);
+    }
+
+    private static void usage(PrintStream out) {
+       out.println("usage: jagi.jar [-h] [-T [HOST:]PORT] HANDLER-CLASS [ARGS...]");
+    }
+
+    public static void main(String[] args) {
+       PosixArgs opt = PosixArgs.getopt(args, "hT:");
+       if(opt == null) {
+           usage(System.err);
+           System.exit(1);
+           return;
+       }
+       String tcpspec = null;
+       ClassLoader loader = Bootstrap.class.getClassLoader();
+       for(char c : opt.parsed()) {
+           switch(c) {
+           case 'h':
+               usage(System.out);
+               System.exit(0);
+               break;
+           case 'T':
+               tcpspec = opt.arg;
+               break;
+           }
+       }
+       if(opt.rest.length < 1) {
+           usage(System.err);
+           System.exit(1);
+           return;
+       }
+       Function handler = gethandler(loader, opt.rest[0], Arrays.copyOfRange(opt.rest, 1, opt.rest.length));
+       ServerSocketChannel sk;
+       if(tcpspec != null) {
+           sk = tcplisten(tcpspec);
+       } else {
+           sk = getstdin();
+       }
+       Runnable server = new SimpleServer(sk, handler);
+       try {
+           server.run();
+       } catch(Throwable e) {
+           System.err.println("scgi-jagi: server exited abnormally");
+           e.printStackTrace();
+           System.exit(1);
+       }
+       System.exit(0);
+    }
+}
diff --git a/src/jagi/scgi/Jagi.java b/src/jagi/scgi/Jagi.java
new file mode 100644 (file)
index 0000000..758afdc
--- /dev/null
@@ -0,0 +1,44 @@
+package jagi.scgi;
+
+import jagi.*;
+import java.util.*;
+import java.io.*;
+import java.nio.*;
+import java.nio.channels.*;
+import java.nio.charset.*;
+
+public class Jagi {
+    public static void decodehead(Map<? super String, ? super String> into, Map<ByteBuffer, ByteBuffer> head, Charset coding) throws CharacterCodingException {
+       for(Map.Entry<ByteBuffer, ByteBuffer> h : head.entrySet())
+           into.put(coding.newDecoder().decode(h.getKey()).toString(), coding.decode(h.getValue()).toString());
+    }
+
+    public static Map<Object, Object> mkenv(ReadableByteChannel sk) throws IOException {
+       Map<ByteBuffer, ByteBuffer> rawhead = Scgi.readhead(sk);
+       Map<Object, Object> env;
+       try {
+           env = new HashMap<>();
+           decodehead(env, rawhead, Utils.UTF8);
+           env.put("jagi.uri_encoding", "utf-8");
+       } catch(CharacterCodingException e) {
+           env = new HashMap<>();
+           decodehead(env, rawhead, Utils.LATIN1);
+           env.put("jagi.uri_encoding", "latin-1");
+       }
+       env.put("jagi.version.major", 1);
+       env.put("jagi.version.minor", 0);
+       if(env.containsKey("HTTP_X_ASH_PROTOCOL"))
+           env.put("jagi.url_scheme", env.get("HTTP_X_ASH_PROTOCOL"));
+       else if(env.containsKey("HTTPS"))
+           env.put("jagi.url_scheme", "https");
+       else
+           env.put("jagi.url_scheme", "http");
+       env.put("jagi.input", sk);
+       env.put("jagi.errors", System.err);
+       env.put("jagi.multithread", true);
+       env.put("jagi.multiprocess", false);
+       env.put("jagi.run_once", false);
+       env.put("jagi.cleanup", new HashSet<>());
+       return(env);
+    }
+}
diff --git a/src/jagi/scgi/Scgi.java b/src/jagi/scgi/Scgi.java
new file mode 100644 (file)
index 0000000..5e219b0
--- /dev/null
@@ -0,0 +1,48 @@
+package jagi.scgi;
+
+import jagi.*;
+import java.util.*;
+import java.io.*;
+import java.nio.*;
+import java.nio.channels.*;
+
+public class Scgi {
+    public static ByteBuffer readns(ReadableByteChannel sk) throws IOException {
+       int hln = 0;
+       while(true) {
+           int c = Utils.read(sk);
+           if(c == ':')
+               break;
+           else if((c >= '0') && (c <= '9'))
+               hln = (hln * 10) + (c - '0');
+           else if(c < 0)
+               throw(new IOException("unexpected eof in netstring header"));
+           else
+               throw(new IOException("invalid netstring length byte: " + (c & 0xff)));
+       }
+       ByteBuffer data = Utils.readall(sk, ByteBuffer.allocate(hln));
+       if(Utils.read(sk) != ',')
+           throw(new IOException("non-terminated netstring"));
+       return(data);
+    }
+
+    public static Map<ByteBuffer, ByteBuffer> readhead(ReadableByteChannel sk) throws IOException {
+       Map<ByteBuffer, ByteBuffer> ret = new HashMap<>();
+       ByteBuffer ns = readns(sk);
+       ByteBuffer k = null;
+       for(int i = 0, p = 0; i < ns.limit(); i++) {
+           if(ns.get(i) == 0) {
+               ByteBuffer s = ns.duplicate();
+               s.position(p).limit(i);
+               if(k == null) {
+                   k = s;
+               } else {
+                   ret.put(k, s);
+                   k = null;
+               }
+               p = i + 1;
+           }
+       }
+       return(ret);
+    }
+}
diff --git a/src/jagi/scgi/SimpleServer.java b/src/jagi/scgi/SimpleServer.java
new file mode 100644 (file)
index 0000000..7e6dec1
--- /dev/null
@@ -0,0 +1,157 @@
+package jagi.scgi;
+
+import jagi.*;
+import java.util.*;
+import java.util.function.*;
+import java.io.*;
+import java.nio.*;
+import java.nio.channels.*;
+
+public class SimpleServer implements Runnable {
+    private final ServerSocketChannel sk;
+    private final Function handler;
+
+    public SimpleServer(ServerSocketChannel sk, Function handler) {
+       this.sk = sk;
+       this.handler = handler;
+    }
+
+    private void respond(SocketChannel cl, String status, Map resp) throws IOException {
+       Object output = resp.get("jagi.output");
+       try {
+           BufferedWriter fm = new BufferedWriter(Channels.newWriter(cl, Utils.UTF8.newEncoder(), -1));
+           fm.write("Status: ");
+           fm.write(status);
+           fm.write("\n");
+           for(Iterator it = resp.entrySet().iterator(); it.hasNext();) {
+               Map.Entry ent = (Map.Entry)it.next();
+               Object val = ent.getValue();
+               if((ent.getKey() instanceof String) && (val != null)) {
+                   String key = (String)ent.getKey();
+                   if(key.startsWith("http.")) {
+                       String head = key.substring(5);
+                       if(head.equalsIgnoreCase("status"))
+                           continue;
+                       if(val instanceof Collection) {
+                           for(Object part : (Collection)val) {
+                               fm.write(head);
+                               fm.write(": ");
+                               fm.write(part.toString());
+                               fm.write("\n");
+                           }
+                       } else {
+                           fm.write(head);
+                           fm.write(": ");
+                           fm.write(val.toString());
+                           fm.write("\n");
+                       }
+                   }
+               }
+           }
+           fm.write("\n");
+           fm.flush();
+           if(output == null) {
+           } else if(output instanceof byte[]) {
+               Utils.writeall(cl, ByteBuffer.wrap((byte[])output));
+           } else if(output instanceof ByteBuffer) {
+               Utils.writeall(cl, (ByteBuffer)output);
+           } else if(output instanceof String) {
+               Utils.writeall(cl, ByteBuffer.wrap(((String)output).getBytes(Utils.UTF8)));
+           } else if(output instanceof CharSequence) {
+               Utils.writeall(cl, Utils.UTF8.encode(CharBuffer.wrap((CharSequence)output)));
+           } else if(output instanceof InputStream) {
+               Utils.transfer(cl, Channels.newChannel((InputStream)output));
+           } else if(output instanceof ReadableByteChannel) {
+               Utils.transfer(cl, (ReadableByteChannel)output);
+           } else {
+               throw(new  IllegalArgumentException("response-body: " + String.valueOf(output)));
+           }
+       } finally {
+           if(output instanceof Closeable)
+               ((Closeable)output).close();
+       }
+    }
+
+    private void feedinput(SocketChannel cl, Map resp) throws IOException {
+       Object sink = resp.get("jagi.input-sink");
+       try {
+           if(sink instanceof OutputStream) {
+               Utils.transfer(Channels.newChannel((OutputStream)sink), cl);
+           } else if(sink instanceof WritableByteChannel) {
+               Utils.transfer((WritableByteChannel)sink, cl);
+           } else {
+               throw(new IllegalArgumentException("input-sink: " + String.valueOf(sink)));
+           }
+       } finally {
+           if(sink instanceof Closeable)
+               ((Closeable)sink).close();
+       }
+    }
+
+    @SuppressWarnings("unchecked")
+    private void serve(SocketChannel cl) throws IOException {
+       Function handler = this.handler;
+       Map<Object, Object> env = Jagi.mkenv(cl);
+       Throwable error = null;
+       try {
+           while(true) {
+               Map resp = (Map)handler.apply(env);
+               String st;
+               if((st = (String)resp.get("jagi.status")) != null) {
+                   handler = (Function)resp.get("jagi.next");
+                   switch(st) {
+                   case "feed-input":
+                       feedinput(cl, resp);
+                       break;
+                   default:
+                       throw(new IllegalArgumentException(st));
+                   }
+               } else if((st = (String)resp.get("http.status")) != null) {
+                   respond(cl, st, resp);
+                   break;
+               }
+           }
+       } catch(Throwable t) {
+           error = t;
+           throw(t);
+       } finally {
+           Collection cleanup = (Collection)env.get("jagi.cleanup");
+           RuntimeException ce = null;
+           for(Object obj : cleanup) {
+               if(obj instanceof AutoCloseable) {
+                   try {
+                       ((AutoCloseable)obj).close();
+                   } catch(Exception e) {
+                       if(error == null)
+                           error = ce = new RuntimeException("error(s) occurred during cleanup");
+                       error.addSuppressed(e);
+                   }
+               }
+           }
+           if(ce != null)
+               throw(ce);
+       }
+    }
+
+    public void run() {
+       while(true) {
+           SocketChannel cl;
+           try {
+               cl = sk.accept();
+           } catch(IOException e) {
+               throw(new RuntimeException(e));
+           }
+           try {
+               serve(cl);
+           } catch(Exception e) {
+               e.printStackTrace();
+           } finally {
+               try {
+                   cl.close();
+               } catch(IOException e) {
+                   e.printStackTrace();
+               }
+           }
+       }
+    }
+}