diff --git a/src/main/java/org/openautonomousconnection/webclient/ClientImpl.java b/src/main/java/org/openautonomousconnection/webclient/ClientImpl.java index 036c33b..099e91f 100644 --- a/src/main/java/org/openautonomousconnection/webclient/ClientImpl.java +++ b/src/main/java/org/openautonomousconnection/webclient/ClientImpl.java @@ -11,7 +11,7 @@ import org.openautonomousconnection.protocol.urlhandler.v1_0_1.beta.web.WebReque import org.openautonomousconnection.protocol.versions.v1_0_1.beta.WebPacketHeader; import org.openautonomousconnection.webclient.ui.BrowserTab; -import java.awt.Component; +import java.awt.*; import java.io.ByteArrayOutputStream; import java.net.URL; import java.util.Map; @@ -85,6 +85,70 @@ public final class ClientImpl extends ProtocolClient { } } + 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 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 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(); @@ -176,68 +240,4 @@ public final class ClientImpl extends ProtocolClient { } } } - - 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 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 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; - } - } } \ No newline at end of file diff --git a/src/main/java/org/openautonomousconnection/webclient/Main.java b/src/main/java/org/openautonomousconnection/webclient/Main.java index 70602a1..829db42 100644 --- a/src/main/java/org/openautonomousconnection/webclient/Main.java +++ b/src/main/java/org/openautonomousconnection/webclient/Main.java @@ -53,7 +53,7 @@ public class Main { CookieHandler.setDefault(cm); } - public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException { + 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); diff --git a/src/main/java/org/openautonomousconnection/webclient/settings/InsEndpoint.java b/src/main/java/org/openautonomousconnection/webclient/settings/InsEndpoint.java index 59caaac..c8ffc4d 100644 --- a/src/main/java/org/openautonomousconnection/webclient/settings/InsEndpoint.java +++ b/src/main/java/org/openautonomousconnection/webclient/settings/InsEndpoint.java @@ -48,8 +48,8 @@ public record InsEndpoint(String host, int port) { @Override public boolean equals(Object o) { - if (!(o instanceof InsEndpoint other)) return false; - return host.equalsIgnoreCase(other.host) && port == other.port; + if (!(o instanceof InsEndpoint(String host1, int port1))) return false; + return host.equalsIgnoreCase(host1) && port == port1; } @Override diff --git a/src/main/java/org/openautonomousconnection/webclient/ui/BrowserTab.java b/src/main/java/org/openautonomousconnection/webclient/ui/BrowserTab.java index 60af345..4d80c28 100644 --- a/src/main/java/org/openautonomousconnection/webclient/ui/BrowserTab.java +++ b/src/main/java/org/openautonomousconnection/webclient/ui/BrowserTab.java @@ -104,6 +104,108 @@ public final class BrowserTab extends OACPanel { 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 headers) { + if (headers == null || headers.isEmpty()) return null; + + String cd = null; + for (Map.Entry 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. * @@ -234,6 +336,8 @@ public final class BrowserTab extends OACPanel { return getEngineLocation(); } + // -------------------- Stream render/save helpers -------------------- + /** * Returns current engine location. * @@ -470,8 +574,6 @@ public final class BrowserTab extends OACPanel { } } - // -------------------- Stream render/save helpers -------------------- - private void showSaveAsDialogAndWriteBytes(String suggestedFilename, String contentType, byte[] data) { SwingUtilities.invokeLater(() -> { Window parent = SwingUtilities.getWindowAncestor(this); @@ -539,108 +641,6 @@ public final class BrowserTab extends OACPanel { }); } - 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 headers) { - if (headers == null || headers.isEmpty()) return null; - - String cd = null; - for (Map.Entry 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); }