Refactored using IntelliJ
This commit is contained in:
@@ -1,7 +1,5 @@
|
||||
package org.openautonomousconnection.webserver;
|
||||
|
||||
import jdk.dynalink.linker.LinkerServices;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public final class ContentTypeResolver {
|
||||
|
||||
@@ -17,7 +17,8 @@ import java.util.Scanner;
|
||||
|
||||
public class Main {
|
||||
public static final CommandPermission PERMISSION_ALL = new CommandPermission("all", 1);
|
||||
private static final CommandExecutor commandExecutor = new CommandExecutor("WebServer", PERMISSION_ALL) {};
|
||||
private static final CommandExecutor commandExecutor = new CommandExecutor("WebServer", PERMISSION_ALL) {
|
||||
};
|
||||
|
||||
@Getter
|
||||
private static CommandManager commandManager;
|
||||
|
||||
@@ -18,7 +18,8 @@ import org.openautonomousconnection.webserver.utils.WebHasher;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.Files;
|
||||
import java.util.*;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
|
||||
@ProtocolInfo(protocolSide = ProtocolVersion.ProtocolSide.WEB)
|
||||
public final class WebServer extends ProtocolWebServer {
|
||||
@@ -35,7 +36,7 @@ public final class WebServer extends ProtocolWebServer {
|
||||
int sessionExpire,
|
||||
int maxUpload
|
||||
) throws Exception {
|
||||
super(authFile, rulesFile,sessionExpire, maxUpload);
|
||||
super(authFile, rulesFile, sessionExpire, maxUpload);
|
||||
|
||||
// NOTE: Values chosen as safe defaults.
|
||||
// move them to Main and pass them in here (no hidden assumptions).
|
||||
@@ -80,8 +81,10 @@ public final class WebServer extends ProtocolWebServer {
|
||||
File root = getContentFolder().getCanonicalFile();
|
||||
File file = new File(root, path).getCanonicalFile();
|
||||
|
||||
if (!file.getPath().startsWith(root.getPath())) return new WebResponsePacket(403, "text/plain; charset=utf-8", Map.of(), "Forbidden".getBytes());
|
||||
if (!file.exists() || !file.isFile()) return new WebResponsePacket(404, "text/plain; charset=utf-8", Map.of(), "Not found".getBytes());
|
||||
if (!file.getPath().startsWith(root.getPath()))
|
||||
return new WebResponsePacket(403, "text/plain; charset=utf-8", Map.of(), "Forbidden".getBytes());
|
||||
if (!file.exists() || !file.isFile())
|
||||
return new WebResponsePacket(404, "text/plain; charset=utf-8", Map.of(), "Not found".getBytes());
|
||||
|
||||
String contentType = ContentTypeResolver.resolve(file.getName());
|
||||
long size = file.length();
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package org.openautonomousconnection.webserver.api;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* Declares a route path for a server-side Java WebPage.
|
||||
|
||||
@@ -41,7 +41,15 @@ public final class SessionContext {
|
||||
return new SessionContext(sessionId, user, true);
|
||||
}
|
||||
|
||||
public boolean isValid() { return valid; }
|
||||
public String getSessionId() { return sessionId; }
|
||||
public String getUser() { return user; }
|
||||
public boolean isValid() {
|
||||
return valid;
|
||||
}
|
||||
|
||||
public String getSessionId() {
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
public String getUser() {
|
||||
return user;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ package org.openautonomousconnection.webserver.api;
|
||||
import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.WebRequestPacket;
|
||||
import org.openautonomousconnection.protocol.side.server.CustomConnectedClient;
|
||||
import org.openautonomousconnection.protocol.side.web.ProtocolWebServer;
|
||||
import org.openautonomousconnection.webserver.utils.WebHasher;
|
||||
import org.openautonomousconnection.webserver.utils.RequestParams;
|
||||
import org.openautonomousconnection.webserver.utils.WebHasher;
|
||||
|
||||
/**
|
||||
* Context passed to Java WebPages (client, request, session, params, hasher).
|
||||
|
||||
@@ -8,16 +8,6 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
*/
|
||||
public final class JavaPageCache {
|
||||
|
||||
private static final class Entry {
|
||||
final long lastModified;
|
||||
final Class<?> clazz;
|
||||
|
||||
Entry(long lastModified, Class<?> clazz) {
|
||||
this.lastModified = lastModified;
|
||||
this.clazz = clazz;
|
||||
}
|
||||
}
|
||||
|
||||
private final ConcurrentHashMap<String, Entry> cache = new ConcurrentHashMap<>();
|
||||
|
||||
public Class<?> getOrCompile(File javaFile) throws Exception {
|
||||
@@ -33,4 +23,7 @@ public final class JavaPageCache {
|
||||
cache.put(key, new Entry(lm, compiled));
|
||||
return compiled;
|
||||
}
|
||||
|
||||
private record Entry(long lastModified, Class<?> clazz) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package org.openautonomousconnection.webserver.runtime;
|
||||
|
||||
import javax.tools.*;
|
||||
import javax.tools.JavaCompiler;
|
||||
import javax.tools.JavaFileObject;
|
||||
import javax.tools.StandardJavaFileManager;
|
||||
import javax.tools.ToolProvider;
|
||||
import java.io.File;
|
||||
import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
@@ -16,7 +19,8 @@ import java.util.List;
|
||||
*/
|
||||
public final class JavaPageCompiler {
|
||||
|
||||
private JavaPageCompiler() {}
|
||||
private JavaPageCompiler() {
|
||||
}
|
||||
|
||||
public static Class<?> compile(File javaFile) throws Exception {
|
||||
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
|
||||
@@ -35,7 +39,7 @@ public final class JavaPageCompiler {
|
||||
String fqcn = deriveFqcn(javaFile);
|
||||
|
||||
// Root must be the folder containing the package root (javaFile parent is fine if classes output there).
|
||||
URLClassLoader cl = new URLClassLoader(new URL[]{ javaFile.getParentFile().toURI().toURL() });
|
||||
URLClassLoader cl = new URLClassLoader(new URL[]{javaFile.getParentFile().toURI().toURL()});
|
||||
|
||||
return cl.loadClass(fqcn);
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ import org.openautonomousconnection.protocol.side.web.ProtocolWebServer;
|
||||
import org.openautonomousconnection.webserver.WebServer;
|
||||
import org.openautonomousconnection.webserver.api.WebPage;
|
||||
import org.openautonomousconnection.webserver.api.WebPageContext;
|
||||
import org.openautonomousconnection.webserver.utils.WebHasher;
|
||||
import org.openautonomousconnection.webserver.utils.RequestParams;
|
||||
import org.openautonomousconnection.webserver.utils.WebHasher;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
@@ -21,7 +21,8 @@ public final class JavaPageDispatcher {
|
||||
|
||||
private static final JavaRouteRegistry ROUTES = new JavaRouteRegistry();
|
||||
|
||||
private JavaPageDispatcher() {}
|
||||
private JavaPageDispatcher() {
|
||||
}
|
||||
|
||||
public static WebResponsePacket dispatch(
|
||||
CustomConnectedClient client,
|
||||
|
||||
@@ -4,7 +4,10 @@ import org.openautonomousconnection.webserver.api.Route;
|
||||
import org.openautonomousconnection.webserver.api.WebPage;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.*;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Deque;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
@@ -18,24 +21,50 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
*/
|
||||
public final class JavaRouteRegistry {
|
||||
|
||||
private static final class RouteEntry {
|
||||
final File sourceFile;
|
||||
final long lastModified;
|
||||
final String fqcn;
|
||||
final boolean routable;
|
||||
|
||||
RouteEntry(File sourceFile, long lastModified, String fqcn, boolean routable) {
|
||||
this.sourceFile = sourceFile;
|
||||
this.lastModified = lastModified;
|
||||
this.fqcn = fqcn;
|
||||
this.routable = routable;
|
||||
}
|
||||
}
|
||||
|
||||
private final JavaPageCache cache = new JavaPageCache();
|
||||
private final ConcurrentHashMap<String, RouteEntry> routes = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<String, Long> scanState = new ConcurrentHashMap<>();
|
||||
|
||||
private static String normalizeRoute(String value) {
|
||||
if (value == null) return "/";
|
||||
String v = value.trim();
|
||||
if (v.isEmpty()) return "/";
|
||||
if (!v.startsWith("/")) v = "/" + v;
|
||||
return v;
|
||||
}
|
||||
|
||||
private static List<File> listJavaFiles(File root) {
|
||||
ArrayList<File> out = new ArrayList<>();
|
||||
Deque<File> stack = new ArrayDeque<>();
|
||||
stack.push(root);
|
||||
|
||||
while (!stack.isEmpty()) {
|
||||
File cur = stack.pop();
|
||||
File[] children = cur.listFiles();
|
||||
if (children == null) continue;
|
||||
|
||||
for (File c : children) {
|
||||
if (c.isDirectory()) {
|
||||
stack.push(c);
|
||||
} else if (c.isFile() && c.getName().endsWith(".java")) {
|
||||
out.add(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
private static long folderLastModified(File folder) {
|
||||
long lm = folder.lastModified();
|
||||
File[] children = folder.listFiles();
|
||||
if (children == null) return lm;
|
||||
for (File c : children) {
|
||||
lm = Math.max(lm, c.isDirectory() ? folderLastModified(c) : c.lastModified());
|
||||
}
|
||||
return lm;
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes registry when content folder changed.
|
||||
*
|
||||
@@ -88,52 +117,16 @@ public final class JavaRouteRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
private static String normalizeRoute(String value) {
|
||||
if (value == null) return "/";
|
||||
String v = value.trim();
|
||||
if (v.isEmpty()) return "/";
|
||||
if (!v.startsWith("/")) v = "/" + v;
|
||||
return v;
|
||||
}
|
||||
|
||||
private static List<File> listJavaFiles(File root) {
|
||||
ArrayList<File> out = new ArrayList<>();
|
||||
Deque<File> stack = new ArrayDeque<>();
|
||||
stack.push(root);
|
||||
|
||||
while (!stack.isEmpty()) {
|
||||
File cur = stack.pop();
|
||||
File[] children = cur.listFiles();
|
||||
if (children == null) continue;
|
||||
|
||||
for (File c : children) {
|
||||
if (c.isDirectory()) {
|
||||
stack.push(c);
|
||||
} else if (c.isFile() && c.getName().endsWith(".java")) {
|
||||
out.add(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
private static long folderLastModified(File folder) {
|
||||
long lm = folder.lastModified();
|
||||
File[] children = folder.listFiles();
|
||||
if (children == null) return lm;
|
||||
for (File c : children) {
|
||||
lm = Math.max(lm, c.isDirectory() ? folderLastModified(c) : c.lastModified());
|
||||
}
|
||||
return lm;
|
||||
private record RouteEntry(File sourceFile, long lastModified, String fqcn, boolean routable) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Lookup result for a route.
|
||||
*
|
||||
* @param sourceFile Java source file
|
||||
* @param fqcn fully qualified class name
|
||||
* @param routable true if @Route + implements WebPage
|
||||
* @param fqcn fully qualified class name
|
||||
* @param routable true if @Route + implements WebPage
|
||||
*/
|
||||
public record RouteLookupResult(File sourceFile, String fqcn, boolean routable) {}
|
||||
public record RouteLookupResult(File sourceFile, String fqcn, boolean routable) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@ import java.nio.charset.StandardCharsets;
|
||||
*/
|
||||
public final class Html {
|
||||
|
||||
private Html() {}
|
||||
private Html() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes text for HTML.
|
||||
@@ -37,7 +38,7 @@ public final class Html {
|
||||
* Simple page wrapper.
|
||||
*
|
||||
* @param title title
|
||||
* @param body body html
|
||||
* @param body body html
|
||||
* @return full html
|
||||
*/
|
||||
public static String page(String title, String body) {
|
||||
|
||||
@@ -88,7 +88,7 @@ public final class RequestParams {
|
||||
* Returns SHA-256 hex of a header value.
|
||||
*
|
||||
* @param hasher hasher
|
||||
* @param key header key
|
||||
* @param key header key
|
||||
* @return sha256 hex (or null if missing)
|
||||
*/
|
||||
public String getSha256Hex(WebHasher hasher, String key) {
|
||||
|
||||
@@ -26,8 +26,8 @@ public final class WebHasher {
|
||||
* Creates a hasher.
|
||||
*
|
||||
* @param pbkdf2Iterations iterations (recommended >= 100k)
|
||||
* @param pbkdf2SaltBytes salt bytes
|
||||
* @param pbkdf2KeyBytes derived key bytes
|
||||
* @param pbkdf2SaltBytes salt bytes
|
||||
* @param pbkdf2KeyBytes derived key bytes
|
||||
*/
|
||||
public WebHasher(int pbkdf2Iterations, int pbkdf2SaltBytes, int pbkdf2KeyBytes) {
|
||||
if (pbkdf2Iterations < 10_000) throw new IllegalArgumentException("pbkdf2Iterations too low");
|
||||
@@ -38,6 +38,35 @@ public final class WebHasher {
|
||||
this.pbkdf2KeyBytes = pbkdf2KeyBytes;
|
||||
}
|
||||
|
||||
private static byte[] derive(char[] password, byte[] salt, int iterations, int keyBytes) {
|
||||
try {
|
||||
PBEKeySpec spec = new PBEKeySpec(password, salt, iterations, keyBytes * 8);
|
||||
SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
|
||||
return skf.generateSecret(spec).getEncoded();
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("PBKDF2 not available", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean constantTimeEquals(byte[] a, byte[] b) {
|
||||
if (a == null || b == null) return false;
|
||||
if (a.length != b.length) return false;
|
||||
int r = 0;
|
||||
for (int i = 0; i < a.length; i++) r |= (a[i] ^ b[i]);
|
||||
return r == 0;
|
||||
}
|
||||
|
||||
private static String toHexLower(byte[] data) {
|
||||
final char[] hex = "0123456789abcdef".toCharArray();
|
||||
char[] out = new char[data.length * 2];
|
||||
int i = 0;
|
||||
for (byte b : data) {
|
||||
out[i++] = hex[(b >>> 4) & 0x0F];
|
||||
out[i++] = hex[b & 0x0F];
|
||||
}
|
||||
return new String(out);
|
||||
}
|
||||
|
||||
/**
|
||||
* SHA-256 hashes text (lowercase hex).
|
||||
*
|
||||
@@ -77,7 +106,7 @@ public final class WebHasher {
|
||||
* Verifies a raw password against a stored PBKDF2 string.
|
||||
*
|
||||
* @param password raw password
|
||||
* @param stored stored format
|
||||
* @param stored stored format
|
||||
* @return true if valid
|
||||
*/
|
||||
public boolean pbkdf2Verify(String password, String stored) {
|
||||
@@ -107,33 +136,4 @@ public final class WebHasher {
|
||||
byte[] actual = derive(password.toCharArray(), salt, it, expected.length);
|
||||
return constantTimeEquals(expected, actual);
|
||||
}
|
||||
|
||||
private static byte[] derive(char[] password, byte[] salt, int iterations, int keyBytes) {
|
||||
try {
|
||||
PBEKeySpec spec = new PBEKeySpec(password, salt, iterations, keyBytes * 8);
|
||||
SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
|
||||
return skf.generateSecret(spec).getEncoded();
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("PBKDF2 not available", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean constantTimeEquals(byte[] a, byte[] b) {
|
||||
if (a == null || b == null) return false;
|
||||
if (a.length != b.length) return false;
|
||||
int r = 0;
|
||||
for (int i = 0; i < a.length; i++) r |= (a[i] ^ b[i]);
|
||||
return r == 0;
|
||||
}
|
||||
|
||||
private static String toHexLower(byte[] data) {
|
||||
final char[] hex = "0123456789abcdef".toCharArray();
|
||||
char[] out = new char[data.length * 2];
|
||||
int i = 0;
|
||||
for (byte b : data) {
|
||||
out[i++] = hex[(b >>> 4) & 0x0F];
|
||||
out[i++] = hex[b & 0x0F];
|
||||
}
|
||||
return new String(out);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user