Finished
This commit is contained in:
10
README.MD
10
README.MD
@@ -6,15 +6,21 @@ Feel free to join our Discord.
|
||||
|
||||
## License Notice
|
||||
|
||||
This project (OAC) is licensed under the [Open Autonomous Public License (OAPL)](https://open-autonomous-connection.org/license.html).
|
||||
This project (OAC) is licensed under
|
||||
the [Open Autonomous Public License (OAPL)](https://open-autonomous-connection.org/license.html).
|
||||
|
||||
**Third-party components:**
|
||||
|
||||
- *UnlegitLibrary* is authored by the same copyright holder and is used here under a special agreement:
|
||||
While [UnlegitLibrary](https://repo.unlegitdqrk.dev/UnlegitDqrk/unlegitlibrary/) is generally distributed under the [GNU GPLv3](https://repo.unlegitdqrk.dev/UnlegitDqrk/unlegitlibrary/src/branch/master/LICENSE),
|
||||
While [UnlegitLibrary](https://repo.unlegitdqrk.dev/UnlegitDqrk/unlegitlibrary/) is generally distributed under
|
||||
the [GNU GPLv3](https://repo.unlegitdqrk.dev/UnlegitDqrk/unlegitlibrary/src/branch/master/LICENSE),
|
||||
it is additionally licensed under OAPL **exclusively for the OAC project**.
|
||||
Therefore, within OAC, the OAPL terms apply to UnlegitLibrary as well.
|
||||
|
||||
# Bugs/Problems
|
||||
|
||||
# In progress
|
||||
|
||||
# TODO
|
||||
|
||||
everything
|
||||
@@ -3,7 +3,7 @@
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.openautonomousconnection</groupId>
|
||||
<artifactId>WebServer</artifactId>
|
||||
<version>1.0.0-BETA.1.10</version>
|
||||
<version>1.0.0-BETA.1.0</version>
|
||||
<description>The default DNS-Server</description>
|
||||
<url>https://open-autonomous-connection.org/</url>
|
||||
<issueManagement>
|
||||
@@ -40,7 +40,8 @@
|
||||
<license>
|
||||
<name>GNU General Public License v3.0</name>
|
||||
<url>https://www.gnu.org/licenses/gpl-3.0.html</url>
|
||||
<comments>Default license: Applies to all users and projects unless an explicit alternative license has been granted.</comments>
|
||||
<comments>Default license: Applies to all users and projects unless an explicit alternative license has been
|
||||
granted.</comments>
|
||||
</license>
|
||||
<license>
|
||||
<name>LPGL 3</name>
|
||||
|
||||
17
pom.xml
17
pom.xml
@@ -1,12 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>org.openautonomousconnection</groupId>
|
||||
<artifactId>WebServer</artifactId>
|
||||
<version>1.0.0-BETA.1.10</version>
|
||||
<version>1.0.0-BETA.1.0</version>
|
||||
<organization>
|
||||
<name>Open Autonomous Connection</name>
|
||||
<url>https://open-autonomous-connection.org/</url>
|
||||
@@ -65,7 +65,8 @@
|
||||
<name>GNU General Public License v3.0</name>
|
||||
<url>https://www.gnu.org/licenses/gpl-3.0.html</url>
|
||||
<comments>
|
||||
Default license: Applies to all users and projects unless an explicit alternative license has been granted.
|
||||
Default license: Applies to all users and projects unless an explicit alternative license has been
|
||||
granted.
|
||||
</comments>
|
||||
</license>
|
||||
<license>
|
||||
@@ -98,7 +99,8 @@
|
||||
</license>
|
||||
<license>
|
||||
<name>mariadb</name>
|
||||
<url>https://mariadb.com/docs/general-resources/community/community/faq/licensing-questions/licensing-faq</url>
|
||||
<url>https://mariadb.com/docs/general-resources/community/community/faq/licensing-questions/licensing-faq
|
||||
</url>
|
||||
</license>
|
||||
</licenses>
|
||||
|
||||
@@ -116,7 +118,7 @@
|
||||
<dependency>
|
||||
<groupId>org.openautonomousconnection</groupId>
|
||||
<artifactId>Protocol</artifactId>
|
||||
<version>1.0.0-BETA.7.7</version>
|
||||
<version>1.0.0-BETA.1.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
@@ -145,7 +147,8 @@
|
||||
</goals>
|
||||
<configuration>
|
||||
<transformers>
|
||||
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||
<transformer
|
||||
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||
<mainClass>org.openautonomousconnection.webserver.Main</mainClass>
|
||||
</transformer>
|
||||
</transformers>
|
||||
|
||||
@@ -3,26 +3,18 @@ package org.openautonomousconnection.webserver;
|
||||
import dev.unlegitdqrk.unlegitlibrary.command.CommandExecutor;
|
||||
import dev.unlegitdqrk.unlegitlibrary.command.CommandManager;
|
||||
import dev.unlegitdqrk.unlegitlibrary.command.CommandPermission;
|
||||
import dev.unlegitdqrk.unlegitlibrary.event.EventListener;
|
||||
import dev.unlegitdqrk.unlegitlibrary.event.EventManager;
|
||||
import dev.unlegitdqrk.unlegitlibrary.event.Listener;
|
||||
import dev.unlegitdqrk.unlegitlibrary.file.ConfigurationManager;
|
||||
import dev.unlegitdqrk.unlegitlibrary.network.system.client.events.state.ClientConnectedEvent;
|
||||
import dev.unlegitdqrk.unlegitlibrary.network.system.packets.PacketHandler;
|
||||
import dev.unlegitdqrk.unlegitlibrary.network.system.server.NetworkServer;
|
||||
import dev.unlegitdqrk.unlegitlibrary.network.system.server.events.packets.S_PacketSendEvent;
|
||||
import dev.unlegitdqrk.unlegitlibrary.network.system.server.events.state.ServerStoppedEvent;
|
||||
import dev.unlegitdqrk.unlegitlibrary.network.system.utils.ClientAuthMode;
|
||||
import lombok.Getter;
|
||||
import org.openautonomousconnection.protocol.ProtocolBridge;
|
||||
import org.openautonomousconnection.protocol.ProtocolValues;
|
||||
import org.openautonomousconnection.protocol.side.server.events.S_CustomClientConnectedEvent;
|
||||
import org.openautonomousconnection.protocol.versions.ProtocolVersion;
|
||||
import org.openautonomousconnection.webserver.commands.StopCommand;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Scanner;
|
||||
|
||||
public class Main {
|
||||
@@ -44,7 +36,8 @@ public class Main {
|
||||
values.packetHandler = new PacketHandler();
|
||||
values.eventManager = new EventManager();
|
||||
|
||||
if (!Files.exists(new File("config.properties").toPath())) Files.createFile(new File("config.properties").toPath());
|
||||
if (!Files.exists(new File("config.properties").toPath()))
|
||||
Files.createFile(new File("config.properties").toPath());
|
||||
|
||||
ConfigurationManager config = new ConfigurationManager(new File("config.properties"));
|
||||
config.loadProperties();
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
package org.openautonomousconnection.webserver;
|
||||
|
||||
import dev.unlegitdqrk.unlegitlibrary.event.Listener;
|
||||
import dev.unlegitdqrk.unlegitlibrary.network.system.server.events.packets.S_PacketReadEvent;
|
||||
import dev.unlegitdqrk.unlegitlibrary.network.system.server.events.packets.S_PacketSendEvent;
|
||||
import dev.unlegitdqrk.unlegitlibrary.network.system.utils.TransportProtocol;
|
||||
import lombok.Getter;
|
||||
import org.openautonomousconnection.protocol.annotations.ProtocolInfo;
|
||||
@@ -94,20 +91,6 @@ public final class WebServer extends ProtocolWebServer {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Temporary solution until its fixed in Protocol
|
||||
@Listener
|
||||
public void onRequest(S_PacketReadEvent event) {
|
||||
if (event.getPacket() instanceof WebRequestPacket) {
|
||||
try {
|
||||
event.getClient().sendPacket(
|
||||
onWebRequest(getClientByID(event.getClient().getUniqueID()), (WebRequestPacket) event.getPacket()),
|
||||
TransportProtocol.TCP);
|
||||
} catch (IOException e) {
|
||||
getProtocolBridge().getLogger().exception("Failed to send web response", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private WebResponsePacket serveFile(CustomConnectedClient client, String path) throws Exception {
|
||||
if (path.startsWith("/")) path = path.substring(1);
|
||||
if (path.isEmpty()) path = "index.html";
|
||||
@@ -115,11 +98,11 @@ public final class WebServer extends ProtocolWebServer {
|
||||
File root = getContentFolder().getCanonicalFile();
|
||||
File file = new File(root, path).getCanonicalFile();
|
||||
|
||||
if (!file.getPath().startsWith(root.getPath())) {
|
||||
if (!RuleManager.isAllowed(path)) {
|
||||
return new WebResponsePacket(403, "text/plain; charset=utf-8", HeaderMaps.mutable(), "Forbidden".getBytes());
|
||||
}
|
||||
if (!file.exists() || !file.isFile()) {
|
||||
return new WebResponsePacket(404, "text/plain; charset=utf-8", HeaderMaps.mutable(), "Not found".getBytes());
|
||||
if (RuleManager.isDenied(path)) {
|
||||
return new WebResponsePacket(403, "text/plain; charset=utf-8", HeaderMaps.mutable(), "Forbidden".getBytes());
|
||||
}
|
||||
|
||||
String contentType = ContentTypeResolver.resolve(file.getName());
|
||||
|
||||
@@ -5,14 +5,17 @@ import org.openautonomousconnection.protocol.side.web.ProtocolWebServer;
|
||||
import org.openautonomousconnection.protocol.side.web.managers.SessionManager;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Provides session-related information for Java WebPages.
|
||||
* Thin layer on top of SessionManager.
|
||||
* Reads session id primarily from Cookie header ("session=...").
|
||||
*/
|
||||
public final class SessionContext {
|
||||
|
||||
private static final String COOKIE_NAME = "session";
|
||||
|
||||
private final String sessionId;
|
||||
private final String user;
|
||||
private final boolean valid;
|
||||
@@ -23,16 +26,30 @@ public final class SessionContext {
|
||||
this.valid = valid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a SessionContext from request headers (case-insensitive).
|
||||
*
|
||||
* @param client connected client
|
||||
* @param server web server
|
||||
* @param headers request headers
|
||||
* @return session context
|
||||
* @throws IOException on errors
|
||||
*/
|
||||
public static SessionContext from(CustomConnectedClient client, ProtocolWebServer server, Map<String, String> headers) throws IOException {
|
||||
if (headers == null) return new SessionContext(null, null, false);
|
||||
if (headers == null || headers.isEmpty()) return new SessionContext(null, null, false);
|
||||
|
||||
String sessionId = headers.get("session");
|
||||
if (sessionId == null) return new SessionContext(null, null, false);
|
||||
String sessionId = extractSessionId(headers);
|
||||
if (sessionId == null || sessionId.isBlank()) return new SessionContext(null, null, false);
|
||||
|
||||
String ip = (client.getConnection().getTcpSocket() != null && client.getConnection().getTcpSocket().getInetAddress() != null)
|
||||
? client.getConnection().getTcpSocket().getInetAddress().getHostAddress() : "";
|
||||
String ip = (client != null
|
||||
&& client.getConnection() != null
|
||||
&& client.getConnection().getTcpSocket() != null
|
||||
&& client.getConnection().getTcpSocket().getInetAddress() != null)
|
||||
? client.getConnection().getTcpSocket().getInetAddress().getHostAddress()
|
||||
: "";
|
||||
|
||||
String userAgent = headers.getOrDefault("user-agent", "");
|
||||
String userAgent = getHeaderIgnoreCase(headers, "user-agent");
|
||||
if (userAgent == null) userAgent = "";
|
||||
|
||||
boolean valid = SessionManager.isValid(sessionId, ip, userAgent, server);
|
||||
if (!valid) return new SessionContext(sessionId, null, false);
|
||||
@@ -41,14 +58,64 @@ public final class SessionContext {
|
||||
return new SessionContext(sessionId, user, true);
|
||||
}
|
||||
|
||||
private static String extractSessionId(Map<String, String> headers) {
|
||||
// 1) Cookie header preferred
|
||||
String cookie = getHeaderIgnoreCase(headers, "cookie");
|
||||
String fromCookie = parseCookie(cookie, COOKIE_NAME);
|
||||
if (fromCookie != null && !fromCookie.isBlank()) return fromCookie;
|
||||
|
||||
// 2) Backward-compatible fallback: old custom header
|
||||
String legacy = getHeaderIgnoreCase(headers, "session");
|
||||
return (legacy == null || legacy.isBlank()) ? null : legacy.trim();
|
||||
}
|
||||
|
||||
private static String parseCookie(String cookieHeader, String name) {
|
||||
if (cookieHeader == null || cookieHeader.isBlank() || name == null || name.isBlank()) return null;
|
||||
|
||||
String[] parts = cookieHeader.split(";");
|
||||
for (String p : parts) {
|
||||
String t = p.trim();
|
||||
int eq = t.indexOf('=');
|
||||
if (eq <= 0) continue;
|
||||
|
||||
String k = t.substring(0, eq).trim();
|
||||
if (!k.equalsIgnoreCase(name)) continue;
|
||||
|
||||
String v = t.substring(eq + 1).trim();
|
||||
return v.isEmpty() ? null : v;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String getHeaderIgnoreCase(Map<String, String> headers, String key) {
|
||||
if (key == null) return null;
|
||||
String needle = key.trim().toLowerCase(Locale.ROOT);
|
||||
for (Map.Entry<String, String> e : headers.entrySet()) {
|
||||
if (e.getKey() == null) continue;
|
||||
if (e.getKey().trim().toLowerCase(Locale.ROOT).equals(needle)) {
|
||||
return e.getValue();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return whether session is valid
|
||||
*/
|
||||
public boolean isValid() {
|
||||
return valid;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return session id
|
||||
*/
|
||||
public String getSessionId() {
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return user id stored in session (string)
|
||||
*/
|
||||
public String getUser() {
|
||||
return user;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
package org.openautonomousconnection.webserver.runtime;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@@ -43,7 +39,8 @@ public final class JavaPageCache {
|
||||
return loaded;
|
||||
}
|
||||
|
||||
private record Entry(long contentLastModified, LoadedClass loadedClass) { }
|
||||
private record Entry(long contentLastModified, LoadedClass loadedClass) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Result wrapper holding both the ClassLoader and the loaded class.
|
||||
@@ -51,5 +48,6 @@ public final class JavaPageCache {
|
||||
* @param classLoader loader used to load the class
|
||||
* @param clazz loaded class
|
||||
*/
|
||||
public record LoadedClass(ClassLoader classLoader, Class<?> clazz) { }
|
||||
public record LoadedClass(ClassLoader classLoader, Class<?> clazz) {
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,8 @@ public final class JavaPageCompiler {
|
||||
Files.createDirectories(buildDir);
|
||||
|
||||
List<File> sources = listJavaFiles(contentRoot);
|
||||
if (sources.isEmpty()) throw new IllegalStateException("No .java files found under content root: " + contentRoot);
|
||||
if (sources.isEmpty())
|
||||
throw new IllegalStateException("No .java files found under content root: " + contentRoot);
|
||||
|
||||
try (StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, StandardCharsets.UTF_8)) {
|
||||
Iterable<? extends JavaFileObject> units = fileManager.getJavaFileObjectsFromFiles(sources);
|
||||
|
||||
@@ -13,7 +13,6 @@ import org.openautonomousconnection.webserver.utils.WebHasher;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Dispatches Java WebPages using {@code @Route} annotation.
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package org.openautonomousconnection.webserver.runtime;
|
||||
|
||||
import org.openautonomousconnection.webserver.api.Route;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
@@ -20,68 +18,10 @@ import java.util.regex.Pattern;
|
||||
*/
|
||||
public final class JavaRouteRegistry {
|
||||
|
||||
private final ConcurrentHashMap<String, RouteEntry> routes = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<String, Long> scanState = new ConcurrentHashMap<>();
|
||||
|
||||
private static final Pattern PACKAGE_PATTERN = Pattern.compile("^\\s*package\\s+([a-zA-Z0-9_\\.]+)\\s*;\\s*$");
|
||||
private static final Pattern ROUTE_PATTERN = Pattern.compile("@Route\\s*\\(\\s*path\\s*=\\s*\"([^\"]+)\"\\s*\\)");
|
||||
|
||||
/**
|
||||
* Returns the aggregate last modified timestamp of the content tree.
|
||||
*
|
||||
* @param contentRoot content root
|
||||
* @return last modified max value
|
||||
*/
|
||||
public long currentContentLastModified(File contentRoot) {
|
||||
return folderLastModified(contentRoot);
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes registry when content folder changed.
|
||||
*
|
||||
* @param contentRoot content root
|
||||
*/
|
||||
public void refreshIfNeeded(File contentRoot) {
|
||||
if (contentRoot == null) return;
|
||||
|
||||
long lm = folderLastModified(contentRoot);
|
||||
Long prev = scanState.get("lm");
|
||||
|
||||
if (prev != null && prev == lm && !routes.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
scanState.put("lm", lm);
|
||||
rebuild(contentRoot);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a route mapping.
|
||||
*
|
||||
* @param route route path
|
||||
* @return result or null
|
||||
*/
|
||||
public RouteLookupResult find(String route) {
|
||||
RouteEntry e = routes.get(route);
|
||||
if (e == null) return null;
|
||||
return new RouteLookupResult(e.sourceFile, e.fqcn);
|
||||
}
|
||||
|
||||
private void rebuild(File contentRoot) {
|
||||
routes.clear();
|
||||
|
||||
for (File f : listJavaFiles(contentRoot)) {
|
||||
try {
|
||||
RouteMeta meta = parseRouteMeta(f);
|
||||
if (meta == null) continue;
|
||||
|
||||
String route = normalizeRoute(meta.routePath());
|
||||
routes.put(route, new RouteEntry(f, meta.fqcn()));
|
||||
} catch (Exception ignored) {
|
||||
// Ignore invalid sources; compilation will surface errors later when requested.
|
||||
}
|
||||
}
|
||||
}
|
||||
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 "/";
|
||||
@@ -151,7 +91,65 @@ public final class JavaRouteRegistry {
|
||||
return new RouteMeta(routePath, fqcn);
|
||||
}
|
||||
|
||||
private record RouteEntry(File sourceFile, String fqcn) { }
|
||||
/**
|
||||
* Returns the aggregate last modified timestamp of the content tree.
|
||||
*
|
||||
* @param contentRoot content root
|
||||
* @return last modified max value
|
||||
*/
|
||||
public long currentContentLastModified(File contentRoot) {
|
||||
return folderLastModified(contentRoot);
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes registry when content folder changed.
|
||||
*
|
||||
* @param contentRoot content root
|
||||
*/
|
||||
public void refreshIfNeeded(File contentRoot) {
|
||||
if (contentRoot == null) return;
|
||||
|
||||
long lm = folderLastModified(contentRoot);
|
||||
Long prev = scanState.get("lm");
|
||||
|
||||
if (prev != null && prev == lm && !routes.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
scanState.put("lm", lm);
|
||||
rebuild(contentRoot);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a route mapping.
|
||||
*
|
||||
* @param route route path
|
||||
* @return result or null
|
||||
*/
|
||||
public RouteLookupResult find(String route) {
|
||||
RouteEntry e = routes.get(route);
|
||||
if (e == null) return null;
|
||||
return new RouteLookupResult(e.sourceFile, e.fqcn);
|
||||
}
|
||||
|
||||
private void rebuild(File contentRoot) {
|
||||
routes.clear();
|
||||
|
||||
for (File f : listJavaFiles(contentRoot)) {
|
||||
try {
|
||||
RouteMeta meta = parseRouteMeta(f);
|
||||
if (meta == null) continue;
|
||||
|
||||
String route = normalizeRoute(meta.routePath());
|
||||
routes.put(route, new RouteEntry(f, meta.fqcn()));
|
||||
} catch (Exception ignored) {
|
||||
// Ignore invalid sources; compilation will surface errors later when requested.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private record RouteEntry(File sourceFile, String fqcn) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Lookup result for a route.
|
||||
@@ -159,7 +157,9 @@ public final class JavaRouteRegistry {
|
||||
* @param sourceFile Java source file
|
||||
* @param fqcn fully qualified class name
|
||||
*/
|
||||
public record RouteLookupResult(File sourceFile, String fqcn) { }
|
||||
public record RouteLookupResult(File sourceFile, String fqcn) {
|
||||
}
|
||||
|
||||
private record RouteMeta(String routePath, String fqcn) { }
|
||||
private record RouteMeta(String routePath, String fqcn) {
|
||||
}
|
||||
}
|
||||
@@ -8,8 +8,6 @@ import java.io.ByteArrayOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Simple HTTPS -> OAC proxy helper.
|
||||
|
||||
@@ -0,0 +1,393 @@
|
||||
package org.openautonomousconnection.webserver.utils;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Parses and merges request parameters from:
|
||||
* <ul>
|
||||
* <li>URL query string (GET params)</li>
|
||||
* <li>POST body (application/x-www-form-urlencoded or multipart/form-data text fields)</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>Precedence: POST body overrides query string on key collision.</p>
|
||||
*/
|
||||
public final class MergedRequestParams {
|
||||
|
||||
private final Map<String, List<String>> params;
|
||||
|
||||
private MergedRequestParams(Map<String, List<String>> params) {
|
||||
this.params = params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link MergedRequestParams} instance by parsing query string and POST body.
|
||||
*
|
||||
* @param rawTarget the raw request target (path + optional query), e.g. "/dashboard.html?action=..."
|
||||
* @param headers request headers (may be null)
|
||||
* @param body request body bytes (may be null/empty)
|
||||
* @return merged parameters
|
||||
*/
|
||||
public static MergedRequestParams from(String rawTarget, Map<String, String> headers, byte[] body) {
|
||||
Map<String, List<String>> merged = new LinkedHashMap<>();
|
||||
|
||||
// 1) Query string
|
||||
String query = extractQuery(rawTarget);
|
||||
if (query != null && !query.isBlank()) {
|
||||
mergeInto(merged, parseUrlEncoded(query, StandardCharsets.UTF_8), false);
|
||||
}
|
||||
|
||||
// 2) Body
|
||||
if (body != null && body.length > 0) {
|
||||
String contentType = header(headers, "content-type");
|
||||
Map<String, List<String>> bodyParams = parseBody(contentType, body);
|
||||
mergeInto(merged, bodyParams, true);
|
||||
}
|
||||
|
||||
return new MergedRequestParams(merged);
|
||||
}
|
||||
|
||||
private static String extractQuery(String rawTarget) {
|
||||
if (rawTarget == null) return null;
|
||||
int q = rawTarget.indexOf('?');
|
||||
if (q < 0) return null;
|
||||
if (q == rawTarget.length() - 1) return "";
|
||||
return rawTarget.substring(q + 1);
|
||||
}
|
||||
|
||||
private static String header(Map<String, String> headers, String keyLower) {
|
||||
if (headers == null || headers.isEmpty()) return null;
|
||||
for (Map.Entry<String, String> e : headers.entrySet()) {
|
||||
if (e.getKey() == null) continue;
|
||||
if (e.getKey().trim().equalsIgnoreCase(keyLower)) {
|
||||
return e.getValue();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void mergeInto(Map<String, List<String>> target, Map<String, List<String>> src, boolean override) {
|
||||
if (src == null || src.isEmpty()) return;
|
||||
for (Map.Entry<String, List<String>> e : src.entrySet()) {
|
||||
if (e.getKey() == null) continue;
|
||||
String k = e.getKey();
|
||||
List<String> vals = e.getValue() == null ? List.of() : e.getValue();
|
||||
if (!override && target.containsKey(k)) {
|
||||
// append
|
||||
target.get(k).addAll(vals);
|
||||
continue;
|
||||
}
|
||||
// override or insert
|
||||
target.put(k, new ArrayList<>(vals));
|
||||
}
|
||||
}
|
||||
|
||||
private static Map<String, List<String>> parseBody(String contentType, byte[] body) {
|
||||
if (contentType != null) {
|
||||
String ct = contentType.toLowerCase(Locale.ROOT);
|
||||
if (ct.startsWith("application/x-www-form-urlencoded")) {
|
||||
Charset cs = charsetFromContentType(contentType, StandardCharsets.UTF_8);
|
||||
return parseUrlEncoded(new String(body, cs), cs);
|
||||
}
|
||||
if (ct.startsWith("multipart/form-data")) {
|
||||
String boundary = boundaryFromContentType(contentType);
|
||||
if (boundary != null && !boundary.isBlank()) {
|
||||
return parseMultipartTextFields(body, boundary, StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: try urlencoded safely (common when content-type is missing in custom stacks)
|
||||
return parseUrlEncoded(new String(body, StandardCharsets.UTF_8), StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private static Charset charsetFromContentType(String contentType, Charset def) {
|
||||
if (contentType == null) return def;
|
||||
String[] parts = contentType.split(";");
|
||||
for (String p : parts) {
|
||||
String s = p.trim().toLowerCase(Locale.ROOT);
|
||||
if (s.startsWith("charset=")) {
|
||||
String name = s.substring("charset=".length()).trim();
|
||||
try {
|
||||
return Charset.forName(name);
|
||||
} catch (Exception ignored) {
|
||||
return def;
|
||||
}
|
||||
}
|
||||
}
|
||||
return def;
|
||||
}
|
||||
|
||||
private static String boundaryFromContentType(String contentType) {
|
||||
if (contentType == null) return null;
|
||||
String[] parts = contentType.split(";");
|
||||
for (String p : parts) {
|
||||
String s = p.trim();
|
||||
if (s.toLowerCase(Locale.ROOT).startsWith("boundary=")) {
|
||||
String b = s.substring("boundary=".length()).trim();
|
||||
if (b.startsWith("\"") && b.endsWith("\"") && b.length() >= 2) {
|
||||
b = b.substring(1, b.length() - 1);
|
||||
}
|
||||
return b;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ----------------------------- Internals -----------------------------
|
||||
|
||||
/**
|
||||
* Parses "application/x-www-form-urlencoded".
|
||||
*/
|
||||
private static Map<String, List<String>> parseUrlEncoded(String s, Charset charset) {
|
||||
Map<String, List<String>> out = new LinkedHashMap<>();
|
||||
if (s == null || s.isBlank()) return out;
|
||||
|
||||
String[] pairs = s.split("&");
|
||||
for (String pair : pairs) {
|
||||
if (pair.isEmpty()) continue;
|
||||
int eq = pair.indexOf('=');
|
||||
String k;
|
||||
String v;
|
||||
if (eq < 0) {
|
||||
k = urlDecode(pair, charset);
|
||||
v = "";
|
||||
} else {
|
||||
k = urlDecode(pair.substring(0, eq), charset);
|
||||
v = urlDecode(pair.substring(eq + 1), charset);
|
||||
}
|
||||
if (k == null || k.isBlank()) continue;
|
||||
out.computeIfAbsent(k, __ -> new ArrayList<>()).add(v == null ? "" : v);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimal multipart parser for text fields only.
|
||||
* <p>Ignores file uploads and binary content.</p>
|
||||
*/
|
||||
private static Map<String, List<String>> parseMultipartTextFields(byte[] body, String boundary, Charset charset) {
|
||||
Map<String, List<String>> out = new LinkedHashMap<>();
|
||||
if (body == null || body.length == 0) return out;
|
||||
|
||||
byte[] boundaryBytes = ("--" + boundary).getBytes(StandardCharsets.ISO_8859_1);
|
||||
byte[] endBoundaryBytes = ("--" + boundary + "--").getBytes(StandardCharsets.ISO_8859_1);
|
||||
|
||||
int i = 0;
|
||||
while (i < body.length) {
|
||||
int bStart = indexOf(body, boundaryBytes, i);
|
||||
if (bStart < 0) break;
|
||||
int bLineEnd = indexOfCrlf(body, bStart);
|
||||
if (bLineEnd < 0) break;
|
||||
|
||||
// Check end boundary
|
||||
if (startsWithAt(body, endBoundaryBytes, bStart)) {
|
||||
break;
|
||||
}
|
||||
|
||||
int partStart = bLineEnd + 2; // skip CRLF after boundary line
|
||||
|
||||
int headersEnd = indexOfDoubleCrlf(body, partStart);
|
||||
if (headersEnd < 0) break;
|
||||
|
||||
String headersStr = new String(body, partStart, headersEnd - partStart, StandardCharsets.ISO_8859_1);
|
||||
String name = extractMultipartName(headersStr);
|
||||
boolean isFile = isMultipartFile(headersStr);
|
||||
|
||||
int dataStart = headersEnd + 4; // skip CRLFCRLF
|
||||
|
||||
int nextBoundary = indexOf(body, boundaryBytes, dataStart);
|
||||
if (nextBoundary < 0) break;
|
||||
|
||||
int dataEnd = nextBoundary - 2; // strip trailing CRLF before boundary
|
||||
if (dataEnd < dataStart) dataEnd = dataStart;
|
||||
|
||||
if (name != null && !name.isBlank() && !isFile) {
|
||||
String value = new String(body, dataStart, dataEnd - dataStart, charset);
|
||||
out.computeIfAbsent(name, __ -> new ArrayList<>()).add(value);
|
||||
}
|
||||
|
||||
i = nextBoundary;
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
private static String extractMultipartName(String headers) {
|
||||
// Content-Disposition: form-data; name="action"
|
||||
String[] lines = headers.split("\r\n");
|
||||
for (String line : lines) {
|
||||
String lower = line.toLowerCase(Locale.ROOT);
|
||||
if (!lower.startsWith("content-disposition:")) continue;
|
||||
int nameIdx = lower.indexOf("name=");
|
||||
if (nameIdx < 0) continue;
|
||||
String after = line.substring(nameIdx + "name=".length()).trim();
|
||||
if (after.startsWith("\"")) {
|
||||
int end = after.indexOf('"', 1);
|
||||
if (end > 1) return after.substring(1, end);
|
||||
}
|
||||
int semi = after.indexOf(';');
|
||||
if (semi >= 0) after = after.substring(0, semi).trim();
|
||||
return after;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static boolean isMultipartFile(String headers) {
|
||||
String lower = headers.toLowerCase(Locale.ROOT);
|
||||
return lower.contains("filename=");
|
||||
}
|
||||
|
||||
private static int indexOfDoubleCrlf(byte[] haystack, int from) {
|
||||
for (int i = from; i + 3 < haystack.length; i++) {
|
||||
if (haystack[i] == '\r' && haystack[i + 1] == '\n' && haystack[i + 2] == '\r' && haystack[i + 3] == '\n') {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static int indexOfCrlf(byte[] haystack, int from) {
|
||||
for (int i = from; i + 1 < haystack.length; i++) {
|
||||
if (haystack[i] == '\r' && haystack[i + 1] == '\n') return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static boolean startsWithAt(byte[] haystack, byte[] needle, int pos) {
|
||||
if (pos < 0) return false;
|
||||
if (pos + needle.length > haystack.length) return false;
|
||||
for (int i = 0; i < needle.length; i++) {
|
||||
if (haystack[pos + i] != needle[i]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int indexOf(byte[] haystack, byte[] needle, int from) {
|
||||
if (needle.length == 0) return from;
|
||||
outer:
|
||||
for (int i = Math.max(0, from); i <= haystack.length - needle.length; i++) {
|
||||
for (int j = 0; j < needle.length; j++) {
|
||||
if (haystack[i + j] != needle[j]) continue outer;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static String urlDecode(String s, Charset charset) {
|
||||
if (s == null) return null;
|
||||
// Replace '+' with space
|
||||
String in = s.replace('+', ' ');
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream(in.length());
|
||||
for (int i = 0; i < in.length(); i++) {
|
||||
char c = in.charAt(i);
|
||||
if (c == '%' && i + 2 < in.length()) {
|
||||
int hi = hex(in.charAt(i + 1));
|
||||
int lo = hex(in.charAt(i + 2));
|
||||
if (hi >= 0 && lo >= 0) {
|
||||
baos.write((hi << 4) | lo);
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// ISO-8859-1 safe char -> byte
|
||||
if (c <= 0xFF) baos.write((byte) c);
|
||||
else {
|
||||
// for non-latin chars, fall back to UTF-8 bytes of that char
|
||||
byte[] b = String.valueOf(c).getBytes(charset);
|
||||
baos.writeBytes(b);
|
||||
}
|
||||
}
|
||||
return baos.toString(charset);
|
||||
}
|
||||
|
||||
private static int hex(char c) {
|
||||
if (c >= '0' && c <= '9') return c - '0';
|
||||
if (c >= 'a' && c <= 'f') return 10 + (c - 'a');
|
||||
if (c >= 'A' && c <= 'F') return 10 + (c - 'A');
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first value for a key or {@code null} if absent.
|
||||
*
|
||||
* @param key parameter key
|
||||
* @return first value or null
|
||||
*/
|
||||
public String get(String key) {
|
||||
List<String> v = params.get(key);
|
||||
if (v == null || v.isEmpty()) return null;
|
||||
return v.get(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first value for a key, or a default if absent.
|
||||
*
|
||||
* @param key parameter key
|
||||
* @param def default
|
||||
* @return value or default
|
||||
*/
|
||||
public String getOr(String key, String def) {
|
||||
String v = get(key);
|
||||
return v == null ? def : v;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an int parameter or fallback if missing/invalid.
|
||||
*
|
||||
* @param key parameter key
|
||||
* @param def default
|
||||
* @return parsed int or default
|
||||
*/
|
||||
public int getInt(String key, int def) {
|
||||
String v = get(key);
|
||||
if (v == null) return def;
|
||||
try {
|
||||
return Integer.parseInt(v.trim());
|
||||
} catch (Exception ignored) {
|
||||
return def;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if parameter is "1", "true", "yes", "on" (case-insensitive).
|
||||
* Missing parameter returns {@code false}.
|
||||
*
|
||||
* @param key parameter key
|
||||
* @return boolean value
|
||||
*/
|
||||
public boolean getBool(String key) {
|
||||
String v = get(key);
|
||||
if (v == null) return false;
|
||||
String s = v.trim().toLowerCase(Locale.ROOT);
|
||||
return "1".equals(s) || "true".equals(s) || "yes".equals(s) || "on".equals(s);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all values for a key (never null).
|
||||
*
|
||||
* @param key key
|
||||
* @return list (immutable)
|
||||
*/
|
||||
public List<String> getAll(String key) {
|
||||
List<String> v = params.get(key);
|
||||
if (v == null) return List.of();
|
||||
return Collections.unmodifiableList(v);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an immutable view of all parameters.
|
||||
*
|
||||
* @return map
|
||||
*/
|
||||
public Map<String, List<String>> asMap() {
|
||||
Map<String, List<String>> out = new LinkedHashMap<>();
|
||||
for (Map.Entry<String, List<String>> e : params.entrySet()) {
|
||||
out.put(e.getKey(), List.copyOf(e.getValue()));
|
||||
}
|
||||
return Collections.unmodifiableMap(out);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package org.openautonomousconnection.webserver.utils;
|
||||
|
||||
/**
|
||||
* Password hashing interface.
|
||||
*/
|
||||
public interface PasswordHasher {
|
||||
|
||||
/**
|
||||
* Hashes a raw password into a stored representation.
|
||||
*
|
||||
* @param rawPassword raw password
|
||||
* @return stored string
|
||||
*/
|
||||
String hash(String rawPassword);
|
||||
|
||||
/**
|
||||
* Verifies a raw password against a stored representation.
|
||||
*
|
||||
* @param rawPassword raw password
|
||||
* @param stored stored string
|
||||
* @return true if valid
|
||||
*/
|
||||
boolean verify(String rawPassword, String stored);
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package org.openautonomousconnection.webserver.utils;
|
||||
|
||||
import javax.crypto.SecretKeyFactory;
|
||||
import javax.crypto.spec.PBEKeySpec;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
* PBKDF2 password hasher: PBKDF2$sha256$iterations$saltB64$hashB64
|
||||
*/
|
||||
public final class Pbkdf2Sha256Hasher implements PasswordHasher {
|
||||
|
||||
private final SecureRandom rng = new SecureRandom();
|
||||
private final int iterations;
|
||||
private final int saltBytes;
|
||||
private final int keyBytes;
|
||||
|
||||
/**
|
||||
* Creates a PBKDF2 hasher.
|
||||
*
|
||||
* @param iterations iterations (recommend >= 100k)
|
||||
* @param saltBytes salt size
|
||||
* @param keyBytes derived key size
|
||||
*/
|
||||
public Pbkdf2Sha256Hasher(int iterations, int saltBytes, int keyBytes) {
|
||||
if (iterations < 10_000) throw new IllegalArgumentException("iterations too low");
|
||||
if (saltBytes < 16) throw new IllegalArgumentException("saltBytes too low");
|
||||
if (keyBytes < 32) throw new IllegalArgumentException("keyBytes too low");
|
||||
this.iterations = iterations;
|
||||
this.saltBytes = saltBytes;
|
||||
this.keyBytes = keyBytes;
|
||||
}
|
||||
|
||||
private static byte[] pbkdf2(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("PBKDF2WithHmacSHA256 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;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String hash(String rawPassword) {
|
||||
if (rawPassword == null) rawPassword = "";
|
||||
byte[] salt = new byte[saltBytes];
|
||||
rng.nextBytes(salt);
|
||||
byte[] dk = pbkdf2(rawPassword.toCharArray(), salt, iterations, keyBytes);
|
||||
return "PBKDF2$sha256$" + iterations + "$" +
|
||||
Base64.getEncoder().encodeToString(salt) + "$" +
|
||||
Base64.getEncoder().encodeToString(dk);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean verify(String rawPassword, String stored) {
|
||||
if (rawPassword == null) rawPassword = "";
|
||||
if (stored == null || stored.isBlank()) return false;
|
||||
|
||||
try {
|
||||
String[] parts = stored.split("\\$");
|
||||
if (parts.length != 5) return false;
|
||||
if (!"PBKDF2".equals(parts[0])) return false;
|
||||
if (!"sha256".equalsIgnoreCase(parts[1])) return false;
|
||||
|
||||
int it = Integer.parseInt(parts[2]);
|
||||
byte[] salt = Base64.getDecoder().decode(parts[3]);
|
||||
byte[] want = Base64.getDecoder().decode(parts[4]);
|
||||
|
||||
byte[] got = pbkdf2(rawPassword.toCharArray(), salt, it, want.length);
|
||||
return constantTimeEquals(want, got);
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package org.openautonomousconnection.webserver.utils;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.URLDecoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* GET-only support: extracts request target (path + query) and parses query parameters.
|
||||
*/
|
||||
public final class QuerySupport {
|
||||
|
||||
private QuerySupport() {
|
||||
// Utility class
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the raw request target (path + optional query) from a request object via reflection.
|
||||
*
|
||||
* <p>This avoids hard-coding a method name you did not confirm.</p>
|
||||
*
|
||||
* @param request request object (e.g. WebRequestPacket)
|
||||
* @return raw target string, never null
|
||||
*/
|
||||
public static String extractRawTarget(Object request) {
|
||||
if (request == null) return "";
|
||||
|
||||
String[] candidates = {
|
||||
"getTarget",
|
||||
"getRequestTarget",
|
||||
"getPath",
|
||||
"getUrl",
|
||||
"getURI",
|
||||
"getUri"
|
||||
};
|
||||
|
||||
for (String name : candidates) {
|
||||
String v = tryInvokeStringGetter(request, name);
|
||||
if (v != null && !v.isBlank()) return v;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
private static String tryInvokeStringGetter(Object obj, String methodName) {
|
||||
try {
|
||||
Method m = obj.getClass().getMethod(methodName);
|
||||
if (!m.getReturnType().equals(String.class)) return null;
|
||||
return (String) m.invoke(obj);
|
||||
} catch (Exception ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses query parameters from a raw target string.
|
||||
*
|
||||
* @param rawTarget e.g. "/ins/dashboard?action=x&k=v"
|
||||
* @return map of params (decoded), never null
|
||||
*/
|
||||
public static Map<String, String> parseQuery(String rawTarget) {
|
||||
Map<String, String> out = new HashMap<>();
|
||||
if (rawTarget == null || rawTarget.isBlank()) return out;
|
||||
|
||||
int q = rawTarget.indexOf('?');
|
||||
if (q < 0 || q == rawTarget.length() - 1) return out;
|
||||
|
||||
String query = rawTarget.substring(q + 1);
|
||||
for (String pair : query.split("&")) {
|
||||
if (pair.isEmpty()) continue;
|
||||
int eq = pair.indexOf('=');
|
||||
String k = (eq >= 0) ? pair.substring(0, eq) : pair;
|
||||
String v = (eq >= 0) ? pair.substring(eq + 1) : "";
|
||||
out.put(urlDecode(k), urlDecode(v));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private static String urlDecode(String s) {
|
||||
try {
|
||||
return URLDecoder.decode(s, StandardCharsets.UTF_8);
|
||||
} catch (Exception e) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience wrapper for query maps.
|
||||
*/
|
||||
public static final class Q {
|
||||
|
||||
private final Map<String, String> map;
|
||||
|
||||
/**
|
||||
* Creates a wrapper.
|
||||
*
|
||||
* @param map parsed query map
|
||||
*/
|
||||
public Q(Map<String, String> map) {
|
||||
this.map = (map == null) ? Map.of() : map;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param key key
|
||||
* @return value or null
|
||||
*/
|
||||
public String get(String key) {
|
||||
return map.get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param key key
|
||||
* @param def default
|
||||
* @return value or default
|
||||
*/
|
||||
public String getOr(String key, String def) {
|
||||
String v = map.get(key);
|
||||
return (v == null) ? def : v;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param key key
|
||||
* @param def default
|
||||
* @return int or default
|
||||
*/
|
||||
public int getInt(String key, int def) {
|
||||
String v = map.get(key);
|
||||
if (v == null) return def;
|
||||
try {
|
||||
return Integer.parseInt(v.trim());
|
||||
} catch (Exception ignored) {
|
||||
return def;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Accepts "1"/"0", "true"/"false", "yes"/"no".
|
||||
*
|
||||
* @param key key
|
||||
* @return boolean
|
||||
*/
|
||||
public boolean getBool(String key) {
|
||||
String v = map.get(key);
|
||||
if (v == null) return false;
|
||||
String t = v.trim();
|
||||
return "1".equals(t) || "true".equalsIgnoreCase(t) || "yes".equalsIgnoreCase(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package org.openautonomousconnection.webserver.utils;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
|
||||
/**
|
||||
* SHA-256 helper.
|
||||
*/
|
||||
public final class Sha256 {
|
||||
|
||||
private Sha256() {
|
||||
// Utility class
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes SHA-256 hex (lowercase) of a string using UTF-8.
|
||||
*
|
||||
* @param text input
|
||||
* @return sha256 hex
|
||||
*/
|
||||
public static String hex(String text) {
|
||||
if (text == null) text = "";
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
||||
byte[] digest = md.digest(text.getBytes(StandardCharsets.UTF_8));
|
||||
return toHexLower(digest);
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("SHA-256 not available", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static String toHexLower(byte[] bytes) {
|
||||
char[] out = new char[bytes.length * 2];
|
||||
final char[] hex = "0123456789abcdef".toCharArray();
|
||||
int i = 0;
|
||||
for (byte b : bytes) {
|
||||
out[i++] = hex[(b >>> 4) & 0x0F];
|
||||
out[i++] = hex[b & 0x0F];
|
||||
}
|
||||
return new String(out);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user