4 Commits

Author SHA1 Message Date
UnlegitDqrk
b3457ee133 Updated to latest Protocol Version 2026-02-27 20:56:16 +01:00
UnlegitDqrk
b6f7110d16 Updated to latest Protocol Version 2026-02-22 18:21:18 +01:00
UnlegitDqrk
4376fe6daa Updated to latest Protocol Version 2026-02-22 18:20:57 +01:00
UnlegitDqrk
69be55cac0 Usable Browser 2026-02-14 22:27:15 +01:00
10 changed files with 697 additions and 87 deletions

View File

@@ -1 +1,2 @@
Please read the license here: https://open-autonomous-connection.org/license.html Please read the license here: https://open-autonomous-connection.org/license.html
Download all third parties licenses here: https://open-autonomous-connection.org/assets/licenses.zip

View File

@@ -3,3 +3,16 @@
This is the Protocol for our Open Autonomous Connection project.<br /> This is the Protocol for our Open Autonomous Connection project.<br />
Feel free to join our Discord. Feel free to join our Discord.
<br /> <br />
## License Notice
This project (OAC) is licensed under
the [Open Autonomous Public License (OAPL)](https://open-autonomous-connection.org/license.html).
**Third-party components:**
<br />
Download all license here: https://open-autonomous-connection.org/assets/licenses.zip
- *UnlegitLibrary* is authored by the same copyright holder and is used here under a special agreement:
While [UnlegitLibrary](https://repo.unlegitdqrk.dev/UnlegitDqrk/unlegitlibrary/) is generally distributed under
the [GNU GPLv3](https://repo.unlegitdqrk.dev/UnlegitDqrk/unlegitlibrary/src/branch/master/LICENSE),
it is additionally licensed under OAPL **exclusively for the OAC project**.
Therefore, within OAC, the OAPL terms apply to UnlegitLibrary as well.

128
dependency-reduced-pom.xml Normal file
View File

@@ -0,0 +1,128 @@
<?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" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.openautonomousconnection</groupId>
<artifactId>WebClient</artifactId>
<version>1.0.1-BETA.0.1</version>
<description>The default WebClient</description>
<url>https://open-autonomous-connection.org/</url>
<issueManagement>
<system>Issue Tracker</system>
<url>https://repo.open-autonomous-connection.org/open-autonomous-connection/WebClient/issues</url>
</issueManagement>
<developers>
<developer>
<name>UnlegitDqrk</name>
<url>https://unlegitdqrk.dev/</url>
<organization>Open Autonomous Connection</organization>
<organizationUrl>https://open-autonomous-connection.org/</organizationUrl>
<roles>
<role>Owner</role>
<role>Head Developer</role>
</roles>
</developer>
<developer>
<name>Maple</name>
<url>https://niumaple.carrd.co/</url>
<organization>Open Autonomous Connection</organization>
<organizationUrl>https://open-autonomous-connection.org/</organizationUrl>
<roles>
<role>Owner</role>
<role>Head Developer</role>
</roles>
</developer>
</developers>
<licenses>
<license>
<name>Open Autonomous Public License (OAPL)</name>
<url>https://open-autonomous-connection.org/license.html</url>
</license>
</licenses>
<organization>
<name>Open Autonomous Connection</name>
<url>https://open-autonomous-connection.org/</url>
</organization>
<build>
<plugins>
<plugin>
<artifactId>maven-shade-plugin</artifactId>
<version>3.6.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer>
<mainClass>org.openautonomousconnection.webclient.Main</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.42</version>
</path>
</annotationProcessorPaths>
<compilerArgs>
<arg>--add-exports</arg>
<arg>java.base/sun.security.x509=ALL-UNNAMED</arg>
<arg>--add-exports</arg>
<arg>java.base/sun.security.util=ALL-UNNAMED</arg>
</compilerArgs>
</configuration>
</plugin>
<plugin>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.6.3</version>
<executions>
<execution>
<id>attach-javadocs</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
<configuration>
<failOnError>false</failOnError>
<failOnWarnings>false</failOnWarnings>
<doclint>none</doclint>
<locale>en_US</locale>
<encoding>UTF-8</encoding>
<docencoding>UTF-8</docencoding>
<charset>UTF-8</charset>
</configuration>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<snapshots />
<id>oac</id>
<url>https://repo.open-autonomous-connection.org/api/packages/open-autonomous-connection/maven</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.42</version>
<scope>provided</scope>
</dependency>
</dependencies>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.target>25</maven.compiler.target>
<maven.compiler.source>25</maven.compiler.source>
</properties>
</project>

53
pom.xml
View File

@@ -6,7 +6,7 @@
<groupId>org.openautonomousconnection</groupId> <groupId>org.openautonomousconnection</groupId>
<artifactId>WebClient</artifactId> <artifactId>WebClient</artifactId>
<version>1.0.0-BETA.1.0</version> <version>1.0.1-BETA.0.2</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>
@@ -15,8 +15,8 @@
<description>The default WebClient</description> <description>The default WebClient</description>
<properties> <properties>
<maven.compiler.source>23</maven.compiler.source> <maven.compiler.source>25</maven.compiler.source>
<maven.compiler.target>23</maven.compiler.target> <maven.compiler.target>25</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties> </properties>
@@ -69,22 +69,22 @@
<dependency> <dependency>
<groupId>org.openautonomousconnection</groupId> <groupId>org.openautonomousconnection</groupId>
<artifactId>OACSwing</artifactId> <artifactId>OACSwing</artifactId>
<version>1.0.0-BETA.1.1</version> <version>0.0.0-STABLE.1.3</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.openautonomousconnection</groupId> <groupId>org.openautonomousconnection</groupId>
<artifactId>LuaScript</artifactId> <artifactId>LuaScript</artifactId>
<version>1.0.0-BETA.1.1</version> <version>1.0.0-BETA.1.3</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.openautonomousconnection</groupId> <groupId>org.openautonomousconnection</groupId>
<artifactId>InfoNameLib</artifactId> <artifactId>Protocol</artifactId>
<version>1.0.0-BETA.1.3</version> <version>1.0.1-BETA.0.5</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.projectlombok</groupId> <groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId> <artifactId>lombok</artifactId>
<version>1.18.38</version> <version>1.18.42</version>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<dependency> <dependency>
@@ -111,17 +111,46 @@
<plugins> <plugins>
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId> <artifactId>maven-shade-plugin</artifactId>
<version>3.3.0</version> <version>3.6.0</version>
<executions> <executions>
<execution> <execution>
<id>attach-sources</id> <phase>package</phase>
<goals> <goals>
<goal>jar</goal> <goal>shade</goal>
</goals> </goals>
<configuration>
<transformers>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>org.openautonomousconnection.webclient.Main</mainClass>
</transformer>
</transformers>
</configuration>
</execution> </execution>
</executions> </executions>
</plugin> </plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.42</version>
</path>
</annotationProcessorPaths>
<compilerArgs>
<arg>--add-exports</arg>
<arg>java.base/sun.security.x509=ALL-UNNAMED</arg>
<arg>--add-exports</arg>
<arg>java.base/sun.security.util=ALL-UNNAMED</arg>
</compilerArgs>
</configuration>
</plugin>
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId> <artifactId>maven-javadoc-plugin</artifactId>

View File

@@ -1,93 +1,171 @@
package org.openautonomousconnection.webclient; package org.openautonomousconnection.webclient;
import dev.unlegitdqrk.unlegitlibrary.event.Listener; import dev.unlegitdqrk.unlegitlibrary.event.Listener;
import org.openautonomousconnection.infonamelib.LibClientImpl; import dev.unlegitdqrk.unlegitlibrary.network.system.client.events.packets.C_PacketReadEvent;
import org.openautonomousconnection.infonamelib.OacWebUrlInstaller; import lombok.Getter;
import org.openautonomousconnection.oacswing.component.OACOptionPane; import org.openautonomousconnection.oacswing.component.OACOptionPane;
import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.WebPacket;
import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.navigate.WebNavigateAckPacket;
import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.resource.WebResourceResponsePacket;
import org.openautonomousconnection.protocol.side.client.ProtocolClient; import org.openautonomousconnection.protocol.side.client.ProtocolClient;
import org.openautonomousconnection.protocol.side.client.ProtocolWebClient;
import org.openautonomousconnection.protocol.side.client.events.ConnectedToProtocolINSServerEvent; import org.openautonomousconnection.protocol.side.client.events.ConnectedToProtocolINSServerEvent;
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;
import java.awt.*; import java.awt.*;
import java.io.ByteArrayOutputStream;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
/** /**
* Protocol client implementation for the WebClient. * WebClient Protocol implementation (v1.0.1).
*
* <p>Implements full stream assembly with strict correlation via:
* requestId + tabId + pageId + frameId.</p>
*/ */
public class ClientImpl extends ProtocolClient { public final class ClientImpl extends ProtocolWebClient {
private static final long MAX_STREAM_BYTES = 64L * 1024L * 1024L; // 64MB safety cap
private static final int MAX_CONCURRENT_STREAMS = 256;
@Getter
private final LibImpl libImpl = new LibImpl(); private final LibImpl libImpl = new LibImpl();
private final AtomicBoolean connectedInitialized = new AtomicBoolean(false);
private final AtomicBoolean serverConnectionInitialized = new AtomicBoolean(false);
private final Component dialogParent; private final Component dialogParent;
private final Runnable onServerReady; private final Runnable onServerReady;
public ClientImpl(Component dialogParent, Runnable onServerReady) { public ClientImpl(Component dialogParent, Runnable onServerReady) {
this.dialogParent = dialogParent; this.dialogParent = dialogParent;
this.onServerReady = Objects.requireNonNull(onServerReady, "onServerReady"); this.onServerReady = Objects.requireNonNull(onServerReady);
} }
@Override @Override
public boolean trustINS(String caFingerprint) { public boolean trustINS(String caFingerprint) {
Object[] options = {"Continue", "Cancel"}; Object[] options = {"Continue", "Cancel"};
int result = OACOptionPane.showOptionDialog( return OACOptionPane.showOptionDialog(
dialogParent, dialogParent,
"You never connected to this INS before!\n" + "Fingerprint: " + caFingerprint + "\nContinue?",
"Fingerprint: " + caFingerprint + "\nDo you want to connect?",
"INS Connection", "INS Connection",
OACOptionPane.YES_NO_OPTION, OACOptionPane.YES_NO_OPTION,
OACOptionPane.INFORMATION_MESSAGE, OACOptionPane.INFORMATION_MESSAGE,
null, null,
options, options,
options[0] options[0]
); ) == 0;
return result == 0;
} }
@Override @Override
public boolean trustNewINSFingerprint(String oldCAFingerprint, String newCAFingerprint) { public boolean trustNewINSFingerprint(String oldCAFingerprint, String newCAFingerprint) {
Object[] options = {"Continue", "Cancel"}; Object[] options = {"Continue", "Cancel"};
return OACOptionPane.showOptionDialog(
int result = OACOptionPane.showOptionDialog(
dialogParent, dialogParent,
"The fingerprint does not match with the saved fingerprint!\n" + "Saved: " + oldCAFingerprint + "\nNew: " + newCAFingerprint + "\nContinue?",
"Saved Fingerprint: " + oldCAFingerprint + "\n" +
"New Fingerprint: " + newCAFingerprint + "\n" +
"Do you want to connect?",
"INS Connection", "INS Connection",
OACOptionPane.YES_NO_OPTION, OACOptionPane.YES_NO_OPTION,
OACOptionPane.INFORMATION_MESSAGE, OACOptionPane.INFORMATION_MESSAGE,
null, null,
options, options,
options[0] options[0]
); ) == 0;
return result == 0;
} }
@Listener @Listener
public void onConnected(ConnectedToProtocolINSServerEvent event) { public void onConnected(ConnectedToProtocolINSServerEvent event) {
try { try {
buildServerConnection(null, getProtocolBridge().getProtocolValues().ssl); if (serverConnectionInitialized.compareAndSet(false, true)) {
OacWebUrlInstaller.installOnce(getProtocolBridge().getProtocolValues().eventManager, this, libImpl); buildServerConnection(null, getProtocolBridge().getProtocolValues().ssl);
if (connectedInitialized.compareAndSet(false, true)) {
onServerReady.run(); onServerReady.run();
} }
} catch (Exception e) { } catch (Exception e) {
getProtocolBridge().getLogger().exception("Failed to build Server connection", e); serverConnectionInitialized.set(false);
OACOptionPane.showMessageDialog( throw new RuntimeException(e);
dialogParent,
"Failed to to build Server connection:\n" + e.getMessage(),
"Server Connection",
OACOptionPane.ERROR_MESSAGE
);
} }
} }
private class LibImpl extends LibClientImpl { 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;
}
}
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;
@Override @Override
public void serverConnectionFailed(Exception exception) { public void serverConnectionFailed(Exception exception) {
getProtocolBridge().getLogger().exception("Failed to connect to server", exception); getProtocolBridge().getProtocolValues().logger.exception("Failed to connect to server", exception);
OACOptionPane.showMessageDialog( OACOptionPane.showMessageDialog(
dialogParent, dialogParent,
"Failed to connect to Server:\n" + exception.getMessage(), "Failed to connect to Server:\n" + exception.getMessage(),
@@ -95,5 +173,77 @@ public class ClientImpl extends ProtocolClient {
OACOptionPane.ERROR_MESSAGE OACOptionPane.ERROR_MESSAGE
); );
} }
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
);
}
}
} }
} }

View File

@@ -1,6 +1,8 @@
package org.openautonomousconnection.webclient; package org.openautonomousconnection.webclient;
import dev.unlegitdqrk.unlegitlibrary.addon.AddonLoader; import dev.unlegitdqrk.unlegitlibrary.addon.AddonLoader;
import dev.unlegitdqrk.unlegitlibrary.event.EventManager;
import dev.unlegitdqrk.unlegitlibrary.utils.Logger;
import org.openautonomousconnection.oacswing.component.design.Design; import org.openautonomousconnection.oacswing.component.design.Design;
import org.openautonomousconnection.oacswing.component.design.DesignManager; import org.openautonomousconnection.oacswing.component.design.DesignManager;
import org.openautonomousconnection.webclient.settings.AppSettings; import org.openautonomousconnection.webclient.settings.AppSettings;
@@ -8,6 +10,7 @@ import org.openautonomousconnection.webclient.settings.SettingsManager;
import org.openautonomousconnection.webclient.ui.BrowserUI; import org.openautonomousconnection.webclient.ui.BrowserUI;
import javax.swing.*; import javax.swing.*;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.net.CookieHandler; import java.net.CookieHandler;
import java.net.CookieManager; import java.net.CookieManager;
@@ -23,6 +26,8 @@ public class Main {
private static AppSettings settings; private static AppSettings settings;
private static AddonLoader addonLoader; private static AddonLoader addonLoader;
private static Logger logger;
private static EventManager eventManager;
public static BrowserUI getUi() { public static BrowserUI getUi() {
return ui; return ui;
@@ -32,6 +37,14 @@ public class Main {
return settings; return settings;
} }
public static AddonLoader getAddonLoader() {
return addonLoader;
}
public static Logger getLogger() {
return logger;
}
private static void installDefaultCookieManager() { private static void installDefaultCookieManager() {
if (CookieHandler.getDefault() != null) return; if (CookieHandler.getDefault() != null) return;
@@ -40,7 +53,13 @@ public class Main {
CookieHandler.setDefault(cm); CookieHandler.setDefault(cm);
} }
public static void main(String[] args) throws IOException { static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException {
File logsFolder = new File("logs");
if (!logsFolder.exists()) logsFolder.mkdir();
eventManager = new EventManager();
logger = new Logger(logsFolder, false, true);
addonLoader = new AddonLoader(eventManager, logger);
settings = SettingsManager.load(); settings = SettingsManager.load();
FxBootstrap.ensureInitialized(); FxBootstrap.ensureInitialized();
@@ -50,6 +69,7 @@ public class Main {
SwingUtilities.invokeLater(() -> { SwingUtilities.invokeLater(() -> {
ui = new BrowserUI(settings); ui = new BrowserUI(settings);
ui.setSize(1200, 800); ui.setSize(1200, 800);
ui.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
ui.setLocationRelativeTo(null); ui.setLocationRelativeTo(null);
ui.setVisible(true); ui.setVisible(true);
ui.openNewTab(settings.getStartPageUrl()); ui.openNewTab(settings.getStartPageUrl());

View File

@@ -15,26 +15,26 @@ public class WebLogger {
} }
public void log(String string) { public void log(String string) {
client.getProtocolBridge().getLogger().log(host + ": " + string); client.getProtocolBridge().getProtocolValues().logger.log(host + ": " + string);
} }
public void info(String info) { public void info(String info) {
client.getProtocolBridge().getLogger().info(host + ": " + info); client.getProtocolBridge().getProtocolValues().logger.info(host + ": " + info);
} }
public void warn(String warn) { public void warn(String warn) {
client.getProtocolBridge().getLogger().warn(host + ": " + warn); client.getProtocolBridge().getProtocolValues().logger.warn(host + ": " + warn);
} }
public void error(String error) { public void error(String error) {
client.getProtocolBridge().getLogger().error(host + ": " + error); client.getProtocolBridge().getProtocolValues().logger.error(host + ": " + error);
} }
public void exception(String infoLine, Exception exception) { public void exception(String infoLine, Exception exception) {
client.getProtocolBridge().getLogger().exception(host + ": " + infoLine, exception); client.getProtocolBridge().getProtocolValues().logger.exception(host + ": " + infoLine, exception);
} }
public void debug(String debug) { public void debug(String debug) {
client.getProtocolBridge().getLogger().debug(host + ": " + debug); client.getProtocolBridge().getProtocolValues().logger.debug(host + ": " + debug);
} }
} }

View File

@@ -48,8 +48,8 @@ public record InsEndpoint(String host, int port) {
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (!(o instanceof InsEndpoint other)) return false; if (!(o instanceof InsEndpoint(String host1, int port1))) return false;
return host.equalsIgnoreCase(other.host) && port == other.port; return host.equalsIgnoreCase(host1) && port == port1;
} }
@Override @Override

View File

@@ -26,7 +26,16 @@ import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource; import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult; import javax.xml.transform.stream.StreamResult;
import java.awt.*; import java.awt.*;
import java.io.File;
import java.io.IOException;
import java.io.StringWriter; import java.io.StringWriter;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.time.Instant;
import java.util.Base64;
import java.util.Locale;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
@@ -64,8 +73,14 @@ public final class BrowserTab extends OACPanel {
* @param onLocationChange callback invoked on URL changes * @param onLocationChange callback invoked on URL changes
* @param luaEnabled whether Lua is enabled for this tab * @param luaEnabled whether Lua is enabled for this tab
* @param luaPolicy execution policy for Lua * @param luaPolicy execution policy for Lua
* @param protocolClient protocol client used for OAC network requests
*/ */
public BrowserTab(String key, String initialUrl, Consumer<String> onLocationChange, boolean luaEnabled, LuaExecutionPolicy luaPolicy, ClientImpl protocolClient) { public BrowserTab(String key,
String initialUrl,
Consumer<String> onLocationChange,
boolean luaEnabled,
LuaExecutionPolicy luaPolicy,
ClientImpl protocolClient) {
super(); super();
this.key = Objects.requireNonNull(key, "key"); this.key = Objects.requireNonNull(key, "key");
this.onLocationChanged = Objects.requireNonNull(onLocationChange, "onLocationChange"); this.onLocationChanged = Objects.requireNonNull(onLocationChange, "onLocationChange");
@@ -89,6 +104,108 @@ public final class BrowserTab extends OACPanel {
return sw.toString(); return sw.toString();
} }
private static String normalizeMime(String contentType) {
String ct = (contentType == null || contentType.isBlank()) ? "application/octet-stream" : contentType.trim();
int semi = ct.indexOf(';');
String base = (semi >= 0 ? ct.substring(0, semi) : ct).trim();
return base.isEmpty() ? "application/octet-stream" : base;
}
private static boolean isHtml(String contentType) {
String ct = normalizeMime(contentType).toLowerCase(Locale.ROOT);
return ct.equals("text/html") || ct.equals("application/xhtml+xml");
}
private static boolean isText(String contentType) {
String ct = normalizeMime(contentType).toLowerCase(Locale.ROOT);
return ct.startsWith("text/") || ct.equals("application/json") || ct.equals("application/xml") || ct.endsWith("+json") || ct.endsWith("+xml");
}
private static boolean isImage(String contentType) {
String ct = normalizeMime(contentType).toLowerCase(Locale.ROOT);
return ct.startsWith("image/");
}
private static boolean isPdf(String contentType) {
String ct = normalizeMime(contentType).toLowerCase(Locale.ROOT);
return ct.equals("application/pdf");
}
private static Charset charsetFromContentType(String contentType, Charset def) {
if (contentType == null) return def;
String[] parts = contentType.split(";");
for (String p : parts) {
String s = p.trim();
if (s.toLowerCase(Locale.ROOT).startsWith("charset=")) {
String name = s.substring("charset=".length()).trim();
try {
return Charset.forName(name);
} catch (Exception ignored) {
return def;
}
}
}
return def;
}
private static String extractFilenameFromContentDisposition(Map<String, String> headers) {
if (headers == null || headers.isEmpty()) return null;
String cd = null;
for (Map.Entry<String, String> e : headers.entrySet()) {
if (e.getKey() != null && e.getKey().equalsIgnoreCase("content-disposition")) {
cd = e.getValue();
break;
}
}
if (cd == null || cd.isBlank()) return null;
String lower = cd.toLowerCase(Locale.ROOT);
int fn = lower.indexOf("filename=");
if (fn < 0) return null;
String v = cd.substring(fn + "filename=".length()).trim();
if (v.startsWith("\"")) {
int end = v.indexOf('"', 1);
if (end > 1) return v.substring(1, end);
return null;
}
int semi = v.indexOf(';');
if (semi >= 0) v = v.substring(0, semi).trim();
return v.isBlank() ? null : v;
}
private static String sanitizeFilename(String name) {
String s = name.replace('\\', '_').replace('/', '_');
s = s.replace("..", "_");
s = s.replace(':', '_').replace('*', '_').replace('?', '_').replace('"', '_')
.replace('<', '_').replace('>', '_').replace('|', '_');
return s.isBlank() ? "download.bin" : s;
}
private static String extensionFromContentType(String contentType) {
String ct = normalizeMime(contentType).toLowerCase(Locale.ROOT);
if (ct.equals("application/pdf")) return ".pdf";
if (ct.equals("application/zip")) return ".zip";
if (ct.equals("application/x-7z-compressed")) return ".7z";
if (ct.equals("application/x-rar-compressed")) return ".rar";
if (ct.equals("application/gzip")) return ".gz";
if (ct.equals("application/json")) return ".json";
if (ct.equals("application/xml") || ct.endsWith("+xml")) return ".xml";
if (ct.startsWith("image/")) {
int slash = ct.indexOf('/');
if (slash > 0 && slash < ct.length() - 1) {
String ext = ct.substring(slash + 1).trim();
if (!ext.isEmpty()) return "." + ext;
}
}
if (ct.startsWith("text/")) return ".txt";
return ".bin";
}
/** /**
* Returns the stable tab key. * Returns the stable tab key.
* *
@@ -98,6 +215,11 @@ public final class BrowserTab extends OACPanel {
return key; return key;
} }
/**
* Returns the protocol client used by this tab.
*
* @return protocol client
*/
public ClientImpl getProtocolClient() { public ClientImpl getProtocolClient() {
return protocolClient; return protocolClient;
} }
@@ -214,6 +336,8 @@ public final class BrowserTab extends OACPanel {
return getEngineLocation(); return getEngineLocation();
} }
// -------------------- Stream render/save helpers --------------------
/** /**
* Returns current engine location. * Returns current engine location.
* *
@@ -270,6 +394,78 @@ public final class BrowserTab extends OACPanel {
} }
} }
/**
* Receives a fully assembled stream payload from the protocol layer and renders or saves it.
*
* <p>Rules:
* <ul>
* <li>Renderable: HTML/text/images/pdf are rendered directly (no wrapper pages for non-html).</li>
* <li>Not renderable: opens "Save As" dialog.</li>
* <li>If user cancels "Save As": content is shown raw in the browser via data: URL.</li>
* </ul>
*
* @param requestId request correlation id
* @param tabId protocol tab id
* @param pageId protocol page id
* @param frameId protocol frame id
* @param statusCode http-like status code
* @param contentType mime type
* @param headers response headers
* @param content full payload bytes
*/
public void handleStreamFinished(long requestId,
long tabId,
long pageId,
long frameId,
int statusCode,
String contentType,
Map<String, String> headers,
byte[] content) {
String ct = (contentType == null || contentType.isBlank())
? "application/octet-stream"
: contentType.trim();
byte[] data = (content == null) ? new byte[0] : content;
// ---- Renderable types -> render without extra wrapper pages ----
if (isHtml(ct)) {
Charset cs = charsetFromContentType(ct, StandardCharsets.UTF_8);
String html = new String(data, cs);
Platform.runLater(() -> {
WebEngine e = engine;
if (e != null) e.loadContent(html, "text/html");
});
return;
}
if (isText(ct)) {
Charset cs = charsetFromContentType(ct, StandardCharsets.UTF_8);
String text = new String(data, cs);
Platform.runLater(() -> {
WebEngine e = engine;
if (e != null) e.loadContent(text, "text/plain");
});
return;
}
if (isImage(ct) || isPdf(ct)) {
renderRawDataUrl(ct, data);
return;
}
// ---- Not renderable -> Save As; if cancelled -> raw data: URL ----
String suggested = extractFilenameFromContentDisposition(headers);
if (suggested == null || suggested.isBlank()) {
String ext = extensionFromContentType(ct);
suggested = "download_" + requestId + "_" + Instant.now().toEpochMilli() + ext;
} else {
suggested = sanitizeFilename(suggested);
}
showSaveAsDialogAndWriteBytes(suggested, ct, data);
}
/** /**
* Releases resources. * Releases resources.
*/ */
@@ -377,4 +573,75 @@ public final class BrowserTab extends OACPanel {
// Best-effort shutdown. // Best-effort shutdown.
} }
} }
private void showSaveAsDialogAndWriteBytes(String suggestedFilename, String contentType, byte[] data) {
SwingUtilities.invokeLater(() -> {
Window parent = SwingUtilities.getWindowAncestor(this);
JFileChooser chooser = new JFileChooser();
chooser.setDialogTitle("Save As");
chooser.setSelectedFile(new File(suggestedFilename));
int result = chooser.showSaveDialog(parent);
if (result != JFileChooser.APPROVE_OPTION) {
// Cancel -> show raw in browser
renderRawDataUrl(contentType, data);
return;
}
File file = chooser.getSelectedFile();
if (file == null) {
renderRawDataUrl(contentType, data);
return;
}
if (file.isDirectory()) {
JOptionPane.showMessageDialog(parent, "Please choose a file, not a directory.", "Save As", JOptionPane.WARNING_MESSAGE);
renderRawDataUrl(contentType, data);
return;
}
if (file.exists()) {
int overwrite = JOptionPane.showConfirmDialog(
parent,
"File already exists. Overwrite?\n" + file.getAbsolutePath(),
"Confirm Overwrite",
JOptionPane.YES_NO_OPTION,
JOptionPane.WARNING_MESSAGE
);
if (overwrite != JOptionPane.YES_OPTION) {
renderRawDataUrl(contentType, data);
return;
}
}
try {
Files.write(file.toPath(), data);
} catch (IOException ex) {
JOptionPane.showMessageDialog(
parent,
"Failed to save file:\n" + ex.getMessage(),
"Save As",
JOptionPane.ERROR_MESSAGE
);
// On failure, still show raw so user can at least see bytes
renderRawDataUrl(contentType, data);
}
});
}
private void renderRawDataUrl(String contentType, byte[] data) {
String mime = normalizeMime(contentType);
String b64 = Base64.getEncoder().encodeToString(data == null ? new byte[0] : data);
String dataUrl = "data:" + mime + ";base64," + b64;
Platform.runLater(() -> {
WebEngine e = engine;
if (e != null) e.load(dataUrl);
});
}
public void bindProtocolClient() {
protocolClient.getLibImpl().bindTab(this);
}
} }

View File

@@ -3,13 +3,13 @@ package org.openautonomousconnection.webclient.ui;
import dev.unlegitdqrk.unlegitlibrary.addon.AddonLoader; import dev.unlegitdqrk.unlegitlibrary.addon.AddonLoader;
import dev.unlegitdqrk.unlegitlibrary.event.EventManager; import dev.unlegitdqrk.unlegitlibrary.event.EventManager;
import dev.unlegitdqrk.unlegitlibrary.network.system.packets.PacketHandler; import dev.unlegitdqrk.unlegitlibrary.network.system.packets.PacketHandler;
import dev.unlegitdqrk.unlegitlibrary.utils.Logger;
import org.openautonomousconnection.oacswing.component.*; import org.openautonomousconnection.oacswing.component.*;
import org.openautonomousconnection.oacswing.component.design.DesignManager; import org.openautonomousconnection.oacswing.component.design.DesignManager;
import org.openautonomousconnection.protocol.ProtocolBridge; import org.openautonomousconnection.protocol.ProtocolBridge;
import org.openautonomousconnection.protocol.ProtocolValues; import org.openautonomousconnection.protocol.ProtocolValues;
import org.openautonomousconnection.protocol.versions.ProtocolVersion; import org.openautonomousconnection.protocol.versions.ProtocolVersion;
import org.openautonomousconnection.webclient.ClientImpl; import org.openautonomousconnection.webclient.ClientImpl;
import org.openautonomousconnection.webclient.Main;
import org.openautonomousconnection.webclient.settings.AppSettings; import org.openautonomousconnection.webclient.settings.AppSettings;
import org.openautonomousconnection.webclient.settings.HistoryStore; import org.openautonomousconnection.webclient.settings.HistoryStore;
import org.openautonomousconnection.webclient.settings.InsEndpoint; import org.openautonomousconnection.webclient.settings.InsEndpoint;
@@ -231,6 +231,8 @@ public class BrowserUI extends OACFrame {
readyTab.loadUrl(normalized); readyTab.loadUrl(normalized);
})); }));
connectTabClient(key, client);
BrowserTab tab = new BrowserTab( BrowserTab tab = new BrowserTab(
key, key,
normalized, normalized,
@@ -247,6 +249,9 @@ public class BrowserUI extends OACFrame {
client client
); );
// Bind stream callbacks to this tab (v1.0.1 stream assembly -> tab render).
tab.bindProtocolClient();
tabRef.set(tab); tabRef.set(tab);
tab.setOpenInNewTabCallback(() -> openNewTab( tab.setOpenInNewTabCallback(() -> openNewTab(
tab.getEngineLocation() == null ? settings.getStartPageUrl() : tab.getEngineLocation() tab.getEngineLocation() == null ? settings.getStartPageUrl() : tab.getEngineLocation()
@@ -272,41 +277,37 @@ public class BrowserUI extends OACFrame {
cardLayout.show(pageHost, key); cardLayout.show(pageHost, key);
browser.addressField().setText(normalized); browser.addressField().setText(normalized);
connectTabClient(key, client);
} }
private void connectTabClient(String key, ClientImpl client) { private void connectTabClient(String key, ClientImpl client) {
try { try {
File logsFolder = new File("logs");
if (!logsFolder.exists()) logsFolder.mkdir();
ProtocolValues values = new ProtocolValues(); ProtocolValues values = new ProtocolValues();
values.packetHandler = new PacketHandler(); values.packetHandler = new PacketHandler();
values.eventManager = new EventManager(); values.eventManager = new EventManager();
values.ssl = settings.isSslEnabled(); values.ssl = settings.isSslEnabled();
AddonLoader addonLoader = new AddonLoader(values.eventManager, values.logger = Main.getLogger();
new Logger(new File(logsFolder, "addons"), false, true)); values.addonLoader = Main.getAddonLoader();
values.protocolVersion = ProtocolVersion.PV_1_0_1_BETA;
AddonLoader addonLoader = values.addonLoader;
ProtocolBridge bridge = new ProtocolBridge( ProtocolBridge bridge = new ProtocolBridge(
client, client,
values, client.getLibImpl(),
ProtocolVersion.PV_1_0_0_BETA, values
new File(logsFolder, "client")
); );
protocolByKey.put(key, bridge); protocolByKey.put(key, bridge);
addonLoaderByKey.put(key, addonLoader); addonLoaderByKey.put(key, addonLoader);
File addonsFolder = new File("addons");
if (!addonsFolder.exists()) addonsFolder.mkdir();
addonLoader.loadAddonsFromDirectory(addonsFolder);
client.buildINSConnection(); client.buildINSConnection();
bridge.getProtocolValues().eventManager.registerListener(client); bridge.getProtocolValues().eventManager.registerListener(client);
InsEndpoint ep = Objects.requireNonNull(settings.getSelectedIns(), "selectedIns"); InsEndpoint ep = Objects.requireNonNull(settings.getSelectedIns(), "selectedIns");
client.getClientINSConnection().connect(ep.host(), ep.port()); client.getClientINSConnection().connect(ep.host(), ep.port());
File addonsFolder = new File("addons");
if (!addonsFolder.exists()) addonsFolder.mkdir();
addonLoader.loadAddonsFromDirectory(addonsFolder);
} catch (Exception e) { } catch (Exception e) {
OACOptionPane.showMessageDialog( OACOptionPane.showMessageDialog(
this, this,
@@ -351,12 +352,6 @@ public class BrowserUI extends OACFrame {
} }
} }
private void closeCurrentTab() {
String key = getSelectedTabKey();
if (key == null) return;
closeTabByKey(key);
}
private void closeTabByKey(String key) { private void closeTabByKey(String key) {
int idx = findTabIndexByKey(key); int idx = findTabIndexByKey(key);
if (idx < 0) return; if (idx < 0) return;
@@ -417,12 +412,9 @@ public class BrowserUI extends OACFrame {
if (idx < 0) return; if (idx < 0) return;
if (plusTabSupport.isPlusTab(idx)) { if (plusTabSupport.isPlusTab(idx)) {
if (suppressPlusAutoOpen) { if (suppressPlusAutoOpen) return;
return; if (tabsByKey.isEmpty()) return;
}
if (tabsByKey.isEmpty()) {
return;
}
handlingTabSwitch = true; handlingTabSwitch = true;
try { try {
int fallback = lastSelectedRealTab >= 0 int fallback = lastSelectedRealTab >= 0
@@ -452,12 +444,22 @@ public class BrowserUI extends OACFrame {
} }
} }
/**
* Returns the currently active tab (excluding the plus-tab).
*
* @return current tab or null
*/
public BrowserTab getCurrentTab() { public BrowserTab getCurrentTab() {
String key = getSelectedTabKey(); String key = getSelectedTabKey();
if (key == null) return null; if (key == null) return null;
return tabsByKey.get(key); return tabsByKey.get(key);
} }
/**
* Returns the addon loader associated with the current tab.
*
* @return addon loader or null
*/
public AddonLoader getCurrentAddonLoader() { public AddonLoader getCurrentAddonLoader() {
String key = getSelectedTabKey(); String key = getSelectedTabKey();
if (key == null) return null; if (key == null) return null;