From 49ccd711f15e0fbb64afdef0e6698aca14ecbc79 Mon Sep 17 00:00:00 2001 From: Fredrik Tolf Date: Thu, 10 Feb 2022 17:48:46 +0100 Subject: [PATCH 1/1] Initial commit. --- .gitignore | 1 + build.xml | 30 +++++++ src/jagi/PosixArgs.java | 100 +++++++++++++++++++++ src/jagi/Utils.java | 85 ++++++++++++++++++ src/jagi/fs/Bootstrap.java | 29 ++++++ src/jagi/fs/Compiler.java | 195 ++++++++++++++++++++++++++++++++++++++++ src/jagi/fs/Handler.java | 112 +++++++++++++++++++++++ src/jagi/fs/JavaHandler.java | 102 +++++++++++++++++++++ src/jagi/scgi/Bootstrap.java | 172 +++++++++++++++++++++++++++++++++++ src/jagi/scgi/Jagi.java | 44 +++++++++ src/jagi/scgi/Scgi.java | 48 ++++++++++ src/jagi/scgi/SimpleServer.java | 157 ++++++++++++++++++++++++++++++++ 12 files changed, 1075 insertions(+) create mode 100644 .gitignore create mode 100644 build.xml create mode 100644 src/jagi/PosixArgs.java create mode 100644 src/jagi/Utils.java create mode 100644 src/jagi/fs/Bootstrap.java create mode 100644 src/jagi/fs/Compiler.java create mode 100644 src/jagi/fs/Handler.java create mode 100644 src/jagi/fs/JavaHandler.java create mode 100644 src/jagi/scgi/Bootstrap.java create mode 100644 src/jagi/scgi/Jagi.java create mode 100644 src/jagi/scgi/Scgi.java create mode 100644 src/jagi/scgi/SimpleServer.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/build diff --git a/build.xml b/build.xml new file mode 100644 index 0000000..e805cd6 --- /dev/null +++ b/build.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/jagi/PosixArgs.java b/src/jagi/PosixArgs.java new file mode 100644 index 0000000..fa6d28b --- /dev/null +++ b/src/jagi/PosixArgs.java @@ -0,0 +1,100 @@ +package jagi; + +import java.util.*; + +public class PosixArgs { + private List 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(); + } + + public static PosixArgs getopt(String[] argv, int start, String desc) { + PosixArgs ret = new PosixArgs(); + List fl = new ArrayList(), fla = new ArrayList(); + List rest = new ArrayList(); + 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 parsed() { + return(new Iterable() { + public Iterator iterator() { + return(new Iterator() { + 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 index 0000000..ef4e02a --- /dev/null +++ b/src/jagi/Utils.java @@ -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("&"); break; + case '<': buf.append("<"); break; + case '>': buf.append(">"); break; + case '"': buf.append("""); break; + default: buf.append(c); break; + } + } + return(buf.toString()); + } + + public static Map simpleerror(int code, CharSequence title, CharSequence msg) { + StringBuilder buf = new StringBuilder(); + buf.append("\n"); + buf.append("\n"); + buf.append("\n"); + buf.append("\n"); + buf.append("" + title + "\n"); + buf.append("\n"); + buf.append("\n"); + buf.append("

" + title + "

\n"); + buf.append("

" + htmlquote(msg) + "

\n"); + buf.append("\n"); + buf.append("\n"); + ByteBuffer out = ASCII.encode(CharBuffer.wrap(buf)); + Map 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 index 0000000..d24a258 --- /dev/null +++ b/src/jagi/fs/Bootstrap.java @@ -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 index 0000000..dcfee03 --- /dev/null +++ b/src/jagi/fs/Compiler.java @@ -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 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 split(Path file) throws IOException { + Collection 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 { + public final Collection 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> messages; + + public CompilationException(Path file, List> 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 compile(Path file) throws IOException { + List opt = Arrays.asList(); + JavaCompiler javac = ToolProvider.getSystemJavaCompiler(); + if(javac == null) + throw(new RuntimeException("no javac present")); + Collection files; + files = FilePart.split(file); + DiagnosticCollector 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 contents; + + public BufferedClassLoader(Collection 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 index 0000000..0f61ecd --- /dev/null +++ b/src/jagi/fs/Handler.java @@ -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> { + private static final Logger log = Logger.getLogger("jagi-fs"); + private Map, Map>> handlers = new HashMap<>(); + private Map, Map>> exts = new HashMap<>(); + + @SuppressWarnings("unchecked") + private static Function, Map> 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>)handler); + } + + public Function, Map> resolve(String nm) { + synchronized(handlers) { + Function, Map> 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> handler) { + Map, Map>> exts = new HashMap<>(this.exts); + synchronized(exts) { + exts.put(ext, handler); + } + this.exts = exts; + } + + { + addext("jagi", new JavaHandler()); + } + + public Map apply(Map 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> 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> 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 index 0000000..111264f --- /dev/null +++ b/src/jagi/fs/JavaHandler.java @@ -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> { + private static final Logger log = Logger.getLogger("jagi-fs"); + private final Map, Map>> 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> 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>)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 cons = main.asSubclass(Function.class).getConstructor(); + Function handler = cons.newInstance(); + return((Function, Map>)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> gethandler(Compiler.Module mod) { + ClassLoader code = mod.code(); + synchronized(handlers) { + Function, Map> ret = handlers.get(code); + if(ret == null) + handlers.put(code, ret = makehandler(mod)); + return(ret); + } + } + + public Map apply(Map 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> 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 index 0000000..accac96 --- /dev/null +++ b/src/jagi/scgi/Bootstrap.java @@ -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 index 0000000..758afdc --- /dev/null +++ b/src/jagi/scgi/Jagi.java @@ -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 into, Map head, Charset coding) throws CharacterCodingException { + for(Map.Entry h : head.entrySet()) + into.put(coding.newDecoder().decode(h.getKey()).toString(), coding.decode(h.getValue()).toString()); + } + + public static Map mkenv(ReadableByteChannel sk) throws IOException { + Map rawhead = Scgi.readhead(sk); + Map 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 index 0000000..5e219b0 --- /dev/null +++ b/src/jagi/scgi/Scgi.java @@ -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 readhead(ReadableByteChannel sk) throws IOException { + Map 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 index 0000000..7e6dec1 --- /dev/null +++ b/src/jagi/scgi/SimpleServer.java @@ -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 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(); + } + } + } + } +} -- 2.11.0