initial commit

This commit is contained in:
00asdf 2023-10-01 18:01:18 +02:00
commit d40ca8cc56
13 changed files with 526 additions and 0 deletions

30
.gitignore vendored Normal file
View File

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

11
asdf00.lib.httpserver.iml Normal file
View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@ -0,0 +1,7 @@
package dev.asdf00.lib.httpserver;
import com.sun.net.httpserver.HttpExchange;
public interface Authorizer {
boolean isAuthenticated(HttpExchange exchange);
}

View File

@ -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<String, EndpointContainer>[] endpoints = new HashMap[3];
HashSet<String> 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<String, EndpointContainer> 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;
}
}

View File

@ -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<String, String> 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> T parseValue(Class<T> 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");
}
}

View File

@ -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.<br/>
* default: don't care = -1
*/
Class<? extends Authorizer> auth() default DefaultAuthorizer.class;
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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("<h1>ERROR %s</h1>\n", errorCode));
r.append(String.format("<p>something fucky is a foot!\n%s</p>", e.getMessage()));
r.send();
}
}
}

View File

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

View File

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