Updated to latest Protocol Version

This commit is contained in:
UnlegitDqrk
2026-02-22 18:20:57 +01:00
parent 69be55cac0
commit 4376fe6daa
7 changed files with 517 additions and 63 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

@@ -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.

20
pom.xml
View File

@@ -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,7 +69,7 @@
<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>
@@ -78,13 +78,13 @@
</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.3</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>

View File

@@ -1,90 +1,98 @@
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 lombok.Getter;
import org.openautonomousconnection.infonamelib.OacWebUrlInstaller;
import org.openautonomousconnection.oacswing.component.OACOptionPane; import org.openautonomousconnection.oacswing.component.OACOptionPane;
import org.openautonomousconnection.protocol.side.client.ProtocolClient; import org.openautonomousconnection.protocol.side.client.ProtocolClient;
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.Component;
import java.io.ByteArrayOutputStream;
import java.net.URL;
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 ProtocolClient {
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 { 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().getLogger().exception("Failed to connect to server", exception);
@@ -95,5 +103,141 @@ 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
);
}
}
} }
}
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;
}
}
}

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,10 @@ public class Main {
CookieHandler.setDefault(cm); CookieHandler.setDefault(cm);
} }
public static void main(String[] args) throws IOException { public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException {
eventManager = new EventManager();
logger = new Logger(new File("logs", "client"), false, true);
addonLoader = new AddonLoader(eventManager, logger);
settings = SettingsManager.load(); settings = SettingsManager.load();
FxBootstrap.ensureInitialized(); FxBootstrap.ensureInitialized();

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");
@@ -98,6 +113,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;
} }
@@ -270,6 +290,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 +469,179 @@ public final class BrowserTab extends OACPanel {
// Best-effort shutdown. // Best-effort shutdown.
} }
} }
}
// -------------------- Stream render/save helpers --------------------
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);
});
}
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";
}
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;
@@ -247,6 +247,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()
@@ -285,14 +288,15 @@ public class BrowserUI extends OACFrame {
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, AddonLoader addonLoader = Main.getAddonLoader();
new Logger(new File(logsFolder, "addons"), false, true));
ProtocolBridge bridge = new ProtocolBridge( ProtocolBridge bridge = new ProtocolBridge(
client, client,
values, values,
ProtocolVersion.PV_1_0_0_BETA, ProtocolVersion.PV_1_0_1_BETA,
new File(logsFolder, "client") Main.getLogger(),
Main.getAddonLoader(),
client.getLibImpl()
); );
protocolByKey.put(key, bridge); protocolByKey.put(key, bridge);
@@ -351,12 +355,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 +415,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 +447,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 +475,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);
} }
} }