amazing progress, add src dir because somehow it wasnt in the git repo before

This commit is contained in:
Daniella / Tove 2022-07-03 16:02:52 +02:00
parent 487a7b001c
commit c7d3e51ba8
20 changed files with 1076 additions and 0 deletions

36
index.html Normal file
View file

@ -0,0 +1,36 @@
<html>
<head>
<title>Tryumph example page</title>
</head>
<body>
<h1> This is the Tryumph example page </h1>
Here, you will be able to test out some functions of Tryumph.
<br/>
<br/>
<h2>Get:</h2>
<form onsubmit="try{document.getElementById('value').value = data[document.getElementById('name').value].toString()}catch(e){} ; return false;">
<input type="text" id="name">=
<input readonly type="text" id="value">
<input type="submit" value="Get!">
</form>
<h2>Server-side set:</h2>
<form method="post">
<input type="text" name="name">=
<input type="text" name="value">
<input type="submit" value="Set!">
</form>
<h2>Client-side set:</h2>
<form onsubmit="try{data[document.getElementById('sname').value] = document.getElementById('svalue').value}catch(e){} ; return false;">
<input type="text" id="sname">=
<input type="text" id="svalue">
<input type="submit" value="Set!">
<button onclick="data.save()" type="button">Save!</button>
</form>
</body>
</html>

View file

@ -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<ComposeCallback<Request, Response>> onConnect(Socket socket) { return (tres, trej) -> trej.call(new Nothing()); }
default Task<BrowserContext> processBrowserContext(BrowserContext context) { return t((res, rej) -> res.call(context)); }
}

View file

@ -0,0 +1,6 @@
package de.tudbut.tryumph.config;
public class Nothing extends RuntimeException {
}

View file

@ -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<Response> res, Callback<Throwable> 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();
}
}
}
}
}

View file

@ -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 {
}

View file

@ -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 {
}

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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<ComposeCallback<Request, Response>> 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, "<h1>Error: 404 Not found " + resp.realPath + "</h1>", 404, "Not Found"));
}
});
}
@GET
@Path("/")
public void onIndex(Request request, Callback<Response> res, Callback<Throwable> rej) {
res.call(new Response(request, request.context.file("index.html"), 200, "OK"));
}
@POST
@Path("/")
public void onIndexSubmit(Request request, Callback<Response> res, Callback<Throwable> 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"));
}
}

View file

@ -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<UUID, BrowserContext> 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<BrowserContext> 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<BrowserContext> 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<BrowserContext> 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<BrowserContext> 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<Response> 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<String, String> 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<br/><h1>---CUT---</h1><br/>\n");
builder.append("Error reading rest of file! Sorry.");
}
return builder.toString();
}
}

View file

@ -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("<meta name=\"generator\" content=\n\"HTML Tidy for Java (vers. 2009-12-01), see jtidy.sourceforge.net\">\n", "");
}
public static Document newDocument() {
return Tidy.createEmptyDocument();
}
}

View file

@ -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<String, String> parameters;
public Header(String name, String value, HashMap<String, String> 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;
}
}

View file

@ -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<String, String> cookies;
public final HashMap<String, Header> headers;
public final byte[] body;
boolean hasResponseFlag = false;
public Request(Socket socket, BrowserContext context, String httpVersion, String method, String path, HashMap<String, String> cookies, HashMap<String, Header> 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;
}
}

View file

@ -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<String, Header> headers = new HashMap<>();
public final HashMap<String, String> 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;
}
}

View file

@ -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<String, Header> headers = new HashMap<>();
byte[] body = new byte[0];
HashMap<String, String> 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<String, String> 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<String, String> splitParameters(String parameters) {
HashMap<String, String> 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;
}
}

View file

@ -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<String, String> parameters) {
StringBuilder builder = new StringBuilder();
Entry<String, String>[] 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));
}
}

View file

@ -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<ComposeCallback<Request, Response>> composer = new AtomicReference<>();
try {
try {
catcher.onConnect(socket).execute(composer::set, x -> {throw new Reject(x);});
} catch (Reject reject) {
throw reject.<Throwable>getReal();
}
} catch (Stop stop) {
throw stop;
} catch(Throwable e) {
if(e instanceof Nothing)
return;
else {
e.printStackTrace();
break;
}
}
AtomicReference<Response> 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.<Throwable>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();
}
}

View file

@ -0,0 +1,4 @@
package de.tudbut.tryumph.server.http;
public class Stop extends RuntimeException {
}

View file

@ -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);
}
}

View file

@ -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<K, V> extends HashMap<K, V> {
public ReadOnlyHashMap(HashMap<K, V> 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");
}
}