Changed Web Routing and added WebHash
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
package org.openautonomousconnection.webserver.utils;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* Small HTML helpers for server-side rendering.
|
||||
*/
|
||||
public final class Html {
|
||||
|
||||
private Html() {}
|
||||
|
||||
/**
|
||||
* Escapes text for HTML.
|
||||
*
|
||||
* @param s input
|
||||
* @return escaped
|
||||
*/
|
||||
public static String esc(String s) {
|
||||
if (s == null) return "";
|
||||
return s.replace("&", "&")
|
||||
.replace("<", "<")
|
||||
.replace(">", ">")
|
||||
.replace("\"", """);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes to UTF-8 bytes.
|
||||
*
|
||||
* @param html html
|
||||
* @return bytes
|
||||
*/
|
||||
public static byte[] utf8(String html) {
|
||||
return (html == null ? "" : html).getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple page wrapper.
|
||||
*
|
||||
* @param title title
|
||||
* @param body body html
|
||||
* @return full html
|
||||
*/
|
||||
public static String page(String title, String body) {
|
||||
return """
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>%s</title>
|
||||
<style>
|
||||
body{font-family:system-ui,Segoe UI,Arial,sans-serif;background:#0f111a;color:#e5e7eb;margin:0;padding:24px;}
|
||||
a{color:#60a5fa;text-decoration:none} a:hover{text-decoration:underline}
|
||||
.card{background:#111827;border:1px solid #1f2937;border-radius:12px;padding:16px;max-width:900px}
|
||||
input,select{background:#0b1220;color:#e5e7eb;border:1px solid #243042;border-radius:8px;padding:10px;width:100%%;box-sizing:border-box}
|
||||
button{background:#2563eb;color:white;border:none;border-radius:8px;padding:10px 14px;cursor:pointer}
|
||||
button:hover{filter:brightness(1.05)}
|
||||
.row{display:flex;gap:12px;flex-wrap:wrap}
|
||||
.col{flex:1;min-width:220px}
|
||||
.muted{color:#94a3b8}
|
||||
.err{color:#fca5a5}
|
||||
.ok{color:#86efac}
|
||||
code{background:#0b1220;padding:2px 6px;border-radius:6px}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
%s
|
||||
</body>
|
||||
</html>
|
||||
""".formatted(esc(title), body == null ? "" : body);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package org.openautonomousconnection.webserver.utils;
|
||||
|
||||
import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.WebResponsePacket;
|
||||
import org.openautonomousconnection.webserver.api.WebPageContext;
|
||||
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
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.
|
||||
*
|
||||
* <p>Limitations:
|
||||
* <ul>
|
||||
* <li>Does not rewrite HTML/CSS URLs. If you need full offline/proxied subresources,
|
||||
* implement URL rewriting and route subresource paths through this proxy as well.</li>
|
||||
* </ul>
|
||||
*/
|
||||
public final class HttpsProxy {
|
||||
|
||||
private static final int CONNECT_TIMEOUT_MS = 10_000;
|
||||
private static final int READ_TIMEOUT_MS = 25_000;
|
||||
private static final int MAX_REDIRECTS = 8;
|
||||
|
||||
private HttpsProxy() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches an HTTPS URL and returns it as a WebResponsePacket.
|
||||
*
|
||||
* @param ctx the current web page context (for optional user-agent forwarding)
|
||||
* @param url the target HTTPS URL
|
||||
* @return proxied response
|
||||
*/
|
||||
public static WebResponsePacket proxyGet(WebPageContext ctx, String url) {
|
||||
try {
|
||||
return proxyGetInternal(ctx, url, 0);
|
||||
} catch (Exception e) {
|
||||
return new WebResponsePacket(
|
||||
502,
|
||||
"text/plain; charset=utf-8",
|
||||
Map.of(),
|
||||
("Bad Gateway: " + e.getClass().getName() + ": " + e.getMessage()).getBytes(StandardCharsets.UTF_8)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static WebResponsePacket proxyGetInternal(WebPageContext ctx, String url, int depth) throws Exception {
|
||||
if (depth > MAX_REDIRECTS) {
|
||||
return new WebResponsePacket(
|
||||
508,
|
||||
"text/plain; charset=utf-8",
|
||||
Map.of(),
|
||||
"Too many redirects".getBytes(StandardCharsets.UTF_8)
|
||||
);
|
||||
}
|
||||
|
||||
URL target = new URL(url);
|
||||
HttpsURLConnection con = (HttpsURLConnection) target.openConnection();
|
||||
con.setInstanceFollowRedirects(false);
|
||||
con.setRequestMethod("GET");
|
||||
con.setConnectTimeout(CONNECT_TIMEOUT_MS);
|
||||
con.setReadTimeout(READ_TIMEOUT_MS);
|
||||
|
||||
// Forward a user-agent if you have one (optional).
|
||||
String ua = null;
|
||||
if (ctx != null && ctx.request != null && ctx.request.getHeaders() != null) {
|
||||
ua = ctx.request.getHeaders().get("user-agent");
|
||||
}
|
||||
con.setRequestProperty("User-Agent", ua != null ? ua : "OAC-HttpsProxy/1.0");
|
||||
|
||||
int code = con.getResponseCode();
|
||||
|
||||
// Handle redirects manually to preserve content and avoid silent issues
|
||||
if (code == 301 || code == 302 || code == 303 || code == 307 || code == 308) {
|
||||
String location = con.getHeaderField("Location");
|
||||
if (location == null || location.isBlank()) {
|
||||
return new WebResponsePacket(
|
||||
502,
|
||||
"text/plain; charset=utf-8",
|
||||
Map.of(),
|
||||
("Bad Gateway: redirect without Location (code=" + code + ")").getBytes(StandardCharsets.UTF_8)
|
||||
);
|
||||
}
|
||||
// Resolve relative redirects
|
||||
URL resolved = new URL(target, location);
|
||||
con.disconnect();
|
||||
return proxyGetInternal(ctx, resolved.toString(), depth + 1);
|
||||
}
|
||||
|
||||
String contentType = con.getContentType();
|
||||
if (contentType == null || contentType.isBlank()) {
|
||||
contentType = "application/octet-stream";
|
||||
}
|
||||
|
||||
byte[] body;
|
||||
try (InputStream in = (code >= 400 ? con.getErrorStream() : con.getInputStream())) {
|
||||
body = readAllBytes(in);
|
||||
} finally {
|
||||
con.disconnect();
|
||||
}
|
||||
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
return new WebResponsePacket(code, contentType, headers, body);
|
||||
}
|
||||
|
||||
private static byte[] readAllBytes(InputStream in) throws Exception {
|
||||
if (in == null) return new byte[0];
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(Math.max(1024, in.available()));
|
||||
byte[] buf = new byte[32 * 1024];
|
||||
int r;
|
||||
while ((r = in.read(buf)) != -1) {
|
||||
out.write(buf, 0, r);
|
||||
}
|
||||
return out.toByteArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package org.openautonomousconnection.webserver.utils;
|
||||
|
||||
import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.WebRequestPacket;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Reads parameters from WebRequestPacket headers (case-insensitive).
|
||||
*
|
||||
* <p>Additionally provides hashing helpers via a supplied {@link WebHasher}.
|
||||
*/
|
||||
public final class RequestParams {
|
||||
|
||||
private final Map<String, String> headers;
|
||||
|
||||
/**
|
||||
* Creates a param reader.
|
||||
*
|
||||
* @param request request
|
||||
*/
|
||||
public RequestParams(WebRequestPacket request) {
|
||||
Map<String, String> h = request != null ? request.getHeaders() : null;
|
||||
this.headers = (h == null) ? Collections.emptyMap() : h;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a header (case-insensitive).
|
||||
*
|
||||
* @param key key
|
||||
* @return value or null
|
||||
*/
|
||||
public String get(String key) {
|
||||
if (key == null) return null;
|
||||
String needle = key.toLowerCase(Locale.ROOT);
|
||||
for (Map.Entry<String, String> e : headers.entrySet()) {
|
||||
if (e.getKey() != null && e.getKey().toLowerCase(Locale.ROOT).equals(needle)) {
|
||||
return e.getValue();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a header or default.
|
||||
*
|
||||
* @param key key
|
||||
* @param def default
|
||||
* @return value or default
|
||||
*/
|
||||
public String getOr(String key, String def) {
|
||||
String v = get(key);
|
||||
return (v == null) ? def : v;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an int header.
|
||||
*
|
||||
* @param key key
|
||||
* @param def default
|
||||
* @return int
|
||||
*/
|
||||
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 a trimmed header value.
|
||||
*
|
||||
* @param key key
|
||||
* @return trimmed value or null
|
||||
*/
|
||||
public String getTrimmed(String key) {
|
||||
String v = get(key);
|
||||
if (v == null) return null;
|
||||
String t = v.trim();
|
||||
return t.isEmpty() ? null : t;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns SHA-256 hex of a header value.
|
||||
*
|
||||
* @param hasher hasher
|
||||
* @param key header key
|
||||
* @return sha256 hex (or null if missing)
|
||||
*/
|
||||
public String getSha256Hex(WebHasher hasher, String key) {
|
||||
String v = getTrimmed(key);
|
||||
if (v == null) return null;
|
||||
return hasher.sha256Hex(v);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
package org.openautonomousconnection.webserver.utils;
|
||||
|
||||
import javax.crypto.SecretKeyFactory;
|
||||
import javax.crypto.spec.PBEKeySpec;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Base64;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Provides hashing utilities for the registrar frontend.
|
||||
*
|
||||
* <p>Username: SHA-256 hex
|
||||
* <p>Password: PBKDF2WithHmacSHA256 storage format:
|
||||
* PBKDF2$sha256$<iterations>$<saltB64>$<hashB64>
|
||||
*/
|
||||
public final class WebHasher {
|
||||
|
||||
private final SecureRandom rng = new SecureRandom();
|
||||
private final int pbkdf2Iterations;
|
||||
private final int pbkdf2SaltBytes;
|
||||
private final int pbkdf2KeyBytes;
|
||||
|
||||
/**
|
||||
* Creates a hasher.
|
||||
*
|
||||
* @param pbkdf2Iterations iterations (recommended >= 100k)
|
||||
* @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");
|
||||
if (pbkdf2SaltBytes < 8) throw new IllegalArgumentException("pbkdf2SaltBytes too low");
|
||||
if (pbkdf2KeyBytes < 16) throw new IllegalArgumentException("pbkdf2KeyBytes too low");
|
||||
this.pbkdf2Iterations = pbkdf2Iterations;
|
||||
this.pbkdf2SaltBytes = pbkdf2SaltBytes;
|
||||
this.pbkdf2KeyBytes = pbkdf2KeyBytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* SHA-256 hashes text (lowercase hex).
|
||||
*
|
||||
* @param text input
|
||||
* @return sha256 hex
|
||||
*/
|
||||
public String sha256Hex(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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PBKDF2-hashes a raw password into storage format.
|
||||
*
|
||||
* @param password raw password
|
||||
* @return encoded storage string
|
||||
*/
|
||||
public String pbkdf2Hash(String password) {
|
||||
Objects.requireNonNull(password, "password");
|
||||
byte[] salt = new byte[pbkdf2SaltBytes];
|
||||
rng.nextBytes(salt);
|
||||
|
||||
byte[] dk = derive(password.toCharArray(), salt, pbkdf2Iterations, pbkdf2KeyBytes);
|
||||
|
||||
return "PBKDF2$sha256$" + pbkdf2Iterations + "$" +
|
||||
Base64.getEncoder().encodeToString(salt) + "$" +
|
||||
Base64.getEncoder().encodeToString(dk);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies a raw password against a stored PBKDF2 string.
|
||||
*
|
||||
* @param password raw password
|
||||
* @param stored stored format
|
||||
* @return true if valid
|
||||
*/
|
||||
public boolean pbkdf2Verify(String password, String stored) {
|
||||
if (password == null || stored == null) return false;
|
||||
|
||||
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;
|
||||
try {
|
||||
it = Integer.parseInt(parts[2]);
|
||||
} catch (NumberFormatException e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
byte[] salt;
|
||||
byte[] expected;
|
||||
try {
|
||||
salt = Base64.getDecoder().decode(parts[3]);
|
||||
expected = Base64.getDecoder().decode(parts[4]);
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
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