From fcd9c576509b45c961ba7933e6d07befba9b1792 Mon Sep 17 00:00:00 2001 From: UnlegitDqrk Date: Tue, 10 Feb 2026 19:34:28 +0100 Subject: [PATCH] Web protocol ready --- README.md | 20 +- pom.xml | 11 +- .../InfoNameURLStreamHandlerFactory.java | 24 - .../infonamelib/InfoNames.java | 16 - .../infonamelib/OacWebPacketListener.java | 173 ++++++++ .../infonamelib/OacWebRequestBroker.java | 419 ++++++++++++++++++ .../infonamelib/OacWebResponse.java | 32 ++ .../infonamelib/OacWebURLConnection.java | 126 ++++++ .../infonamelib/OacWebUrlInstaller.java | 52 +++ .../infonamelib/OacWebUrlStreamHandler.java | 28 ++ .../infonamelib/ProtocolHandlerPackages.java | 42 ++ .../ftp/FTPInfoNameURLConnection.java | 26 -- .../ftp/FTPInfoNameURLStreamHandler.java | 17 - .../web/WebInfoNameURLConnection.java | 26 -- .../web/WebInfoNameURLStreamHandler.java | 17 - .../infonamelib/web/Handler.java | 22 + src/main/resources/license/protocol/oapl | 1 + 17 files changed, 921 insertions(+), 131 deletions(-) delete mode 100644 src/main/java/org/openautonomousconnection/infonamelib/InfoNameURLStreamHandlerFactory.java delete mode 100644 src/main/java/org/openautonomousconnection/infonamelib/InfoNames.java create mode 100644 src/main/java/org/openautonomousconnection/infonamelib/OacWebPacketListener.java create mode 100644 src/main/java/org/openautonomousconnection/infonamelib/OacWebRequestBroker.java create mode 100644 src/main/java/org/openautonomousconnection/infonamelib/OacWebResponse.java create mode 100644 src/main/java/org/openautonomousconnection/infonamelib/OacWebURLConnection.java create mode 100644 src/main/java/org/openautonomousconnection/infonamelib/OacWebUrlInstaller.java create mode 100644 src/main/java/org/openautonomousconnection/infonamelib/OacWebUrlStreamHandler.java create mode 100644 src/main/java/org/openautonomousconnection/infonamelib/ProtocolHandlerPackages.java delete mode 100644 src/main/java/org/openautonomousconnection/infonamelib/protocols/ftp/FTPInfoNameURLConnection.java delete mode 100644 src/main/java/org/openautonomousconnection/infonamelib/protocols/ftp/FTPInfoNameURLStreamHandler.java delete mode 100644 src/main/java/org/openautonomousconnection/infonamelib/protocols/web/WebInfoNameURLConnection.java delete mode 100644 src/main/java/org/openautonomousconnection/infonamelib/protocols/web/WebInfoNameURLStreamHandler.java create mode 100644 src/main/java/org/openautonomousconnection/infonamelib/web/Handler.java create mode 100644 src/main/resources/license/protocol/oapl diff --git a/README.md b/README.md index 2f0bc90..48e8d85 100644 --- a/README.md +++ b/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
Just set the protocol schemes to OAC-InfoName ones by calling: -InfoNames.registerOACInfoNameProtocols() \ No newline at end of file + +```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); + } +} +``` \ No newline at end of file diff --git a/pom.xml b/pom.xml index 8d8dda3..d517320 100644 --- a/pom.xml +++ b/pom.xml @@ -1,12 +1,12 @@ - 4.0.0 org.openautonomousconnection InfoNameLib - 1.0.0-BETA.1.1 + 1.0.0-BETA.1.2 Open Autonomous Connection https://open-autonomous-connection.org/ @@ -94,6 +94,11 @@ 6.0.0 test + + org.openautonomousconnection + Protocol + 1.0.0-BETA.7.7 + diff --git a/src/main/java/org/openautonomousconnection/infonamelib/InfoNameURLStreamHandlerFactory.java b/src/main/java/org/openautonomousconnection/infonamelib/InfoNameURLStreamHandlerFactory.java deleted file mode 100644 index 566df08..0000000 --- a/src/main/java/org/openautonomousconnection/infonamelib/InfoNameURLStreamHandlerFactory.java +++ /dev/null @@ -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; - }; - - - } -} diff --git a/src/main/java/org/openautonomousconnection/infonamelib/InfoNames.java b/src/main/java/org/openautonomousconnection/infonamelib/InfoNames.java deleted file mode 100644 index 04ba44c..0000000 --- a/src/main/java/org/openautonomousconnection/infonamelib/InfoNames.java +++ /dev/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()); - } -} diff --git a/src/main/java/org/openautonomousconnection/infonamelib/OacWebPacketListener.java b/src/main/java/org/openautonomousconnection/infonamelib/OacWebPacketListener.java new file mode 100644 index 0000000..0b73e1f --- /dev/null +++ b/src/main/java/org/openautonomousconnection/infonamelib/OacWebPacketListener.java @@ -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. + * + *

Important:

+ *
    + *
  • The shown protocol types do not contain any correlation id.
  • + *
  • Therefore, the broker must treat the connection as single-flight (one in-flight request).
  • + *
+ */ +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 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()); + } +} \ No newline at end of file diff --git a/src/main/java/org/openautonomousconnection/infonamelib/OacWebRequestBroker.java b/src/main/java/org/openautonomousconnection/infonamelib/OacWebRequestBroker.java new file mode 100644 index 0000000..8a93315 --- /dev/null +++ b/src/main/java/org/openautonomousconnection/infonamelib/OacWebRequestBroker.java @@ -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. + * + *

Protocol limitation: no correlation id -> single-flight (one in-flight request at a time).

+ */ +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 safeHeaders(Map 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 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 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 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 chunkBuffer = new HashMap<>(); + private int statusCode = 0; + private String contentType = "application/octet-stream"; + private Map headers = Map.of(); + private boolean streamStarted; + private long totalLength; + private int nextExpectedSeq = 0; + + private boolean completed; + private boolean success; + private String errorMessage; + } +} \ No newline at end of file diff --git a/src/main/java/org/openautonomousconnection/infonamelib/OacWebResponse.java b/src/main/java/org/openautonomousconnection/infonamelib/OacWebResponse.java new file mode 100644 index 0000000..208fc7b --- /dev/null +++ b/src/main/java/org/openautonomousconnection/infonamelib/OacWebResponse.java @@ -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 headers, + InputStream bodyStream, + long contentLength +) { + public OacWebResponse { + Objects.requireNonNull(headers, "headers"); + Objects.requireNonNull(bodyStream, "bodyStream"); + } + + public static Map safeHeaders(Map h) { + return (h == null) ? Collections.emptyMap() : Collections.unmodifiableMap(h); + } +} \ No newline at end of file diff --git a/src/main/java/org/openautonomousconnection/infonamelib/OacWebURLConnection.java b/src/main/java/org/openautonomousconnection/infonamelib/OacWebURLConnection.java new file mode 100644 index 0000000..a909d87 --- /dev/null +++ b/src/main/java/org/openautonomousconnection/infonamelib/OacWebURLConnection.java @@ -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. + * + *

Important: This connection enforces global serialization of requests because the protocol has no request IDs.

+ */ +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 headers, String nameLower) { + if (headers == null || headers.isEmpty()) return null; + for (Map.Entry 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> getHeaderFields() { + try { + connect(); + } catch (IOException e) { + return Map.of(); + } + + Map> out = new LinkedHashMap<>(); + for (Map.Entry 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; + } +} \ No newline at end of file diff --git a/src/main/java/org/openautonomousconnection/infonamelib/OacWebUrlInstaller.java b/src/main/java/org/openautonomousconnection/infonamelib/OacWebUrlInstaller.java new file mode 100644 index 0000000..7c5a0b2 --- /dev/null +++ b/src/main/java/org/openautonomousconnection/infonamelib/OacWebUrlInstaller.java @@ -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}. + * + *

This avoids {@link java.net.URL#setURLStreamHandlerFactory} which can only be set once per JVM.

+ */ +public final class OacWebUrlInstaller { + + private static final AtomicBoolean INSTALLED = new AtomicBoolean(false); + + private OacWebUrlInstaller() { + } + + /** + * Installs: + *
    + *
  • protocol handler package prefix
  • + *
  • packet listener forwarding WebResponse/WebStream + INSResponse into the broker
  • + *
+ * + *

Must be called before any {@code web://} URL is resolved/loaded.

+ * + * @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"); + } +} \ No newline at end of file diff --git a/src/main/java/org/openautonomousconnection/infonamelib/OacWebUrlStreamHandler.java b/src/main/java/org/openautonomousconnection/infonamelib/OacWebUrlStreamHandler.java new file mode 100644 index 0000000..22d311e --- /dev/null +++ b/src/main/java/org/openautonomousconnection/infonamelib/OacWebUrlStreamHandler.java @@ -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); + } +} \ No newline at end of file diff --git a/src/main/java/org/openautonomousconnection/infonamelib/ProtocolHandlerPackages.java b/src/main/java/org/openautonomousconnection/infonamelib/ProtocolHandlerPackages.java new file mode 100644 index 0000000..a51dd14 --- /dev/null +++ b/src/main/java/org/openautonomousconnection/infonamelib/ProtocolHandlerPackages.java @@ -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 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); + } +} \ No newline at end of file diff --git a/src/main/java/org/openautonomousconnection/infonamelib/protocols/ftp/FTPInfoNameURLConnection.java b/src/main/java/org/openautonomousconnection/infonamelib/protocols/ftp/FTPInfoNameURLConnection.java deleted file mode 100644 index cde274e..0000000 --- a/src/main/java/org/openautonomousconnection/infonamelib/protocols/ftp/FTPInfoNameURLConnection.java +++ /dev/null @@ -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 { - - } -} diff --git a/src/main/java/org/openautonomousconnection/infonamelib/protocols/ftp/FTPInfoNameURLStreamHandler.java b/src/main/java/org/openautonomousconnection/infonamelib/protocols/ftp/FTPInfoNameURLStreamHandler.java deleted file mode 100644 index ab6841f..0000000 --- a/src/main/java/org/openautonomousconnection/infonamelib/protocols/ftp/FTPInfoNameURLStreamHandler.java +++ /dev/null @@ -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); - } -} diff --git a/src/main/java/org/openautonomousconnection/infonamelib/protocols/web/WebInfoNameURLConnection.java b/src/main/java/org/openautonomousconnection/infonamelib/protocols/web/WebInfoNameURLConnection.java deleted file mode 100644 index 8f6872c..0000000 --- a/src/main/java/org/openautonomousconnection/infonamelib/protocols/web/WebInfoNameURLConnection.java +++ /dev/null @@ -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 { - - } -} diff --git a/src/main/java/org/openautonomousconnection/infonamelib/protocols/web/WebInfoNameURLStreamHandler.java b/src/main/java/org/openautonomousconnection/infonamelib/protocols/web/WebInfoNameURLStreamHandler.java deleted file mode 100644 index 9930204..0000000 --- a/src/main/java/org/openautonomousconnection/infonamelib/protocols/web/WebInfoNameURLStreamHandler.java +++ /dev/null @@ -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); - } -} diff --git a/src/main/java/org/openautonomousconnection/infonamelib/web/Handler.java b/src/main/java/org/openautonomousconnection/infonamelib/web/Handler.java new file mode 100644 index 0000000..b08b564 --- /dev/null +++ b/src/main/java/org/openautonomousconnection/infonamelib/web/Handler.java @@ -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. + * + *

Loaded via the "java.protocol.handler.pkgs" mechanism.

+ */ +public final class Handler extends URLStreamHandler { + + @Override + protected URLConnection openConnection(URL u) throws IOException { + return new OacWebURLConnection(u, OacWebRequestBroker.get()); + } +} \ No newline at end of file diff --git a/src/main/resources/license/protocol/oapl b/src/main/resources/license/protocol/oapl new file mode 100644 index 0000000..3f64855 --- /dev/null +++ b/src/main/resources/license/protocol/oapl @@ -0,0 +1 @@ +Please read the license here: https://open-autonomous-connection.org/license.html \ No newline at end of file