Web protocol ready
This commit is contained in:
20
README.md
20
README.md
@@ -1,8 +1,24 @@
|
||||
# InfoName - Lib
|
||||
|
||||
InfoName and URL library to make client-side connection set-ups easier
|
||||
InfoName and URL library to make client-side connection set-ups easier
|
||||
|
||||
<hr>
|
||||
|
||||
Just set the protocol schemes to OAC-InfoName ones by calling:
|
||||
<code>InfoNames.registerOACInfoNameProtocols()</code>
|
||||
|
||||
```java
|
||||
import dev.unlegitdqrk.unlegitlibrary.event.EventManager;
|
||||
import org.openautonomousconnection.infonamelib.OacWebUrlInstaller;
|
||||
import org.openautonomousconnection.infonamelib.ProtocolHandlerPackages;
|
||||
import org.openautonomousconnection.protocol.side.client.ProtocolClient;
|
||||
|
||||
class Example {
|
||||
private void init() {
|
||||
EventManager eventManager = new EventManager();
|
||||
ProtocolClient client = new MyImpl();
|
||||
|
||||
ProtocolHandlerPackages.installPackage("org.openautonomousconnection.infonamelib");
|
||||
OacWebUrlInstaller.installOnce(eventManager, client);
|
||||
}
|
||||
}
|
||||
```
|
||||
11
pom.xml
11
pom.xml
@@ -1,12 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>org.openautonomousconnection</groupId>
|
||||
<artifactId>InfoNameLib</artifactId>
|
||||
<version>1.0.0-BETA.1.1</version>
|
||||
<version>1.0.0-BETA.1.2</version>
|
||||
<organization>
|
||||
<name>Open Autonomous Connection</name>
|
||||
<url>https://open-autonomous-connection.org/</url>
|
||||
@@ -94,6 +94,11 @@
|
||||
<version>6.0.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openautonomousconnection</groupId>
|
||||
<artifactId>Protocol</artifactId>
|
||||
<version>1.0.0-BETA.7.7</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
/* Author: Maple
|
||||
* Jan. 18 2026
|
||||
* */
|
||||
|
||||
package org.openautonomousconnection.infonamelib;
|
||||
|
||||
import org.openautonomousconnection.infonamelib.protocols.ftp.FTPInfoNameURLStreamHandler;
|
||||
import org.openautonomousconnection.infonamelib.protocols.web.WebInfoNameURLStreamHandler;
|
||||
|
||||
import java.net.URLStreamHandler;
|
||||
import java.net.URLStreamHandlerFactory;
|
||||
|
||||
public class InfoNameURLStreamHandlerFactory implements URLStreamHandlerFactory {
|
||||
@Override
|
||||
public URLStreamHandler createURLStreamHandler(String protocol) {
|
||||
return switch (protocol) {
|
||||
case "web" -> new WebInfoNameURLStreamHandler();
|
||||
case "ftp" -> new FTPInfoNameURLStreamHandler();
|
||||
default -> null;
|
||||
};
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
/* Author: Maple
|
||||
* Jan. 18 2026
|
||||
* */
|
||||
|
||||
package org.openautonomousconnection.infonamelib;
|
||||
|
||||
import java.net.URL;
|
||||
|
||||
public class InfoNames {
|
||||
/**
|
||||
* Switches accepted Schemes in URLs to InfoName ones
|
||||
*/
|
||||
public static void registerOACInfoNameProtocols() {
|
||||
URL.setURLStreamHandlerFactory(new InfoNameURLStreamHandlerFactory());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
package org.openautonomousconnection.infonamelib;
|
||||
|
||||
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;
|
||||
import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.stream.WebStreamEndPacket;
|
||||
import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.stream.WebStreamStartPacket;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Creates a new listener bound to the given broker.
|
||||
*
|
||||
* @param broker broker instance
|
||||
*/
|
||||
public OacWebPacketListener(OacWebRequestBroker broker, ProtocolClient client) {
|
||||
this.broker = Objects.requireNonNull(broker, "broker");
|
||||
this.client = Objects.requireNonNull(client, "client");
|
||||
}
|
||||
|
||||
@Listener(priority = EventPriority.HIGHEST)
|
||||
public void onConnected(ConnectedToProtocolServerEvent event) {
|
||||
OacWebRequestBroker.get().notifyServerConnected();
|
||||
}
|
||||
|
||||
@Listener(priority = EventPriority.HIGHEST)
|
||||
public void onPacketRead(C_PacketReadEvent event) {
|
||||
Object p = event.getPacket();
|
||||
|
||||
if (p instanceof 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;
|
||||
port = 1028;
|
||||
} 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();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (p instanceof WebResponsePacket resp) {
|
||||
onWebResponse(resp);
|
||||
return;
|
||||
}
|
||||
|
||||
if (p instanceof WebStreamStartPacket start) {
|
||||
onStreamStart(start);
|
||||
return;
|
||||
}
|
||||
|
||||
if (p instanceof WebStreamChunkPacket chunk) {
|
||||
onStreamChunk(chunk);
|
||||
return;
|
||||
}
|
||||
|
||||
if (p instanceof WebStreamEndPacket end) {
|
||||
onStreamEnd(end);
|
||||
}
|
||||
}
|
||||
|
||||
private void connectServer(String hostname, int port) {
|
||||
try {
|
||||
// Ensure the server connection object exists
|
||||
client.buildServerConnection(null, client.getProtocolBridge().getProtocolValues().ssl);
|
||||
|
||||
if (client.getClientServerConnection() != null && client.getClientServerConnection().isConnected()) {
|
||||
client.getClientServerConnection().disconnect();
|
||||
}
|
||||
|
||||
client.getClientServerConnection().connect(hostname, port);
|
||||
} catch (Exception e) {
|
||||
broker.invalidateCurrentInfoName();
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a non-streamed WebResponsePacket.
|
||||
*
|
||||
* @param resp response packet
|
||||
*/
|
||||
private void onWebResponse(WebResponsePacket resp) {
|
||||
broker.onWebResponse(resp.getStatusCode(), resp.getContentType(), resp.getHeaders(), resp.getBody());
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the beginning of a streamed response.
|
||||
*
|
||||
* @param start stream start packet
|
||||
*/
|
||||
private void onStreamStart(WebStreamStartPacket start) {
|
||||
broker.onStreamStart(start.getStatusCode(), start.getContentType(), start.getHeaders(), start.getTotalLength());
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a chunk of a streamed response.
|
||||
*
|
||||
* @param chunk chunk packet
|
||||
*/
|
||||
private void onStreamChunk(WebStreamChunkPacket chunk) {
|
||||
broker.onStreamChunk(chunk.getSeq(), chunk.getData());
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles stream end.
|
||||
*
|
||||
* @param end stream end packet
|
||||
*/
|
||||
private void onStreamEnd(WebStreamEndPacket end) {
|
||||
broker.onStreamEnd(end.isOk());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,419 @@
|
||||
package org.openautonomousconnection.infonamelib;
|
||||
|
||||
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>
|
||||
*/
|
||||
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;
|
||||
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 normalizePath(String path) {
|
||||
if (path == null || path.isBlank() || "/".equals(path)) {
|
||||
return "index.html";
|
||||
}
|
||||
String p = path.startsWith("/") ? path.substring(1) : path;
|
||||
return p.isBlank() ? "index.html" : p;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches the client used to send INS/Web packets.
|
||||
*
|
||||
* @param client protocol client
|
||||
*/
|
||||
public void attachClient(ProtocolClient client) {
|
||||
this.client = Objects.requireNonNull(client, "client");
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a URL via OAC protocol (used by {@link java.net.URLConnection}).
|
||||
*
|
||||
* @param url web:// URL
|
||||
* @return response
|
||||
*/
|
||||
public OacWebResponse fetch(URL url) {
|
||||
Objects.requireNonNull(url, "url");
|
||||
|
||||
ProtocolClient c = this.client;
|
||||
if (c == null) {
|
||||
throw new IllegalStateException("ProtocolClient not attached. Call OacWebUrlInstaller.installOnce(..., client) first.");
|
||||
}
|
||||
|
||||
Response r = openAndAwait(c, url);
|
||||
|
||||
byte[] body = (r.body() == null) ? new byte[0] : r.body();
|
||||
long len = body.length;
|
||||
|
||||
return new OacWebResponse(
|
||||
r.statusCode(),
|
||||
r.contentType(),
|
||||
OacWebResponse.safeHeaders(r.headers()),
|
||||
new ByteArrayInputStream(body),
|
||||
len
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a resource and blocks until the current single-flight response completes.
|
||||
*
|
||||
* @param client protocol client
|
||||
* @param url web:// URL
|
||||
* @return response snapshot
|
||||
*/
|
||||
public Response openAndAwait(ProtocolClient client, URL url) {
|
||||
Objects.requireNonNull(client, "client");
|
||||
Objects.requireNonNull(url, "url");
|
||||
|
||||
open(client, url);
|
||||
return awaitResponse(RESPONSE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends required packets for a {@code web://} URL.
|
||||
*
|
||||
* @param client protocol client
|
||||
* @param url web:// URL
|
||||
*/
|
||||
public synchronized void open(ProtocolClient client, URL url) {
|
||||
Objects.requireNonNull(client, "client");
|
||||
Objects.requireNonNull(url, "url");
|
||||
|
||||
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 = normalizePath(url.getPath());
|
||||
|
||||
beginNewResponse();
|
||||
|
||||
if (!infoName.equals(currentInfoName)) {
|
||||
resolveAndConnect(client, infoName);
|
||||
currentInfoName = infoName;
|
||||
} else {
|
||||
awaitConnectionIfPending();
|
||||
}
|
||||
|
||||
sendWebRequest(client, path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called once the ServerConnection is established (from listener).
|
||||
*/
|
||||
public void notifyServerConnected() {
|
||||
CountDownLatch latch = connectionLatch;
|
||||
if (latch != null) {
|
||||
latch.countDown();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Forces re-resolution on next request.
|
||||
*/
|
||||
public synchronized void invalidateCurrentInfoName() {
|
||||
currentInfoName = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Receives non-streamed WebResponsePacket.
|
||||
*/
|
||||
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.write(b, 0, b.length);
|
||||
|
||||
st.completed = true;
|
||||
st.success = true;
|
||||
st.done.countDown();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Receives stream start.
|
||||
*/
|
||||
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 = Math.max(0, totalLength);
|
||||
|
||||
st.streamStarted = true;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================
|
||||
// Internal connect + request
|
||||
// ============================
|
||||
|
||||
/**
|
||||
* Receives a stream chunk (may arrive out-of-order).
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
st.chunkBuffer.put(seq, Arrays.copyOf(data, data.length));
|
||||
flushChunksLocked(st);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Receives stream end.
|
||||
*/
|
||||
public void onStreamEnd(boolean ok) {
|
||||
ResponseState st = responseState;
|
||||
if (st == null) return;
|
||||
|
||||
synchronized (responseLock) {
|
||||
if (st.completed) return;
|
||||
|
||||
if (!st.streamStarted) {
|
||||
st.streamStarted = true;
|
||||
}
|
||||
|
||||
flushChunksLocked(st);
|
||||
|
||||
st.completed = true;
|
||||
st.success = ok;
|
||||
if (!ok) {
|
||||
st.errorMessage = "Streaming failed";
|
||||
}
|
||||
st.done.countDown();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for the current 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");
|
||||
}
|
||||
|
||||
try {
|
||||
boolean ok = st.done.await(timeout, unit);
|
||||
if (!ok) {
|
||||
throw new IllegalStateException("Timeout while waiting for Web response");
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IllegalStateException("Interrupted while waiting for Web response", e);
|
||||
}
|
||||
|
||||
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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================
|
||||
// Response helpers
|
||||
// ============================
|
||||
|
||||
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.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) {
|
||||
Objects.requireNonNull(client, "client");
|
||||
Objects.requireNonNull(path, "path");
|
||||
|
||||
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,
|
||||
WebRequestMethod.GET,
|
||||
Map.of(),
|
||||
null
|
||||
);
|
||||
|
||||
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 flushChunksLocked(ResponseState st) {
|
||||
for (; ; ) {
|
||||
byte[] next = st.chunkBuffer.remove(st.nextExpectedSeq);
|
||||
if (next == null) break;
|
||||
|
||||
st.body.write(next, 0, next.length);
|
||||
st.nextExpectedSeq++;
|
||||
}
|
||||
}
|
||||
|
||||
private void failLocked(ResponseState st, String message) {
|
||||
st.completed = true;
|
||||
st.success = false;
|
||||
st.errorMessage = message;
|
||||
st.done.countDown();
|
||||
}
|
||||
|
||||
/**
|
||||
* Immutable response snapshot.
|
||||
*
|
||||
* @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) {
|
||||
}
|
||||
|
||||
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 nextExpectedSeq = 0;
|
||||
|
||||
private boolean completed;
|
||||
private boolean success;
|
||||
private String errorMessage;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package org.openautonomousconnection.infonamelib;
|
||||
|
||||
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,126 @@
|
||||
package org.openautonomousconnection.infonamelib;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* URLConnection implementation that maps "web://" URLs to OAC WebRequestPacket/WebResponsePacket.
|
||||
*
|
||||
* <p>Important: This connection enforces global serialization of requests because the protocol has no request IDs.</p>
|
||||
*/
|
||||
public final class OacWebURLConnection extends URLConnection {
|
||||
|
||||
private static final int MAX_REDIRECTS = 8;
|
||||
|
||||
private final OacWebRequestBroker broker;
|
||||
|
||||
private boolean connected;
|
||||
private OacWebResponse response;
|
||||
|
||||
public OacWebURLConnection(URL url, OacWebRequestBroker broker) {
|
||||
super(url);
|
||||
this.broker = Objects.requireNonNull(broker, "broker");
|
||||
}
|
||||
|
||||
private static String headerValue(Map<String, String> headers, String nameLower) {
|
||||
if (headers == null || headers.isEmpty()) return null;
|
||||
for (Map.Entry<String, String> e : headers.entrySet()) {
|
||||
if (e.getKey() == null) continue;
|
||||
if (e.getKey().trim().toLowerCase(Locale.ROOT).equals(nameLower)) {
|
||||
return e.getValue();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void connect() throws IOException {
|
||||
if (connected) return;
|
||||
|
||||
URL cur = this.url;
|
||||
OacWebResponse resp = null;
|
||||
|
||||
for (int i = 0; i <= MAX_REDIRECTS; i++) {
|
||||
resp = broker.fetch(cur);
|
||||
|
||||
int code = resp.statusCode();
|
||||
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);
|
||||
continue;
|
||||
} catch (Exception ex) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Non-redirect or redirect that cannot be followed -> stop here.
|
||||
break;
|
||||
}
|
||||
|
||||
this.response = resp;
|
||||
this.connected = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getInputStream() throws IOException {
|
||||
connect();
|
||||
return response.bodyStream();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getContentType() {
|
||||
try {
|
||||
connect();
|
||||
} catch (IOException e) {
|
||||
return "application/octet-stream";
|
||||
}
|
||||
String ct = response.contentType();
|
||||
return (ct == null || ct.isBlank()) ? "application/octet-stream" : ct;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getContentLength() {
|
||||
try {
|
||||
connect();
|
||||
} catch (IOException e) {
|
||||
return -1;
|
||||
}
|
||||
long len = response.contentLength();
|
||||
return (len <= 0 || len > Integer.MAX_VALUE) ? -1 : (int) len;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getContentLengthLong() {
|
||||
try {
|
||||
connect();
|
||||
} catch (IOException e) {
|
||||
return -1L;
|
||||
}
|
||||
return response.contentLength();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, List<String>> getHeaderFields() {
|
||||
try {
|
||||
connect();
|
||||
} catch (IOException e) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package org.openautonomousconnection.infonamelib;
|
||||
|
||||
import dev.unlegitdqrk.unlegitlibrary.event.EventManager;
|
||||
import org.openautonomousconnection.protocol.side.client.ProtocolClient;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* Installs the "web://" protocol handler using the standard Java mechanism:
|
||||
* {@code java.protocol.handler.pkgs}.
|
||||
*
|
||||
* <p>This avoids {@link java.net.URL#setURLStreamHandlerFactory} which can only be set once per JVM.</p>
|
||||
*/
|
||||
public final class OacWebUrlInstaller {
|
||||
|
||||
private static final AtomicBoolean INSTALLED = new AtomicBoolean(false);
|
||||
|
||||
private OacWebUrlInstaller() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs:
|
||||
* <ul>
|
||||
* <li>protocol handler package prefix</li>
|
||||
* <li>packet listener forwarding WebResponse/WebStream + INSResponse into the broker</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>Must be called before any {@code web://} URL is resolved/loaded.</p>
|
||||
*
|
||||
* @param eventManager global event manager
|
||||
* @param client protocol client (required for connecting ServerConnection after INS resolution)
|
||||
*/
|
||||
public static void installOnce(EventManager eventManager, ProtocolClient client) {
|
||||
Objects.requireNonNull(eventManager, "eventManager");
|
||||
Objects.requireNonNull(client, "client");
|
||||
|
||||
if (!INSTALLED.compareAndSet(false, true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Make client available for broker.fetch(...)
|
||||
OacWebRequestBroker.get().attachClient(client);
|
||||
|
||||
// Register packet listener (INSResponse + WebResponse + WebStream*)
|
||||
eventManager.registerListener(new OacWebPacketListener(OacWebRequestBroker.get(), client));
|
||||
|
||||
// Register protocol handler package prefix:
|
||||
// JVM will load: "org.openautonomousconnection.webclient.recode.url.web.Handler"
|
||||
ProtocolHandlerPackages.installPackage("org.openautonomousconnection.webclient.recode.url");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package org.openautonomousconnection.infonamelib;
|
||||
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.net.URLStreamHandler;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* URLStreamHandler for the custom OAC scheme "web://".
|
||||
*/
|
||||
public final class OacWebUrlStreamHandler extends URLStreamHandler {
|
||||
|
||||
private final OacWebRequestBroker broker;
|
||||
|
||||
/**
|
||||
* Creates a handler.
|
||||
*
|
||||
* @param broker request broker
|
||||
*/
|
||||
public OacWebUrlStreamHandler(OacWebRequestBroker broker) {
|
||||
this.broker = Objects.requireNonNull(broker, "broker");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected URLConnection openConnection(URL u) {
|
||||
return new OacWebURLConnection(u, broker);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package org.openautonomousconnection.infonamelib;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
/* Author: Maple
|
||||
* Jan. 18 2026
|
||||
* */
|
||||
|
||||
package org.openautonomousconnection.infonamelib.protocols.ftp;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
|
||||
public class FTPInfoNameURLConnection extends URLConnection {
|
||||
/**
|
||||
* Constructs a URL connection to the specified URL. A connection to
|
||||
* the object referenced by the URL is not created.
|
||||
*
|
||||
* @param url the specified URL.
|
||||
*/
|
||||
protected FTPInfoNameURLConnection(URL url) {
|
||||
super(url);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void connect() throws IOException {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
/* Author: Maple
|
||||
* Jan. 18 2026
|
||||
* */
|
||||
|
||||
package org.openautonomousconnection.infonamelib.protocols.ftp;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.net.URLStreamHandler;
|
||||
|
||||
public class FTPInfoNameURLStreamHandler extends URLStreamHandler {
|
||||
@Override
|
||||
protected URLConnection openConnection(URL url) throws IOException {
|
||||
return new FTPInfoNameURLConnection(url);
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
/* Author: Maple
|
||||
* Jan. 18 2026
|
||||
* */
|
||||
|
||||
package org.openautonomousconnection.infonamelib.protocols.web;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
|
||||
public class WebInfoNameURLConnection extends URLConnection {
|
||||
/**
|
||||
* Constructs a URL connection to the specified URL. A connection to
|
||||
* the object referenced by the URL is not created.
|
||||
*
|
||||
* @param url the specified URL.
|
||||
*/
|
||||
protected WebInfoNameURLConnection(URL url) {
|
||||
super(url);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void connect() throws IOException {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
/* Author: Maple
|
||||
* Jan. 18 2026
|
||||
* */
|
||||
|
||||
package org.openautonomousconnection.infonamelib.protocols.web;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.net.URLStreamHandler;
|
||||
|
||||
public class WebInfoNameURLStreamHandler extends URLStreamHandler {
|
||||
@Override
|
||||
protected URLConnection openConnection(URL url) throws IOException {
|
||||
return new WebInfoNameURLConnection(url);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package org.openautonomousconnection.infonamelib.web;
|
||||
|
||||
import org.openautonomousconnection.infonamelib.OacWebRequestBroker;
|
||||
import org.openautonomousconnection.infonamelib.OacWebURLConnection;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.net.URLStreamHandler;
|
||||
|
||||
/**
|
||||
* URLStreamHandler for the "web" protocol.
|
||||
*
|
||||
* <p>Loaded via the "java.protocol.handler.pkgs" mechanism.</p>
|
||||
*/
|
||||
public final class Handler extends URLStreamHandler {
|
||||
|
||||
@Override
|
||||
protected URLConnection openConnection(URL u) throws IOException {
|
||||
return new OacWebURLConnection(u, OacWebRequestBroker.get());
|
||||
}
|
||||
}
|
||||
1
src/main/resources/license/protocol/oapl
Normal file
1
src/main/resources/license/protocol/oapl
Normal file
@@ -0,0 +1 @@
|
||||
Please read the license here: https://open-autonomous-connection.org/license.html
|
||||
Reference in New Issue
Block a user