Files
WebClient/src/main/java/org/openautonomousconnection/webclient/ClientImpl.java

243 lines
8.3 KiB
Java
Raw Normal View History

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
);
}
}
}
}