From d40ca8cc569eb376f34518928fcccf474e5d8a14 Mon Sep 17 00:00:00 2001 From: 00asdf Date: Sun, 1 Oct 2023 18:01:18 +0200 Subject: [PATCH] initial commit --- .gitignore | 30 +++++ asdf00.lib.httpserver.iml | 11 ++ src/dev/asdf00/lib/httpserver/Authorizer.java | 7 ++ .../asdf00/lib/httpserver/HttpServerApi.java | 119 ++++++++++++++++++ .../lib/httpserver/HttpServerApiUtils.java | 86 +++++++++++++ .../httpserver/annotations/HttpEndpoint.java | 26 ++++ .../lib/httpserver/annotations/QParam.java | 14 +++ .../exceptions/HttpHandlingException.java | 9 ++ .../internal/DefaultAuthorizer.java | 11 ++ .../internal/EndpointContainer.java | 58 +++++++++ .../httpserver/internal/HttpHandlerImpl.java | 56 +++++++++ .../lib/httpserver/utils/RequestType.java | 25 ++++ .../asdf00/lib/httpserver/utils/Response.java | 74 +++++++++++ 13 files changed, 526 insertions(+) create mode 100644 .gitignore create mode 100644 asdf00.lib.httpserver.iml create mode 100644 src/dev/asdf00/lib/httpserver/Authorizer.java create mode 100644 src/dev/asdf00/lib/httpserver/HttpServerApi.java create mode 100644 src/dev/asdf00/lib/httpserver/HttpServerApiUtils.java create mode 100644 src/dev/asdf00/lib/httpserver/annotations/HttpEndpoint.java create mode 100644 src/dev/asdf00/lib/httpserver/annotations/QParam.java create mode 100644 src/dev/asdf00/lib/httpserver/exceptions/HttpHandlingException.java create mode 100644 src/dev/asdf00/lib/httpserver/internal/DefaultAuthorizer.java create mode 100644 src/dev/asdf00/lib/httpserver/internal/EndpointContainer.java create mode 100644 src/dev/asdf00/lib/httpserver/internal/HttpHandlerImpl.java create mode 100644 src/dev/asdf00/lib/httpserver/utils/RequestType.java create mode 100644 src/dev/asdf00/lib/httpserver/utils/Response.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7baa262 --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +### IntelliJ IDEA ### +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ +.idea/ + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/asdf00.lib.httpserver.iml b/asdf00.lib.httpserver.iml new file mode 100644 index 0000000..c90834f --- /dev/null +++ b/asdf00.lib.httpserver.iml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/dev/asdf00/lib/httpserver/Authorizer.java b/src/dev/asdf00/lib/httpserver/Authorizer.java new file mode 100644 index 0000000..a5fdc50 --- /dev/null +++ b/src/dev/asdf00/lib/httpserver/Authorizer.java @@ -0,0 +1,7 @@ +package dev.asdf00.lib.httpserver; + +import com.sun.net.httpserver.HttpExchange; + +public interface Authorizer { + boolean isAuthenticated(HttpExchange exchange); +} diff --git a/src/dev/asdf00/lib/httpserver/HttpServerApi.java b/src/dev/asdf00/lib/httpserver/HttpServerApi.java new file mode 100644 index 0000000..5ad2631 --- /dev/null +++ b/src/dev/asdf00/lib/httpserver/HttpServerApi.java @@ -0,0 +1,119 @@ +package dev.asdf00.lib.httpserver; + +import com.sun.net.httpserver.HttpContext; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpServer; +import dev.asdf00.lib.httpserver.annotations.HttpEndpoint; +import dev.asdf00.lib.httpserver.annotations.QParam; +import dev.asdf00.lib.httpserver.exceptions.HttpHandlingException; +import dev.asdf00.lib.httpserver.internal.DefaultAuthorizer; +import dev.asdf00.lib.httpserver.internal.EndpointContainer; +import dev.asdf00.lib.httpserver.internal.HttpHandlerImpl; +import dev.asdf00.lib.httpserver.utils.RequestType; +import dev.asdf00.lib.httpserver.utils.Response; + +import java.io.IOException; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.net.InetSocketAddress; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.concurrent.Executor; + +import static dev.asdf00.lib.httpserver.HttpServerApiUtils.*; +import static dev.asdf00.lib.httpserver.utils.RequestType.*; + +public class HttpServerApi { + + public static Response respond404(HttpExchange exchange) throws HttpHandlingException { + throw new HttpHandlingException(404); + } + + public static HttpServer create(int port, String rootLocation, Class apiDefinition, Executor exec) throws IOException { + HttpServer server = HttpServer.create(new InetSocketAddress( port), 0); + server.setExecutor(exec); + + // collect all http endpoints + HashMap[] endpoints = new HashMap[3]; + HashSet locations = new HashSet<>(); + for (int i = 0; i < endpoints.length; endpoints[i++] = new HashMap<>()); + endpointLoop: + for (Method endpoint : apiDefinition.getDeclaredMethods()) { + HttpEndpoint spec = endpoint.getAnnotation(HttpEndpoint.class); + if (spec == null || spec.type() == INVALID) { + continue; + } + if (!endpoint.getReturnType().equals(Response.class)) { + System.err.printf("invalid return type %s!\nskipping %s ...\n", endpoint.getReturnType().getSimpleName(), endpoint.getName()); + continue; + } + + RequestType type = spec.type(); + String location = spec.location(); + HashMap epMap = endpoints[type.id]; + if (epMap.containsKey(location)) { + System.err.printf("HttpEndpoint for type %s and location \"%s\" is defined multiple times!\nskipping %s ...\n", + type.name(), location, endpoint.getName()); + continue; + } + + Parameter[] params = endpoint.getParameters(); + if (params.length < 1 || !HttpExchange.class.equals(params[0].getType())) { + System.err.printf("HttpEndpoint must have HttpExchange as first parameter!\nskipping %s ...\n", + endpoint.getReturnType().getSimpleName(), endpoint.getName()); + continue; + } + params = Arrays.stream(params).skip(1).toArray(Parameter[]::new); + + Class[] pTypes = Arrays.stream(params).map(p -> p.getType()).toArray(Class[]::new); + String[] pNames = new String[params.length]; + String[] stringVals = new String[params.length]; + for (int i = 0; i < params.length; i++) { + QParam name = params[i].getAnnotation(QParam.class); + if (name == null) { + System.err.printf("missing QParam annotation!\nskipping %s ...\n", endpoint.getName()); + continue endpointLoop; + } + pNames[i] = name.name(); + stringVals[i] = name.empty(); + } + Object[] pDefaults = new Object[pTypes.length]; + try { + for (int i = 0; i < pTypes.length; i++) { + pDefaults[i] = parseValue(pTypes[i], stringVals[i]); + } + } catch (IllegalArgumentException e) { + System.err.printf("encountered error setting up HttpEndpoint (%s)!\nskipping %s ...\n", e.getMessage(), endpoint.getName()); + continue; + } + + Authorizer auth; + try { + auth = spec.auth().getConstructor().newInstance(); + } catch (ReflectiveOperationException e) { + System.err.printf("encountered error setting up HttpEndpoint (%s)!\nskipping %s ...\n", e.getMessage(), endpoint.getName()); + continue; + } + + epMap.put(location, new EndpointContainer(endpoint, auth, pTypes, pNames, pDefaults)); + locations.add(location); + } + + // create appropriate contexts and headers + EndpointContainer default404; + try { + Method respond404 = HttpServerApi.class.getMethod("respond404", HttpExchange.class); + default404 = new EndpointContainer(respond404, new DefaultAuthorizer(), new Class[0], new String[0], new Object[0]); + } catch (NoSuchMethodException e) { + // this would be an error inside this class + throw new RuntimeException(e); + } + for (String loc : locations) { + HttpContext context = server.createContext(rootLocation + loc); + context.setHandler(new HttpHandlerImpl(Arrays.stream(endpoints).map(e -> e.getOrDefault(loc, default404)).toArray(EndpointContainer[]::new))); + } + + return server; + } +} diff --git a/src/dev/asdf00/lib/httpserver/HttpServerApiUtils.java b/src/dev/asdf00/lib/httpserver/HttpServerApiUtils.java new file mode 100644 index 0000000..efbee82 --- /dev/null +++ b/src/dev/asdf00/lib/httpserver/HttpServerApiUtils.java @@ -0,0 +1,86 @@ +package dev.asdf00.lib.httpserver; + +import com.sun.net.httpserver.Headers; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpPrincipal; +import dev.asdf00.lib.httpserver.exceptions.HttpHandlingException; + +import java.io.*; +import java.net.URI; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.util.Map; +import java.util.stream.Stream; + +public class HttpServerApiUtils { + + private static ClassLoader clsldr = HttpServerApiUtils.class.getClassLoader(); + + public static Stream patchFile(Map patchMap, InputStream file) { + return new BufferedReader(new InputStreamReader(file)).lines().map(line -> { + for (String patch : patchMap.keySet()) { + String fullPatch = "${" + patch + "}"; + line = line.replace(fullPatch, patchMap.get(patch)); + } + return line; + }); + } + + public static InputStream readResource(Path path) throws HttpHandlingException { + InputStream res; + try { + res = clsldr.getResourceAsStream(path.toString()); + } catch (InvalidPathException e) { + throw new HttpHandlingException(400); + } + if (res == null) { + throw new HttpHandlingException(404); + } + return res; + } + + public static void printRequestInfo(HttpExchange exchange) { + System.out.println("\n-- headers --"); + Headers requestHeaders = exchange.getRequestHeaders(); + requestHeaders.entrySet().forEach(System.out::println); + + System.out.println("-- principle --"); + HttpPrincipal principal = exchange.getPrincipal(); + System.out.println(principal); + + System.out.println("-- HTTP method --"); + String requestMethod = exchange.getRequestMethod(); + System.out.println(requestMethod); + + System.out.println("-- query --"); + URI requestURI = exchange.getRequestURI(); + String query = requestURI.getQuery(); + System.out.println(query); + } + + public static T parseValue(Class type, String value) { + if (String.class.equals(type)) { + return (T) value; + } else if (byte.class.equals(type)) { + return (T) Byte.valueOf(value); + } else if (short.class.equals(type)) { + return (T) Short.valueOf(value); + } else if (int.class.equals(type)) { + return (T) Integer.valueOf(value); + } else if (long.class.equals(type)) { + return (T) Long.valueOf(value); + } else if (float.class.equals(type)) { + return (T) Float.valueOf(value); + } else if (double.class.equals(type)) { + return (T) Double.valueOf(value); + } else if (boolean.class.equals(type)) { + return (T) Boolean.valueOf(value); + } else if (char.class.equals(type)) { + if (value.length() < 1) { + throw new IllegalArgumentException("cannon convert empty string to char"); + } + return (T) Character.valueOf(value.charAt(0)); + } + throw new IllegalArgumentException(type.getSimpleName() + " is not a primitive"); + } +} diff --git a/src/dev/asdf00/lib/httpserver/annotations/HttpEndpoint.java b/src/dev/asdf00/lib/httpserver/annotations/HttpEndpoint.java new file mode 100644 index 0000000..60d3b9b --- /dev/null +++ b/src/dev/asdf00/lib/httpserver/annotations/HttpEndpoint.java @@ -0,0 +1,26 @@ +package dev.asdf00.lib.httpserver.annotations; + +import dev.asdf00.lib.httpserver.Authorizer; +import dev.asdf00.lib.httpserver.internal.DefaultAuthorizer; +import dev.asdf00.lib.httpserver.utils.RequestType; + +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 HttpEndpoint { + RequestType type(); + + String location(); + + /** + * Minimum auth level required to access the given resource. If an auth level greater -1 is + * specified, the auth service is queried and the returned auth level is appended to the + * HttpExchange provided to this endpoint.
+ * default: don't care = -1 + */ + Class auth() default DefaultAuthorizer.class; +} \ No newline at end of file diff --git a/src/dev/asdf00/lib/httpserver/annotations/QParam.java b/src/dev/asdf00/lib/httpserver/annotations/QParam.java new file mode 100644 index 0000000..bd6c02b --- /dev/null +++ b/src/dev/asdf00/lib/httpserver/annotations/QParam.java @@ -0,0 +1,14 @@ +package dev.asdf00.lib.httpserver.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface QParam { + String name(); + String empty(); +} + diff --git a/src/dev/asdf00/lib/httpserver/exceptions/HttpHandlingException.java b/src/dev/asdf00/lib/httpserver/exceptions/HttpHandlingException.java new file mode 100644 index 0000000..192863f --- /dev/null +++ b/src/dev/asdf00/lib/httpserver/exceptions/HttpHandlingException.java @@ -0,0 +1,9 @@ +package dev.asdf00.lib.httpserver.exceptions; + +public class HttpHandlingException extends Exception { + public final int statusCode; + public HttpHandlingException(int statusCode) { + super("Http Error " + statusCode); + this.statusCode = statusCode; + } +} diff --git a/src/dev/asdf00/lib/httpserver/internal/DefaultAuthorizer.java b/src/dev/asdf00/lib/httpserver/internal/DefaultAuthorizer.java new file mode 100644 index 0000000..0b513cd --- /dev/null +++ b/src/dev/asdf00/lib/httpserver/internal/DefaultAuthorizer.java @@ -0,0 +1,11 @@ +package dev.asdf00.lib.httpserver.internal; + +import com.sun.net.httpserver.HttpExchange; +import dev.asdf00.lib.httpserver.Authorizer; + +public class DefaultAuthorizer implements Authorizer { + @Override + public boolean isAuthenticated(HttpExchange exchange) { + return true; + } +} diff --git a/src/dev/asdf00/lib/httpserver/internal/EndpointContainer.java b/src/dev/asdf00/lib/httpserver/internal/EndpointContainer.java new file mode 100644 index 0000000..569c574 --- /dev/null +++ b/src/dev/asdf00/lib/httpserver/internal/EndpointContainer.java @@ -0,0 +1,58 @@ +package dev.asdf00.lib.httpserver.internal; + +import com.sun.net.httpserver.HttpExchange; +import dev.asdf00.lib.httpserver.Authorizer; +import dev.asdf00.lib.httpserver.exceptions.HttpHandlingException; + +import java.lang.reflect.Method; + +import static dev.asdf00.lib.httpserver.HttpServerApiUtils.parseValue; + +public class EndpointContainer { + public final Method handler; + public final Authorizer auth; + public final Class[] pTypes; + public final String[] pNames; + public final Object[] pDefaults; + + public EndpointContainer(Method handler, Authorizer auth, Class[] pTypes, String[] paramNames, Object[] pDefaults) { + this.handler = handler; + this.auth = auth; + this.pTypes = pTypes; + this.pNames = paramNames; + this.pDefaults = pDefaults; + } + + public Object[] parseOrDefaults(HttpExchange exchange, String values) throws HttpHandlingException { + String[] args; + if (values == null) { + args = new String[0]; + } else { + args = values.split("&"); + } + return parseOrDefaults(exchange, args); + } + + public Object[] parseOrDefaults(HttpExchange exchange, String[] values) throws HttpHandlingException { + Object[] vals = new Object[pNames.length + 1]; + vals[0] = exchange; + for (int i = 0; i < pNames.length; i++) { + vals[i + 1] = pDefaults[i]; + for (int j = 0; j < values.length; j++) { + String[] split = values[j].split("="); + if (split.length < 2) { + continue; + } + if (pNames[i].equals(split[0])) { + try { + vals[i + 1] = parseValue(pTypes[i], split[1]); + break; + } catch (IllegalArgumentException ignore) { + // skip assignment and use default value + } + } + } + } + return vals; + } +} diff --git a/src/dev/asdf00/lib/httpserver/internal/HttpHandlerImpl.java b/src/dev/asdf00/lib/httpserver/internal/HttpHandlerImpl.java new file mode 100644 index 0000000..4ea82dc --- /dev/null +++ b/src/dev/asdf00/lib/httpserver/internal/HttpHandlerImpl.java @@ -0,0 +1,56 @@ +package dev.asdf00.lib.httpserver.internal; + +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import dev.asdf00.lib.httpserver.HttpServerApiUtils; +import dev.asdf00.lib.httpserver.exceptions.HttpHandlingException; +import dev.asdf00.lib.httpserver.utils.RequestType; +import dev.asdf00.lib.httpserver.utils.Response; + +import java.lang.reflect.InvocationTargetException; + +import static dev.asdf00.lib.httpserver.utils.RequestType.*; + +public class HttpHandlerImpl implements HttpHandler { + private final EndpointContainer[] endpointContainer; + + public HttpHandlerImpl(EndpointContainer... containers) { + endpointContainer = containers; + } + + @Override + public void handle(HttpExchange exchange) { + try { + RequestType type = RequestType.from(exchange.getRequestMethod()); + if (type == INVALID) { + throw new HttpHandlingException(400); + } + + EndpointContainer target = endpointContainer[type.id]; + if (!target.auth.isAuthenticated(exchange)) { + throw new HttpHandlingException(402); + } + + String query = exchange.getRequestURI().getQuery(); + Object[] args = target.parseOrDefaults(exchange, query); + Response response = (Response) target.handler.invoke(null, args); + response.send(); + + } catch (Exception e) { + if (e instanceof InvocationTargetException) { + e = (Exception) e.getCause(); + } + + int errorCode = 500; + if (e instanceof HttpHandlingException) { + errorCode = ((HttpHandlingException) e).statusCode; + } else if (e instanceof IndexOutOfBoundsException) { + errorCode = 400; + } + Response r = new Response(exchange).status(errorCode).contentType(Response.ContentType.HTML); + r.append(String.format("

ERROR %s

\n", errorCode)); + r.append(String.format("

something fucky is a foot!\n%s

", e.getMessage())); + r.send(); + } + } +} diff --git a/src/dev/asdf00/lib/httpserver/utils/RequestType.java b/src/dev/asdf00/lib/httpserver/utils/RequestType.java new file mode 100644 index 0000000..033024e --- /dev/null +++ b/src/dev/asdf00/lib/httpserver/utils/RequestType.java @@ -0,0 +1,25 @@ +package dev.asdf00.lib.httpserver.utils; + + +public enum RequestType { + INVALID(-1), + GET(0), + POST(1), + DELETE(2); + + public final int id; + RequestType(int id) { + this.id = id; + } + + public static RequestType from(String parse) { + if ("GET".equals(parse)) { + return GET; + } else if ("POST".equals(parse)) { + return POST; + } else if ("DELETE".equals(parse)) { + return DELETE; + } + return INVALID; + } +} diff --git a/src/dev/asdf00/lib/httpserver/utils/Response.java b/src/dev/asdf00/lib/httpserver/utils/Response.java new file mode 100644 index 0000000..df884cc --- /dev/null +++ b/src/dev/asdf00/lib/httpserver/utils/Response.java @@ -0,0 +1,74 @@ +package dev.asdf00.lib.httpserver.utils; + +import com.sun.net.httpserver.HttpExchange; +import dev.asdf00.lib.httpserver.HttpServerApiUtils; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class Response { + public enum ContentType { + PLAIN("text/plain"), + HTML("text/html"), + JS("text/javascript"); + private final String type; + ContentType(String type) { + this.type = type; + } + } + + private int statusCode = 500; + private ContentType cType = ContentType.PLAIN; + private final HttpExchange exchange; + private final StringBuilder response; + + public Response(HttpExchange exchange) { + this.exchange = exchange; + response = new StringBuilder(); + } + + public Response status(int code) { + statusCode = code; + return this; + } + + public Response contentType(ContentType type) { + cType = type; + return this; + } + + public Response append(String line) { + response.append(line); + return this; + } + + public Response append(StringBuilder line) { + response.append(line); + return this; + } + + public Response append(Stream line) { + response.append(line.collect(Collectors.joining("\n"))); + return this; + } + + public Response append(InputStream line) { + response.append(new BufferedReader(new InputStreamReader(line)).lines().collect(Collectors.joining("\n"))); + return this; + } + + public boolean send() { + try { + exchange.getResponseHeaders().add("Content-type", cType.type); + exchange.sendResponseHeaders(statusCode, response.length()); + try (OutputStream os = exchange.getResponseBody()) { + os.write(response.toString().getBytes(StandardCharsets.UTF_8)); + } + } catch (IOException e) { + return false; + } + return true; + } +}