--- /dev/null
+<?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>
--- /dev/null
+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());
+ }
+ });
+ }
+ });
+ }
+}
--- /dev/null
+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("&"); break;
+ case '<': buf.append("<"); break;
+ case '>': buf.append(">"); break;
+ case '"': buf.append("""); 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);
+ }
+}
--- /dev/null
+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());
+ }
+}
--- /dev/null
+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);
+ }
+}
--- /dev/null
+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));
+ }
+ }
+}
--- /dev/null
+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));
+ }
+}
--- /dev/null
+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);
+ }
+}
--- /dev/null
+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);
+ }
+}
--- /dev/null
+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);
+ }
+}
--- /dev/null
+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();
+ }
+ }
+ }
+ }
+}