Updated to latest Protocol Version

This commit is contained in:
UnlegitDqrk
2026-02-22 17:26:22 +01:00
parent 5058e41ce1
commit 5642869097
16 changed files with 879 additions and 173 deletions

View File

@@ -1,6 +1,9 @@
package org.openautonomousconnection.webserver.utils;
import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.WebResponsePacket;
import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.resource.WebResourceResponsePacket;
import org.openautonomousconnection.protocol.versions.v1_0_1.beta.WebPacketFlags;
import org.openautonomousconnection.protocol.versions.v1_0_1.beta.WebPacketHeader;
import org.openautonomousconnection.webserver.api.WebPageContext;
import javax.net.ssl.HttpsURLConnection;
@@ -8,15 +11,16 @@ import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.LinkedHashMap;
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>
* <p>v1.0.1 entry returns {@link WebResourceResponsePacket} and mirrors correlation header fields
* from {@link WebPageContext#request}.</p>
*
* <p>v1.0.0 method is kept for older call sites.</p>
*/
public final class HttpsProxy {
@@ -28,15 +32,151 @@ public final class HttpsProxy {
}
/**
* Fetches an HTTPS URL and returns it as a WebResponsePacket.
* Fetches an HTTPS URL and returns it as a v1.0.1 {@link WebResourceResponsePacket}.
*
* @param ctx the current web page context
* @param url the target HTTPS URL
* @return proxied response (never null)
*/
public static WebResourceResponsePacket proxyGet(WebPageContext ctx, String url) {
WebPacketHeader in = (ctx != null && ctx.request != null) ? ctx.request.getHeader() : null;
WebPacketHeader baseHeader = (in == null)
? new WebPacketHeader(0, 0, 0, 0, WebPacketFlags.RESOURCE, System.currentTimeMillis())
: new WebPacketHeader(
in.getRequestId(),
in.getTabId(),
in.getPageId(),
in.getFrameId(),
in.getFlags() | WebPacketFlags.RESOURCE,
System.currentTimeMillis()
);
try {
return proxyGetInternalV101(ctx, url, baseHeader, 0);
} catch (Exception e) {
byte[] body = ("Bad Gateway: " + e.getClass().getName() + ": " + e.getMessage())
.getBytes(StandardCharsets.UTF_8);
Map<String, String> headers = new LinkedHashMap<>();
headers.put("content-length", String.valueOf(body.length));
return new WebResourceResponsePacket(
baseHeader,
502,
"text/plain; charset=utf-8",
headers,
body,
null
);
}
}
private static WebResourceResponsePacket proxyGetInternalV101(WebPageContext ctx, String url, WebPacketHeader header, int depth) throws Exception {
if (depth > MAX_REDIRECTS) {
byte[] body = "Too many redirects".getBytes(StandardCharsets.UTF_8);
Map<String, String> headers = new LinkedHashMap<>();
headers.put("content-length", String.valueOf(body.length));
return new WebResourceResponsePacket(header, 508, "text/plain; charset=utf-8", headers, body, null);
}
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 present (optional).
String ua = null;
if (ctx != null && ctx.request != null && ctx.request.getHeaders() != null) {
ua = getHeaderIgnoreCase(ctx.request.getHeaders(), "user-agent");
}
con.setRequestProperty("User-Agent", ua != null ? ua : "OAC-HttpsProxy/1.0.1");
int code = con.getResponseCode();
// Manual redirect handling
if (code == 301 || code == 302 || code == 303 || code == 307 || code == 308) {
String location = con.getHeaderField("Location");
if (location == null || location.isBlank()) {
con.disconnect();
byte[] body = ("Bad Gateway: redirect without Location (code=" + code + ")")
.getBytes(StandardCharsets.UTF_8);
Map<String, String> headers = new LinkedHashMap<>();
headers.put("content-length", String.valueOf(body.length));
return new WebResourceResponsePacket(header, 502, "text/plain; charset=utf-8", headers, body, null);
}
URL resolved = new URL(target, location);
con.disconnect();
return proxyGetInternalV101(ctx, resolved.toString(), header, depth + 1);
}
String contentType = con.getContentType();
if (contentType == null || contentType.isBlank()) contentType = "application/octet-stream";
byte[] body;
Map<String, String> outHeaders = new LinkedHashMap<>();
try (InputStream in = (code >= 400 ? con.getErrorStream() : con.getInputStream())) {
body = readAllBytes(in);
outHeaders.put("content-length", String.valueOf(body.length));
// Pass through a few useful headers (safe subset)
copyHeaderIfPresent(con, outHeaders, "cache-control");
copyHeaderIfPresent(con, outHeaders, "etag");
copyHeaderIfPresent(con, outHeaders, "last-modified");
} finally {
con.disconnect();
}
return new WebResourceResponsePacket(
header,
code,
contentType,
outHeaders,
body,
null
);
}
private static void copyHeaderIfPresent(HttpsURLConnection con, Map<String, String> out, String name) {
String v = con.getHeaderField(name);
if (v != null && !v.isBlank()) out.put(name, v);
}
private static String getHeaderIgnoreCase(Map<String, String> headers, String key) {
if (headers == null || headers.isEmpty() || key == null) return null;
String needle = key.trim().toLowerCase(java.util.Locale.ROOT);
for (Map.Entry<String, String> e : headers.entrySet()) {
if (e.getKey() == null) continue;
if (e.getKey().trim().toLowerCase(java.util.Locale.ROOT).equals(needle)) return e.getValue();
}
return null;
}
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();
}
/**
* Fetches an HTTPS URL and returns it as a v1.0.0 {@link WebResponsePacket}.
*
* @param ctx the current web page context (for optional user-agent forwarding)
* @param url the target HTTPS URL
* @return proxied response
* @deprecated v1.0.1 code should call {@link #proxyGet(WebPageContext, String)} returning {@link WebResourceResponsePacket}.
*/
public static WebResponsePacket proxyGet(WebPageContext ctx, String url) {
@Deprecated(forRemoval = false, since = "1.0.1-BETA.0.1")
public static WebResponsePacket proxyGetV100B(WebPageContext ctx, String url) {
try {
return proxyGetInternal(ctx, url, 0);
return proxyGetInternalV100B(ctx, url, 0);
} catch (Exception e) {
return new WebResponsePacket(
502,
@@ -47,7 +187,7 @@ public final class HttpsProxy {
}
}
private static WebResponsePacket proxyGetInternal(WebPageContext ctx, String url, int depth) throws Exception {
private static WebResponsePacket proxyGetInternalV100B(WebPageContext ctx, String url, int depth) throws Exception {
if (depth > MAX_REDIRECTS) {
return new WebResponsePacket(
508,
@@ -64,16 +204,14 @@ public final class HttpsProxy {
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");
ua = getHeaderIgnoreCase(ctx.request.getHeaders(), "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()) {
@@ -84,16 +222,13 @@ public final class HttpsProxy {
("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);
return proxyGetInternalV100B(ctx, resolved.toString(), depth + 1);
}
String contentType = con.getContentType();
if (contentType == null || contentType.isBlank()) {
contentType = "application/octet-stream";
}
if (contentType == null || contentType.isBlank()) contentType = "application/octet-stream";
byte[] body;
try (InputStream in = (code >= 400 ? con.getErrorStream() : con.getInputStream())) {
@@ -104,15 +239,4 @@ public final class HttpsProxy {
return new WebResponsePacket(code, contentType, HeaderMaps.mutable(), 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();
}
}
}

View File

@@ -33,20 +33,20 @@ public final class MergedRequestParams {
public static MergedRequestParams from(String rawTarget, Map<String, String> headers, byte[] body) {
Map<String, List<String>> merged = new LinkedHashMap<>();
// 1) Query string
// Query string
String query = extractQuery(rawTarget);
if (query != null && !query.isBlank()) {
mergeInto(merged, parseUrlEncoded(query, StandardCharsets.UTF_8), false);
}
// 2) Body
// 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);
return new MergedRequestParams(Collections.unmodifiableMap(merged));
}
private static String extractQuery(String rawTarget) {
@@ -70,16 +70,20 @@ public final class MergedRequestParams {
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();
List<String> vals = (e.getValue() == null) ? List.of() : e.getValue();
if (!override && target.containsKey(k)) {
// append
// Always keep ArrayList in target to allow appends safely.
target.get(k).addAll(vals);
continue;
}
// override or insert
// Insert/override with a mutable list to preserve later merge behavior.
target.put(k, new ArrayList<>(vals));
}
}
@@ -166,6 +170,7 @@ public final class MergedRequestParams {
/**
* 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) {
@@ -293,13 +298,8 @@ public final class MergedRequestParams {
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);
}
else baos.writeBytes(String.valueOf(c).getBytes(charset));
}
return baos.toString(charset);
}

View File

@@ -1,25 +1,47 @@
package org.openautonomousconnection.webserver.utils;
import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.WebRequestPacket;
import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.resource.WebResourceRequestPacket;
import java.util.Collections;
import java.util.Locale;
import java.util.Map;
/**
* Reads parameters from WebRequestPacket headers (case-insensitive).
* Reads parameters from request headers (case-insensitive).
*
* <p>Additionally provides hashing helpers via a supplied {@link WebHasher}.
* <p>This utility is used by server-side pages to access request metadata.</p>
*/
public final class RequestParams {
private final Map<String, String> headers;
/**
* Creates a param reader.
* Creates a param reader from v1.0.1 resource request headers.
*
* @param request request
* @param request v1.0.1 resource request (may be null)
*/
public RequestParams(WebResourceRequestPacket request) {
Map<String, String> h = request != null ? request.getHeaders() : null;
this.headers = (h == null) ? Collections.emptyMap() : h;
}
/**
* Creates a param reader from a header map.
*
* @param headers headers (may be null)
*/
public RequestParams(Map<String, String> headers) {
this.headers = (headers == null) ? Collections.emptyMap() : headers;
}
/**
* v1.0.0 constructor (v1.0.0).
*
* @param request v1.0.0 request (may be null)
* @deprecated v1.0.1 uses {@link WebResourceRequestPacket}. Keep only for older modules still compiled in.
*/
@Deprecated(forRemoval = false, since = "1.0.1-BETA.0.1")
public RequestParams(WebRequestPacket request) {
Map<String, String> h = request != null ? request.getHeaders() : null;
this.headers = (h == null) ? Collections.emptyMap() : h;
@@ -92,8 +114,9 @@ public final class RequestParams {
* @return sha256 hex (or null if missing)
*/
public String getSha256Hex(WebHasher hasher, String key) {
if (hasher == null) throw new IllegalArgumentException("hasher is null");
String v = getTrimmed(key);
if (v == null) return null;
return hasher.sha256Hex(v);
}
}
}

View File

@@ -0,0 +1,50 @@
package org.openautonomousconnection.webserver.utils;
import java.net.URI;
/**
* URL utilities for the server.
*/
public final class WebUrlUtil {
private WebUrlUtil() {
}
/**
* Extracts path + query from an absolute URL string.
*
* @param url absolute URL
* @return path + optional query (e.g. "/a/b?x=1") or null
*/
public static String extractPathAndQuery(String url) {
try {
URI u = URI.create(url);
String p = u.getPath();
if (p == null || p.isBlank()) p = "/";
String q = u.getRawQuery();
return (q == null || q.isBlank()) ? p : (p + "?" + q);
} catch (Exception e) {
return null;
}
}
/**
* Normalizes a requested path to a content-root relative path.
*
* @param pathWithQuery "/foo/bar?x=1"
* @return "foo/bar" (default: "index.html")
*/
public static String normalizeToContentPath(String pathWithQuery) {
String p = pathWithQuery;
int q = p.indexOf('?');
if (q >= 0) p = p.substring(0, q);
if (p == null || p.isBlank() || "/".equals(p)) {
return "index.html";
}
if (p.startsWith("/")) p = p.substring(1);
if (p.isBlank()) return "index.html";
return p;
}
}