Implemented InfoNameLib
This commit is contained in:
2
pom.xml
2
pom.xml
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
<groupId>org.openautonomousconnection</groupId>
|
<groupId>org.openautonomousconnection</groupId>
|
||||||
<artifactId>Protocol</artifactId>
|
<artifactId>Protocol</artifactId>
|
||||||
<version>1.0.1-BETA.0.2</version>
|
<version>1.0.1-BETA.0.3</version>
|
||||||
<organization>
|
<organization>
|
||||||
<name>Open Autonomous Connection</name>
|
<name>Open Autonomous Connection</name>
|
||||||
<url>https://open-autonomous-connection.org/</url>
|
<url>https://open-autonomous-connection.org/</url>
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ import org.openautonomousconnection.protocol.side.client.ProtocolWebClient;
|
|||||||
import org.openautonomousconnection.protocol.side.ins.ProtocolINSServer;
|
import org.openautonomousconnection.protocol.side.ins.ProtocolINSServer;
|
||||||
import org.openautonomousconnection.protocol.side.server.ProtocolCustomServer;
|
import org.openautonomousconnection.protocol.side.server.ProtocolCustomServer;
|
||||||
import org.openautonomousconnection.protocol.side.web.ProtocolWebServer;
|
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.ProtocolVersion;
|
||||||
import org.openautonomousconnection.protocol.versions.v1_0_0.beta.ProtocolWebServer_1_0_0_B;
|
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)
|
@ProtocolInfo(protocolSide = ProtocolVersion.ProtocolSide.CLIENT)
|
||||||
public ProtocolBridge(ProtocolClient protocolClient, ProtocolValues protocolValues, ProtocolVersion protocolVersion,
|
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
|
// Assign the parameters to the class fields
|
||||||
this.protocolClient = protocolClient;
|
this.protocolClient = protocolClient;
|
||||||
this.protocolValues = protocolValues;
|
this.protocolValues = protocolValues;
|
||||||
@@ -138,6 +141,17 @@ public final class ProtocolBridge {
|
|||||||
// Register the appropriate listeners and packets
|
// Register the appropriate listeners and packets
|
||||||
registerListeners();
|
registerListeners();
|
||||||
registerPackets();
|
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 {
|
private void downloadLicenses() throws IOException {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -262,6 +262,7 @@ public final class WebCompatMapper {
|
|||||||
return switch (method) {
|
return switch (method) {
|
||||||
case GET -> "GET";
|
case GET -> "GET";
|
||||||
case POST, EXECUTE, SCRIPT -> "POST";
|
case POST, EXECUTE, SCRIPT -> "POST";
|
||||||
|
default -> "GET";
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,7 +275,7 @@ public final class WebCompatMapper {
|
|||||||
public static WebRequestMethod map100BMethod(String method) {
|
public static WebRequestMethod map100BMethod(String method) {
|
||||||
if (method == null) return WebRequestMethod.GET;
|
if (method == null) return WebRequestMethod.GET;
|
||||||
return switch (method.toUpperCase()) {
|
return switch (method.toUpperCase()) {
|
||||||
case "POST" -> WebRequestMethod.POST;
|
case "POST", "SCRIPT", "EXECUTE" -> WebRequestMethod.POST;
|
||||||
default -> WebRequestMethod.GET;
|
default -> WebRequestMethod.GET;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user