From c7d3e51ba88e58dd1b7e4f92d8c9f83aa49224d5 Mon Sep 17 00:00:00 2001 From: TudbuT Date: Sun, 3 Jul 2022 16:02:52 +0200 Subject: [PATCH] amazing progress, add src dir because somehow it wasnt in the git repo before --- index.html | 36 +++ .../tryumph/config/IRequestCatcher.java | 19 ++ src/de/tudbut/tryumph/config/Nothing.java | 6 + .../tudbut/tryumph/events/EventListener.java | 59 +++++ src/de/tudbut/tryumph/events/GET.java | 11 + src/de/tudbut/tryumph/events/POST.java | 11 + src/de/tudbut/tryumph/events/Path.java | 12 + .../tudbut/tryumph/events/RequestMethod.java | 12 + src/de/tudbut/tryumph/example/Main.java | 47 ++++ .../tudbut/tryumph/server/BrowserContext.java | 151 +++++++++++++ src/de/tudbut/tryumph/server/HTMLParsing.java | 35 +++ src/de/tudbut/tryumph/server/Header.java | 28 +++ src/de/tudbut/tryumph/server/Request.java | 85 ++++++++ src/de/tudbut/tryumph/server/Response.java | 89 ++++++++ .../server/http/HTTPRequestReader.java | 205 ++++++++++++++++++ .../server/http/HTTPResponseWriter.java | 78 +++++++ src/de/tudbut/tryumph/server/http/Server.java | 106 +++++++++ src/de/tudbut/tryumph/server/http/Stop.java | 4 + src/de/tudbut/tryumph/util/Bug.java | 30 +++ .../tudbut/tryumph/util/ReadOnlyHashMap.java | 52 +++++ 20 files changed, 1076 insertions(+) create mode 100644 index.html create mode 100644 src/de/tudbut/tryumph/config/IRequestCatcher.java create mode 100644 src/de/tudbut/tryumph/config/Nothing.java create mode 100644 src/de/tudbut/tryumph/events/EventListener.java create mode 100644 src/de/tudbut/tryumph/events/GET.java create mode 100644 src/de/tudbut/tryumph/events/POST.java create mode 100644 src/de/tudbut/tryumph/events/Path.java create mode 100644 src/de/tudbut/tryumph/events/RequestMethod.java create mode 100644 src/de/tudbut/tryumph/example/Main.java create mode 100644 src/de/tudbut/tryumph/server/BrowserContext.java create mode 100644 src/de/tudbut/tryumph/server/HTMLParsing.java create mode 100644 src/de/tudbut/tryumph/server/Header.java create mode 100644 src/de/tudbut/tryumph/server/Request.java create mode 100644 src/de/tudbut/tryumph/server/Response.java create mode 100644 src/de/tudbut/tryumph/server/http/HTTPRequestReader.java create mode 100644 src/de/tudbut/tryumph/server/http/HTTPResponseWriter.java create mode 100644 src/de/tudbut/tryumph/server/http/Server.java create mode 100644 src/de/tudbut/tryumph/server/http/Stop.java create mode 100644 src/de/tudbut/tryumph/util/Bug.java create mode 100644 src/de/tudbut/tryumph/util/ReadOnlyHashMap.java 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 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 function) { + throw new IllegalStateException("Write to ReadOnlyHashMap"); + } + +}