Implemented InfoNameLib

This commit is contained in:
UnlegitDqrk
2026-02-22 16:00:57 +01:00
parent abc989a675
commit d715c0758d
27 changed files with 2589 additions and 3 deletions

View File

@@ -32,6 +32,9 @@ import org.openautonomousconnection.protocol.side.client.ProtocolWebClient;
import org.openautonomousconnection.protocol.side.ins.ProtocolINSServer;
import org.openautonomousconnection.protocol.side.server.ProtocolCustomServer;
import org.openautonomousconnection.protocol.side.web.ProtocolWebServer;
import org.openautonomousconnection.protocol.urlhandler.v1_0_0.beta.OacWebUrlInstaller_v1_0_0_B;
import org.openautonomousconnection.protocol.urlhandler.v1_0_1.beta.LibClientImpl_v1_0_1_B;
import org.openautonomousconnection.protocol.urlhandler.v1_0_1.beta.OacUrlHandlerInstaller_v1_0_1_B;
import org.openautonomousconnection.protocol.versions.ProtocolVersion;
import org.openautonomousconnection.protocol.versions.v1_0_0.beta.ProtocolWebServer_1_0_0_B;
@@ -122,7 +125,7 @@ public final class ProtocolBridge {
*/
@ProtocolInfo(protocolSide = ProtocolVersion.ProtocolSide.CLIENT)
public ProtocolBridge(ProtocolClient protocolClient, ProtocolValues protocolValues, ProtocolVersion protocolVersion,
Logger logger, AddonLoader addonLoader) throws Exception {
Logger logger, AddonLoader addonLoader, LibClientImpl_v1_0_1_B libClientImpl) throws Exception {
// Assign the parameters to the class fields
this.protocolClient = protocolClient;
this.protocolValues = protocolValues;
@@ -138,6 +141,17 @@ public final class ProtocolBridge {
// Register the appropriate listeners and packets
registerListeners();
registerPackets();
installUrl(libClientImpl);
}
private void installUrl(LibClientImpl_v1_0_1_B libClientImpl) {
if (protocolVersion == ProtocolVersion.PV_1_0_0_BETA) {
OacWebUrlInstaller_v1_0_0_B.installOnce(this, libClientImpl);
}
if (protocolVersion == ProtocolVersion.PV_1_0_1_BETA) {
OacUrlHandlerInstaller_v1_0_1_B.installOnce(this, libClientImpl, libClientImpl, libClientImpl);
}
}
private void downloadLicenses() throws IOException {

View File

@@ -0,0 +1,15 @@
package org.openautonomousconnection.protocol.urlhandler.v1_0_0.beta;
/**
* Installs the "web://" protocol handler using java.protocol.handler.pkgs.
* Recommended to use Version v1.0.0-BETA or newer
*/
@Deprecated(forRemoval = false, since = "1.0.1-BETA.0.3")
public abstract class LibClientImpl_v1_0_0_B {
/**
* Called when connecting to the resolved server endpoint fails.
*
* @param exception the connection failure
*/
public abstract void serverConnectionFailed(Exception exception);
}

View File

@@ -0,0 +1,29 @@
package org.openautonomousconnection.protocol.urlhandler.v1_0_0.beta;
/**
* Stores the current session token across multiple HttpURLConnection instances.
*
* <p>JavaFX WebView creates a new connection per navigation, so headers must be re-injected
* for every request.</p>
*/
public final class OacSessionJar {
private volatile String session;
/**
* Stores a session token (e.g. from response header "session").
*
* @param session session token
*/
public void store(String session) {
String s = (session == null) ? null : session.trim();
this.session = (s == null || s.isEmpty()) ? null : s;
}
/**
* @return stored session token or null
*/
public String get() {
return session;
}
}

View File

@@ -0,0 +1,288 @@
package org.openautonomousconnection.protocol.urlhandler.v1_0_0.beta;
import org.openautonomousconnection.protocol.versions.v1_0_0.beta.WebRequestMethod;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.ProtocolException;
import java.net.URL;
import java.util.*;
/**
* HttpURLConnection implementation that maps "web://" URLs to OAC WebRequestPacket/WebResponsePacket.
*
* <p>Important semantics for JavaFX WebView:</p>
* <ul>
* <li>WebView may call {@link #connect()} before {@link #getOutputStream()}.</li>
* <li>WebView creates a NEW connection instance per navigation; therefore persistent headers
* (like session) must be injected from an external store per request.</li>
* </ul>
*/
public final class OacWebHttpURLConnection extends HttpURLConnection {
private static final int MAX_REDIRECTS = 8;
private final OacWebRequestBroker broker;
private final OacSessionJar sessionJar;
private final Map<String, String> requestHeaders = new LinkedHashMap<>();
private final ByteArrayOutputStream requestBody = new ByteArrayOutputStream(1024);
private boolean connected;
private boolean requestSent;
private OacWebResponse response;
/**
* Creates a new OAC HttpURLConnection.
*
* @param url the web:// URL
* @param broker request broker
* @param sessionJar shared session store (must be shared across connections)
*/
public OacWebHttpURLConnection(URL url, OacWebRequestBroker broker, OacSessionJar sessionJar) {
super(url);
this.broker = Objects.requireNonNull(broker, "broker");
this.sessionJar = Objects.requireNonNull(sessionJar, "sessionJar");
this.method = "GET";
setInstanceFollowRedirects(true);
}
private static String headerValue(Map<String, String> headers, String nameLower) {
if (headers == null || headers.isEmpty() || nameLower == null) return null;
String needle = nameLower.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;
}
@Override
public void setRequestProperty(String key, String value) {
if (key == null) return;
// DO NOT block after connect(): WebView may call connect() early.
// Only block once the request was actually sent.
if (requestSent) throw new IllegalStateException("Request already sent");
if (value == null) requestHeaders.remove(key);
else requestHeaders.put(key, value);
}
@Override
public String getRequestProperty(String key) {
if (key == null) return null;
for (Map.Entry<String, String> e : requestHeaders.entrySet()) {
if (e.getKey() != null && e.getKey().equalsIgnoreCase(key)) return e.getValue();
}
return null;
}
@Override
public Map<String, List<String>> getRequestProperties() {
Map<String, List<String>> out = new LinkedHashMap<>();
for (Map.Entry<String, String> e : requestHeaders.entrySet()) {
out.put(e.getKey(), e.getValue() == null ? List.of() : List.of(e.getValue()));
}
return Collections.unmodifiableMap(out);
}
@Override
public void setRequestMethod(String method) throws ProtocolException {
if (method == null) throw new ProtocolException("method is null");
if (requestSent) throw new ProtocolException("Request already sent");
String m = method.trim().toUpperCase(Locale.ROOT);
if (!m.equals("GET") && !m.equals("POST")) {
throw new ProtocolException("Unsupported method: " + method);
}
this.method = m;
}
@Override
public OutputStream getOutputStream() throws IOException {
// WebView may call connect() first, so do NOT throw "Already connected".
if (requestSent) throw new IllegalStateException("Request already sent");
setDoOutput(true);
return requestBody;
}
@Override
public void connect() {
// MUST NOT send here. WebView may call connect() before writing the POST body.
connected = true;
}
private void ensureResponse() throws IOException {
if (requestSent) return;
URL cur = this.url;
String methodStr = (this.method == null) ? "GET" : this.method.trim().toUpperCase(Locale.ROOT);
WebRequestMethod reqMethod = "POST".equals(methodStr) ? WebRequestMethod.POST : WebRequestMethod.GET;
// Snapshot headers/body at send time.
Map<String, String> carryHeaders = new LinkedHashMap<>(requestHeaders);
// Each navigation creates a new connection, so we re-add the session for every request.
String session = sessionJar.get();
if (session != null && !session.isBlank() && headerValue(carryHeaders, "session") == null) {
carryHeaders.put("session", session);
}
byte[] carryBody = null;
if (getDoOutput()) {
carryBody = requestBody.toByteArray();
if (reqMethod == WebRequestMethod.POST && carryBody == null) carryBody = new byte[0];
}
if (reqMethod == WebRequestMethod.POST && headerValue(carryHeaders, "content-type") == null) {
carryHeaders.put("content-type", "application/x-www-form-urlencoded; charset=utf-8");
}
OacWebResponse resp = null;
WebRequestMethod carryMethod = reqMethod;
for (int i = 0; i <= MAX_REDIRECTS; i++) {
resp = broker.fetch(cur, carryMethod, carryHeaders, carryBody);
String newSession = headerValue(resp.headers(), "session");
if (newSession != null && !newSession.isBlank()) {
sessionJar.store(newSession);
// keep it for the next request in this redirect chain too
carryHeaders.put("session", newSession);
}
int code = resp.statusCode();
if (!getInstanceFollowRedirects()) break;
if (code == 301 || code == 302 || code == 303 || code == 307 || code == 308) {
String loc = headerValue(resp.headers(), "location");
if (loc == null || loc.isBlank()) break;
try {
cur = new URL(cur, loc);
} catch (Exception ex) {
break;
}
if (code == 303) {
carryMethod = WebRequestMethod.GET;
carryBody = null;
} else if ((code == 301 || code == 302) && carryMethod == WebRequestMethod.POST) {
carryMethod = WebRequestMethod.GET;
carryBody = null;
}
continue;
}
break;
}
this.response = resp;
this.requestSent = true;
this.connected = true;
}
@Override
public InputStream getInputStream() throws IOException {
ensureResponse();
if (response == null) return new ByteArrayInputStream(new byte[0]);
return response.bodyStream();
}
@Override
public InputStream getErrorStream() {
try {
ensureResponse();
if (response == null) return null;
int code = response.statusCode();
return (code >= 400) ? response.bodyStream() : null;
} catch (IOException e) {
return null;
}
}
@Override
public int getResponseCode() throws IOException {
ensureResponse();
return response == null ? -1 : response.statusCode();
}
@Override
public String getContentType() {
try {
ensureResponse();
} catch (IOException e) {
return "application/octet-stream";
}
String ct = (response == null) ? null : response.contentType();
return (ct == null || ct.isBlank()) ? "application/octet-stream" : ct;
}
@Override
public int getContentLength() {
try {
ensureResponse();
} catch (IOException e) {
return -1;
}
long len = (response == null) ? -1L : response.contentLength();
return (len <= 0 || len > Integer.MAX_VALUE) ? -1 : (int) len;
}
@Override
public long getContentLengthLong() {
try {
ensureResponse();
} catch (IOException e) {
return -1L;
}
return (response == null) ? -1L : response.contentLength();
}
@Override
public Map<String, List<String>> getHeaderFields() {
try {
ensureResponse();
} catch (IOException e) {
return Map.of();
}
if (response == null) return Map.of();
Map<String, List<String>> out = new LinkedHashMap<>();
for (Map.Entry<String, String> e : response.headers().entrySet()) {
String k = e.getKey();
String v = e.getValue();
if (k == null) continue;
out.put(k, v == null ? List.of() : List.of(v));
}
return out;
}
@Override
public String getHeaderField(String name) {
if (name == null) return null;
try {
ensureResponse();
} catch (IOException e) {
return null;
}
if (response == null) return null;
return headerValue(response.headers(), name.trim().toLowerCase(Locale.ROOT));
}
@Override
public void disconnect() {
// No persistent socket owned by this object.
connected = false;
}
@Override
public boolean usingProxy() {
return false;
}
}

View File

@@ -0,0 +1,153 @@
package org.openautonomousconnection.protocol.urlhandler.v1_0_0.beta;
import dev.unlegitdqrk.unlegitlibrary.event.EventListener;
import dev.unlegitdqrk.unlegitlibrary.event.EventPriority;
import dev.unlegitdqrk.unlegitlibrary.event.Listener;
import dev.unlegitdqrk.unlegitlibrary.network.system.client.events.packets.C_PacketReadEvent;
import org.openautonomousconnection.protocol.packets.v1_0_0.beta.INSResponsePacket;
import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.WebResponsePacket;
import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.stream.WebStreamChunkPacket_v1_0_0_B;
import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.stream.WebStreamEndPacket_v1_0_0_B;
import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.stream.WebStreamStartPacket_v1_0_0_B;
import org.openautonomousconnection.protocol.side.client.ProtocolClient;
import org.openautonomousconnection.protocol.side.client.events.ConnectedToProtocolServerEvent;
import org.openautonomousconnection.protocol.versions.v1_0_0.beta.INSRecord;
import org.openautonomousconnection.protocol.versions.v1_0_0.beta.INSResponseStatus;
import java.util.List;
import java.util.Objects;
/**
* Receives incoming OAC web response packets and forwards them into the request broker.
*
* <p>Important:</p>
* <ul>
* <li>The shown protocol types do not contain any correlation id.</li>
* <li>Therefore, the broker must treat the connection as single-flight (one in-flight request).</li>
* </ul>
*/
public final class OacWebPacketListener extends EventListener {
private final OacWebRequestBroker broker;
private final ProtocolClient client;
private final LibClientImpl_v1_0_0_B impl;
/**
* Creates a new listener bound to the given broker.
*
* @param broker broker instance
* @param client protocol client
*/
public OacWebPacketListener(OacWebRequestBroker broker, ProtocolClient client, LibClientImpl_v1_0_0_B impl) {
this.broker = Objects.requireNonNull(broker, "broker");
this.client = Objects.requireNonNull(client, "client");
this.impl = Objects.requireNonNull(impl, "impl");
}
/**
* Notifies the broker that the server connection is established.
*
* @param event connected event
*/
@Listener(priority = EventPriority.HIGHEST)
public void onConnected(ConnectedToProtocolServerEvent event) {
broker.notifyServerConnected();
}
/**
* Handles packets coming from INS and the web server side.
*
* @param event packet event
*/
@Listener(priority = EventPriority.HIGHEST)
public void onPacketRead(C_PacketReadEvent event) {
Object p = event.getPacket();
if (p instanceof INSResponsePacket resp) {
onInsResponse(resp);
return;
}
if (p instanceof WebResponsePacket resp) {
broker.onWebResponse(resp.getStatusCode(), resp.getContentType(), resp.getHeaders(), resp.getBody());
return;
}
if (p instanceof WebStreamStartPacket_v1_0_0_B start) {
broker.onStreamStart(start.getStatusCode(), start.getContentType(), start.getHeaders(), start.getTotalLength());
return;
}
if (p instanceof WebStreamChunkPacket_v1_0_0_B chunk) {
broker.onStreamChunk(chunk.getSeq(), chunk.getData());
return;
}
if (p instanceof WebStreamEndPacket_v1_0_0_B end) {
broker.onStreamEnd(end.isOk());
}
}
private void onInsResponse(INSResponsePacket resp) {
INSResponseStatus status = resp.getStatus();
List<INSRecord> records = resp.getRecords();
if (status != INSResponseStatus.OK) {
broker.invalidateCurrentInfoName();
throw new IllegalStateException("INS resolution failed: " + status);
}
if (records == null || records.isEmpty() || records.getFirst() == null || records.getFirst().value == null) {
broker.invalidateCurrentInfoName();
throw new IllegalStateException("INS resolution returned no usable records");
}
String host = records.getFirst().value.trim();
if (host.isEmpty()) {
broker.invalidateCurrentInfoName();
throw new IllegalStateException("INS record value is empty");
}
String hostname;
int port;
if (!host.contains(":")) {
hostname = host;
if (records.getFirst().port == 0) port = 1028;
else port = records.getFirst().port;
} else {
String[] split = host.split(":", 2);
hostname = split[0].trim();
String p1 = split[1].trim();
if (hostname.isEmpty() || p1.isEmpty()) {
broker.invalidateCurrentInfoName();
throw new IllegalStateException("Invalid INS host:port value: " + host);
}
try {
port = Integer.parseInt(p1);
} catch (NumberFormatException e) {
broker.invalidateCurrentInfoName();
throw new IllegalStateException("Invalid port in INS record: " + host, e);
}
}
Thread t = new Thread(() -> connectServer(hostname, port), "oac-web-server-connect");
t.setDaemon(true);
t.start();
}
private void connectServer(String hostname, int port) {
try {
if (client.getClientServerConnection() != null && client.getClientServerConnection().isConnected()) {
client.getClientServerConnection().disconnect();
}
client.buildServerConnection(null, client.getProtocolBridge().getProtocolValues().ssl);
client.getClientServerConnection().connect(hostname, port);
} catch (Exception e) {
broker.invalidateCurrentInfoName();
impl.serverConnectionFailed(e);
}
}
}

View File

@@ -0,0 +1,529 @@
package org.openautonomousconnection.protocol.urlhandler.v1_0_0.beta;
import dev.unlegitdqrk.unlegitlibrary.network.system.utils.TransportProtocol;
import org.openautonomousconnection.protocol.packets.v1_0_0.beta.INSQueryPacket;
import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.WebRequestPacket;
import org.openautonomousconnection.protocol.side.client.ProtocolClient;
import org.openautonomousconnection.protocol.versions.v1_0_0.beta.INSRecordType;
import org.openautonomousconnection.protocol.versions.v1_0_0.beta.WebRequestMethod;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.net.URL;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
/**
* Central broker that translates {@code web://} URLs into OAC protocol traffic.
*
* <p>Protocol limitation: no correlation id -> single-flight (one in-flight request at a time).</p>
*
* <p>UDP streaming semantics (best-effort):</p>
* <ul>
* <li>Chunks may arrive out of order or be lost.</li>
* <li>We accept gaps and assemble what we have after {@code WebStreamEndPacket}.</li>
* <li>We wait a short grace window after stream end to allow late UDP packets.</li>
* </ul>
*/
public final class OacWebRequestBroker {
private static final OacWebRequestBroker INSTANCE = new OacWebRequestBroker();
private static final long CONNECT_TIMEOUT_SECONDS = 10;
private static final long RESPONSE_TIMEOUT_SECONDS = 25;
/**
* Grace time after receiving WebStreamEndPacket to allow late UDP packets (reordering).
*/
private static final long UDP_END_GRACE_MILLIS = 150;
private final Object responseLock = new Object();
private volatile ProtocolClient client;
private volatile CountDownLatch connectionLatch;
private volatile String currentInfoName;
private volatile ResponseState responseState;
private OacWebRequestBroker() {
}
/**
* Returns the singleton broker.
*
* @return broker
*/
public static OacWebRequestBroker get() {
return INSTANCE;
}
private static String safeContentType(String ct) {
return (ct == null || ct.isBlank()) ? "application/octet-stream" : ct;
}
private static Map<String, String> safeHeaders(Map<String, String> headers) {
return (headers == null || headers.isEmpty()) ? Map.of() : Map.copyOf(headers);
}
private static String normalizePathWithQuery(String path, String query) {
String p;
if (path == null || path.isBlank() || "/".equals(path)) {
p = "index.html";
} else {
p = path.startsWith("/") ? path.substring(1) : path;
if (p.isBlank()) p = "index.html";
}
if (query != null && !query.isBlank()) {
return p + "?" + query;
}
return p;
}
/**
* Assembles the response body from received chunks in ascending seq order.
*
* <p>Best-effort UDP behavior: gaps are ignored.</p>
*/
private static void assembleBestEffortLocked(ResponseState st) {
st.body.reset();
if (st.chunkBuffer.isEmpty() || st.maxSeqSeen < 0) {
return;
}
for (int seq = 0; seq <= st.maxSeqSeen; seq++) {
byte[] chunk = st.chunkBuffer.get(seq);
if (chunk == null) continue; // gap accepted
st.body.write(chunk, 0, chunk.length);
}
}
private static void sleepSilently(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
/**
* Attaches the client used to send INS/Web packets.
*
* @param client protocol client
*/
public void attachClient(ProtocolClient client) {
this.client = Objects.requireNonNull(client, "client");
}
/**
* Legacy overload (GET with no body).
*
* @param url web:// URL
* @param headers request headers
* @return response
*/
public OacWebResponse fetch(URL url, Map<String, String> headers) {
return fetch(url, WebRequestMethod.GET, headers, null);
}
/**
* Fetches a URL via OAC protocol (used by {@link java.net.URLConnection}).
*
* @param url web:// URL
* @param method request method
* @param headers request headers
* @param body request body (may be null)
* @return response
*/
public OacWebResponse fetch(URL url, WebRequestMethod method, Map<String, String> headers, byte[] body) {
Objects.requireNonNull(url, "url");
Objects.requireNonNull(method, "method");
ProtocolClient c = this.client;
if (c == null) {
throw new IllegalStateException("ProtocolClient not attached. Call OacWebUrlInstaller.installOnce(..., client) first.");
}
Response r = openAndAwait(c, url, method, headers, body);
byte[] respBody = (r.body() == null) ? new byte[0] : r.body();
long len = respBody.length;
return new OacWebResponse(
r.statusCode(),
r.contentType(),
OacWebResponse.safeHeaders(r.headers()),
new ByteArrayInputStream(respBody),
len
);
}
/**
* Opens a resource and blocks until the current single-flight response completes.
*/
public Response openAndAwait(ProtocolClient client, URL url, WebRequestMethod method, Map<String, String> headers, byte[] body) {
Objects.requireNonNull(client, "client");
Objects.requireNonNull(url, "url");
Objects.requireNonNull(method, "method");
open(client, url, method, headers, body);
return awaitResponse(RESPONSE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
}
/**
* Sends required packets for a {@code web://} URL.
*/
public synchronized void open(ProtocolClient client, URL url, WebRequestMethod method, Map<String, String> headers, byte[] body) {
Objects.requireNonNull(client, "client");
Objects.requireNonNull(url, "url");
Objects.requireNonNull(method, "method");
if (!"web".equalsIgnoreCase(url.getProtocol())) {
throw new IllegalArgumentException("Unsupported protocol: " + url.getProtocol());
}
String infoName = url.getHost();
if (infoName == null || infoName.isBlank()) {
throw new IllegalArgumentException("Missing InfoName in URL: " + url);
}
String path = normalizePathWithQuery(url.getPath(), url.getQuery());
beginNewResponse();
if (!infoName.equals(currentInfoName)) {
resolveAndConnect(client, infoName);
currentInfoName = infoName;
} else {
awaitConnectionIfPending();
}
sendWebRequest(client, path, method, headers, body);
}
/**
* Called by packet listener when server connection is established.
*/
public void notifyServerConnected() {
CountDownLatch latch = connectionLatch;
if (latch != null) {
latch.countDown();
}
}
/**
* Invalidates cached InfoName, forcing a new INS resolution on next request.
*/
public synchronized void invalidateCurrentInfoName() {
currentInfoName = null;
}
/**
* Handles a non-streamed WebResponsePacket.
*
* @param statusCode status code
* @param contentType content-type
* @param headers headers
* @param body body
*/
public void onWebResponse(int statusCode, String contentType, Map<String, String> headers, byte[] body) {
ResponseState st = responseState;
if (st == null) return;
synchronized (responseLock) {
if (st.completed) return;
st.statusCode = statusCode;
st.contentType = safeContentType(contentType);
st.headers = safeHeaders(headers);
byte[] b = (body == null) ? new byte[0] : body;
st.body.reset();
st.body.write(b, 0, b.length);
st.completed = true;
st.success = true;
st.done.countDown();
}
}
/**
* Handles the beginning of a streamed response.
*
* @param statusCode status code
* @param contentType content-type
* @param headers headers
* @param totalLength total length (may be -1)
*/
public void onStreamStart(int statusCode, String contentType, Map<String, String> headers, long totalLength) {
ResponseState st = responseState;
if (st == null) return;
synchronized (responseLock) {
if (st.completed) return;
st.statusCode = statusCode;
st.contentType = safeContentType(contentType);
st.headers = safeHeaders(headers);
st.totalLength = totalLength;
st.streamStarted = true;
st.maxSeqSeen = -1;
st.endReceived = false;
st.endReceivedAtMillis = 0L;
// Streaming body will be assembled on end (best-effort UDP)
st.body.reset();
}
}
/**
* Handles a streamed chunk.
*
* <p>UDP best-effort: store by seq and assemble later; accept gaps.</p>
*
* @param seq chunk sequence number
* @param data chunk bytes
*/
public void onStreamChunk(int seq, byte[] data) {
ResponseState st = responseState;
if (st == null) return;
if (data == null || data.length == 0) return;
synchronized (responseLock) {
if (st.completed) return;
if (!st.streamStarted) {
failLocked(st, "Stream chunk received before stream start");
return;
}
if (seq < 0) return;
st.chunkBuffer.put(seq, Arrays.copyOf(data, data.length));
if (seq > st.maxSeqSeen) st.maxSeqSeen = seq;
}
}
/**
* Handles stream end.
*
* <p>UDP best-effort: do not complete immediately; allow late UDP packets and assemble after grace.</p>
*
* @param ok end status
*/
public void onStreamEnd(boolean ok) {
ResponseState st = responseState;
if (st == null) return;
synchronized (responseLock) {
if (st.completed) return;
if (!st.streamStarted) {
st.streamStarted = true;
}
st.success = ok;
st.endReceived = true;
st.endReceivedAtMillis = System.currentTimeMillis();
// completion + assembly happens in awaitResponse() after grace window
}
}
/**
* Waits for the current response (single-flight).
*
* @param timeout timeout
* @param unit unit
* @return response
*/
public Response awaitResponse(long timeout, TimeUnit unit) {
Objects.requireNonNull(unit, "unit");
ResponseState st = responseState;
if (st == null) {
throw new IllegalStateException("No in-flight request");
}
long deadlineNanos = System.nanoTime() + unit.toNanos(timeout);
for (; ; ) {
synchronized (responseLock) {
if (st.completed) {
break;
}
// Non-stream response already completed by onWebResponse()
if (!st.streamStarted && st.done.getCount() == 0) {
st.completed = true;
break;
}
if (st.endReceived) {
long now = System.currentTimeMillis();
if (now - st.endReceivedAtMillis >= UDP_END_GRACE_MILLIS) {
// Assemble best-effort body from received chunks
assembleBestEffortLocked(st);
st.completed = true;
if (!st.success) {
st.errorMessage = (st.errorMessage == null) ? "Streaming failed" : st.errorMessage;
}
st.done.countDown();
break;
}
}
}
if (System.nanoTime() >= deadlineNanos) {
throw new IllegalStateException("Timeout while waiting for Web response");
}
sleepSilently(10);
}
synchronized (responseLock) {
if (!st.success) {
throw new IllegalStateException(st.errorMessage == null ? "Request failed" : st.errorMessage);
}
return new Response(
st.statusCode,
st.contentType,
st.headers,
st.body.toByteArray()
);
}
}
private void resolveAndConnect(ProtocolClient client, String infoName) {
if (client.getClientINSConnection() == null || !client.getClientINSConnection().isConnected()) return;
String[] parts = infoName.split("\\.");
if (parts.length < 2 || parts.length > 3) {
throw new IllegalArgumentException(
"Invalid INS address format: " + infoName + " (expected name.tln or sub.name.tln)"
);
}
String tln = parts[parts.length - 1];
String name = parts[parts.length - 2];
String sub = (parts.length == 3) ? parts[0] : null;
connectionLatch = new CountDownLatch(1);
INSQueryPacket query = new INSQueryPacket(
tln,
name,
sub,
INSRecordType.A,
client.getClientINSConnection().getUniqueID()
);
try {
client.getClientINSConnection().sendPacket(query, TransportProtocol.TCP);
} catch (Exception e) {
throw new IllegalStateException("Failed to send INSQueryPacket for " + infoName, e);
}
awaitConnectionIfPending();
}
private void awaitConnectionIfPending() {
CountDownLatch latch = connectionLatch;
if (latch == null || client == null || client.getClientServerConnection() == null || client.getClientINSConnection() == null) {
return;
}
try {
if (!latch.await(CONNECT_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
throw new IllegalStateException("Timeout while waiting for ServerConnection");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IllegalStateException("Interrupted while waiting for ServerConnection", e);
}
}
private void sendWebRequest(ProtocolClient client, String path, WebRequestMethod method, Map<String, String> headers, byte[] body) {
Objects.requireNonNull(client, "client");
Objects.requireNonNull(path, "path");
Objects.requireNonNull(method, "method");
if (client.getClientServerConnection() == null || !client.getClientServerConnection().isConnected()) {
awaitConnectionIfPending();
}
if (client.getClientServerConnection() == null || !client.getClientServerConnection().isConnected()) {
throw new IllegalStateException("ServerConnection is not connected after waiting");
}
WebRequestPacket packet = new WebRequestPacket(
path,
method,
(headers == null ? Map.of() : headers),
body
);
try {
client.getClientServerConnection().sendPacket(packet, TransportProtocol.TCP);
} catch (Exception e) {
throw new IllegalStateException("Failed to send WebRequestPacket for path " + path, e);
}
}
private void beginNewResponse() {
synchronized (responseLock) {
responseState = new ResponseState();
}
}
private void failLocked(ResponseState st, String message) {
st.completed = true;
st.success = false;
st.errorMessage = message;
st.done.countDown();
}
/**
* Response DTO.
*
* @param statusCode status code
* @param contentType content type
* @param headers headers
* @param body body bytes
*/
public record Response(int statusCode, String contentType, Map<String, String> headers, byte[] body) {
}
/**
* In-flight state for a single request (single-flight).
*/
private static final class ResponseState {
private final CountDownLatch done = new CountDownLatch(1);
private final ByteArrayOutputStream body = new ByteArrayOutputStream(64 * 1024);
private final Map<Integer, byte[]> chunkBuffer = new HashMap<>();
private int statusCode = 0;
private String contentType = "application/octet-stream";
private Map<String, String> headers = Map.of();
private boolean streamStarted;
private long totalLength;
private int maxSeqSeen = -1;
private boolean endReceived;
private long endReceivedAtMillis;
private boolean completed;
private boolean success;
private String errorMessage;
}
}

View File

@@ -0,0 +1,32 @@
package org.openautonomousconnection.protocol.urlhandler.v1_0_0.beta;
import java.io.InputStream;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
/**
* Represents a resolved web response for the JavaFX WebView.
*
* @param statusCode HTTP-like status code
* @param contentType response content-type (as sent by server)
* @param headers response headers
* @param bodyStream body stream (may be streaming)
* @param contentLength content length if known, else -1
*/
public record OacWebResponse(
int statusCode,
String contentType,
Map<String, String> headers,
InputStream bodyStream,
long contentLength
) {
public OacWebResponse {
Objects.requireNonNull(headers, "headers");
Objects.requireNonNull(bodyStream, "bodyStream");
}
public static Map<String, String> safeHeaders(Map<String, String> h) {
return (h == null) ? Collections.emptyMap() : Collections.unmodifiableMap(h);
}
}

View File

@@ -0,0 +1,32 @@
package org.openautonomousconnection.protocol.urlhandler.v1_0_0.beta;
import org.openautonomousconnection.protocol.ProtocolBridge;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Installs the "web://" protocol handler using java.protocol.handler.pkgs.
* Recommended to use Version v1.0.0-BETA or newer
*/
@Deprecated(forRemoval = false, since = "1.0.1-BETA.0.3")
public final class OacWebUrlInstaller_v1_0_0_B {
private static final AtomicBoolean INSTALLED = new AtomicBoolean(false);
private OacWebUrlInstaller_v1_0_0_B() {
}
public static void installOnce(ProtocolBridge protocolBridge, LibClientImpl_v1_0_0_B impl) {
Objects.requireNonNull(protocolBridge, "protocolBridge");
Objects.requireNonNull(impl, "impl");
if (!INSTALLED.compareAndSet(false, true)) return;
OacWebRequestBroker.get().attachClient(protocolBridge.getProtocolClient());
protocolBridge.getProtocolValues().eventManager.
registerListener(new OacWebPacketListener(OacWebRequestBroker.get(), protocolBridge.getProtocolClient(), impl));
ProtocolHandlerPackages.installPackage("org.openautonomousconnection.urlhandler");
}
}

View File

@@ -0,0 +1,42 @@
package org.openautonomousconnection.protocol.urlhandler.v1_0_0.beta;
import java.util.LinkedHashSet;
import java.util.Objects;
import java.util.Set;
/**
* Utility to safely append packages to "java.protocol.handler.pkgs".
*/
public final class ProtocolHandlerPackages {
private static final String KEY = "java.protocol.handler.pkgs";
private ProtocolHandlerPackages() {
}
/**
* Appends a package prefix to the protocol handler search path.
*
* @param pkg package prefix (e.g. "com.example.protocols")
*/
public static void installPackage(String pkg) {
Objects.requireNonNull(pkg, "pkg");
String p = pkg.trim();
if (p.isEmpty()) return;
String existing = System.getProperty(KEY, "");
Set<String> parts = new LinkedHashSet<>();
if (!existing.isBlank()) {
for (String s : existing.split("\\|")) {
String t = s.trim();
if (!t.isEmpty()) parts.add(t);
}
}
parts.add(p);
String merged = String.join("|", parts);
System.setProperty(KEY, merged);
}
}

View File

@@ -0,0 +1,22 @@
package org.openautonomousconnection.protocol.urlhandler.v1_0_0.beta.web;
import org.openautonomousconnection.protocol.urlhandler.v1_0_0.beta.OacSessionJar;
import org.openautonomousconnection.protocol.urlhandler.v1_0_0.beta.OacWebHttpURLConnection;
import org.openautonomousconnection.protocol.urlhandler.v1_0_0.beta.OacWebRequestBroker;
import java.io.IOException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
/**
* URLStreamHandler for the "web" protocol (loaded via java.protocol.handler.pkgs).
*/
public final class Handler extends URLStreamHandler {
private static final OacSessionJar SESSION_JAR = new OacSessionJar();
@Override
protected URLConnection openConnection(URL u) throws IOException {
return new OacWebHttpURLConnection(u, OacWebRequestBroker.get(), SESSION_JAR);
}
}

View File

@@ -0,0 +1,55 @@
package org.openautonomousconnection.protocol.urlhandler.v1_0_1.beta;
import org.openautonomousconnection.protocol.urlhandler.v1_0_0.beta.LibClientImpl_v1_0_0_B;
import org.openautonomousconnection.protocol.urlhandler.v1_0_1.beta.web.WebFlagInspector;
import org.openautonomousconnection.protocol.urlhandler.v1_0_1.beta.web.WebRequestContextProvider;
import org.openautonomousconnection.protocol.versions.v1_0_1.beta.WebPacketHeader;
import java.util.Map;
/**
* Callback surface for URL-handler related streaming events (v1.0.1-BETA).
*/
public abstract class LibClientImpl_v1_0_1_B extends LibClientImpl_v1_0_0_B implements WebRequestContextProvider, WebFlagInspector {
/**
* Called when a streamed response begins.
*/
public void streamStart(
WebPacketHeader header,
int statusCode,
String contentType,
Map<String, String> headers,
long totalLength
) {
}
/**
* Called for each streamed chunk.
*/
public void streamChunk(
WebPacketHeader header,
int seq,
byte[] data
) {
}
/**
* Called when the stream ends.
*/
public void streamEnd(
WebPacketHeader header,
boolean ok,
String error
) {
}
/**
* Called after full best-effort assembly (only if ok=true).
*/
public void streamFinish(
WebPacketHeader header,
byte[] content
) {
}
}

View File

@@ -0,0 +1,50 @@
package org.openautonomousconnection.protocol.urlhandler.v1_0_1.beta;
import org.openautonomousconnection.protocol.ProtocolBridge;
import org.openautonomousconnection.protocol.urlhandler.v1_0_1.beta.web.OacWebProtocolModule_v1_0_1_B;
import org.openautonomousconnection.protocol.urlhandler.v1_0_1.beta.web.WebFlagInspector;
import org.openautonomousconnection.protocol.urlhandler.v1_0_1.beta.web.WebRequestContextProvider;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Installs OAC URL protocol modules for v1.0.1-BETA.
*
* <p>Call once during startup (before creating {@link java.net.URL} instances).</p>
*/
public final class OacUrlHandlerInstaller_v1_0_1_B {
private static final AtomicBoolean INSTALLED = new AtomicBoolean(false);
private OacUrlHandlerInstaller_v1_0_1_B() {
}
/**
* Installs the URL handler package path and registers the WEB module.
*
* @param protocolBridge protocol bridge
* @param impl callback implementation
* @param ctxProvider provides tab/page/frame correlation for each request
* @param flagInspector interprets {@code WebPacketHeader.flags} (e.g. stream bit)
*/
public static void installOnce(
ProtocolBridge protocolBridge,
LibClientImpl_v1_0_1_B impl,
WebRequestContextProvider ctxProvider,
WebFlagInspector flagInspector
) {
Objects.requireNonNull(protocolBridge, "protocolBridge");
Objects.requireNonNull(impl, "impl");
Objects.requireNonNull(ctxProvider, "ctxProvider");
Objects.requireNonNull(flagInspector, "flagInspector");
if (!INSTALLED.compareAndSet(false, true)) return;
ProtocolHandlerPackages.installPackage("org.openautonomousconnection.protocol.urlhandler.v1_0_1.beta");
OacWebProtocolModule_v1_0_1_B web = new OacWebProtocolModule_v1_0_1_B(ctxProvider, flagInspector);
OacUrlProtocolRegistry.register(web);
web.install(protocolBridge, impl);
}
}

View File

@@ -0,0 +1,38 @@
package org.openautonomousconnection.protocol.urlhandler.v1_0_1.beta;
import org.openautonomousconnection.protocol.ProtocolBridge;
import java.io.IOException;
import java.net.URL;
import java.net.URLConnection;
/**
* Pluggable module for a URL protocol scheme (e.g. "web", "ftp").
*
* <p>Each module is responsible for providing {@link URLConnection} instances for its scheme and
* wiring packet listeners into the {@link ProtocolBridge} runtime.</p>
*/
public interface OacUrlProtocolModule {
/**
* @return the URL scheme handled by this module (e.g. "web", "ftp")
*/
String scheme();
/**
* Opens a connection for the given URL.
*
* @param url the URL
* @return the connection
* @throws IOException if opening fails
*/
URLConnection openConnection(URL url) throws IOException;
/**
* Installs the module into the given protocol bridge (listeners, brokers, etc.).
*
* @param protocolBridge protocol bridge
* @param impl callback implementation
*/
void install(ProtocolBridge protocolBridge, LibClientImpl_v1_0_1_B impl);
}

View File

@@ -0,0 +1,52 @@
package org.openautonomousconnection.protocol.urlhandler.v1_0_1.beta;
import java.util.Locale;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
/**
* Global registry for URL protocol modules.
*/
public final class OacUrlProtocolRegistry {
private static final ConcurrentHashMap<String, OacUrlProtocolModule> MODULES = new ConcurrentHashMap<>();
private OacUrlProtocolRegistry() {
}
/**
* Registers a module for its scheme.
*
* @param module the module
* @throws IllegalStateException if a different module is already registered for the scheme
*/
public static void register(OacUrlProtocolModule module) {
Objects.requireNonNull(module, "module");
String scheme = normalizeScheme(module.scheme());
OacUrlProtocolModule prev = MODULES.putIfAbsent(scheme, module);
if (prev != null && prev != module) {
throw new IllegalStateException(
"Module already registered for scheme '" + scheme + "': " + prev.getClass().getName()
);
}
}
/**
* Returns the module for the given scheme.
*
* @param scheme the scheme
* @return module or null if not registered
*/
public static OacUrlProtocolModule get(String scheme) {
if (scheme == null) return null;
return MODULES.get(normalizeScheme(scheme));
}
private static String normalizeScheme(String scheme) {
String s = Objects.requireNonNull(scheme, "scheme").trim();
if (s.isEmpty()) throw new IllegalArgumentException("scheme is empty");
return s.toLowerCase(Locale.ROOT);
}
}

View File

@@ -0,0 +1,44 @@
package org.openautonomousconnection.protocol.urlhandler.v1_0_1.beta;
import java.util.LinkedHashSet;
import java.util.Objects;
import java.util.Set;
/**
* Utility to safely append packages to "java.protocol.handler.pkgs".
*
* <p>JVM lookup rule: it searches {@code <pkgprefix>.<protocol>.Handler}.</p>
*/
public final class ProtocolHandlerPackages {
private static final String KEY = "java.protocol.handler.pkgs";
private ProtocolHandlerPackages() {
}
/**
* Appends a package prefix to the protocol handler search path.
*
* @param pkg package prefix (e.g. "org.openautonomousconnection.protocol.urlhandler.v1_0_1.beta")
*/
public static void installPackage(String pkg) {
Objects.requireNonNull(pkg, "pkg");
String p = pkg.trim();
if (p.isEmpty()) return;
String existing = System.getProperty(KEY, "");
Set<String> parts = new LinkedHashSet<>();
if (!existing.isBlank()) {
for (String s : existing.split("\\|")) {
String t = s.trim();
if (!t.isEmpty()) parts.add(t);
}
}
parts.add(p);
String merged = String.join("|", parts);
System.setProperty(KEY, merged);
}
}

View File

@@ -0,0 +1,71 @@
package org.openautonomousconnection.protocol.urlhandler.v1_0_1.beta.web;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Objects;
/**
* InputStream wrapper that deletes one or more paths (files/directories) on close.
*
* <p>Useful for temporary downloads/streams that must be cleaned up automatically.</p>
*/
final class DeleteOnCloseInputStream extends FilterInputStream {
private final Path[] deletePaths;
private volatile boolean closed;
DeleteOnCloseInputStream(InputStream in, Path... deletePaths) {
super(Objects.requireNonNull(in, "in"));
this.deletePaths = (deletePaths == null) ? new Path[0] : deletePaths.clone();
}
@Override
public void close() throws IOException {
if (closed) return;
closed = true;
IOException io = null;
try {
super.close();
} catch (IOException e) {
io = e;
}
for (Path p : deletePaths) {
if (p == null) continue;
try {
deleteRecursivelyIfExists(p);
} catch (IOException e) {
if (io == null) io = e;
}
}
if (io != null) throw io;
}
private static void deleteRecursivelyIfExists(Path path) throws IOException {
if (!Files.exists(path)) return;
if (Files.isDirectory(path)) {
Files.walkFileTree(path, new SimpleFileVisitor<>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Files.deleteIfExists(file);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
Files.deleteIfExists(dir);
return FileVisitResult.CONTINUE;
}
});
return;
}
Files.deleteIfExists(path);
}
}

View File

@@ -0,0 +1,26 @@
package org.openautonomousconnection.protocol.urlhandler.v1_0_1.beta.web;
import org.openautonomousconnection.protocol.urlhandler.v1_0_1.beta.OacUrlProtocolModule;
import org.openautonomousconnection.protocol.urlhandler.v1_0_1.beta.OacUrlProtocolRegistry;
import java.io.IOException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
/**
* URLStreamHandler for the "web" protocol.
*
* <p>Loaded by JVM via {@code java.protocol.handler.pkgs} and delegates to the registered module.</p>
*/
public final class Handler extends URLStreamHandler {
@Override
protected URLConnection openConnection(URL u) throws IOException {
OacUrlProtocolModule module = OacUrlProtocolRegistry.get("web");
if (module == null) {
throw new IOException("No module registered for scheme 'web'. Did you call OacUrlHandlerInstaller_v1_0_1_B.installOnce(...)?");
}
return module.openConnection(u);
}
}

View File

@@ -0,0 +1,26 @@
package org.openautonomousconnection.protocol.urlhandler.v1_0_1.beta.web;
/**
* Stores the current session token across multiple URLConnection instances.
*/
public final class OacSessionJar {
private volatile String session;
/**
* Stores a session token (e.g. from response header "session").
*
* @param session session token
*/
public void store(String session) {
String s = (session == null) ? null : session.trim();
this.session = (s == null || s.isEmpty()) ? null : s;
}
/**
* @return stored session token or null
*/
public String get() {
return session;
}
}

View File

@@ -0,0 +1,270 @@
package org.openautonomousconnection.protocol.urlhandler.v1_0_1.beta.web;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.ProtocolException;
import java.net.URL;
import java.util.*;
/**
* HttpURLConnection implementation that maps "web://" URLs to WEB v1.0.1 protocol requests.
*
* <p>This implementation is designed for JavaFX WebView semantics.</p>
*/
public final class OacWebHttpURLConnection extends HttpURLConnection {
private static final int MAX_REDIRECTS = 8;
private final OacWebRequestBroker broker;
private final OacSessionJar sessionJar;
private final WebRequestContextProvider ctxProvider;
private final Map<String, String> requestHeaders = new LinkedHashMap<>();
private final ByteArrayOutputStream requestBody = new ByteArrayOutputStream(1024);
private boolean requestSent;
private OacWebResponse response;
public OacWebHttpURLConnection(URL url, OacWebRequestBroker broker, OacSessionJar sessionJar, WebRequestContextProvider ctxProvider) {
super(url);
this.broker = Objects.requireNonNull(broker, "broker");
this.sessionJar = Objects.requireNonNull(sessionJar, "sessionJar");
this.ctxProvider = Objects.requireNonNull(ctxProvider, "ctxProvider");
this.method = "GET";
setInstanceFollowRedirects(true);
}
private static String headerValue(Map<String, String> headers, String nameLower) {
if (headers == null || headers.isEmpty() || nameLower == null) return null;
String needle = nameLower.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;
}
@Override
public void setRequestProperty(String key, String value) {
if (key == null) return;
if (requestSent) throw new IllegalStateException("Request already sent");
if (value == null) requestHeaders.remove(key);
else requestHeaders.put(key, value);
}
@Override
public String getRequestProperty(String key) {
if (key == null) return null;
for (Map.Entry<String, String> e : requestHeaders.entrySet()) {
if (e.getKey() != null && e.getKey().equalsIgnoreCase(key)) return e.getValue();
}
return null;
}
@Override
public Map<String, List<String>> getRequestProperties() {
Map<String, List<String>> out = new LinkedHashMap<>();
for (Map.Entry<String, String> e : requestHeaders.entrySet()) {
out.put(e.getKey(), e.getValue() == null ? List.of() : List.of(e.getValue()));
}
return Collections.unmodifiableMap(out);
}
@Override
public void setRequestMethod(String method) throws ProtocolException {
if (method == null) throw new ProtocolException("method is null");
if (requestSent) throw new ProtocolException("Request already sent");
String m = method.trim().toUpperCase(Locale.ROOT);
if (!m.equals("GET") && !m.equals("POST")) {
throw new ProtocolException("Unsupported method: " + method);
}
this.method = m;
}
@Override
public OutputStream getOutputStream() {
if (requestSent) throw new IllegalStateException("Request already sent");
setDoOutput(true);
return requestBody;
}
@Override
public void connect() {
// Intentionally no-op: JavaFX WebView may call connect() before writing POST body.
}
private void ensureResponse() throws IOException {
if (requestSent) return;
URL cur = this.url;
String methodStr = (this.method == null) ? "GET" : this.method.trim().toUpperCase(Locale.ROOT);
Map<String, String> carryHeaders = new LinkedHashMap<>(requestHeaders);
String session = sessionJar.get();
if (session != null && !session.isBlank() && headerValue(carryHeaders, "session") == null) {
carryHeaders.put("session", session);
}
byte[] carryBody = null;
if (getDoOutput()) {
carryBody = requestBody.toByteArray();
if ("POST".equals(methodStr) && carryBody == null) carryBody = new byte[0];
}
if ("POST".equals(methodStr) && headerValue(carryHeaders, "content-type") == null) {
carryHeaders.put("content-type", "application/x-www-form-urlencoded; charset=utf-8");
}
OacWebResponse resp = null;
for (int i = 0; i <= MAX_REDIRECTS; i++) {
WebRequestContextProvider.WebRequestContext ctx = ctxProvider.contextFor(cur);
resp = broker.fetch(
cur,
methodStr,
carryHeaders,
carryBody,
ctx.tabId(),
ctx.pageId(),
ctx.frameId()
);
String newSession = broker.extractSession(resp.headers());
if (newSession != null && !newSession.isBlank()) {
sessionJar.store(newSession);
carryHeaders.put("session", newSession);
}
int code = resp.statusCode();
if (!getInstanceFollowRedirects()) break;
if (code == 301 || code == 302 || code == 303 || code == 307 || code == 308) {
String loc = headerValue(resp.headers(), "location");
if (loc == null || loc.isBlank()) break;
try {
cur = new URL(cur, loc);
} catch (Exception ex) {
break;
}
if (code == 303) {
methodStr = "GET";
carryBody = null;
} else if ((code == 301 || code == 302) && "POST".equals(methodStr)) {
methodStr = "GET";
carryBody = null;
}
continue;
}
break;
}
this.response = resp;
this.requestSent = true;
}
@Override
public InputStream getInputStream() throws IOException {
ensureResponse();
if (response == null) return new ByteArrayInputStream(new byte[0]);
return response.bodyStream();
}
@Override
public InputStream getErrorStream() {
try {
ensureResponse();
if (response == null) return null;
return (response.statusCode() >= 400) ? response.bodyStream() : null;
} catch (IOException e) {
return null;
}
}
@Override
public int getResponseCode() throws IOException {
ensureResponse();
return response == null ? -1 : response.statusCode();
}
@Override
public String getContentType() {
try {
ensureResponse();
} catch (IOException e) {
return "application/octet-stream";
}
String ct = (response == null) ? null : response.contentType();
return (ct == null || ct.isBlank()) ? "application/octet-stream" : ct;
}
@Override
public int getContentLength() {
try {
ensureResponse();
} catch (IOException e) {
return -1;
}
long len = (response == null) ? -1L : response.contentLength();
return (len <= 0 || len > Integer.MAX_VALUE) ? -1 : (int) len;
}
@Override
public long getContentLengthLong() {
try {
ensureResponse();
} catch (IOException e) {
return -1L;
}
return (response == null) ? -1L : response.contentLength();
}
@Override
public Map<String, List<String>> getHeaderFields() {
try {
ensureResponse();
} catch (IOException e) {
return Map.of();
}
if (response == null) return Map.of();
Map<String, List<String>> out = new LinkedHashMap<>();
for (Map.Entry<String, String> e : response.headers().entrySet()) {
String k = e.getKey();
String v = e.getValue();
if (k == null) continue;
out.put(k, v == null ? List.of() : List.of(v));
}
return out;
}
@Override
public String getHeaderField(String name) {
if (name == null) return null;
try {
ensureResponse();
} catch (IOException e) {
return null;
}
if (response == null) return null;
return headerValue(response.headers(), name.trim().toLowerCase(Locale.ROOT));
}
@Override
public void disconnect() {
// No persistent socket owned by this object.
}
@Override
public boolean usingProxy() {
return false;
}
}

View File

@@ -0,0 +1,71 @@
package org.openautonomousconnection.protocol.urlhandler.v1_0_1.beta.web;
import dev.unlegitdqrk.unlegitlibrary.event.EventListener;
import dev.unlegitdqrk.unlegitlibrary.event.EventPriority;
import dev.unlegitdqrk.unlegitlibrary.event.Listener;
import dev.unlegitdqrk.unlegitlibrary.network.system.client.events.packets.C_PacketReadEvent;
import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.resource.WebResourceResponsePacket;
import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.stream.WebStreamChunkPacket_v1_0_1_B;
import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.stream.WebStreamEndPacket_v1_0_1_B;
import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.stream.WebStreamStartPacket_v1_0_1_B;
import org.openautonomousconnection.protocol.urlhandler.v1_0_1.beta.LibClientImpl_v1_0_1_B;
import java.util.Map;
import java.util.Objects;
/**
* Receives incoming WEB packets and forwards them into the broker and client callbacks.
*/
public final class OacWebPacketListener extends EventListener {
private final OacWebRequestBroker broker;
private final LibClientImpl_v1_0_1_B impl;
/**
* Creates a listener bound to the given broker.
*
* @param broker broker instance
* @param impl callback implementation
*/
public OacWebPacketListener(OacWebRequestBroker broker, LibClientImpl_v1_0_1_B impl) {
this.broker = Objects.requireNonNull(broker, "broker");
this.impl = Objects.requireNonNull(impl, "impl");
}
@Listener(priority = EventPriority.HIGHEST)
public void onPacketRead(C_PacketReadEvent event) {
Object p = event.getPacket();
if (p instanceof WebResourceResponsePacket resp) {
broker.onResourceResponse(resp);
return;
}
if (p instanceof WebStreamStartPacket_v1_0_1_B start) {
impl.streamStart(
start.getHeader(),
start.getStatusCode(),
start.getContentType(),
start.getHeaders() == null ? Map.of() : start.getHeaders(),
start.getTotalLength()
);
broker.onStreamStart(start);
return;
}
if (p instanceof WebStreamChunkPacket_v1_0_1_B chunk) {
byte[] data = (chunk.getData() == null) ? new byte[0] : chunk.getData();
impl.streamChunk(chunk.getHeader(), chunk.getSeq(), data);
broker.onStreamChunk(chunk);
return;
}
if (p instanceof WebStreamEndPacket_v1_0_1_B end) {
impl.streamEnd(end.getHeader(), end.isOk(), end.getError());
byte[] full = broker.onStreamEndAndAssemble(end);
if (full != null) {
impl.streamFinish(end.getHeader(), full);
}
}
}
}

View File

@@ -0,0 +1,51 @@
package org.openautonomousconnection.protocol.urlhandler.v1_0_1.beta.web;
import org.openautonomousconnection.protocol.ProtocolBridge;
import org.openautonomousconnection.protocol.urlhandler.v1_0_1.beta.LibClientImpl_v1_0_1_B;
import org.openautonomousconnection.protocol.urlhandler.v1_0_1.beta.OacUrlProtocolModule;
import java.io.IOException;
import java.net.URL;
import java.net.URLConnection;
import java.util.Objects;
/**
* WEB protocol module implementation for v1.0.1-BETA.
*/
public final class OacWebProtocolModule_v1_0_1_B implements OacUrlProtocolModule {
private final OacSessionJar sessionJar = new OacSessionJar();
private final WebRequestContextProvider ctxProvider;
private final WebFlagInspector flagInspector;
public OacWebProtocolModule_v1_0_1_B(WebRequestContextProvider ctxProvider, WebFlagInspector flagInspector) {
this.ctxProvider = Objects.requireNonNull(ctxProvider, "ctxProvider");
this.flagInspector = Objects.requireNonNull(flagInspector, "flagInspector");
}
@Override
public String scheme() {
return "web";
}
@Override
public URLConnection openConnection(URL url) throws IOException {
Objects.requireNonNull(url, "url");
if (!"web".equalsIgnoreCase(url.getProtocol())) {
throw new IOException("Unsupported scheme for this module: " + url.getProtocol());
}
return new OacWebHttpURLConnection(url, OacWebRequestBroker.get(), sessionJar, ctxProvider);
}
@Override
public void install(ProtocolBridge protocolBridge, LibClientImpl_v1_0_1_B impl) {
Objects.requireNonNull(protocolBridge, "protocolBridge");
Objects.requireNonNull(impl, "impl");
OacWebRequestBroker.get().attachRuntime(protocolBridge.getProtocolClient(), flagInspector);
protocolBridge.getProtocolValues().eventManager.registerListener(
new OacWebPacketListener(OacWebRequestBroker.get(), impl)
);
}
}

View File

@@ -0,0 +1,576 @@
package org.openautonomousconnection.protocol.urlhandler.v1_0_1.beta.web;
import dev.unlegitdqrk.unlegitlibrary.network.system.utils.TransportProtocol;
import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.resource.WebResourceRequestPacket;
import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.resource.WebResourceResponsePacket;
import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.stream.WebStreamChunkPacket_v1_0_1_B;
import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.stream.WebStreamEndPacket_v1_0_1_B;
import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.stream.WebStreamStartPacket_v1_0_1_B;
import org.openautonomousconnection.protocol.side.client.ProtocolClient;
import org.openautonomousconnection.protocol.versions.v1_0_1.beta.WebPacketFlags;
import org.openautonomousconnection.protocol.versions.v1_0_1.beta.WebPacketHeader;
import java.io.*;
import java.net.URL;
import java.nio.file.*;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
/**
* Multi-flight broker for WEB v1.0.1-BETA with best-effort streaming and temp-file assembly.
*
* <p>Range support: request headers are passed through unchanged (e.g. "Range").</p>
*
* <p>Streaming is best-effort: missing sequence numbers are ignored.</p>
* <p>Chunks are spooled to temp chunk files and merged into a final temp file on stream end.</p>
*/
public final class OacWebRequestBroker {
private static final OacWebRequestBroker INSTANCE = new OacWebRequestBroker();
private static final long RESPONSE_TIMEOUT_SECONDS = 30;
private final ConcurrentHashMap<Long, ResponseState> inFlight = new ConcurrentHashMap<>();
private final AtomicLong requestCounter = new AtomicLong(1);
private volatile ProtocolClient client;
private volatile WebFlagInspector flagInspector;
private OacWebRequestBroker() {
}
/**
* @return global singleton instance
*/
public static OacWebRequestBroker get() {
return INSTANCE;
}
/**
* Attaches runtime dependencies required for dispatching requests and interpreting flags.
*
* <p>Safe to call multiple times with the same instances. Different instances are rejected.</p>
*
* @param client protocol client used to send packets
* @param flagInspector inspector for header flags (STREAM bit)
*/
public synchronized void attachRuntime(ProtocolClient client, WebFlagInspector flagInspector) {
Objects.requireNonNull(client, "client");
Objects.requireNonNull(flagInspector, "flagInspector");
if (this.client == null && this.flagInspector == null) {
this.client = client;
this.flagInspector = flagInspector;
return;
}
if (this.client == client && this.flagInspector == flagInspector) {
return;
}
throw new IllegalStateException("OacWebRequestBroker runtime already initialized with different instances");
}
/**
* Sends a resource request and blocks until completion.
*
* @param url requested URL
* @param method HTTP-like method (GET/POST/...)
* @param headers request headers (passed through unchanged; includes Range)
* @param body request body (nullable)
* @param tabId tab id
* @param pageId page id
* @param frameId frame id
* @return resolved response
*/
public OacWebResponse fetch(
URL url,
String method,
Map<String, String> headers,
byte[] body,
long tabId,
long pageId,
long frameId
) throws IOException {
Objects.requireNonNull(url, "url");
ProtocolClient c = this.client;
if (c == null) {
throw new IllegalStateException("ProtocolClient not attached. Call OacWebRequestBroker.attachRuntime(...) during install.");
}
String m = (method == null || method.isBlank()) ? "GET" : method.trim().toUpperCase(Locale.ROOT);
long requestId = requestCounter.getAndIncrement();
int flags = WebPacketFlags.RESOURCE;
WebPacketHeader header = new WebPacketHeader(
requestId,
tabId,
pageId,
frameId,
flags,
System.currentTimeMillis()
);
ResponseState st = new ResponseState(requestId);
inFlight.put(requestId, st);
Map<String, String> safeHeaders = (headers == null) ? Map.of() : new LinkedHashMap<>(headers);
byte[] safeBody = (body == null) ? new byte[0] : body;
WebResourceRequestPacket packet = new WebResourceRequestPacket(
header,
url.toString(),
m,
safeHeaders,
safeBody,
safeHeaders.get("content-type"),
null,
null
);
try {
if (c.getClientServerConnection() == null || !c.getClientServerConnection().isConnected()) {
inFlight.remove(requestId);
throw new IOException("ServerConnection is not connected");
}
c.getClientServerConnection().sendPacket(packet, TransportProtocol.TCP);
} catch (Exception e) {
inFlight.remove(requestId);
throw new IOException("Failed to send WebResourceRequestPacket", e);
}
return awaitAndBuildResponse(st);
}
/**
* Extracts the session token from response headers (case-insensitive).
*
* @param headers headers
* @return session token or null
*/
public String extractSession(Map<String, String> headers) {
return headerValue(headers, "session");
}
/**
* Handles a WebResourceResponsePacket.
*
* <p>If STREAM flag is set in header, body is expected to be streamed via start/chunk/end packets.</p>
*
* @param p packet
*/
public void onResourceResponse(WebResourceResponsePacket p) {
if (p == null || p.getHeader() == null) return;
ResponseState st = inFlight.get(p.getHeader().getRequestId());
if (st == null) return;
synchronized (st.lock) {
if (st.completed) return;
st.statusCode = p.getStatusCode();
st.contentType = safeContentType(p.getContentType());
st.headers = safeHeaders(p.getHeaders());
boolean stream = false;
WebFlagInspector inspector = this.flagInspector;
if (inspector != null) {
stream = inspector.isStream(p.getHeader());
}
if (!stream) {
byte[] b = (p.getBody() == null) ? new byte[0] : p.getBody();
st.memoryBody = b;
st.success = true;
st.completed = true;
st.done.countDown();
return;
}
st.streamExpected = true;
// Some servers may already include metadata here; do not complete until stream end.
}
}
/**
* Handles stream start.
*
* @param p start packet
*/
public void onStreamStart(WebStreamStartPacket_v1_0_1_B p) {
if (p == null || p.getHeader() == null) return;
ResponseState st = inFlight.get(p.getHeader().getRequestId());
if (st == null) return;
synchronized (st.lock) {
if (st.completed) return;
st.statusCode = p.getStatusCode();
st.contentType = safeContentType(p.getContentType());
st.headers = safeHeaders(p.getHeaders());
st.totalLength = p.getTotalLength();
st.streamExpected = true;
if (st.spooler == null) {
try {
st.spooler = TempChunkSpooler.create(st.requestId);
st.spoolDir = st.spooler.dir();
} catch (IOException e) {
failLocked(st, "Failed to create stream spooler: " + e.getMessage());
}
}
}
}
/**
* Handles stream chunk (best-effort; stores chunk by seq).
*
* @param p chunk packet
*/
public void onStreamChunk(WebStreamChunkPacket_v1_0_1_B p) {
if (p == null || p.getHeader() == null) return;
ResponseState st = inFlight.get(p.getHeader().getRequestId());
if (st == null) return;
int seq = p.getSeq();
if (seq < 0) return;
byte[] data = (p.getData() == null) ? new byte[0] : p.getData();
if (data.length == 0) return;
synchronized (st.lock) {
if (st.completed) return;
st.streamExpected = true;
if (st.spooler == null) {
try {
st.spooler = TempChunkSpooler.create(st.requestId);
st.spoolDir = st.spooler.dir();
} catch (IOException e) {
failLocked(st, "Failed to create stream spooler: " + e.getMessage());
return;
}
}
try {
st.spooler.writeChunk(seq, data);
if (seq > st.maxSeqSeen) st.maxSeqSeen = seq;
} catch (IOException e) {
failLocked(st, "Failed to spool stream chunk: " + e.getMessage());
}
}
}
/**
* Handles stream end and assembles best-effort output.
*
* <p>On success, assembles final temp file and returns full content bytes (optional; may be large).</p>
*
* @param p end packet
* @return full content bytes (best-effort) or null on failure
*/
public byte[] onStreamEndAndAssemble(WebStreamEndPacket_v1_0_1_B p) {
if (p == null || p.getHeader() == null) return null;
ResponseState st = inFlight.get(p.getHeader().getRequestId());
if (st == null) return null;
synchronized (st.lock) {
if (st.completed) return null;
st.success = p.isOk();
st.errorMessage = p.getError();
if (!st.success) {
st.completed = true;
st.done.countDown();
return null;
}
if (st.spooler == null) {
// No chunks received; treat as empty success.
st.finalFile = null;
st.completed = true;
st.done.countDown();
return new byte[0];
}
st.spoolDir = st.spooler.dir();
try {
st.finalFile = st.spooler.assembleBestEffort(st.maxSeqSeen);
} catch (IOException e) {
failLocked(st, "Failed to assemble stream: " + e.getMessage());
return null;
} finally {
try {
st.spooler.close();
} catch (IOException ignored) {
}
}
st.completed = true;
st.done.countDown();
// Optional: deliver full bytes to callback (requested).
// WARNING: This can be large. You explicitly requested it, so we do it.
if (st.finalFile == null) return new byte[0];
try (InputStream in = Files.newInputStream(st.finalFile)) {
return in.readAllBytes();
} catch (IOException e) {
// Assembly is still fine; just no full byte[].
return null;
}
}
}
private OacWebResponse awaitAndBuildResponse(ResponseState st) throws IOException {
try {
if (!st.done.await(RESPONSE_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
removeAndCleanup(st);
throw new IOException("Timeout waiting for web response (requestId=" + st.requestId + ")");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
removeAndCleanup(st);
throw new IOException("Interrupted while waiting for web response (requestId=" + st.requestId + ")", e);
}
// Remove from map; response is completed now.
inFlight.remove(st.requestId);
synchronized (st.lock) {
if (!st.success) {
cleanupLocked(st);
String msg = (st.errorMessage == null || st.errorMessage.isBlank()) ? "Request failed" : st.errorMessage;
throw new IOException(msg);
}
// Non-stream response
if (!st.streamExpected && st.finalFile == null) {
byte[] b = (st.memoryBody == null) ? new byte[0] : st.memoryBody;
return new OacWebResponse(
st.statusCode,
safeContentType(st.contentType),
OacWebResponse.safeHeaders(st.headers),
new ByteArrayInputStream(b),
b.length
);
}
// Stream response -> return InputStream backed by final temp file (delete-on-close)
if (st.finalFile == null) {
// Valid empty stream
return new OacWebResponse(
st.statusCode,
safeContentType(st.contentType),
OacWebResponse.safeHeaders(st.headers),
new ByteArrayInputStream(new byte[0]),
0L
);
}
InputStream fileIn = Files.newInputStream(st.finalFile, StandardOpenOption.READ);
// Delete final file + spool directory when WebView/URLConnection closes the stream.
return new OacWebResponse(
st.statusCode,
safeContentType(st.contentType),
OacWebResponse.safeHeaders(st.headers),
new DeleteOnCloseInputStream(fileIn, st.finalFile, st.spoolDir),
safeFileSize(st.finalFile)
);
}
}
private void removeAndCleanup(ResponseState st) {
inFlight.remove(st.requestId);
synchronized (st.lock) {
cleanupLocked(st);
}
}
private static void cleanupLocked(ResponseState st) {
if (st.spooler != null) {
try {
st.spooler.close();
} catch (IOException ignored) {
}
st.spooler = null;
}
if (st.finalFile != null) {
try {
Files.deleteIfExists(st.finalFile);
} catch (IOException ignored) {
}
st.finalFile = null;
}
if (st.spoolDir != null) {
try {
// Let DeleteOnCloseInputStream handle recursive deletion in success path,
// but ensure cleanup on failure/timeout.
if (Files.exists(st.spoolDir)) {
// best-effort delete
try (DirectoryStream<Path> ds = Files.newDirectoryStream(st.spoolDir)) {
for (Path p : ds) {
try {
Files.deleteIfExists(p);
} catch (IOException ignored2) {
}
}
}
try {
Files.deleteIfExists(st.spoolDir);
} catch (IOException ignored2) {
}
}
} catch (IOException ignored) {
}
st.spoolDir = null;
}
}
private static void failLocked(ResponseState st, String message) {
st.success = false;
st.errorMessage = message;
st.completed = true;
st.done.countDown();
}
private static long safeFileSize(Path p) {
try {
return Files.size(p);
} catch (IOException e) {
return -1L;
}
}
private static String safeContentType(String ct) {
return (ct == null || ct.isBlank()) ? "application/octet-stream" : ct;
}
private static Map<String, String> safeHeaders(Map<String, String> headers) {
return (headers == null || headers.isEmpty()) ? Map.of() : Map.copyOf(headers);
}
private static String headerValue(Map<String, String> headers, String nameLower) {
if (headers == null || headers.isEmpty() || nameLower == null) return null;
String needle = nameLower.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;
}
/**
* Per-request state (multi-flight, correlated by requestId).
*/
private static final class ResponseState {
private final long requestId;
private final Object lock = new Object();
private final CountDownLatch done = new CountDownLatch(1);
private int statusCode = 0;
private String contentType = "application/octet-stream";
private Map<String, String> headers = Map.of();
private boolean streamExpected;
private long totalLength = -1L;
private int maxSeqSeen = -1;
private boolean completed;
private boolean success;
private String errorMessage;
private byte[] memoryBody;
private TempChunkSpooler spooler;
private Path spoolDir;
private Path finalFile;
private ResponseState(long requestId) {
this.requestId = requestId;
}
}
/**
* Spools stream chunks to temp files and assembles best-effort output on end.
*/
private static final class TempChunkSpooler implements Closeable {
private final Path dir;
private TempChunkSpooler(Path dir) {
this.dir = dir;
}
static TempChunkSpooler create(long requestId) throws IOException {
Path dir = Files.createTempDirectory("oac-web-stream-" + requestId + "-");
return new TempChunkSpooler(dir);
}
void writeChunk(int seq, byte[] data) throws IOException {
// One file per seq -> no offset assumptions needed.
Path p = dir.resolve(seq + ".chunk");
Files.write(p, data, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE);
}
Path assembleBestEffort(int maxSeqSeen) throws IOException {
Path out = Files.createTempFile("oac-web-stream-final-", ".bin");
try (OutputStream os = Files.newOutputStream(out, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE)) {
for (int seq = 0; seq <= maxSeqSeen; seq++) {
Path p = dir.resolve(seq + ".chunk");
if (!Files.exists(p)) {
continue; // best-effort: skip gaps
}
try (InputStream in = Files.newInputStream(p, StandardOpenOption.READ)) {
in.transferTo(os);
}
Files.deleteIfExists(p);
}
}
return out;
}
Path dir() {
return dir;
}
@Override
public void close() throws IOException {
// Cleanup remaining chunk files (best-effort).
if (!Files.exists(dir)) return;
try (DirectoryStream<Path> ds = Files.newDirectoryStream(dir)) {
for (Path p : ds) {
try {
Files.deleteIfExists(p);
} catch (IOException ignored) {
}
}
}
try {
Files.deleteIfExists(dir);
} catch (IOException ignored) {
}
}
}
}

View File

@@ -0,0 +1,32 @@
package org.openautonomousconnection.protocol.urlhandler.v1_0_1.beta.web;
import java.io.InputStream;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
/**
* Represents a resolved web response for the JavaFX WebView.
*
* @param statusCode HTTP-like status code
* @param contentType response content-type
* @param headers response headers
* @param bodyStream body stream
* @param contentLength content length if known, else -1
*/
public record OacWebResponse(
int statusCode,
String contentType,
Map<String, String> headers,
InputStream bodyStream,
long contentLength
) {
public OacWebResponse {
Objects.requireNonNull(headers, "headers");
Objects.requireNonNull(bodyStream, "bodyStream");
}
public static Map<String, String> safeHeaders(Map<String, String> h) {
return (h == null) ? Collections.emptyMap() : Collections.unmodifiableMap(h);
}
}

View File

@@ -0,0 +1,30 @@
package org.openautonomousconnection.protocol.urlhandler.v1_0_1.beta.web;
import org.openautonomousconnection.protocol.versions.v1_0_1.beta.WebPacketFlags;
import org.openautonomousconnection.protocol.versions.v1_0_1.beta.WebPacketHeader;
import java.util.Objects;
/**
* Interprets {@link WebPacketHeader#getFlags()} into semantic booleans.
*/
public interface WebFlagInspector {
/**
* @param header web packet header
* @return true if the packet is part of a streamed body transfer
*/
boolean isStream(WebPacketHeader header);
/**
* Default implementation based on {@link WebPacketFlags#STREAM}.
*/
final class Default implements WebFlagInspector {
@Override
public boolean isStream(WebPacketHeader header) {
Objects.requireNonNull(header, "header");
return WebPacketFlags.has(header.getFlags(), WebPacketFlags.STREAM);
}
}
}

View File

@@ -0,0 +1,37 @@
package org.openautonomousconnection.protocol.urlhandler.v1_0_1.beta.web;
import java.net.URL;
/**
* Provides per-request WEB correlation context (tab/page/frame).
*/
public interface WebRequestContextProvider {
/**
* Returns the context to use for this URL request.
*
* @param url requested URL
* @return context (never null)
*/
WebRequestContext contextFor(URL url);
/**
* Immutable context DTO.
*
* @param tabId stable tab id
* @param pageId navigation instance id
* @param frameId frame id (0 = main frame)
*/
record WebRequestContext(long tabId, long pageId, long frameId) {
}
/**
* Default context provider (tab/page/frame = 0).
*/
final class Default implements WebRequestContextProvider {
@Override
public WebRequestContext contextFor(URL url) {
return new WebRequestContext(0L, 0L, 0L);
}
}
}

View File

@@ -262,6 +262,7 @@ public final class WebCompatMapper {
return switch (method) {
case GET -> "GET";
case POST, EXECUTE, SCRIPT -> "POST";
default -> "GET";
};
}
@@ -274,7 +275,7 @@ public final class WebCompatMapper {
public static WebRequestMethod map100BMethod(String method) {
if (method == null) return WebRequestMethod.GET;
return switch (method.toUpperCase()) {
case "POST" -> WebRequestMethod.POST;
case "POST", "SCRIPT", "EXECUTE" -> WebRequestMethod.POST;
default -> WebRequestMethod.GET;
};
}