diff --git a/index.html b/index.html
new file mode 100644
index 0000000..3859720
--- /dev/null
+++ b/index.html
@@ -0,0 +1,36 @@
+
+
+ Tryumph example page
+
+
+ This is the Tryumph example page
+
+ Here, you will be able to test out some functions of Tryumph.
+
+
+
+
+ Get:
+
+
+
+ Server-side set:
+
+
+ Client-side set:
+
+
+
diff --git a/src/de/tudbut/tryumph/config/IRequestCatcher.java b/src/de/tudbut/tryumph/config/IRequestCatcher.java
new file mode 100644
index 0000000..42d91a3
--- /dev/null
+++ b/src/de/tudbut/tryumph/config/IRequestCatcher.java
@@ -0,0 +1,19 @@
+package de.tudbut.tryumph.config;
+
+import java.net.Socket;
+
+import de.tudbut.async.ComposeCallback;
+import de.tudbut.async.Task;
+import de.tudbut.async.TaskCallable;
+import de.tudbut.tryumph.server.BrowserContext;
+import de.tudbut.tryumph.server.Request;
+import de.tudbut.tryumph.server.Response;
+
+import static de.tudbut.async.Async.*;
+
+public interface IRequestCatcher {
+
+ default TaskCallable> onConnect(Socket socket) { return (tres, trej) -> trej.call(new Nothing()); }
+ default Task processBrowserContext(BrowserContext context) { return t((res, rej) -> res.call(context)); }
+}
+
diff --git a/src/de/tudbut/tryumph/config/Nothing.java b/src/de/tudbut/tryumph/config/Nothing.java
new file mode 100644
index 0000000..395329f
--- /dev/null
+++ b/src/de/tudbut/tryumph/config/Nothing.java
@@ -0,0 +1,6 @@
+package de.tudbut.tryumph.config;
+
+public class Nothing extends RuntimeException {
+
+}
+
diff --git a/src/de/tudbut/tryumph/events/EventListener.java b/src/de/tudbut/tryumph/events/EventListener.java
new file mode 100644
index 0000000..fa8f963
--- /dev/null
+++ b/src/de/tudbut/tryumph/events/EventListener.java
@@ -0,0 +1,59 @@
+package de.tudbut.tryumph.events;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+import de.tudbut.async.Callback;
+import de.tudbut.tryumph.config.IRequestCatcher;
+import de.tudbut.tryumph.server.Request;
+import de.tudbut.tryumph.server.Response;
+
+public class EventListener {
+
+ private IRequestCatcher catcher;
+
+ public EventListener(IRequestCatcher catcher) {
+ this.catcher = catcher;
+ }
+
+ public void handle(Request request, Callback res, Callback rej) {
+ Method[] methods = catcher.getClass().getDeclaredMethods();
+ for(int i = 0; i < methods.length; i++) {
+ Method method = methods[i];
+ if(method.getDeclaredAnnotations().length == 0)
+ continue;
+ if(
+ method.getParameterCount() != 3 ||
+ method.getParameterTypes()[0] != Request.class ||
+ method.getParameterTypes()[1] != Callback.class ||
+ method.getParameterTypes()[2] != Callback.class ||
+ method.getReturnType() != void.class
+ ) {
+ continue;
+ }
+ boolean usable = true;
+ if(method.getDeclaredAnnotation(GET.class) != null && !request.method.equals("GET")) {
+ usable = false;
+ }
+ if(method.getDeclaredAnnotation(POST.class) != null && !request.method.equals("POST")) {
+ usable = false;
+ }
+ Path pathA = method.getDeclaredAnnotation(Path.class);
+ if(pathA != null && !request.realPath.matches("^" + pathA.value() + "$")) {
+ usable = false;
+ }
+ RequestMethod methodA = method.getDeclaredAnnotation(RequestMethod.class);
+ if(methodA != null && !request.method.matches("^" + methodA.value() + "$")) {
+ usable = false;
+ }
+
+ if(usable) {
+ try {
+ method.invoke(catcher, request, res, rej);
+ } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+}
diff --git a/src/de/tudbut/tryumph/events/GET.java b/src/de/tudbut/tryumph/events/GET.java
new file mode 100644
index 0000000..cdf909e
--- /dev/null
+++ b/src/de/tudbut/tryumph/events/GET.java
@@ -0,0 +1,11 @@
+package de.tudbut.tryumph.events;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface GET {
+}
diff --git a/src/de/tudbut/tryumph/events/POST.java b/src/de/tudbut/tryumph/events/POST.java
new file mode 100644
index 0000000..6a51a5a
--- /dev/null
+++ b/src/de/tudbut/tryumph/events/POST.java
@@ -0,0 +1,11 @@
+package de.tudbut.tryumph.events;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface POST {
+}
diff --git a/src/de/tudbut/tryumph/events/Path.java b/src/de/tudbut/tryumph/events/Path.java
new file mode 100644
index 0000000..e3f0897
--- /dev/null
+++ b/src/de/tudbut/tryumph/events/Path.java
@@ -0,0 +1,12 @@
+package de.tudbut.tryumph.events;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Path {
+ String value();
+}
diff --git a/src/de/tudbut/tryumph/events/RequestMethod.java b/src/de/tudbut/tryumph/events/RequestMethod.java
new file mode 100644
index 0000000..2d37699
--- /dev/null
+++ b/src/de/tudbut/tryumph/events/RequestMethod.java
@@ -0,0 +1,12 @@
+package de.tudbut.tryumph.events;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface RequestMethod {
+ String value();
+}
diff --git a/src/de/tudbut/tryumph/example/Main.java b/src/de/tudbut/tryumph/example/Main.java
new file mode 100644
index 0000000..9b52695
--- /dev/null
+++ b/src/de/tudbut/tryumph/example/Main.java
@@ -0,0 +1,47 @@
+package de.tudbut.tryumph.example;
+
+import java.net.Socket;
+
+import de.tudbut.async.Callback;
+import de.tudbut.async.ComposeCallback;
+import de.tudbut.async.TaskCallable;
+import de.tudbut.tryumph.config.IRequestCatcher;
+import de.tudbut.tryumph.events.EventListener;
+import de.tudbut.tryumph.events.GET;
+import de.tudbut.tryumph.events.POST;
+import de.tudbut.tryumph.events.Path;
+import de.tudbut.tryumph.server.Request;
+import de.tudbut.tryumph.server.Response;
+import tudbut.parsing.TCN;
+
+public class Main implements IRequestCatcher {
+
+ EventListener listener = new EventListener(this);
+
+ @Override
+ public TaskCallable> onConnect(Socket socket) {
+ return (tres, trej) -> tres.call((resp, res, rej) -> {
+ System.out.println(resp.toString());
+ listener.handle(resp, res, rej);
+ if(!resp.hasResponse()) {
+ res.call(new Response(resp, "Error: 404 Not found " + resp.realPath + "
", 404, "Not Found"));
+ }
+ });
+ }
+
+ @GET
+ @Path("/")
+ public void onIndex(Request request, Callback res, Callback rej) {
+ res.call(new Response(request, request.context.file("index.html"), 200, "OK"));
+ }
+
+ @POST
+ @Path("/")
+ public void onIndexSubmit(Request request, Callback res, Callback rej) {
+ TCN query = request.bodyURLEncoded();
+ request.context.data.set(query.getString("name"), query.getString("value"));
+ request.context.save();
+ res.call(new Response(request, request.context.file("index.html"), 200, "OK"));
+ }
+
+}
diff --git a/src/de/tudbut/tryumph/server/BrowserContext.java b/src/de/tudbut/tryumph/server/BrowserContext.java
new file mode 100644
index 0000000..910f10a
--- /dev/null
+++ b/src/de/tudbut/tryumph/server/BrowserContext.java
@@ -0,0 +1,151 @@
+package de.tudbut.tryumph.server;
+
+import static de.tudbut.async.Async.*;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.UUID;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+import de.tudbut.async.Task;
+import de.tudbut.tryumph.config.IRequestCatcher;
+import tudbut.parsing.AsyncJSON;
+import tudbut.parsing.TCN;
+
+public class BrowserContext {
+
+ public static final HashMap sessions = new HashMap<>();
+
+ public final UUID uuid = UUID.randomUUID();
+ public TCN data;
+ private final IRequestCatcher requestCatcher;
+ public boolean useJavaScript = false;
+ private boolean needsChange = false;
+
+ private BrowserContext(IRequestCatcher requestCatcher) {
+ this.requestCatcher = requestCatcher;
+ data = new TCN("json");
+ }
+
+ private BrowserContext(String cookie, IRequestCatcher requestCatcher) {
+ this.requestCatcher = requestCatcher;
+ try {
+ System.out.println("Reading cookie");
+ data = AsyncJSON.read(cookie).err(e -> {throw new RuntimeException(e);}).ok().await();
+ System.out.println("Read cookie");
+ } catch (Exception e) {
+ data = new TCN("JSON");
+ }
+ }
+
+ public static Task create(IRequestCatcher requestCatcher) {
+ return t((res, rej) -> {
+ BrowserContext it = new BrowserContext(requestCatcher);
+ sessions.put(it.uuid, it);
+ it.init().err(rej).then(res).ok();
+ });
+ }
+
+ public static Task create(String cookie, IRequestCatcher requestCatcher) {
+ return t((res, rej) -> {
+ BrowserContext it = new BrowserContext(cookie, requestCatcher);
+ sessions.put(it.uuid, it);
+ it.init().err(rej).then(res).ok();
+ });
+ }
+
+ public static Task get(UUID browserUUID, String cookie, IRequestCatcher requestCatcher) {
+ if(sessions.containsKey(browserUUID))
+ return t((res, rej) -> res.call(sessions.get(browserUUID)));
+ return create(cookie, requestCatcher);
+ }
+
+ private Task init() {
+ return t((res, rej) -> {
+ requestCatcher.processBrowserContext(this).err(rej).then(res).ok();
+ });
+ }
+
+ public void onReceive(Request request) {
+ if(request.cookies.containsKey("tryumph.data")) {
+ AsyncJSON.read(request.cookies.get("tryumph.data")).err(e -> {}).then(x -> data = x).ok();
+ }
+ }
+
+ public Task onSend(Response response) {
+ return AsyncJSON.write(data)
+ .compose((resp, res, rej) -> {
+ if(response.isHTML) {
+ Document document = response.getHtml();
+ Element element = document.createElement("script");
+ Node text = document.createTextNode(
+ "function setCookie(cname, cvalue) {" +
+ "let d = new Date();" +
+ "d.setTime(d.getTime() + 365 * 24 * 60 * 60 * 1000);" +
+ "var expires = 'Expires=' + d.toUTCString();" +
+ "document.cookie = cname + '=' + cvalue + ';' + expires + ';path=/';" +
+ "} " +
+ "function getCookie(cname) {" +
+ "let name = cname + '=';" +
+ "let ca = document.cookie.split(';');" +
+ "for (let i = 0; i < ca.length; i++) {" +
+ "let c = ca[i];" +
+ "while (c.charAt(0) == ' ') {" +
+ "c = c.substring(1);" +
+ "} " +
+ "if (c.indexOf(name) == 0) {" +
+ "return c.substring(name.length, c.length);" +
+ "} " +
+ "} " +
+ "return '';" +
+ "} " +
+ "let data = JSON.parse(decodeURIComponent(getCookie('tryumph.data'))); " +
+ "data.save = function saveData() { setCookie('tryumph.data', encodeURIComponent(JSON.stringify(data))) }"
+ );
+ element.appendChild(text);
+ Node body = document.getElementsByTagName("body").item(0);
+ body.insertBefore(element, body.getFirstChild());
+ response.updateHTMLData();
+ }
+ if(needsChange) {
+ response.cookiesToSet.put("tryumph.data", resp);
+ needsChange = false;
+ }
+ response.cookiesToSet.put("tryumph.uuid", uuid.toString());
+ res.call(response);
+ });
+ }
+
+ public void save() {
+ needsChange = true;
+ }
+
+ private final HashMap cache = new HashMap<>();
+ public String file(String file) {
+ if(cache.containsKey(file))
+ return cache.get(file);
+ StringBuilder builder = new StringBuilder();
+ try {
+ InputStream stream = new FileInputStream(file);
+
+ int i = 0;
+ while((i = stream.read()) != -1) {
+ builder.append((char) i);
+ }
+
+ stream.close();
+ cache.put(file, builder.toString());
+ } catch (IOException e) {
+ builder.append("\n
---CUT---
\n");
+ builder.append("Error reading rest of file! Sorry.");
+ }
+ return builder.toString();
+ }
+
+}
+
diff --git a/src/de/tudbut/tryumph/server/HTMLParsing.java b/src/de/tudbut/tryumph/server/HTMLParsing.java
new file mode 100644
index 0000000..2e37077
--- /dev/null
+++ b/src/de/tudbut/tryumph/server/HTMLParsing.java
@@ -0,0 +1,35 @@
+package de.tudbut.tryumph.server;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.StringReader;
+import java.io.Writer;
+
+import org.w3c.dom.Document;
+import org.w3c.tidy.Tidy;
+
+import de.tudbut.tryumph.util.Bug;
+
+public class HTMLParsing {
+ private static Tidy tidy = new Tidy();
+
+ public static Document parse(String html) {
+ return (Document) tidy.parseDOM(new StringReader(html), (Writer) null);
+ }
+
+ public static String print(Document html) {
+ ByteArrayOutputStream writer = new ByteArrayOutputStream();
+ tidy.pprint(html, writer);
+ String s = writer.toString();
+ try {
+ writer.close();
+ } catch (IOException e) {
+ throw new Bug(e);
+ }
+ return s.replace("\n", "");
+ }
+
+ public static Document newDocument() {
+ return Tidy.createEmptyDocument();
+ }
+}
diff --git a/src/de/tudbut/tryumph/server/Header.java b/src/de/tudbut/tryumph/server/Header.java
new file mode 100644
index 0000000..8f96fa4
--- /dev/null
+++ b/src/de/tudbut/tryumph/server/Header.java
@@ -0,0 +1,28 @@
+package de.tudbut.tryumph.server;
+
+import java.util.HashMap;
+
+import de.tudbut.tryumph.util.ReadOnlyHashMap;
+
+public class Header {
+ public final String name;
+ public final String value;
+ public final HashMap parameters;
+
+ public Header(String name, String value, HashMap parameters) {
+ this.name = name;
+ this.value = value;
+ this.parameters = new ReadOnlyHashMap<>(parameters);
+ }
+
+ public String getParameter(String name) {
+ return parameters.get(name);
+ }
+
+ @Override
+ public String toString() {
+ return name + ": " + value + "; " + parameters;
+ }
+
+}
+
diff --git a/src/de/tudbut/tryumph/server/Request.java b/src/de/tudbut/tryumph/server/Request.java
new file mode 100644
index 0000000..6d1d39a
--- /dev/null
+++ b/src/de/tudbut/tryumph/server/Request.java
@@ -0,0 +1,85 @@
+package de.tudbut.tryumph.server;
+
+import java.net.Socket;
+import java.util.HashMap;
+
+import de.tudbut.tryumph.util.ReadOnlyHashMap;
+import tudbut.net.http.HTTPUtils;
+import tudbut.parsing.TCN;
+
+public class Request {
+
+ final Socket socket;
+ public final BrowserContext context;
+ public final String httpVersion;
+ public final String method;
+ public final String path;
+ public final String realPath;
+ public final String[] splitPath;
+ public final HashMap cookies;
+ public final HashMap headers;
+ public final byte[] body;
+ boolean hasResponseFlag = false;
+
+ public Request(Socket socket, BrowserContext context, String httpVersion, String method, String path, HashMap cookies, HashMap headers, byte[] body) {
+ this.socket = socket;
+ this.context = context;
+ this.httpVersion = httpVersion;
+ this.method = method;
+ while(path.endsWith("/") && path.length() > 1)
+ path = path.substring(0, path.length() - 1);
+ this.path = path;
+ this.cookies = cookies;
+ this.headers = new ReadOnlyHashMap<>(headers);
+ this.body = body;
+
+ realPath = realPath();
+ splitPath = realPath.split("/");
+ }
+
+ @Override
+ public String toString() {
+ return method + " " + path + " " + httpVersion + "\n" +
+ headers + "\n\n" + new String(body);
+ }
+
+ private String realPath() {
+ return path.substring(0, path.indexOf('?') == -1 ? path.length() : path.indexOf('?'));
+ }
+
+ public TCN query() {
+ TCN tcn = new TCN();
+ if(path.indexOf('?') == -1)
+ return tcn;
+ String query = path.substring(path.indexOf('?') + 1);
+ String[] pairs = query.split("&");
+ for(int i = 0; i < pairs.length; i++) {
+ if(pairs[i].indexOf('=') == -1) {
+ continue;
+ }
+ String k = pairs[i].substring(0, pairs[i].indexOf('=')), v = pairs[i].substring(pairs[i].indexOf('=') + 1);
+ tcn.set(HTTPUtils.decodeUTF8(k), HTTPUtils.decodeUTF8(v));
+ }
+ return tcn;
+ }
+
+ public TCN bodyURLEncoded() {
+ TCN tcn = new TCN();
+ String body = new String(this.body);
+ String[] pairs = body.split("&");
+ for(int i = 0; i < pairs.length; i++) {
+ if(pairs[i].indexOf('=') == -1) {
+ continue;
+ }
+ String k = pairs[i].substring(0, pairs[i].indexOf('=')), v = pairs[i].substring(pairs[i].indexOf('=') + 1);
+ tcn.set(HTTPUtils.decodeUTF8(k), HTTPUtils.decodeUTF8(v));
+ }
+ return tcn;
+ }
+
+ public boolean hasResponse() {
+ return hasResponseFlag;
+ }
+
+
+}
diff --git a/src/de/tudbut/tryumph/server/Response.java b/src/de/tudbut/tryumph/server/Response.java
new file mode 100644
index 0000000..24919d2
--- /dev/null
+++ b/src/de/tudbut/tryumph/server/Response.java
@@ -0,0 +1,89 @@
+package de.tudbut.tryumph.server;
+
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+
+import org.w3c.dom.Document;
+
+public class Response {
+ private String htmlData;
+ private Document html = HTMLParsing.newDocument();
+ private byte[] body;
+ public final Request request;
+ public final int statusCode;
+ public final String statusName;
+ public final HashMap headers = new HashMap<>();
+ public final HashMap cookiesToSet = new HashMap<>();
+ public final boolean isHTML;
+
+ public Response(Request request, String htmlData, int statusCode, String statusName) {
+ isHTML = true;
+ request.hasResponseFlag = true;
+ this.request = request;
+ this.htmlData = htmlData;
+ makeDocument();
+ updateHTMLData();
+ this.statusCode = statusCode;
+ this.statusName = statusName;
+ headers.put("Content-Type", new Header("Content-Type", "text/html", new HashMap<>()));
+ }
+ public Response(Request request, int statusCode, String statusName) {
+ isHTML = false;
+ request.hasResponseFlag = true;
+ this.request = request;
+ this.htmlData = "";
+ this.body = new byte[0];
+ this.statusCode = statusCode;
+ this.statusName = statusName;
+ }
+ public Response(Request request, String body, int statusCode, String statusName, String contentType) {
+ isHTML = false;
+ request.hasResponseFlag = true;
+ this.request = request;
+ this.htmlData = "";
+ this.body = body.getBytes(StandardCharsets.ISO_8859_1);
+ this.statusCode = statusCode;
+ this.statusName = statusName;
+ headers.put("Content-Type", new Header("Content-Type", contentType, new HashMap<>()));
+ }
+ public Response(Request request, byte[] body, int statusCode, String statusName, String contentType) {
+ isHTML = false;
+ request.hasResponseFlag = true;
+ this.request = request;
+ this.htmlData = "";
+ this.body = body;
+ this.statusCode = statusCode;
+ this.statusName = statusName;
+ headers.put("Content-Type", new Header("Content-Type", contentType, new HashMap<>()));
+ }
+
+ public String updateHTMLData() {
+ if(!isHTML)
+ throw new IllegalStateException("Tried to access HTML on a non-HTML response");
+ htmlData = HTMLParsing.print(html);
+ body = htmlData.getBytes(StandardCharsets.ISO_8859_1);
+ return htmlData;
+ }
+
+ private Document makeDocument() {
+ if(!isHTML)
+ throw new IllegalStateException("Tried to access HTML on a non-HTML response");
+ return html = HTMLParsing.parse(htmlData);
+ }
+
+ public String getHtmlData() {
+ if(!isHTML)
+ throw new IllegalStateException("Tried to access HTML on a non-HTML response");
+ return htmlData;
+ }
+
+ public Document getHtml() {
+ if(!isHTML)
+ throw new IllegalStateException("Tried to access HTML on a non-HTML response");
+ return html;
+ }
+
+ public byte[] getBody() {
+ return body;
+ }
+}
diff --git a/src/de/tudbut/tryumph/server/http/HTTPRequestReader.java b/src/de/tudbut/tryumph/server/http/HTTPRequestReader.java
new file mode 100644
index 0000000..67d77fa
--- /dev/null
+++ b/src/de/tudbut/tryumph/server/http/HTTPRequestReader.java
@@ -0,0 +1,205 @@
+package de.tudbut.tryumph.server.http;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.Socket;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.UUID;
+
+import de.tudbut.tryumph.config.IRequestCatcher;
+import de.tudbut.tryumph.server.BrowserContext;
+import de.tudbut.tryumph.server.Header;
+import de.tudbut.tryumph.server.Request;
+import tudbut.net.http.HTTPUtils;
+
+public class HTTPRequestReader {
+
+ private Socket socket;
+ private final InputStream stream;
+
+ public HTTPRequestReader(Socket socket) throws IOException {
+ this.socket = socket;
+ this.stream = socket.getInputStream();
+ }
+
+ private int read() throws IOException {
+ int i = stream.read();
+ if(i == -1) {
+ throw new Stop();
+ }
+ return i;
+ }
+
+ private int read(byte[] bytes) throws IOException {
+ int i = stream.read(bytes);
+ if(i != bytes.length) {
+ throw new Stop();
+ }
+ return i;
+ }
+
+ public Request read(IRequestCatcher requestCatcher) throws IOException {
+ String version;
+ String method;
+ String path;
+ HashMap headers = new HashMap<>();
+ byte[] body = new byte[0];
+ HashMap cookies = new HashMap<>();
+ BrowserContext context = null;
+ method = readUntil(' ');
+ path = readUntil(' ');
+ assumeString("HTTP/");
+ byte[] httpversion = new byte["X.X".length()];
+ read(httpversion);
+ version = "HTTP/" + new String(httpversion);
+ assumeCRLF();
+ String header;
+ while(!(header = readUntilCRLF()).isEmpty()) {
+ boolean hasParameters = header.indexOf(';') != -1;
+ String name = header.substring(0, header.indexOf(':'));
+ String value = HTTPUtils.decodeUTF8(header.substring(
+ header.indexOf(':') + 2,
+ hasParameters ? header.indexOf(';') : header.length()
+ ));
+ String parameters = hasParameters ? header.substring(header.indexOf(';') + 2) : "";
+ HashMap parameterMap = splitParameters(parameters);
+ // Handle cookies
+ if(name.equals("Cookie")) {
+ cookies.putAll(splitParameters(value));
+ cookies.putAll(parameterMap);
+ }
+ headers.put(name, new Header(name, value, parameterMap));
+ }
+ if(headers.containsKey("Content-Length")) {
+ body = new byte[Integer.parseInt(headers.get("Content-Length").value)];
+ read(body);
+ }
+
+ // Custom for tryumph only BEGIN
+ // Generate context if possible
+ if(cookies.containsKey("tryumph.uuid")) {
+ try {
+ context = BrowserContext.get(
+ UUID.fromString(cookies.get("tryumph.uuid")),
+ cookies.containsKey("tryumph.data") ? cookies.get("tryumph.data") : "",
+ requestCatcher
+ ).ok().await();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ if(context == null) {
+ try {
+ context = BrowserContext.create(cookies.containsKey("tryumph.data") ? cookies.get("tryumph.data") : "", requestCatcher).ok().await();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ if(context == null) {
+ try {
+ context = BrowserContext.create(requestCatcher).ok().await();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ if(context == null) {
+ System.err.println("Error generating context");
+ }
+ // Custom for tryumph only END
+
+ return new Request(socket, context, version, method, path, cookies, headers, body);
+ }
+
+ private HashMap splitParameters(String parameters) {
+ HashMap parameterMap = new HashMap<>();
+ String[] parameterArray = parameters.split("; ");
+ for(int i = 0; i < parameterArray.length; i++) {
+ String param = parameterArray[i];
+ int idx = param.indexOf('=');
+ if(idx == -1)
+ continue;
+ parameterMap.put(
+ HTTPUtils.decodeUTF8(parameterArray[i].substring(0, idx)),
+ HTTPUtils.decodeUTF8(param.substring(idx + 1, param.length()))
+ );
+ }
+ return parameterMap;
+ }
+
+ /**
+ * Read until linebreak is encountered.
+ * @throws IOException Inherited from {@link InputStream#read()}.
+ */
+ private String readUntilCRLF() throws IOException {
+ int i;
+ StringBuilder builder = new StringBuilder();
+ while((i = read()) != '\n') {
+ if(i != '\r')
+ builder.append((char) i);
+ }
+ return builder.toString();
+ }
+
+ /**
+ * Read until c is encountered.
+ * @throws IOException Inherited from {@link InputStream#read()}.
+ */
+ private String readUntil(char c) throws IOException {
+ int i;
+ StringBuilder builder = new StringBuilder();
+ while((i = read()) != c) {
+ builder.append((char) i);
+ }
+ return builder.toString();
+ }
+
+ /**
+ * Assume a line break in the input stream.
+ * @throws IllegalArgumentException if the break is not encountered
+ * @throws IOException Inherited from {@link InputStream#read()}.
+ */
+ private void assumeCRLF() throws IllegalArgumentException, IOException {
+ int i = read();
+ if(i != '\r') {
+ if(i == '\n') {
+ return;
+ }
+ throw new IllegalArgumentException("Encountered byte " + i + " in stream, but expected \\r or \\n");
+ }
+ i = read();
+ if(i != '\n') {
+ throw new IllegalArgumentException("Encountered byte " + i + " in stream, but expected \\n");
+ }
+ }
+ /**
+ * Assume a byte in the input stream.
+ * @throws IllegalArgumentException if the byte is not encountered
+ * @throws IOException Inherited from {@link InputStream#read()}.
+ */
+ private void assumeByte(char c) throws IllegalArgumentException, IOException {
+ int i = read();
+ if(i != (int) c)
+ throw new IllegalArgumentException("Encountered byte " + i + " in stream, but expected " + c);
+ }
+ /**
+ * Assume a string in the input stream.
+ * @throws IllegalArgumentException if the string is not encountered
+ * @throws IOException Inherited from {@link InputStream#read(byte[])}.
+ */
+ private void assumeString(String string) throws IllegalArgumentException, IOException {
+ byte[] bytes = new byte[string.length()];
+ read(bytes);
+ if(!Arrays.equals(bytes, string.getBytes(StandardCharsets.ISO_8859_1)))
+ throw new IllegalArgumentException("Encountered string " + new String(bytes) + " (" + Arrays.toString(bytes) + ") in stream, but expected " + string);
+ }
+
+ public Socket getSocket() {
+ return socket;
+ }
+
+ public InputStream getStream() {
+ return stream;
+ }
+}
diff --git a/src/de/tudbut/tryumph/server/http/HTTPResponseWriter.java b/src/de/tudbut/tryumph/server/http/HTTPResponseWriter.java
new file mode 100644
index 0000000..8ced6c3
--- /dev/null
+++ b/src/de/tudbut/tryumph/server/http/HTTPResponseWriter.java
@@ -0,0 +1,78 @@
+package de.tudbut.tryumph.server.http;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.Socket;
+import java.nio.charset.StandardCharsets;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map.Entry;
+
+import de.tudbut.tryumph.server.Header;
+import de.tudbut.tryumph.server.Response;
+import tudbut.net.http.HTTPUtils;
+
+public class HTTPResponseWriter {
+
+ private OutputStream stream;
+
+ public HTTPResponseWriter(Socket socket) throws IOException {
+ this.stream = socket.getOutputStream();
+ }
+
+ public void write(Response resp) throws IOException {
+ resp.headers.remove("Content-Length");
+ write("HTTP/1.1 " + resp.statusCode + " " + resp.statusName);
+ writeCRLF();
+ String[] headers = resp.headers.keySet().toArray(new String[0]);
+ for(int i = 0; i < headers.length; i++) {
+ Header header = resp.headers.get(headers[i]);
+ write(header.name + ": " + header.value);
+ write(serializeParams(header.parameters));
+ writeCRLF();
+ }
+ String[] cookies = resp.cookiesToSet.keySet().toArray(new String[0]);
+ for(int i = 0; i < cookies.length; i++) {
+ write("Set-Cookie: ");
+ write(HTTPUtils.encodeUTF8(cookies[i]));
+ write('=');
+ write(HTTPUtils.encodeUTF8(resp.cookiesToSet.get(cookies[i])));
+ write("; ");
+ write("Expires=\"");
+ write(new SimpleDateFormat("HH:MM:SS dd MMM yyyy").format(new Date(System.currentTimeMillis() + 1 * 365 * 24 * 60 * 60 * 1000)));
+ write("\"; Path=/");
+ writeCRLF();
+ }
+ write("Content-Length: " + resp.getBody().length);
+ writeCRLF();
+ writeCRLF();
+ write(resp.getBody());
+ }
+
+ private String serializeParams(HashMap parameters) {
+ StringBuilder builder = new StringBuilder();
+ Entry[] params = parameters.entrySet().toArray(new Entry[0]);
+ for(int i = 0; i < params.length; i++) {
+ builder.append(params[i].getKey()).append("=").append(params[i].getValue()).append("; ");
+ }
+ return builder.toString();
+ }
+
+ private void writeCRLF() throws IOException {
+ write('\r');
+ write('\n');
+ }
+
+ private void write(int i) throws IOException {
+ stream.write(i);
+ }
+
+ private void write(byte[] bytes) throws IOException {
+ stream.write(bytes);
+ }
+
+ private void write(String s) throws IOException {
+ stream.write(s.getBytes(StandardCharsets.ISO_8859_1));
+ }
+}
diff --git a/src/de/tudbut/tryumph/server/http/Server.java b/src/de/tudbut/tryumph/server/http/Server.java
new file mode 100644
index 0000000..b338a32
--- /dev/null
+++ b/src/de/tudbut/tryumph/server/http/Server.java
@@ -0,0 +1,106 @@
+package de.tudbut.tryumph.server.http;
+
+import java.io.IOException;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.util.concurrent.atomic.AtomicReference;
+
+import de.tudbut.async.ComposeCallback;
+import de.tudbut.async.Reject;
+import de.tudbut.tryumph.config.IRequestCatcher;
+import de.tudbut.tryumph.config.Nothing;
+import de.tudbut.tryumph.server.Request;
+import de.tudbut.tryumph.server.Response;
+import de.tudbut.tryumph.util.Bug;
+
+public class Server {
+ private final int port;
+ private boolean listening = false;
+ private IRequestCatcher catcher;
+
+ public Server(int port) {
+ this.port = port;
+ }
+
+ public void listen(IRequestCatcher requestCatcher) throws IOException {
+ catcher = requestCatcher;
+ if(!listening) {
+ startListening();
+ }
+ }
+
+ public void stop() {
+ listening = false;
+ catcher = null;
+ }
+
+ private void startListening() throws IOException {
+ if(listening) {
+ throw new Bug("startListening called but was already listening");
+ }
+ listening = true;
+ ServerSocket serverSocket = new ServerSocket(port);
+ while(listening) {
+ Socket socket = serverSocket.accept();
+ new Thread(() -> {
+ try {
+ while(true) {
+ AtomicReference> composer = new AtomicReference<>();
+ try {
+ try {
+ catcher.onConnect(socket).execute(composer::set, x -> {throw new Reject(x);});
+ } catch (Reject reject) {
+ throw reject.getReal();
+ }
+ } catch (Stop stop) {
+ throw stop;
+ } catch(Throwable e) {
+ if(e instanceof Nothing)
+ return;
+ else {
+ e.printStackTrace();
+ break;
+ }
+ }
+ AtomicReference responseToSend = new AtomicReference<>();
+ try {
+ try {
+ HTTPRequestReader reader = new HTTPRequestReader(socket);
+ Request request = reader.read(catcher);
+ request.context.onReceive(request);
+ composer.get().call(request, responseToSend::set, x -> {throw new Reject(x);});
+ } catch (Reject reject) {
+ throw reject.getReal();
+ }
+ } catch (Stop stop) {
+ throw stop;
+ } catch (Throwable e) {
+ e.printStackTrace();
+ break;
+ }
+ Response response = responseToSend.get();
+ try {
+ HTTPResponseWriter writer = new HTTPResponseWriter(socket);
+ writer.write(response.request.context.onSend(responseToSend.get()).ok().await());
+ } catch(IOException e) {
+ e.printStackTrace();
+ break;
+ }
+ if(!response.request.headers.containsKey("Connection")) {
+ break;
+ }
+ if(!response.request.headers.get("Connection").value.equalsIgnoreCase("Keep-Alive"))
+ break;
+ }
+ } catch (Stop stop) {
+ }
+ try {
+ socket.close();
+ } catch (IOException e) {
+ }
+ System.out.println("Connection with " + socket + " ended");
+ }).start();
+ }
+ serverSocket.close();
+ }
+}
diff --git a/src/de/tudbut/tryumph/server/http/Stop.java b/src/de/tudbut/tryumph/server/http/Stop.java
new file mode 100644
index 0000000..2cd4cd9
--- /dev/null
+++ b/src/de/tudbut/tryumph/server/http/Stop.java
@@ -0,0 +1,4 @@
+package de.tudbut.tryumph.server.http;
+
+public class Stop extends RuntimeException {
+}
diff --git a/src/de/tudbut/tryumph/util/Bug.java b/src/de/tudbut/tryumph/util/Bug.java
new file mode 100644
index 0000000..a29fc3e
--- /dev/null
+++ b/src/de/tudbut/tryumph/util/Bug.java
@@ -0,0 +1,30 @@
+package de.tudbut.tryumph.util;
+
+import java.io.PrintStream;
+import java.io.PrintWriter;
+
+public class Bug extends Error {
+ public Bug(String s) {
+ super(s);
+ }
+ public Bug(Throwable t) {
+ super(t);
+ }
+ public Bug(String s, Throwable t) {
+ super(s, t);
+ }
+
+ @Override
+ public void printStackTrace(PrintStream s) {
+ s.println("Tryumph has found a Bug in itself. Please report this immediately and attach the full log if possible, or the message below if only it is available!");
+ super.printStackTrace(s);
+ }
+
+ @Override
+ public void printStackTrace(PrintWriter s) {
+ s.println("Tryumph has found a Bug in itself. Please report this immediately and attach the full log if possible, or the message below if only it is available!");
+ super.printStackTrace(s);
+ }
+
+
+}
diff --git a/src/de/tudbut/tryumph/util/ReadOnlyHashMap.java b/src/de/tudbut/tryumph/util/ReadOnlyHashMap.java
new file mode 100644
index 0000000..18a57a9
--- /dev/null
+++ b/src/de/tudbut/tryumph/util/ReadOnlyHashMap.java
@@ -0,0 +1,52 @@
+package de.tudbut.tryumph.util;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.BiFunction;
+
+public class ReadOnlyHashMap extends HashMap {
+ public ReadOnlyHashMap(HashMap map) {
+ super(map);
+ }
+
+ @Override
+ public V put(K key, V value) {
+ throw new IllegalStateException("Write to ReadOnlyHashMap");
+ }
+
+ @Override
+ public void putAll(Map extends K, ? extends V> m) {
+ throw new IllegalStateException("Write to ReadOnlyHashMap");
+ }
+
+ @Override
+ public V putIfAbsent(K key, V value) {
+ throw new IllegalStateException("Write to ReadOnlyHashMap");
+ }
+
+ @Override
+ public V remove(Object key) {
+ throw new IllegalStateException("Write to ReadOnlyHashMap");
+ }
+
+ @Override
+ public boolean remove(Object key, Object value) {
+ throw new IllegalStateException("Write to ReadOnlyHashMap");
+ }
+
+ @Override
+ public V replace(K key, V value) {
+ throw new IllegalStateException("Write to ReadOnlyHashMap");
+ }
+
+ @Override
+ public boolean replace(K key, V oldValue, V newValue) {
+ throw new IllegalStateException("Write to ReadOnlyHashMap");
+ }
+
+ @Override
+ public void replaceAll(BiFunction super K, ? super V, ? extends V> function) {
+ throw new IllegalStateException("Write to ReadOnlyHashMap");
+ }
+
+}