Updated to latest Protocol Version
This commit is contained in:
@@ -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.protocol.versions.v1_0_1.beta.WebPacketHeader;
|
||||||
import org.openautonomousconnection.webclient.ui.BrowserTab;
|
import org.openautonomousconnection.webclient.ui.BrowserTab;
|
||||||
|
|
||||||
import java.awt.Component;
|
import java.awt.*;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.util.Map;
|
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<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 {
|
public final class LibImpl extends LibClientImpl_v1_0_1_B {
|
||||||
|
|
||||||
private final WebRequestContextProvider provider = new WebRequestContextProvider.Default();
|
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<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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -53,7 +53,7 @@ public class Main {
|
|||||||
CookieHandler.setDefault(cm);
|
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();
|
eventManager = new EventManager();
|
||||||
logger = new Logger(new File("logs", "client"), false, true);
|
logger = new Logger(new File("logs", "client"), false, true);
|
||||||
addonLoader = new AddonLoader(eventManager, logger);
|
addonLoader = new AddonLoader(eventManager, logger);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -104,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.
|
||||||
*
|
*
|
||||||
@@ -234,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.
|
||||||
*
|
*
|
||||||
@@ -470,8 +574,6 @@ public final class BrowserTab extends OACPanel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------- Stream render/save helpers --------------------
|
|
||||||
|
|
||||||
private void showSaveAsDialogAndWriteBytes(String suggestedFilename, String contentType, byte[] data) {
|
private void showSaveAsDialogAndWriteBytes(String suggestedFilename, String contentType, byte[] data) {
|
||||||
SwingUtilities.invokeLater(() -> {
|
SwingUtilities.invokeLater(() -> {
|
||||||
Window parent = SwingUtilities.getWindowAncestor(this);
|
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<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() {
|
public void bindProtocolClient() {
|
||||||
protocolClient.getLibImpl().bindTab(this);
|
protocolClient.getLibImpl().bindTab(this);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user