Finished
This commit is contained in:
@@ -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