From 3e20c35c34d322ae53cf9feb005e6404f9cdc0a5 Mon Sep 17 00:00:00 2001 From: Fredrik Tolf Date: Fri, 18 Feb 2022 15:50:08 +0100 Subject: [PATCH] Initial commit. --- .gitignore | 1 + build.xml | 26 ++++++++ src/jrw/Environment.java | 15 +++++ src/jrw/FormData.java | 169 +++++++++++++++++++++++++++++++++++++++++++++++ src/jrw/Handler.java | 7 ++ src/jrw/Http.java | 7 ++ src/jrw/JagiWrapper.java | 18 +++++ src/jrw/Request.java | 50 ++++++++++++++ src/jrw/Restart.java | 18 +++++ 9 files changed, 311 insertions(+) create mode 100644 .gitignore create mode 100644 build.xml create mode 100644 src/jrw/Environment.java create mode 100644 src/jrw/FormData.java create mode 100644 src/jrw/Handler.java create mode 100644 src/jrw/Http.java create mode 100644 src/jrw/JagiWrapper.java create mode 100644 src/jrw/Request.java create mode 100644 src/jrw/Restart.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..eb35cbf --- /dev/null +++ b/build.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/jrw/Environment.java b/src/jrw/Environment.java new file mode 100644 index 0000000..f0f1347 --- /dev/null +++ b/src/jrw/Environment.java @@ -0,0 +1,15 @@ +package jrw; + +import java.util.*; + +public class Environment { + public static Map dispatch(Handler handler, Request req) { + while(true) { + try { + return(handler.handle(req)); + } catch(Restart r) { + handler = r; + } + } + } +} diff --git a/src/jrw/FormData.java b/src/jrw/FormData.java new file mode 100644 index 0000000..267aa6f --- /dev/null +++ b/src/jrw/FormData.java @@ -0,0 +1,169 @@ +package jrw; + +import java.util.*; +import java.util.function.*; +import java.io.*; +import java.nio.*; +import java.nio.channels.*; + +public class FormData extends HashMap { + public static final int MAX_LENGTH = 1 << 20; + + private static int htoi(byte hex) { + if((hex >= '0') && (hex <= '9')) + return(hex - '0'); + if((hex >= 'A') && (hex <= 'F')) + return(hex - 'A' + 10); + if((hex >= 'a') && (hex <= 'f')) + return(hex - 'a' + 10); + return(0); + } + + private static ByteBuffer unquoteb(ByteBuffer part) { + ByteBuffer ret = ByteBuffer.allocate(part.remaining()); + while(part.remaining() > 0) { + int b = part.get() & 0xff; + if((b == '%') && (part.remaining() >= 2)) { + int n1 = htoi(part.get()), n2 = htoi(part.get()); + ret.put((byte)((n1 << 4) | n2)); + } else { + ret.put((byte)b); + } + } + ret.flip(); + return(ret); + } + + private static String unquote(ByteBuffer part) { + ByteBuffer dec = unquoteb(part); + try { + return(Http.UTF8.newDecoder().decode(dec.duplicate()).toString()); + } catch(java.nio.charset.CharacterCodingException e) { + return(Http.LATIN1.decode(dec).toString()); + } + } + + private static String unquote(String part) { + if(part.indexOf('%') < 0) + return(part); + return(unquote(Http.UTF8.encode(CharBuffer.wrap(part)))); + } + + public static void parse(Map buf, ByteBuffer data) { + int p = data.position(), p2, p3; + while(p < data.limit()) { + for(p2 = p; (p2 < data.limit()) && (data.get(p2) != '&'); p2++); + for(p3 = p; (p3 < p2) && (data.get(p3) != '='); p3++); + if(p3 < p2) { + buf.put(unquote((ByteBuffer)data.duplicate().position(p).limit(p3)), + unquote((ByteBuffer)data.duplicate().position(p3 + 1).limit(p2))); + } + p = p2 + 1; + } + } + + public static void parse(Map buf, String data) { + int p = 0; + while(true) { + int p2 = data.indexOf('&', p); + String part = (p2 < 0) ? data.substring(p) : data.substring(p, p2); + int p3 = part.indexOf('='); + if(p3 >= 0) + buf.put(unquote(part.substring(0, p3)), unquote(part.substring(p3 + 1))); + if(p2 < 0) + break; + p = p2 + 1; + } + } + + public static FormData read(Request req) { + FormData ret = new FormData(); + String query = (String)req.env.get("QUERY_STRING"); + if(query != null) + parse(ret, query); + if(req.ihead("Content-Type", "").equals("application/x-www-form-urlencoded")) { + int max = MAX_LENGTH; + String clen = req.ihead("Content-Length", null); + if(clen != null) { + try { + max = Math.min(max, Integer.parseInt(clen)); + } catch(NumberFormatException e) { + } + } + ReadableByteChannel in = (ReadableByteChannel)req.env.get("jagi.input"); + if(in instanceof SelectableChannel) { + try { + ((SelectableChannel)in).configureBlocking(true); + } catch(IOException e) { + } + } + ByteBuffer buf = ByteBuffer.allocate(65536); + while(buf.position() < max) { + if(buf.remaining() == 0) { + ByteBuffer n = ByteBuffer.allocate(Math.min(buf.capacity() * 2, max)); + buf.flip(); + n.put(buf); + buf = n; + } + try { + int rv = in.read(buf); + if(rv <= 0) + break; + } catch(IOException e) { + break; + } + } + buf.flip(); + parse(ret, buf); + } + return(ret); + } + + public static FormData get(Request req) { + FormData ret = (FormData)req.env.get(FormData.class); + if(ret == null) + req.env.put(FormData.class, ret = read(req)); + return(ret); + } + + static class Collector extends ByteArrayOutputStream { + final FormData form = new FormData(); + final Request req; + + Collector(Request req) { + this.req = req; + req.env.put(FormData.class, this.form); + String query = (String)req.env.get("QUERY_STRING"); + if(query != null) + parse(form, query); + } + + public void write(int b) { + if(count < MAX_LENGTH) + super.write(b); + } + + public void write(byte[] buf, int off, int len) { + len = Math.min(len, MAX_LENGTH - count); + if(len > 0) + super.write(buf, off, len); + } + + public void close() { + } + } + + public static Map feed(Request req, Handler next) { + Map resp = new HashMap<>(); + if(req.ihead("Content-Type", "").equals("application/x-www-form-urlencoded")) { + resp.put("jagi.status", "feed-input"); + resp.put("jagi.next", (Function, Map>)env -> Environment.dispatch(next, req)); + resp.put("jagi.input-sink", new Collector(req)); + } else { + read(req); + resp.put("jagi.status", "chain"); + resp.put("jagi.next", (Function, Map>)env -> Environment.dispatch(next, req)); + } + return(resp); + } +} diff --git a/src/jrw/Handler.java b/src/jrw/Handler.java new file mode 100644 index 0000000..2c5f979 --- /dev/null +++ b/src/jrw/Handler.java @@ -0,0 +1,7 @@ +package jrw; + +import java.util.*; + +public interface Handler { + public Map handle(Request req); +} diff --git a/src/jrw/Http.java b/src/jrw/Http.java new file mode 100644 index 0000000..f71b062 --- /dev/null +++ b/src/jrw/Http.java @@ -0,0 +1,7 @@ +package jrw; + +public class Http { + 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"); +} diff --git a/src/jrw/JagiWrapper.java b/src/jrw/JagiWrapper.java new file mode 100644 index 0000000..8b1a3fb --- /dev/null +++ b/src/jrw/JagiWrapper.java @@ -0,0 +1,18 @@ +package jrw; + +import java.util.*; +import java.util.function.*; + +public abstract class JagiWrapper implements Handler, Function, Map> { + public Map apply(Map env) { + return(Environment.dispatch(this, new Request(env))); + } + + public static JagiWrapper of(Handler handler) { + return(new JagiWrapper() { + public Map handle(Request req) { + return(handler.handle(req)); + } + }); + } +} diff --git a/src/jrw/Request.java b/src/jrw/Request.java new file mode 100644 index 0000000..abe7161 --- /dev/null +++ b/src/jrw/Request.java @@ -0,0 +1,50 @@ +package jrw; + +import java.util.*; + +public class Request { + public final Map env; + public final Map resp = new HashMap<>(); + + public Request(Map env) { + this.env = env; + } + + public String ihead(String name, String def) { + StringBuilder buf = new StringBuilder(); + buf.append("HTTP_"); + for(int i = 0; i < name.length(); i++) { + char c = name.charAt(i); + if(c == '-') + buf.append('_'); + else if((c >= 'a') && (c <= 'z')) + buf.append((char)(c + ('A' - 'a'))); + else + buf.append(c); + } + Object ret = env.get(buf.toString()); + if(ret instanceof String) + return((String)ret); + return(def); + } + + @SuppressWarnings("unchecked") + public void ohead(String name, Object val, boolean repl) { + name = "http." + name; + if(repl) { + resp.put(name, val); + } else { + Object cur = resp.get(name); + if(cur == null) + resp.put(name, val); + else if(cur instanceof Collection) + ((Collection)cur).add(val); + else + resp.put(name, new ArrayList(Arrays.asList(cur, val))); + } + } + + public Map response() { + return(resp); + } +} diff --git a/src/jrw/Restart.java b/src/jrw/Restart.java new file mode 100644 index 0000000..de11218 --- /dev/null +++ b/src/jrw/Restart.java @@ -0,0 +1,18 @@ +package jrw; + +import java.util.*; + +public abstract class Restart extends RuntimeException implements Handler { + public Restart() {} + public Restart(Throwable cause) {super(cause);} + public Restart(String msg) {super(msg);} + public Restart(String msg, Throwable cause) {super(msg, cause);} + + public static Restart with(Handler handler) { + return(new Restart() { + public Map handle(Request req) { + return(handler.handle(req)); + } + }); + } +} -- 2.11.0