Compare commits
8 Commits
dev
...
1.0.1-BETA
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba44cd6f7d | ||
|
|
35cd895510 | ||
|
|
d3662a0773 | ||
|
|
ff2a76e7da | ||
|
|
4c1cf09f34 | ||
|
|
b3457ee133 | ||
|
|
b6f7110d16 | ||
|
|
4376fe6daa |
3
LICENSE
3
LICENSE
@@ -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
|
||||||
|
|||||||
15
README.MD
15
README.MD
@@ -2,4 +2,17 @@
|
|||||||
|
|
||||||
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.
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
<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.3</version>
|
||||||
<description>The default WebClient</description>
|
<description>The default WebClient</description>
|
||||||
<url>https://open-autonomous-connection.org/</url>
|
<url>https://open-autonomous-connection.org/</url>
|
||||||
<issueManagement>
|
<issueManagement>
|
||||||
@@ -67,6 +67,13 @@
|
|||||||
<artifactId>maven-compiler-plugin</artifactId>
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
<version>3.13.0</version>
|
<version>3.13.0</version>
|
||||||
<configuration>
|
<configuration>
|
||||||
|
<annotationProcessorPaths>
|
||||||
|
<path>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<version>1.18.42</version>
|
||||||
|
</path>
|
||||||
|
</annotationProcessorPaths>
|
||||||
<compilerArgs>
|
<compilerArgs>
|
||||||
<arg>--add-exports</arg>
|
<arg>--add-exports</arg>
|
||||||
<arg>java.base/sun.security.x509=ALL-UNNAMED</arg>
|
<arg>java.base/sun.security.x509=ALL-UNNAMED</arg>
|
||||||
@@ -109,13 +116,13 @@
|
|||||||
<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>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
<properties>
|
<properties>
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
<maven.compiler.target>23</maven.compiler.target>
|
<maven.compiler.target>25</maven.compiler.target>
|
||||||
<maven.compiler.source>23</maven.compiler.source>
|
<maven.compiler.source>25</maven.compiler.source>
|
||||||
</properties>
|
</properties>
|
||||||
</project>
|
</project>
|
||||||
|
|||||||
24
pom.xml
24
pom.xml
@@ -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.4</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>0.0.0-STABLE.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.6</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>
|
||||||
@@ -135,6 +135,14 @@
|
|||||||
<artifactId>maven-compiler-plugin</artifactId>
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
<version>3.13.0</version>
|
<version>3.13.0</version>
|
||||||
<configuration>
|
<configuration>
|
||||||
|
<annotationProcessorPaths>
|
||||||
|
<path>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<version>1.18.42</version>
|
||||||
|
</path>
|
||||||
|
</annotationProcessorPaths>
|
||||||
|
|
||||||
<compilerArgs>
|
<compilerArgs>
|
||||||
<arg>--add-exports</arg>
|
<arg>--add-exports</arg>
|
||||||
<arg>java.base/sun.security.x509=ALL-UNNAMED</arg>
|
<arg>java.base/sun.security.x509=ALL-UNNAMED</arg>
|
||||||
|
|||||||
@@ -1,93 +1,172 @@
|
|||||||
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.side.client.events.ConnectedToProtocolServerEvent;
|
||||||
|
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 +174,75 @@ 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) {
|
||||||
|
|
||||||
|
System.out.println("stream start");
|
||||||
|
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[] data) {
|
||||||
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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();
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,11 @@ import javafx.scene.web.WebView;
|
|||||||
import org.openautonomousconnection.luascript.fx.FxDomHost;
|
import org.openautonomousconnection.luascript.fx.FxDomHost;
|
||||||
import org.openautonomousconnection.luascript.fx.FxThreadBridge;
|
import org.openautonomousconnection.luascript.fx.FxThreadBridge;
|
||||||
import org.openautonomousconnection.luascript.hosts.UiHost;
|
import org.openautonomousconnection.luascript.hosts.UiHost;
|
||||||
|
import org.openautonomousconnection.oacswing.component.OACOptionPane;
|
||||||
|
import org.openautonomousconnection.webclient.Main;
|
||||||
import org.w3c.dom.Element;
|
import org.w3c.dom.Element;
|
||||||
|
|
||||||
|
import javax.swing.*;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -66,23 +69,17 @@ public final class UiHostImpl implements UiHost {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void alert(String message) {
|
public void alert(String message) {
|
||||||
// No JS: use simple JavaFX dialog-less fallback (log-style). You can replace with real Dialogs later.
|
OACOptionPane.showMessageDialog(Main.getUi(), message, "Alert", JOptionPane.INFORMATION_MESSAGE);
|
||||||
// Keeping it deterministic and non-blocking for now.
|
|
||||||
System.out.println("[ui.alert] " + (message == null ? "" : message));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean confirm(String message) {
|
public boolean confirm(String message) {
|
||||||
// No JS: deterministic default (false). Replace with JavaFX dialogs if you want UI interaction.
|
return OACOptionPane.showConfirmDialog(Main.getUi(), message, "Alert", OACOptionPane.YES_NO_OPTION) == OACOptionPane.YES_OPTION;
|
||||||
System.out.println("[ui.confirm] " + (message == null ? "" : message));
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String prompt(String message, String defaultValue) {
|
public String prompt(String message, String defaultValue) {
|
||||||
// No JS: deterministic default.
|
return (String) OACOptionPane.showInputDialog(Main.getUi(), message, "Prompt", JOptionPane.QUESTION_MESSAGE, null, null, defaultValue);
|
||||||
System.out.println("[ui.prompt] " + (message == null ? "" : message));
|
|
||||||
return defaultValue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package org.openautonomousconnection.webclient.settings;
|
|||||||
|
|
||||||
import org.openautonomousconnection.luascript.security.LuaExecutionPolicy;
|
import org.openautonomousconnection.luascript.security.LuaExecutionPolicy;
|
||||||
|
|
||||||
import java.time.Duration;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -187,6 +186,6 @@ public final class AppSettings {
|
|||||||
* Resets the Lua policy back to ui default.
|
* Resets the Lua policy back to ui default.
|
||||||
*/
|
*/
|
||||||
public void resetLuaPolicyToUiDefault() {
|
public void resetLuaPolicyToUiDefault() {
|
||||||
this.luaPolicy = new LuaExecutionPolicy(Duration.ofMillis(50L), 200_000L, 5_000);
|
this.luaPolicy = LuaExecutionPolicy.uiDefault();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import javafx.concurrent.Worker;
|
|||||||
import javafx.scene.web.WebEngine;
|
import javafx.scene.web.WebEngine;
|
||||||
import javafx.scene.web.WebView;
|
import javafx.scene.web.WebView;
|
||||||
import org.luaj.vm2.Globals;
|
import org.luaj.vm2.Globals;
|
||||||
|
import org.luaj.vm2.LuaError;
|
||||||
import org.openautonomousconnection.luascript.fx.FxDomHost;
|
import org.openautonomousconnection.luascript.fx.FxDomHost;
|
||||||
import org.openautonomousconnection.luascript.fx.FxEventHost;
|
import org.openautonomousconnection.luascript.fx.FxEventHost;
|
||||||
import org.openautonomousconnection.luascript.fx.FxWebViewResourceHost;
|
import org.openautonomousconnection.luascript.fx.FxWebViewResourceHost;
|
||||||
@@ -11,6 +12,8 @@ import org.openautonomousconnection.luascript.hosts.HostServices;
|
|||||||
import org.openautonomousconnection.luascript.runtime.LuaRuntime;
|
import org.openautonomousconnection.luascript.runtime.LuaRuntime;
|
||||||
import org.openautonomousconnection.luascript.security.LuaExecutionPolicy;
|
import org.openautonomousconnection.luascript.security.LuaExecutionPolicy;
|
||||||
import org.openautonomousconnection.luascript.utils.LuaGlobalsFactory;
|
import org.openautonomousconnection.luascript.utils.LuaGlobalsFactory;
|
||||||
|
import org.openautonomousconnection.oacswing.component.OACOptionPane;
|
||||||
|
import org.openautonomousconnection.webclient.Main;
|
||||||
import org.openautonomousconnection.webclient.lua.WebLogger;
|
import org.openautonomousconnection.webclient.lua.WebLogger;
|
||||||
import org.openautonomousconnection.webclient.lua.hosts.ConsoleHostImpl;
|
import org.openautonomousconnection.webclient.lua.hosts.ConsoleHostImpl;
|
||||||
import org.openautonomousconnection.webclient.lua.hosts.UiHostImpl;
|
import org.openautonomousconnection.webclient.lua.hosts.UiHostImpl;
|
||||||
@@ -95,17 +98,19 @@ public final class FxEngine implements AutoCloseable {
|
|||||||
ConsoleHostImpl console = new ConsoleHostImpl(logger);
|
ConsoleHostImpl console = new ConsoleHostImpl(logger);
|
||||||
UiHostImpl uiHost = new UiHostImpl(engine, webView, dom);
|
UiHostImpl uiHost = new UiHostImpl(engine, webView, dom);
|
||||||
FxWebViewResourceHost resourceHost = new FxWebViewResourceHost(engine);
|
FxWebViewResourceHost resourceHost = new FxWebViewResourceHost(engine);
|
||||||
|
FxEventHost eventHost = new FxEventHost(dom);
|
||||||
LuaRuntime rt = new LuaRuntime(globals, new HostServices.Default(uiHost, dom, null, resourceHost, console), policy);
|
|
||||||
|
|
||||||
FxEventHost eventHost = new FxEventHost(dom, rt.eventRouter());
|
|
||||||
HostServices services = new HostServices.Default(uiHost, dom, eventHost, resourceHost, console);
|
HostServices services = new HostServices.Default(uiHost, dom, eventHost, resourceHost, console);
|
||||||
|
LuaRuntime rt = new LuaRuntime(globals, services, policy);
|
||||||
rt.close();
|
eventHost.setRouter(rt.eventRouter());
|
||||||
rt = new LuaRuntime(globals, services, policy);
|
|
||||||
|
|
||||||
rt.installStdTables(true);
|
rt.installStdTables(true);
|
||||||
rt.bootstrapFromDom();
|
|
||||||
|
try {
|
||||||
|
rt.bootstrapFromDom();
|
||||||
|
} catch (LuaError error) {
|
||||||
|
Main.getLogger().exception("Failed to start script", error);
|
||||||
|
OACOptionPane.showMessageDialog(Main.getUi(), error.getMessage(), "Script", OACOptionPane.ERROR_MESSAGE);
|
||||||
|
}
|
||||||
|
|
||||||
this.runtime = rt;
|
this.runtime = rt;
|
||||||
}
|
}
|
||||||
@@ -135,4 +140,4 @@ public final class FxEngine implements AutoCloseable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -87,9 +87,10 @@ public final class SettingsManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Lua policy
|
// Lua policy
|
||||||
long timeoutMs = parseLong(p.getProperty("lua.timeoutMs"), 50L);
|
LuaExecutionPolicy defaultPolicy = LuaExecutionPolicy.uiDefault();
|
||||||
long instr = parseLong(p.getProperty("lua.instructionLimit"), 200_000L);
|
long timeoutMs = parseLong(p.getProperty("lua.timeoutMs"), defaultPolicy.timeout().toMillis());
|
||||||
int hook = parseInt(p.getProperty("lua.hookStep"), 5_000);
|
long instr = parseLong(p.getProperty("lua.instructionLimit"), defaultPolicy.instructionLimit());
|
||||||
|
int hook = parseInt(p.getProperty("lua.hookStep"), defaultPolicy.hookStep());
|
||||||
try {
|
try {
|
||||||
s.setLuaPolicy(new LuaExecutionPolicy(Duration.ofMillis(timeoutMs), instr, hook));
|
s.setLuaPolicy(new LuaExecutionPolicy(Duration.ofMillis(timeoutMs), instr, hook));
|
||||||
} catch (Exception ignored) {
|
} catch (Exception ignored) {
|
||||||
@@ -187,4 +188,4 @@ public final class SettingsManager {
|
|||||||
return def;
|
return def;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
@@ -470,4 +472,4 @@ public class BrowserUI extends OACFrame {
|
|||||||
if (plusTabSupport.isPlusTab(idx)) return null;
|
if (plusTabSupport.isPlusTab(idx)) return null;
|
||||||
return getTitleBar().getTabs().getTitleAt(idx);
|
return getTitleBar().getTabs().getTitleAt(idx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user