2026-02-10 23:13:58 +01:00
|
|
|
package org.openautonomousconnection.webclient;
|
2026-02-08 22:36:42 +01:00
|
|
|
|
|
|
|
|
import dev.unlegitdqrk.unlegitlibrary.event.Listener;
|
2026-02-22 18:20:57 +01:00
|
|
|
import lombok.Getter;
|
2026-02-08 22:36:42 +01:00
|
|
|
import org.openautonomousconnection.oacswing.component.OACOptionPane;
|
|
|
|
|
import org.openautonomousconnection.protocol.side.client.ProtocolClient;
|
|
|
|
|
import org.openautonomousconnection.protocol.side.client.events.ConnectedToProtocolINSServerEvent;
|
2026-02-22 18:20:57 +01:00
|
|
|
import org.openautonomousconnection.protocol.urlhandler.v1_0_1.beta.LibClientImpl_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 org.openautonomousconnection.protocol.versions.v1_0_1.beta.WebPacketHeader;
|
|
|
|
|
import org.openautonomousconnection.webclient.ui.BrowserTab;
|
2026-02-08 22:36:42 +01:00
|
|
|
|
2026-02-22 18:21:18 +01:00
|
|
|
import java.awt.*;
|
2026-02-22 18:20:57 +01:00
|
|
|
import java.io.ByteArrayOutputStream;
|
|
|
|
|
import java.net.URL;
|
|
|
|
|
import java.util.Map;
|
2026-02-14 22:16:15 +01:00
|
|
|
import java.util.Objects;
|
2026-02-22 18:20:57 +01:00
|
|
|
import java.util.concurrent.ConcurrentHashMap;
|
2026-02-14 22:16:15 +01:00
|
|
|
import java.util.concurrent.atomic.AtomicBoolean;
|
2026-02-08 22:36:42 +01:00
|
|
|
|
2026-02-14 22:16:15 +01:00
|
|
|
/**
|
2026-02-22 18:20:57 +01:00
|
|
|
* WebClient Protocol implementation (v1.0.1).
|
|
|
|
|
*
|
|
|
|
|
* <p>Implements full stream assembly with strict correlation via:
|
|
|
|
|
* requestId + tabId + pageId + frameId.</p>
|
2026-02-14 22:16:15 +01:00
|
|
|
*/
|
2026-02-22 18:20:57 +01:00
|
|
|
public final class ClientImpl extends ProtocolClient {
|
2026-02-14 22:16:15 +01:00
|
|
|
|
2026-02-22 18:20:57 +01:00
|
|
|
private static final long MAX_STREAM_BYTES = 64L * 1024L * 1024L; // 64MB safety cap
|
|
|
|
|
private static final int MAX_CONCURRENT_STREAMS = 256;
|
|
|
|
|
|
|
|
|
|
@Getter
|
2026-02-14 22:16:15 +01:00
|
|
|
private final LibImpl libImpl = new LibImpl();
|
2026-02-22 18:20:57 +01:00
|
|
|
|
|
|
|
|
private final AtomicBoolean serverConnectionInitialized = new AtomicBoolean(false);
|
2026-02-14 22:16:15 +01:00
|
|
|
private final Component dialogParent;
|
|
|
|
|
private final Runnable onServerReady;
|
|
|
|
|
|
|
|
|
|
public ClientImpl(Component dialogParent, Runnable onServerReady) {
|
|
|
|
|
this.dialogParent = dialogParent;
|
2026-02-22 18:20:57 +01:00
|
|
|
this.onServerReady = Objects.requireNonNull(onServerReady);
|
2026-02-14 22:16:15 +01:00
|
|
|
}
|
|
|
|
|
|
2026-02-08 22:36:42 +01:00
|
|
|
@Override
|
|
|
|
|
public boolean trustINS(String caFingerprint) {
|
|
|
|
|
Object[] options = {"Continue", "Cancel"};
|
2026-02-22 18:20:57 +01:00
|
|
|
return OACOptionPane.showOptionDialog(
|
2026-02-14 22:16:15 +01:00
|
|
|
dialogParent,
|
2026-02-22 18:20:57 +01:00
|
|
|
"Fingerprint: " + caFingerprint + "\nContinue?",
|
2026-02-08 22:36:42 +01:00
|
|
|
"INS Connection",
|
|
|
|
|
OACOptionPane.YES_NO_OPTION,
|
|
|
|
|
OACOptionPane.INFORMATION_MESSAGE,
|
|
|
|
|
null,
|
|
|
|
|
options,
|
2026-02-14 22:16:15 +01:00
|
|
|
options[0]
|
2026-02-22 18:20:57 +01:00
|
|
|
) == 0;
|
2026-02-08 22:36:42 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public boolean trustNewINSFingerprint(String oldCAFingerprint, String newCAFingerprint) {
|
|
|
|
|
Object[] options = {"Continue", "Cancel"};
|
2026-02-22 18:20:57 +01:00
|
|
|
return OACOptionPane.showOptionDialog(
|
2026-02-14 22:16:15 +01:00
|
|
|
dialogParent,
|
2026-02-22 18:20:57 +01:00
|
|
|
"Saved: " + oldCAFingerprint + "\nNew: " + newCAFingerprint + "\nContinue?",
|
2026-02-08 22:36:42 +01:00
|
|
|
"INS Connection",
|
|
|
|
|
OACOptionPane.YES_NO_OPTION,
|
|
|
|
|
OACOptionPane.INFORMATION_MESSAGE,
|
|
|
|
|
null,
|
|
|
|
|
options,
|
2026-02-14 22:16:15 +01:00
|
|
|
options[0]
|
2026-02-22 18:20:57 +01:00
|
|
|
) == 0;
|
2026-02-08 22:36:42 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Listener
|
|
|
|
|
public void onConnected(ConnectedToProtocolINSServerEvent event) {
|
|
|
|
|
try {
|
2026-02-22 18:20:57 +01:00
|
|
|
if (serverConnectionInitialized.compareAndSet(false, true)) {
|
|
|
|
|
buildServerConnection(null, getProtocolBridge().getProtocolValues().ssl);
|
2026-02-14 22:16:15 +01:00
|
|
|
onServerReady.run();
|
|
|
|
|
}
|
2026-02-08 22:36:42 +01:00
|
|
|
} catch (Exception e) {
|
2026-02-22 18:20:57 +01:00
|
|
|
serverConnectionInitialized.set(false);
|
|
|
|
|
throw new RuntimeException(e);
|
2026-02-14 22:16:15 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-22 18:21:18 +01:00
|
|
|
private record StreamKey(long requestId, long tabId, long pageId, long frameId) {
|
|
|
|
|
|
|
|
|
|
StreamKey(WebPacketHeader h) {
|
|
|
|
|
this(h.getRequestId(), h.getTabId(), h.getPageId(), h.getFrameId());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static final class StreamState {
|
|
|
|
|
|
|
|
|
|
private final int statusCode;
|
|
|
|
|
private final String contentType;
|
|
|
|
|
private final Map<String, String> headers;
|
|
|
|
|
private final long declaredLength;
|
|
|
|
|
|
|
|
|
|
private final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
|
|
|
|
|
private int expectedSeq = 0;
|
|
|
|
|
private long written = 0;
|
|
|
|
|
private boolean ended = false;
|
|
|
|
|
private boolean ok = true;
|
|
|
|
|
|
|
|
|
|
StreamState(int statusCode,
|
|
|
|
|
String contentType,
|
|
|
|
|
Map<String, String> headers,
|
|
|
|
|
long declaredLength) {
|
|
|
|
|
|
|
|
|
|
this.statusCode = statusCode;
|
|
|
|
|
this.contentType = contentType == null ? "application/octet-stream" : contentType;
|
|
|
|
|
this.headers = headers == null ? Map.of() : Map.copyOf(headers);
|
|
|
|
|
this.declaredLength = declaredLength;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void append(int seq, byte[] data) {
|
|
|
|
|
|
|
|
|
|
if (ended) throw new IllegalStateException("Chunk after end");
|
|
|
|
|
if (seq != expectedSeq) throw new IllegalStateException("Out-of-order chunk");
|
|
|
|
|
|
|
|
|
|
expectedSeq++;
|
|
|
|
|
|
|
|
|
|
if (data == null || data.length == 0) return;
|
|
|
|
|
|
|
|
|
|
written += data.length;
|
|
|
|
|
if (written > MAX_STREAM_BYTES)
|
|
|
|
|
throw new IllegalStateException("Stream exceeds limit");
|
|
|
|
|
|
|
|
|
|
buffer.writeBytes(data);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void markEnd(boolean ok, String error) {
|
|
|
|
|
this.ended = true;
|
|
|
|
|
this.ok = ok;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
byte[] finish() {
|
|
|
|
|
if (!ok) return new byte[0];
|
|
|
|
|
byte[] data = buffer.toByteArray();
|
|
|
|
|
|
|
|
|
|
if (declaredLength > 0 && data.length != declaredLength) {
|
|
|
|
|
// tolerated but can log if needed
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return data;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-22 18:20:57 +01:00
|
|
|
public final class LibImpl extends LibClientImpl_v1_0_1_B {
|
|
|
|
|
|
|
|
|
|
private final WebRequestContextProvider provider = new WebRequestContextProvider.Default();
|
|
|
|
|
private final WebFlagInspector inspector = new WebFlagInspector.Default();
|
|
|
|
|
private final ConcurrentHashMap<StreamKey, StreamState> streams = new ConcurrentHashMap<>();
|
|
|
|
|
|
|
|
|
|
private BrowserTab currentTab;
|
|
|
|
|
|
2026-02-14 22:16:15 +01:00
|
|
|
@Override
|
|
|
|
|
public void serverConnectionFailed(Exception exception) {
|
|
|
|
|
getProtocolBridge().getLogger().exception("Failed to connect to server", exception);
|
|
|
|
|
OACOptionPane.showMessageDialog(
|
|
|
|
|
dialogParent,
|
|
|
|
|
"Failed to connect to Server:\n" + exception.getMessage(),
|
|
|
|
|
"Server Connection",
|
|
|
|
|
OACOptionPane.ERROR_MESSAGE
|
|
|
|
|
);
|
2026-02-08 22:36:42 +01:00
|
|
|
}
|
2026-02-22 18:20:57 +01:00
|
|
|
|
|
|
|
|
public void bindTab(BrowserTab tab) {
|
|
|
|
|
this.currentTab = tab;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public boolean isStream(WebPacketHeader header) {
|
|
|
|
|
return inspector.isStream(header);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public WebRequestContext contextFor(URL url) {
|
|
|
|
|
return provider.contextFor(url);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void streamStart(WebPacketHeader header,
|
|
|
|
|
int statusCode,
|
|
|
|
|
String contentType,
|
|
|
|
|
Map<String, String> headers,
|
|
|
|
|
long totalLength) {
|
|
|
|
|
|
|
|
|
|
if (streams.size() >= MAX_CONCURRENT_STREAMS) {
|
|
|
|
|
throw new IllegalStateException("Too many concurrent streams");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
StreamKey key = new StreamKey(header);
|
|
|
|
|
streams.put(key, new StreamState(statusCode, contentType, headers, totalLength));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void streamChunk(WebPacketHeader header, int seq, byte[] data) {
|
|
|
|
|
|
|
|
|
|
StreamState state = streams.get(new StreamKey(header));
|
|
|
|
|
if (state == null) {
|
|
|
|
|
throw new IllegalStateException("Chunk without streamStart");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
state.append(seq, data);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void streamEnd(WebPacketHeader header, boolean ok, String error) {
|
|
|
|
|
|
|
|
|
|
StreamState state = streams.get(new StreamKey(header));
|
|
|
|
|
if (state != null) {
|
|
|
|
|
state.markEnd(ok, error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void streamFinish(WebPacketHeader header, byte[] ignored) {
|
|
|
|
|
|
|
|
|
|
StreamKey key = new StreamKey(header);
|
|
|
|
|
StreamState state = streams.remove(key);
|
|
|
|
|
if (state == null) return;
|
|
|
|
|
|
|
|
|
|
byte[] content = state.finish();
|
|
|
|
|
|
|
|
|
|
if (currentTab != null) {
|
|
|
|
|
currentTab.handleStreamFinished(
|
|
|
|
|
header.getRequestId(),
|
|
|
|
|
header.getTabId(),
|
|
|
|
|
header.getPageId(),
|
|
|
|
|
header.getFrameId(),
|
|
|
|
|
state.statusCode,
|
|
|
|
|
state.contentType,
|
|
|
|
|
state.headers,
|
|
|
|
|
content
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|