amazing progress, add src dir because somehow it wasnt in the git repo before
This commit is contained in:
parent
487a7b001c
commit
c7d3e51ba8
20 changed files with 1076 additions and 0 deletions
36
index.html
Normal file
36
index.html
Normal 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>
|
19
src/de/tudbut/tryumph/config/IRequestCatcher.java
Normal file
19
src/de/tudbut/tryumph/config/IRequestCatcher.java
Normal 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)); }
|
||||
}
|
||||
|
6
src/de/tudbut/tryumph/config/Nothing.java
Normal file
6
src/de/tudbut/tryumph/config/Nothing.java
Normal file
|
@ -0,0 +1,6 @@
|
|||
package de.tudbut.tryumph.config;
|
||||
|
||||
public class Nothing extends RuntimeException {
|
||||
|
||||
}
|
||||
|
59
src/de/tudbut/tryumph/events/EventListener.java
Normal file
59
src/de/tudbut/tryumph/events/EventListener.java
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
11
src/de/tudbut/tryumph/events/GET.java
Normal file
11
src/de/tudbut/tryumph/events/GET.java
Normal 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 {
|
||||
}
|
11
src/de/tudbut/tryumph/events/POST.java
Normal file
11
src/de/tudbut/tryumph/events/POST.java
Normal 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 {
|
||||
}
|
12
src/de/tudbut/tryumph/events/Path.java
Normal file
12
src/de/tudbut/tryumph/events/Path.java
Normal 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();
|
||||
}
|
12
src/de/tudbut/tryumph/events/RequestMethod.java
Normal file
12
src/de/tudbut/tryumph/events/RequestMethod.java
Normal 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();
|
||||
}
|
47
src/de/tudbut/tryumph/example/Main.java
Normal file
47
src/de/tudbut/tryumph/example/Main.java
Normal 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"));
|
||||
}
|
||||
|
||||
}
|
151
src/de/tudbut/tryumph/server/BrowserContext.java
Normal file
151
src/de/tudbut/tryumph/server/BrowserContext.java
Normal 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();
|
||||
}
|
||||
|
||||
}
|
||||
|
35
src/de/tudbut/tryumph/server/HTMLParsing.java
Normal file
35
src/de/tudbut/tryumph/server/HTMLParsing.java
Normal 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();
|
||||
}
|
||||
}
|
28
src/de/tudbut/tryumph/server/Header.java
Normal file
28
src/de/tudbut/tryumph/server/Header.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
85
src/de/tudbut/tryumph/server/Request.java
Normal file
85
src/de/tudbut/tryumph/server/Request.java
Normal 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;
|
||||
}
|
||||
|
||||
|
||||
}
|
89
src/de/tudbut/tryumph/server/Response.java
Normal file
89
src/de/tudbut/tryumph/server/Response.java
Normal 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;
|
||||
}
|
||||
}
|
205
src/de/tudbut/tryumph/server/http/HTTPRequestReader.java
Normal file
205
src/de/tudbut/tryumph/server/http/HTTPRequestReader.java
Normal 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;
|
||||
}
|
||||
}
|
78
src/de/tudbut/tryumph/server/http/HTTPResponseWriter.java
Normal file
78
src/de/tudbut/tryumph/server/http/HTTPResponseWriter.java
Normal 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));
|
||||
}
|
||||
}
|
106
src/de/tudbut/tryumph/server/http/Server.java
Normal file
106
src/de/tudbut/tryumph/server/http/Server.java
Normal 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();
|
||||
}
|
||||
}
|
4
src/de/tudbut/tryumph/server/http/Stop.java
Normal file
4
src/de/tudbut/tryumph/server/http/Stop.java
Normal file
|
@ -0,0 +1,4 @@
|
|||
package de.tudbut.tryumph.server.http;
|
||||
|
||||
public class Stop extends RuntimeException {
|
||||
}
|
30
src/de/tudbut/tryumph/util/Bug.java
Normal file
30
src/de/tudbut/tryumph/util/Bug.java
Normal 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);
|
||||
}
|
||||
|
||||
|
||||
}
|
52
src/de/tudbut/tryumph/util/ReadOnlyHashMap.java
Normal file
52
src/de/tudbut/tryumph/util/ReadOnlyHashMap.java
Normal 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");
|
||||
}
|
||||
|
||||
}
|
Loading…
Add table
Reference in a new issue