diff --git a/pom.xml b/pom.xml index 6b16ef3..feee98b 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ org.openautonomousconnection Protocol - 1.0.0-BETA.1.1 + 1.0.1-BETA.0.1 Open Autonomous Connection https://open-autonomous-connection.org/ @@ -84,12 +84,12 @@ dev.unlegitdqrk unlegitlibrary - 1.8.1 + 1.8.2 org.projectlombok lombok - 1.18.38 + 1.18.42 provided diff --git a/src/main/java/org/openautonomousconnection/protocol/ProtocolBridge.java b/src/main/java/org/openautonomousconnection/protocol/ProtocolBridge.java index 706296e..9f27636 100644 --- a/src/main/java/org/openautonomousconnection/protocol/ProtocolBridge.java +++ b/src/main/java/org/openautonomousconnection/protocol/ProtocolBridge.java @@ -1,10 +1,10 @@ package org.openautonomousconnection.protocol; +import dev.unlegitdqrk.unlegitlibrary.addon.AddonLoader; import dev.unlegitdqrk.unlegitlibrary.file.FileUtils; import dev.unlegitdqrk.unlegitlibrary.network.system.utils.ClientAuthMode; import dev.unlegitdqrk.unlegitlibrary.utils.Logger; import lombok.Getter; -import lombok.Setter; import org.openautonomousconnection.protocol.annotations.ProtocolInfo; import org.openautonomousconnection.protocol.listeners.ClientListener; import org.openautonomousconnection.protocol.listeners.CustomServerListener; @@ -14,18 +14,28 @@ import org.openautonomousconnection.protocol.packets.v1_0_0.beta.INSQueryPacket; import org.openautonomousconnection.protocol.packets.v1_0_0.beta.INSResponsePacket; import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.WebRequestPacket; import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.WebResponsePacket; -import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.stream.WebStreamChunkPacket; -import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.stream.WebStreamEndPacket; -import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.stream.WebStreamStartPacket; +import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.stream.WebStreamChunkPacket_v1_0_0_B; +import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.stream.WebStreamEndPacket_v1_0_0_B; +import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.stream.WebStreamStartPacket_v1_0_0_B; +import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.document.WebDocumentApplyRequestPacket; +import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.document.WebDocumentApplyResponsePacket; +import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.document.WebDocumentSnapshotEventPacket; +import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.navigate.WebNavigateAckPacket; +import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.navigate.WebNavigateRequestPacket; +import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.resource.WebResourceRequestPacket; +import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.resource.WebResourceResponsePacket; +import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.stream.WebStreamChunkPacket_v1_0_1_B; +import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.stream.WebStreamEndPacket_v1_0_1_B; +import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.stream.WebStreamStartPacket_v1_0_1_B; import org.openautonomousconnection.protocol.side.client.ProtocolClient; import org.openautonomousconnection.protocol.side.ins.ProtocolINSServer; import org.openautonomousconnection.protocol.side.server.ProtocolCustomServer; -import org.openautonomousconnection.protocol.side.web.ProtocolWebServer; import org.openautonomousconnection.protocol.versions.ProtocolVersion; +import org.openautonomousconnection.protocol.versions.v1_0_0.beta.ProtocolWebServer_1_0_0_B; import java.io.File; import java.io.IOException; -import java.net.Proxy; +import java.util.function.Supplier; /** * The main bridge class for the protocol connection. @@ -63,12 +73,8 @@ public final class ProtocolBridge { @Getter private ProtocolCustomServer protocolServer; - /** - * The proxy for client side - */ @Getter - @Setter - private Proxy proxy; + private AddonLoader addonLoader; /** * Initialize the ProtocolBridge instance for the client side @@ -76,24 +82,25 @@ public final class ProtocolBridge { * @param protocolServer The ProtocolCustomServer instance * @param protocolValues The ProtocolSettings instance * @param protocolVersion The ProtocolVersion instance - * @param logFolder The folder to store the log files + * @param logger The logger + * @param addonLoader The Addon loader to load custom extensions * @throws Exception if an error occurs while initializing the ProtocolBridge */ - public ProtocolBridge(ProtocolCustomServer protocolServer, ProtocolValues protocolValues, ProtocolVersion protocolVersion, File logFolder) throws Exception { + public ProtocolBridge(ProtocolCustomServer protocolServer, ProtocolValues protocolValues, ProtocolVersion protocolVersion, + Logger logger, AddonLoader addonLoader) throws Exception { // Assign the parameters to the class fields this.protocolServer = protocolServer; this.protocolValues = protocolValues; this.protocolVersion = protocolVersion; + this.logger = logger; + this.addonLoader = addonLoader; - if (protocolServer instanceof ProtocolINSServer) { + if (protocolServer instanceof ProtocolINSServer) protocolServer.attachBridge(this, null, false, ClientAuthMode.NONE); - } else + else protocolServer.attachBridge(this, protocolValues.keyPass, protocolValues.ssl, protocolValues.authMode); - // Initialize the logger and protocol version - initializeLogger(logFolder); initializeProtocolVersion(); - downloadLicenses(); // Register the appropriate listeners and packets @@ -107,23 +114,24 @@ public final class ProtocolBridge { * @param protocolClient The ProtocolClient instance * @param protocolValues The ProtocolSettings instance * @param protocolVersion The ProtocolVersion instance - * @param logFolder The folder to store the log files + * @param logger The logger + * @param addonLoader The Addon loader to load custom extensions * @throws Exception if an error occurs while initializing the ProtocolBridge */ @ProtocolInfo(protocolSide = ProtocolVersion.ProtocolSide.CLIENT) - public ProtocolBridge(ProtocolClient protocolClient, ProtocolValues protocolValues, ProtocolVersion protocolVersion, File logFolder) throws Exception { + public ProtocolBridge(ProtocolClient protocolClient, ProtocolValues protocolValues, ProtocolVersion protocolVersion, + Logger logger, AddonLoader addonLoader) throws Exception { // Assign the parameters to the class fields this.protocolClient = protocolClient; this.protocolValues = protocolValues; this.protocolVersion = protocolVersion; + this.logger = logger; + this.addonLoader = addonLoader; protocolClient.attachBridge(this); - + initializeProtocolVersion(); downloadLicenses(); - // Initialize the logger and protocol version - initializeLogger(logFolder); - initializeProtocolVersion(); // Register the appropriate listeners and packets registerListeners(); @@ -132,8 +140,10 @@ public final class ProtocolBridge { private void downloadLicenses() throws IOException { File licensesFolder = new File("licenses"); + if (!licensesFolder.exists() || !licensesFolder.isDirectory()) { if (licensesFolder.exists()) licensesFolder.delete(); + File output = new File("licenses.zip"); output.createNewFile(); FileUtils.downloadFile("https://open-autonomous-connection.org/assets/licenses.zip", output); @@ -143,27 +153,63 @@ public final class ProtocolBridge { } /** - * Register the appropriate packets based on the current protocol version + * Register the appropriate packets + * + *

Overview of all Packets + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
IDPacketProtocolVersion
8{@link org.openautonomousconnection.protocol.packets.v1_0_0.beta.AuthPacket AuthPacket}{@link org.openautonomousconnection.protocol.versions.ProtocolVersion#PV_1_0_0_BETA PV_1_0_0_BETA}
7{@link org.openautonomousconnection.protocol.packets.v1_0_0.beta.INSQueryPacket INSQueryPacket}{@link org.openautonomousconnection.protocol.versions.ProtocolVersion#PV_1_0_0_BETA PV_1_0_0_BETA}
6{@link org.openautonomousconnection.protocol.packets.v1_0_0.beta.INSResponsePacket INSResponsePacket}{@link org.openautonomousconnection.protocol.versions.ProtocolVersion#PV_1_0_0_BETA PV_1_0_0_BETA}
10{@link org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.WebRequestPacket WebRequestPacket}{@link org.openautonomousconnection.protocol.versions.ProtocolVersion#PV_1_0_0_BETA PV_1_0_0_BETA}
9{@link org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.WebResponsePacket WebResponsePacket}{@link org.openautonomousconnection.protocol.versions.ProtocolVersion#PV_1_0_0_BETA PV_1_0_0_BETA}
11{@link WebStreamStartPacket_v1_0_0_B v1_0_0.beta.web.stream.WebStreamStartPacket}{@link org.openautonomousconnection.protocol.versions.ProtocolVersion#PV_1_0_0_BETA PV_1_0_0_BETA}
13{@link WebStreamChunkPacket_v1_0_0_B v1_0_0.beta.web.stream.WebStreamChunkPacket}{@link org.openautonomousconnection.protocol.versions.ProtocolVersion#PV_1_0_0_BETA PV_1_0_0_BETA}
12{@link WebStreamEndPacket_v1_0_0_B v1_0_0.beta.web.stream.WebStreamEndPacket}{@link org.openautonomousconnection.protocol.versions.ProtocolVersion#PV_1_0_0_BETA PV_1_0_0_BETA}
1{@link org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.navigate.WebNavigateRequestPacket WebNavigateRequestPacket}{@link org.openautonomousconnection.protocol.versions.ProtocolVersion#PV_1_0_1_BETA PV_1_0_1_BETA}
2{@link org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.navigate.WebNavigateAckPacket WebNavigateAckPacket}{@link org.openautonomousconnection.protocol.versions.ProtocolVersion#PV_1_0_1_BETA PV_1_0_1_BETA}
3{@link org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.resource.WebResourceRequestPacket WebResourceRequestPacket}{@link org.openautonomousconnection.protocol.versions.ProtocolVersion#PV_1_0_1_BETA PV_1_0_1_BETA}
4{@link org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.resource.WebResourceResponsePacket WebResourceResponsePacket}{@link org.openautonomousconnection.protocol.versions.ProtocolVersion#PV_1_0_1_BETA PV_1_0_1_BETA}
14{@link WebStreamStartPacket_v1_0_1_B v1_0_1.beta.web.impl.stream.WebStreamStartPacket}{@link org.openautonomousconnection.protocol.versions.ProtocolVersion#PV_1_0_1_BETA PV_1_0_1_BETA}
15{@link WebStreamChunkPacket_v1_0_1_B v1_0_1.beta.web.impl.stream.WebStreamChunkPacket}{@link org.openautonomousconnection.protocol.versions.ProtocolVersion#PV_1_0_1_BETA PV_1_0_1_BETA}
16{@link WebStreamEndPacket_v1_0_1_B v1_0_1.beta.web.impl.stream.WebStreamEndPacket}{@link org.openautonomousconnection.protocol.versions.ProtocolVersion#PV_1_0_1_BETA PV_1_0_1_BETA}
17{@link org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.document.WebDocumentSnapshotEventPacket WebDocumentSnapshotEventPacket}{@link org.openautonomousconnection.protocol.versions.ProtocolVersion#PV_1_0_1_BETA PV_1_0_1_BETA}
5{@link org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.document.WebDocumentApplyResponsePacket WebDocumentApplyResponsePacket}{@link org.openautonomousconnection.protocol.versions.ProtocolVersion#PV_1_0_1_BETA PV_1_0_1_BETA}
18{@link org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.document.WebDocumentApplyRequestPacket WebDocumentApplyRequestPacket}{@link org.openautonomousconnection.protocol.versions.ProtocolVersion#PV_1_0_1_BETA PV_1_0_1_BETA}
+ * + * @see org.openautonomousconnection.protocol.versions.ProtocolVersion */ private void registerPackets() { // 1.0.0-BETA packets - if (isPacketSupported(new AuthPacket(this))) - protocolValues.packetHandler.registerPacket(() -> new AuthPacket(this)); - if (isPacketSupported(new INSQueryPacket())) protocolValues.packetHandler.registerPacket(INSQueryPacket::new); - if (isPacketSupported(new INSResponsePacket(this))) - protocolValues.packetHandler.registerPacket(() -> new INSResponsePacket(this)); - if (isPacketSupported(new WebRequestPacket())) - protocolValues.packetHandler.registerPacket(WebRequestPacket::new); - if (isPacketSupported(new WebResponsePacket())) - protocolValues.packetHandler.registerPacket(WebResponsePacket::new); - if (isPacketSupported(new WebStreamChunkPacket())) - protocolValues.packetHandler.registerPacket(WebStreamChunkPacket::new); - if (isPacketSupported(new WebStreamStartPacket())) - protocolValues.packetHandler.registerPacket(WebStreamStartPacket::new); - if (isPacketSupported(new WebStreamEndPacket())) - protocolValues.packetHandler.registerPacket(WebStreamEndPacket::new); + registerPacket(AuthPacket::new); + registerPacket(INSQueryPacket::new); + registerPacket(() -> new INSResponsePacket(this)); + registerPacket(WebRequestPacket::new); + registerPacket(WebResponsePacket::new); + registerPacket(WebStreamStartPacket_v1_0_0_B::new); + registerPacket(WebStreamChunkPacket_v1_0_0_B::new); + registerPacket(WebStreamEndPacket_v1_0_0_B::new); + + // 1.0.1-BETA Packets + registerPacket(WebDocumentApplyRequestPacket::new); + registerPacket(WebDocumentApplyResponsePacket::new); + registerPacket(WebDocumentSnapshotEventPacket::new); + registerPacket(WebNavigateRequestPacket::new); + registerPacket(WebNavigateAckPacket::new); + registerPacket(WebResourceRequestPacket::new); + registerPacket(WebResourceResponsePacket::new); + registerPacket(WebStreamStartPacket_v1_0_1_B::new); + registerPacket(WebStreamChunkPacket_v1_0_1_B::new); + registerPacket(WebStreamEndPacket_v1_0_1_B::new); } + private void registerPacket(Supplier factory) { + if (isPacketSupported(factory.get())) protocolValues.packetHandler.registerPacket(factory); + } /** * Register the appropriate listeners based on the current side @@ -184,27 +230,6 @@ public final class ProtocolBridge { } } - /** - * Initialize the logger instance - * - * @param logFolder The folder to store the log files - */ - private void initializeLogger(File logFolder) { - // Create a temporary logger instance to avoid final field issues - Logger tmpLogger = null; - - try { - // Initialize temporary logger - tmpLogger = new Logger(logFolder, false, true); - } catch (IOException | NoSuchFieldException | IllegalAccessException exception) { - exception.printStackTrace(); - System.exit(1); - } - - // Assign the temporary logger to the final field - this.logger = tmpLogger; - } - /** * Initialize the protocol version * Validate if the protocol version is valid for the current side @@ -244,6 +269,7 @@ public final class ProtocolBridge { */ public boolean isProtocolSupported(ProtocolVersion.Protocol protocol) { boolean yes = false; + for (ProtocolVersion compatibleVersion : protocolVersion.getCompatibleVersions()) { // Check if the compatible version supports the target protocol yes = compatibleVersion.getSupportedProtocols().contains(protocol); @@ -324,7 +350,7 @@ public final class ProtocolBridge { * @return true if the current instance is running as a web server, false otherwise */ public boolean isRunningAsWebServer() { - return isRunningAsServer() && protocolServer instanceof ProtocolWebServer; + return isRunningAsServer() && protocolServer instanceof ProtocolWebServer_1_0_0_B; } /** diff --git a/src/main/java/org/openautonomousconnection/protocol/listeners/ClientListener.java b/src/main/java/org/openautonomousconnection/protocol/listeners/ClientListener.java index bea5444..c88e079 100644 --- a/src/main/java/org/openautonomousconnection/protocol/listeners/ClientListener.java +++ b/src/main/java/org/openautonomousconnection/protocol/listeners/ClientListener.java @@ -1,6 +1,7 @@ package org.openautonomousconnection.protocol.listeners; import dev.unlegitdqrk.unlegitlibrary.event.EventListener; +import dev.unlegitdqrk.unlegitlibrary.event.EventPriority; import dev.unlegitdqrk.unlegitlibrary.event.Listener; import dev.unlegitdqrk.unlegitlibrary.network.system.client.events.state.ClientConnectedEvent; import dev.unlegitdqrk.unlegitlibrary.network.system.utils.TransportProtocol; @@ -37,7 +38,7 @@ public final class ClientListener extends EventListener { * * @param event The client connected event. */ - @Listener + @Listener(priority = EventPriority.HIGHEST) public void onConnect(ClientConnectedEvent event) { try { event.getClient().sendPacket(new AuthPacket(client.getProtocolBridge()), TransportProtocol.TCP); diff --git a/src/main/java/org/openautonomousconnection/protocol/listeners/CustomServerListener.java b/src/main/java/org/openautonomousconnection/protocol/listeners/CustomServerListener.java index e2f460f..3733be1 100644 --- a/src/main/java/org/openautonomousconnection/protocol/listeners/CustomServerListener.java +++ b/src/main/java/org/openautonomousconnection/protocol/listeners/CustomServerListener.java @@ -13,9 +13,9 @@ import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.WebRequestP import org.openautonomousconnection.protocol.side.ins.ProtocolINSServer; import org.openautonomousconnection.protocol.side.server.CustomConnectedClient; import org.openautonomousconnection.protocol.side.server.ProtocolCustomServer; -import org.openautonomousconnection.protocol.side.web.ProtocolWebServer; import org.openautonomousconnection.protocol.versions.v1_0_0.beta.INSRecord; import org.openautonomousconnection.protocol.versions.v1_0_0.beta.INSResponseStatus; +import org.openautonomousconnection.protocol.versions.v1_0_0.beta.ProtocolWebServer_1_0_0_B; import java.io.IOException; import java.util.ArrayList; @@ -47,7 +47,7 @@ public final class CustomServerListener extends EventListener { * * @param event The connection handler connected event. */ - @Listener + @Listener(priority = EventPriority.HIGHEST) public void onConnect(S_ClientConnectedEvent event) { try { server.getClients().add(new CustomConnectedClient(event.getClient(), server)); @@ -57,13 +57,13 @@ public final class CustomServerListener extends EventListener { } } - @Listener(priority = EventPriority.HIGH) + @Listener(priority = EventPriority.HIGHEST) public void onPacketWeb(S_PacketReadEvent event) { if (!server.getProtocolBridge().isRunningAsWebServer()) return; if (event.getPacket() instanceof WebRequestPacket) { try { event.getClient().sendPacket( - ((ProtocolWebServer) server.getProtocolBridge().getProtocolServer()). + ((ProtocolWebServer_1_0_0_B) server.getProtocolBridge().getProtocolServer()). onWebRequest(server.getClientByID(event.getClient().getUniqueID()), (WebRequestPacket) event.getPacket()), TransportProtocol.TCP); } catch (IOException e) { @@ -88,7 +88,7 @@ public final class CustomServerListener extends EventListener { * * @param event The packet event received by the network system. */ - @Listener + @Listener(priority = EventPriority.HIGHEST) public void onPacketINS(S_PacketReadEvent event) { if (!(event.getPacket() instanceof INSQueryPacket q)) return; if (!server.getProtocolBridge().isRunningAsINSServer()) return; diff --git a/src/main/java/org/openautonomousconnection/protocol/packets/v1_0_0/beta/AuthPacket.java b/src/main/java/org/openautonomousconnection/protocol/packets/v1_0_0/beta/AuthPacket.java index 923bfac..387f25f 100644 --- a/src/main/java/org/openautonomousconnection/protocol/packets/v1_0_0/beta/AuthPacket.java +++ b/src/main/java/org/openautonomousconnection/protocol/packets/v1_0_0/beta/AuthPacket.java @@ -55,8 +55,6 @@ public final class AuthPacket extends OACPacket { @Override public void onWrite(DataOutputStream objectOutputStream) throws IOException { - - if (protocolBridge.isRunningAsINSServer()) { objectOutputStream.writeBoolean(true); objectOutputStream.writeUTF(protocolBridge.getProtocolVersion().name()); diff --git a/src/main/java/org/openautonomousconnection/protocol/packets/v1_0_0/beta/INSQueryPacket.java b/src/main/java/org/openautonomousconnection/protocol/packets/v1_0_0/beta/INSQueryPacket.java index 36e8cf3..4cf7d78 100644 --- a/src/main/java/org/openautonomousconnection/protocol/packets/v1_0_0/beta/INSQueryPacket.java +++ b/src/main/java/org/openautonomousconnection/protocol/packets/v1_0_0/beta/INSQueryPacket.java @@ -45,7 +45,7 @@ public final class INSQueryPacket extends OACPacket { * @param clientId Sender client ID for routing. */ public INSQueryPacket(String tln, String name, String sub, INSRecordType type, UUID clientId) { - super(7, ProtocolVersion.PV_1_0_0_BETA); + this(); this.TLN = tln; this.name = name; this.sub = sub; diff --git a/src/main/java/org/openautonomousconnection/protocol/packets/v1_0_0/beta/INSResponsePacket.java b/src/main/java/org/openautonomousconnection/protocol/packets/v1_0_0/beta/INSResponsePacket.java index a12ab33..a9edf4d 100644 --- a/src/main/java/org/openautonomousconnection/protocol/packets/v1_0_0/beta/INSResponsePacket.java +++ b/src/main/java/org/openautonomousconnection/protocol/packets/v1_0_0/beta/INSResponsePacket.java @@ -46,11 +46,10 @@ public final class INSResponsePacket extends OACPacket { * @param bridge Protocol runtime context. */ public INSResponsePacket(INSResponseStatus status, List records, UUID clientId, ProtocolBridge bridge) { - super(6, ProtocolVersion.PV_1_0_0_BETA); + this(bridge); this.status = status; this.records = records; this.clientId = clientId; - this.bridge = bridge; } /** diff --git a/src/main/java/org/openautonomousconnection/protocol/packets/v1_0_0/beta/web/WebRequestPacket.java b/src/main/java/org/openautonomousconnection/protocol/packets/v1_0_0/beta/web/WebRequestPacket.java index e763e8c..05c06fc 100644 --- a/src/main/java/org/openautonomousconnection/protocol/packets/v1_0_0/beta/web/WebRequestPacket.java +++ b/src/main/java/org/openautonomousconnection/protocol/packets/v1_0_0/beta/web/WebRequestPacket.java @@ -49,7 +49,7 @@ public final class WebRequestPacket extends OACPacket { * @param body request body (may be null) */ public WebRequestPacket(String path, WebRequestMethod method, Map headers, byte[] body) { - super(10, ProtocolVersion.PV_1_0_0_BETA); + this(); this.path = (path != null) ? path : "/"; this.method = (method != null) ? method : WebRequestMethod.GET; this.headers = (headers != null) ? new LinkedHashMap<>(headers) : Collections.emptyMap(); diff --git a/src/main/java/org/openautonomousconnection/protocol/packets/v1_0_0/beta/web/WebResponsePacket.java b/src/main/java/org/openautonomousconnection/protocol/packets/v1_0_0/beta/web/WebResponsePacket.java index 07c7375..451ef08 100644 --- a/src/main/java/org/openautonomousconnection/protocol/packets/v1_0_0/beta/web/WebResponsePacket.java +++ b/src/main/java/org/openautonomousconnection/protocol/packets/v1_0_0/beta/web/WebResponsePacket.java @@ -48,7 +48,7 @@ public final class WebResponsePacket extends OACPacket { * @param body response body (may be null) */ public WebResponsePacket(int statusCode, String contentType, Map headers, byte[] body) { - super(9, ProtocolVersion.PV_1_0_0_BETA); + this(); this.statusCode = statusCode; this.contentType = (contentType != null) ? contentType : "text/plain"; this.headers = (headers != null) ? new LinkedHashMap<>(headers) : Collections.emptyMap(); diff --git a/src/main/java/org/openautonomousconnection/protocol/packets/v1_0_0/beta/web/stream/WebStreamChunkPacket.java b/src/main/java/org/openautonomousconnection/protocol/packets/v1_0_0/beta/web/stream/WebStreamChunkPacket_v1_0_0_B.java similarity index 83% rename from src/main/java/org/openautonomousconnection/protocol/packets/v1_0_0/beta/web/stream/WebStreamChunkPacket.java rename to src/main/java/org/openautonomousconnection/protocol/packets/v1_0_0/beta/web/stream/WebStreamChunkPacket_v1_0_0_B.java index 25a50b0..7a86300 100644 --- a/src/main/java/org/openautonomousconnection/protocol/packets/v1_0_0/beta/web/stream/WebStreamChunkPacket.java +++ b/src/main/java/org/openautonomousconnection/protocol/packets/v1_0_0/beta/web/stream/WebStreamChunkPacket_v1_0_0_B.java @@ -9,19 +9,19 @@ import java.io.DataOutputStream; import java.io.IOException; import java.util.UUID; -public final class WebStreamChunkPacket extends OACPacket { +public final class WebStreamChunkPacket_v1_0_0_B extends OACPacket { @Getter private int seq; @Getter private byte[] data; - public WebStreamChunkPacket() { + public WebStreamChunkPacket_v1_0_0_B() { super(13, ProtocolVersion.PV_1_0_0_BETA); } - public WebStreamChunkPacket(int seq, byte[] data) { - super(13, ProtocolVersion.PV_1_0_0_BETA); + public WebStreamChunkPacket_v1_0_0_B(int seq, byte[] data) { + this(); this.seq = seq; this.data = (data != null) ? data : new byte[0]; } diff --git a/src/main/java/org/openautonomousconnection/protocol/packets/v1_0_0/beta/web/stream/WebStreamEndPacket.java b/src/main/java/org/openautonomousconnection/protocol/packets/v1_0_0/beta/web/stream/WebStreamEndPacket_v1_0_0_B.java similarity index 74% rename from src/main/java/org/openautonomousconnection/protocol/packets/v1_0_0/beta/web/stream/WebStreamEndPacket.java rename to src/main/java/org/openautonomousconnection/protocol/packets/v1_0_0/beta/web/stream/WebStreamEndPacket_v1_0_0_B.java index f0707f0..f2076c4 100644 --- a/src/main/java/org/openautonomousconnection/protocol/packets/v1_0_0/beta/web/stream/WebStreamEndPacket.java +++ b/src/main/java/org/openautonomousconnection/protocol/packets/v1_0_0/beta/web/stream/WebStreamEndPacket_v1_0_0_B.java @@ -9,17 +9,17 @@ import java.io.DataOutputStream; import java.io.IOException; import java.util.UUID; -public final class WebStreamEndPacket extends OACPacket { +public final class WebStreamEndPacket_v1_0_0_B extends OACPacket { @Getter private boolean ok; - public WebStreamEndPacket() { - super(12, ProtocolVersion.PV_1_0_0_BETA); + public WebStreamEndPacket_v1_0_0_B() { + super(13, ProtocolVersion.PV_1_0_0_BETA); } - public WebStreamEndPacket(boolean ok) { - super(12, ProtocolVersion.PV_1_0_0_BETA); + public WebStreamEndPacket_v1_0_0_B(boolean ok) { + this(); this.ok = ok; } diff --git a/src/main/java/org/openautonomousconnection/protocol/packets/v1_0_0/beta/web/stream/WebStreamStartPacket.java b/src/main/java/org/openautonomousconnection/protocol/packets/v1_0_0/beta/web/stream/WebStreamStartPacket_v1_0_0_B.java similarity index 88% rename from src/main/java/org/openautonomousconnection/protocol/packets/v1_0_0/beta/web/stream/WebStreamStartPacket.java rename to src/main/java/org/openautonomousconnection/protocol/packets/v1_0_0/beta/web/stream/WebStreamStartPacket_v1_0_0_B.java index 021ca98..05ed689 100644 --- a/src/main/java/org/openautonomousconnection/protocol/packets/v1_0_0/beta/web/stream/WebStreamStartPacket.java +++ b/src/main/java/org/openautonomousconnection/protocol/packets/v1_0_0/beta/web/stream/WebStreamStartPacket_v1_0_0_B.java @@ -18,7 +18,7 @@ import java.util.UUID; *

Important: This packet encodes headers using a deterministic UTF-based map format * (int size, then size times: UTF key, UTF value) instead of Java object serialization.

*/ -public final class WebStreamStartPacket extends OACPacket { +public final class WebStreamStartPacket_v1_0_0_B extends OACPacket { @Getter private int statusCode; @@ -35,7 +35,7 @@ public final class WebStreamStartPacket extends OACPacket { /** * Creates an empty start packet (used by PacketHandler factory). */ - public WebStreamStartPacket() { + public WebStreamStartPacket_v1_0_0_B() { super(11, ProtocolVersion.PV_1_0_0_BETA); } @@ -47,8 +47,8 @@ public final class WebStreamStartPacket extends OACPacket { * @param headers headers (may be null) * @param totalLength total length of the stream (may be -1 if unknown) */ - public WebStreamStartPacket(int statusCode, String contentType, Map headers, long totalLength) { - super(11, ProtocolVersion.PV_1_0_0_BETA); + public WebStreamStartPacket_v1_0_0_B(int statusCode, String contentType, Map headers, long totalLength) { + this(); this.statusCode = statusCode; this.contentType = (contentType != null) ? contentType : "application/octet-stream"; this.headers = (headers != null) ? new LinkedHashMap<>(headers) : Collections.emptyMap(); diff --git a/src/main/java/org/openautonomousconnection/protocol/packets/v1_0_1/beta/web/WebPacket.java b/src/main/java/org/openautonomousconnection/protocol/packets/v1_0_1/beta/web/WebPacket.java new file mode 100644 index 0000000..dde18b6 --- /dev/null +++ b/src/main/java/org/openautonomousconnection/protocol/packets/v1_0_1/beta/web/WebPacket.java @@ -0,0 +1,67 @@ +package org.openautonomousconnection.protocol.packets.v1_0_1.beta.web; + +import lombok.Getter; +import org.openautonomousconnection.protocol.packets.OACPacket; +import org.openautonomousconnection.protocol.versions.ProtocolVersion; +import org.openautonomousconnection.protocol.versions.v1_0_1.beta.WebPacketHeader; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.UUID; + +/** + * Base class for all Web v1.0.1-BETA packets. + * + *

Ensures the {@link WebPacketHeader} is always serialized first.

+ */ +@Getter +public abstract class WebPacket extends OACPacket { + + private WebPacketHeader header; + + protected WebPacket(int packetId, ProtocolVersion version) { + super(packetId, version); + } + + /** + * Sets the header before sending. + * + * @param header header (required) + */ + public void setHeader(WebPacketHeader header) { + this.header = header; + } + + @Override + public final void onWrite(DataOutputStream out) throws IOException { + if (header == null) { + throw new IOException("WebPacketHeader is required"); + } + header.write(out); + writeBody(out); + } + + @Override + public final void onRead(DataInputStream in, UUID clientID) throws IOException { + this.header = WebPacketHeader.read(in); + readBody(in, clientID); + } + + /** + * Writes packet-specific payload after the header. + * + * @param out stream + * @throws IOException on IO error + */ + protected abstract void writeBody(DataOutputStream out) throws IOException; + + /** + * Reads packet-specific payload after the header. + * + * @param in stream + * @param clientID sender client id + * @throws IOException on IO error + */ + protected abstract void readBody(DataInputStream in, UUID clientID) throws IOException; +} \ No newline at end of file diff --git a/src/main/java/org/openautonomousconnection/protocol/packets/v1_0_1/beta/web/impl/document/WebDocumentApplyRequestPacket.java b/src/main/java/org/openautonomousconnection/protocol/packets/v1_0_1/beta/web/impl/document/WebDocumentApplyRequestPacket.java new file mode 100644 index 0000000..dee51d8 --- /dev/null +++ b/src/main/java/org/openautonomousconnection/protocol/packets/v1_0_1/beta/web/impl/document/WebDocumentApplyRequestPacket.java @@ -0,0 +1,42 @@ +package org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.document; + +import lombok.Getter; +import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.WebPacket; +import org.openautonomousconnection.protocol.versions.ProtocolVersion; +import org.openautonomousconnection.protocol.versions.v1_0_1.beta.WebPacketHeader; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.UUID; + +/** + * Request: replace the document content with full HTML. + */ +@Getter +public final class WebDocumentApplyRequestPacket extends WebPacket { + private String fullHtml; + + /** + * Registration constructor. + */ + public WebDocumentApplyRequestPacket() { + super(18, ProtocolVersion.PV_1_0_1_BETA); + } + + public WebDocumentApplyRequestPacket(WebPacketHeader header, String fullHtml) { + this(); + setHeader(header); + this.fullHtml = (fullHtml != null) ? fullHtml : ""; + } + + @Override + protected void writeBody(DataOutputStream out) throws IOException { + out.writeUTF((fullHtml != null) ? fullHtml : ""); + } + + @Override + protected void readBody(DataInputStream in, UUID clientID) throws IOException { + this.fullHtml = in.readUTF(); + } +} \ No newline at end of file diff --git a/src/main/java/org/openautonomousconnection/protocol/packets/v1_0_1/beta/web/impl/document/WebDocumentApplyResponsePacket.java b/src/main/java/org/openautonomousconnection/protocol/packets/v1_0_1/beta/web/impl/document/WebDocumentApplyResponsePacket.java new file mode 100644 index 0000000..e844500 --- /dev/null +++ b/src/main/java/org/openautonomousconnection/protocol/packets/v1_0_1/beta/web/impl/document/WebDocumentApplyResponsePacket.java @@ -0,0 +1,48 @@ +package org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.document; + +import lombok.Getter; +import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.WebPacket; +import org.openautonomousconnection.protocol.versions.ProtocolVersion; +import org.openautonomousconnection.protocol.versions.v1_0_1.beta.WebPacketHeader; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.UUID; + +/** + * Response: result of applying document changes. + */ +@Getter +public final class WebDocumentApplyResponsePacket extends WebPacket { + private boolean ok; + private String error; + + /** + * Registration constructor. + */ + public WebDocumentApplyResponsePacket() { + super(5, ProtocolVersion.PV_1_0_1_BETA); + } + + public WebDocumentApplyResponsePacket(WebPacketHeader header, boolean ok, String error) { + this(); + setHeader(header); + this.ok = ok; + this.error = error; + } + + @Override + protected void writeBody(DataOutputStream out) throws IOException { + out.writeBoolean(ok); + out.writeBoolean(error != null); + if (error != null) out.writeUTF(error); + } + + @Override + protected void readBody(DataInputStream in, UUID clientID) throws IOException { + this.ok = in.readBoolean(); + boolean hasErr = in.readBoolean(); + this.error = hasErr ? in.readUTF() : null; + } +} \ No newline at end of file diff --git a/src/main/java/org/openautonomousconnection/protocol/packets/v1_0_1/beta/web/impl/document/WebDocumentSnapshotEventPacket.java b/src/main/java/org/openautonomousconnection/protocol/packets/v1_0_1/beta/web/impl/document/WebDocumentSnapshotEventPacket.java new file mode 100644 index 0000000..9c3167e --- /dev/null +++ b/src/main/java/org/openautonomousconnection/protocol/packets/v1_0_1/beta/web/impl/document/WebDocumentSnapshotEventPacket.java @@ -0,0 +1,46 @@ +package org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.document; + +import lombok.Getter; +import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.WebPacket; +import org.openautonomousconnection.protocol.versions.ProtocolVersion; +import org.openautonomousconnection.protocol.versions.v1_0_1.beta.WebPacketHeader; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.UUID; + +/** + * Event: sends the current document snapshot (HTML) for a page. + */ +@Getter +public final class WebDocumentSnapshotEventPacket extends WebPacket { + private String baseUrl; + private String html; + + /** + * Registration constructor. + */ + public WebDocumentSnapshotEventPacket() { + super(17, ProtocolVersion.PV_1_0_1_BETA); + } + + public WebDocumentSnapshotEventPacket(WebPacketHeader header, String baseUrl, String html) { + this(); + setHeader(header); + this.baseUrl = (baseUrl != null) ? baseUrl : ""; + this.html = (html != null) ? html : ""; + } + + @Override + protected void writeBody(DataOutputStream out) throws IOException { + out.writeUTF((baseUrl != null) ? baseUrl : ""); + out.writeUTF((html != null) ? html : ""); + } + + @Override + protected void readBody(DataInputStream in, UUID clientID) throws IOException { + this.baseUrl = in.readUTF(); + this.html = in.readUTF(); + } +} \ No newline at end of file diff --git a/src/main/java/org/openautonomousconnection/protocol/packets/v1_0_1/beta/web/impl/navigate/WebNavigateAckPacket.java b/src/main/java/org/openautonomousconnection/protocol/packets/v1_0_1/beta/web/impl/navigate/WebNavigateAckPacket.java new file mode 100644 index 0000000..a0994c9 --- /dev/null +++ b/src/main/java/org/openautonomousconnection/protocol/packets/v1_0_1/beta/web/impl/navigate/WebNavigateAckPacket.java @@ -0,0 +1,48 @@ +package org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.navigate; + +import lombok.Getter; +import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.WebPacket; +import org.openautonomousconnection.protocol.versions.ProtocolVersion; +import org.openautonomousconnection.protocol.versions.v1_0_1.beta.WebPacketHeader; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.UUID; + +/** + * Acknowledges (or rejects) a navigation request. + */ +@Getter +public final class WebNavigateAckPacket extends WebPacket { + private boolean accepted; + private String reason; + + /** + * Registration constructor. + */ + public WebNavigateAckPacket() { + super(2, ProtocolVersion.PV_1_0_1_BETA); + } + + public WebNavigateAckPacket(WebPacketHeader header, boolean accepted, String reason) { + this(); + setHeader(header); + this.accepted = accepted; + this.reason = reason; + } + + @Override + protected void writeBody(DataOutputStream out) throws IOException { + out.writeBoolean(accepted); + out.writeBoolean(reason != null); + if (reason != null) out.writeUTF(reason); + } + + @Override + protected void readBody(DataInputStream in, UUID clientID) throws IOException { + this.accepted = in.readBoolean(); + boolean hasReason = in.readBoolean(); + this.reason = hasReason ? in.readUTF() : null; + } +} \ No newline at end of file diff --git a/src/main/java/org/openautonomousconnection/protocol/packets/v1_0_1/beta/web/impl/navigate/WebNavigateRequestPacket.java b/src/main/java/org/openautonomousconnection/protocol/packets/v1_0_1/beta/web/impl/navigate/WebNavigateRequestPacket.java new file mode 100644 index 0000000..26df01f --- /dev/null +++ b/src/main/java/org/openautonomousconnection/protocol/packets/v1_0_1/beta/web/impl/navigate/WebNavigateRequestPacket.java @@ -0,0 +1,72 @@ +package org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.navigate; + +import lombok.Getter; +import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.WebPacket; +import org.openautonomousconnection.protocol.versions.ProtocolVersion; +import org.openautonomousconnection.protocol.versions.v1_0_1.beta.WebCacheMode; +import org.openautonomousconnection.protocol.versions.v1_0_1.beta.WebPacketHeader; +import org.openautonomousconnection.protocol.versions.v1_0_1.beta.WebTransitionType; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.UUID; + +/** + * Navigation request for a tab/page. + */ +@Getter +public final class WebNavigateRequestPacket extends WebPacket { + private String url; + private String referrer; + private WebTransitionType transitionType; + private long headerProfileId; + private WebCacheMode cacheMode; + + /** + * Registration constructor. + */ + public WebNavigateRequestPacket() { + super(1, ProtocolVersion.PV_1_0_1_BETA); + } + + public WebNavigateRequestPacket( + WebPacketHeader header, + String url, + String referrer, + WebTransitionType transitionType, + long headerProfileId, + WebCacheMode cacheMode + ) { + this(); + setHeader(header); + this.url = (url != null) ? url : ""; + this.referrer = referrer; + this.transitionType = (transitionType != null) ? transitionType : WebTransitionType.TYPED; + this.headerProfileId = headerProfileId; + this.cacheMode = (cacheMode != null) ? cacheMode : WebCacheMode.DEFAULT; + } + + @Override + protected void writeBody(DataOutputStream out) throws IOException { + out.writeUTF((url != null) ? url : ""); + out.writeBoolean(referrer != null); + if (referrer != null) out.writeUTF(referrer); + + out.writeUTF((transitionType != null) ? transitionType.name() : WebTransitionType.TYPED.name()); + out.writeLong(headerProfileId); + out.writeUTF((cacheMode != null) ? cacheMode.name() : WebCacheMode.DEFAULT.name()); + } + + @Override + protected void readBody(DataInputStream in, UUID clientID) throws IOException { + this.url = in.readUTF(); + + boolean hasRef = in.readBoolean(); + this.referrer = hasRef ? in.readUTF() : null; + + this.transitionType = WebTransitionType.valueOf(in.readUTF()); + this.headerProfileId = in.readLong(); + this.cacheMode = WebCacheMode.valueOf(in.readUTF()); + } +} \ No newline at end of file diff --git a/src/main/java/org/openautonomousconnection/protocol/packets/v1_0_1/beta/web/impl/resource/WebResourceRequestPacket.java b/src/main/java/org/openautonomousconnection/protocol/packets/v1_0_1/beta/web/impl/resource/WebResourceRequestPacket.java new file mode 100644 index 0000000..0c8be25 --- /dev/null +++ b/src/main/java/org/openautonomousconnection/protocol/packets/v1_0_1/beta/web/impl/resource/WebResourceRequestPacket.java @@ -0,0 +1,94 @@ +package org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.resource; + +import lombok.Getter; +import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.WebPacket; +import org.openautonomousconnection.protocol.versions.ProtocolVersion; +import org.openautonomousconnection.protocol.versions.v1_0_1.beta.WebCacheMode; +import org.openautonomousconnection.protocol.versions.v1_0_1.beta.WebInitiatorType; +import org.openautonomousconnection.protocol.versions.v1_0_1.beta.WebPacketHeader; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.UUID; + +/** + * Resource request + */ +@Getter +public final class WebResourceRequestPacket extends WebPacket { + private String url; + private String method; + private Map headers; + private byte[] body; + private String bodyContentType; + private WebInitiatorType initiatorType; + private WebCacheMode cacheMode; + + /** + * Registration constructor. + */ + public WebResourceRequestPacket() { + super(3, ProtocolVersion.PV_1_0_1_BETA); + } + + public WebResourceRequestPacket( + WebPacketHeader header, + String url, + String method, + Map headers, + byte[] body, + String bodyContentType, + WebInitiatorType initiatorType, + WebCacheMode cacheMode + ) { + this(); + setHeader(header); + this.url = (url != null) ? url : ""; + this.method = (method != null) ? method : "GET"; + this.headers = (headers != null) ? new LinkedHashMap<>(headers) : Collections.emptyMap(); + this.body = (body != null) ? body : new byte[0]; + this.bodyContentType = bodyContentType; + this.initiatorType = (initiatorType != null) ? initiatorType : WebInitiatorType.OTHER; + this.cacheMode = (cacheMode != null) ? cacheMode : WebCacheMode.DEFAULT; + } + + @Override + protected void writeBody(DataOutputStream out) throws IOException { + out.writeUTF((url != null) ? url : ""); + out.writeUTF((method != null) ? method : "GET"); + + writeStringMap(out, headers); + + out.writeBoolean(bodyContentType != null); + if (bodyContentType != null) out.writeUTF(bodyContentType); + + out.writeUTF((initiatorType != null) ? initiatorType.name() : WebInitiatorType.OTHER.name()); + out.writeUTF((cacheMode != null) ? cacheMode.name() : WebCacheMode.DEFAULT.name()); + + byte[] b = (body != null) ? body : new byte[0]; + out.writeInt(b.length); + out.write(b); + } + + @Override + protected void readBody(DataInputStream in, UUID clientID) throws IOException { + this.url = in.readUTF(); + this.method = in.readUTF(); + + this.headers = readStringMap(in); + + boolean hasBct = in.readBoolean(); + this.bodyContentType = hasBct ? in.readUTF() : null; + + this.initiatorType = WebInitiatorType.valueOf(in.readUTF()); + this.cacheMode = WebCacheMode.valueOf(in.readUTF()); + + int len = in.readInt(); + if (len < 0) throw new IOException("Negative body length in WebResourceRequestPacket"); + this.body = in.readNBytes(len); + } +} \ No newline at end of file diff --git a/src/main/java/org/openautonomousconnection/protocol/packets/v1_0_1/beta/web/impl/resource/WebResourceResponsePacket.java b/src/main/java/org/openautonomousconnection/protocol/packets/v1_0_1/beta/web/impl/resource/WebResourceResponsePacket.java new file mode 100644 index 0000000..77b30c2 --- /dev/null +++ b/src/main/java/org/openautonomousconnection/protocol/packets/v1_0_1/beta/web/impl/resource/WebResourceResponsePacket.java @@ -0,0 +1,83 @@ +package org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.resource; + +import lombok.Getter; +import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.WebPacket; +import org.openautonomousconnection.protocol.versions.ProtocolVersion; +import org.openautonomousconnection.protocol.versions.v1_0_1.beta.WebPacketHeader; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.UUID; + +/** + * Resource response + * + *

If the response body is streamed, send this packet with an empty body and set STREAM flag in the header.

+ */ +@Getter +public final class WebResourceResponsePacket extends WebPacket { + + private int statusCode; + private String contentType; + private Map headers; + private byte[] body; + private String finalUrl; + + /** + * Registration constructor. + */ + public WebResourceResponsePacket() { + super(4, ProtocolVersion.PV_1_0_1_BETA); + } + + public WebResourceResponsePacket( + WebPacketHeader header, + int statusCode, + String contentType, + Map headers, + byte[] body, + String finalUrl + ) { + this(); + setHeader(header); + this.statusCode = statusCode; + this.contentType = (contentType != null) ? contentType : "application/octet-stream"; + this.headers = (headers != null) ? new LinkedHashMap<>(headers) : Collections.emptyMap(); + this.body = (body != null) ? body : new byte[0]; + this.finalUrl = finalUrl; + } + + @Override + protected void writeBody(DataOutputStream out) throws IOException { + out.writeInt(statusCode); + out.writeUTF((contentType != null) ? contentType : "application/octet-stream"); + + writeStringMap(out, headers); + + out.writeBoolean(finalUrl != null); + if (finalUrl != null) out.writeUTF(finalUrl); + + byte[] b = (body != null) ? body : new byte[0]; + out.writeInt(b.length); + out.write(b); + } + + @Override + protected void readBody(DataInputStream in, UUID clientID) throws IOException { + this.statusCode = in.readInt(); + this.contentType = in.readUTF(); + + this.headers = readStringMap(in); + + boolean hasFinal = in.readBoolean(); + this.finalUrl = hasFinal ? in.readUTF() : null; + + int len = in.readInt(); + if (len < 0) throw new IOException("Negative body length in WebResourceResponsePacket"); + this.body = in.readNBytes(len); + } +} \ No newline at end of file diff --git a/src/main/java/org/openautonomousconnection/protocol/packets/v1_0_1/beta/web/impl/stream/WebStreamChunkPacket_v1_0_1_B.java b/src/main/java/org/openautonomousconnection/protocol/packets/v1_0_1/beta/web/impl/stream/WebStreamChunkPacket_v1_0_1_B.java new file mode 100644 index 0000000..0b29cf5 --- /dev/null +++ b/src/main/java/org/openautonomousconnection/protocol/packets/v1_0_1/beta/web/impl/stream/WebStreamChunkPacket_v1_0_1_B.java @@ -0,0 +1,52 @@ +package org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.stream; + +import lombok.Getter; +import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.WebPacket; +import org.openautonomousconnection.protocol.versions.ProtocolVersion; +import org.openautonomousconnection.protocol.versions.v1_0_1.beta.WebPacketHeader; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.UUID; + +/** + * Stream body chunk. + * + *

Correlation is done via {@link WebPacketHeader#getRequestId()}.

+ */ +@Getter +public final class WebStreamChunkPacket_v1_0_1_B extends WebPacket { + private int seq; + private byte[] data; + + /** + * Registration constructor. + */ + public WebStreamChunkPacket_v1_0_1_B() { + super(15, ProtocolVersion.PV_1_0_1_BETA); + } + + public WebStreamChunkPacket_v1_0_1_B(WebPacketHeader header, int seq, byte[] data) { + this(); + setHeader(header); + this.seq = seq; + this.data = (data != null) ? data : new byte[0]; + } + + @Override + protected void writeBody(DataOutputStream out) throws IOException { + out.writeInt(seq); + byte[] b = (data != null) ? data : new byte[0]; + out.writeInt(b.length); + out.write(b); + } + + @Override + protected void readBody(DataInputStream in, UUID clientID) throws IOException { + this.seq = in.readInt(); + int len = in.readInt(); + if (len < 0) throw new IOException("Negative chunk length in WebStreamChunkPacket"); + this.data = in.readNBytes(len); + } +} \ No newline at end of file diff --git a/src/main/java/org/openautonomousconnection/protocol/packets/v1_0_1/beta/web/impl/stream/WebStreamEndPacket_v1_0_1_B.java b/src/main/java/org/openautonomousconnection/protocol/packets/v1_0_1/beta/web/impl/stream/WebStreamEndPacket_v1_0_1_B.java new file mode 100644 index 0000000..9ed161f --- /dev/null +++ b/src/main/java/org/openautonomousconnection/protocol/packets/v1_0_1/beta/web/impl/stream/WebStreamEndPacket_v1_0_1_B.java @@ -0,0 +1,50 @@ +package org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.stream; + +import lombok.Getter; +import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.WebPacket; +import org.openautonomousconnection.protocol.versions.ProtocolVersion; +import org.openautonomousconnection.protocol.versions.v1_0_1.beta.WebPacketHeader; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.UUID; + +/** + * Ends a streamed body transfer. + * + *

Correlation is done via {@link WebPacketHeader#getRequestId()}.

+ */ +@Getter +public final class WebStreamEndPacket_v1_0_1_B extends WebPacket { + private boolean ok; + private String error; + + /** + * Registration constructor. + */ + public WebStreamEndPacket_v1_0_1_B() { + super(16, ProtocolVersion.PV_1_0_1_BETA); + } + + public WebStreamEndPacket_v1_0_1_B(WebPacketHeader header, boolean ok, String error) { + this(); + setHeader(header); + this.ok = ok; + this.error = error; + } + + @Override + protected void writeBody(DataOutputStream out) throws IOException { + out.writeBoolean(ok); + out.writeBoolean(error != null); + if (error != null) out.writeUTF(error); + } + + @Override + protected void readBody(DataInputStream in, UUID clientID) throws IOException { + this.ok = in.readBoolean(); + boolean hasErr = in.readBoolean(); + this.error = hasErr ? in.readUTF() : null; + } +} \ No newline at end of file diff --git a/src/main/java/org/openautonomousconnection/protocol/packets/v1_0_1/beta/web/impl/stream/WebStreamStartPacket_v1_0_1_B.java b/src/main/java/org/openautonomousconnection/protocol/packets/v1_0_1/beta/web/impl/stream/WebStreamStartPacket_v1_0_1_B.java new file mode 100644 index 0000000..4a76edb --- /dev/null +++ b/src/main/java/org/openautonomousconnection/protocol/packets/v1_0_1/beta/web/impl/stream/WebStreamStartPacket_v1_0_1_B.java @@ -0,0 +1,59 @@ +package org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.stream; + +import lombok.Getter; +import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.WebPacket; +import org.openautonomousconnection.protocol.versions.ProtocolVersion; +import org.openautonomousconnection.protocol.versions.v1_0_1.beta.WebPacketHeader; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.UUID; + +/** + * Starts a streamed response body. + * + *

Correlation is done via {@link WebPacketHeader#getRequestId()}.

+ */ +@Getter +public final class WebStreamStartPacket_v1_0_1_B extends WebPacket { + private int statusCode; + private String contentType; + private Map headers; + private long totalLength; + + /** + * Registration constructor. + */ + public WebStreamStartPacket_v1_0_1_B() { + super(14, ProtocolVersion.PV_1_0_1_BETA); + } + + public WebStreamStartPacket_v1_0_1_B(WebPacketHeader header, int statusCode, String contentType, Map headers, long totalLength) { + this(); + setHeader(header); + this.statusCode = statusCode; + this.contentType = (contentType != null) ? contentType : "application/octet-stream"; + this.headers = (headers != null) ? new LinkedHashMap<>(headers) : Collections.emptyMap(); + this.totalLength = totalLength; + } + + @Override + protected void writeBody(DataOutputStream out) throws IOException { + out.writeInt(statusCode); + out.writeUTF((contentType != null) ? contentType : "application/octet-stream"); + writeStringMap(out, headers); + out.writeLong(totalLength); + } + + @Override + protected void readBody(DataInputStream in, UUID clientID) throws IOException { + this.statusCode = in.readInt(); + this.contentType = in.readUTF(); + this.headers = readStringMap(in); + this.totalLength = in.readLong(); + } +} \ No newline at end of file diff --git a/src/main/java/org/openautonomousconnection/protocol/side/client/ProtocolClient.java b/src/main/java/org/openautonomousconnection/protocol/side/client/ProtocolClient.java index 92cc185..890f595 100644 --- a/src/main/java/org/openautonomousconnection/protocol/side/client/ProtocolClient.java +++ b/src/main/java/org/openautonomousconnection/protocol/side/client/ProtocolClient.java @@ -1,6 +1,7 @@ package org.openautonomousconnection.protocol.side.client; import dev.unlegitdqrk.unlegitlibrary.event.EventListener; +import dev.unlegitdqrk.unlegitlibrary.event.EventPriority; import dev.unlegitdqrk.unlegitlibrary.event.Listener; import dev.unlegitdqrk.unlegitlibrary.file.FileUtils; import dev.unlegitdqrk.unlegitlibrary.network.system.client.NetworkClient; @@ -174,7 +175,7 @@ public abstract class ProtocolClient extends EventListener { if (this.insVersion == null) this.insVersion = insVersion; } - @Listener + @Listener(priority = EventPriority.HIGHEST) public final void onDisconnect(ClientDisconnectedEvent event) { if (clientToINS == null || !clientToINS.isConnected()) { insVersion = null; @@ -182,7 +183,7 @@ public abstract class ProtocolClient extends EventListener { disconnectFromServer(); } - if (clientToServer != null && !clientToServer.isConnected()) { + if (clientToServer == null || !clientToServer.isConnected()) { serverVersion = null; clientToServer = null; } @@ -254,7 +255,7 @@ public abstract class ProtocolClient extends EventListener { yes = compatibleVersion.getProtocolType() == ProtocolVersion.ProtocolType.STABLE; if (yes) break; } - return isBetaServer() || yes; + return isStableServer() || yes; } public final boolean isBetaServer() { @@ -267,7 +268,7 @@ public abstract class ProtocolClient extends EventListener { yes = compatibleVersion.getProtocolType() == ProtocolVersion.ProtocolType.BETA; if (yes) break; } - return isStableServer() || yes; + return isBetaServer() || yes; } public final boolean isClassicServer() { @@ -312,7 +313,8 @@ public abstract class ProtocolClient extends EventListener { private final void disconnectFromServer() { if (clientToServer != null) { - protocolBridge.getProtocolValues().eventManager.unregisterListener(this); + if (clientToINS == null || !clientToINS.isConnected()) + protocolBridge.getProtocolValues().eventManager.unregisterListener(this); clientToServer.disconnect(); clientToServer = null; } diff --git a/src/main/java/org/openautonomousconnection/protocol/side/client/ProtocolWebClient.java b/src/main/java/org/openautonomousconnection/protocol/side/client/ProtocolWebClient.java new file mode 100644 index 0000000..45b4006 --- /dev/null +++ b/src/main/java/org/openautonomousconnection/protocol/side/client/ProtocolWebClient.java @@ -0,0 +1,407 @@ +package org.openautonomousconnection.protocol.side.client; + +import dev.unlegitdqrk.unlegitlibrary.event.EventPriority; +import dev.unlegitdqrk.unlegitlibrary.event.Listener; +import dev.unlegitdqrk.unlegitlibrary.network.system.client.events.packets.C_PacketReadEvent; +import dev.unlegitdqrk.unlegitlibrary.network.system.packets.Packet; +import dev.unlegitdqrk.unlegitlibrary.network.system.utils.TransportProtocol; +import lombok.Getter; +import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.WebRequestPacket; +import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.WebResponsePacket; +import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.stream.WebStreamChunkPacket_v1_0_0_B; +import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.stream.WebStreamEndPacket_v1_0_0_B; +import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.stream.WebStreamStartPacket_v1_0_0_B; +import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.WebPacket; +import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.document.WebDocumentApplyRequestPacket; +import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.document.WebDocumentApplyResponsePacket; +import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.document.WebDocumentSnapshotEventPacket; +import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.navigate.WebNavigateAckPacket; +import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.navigate.WebNavigateRequestPacket; +import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.resource.WebResourceRequestPacket; +import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.resource.WebResourceResponsePacket; +import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.stream.WebStreamChunkPacket_v1_0_1_B; +import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.stream.WebStreamEndPacket_v1_0_1_B; +import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.stream.WebStreamStartPacket_v1_0_1_B; +import org.openautonomousconnection.protocol.versions.ProtocolVersion; +import org.openautonomousconnection.protocol.versions.v1_0_0.beta.WebRequestMethod; +import org.openautonomousconnection.protocol.versions.v1_0_1.beta.*; +import org.openautonomousconnection.protocol.versions.v1_0_1.beta.compat.WebCompatMapper; + +import java.util.Collections; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Web-capable protocol client built on top of {@link ProtocolClient}. + * + *

Compatibility strategy:

+ *
    + *
  • In Web v1.0.0 mode, correlation is only possible if there is exactly ONE in-flight request.
  • + *
  • This client therefore enforces single in-flight request and maps responses/streams + * onto that single active {@link WebPacketHeader}.
  • + *
+ */ +public abstract class ProtocolWebClient extends ProtocolClient { + + /** + * Web header factory bound to this client instance. + */ + @Getter + private final WebHeaderFactory webHeaderFactory = new WebHeaderFactory(); + + /** + * Tab contexts by tabId (stable). + */ + private final Map tabs = new ConcurrentHashMap<>(); + + /** + * Single in-flight correlation header. + * + *

v1.0.0 has no correlation fields; without changing the v1.0.0 server this is the only deterministic mapping.

+ */ + private final AtomicReference v100BInFlight = new AtomicReference<>(); + + /** + * True once we observed a 1.0.0 stream start for the current in-flight request. + */ + private volatile boolean v100BStreaming; + + /** + * Creates and registers a new tab context. + */ + public final WebHeaderFactory.WebTabContext createTab() { + WebHeaderFactory.WebTabContext tab = webHeaderFactory.createTab(); + tabs.put(tab.getTabId(), tab); + return tab; + } + + /** + * Removes a tab context. + */ + public final void closeTab(long tabId) { + tabs.remove(tabId); + } + + /** + * Returns the tab context for a tab id. + */ + public final WebHeaderFactory.WebTabContext getTab(long tabId) { + return tabs.get(tabId); + } + + /** + * Begins a new navigation for the given tab. + */ + public final long beginNavigation(WebHeaderFactory.WebTabContext tab) { + Objects.requireNonNull(tab, "tab"); + return webHeaderFactory.beginNavigation(tab); + } + + public final void sendNavigate( + WebHeaderFactory.WebTabContext tab, + String url, + String referrer, + WebTransitionType transition, + long headerProfileId, + WebCacheMode cacheMode, + boolean noStore + ) throws Exception { + Objects.requireNonNull(tab, "tab"); + ensureServerConnected(); + + WebPacketHeader header = webHeaderFactory.navigation(tab, noStore); + header = new WebPacketHeader( + header.getRequestId(), + header.getTabId(), + header.getPageId(), + header.getFrameId(), + header.getFlags() | WebPacketFlags.NAVIGATION, + header.getTimestampMs() + ); + + if (is100BServer()) { + begin100BRequest(header); + WebRequestPacket packet = new WebRequestPacket( + url, + WebRequestMethod.GET, + Collections.emptyMap(), + null + ); + getClientServerConnection().sendPacket(packet, TransportProtocol.TCP); + onWebRequestSent(null); + return; + } + + WebNavigateRequestPacket pkt = new WebNavigateRequestPacket( + header, + url, + referrer, + transition, + headerProfileId, + cacheMode + ); + + getClientServerConnection().sendPacket(pkt, TransportProtocol.TCP); + onWebRequestSent(pkt); + } + + public final long sendResource( + WebHeaderFactory.WebTabContext tab, + long frameId, + String url, + String method, + Map headers, + byte[] body, + String bodyContentType, + WebInitiatorType initiator, + WebCacheMode cacheMode, + boolean noStore + ) throws Exception { + Objects.requireNonNull(tab, "tab"); + ensureServerConnected(); + + WebPacketHeader base = webHeaderFactory.resource(tab, frameId, noStore); + WebPacketHeader header = new WebPacketHeader( + base.getRequestId(), + base.getTabId(), + base.getPageId(), + base.getFrameId(), + base.getFlags() | WebPacketFlags.RESOURCE, + base.getTimestampMs() + ); + + if (is100BServer()) { + begin100BRequest(header); + + WebRequestPacket packet = new WebRequestPacket( + url, + WebCompatMapper.map100BMethod(method), + headers != null ? headers : Collections.emptyMap(), + body + ); + + getClientServerConnection().sendPacket(packet, TransportProtocol.TCP); + onWebRequestSent(null); + return header.getRequestId(); + } + + WebResourceRequestPacket pkt = new WebResourceRequestPacket( + header, + url, + method, + headers, + body, + bodyContentType, + initiator, + cacheMode + ); + + getClientServerConnection().sendPacket(pkt, TransportProtocol.TCP); + onWebRequestSent(pkt); + return header.getRequestId(); + } + + public final long sendDocumentApply(WebHeaderFactory.WebTabContext tab, long frameId, String fullHtml) throws Exception { + Objects.requireNonNull(tab, "tab"); + ensureServerConnected(); + + if (is100BServer()) { + throw new UnsupportedOperationException("Document apply is not supported by Web v1.0.0 servers."); + } + + WebPacketHeader base = webHeaderFactory.documentApply(tab, frameId); + WebPacketHeader header = new WebPacketHeader( + base.getRequestId(), + base.getTabId(), + base.getPageId(), + base.getFrameId(), + base.getFlags(), + base.getTimestampMs() + ); + + WebDocumentApplyRequestPacket pkt = new WebDocumentApplyRequestPacket(header, fullHtml); + getClientServerConnection().sendPacket(pkt, TransportProtocol.TCP); + onWebRequestSent(pkt); + return pkt.getHeader().getRequestId(); + } + + /** + * Stale detection: drops packets that refer to an old pageId for the same tab. + */ + public final boolean isStale(WebPacketHeader header) { + if (header == null) return false; + WebHeaderFactory.WebTabContext tab = tabs.get(header.getTabId()); + if (tab == null) return false; + return webHeaderFactory.isStale(tab, header); + } + + /** + * Hook invoked after sending a web request packet (may be null in 1.0.0 mode). + */ + protected void onWebRequestSent(WebPacket packet) { + } + + protected void onNavigateAck(WebNavigateAckPacket packet) { + } + + protected void onResourceResponse(WebResourceResponsePacket packet) { + } + + protected void onStreamStart(WebStreamStartPacket_v1_0_1_B packet) { + } + + protected void onStreamChunk(WebStreamChunkPacket_v1_0_1_B packet) { + } + + protected void onStreamEnd(WebStreamEndPacket_v1_0_1_B packet) { + } + + protected void onDocumentSnapshot(WebDocumentSnapshotEventPacket packet) { + } + + protected void onDocumentApplyResponse(WebDocumentApplyResponsePacket packet) { + } + + @Listener(priority = EventPriority.HIGHEST) + public final void onPacket(C_PacketReadEvent event) { + Packet p = event.getPacket(); + + // Native v1.0.1 packets + if (p instanceof WebPacket packet) { + handleIncoming(packet); + return; + } + + if (!is100BServer()) { + return; + } + + // 1.0.0 mapping requires an active correlation + WebPacketHeader corr = v100BInFlight.get(); + if (corr == null) { + // Deterministic mapping is impossible without correlation. + // Dropping is safer than misrouting. + return; + } + + // v1.0.0 response -> v1.0.1 resource response + if (p instanceof WebResponsePacket resp) { + WebResourceResponsePacket mapped = WebCompatMapper.toV101ResourceResponse(corr, resp); + onResourceResponse(mapped); + + // If this response is not part of a stream, release correlation. + if (!looksLike100BStreamHandshake(resp) && !v100BStreaming) { + end100BRequest(); + } + return; + } + + // v1.0.0 stream start/chunk/end -> v1.0.1 stream packets + if (p instanceof WebStreamStartPacket_v1_0_0_B start) { + v100BStreaming = true; + WebStreamStartPacket_v1_0_1_B mapped = WebCompatMapper.toV101StreamStart(corr, start); + onStreamStart(mapped); + return; + } + + if (p instanceof WebStreamChunkPacket_v1_0_0_B chunk) { + WebStreamChunkPacket_v1_0_1_B mapped = WebCompatMapper.toV101StreamChunk(corr, chunk); + onStreamChunk(mapped); + return; + } + + if (p instanceof WebStreamEndPacket_v1_0_0_B end) { + WebStreamEndPacket_v1_0_1_B mapped = WebCompatMapper.toV101StreamEnd(corr, end); + onStreamEnd(mapped); + + // stream finished -> release correlation + end100BRequest(); + } + } + + /** + * Central dispatcher for incoming v1.0.1 web packets. + */ + public final void handleIncoming(WebPacket packet) { + if (packet == null) return; + + WebPacketHeader h = packet.getHeader(); + if (isStale(h)) return; + + switch (packet) { + case WebNavigateAckPacket p -> onNavigateAck(p); + case WebResourceResponsePacket p -> onResourceResponse(p); + case WebStreamStartPacket_v1_0_1_B p -> onStreamStart(p); + case WebStreamChunkPacket_v1_0_1_B p -> onStreamChunk(p); + case WebStreamEndPacket_v1_0_1_B p -> onStreamEnd(p); + case WebDocumentSnapshotEventPacket p -> onDocumentSnapshot(p); + case WebDocumentApplyResponsePacket p -> onDocumentApplyResponse(p); + default -> { + } + } + } + + private void ensureServerConnected() { + if (getClientServerConnection() == null) throw new IllegalStateException("Server connection is not built"); + if (!getClientServerConnection().isConnected() && !getClientServerConnection().isTCPConnected()) { + throw new IllegalStateException("Server connection is not connected"); + } + } + + private boolean is100BServer() { + return getServerVersion().equals(ProtocolVersion.PV_1_0_0_BETA); + } + + /** + * Begins a deterministic 1.0.0 request mapping. + * + *

Enforces single in-flight 1.0.0 request.

+ * + * @param correlation correlation header to use for mapping 1.0.0 responses + */ + private void begin100BRequest(WebPacketHeader correlation) { + Objects.requireNonNull(correlation, "correlation"); + v100BStreaming = false; + + if (!v100BInFlight.compareAndSet(null, correlation)) { + throw new IllegalStateException( + "Web v1.0.0 mode supports only 1 in-flight request. " + + "Wait for response/stream end before sending the next request." + ); + } + } + + /** + * Releases 1.0.0 correlation state. + */ + private void end100BRequest() { + v100BStreaming = false; + v100BInFlight.set(null); + } + + /** + * Heuristic: checks if a 1.0.0 response indicates streaming will follow. + * + *

Your v1.0.0 WebServer uses: header 'x-oac-stream' = '1' for the 202 response before streaming.

+ * + * @param resp 1.0.0 response + * @return true if likely stream handshake + */ + private boolean looksLike100BStreamHandshake(WebResponsePacket resp) { + if (resp == null) return false; + Map h = resp.getHeaders(); + if (h == null) return false; + + // Case-insensitive lookup without allocating + for (Map.Entry e : h.entrySet()) { + if (e.getKey() != null && e.getValue() != null + && e.getKey().equalsIgnoreCase("x-oac-stream") + && e.getValue().equals("1")) { + return true; + } + } + return false; + } +} \ No newline at end of file diff --git a/src/main/java/org/openautonomousconnection/protocol/side/server/CustomConnectedClient.java b/src/main/java/org/openautonomousconnection/protocol/side/server/CustomConnectedClient.java index 5b90ca7..385eec4 100644 --- a/src/main/java/org/openautonomousconnection/protocol/side/server/CustomConnectedClient.java +++ b/src/main/java/org/openautonomousconnection/protocol/side/server/CustomConnectedClient.java @@ -38,8 +38,8 @@ public class CustomConnectedClient extends EventListener { clientVersion = null; } - @Listener(priority = EventPriority.HIGH) - public void onDisconnect(S_ClientDisconnectedEvent event) { + @Listener(priority = EventPriority.LOWEST) + public final void onDisconnect(S_ClientDisconnectedEvent event) { if (event.getClient().getUniqueID().equals(this.connection.getUniqueID())) { server.getProtocolBridge().getProtocolValues().eventManager.unregisterListener(this); clientVersion = null; diff --git a/src/main/java/org/openautonomousconnection/protocol/side/server/ProtocolCustomServer.java b/src/main/java/org/openautonomousconnection/protocol/side/server/ProtocolCustomServer.java index 74982a7..5afba15 100644 --- a/src/main/java/org/openautonomousconnection/protocol/side/server/ProtocolCustomServer.java +++ b/src/main/java/org/openautonomousconnection/protocol/side/server/ProtocolCustomServer.java @@ -120,7 +120,7 @@ public abstract class ProtocolCustomServer extends EventListener { trustStore.setCertificateEntry("root-ca-" + (caIndex++), caCert); } - // --- Build server key material (private key + certificate chain) --- + // Build server key material (private key + certificate chain) KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); keyStore.load(null, null); @@ -209,8 +209,8 @@ public abstract class ProtocolCustomServer extends EventListener { } } - @Listener - public void onStop(ServerStoppedEvent event) { + @Listener(priority = EventPriority.HIGHEST) + public final void onStop(ServerStoppedEvent event) { if (event.getServer() == network) { protocolBridge.getProtocolValues().eventManager.unregisterListener(this); } @@ -270,7 +270,7 @@ public abstract class ProtocolCustomServer extends EventListener { } } - @Listener(priority = EventPriority.LOW) + @Listener(priority = EventPriority.HIGHEST) public final void clientDisconnected(S_ClientDisconnectedEvent event) { for (CustomConnectedClient client : new ArrayList<>(clients)) { if (client.getConnection().getUniqueID().equals(event.getClient().getUniqueID())) { diff --git a/src/main/java/org/openautonomousconnection/protocol/side/web/ProtocolWebServer.java b/src/main/java/org/openautonomousconnection/protocol/side/web/ProtocolWebServer.java index 06d8af3..18c634e 100644 --- a/src/main/java/org/openautonomousconnection/protocol/side/web/ProtocolWebServer.java +++ b/src/main/java/org/openautonomousconnection/protocol/side/web/ProtocolWebServer.java @@ -1,131 +1,220 @@ package org.openautonomousconnection.protocol.side.web; -import dev.unlegitdqrk.unlegitlibrary.file.FileUtils; -import dev.unlegitdqrk.unlegitlibrary.string.RandomString; import org.openautonomousconnection.protocol.annotations.ProtocolInfo; import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.WebRequestPacket; import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.WebResponsePacket; +import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.document.WebDocumentApplyRequestPacket; +import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.document.WebDocumentApplyResponsePacket; +import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.navigate.WebNavigateAckPacket; +import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.navigate.WebNavigateRequestPacket; +import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.resource.WebResourceRequestPacket; +import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.resource.WebResourceResponsePacket; +import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.stream.WebStreamChunkPacket_v1_0_1_B; +import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.stream.WebStreamEndPacket_v1_0_1_B; +import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.stream.WebStreamStartPacket_v1_0_1_B; import org.openautonomousconnection.protocol.side.server.CustomConnectedClient; -import org.openautonomousconnection.protocol.side.server.ProtocolCustomServer; -import org.openautonomousconnection.protocol.side.web.managers.AuthManager; -import org.openautonomousconnection.protocol.side.web.managers.RuleManager; import org.openautonomousconnection.protocol.versions.ProtocolVersion; +import org.openautonomousconnection.protocol.versions.v1_0_0.beta.ProtocolWebServer_1_0_0_B; +import org.openautonomousconnection.protocol.versions.v1_0_1.beta.WebPacketFlags; +import org.openautonomousconnection.protocol.versions.v1_0_1.beta.WebPacketHeader; +import org.openautonomousconnection.protocol.versions.v1_0_1.beta.compat.WebCompatMapper; import java.io.File; -import java.util.Random; +import java.nio.charset.StandardCharsets; +import java.util.Objects; /** - * Represents the web server for the protocol. + * Protocol Web Server base for Web v1.0.1 (beta) with built-in v1.0.0 compatibility. + * + *

This class keeps the v1.0.0 entry point ({@link #onWebRequest(CustomConnectedClient, WebRequestPacket)}) + * and adapts it to the v1.0.1 resource pipeline by mapping v1.0.0 requests to a synthetic + * {@link WebResourceRequestPacket} and mapping v1.0.1 responses back to {@link WebResponsePacket}.

+ * + *

Important: + *

    + *
  • This class does NOT perform network packet dispatch automatically. Your server packet-receive listener + * must call the appropriate {@code handle*} methods (v1.0.1) or let the v1.0.0 pipeline call {@code onWebRequest}.
  • + *
  • Responses/streams must preserve correlation via {@link WebPacketHeader#getRequestId()} for v1.0.1 clients.
  • + *
  • v1.0.0 streaming packets cannot carry request correlation. If you stream, you must + * send 1.0.0 stream packets to 1.0.0 clients and v1.0.1 stream packets to v1.0.1 clients.
  • + *
+ *

*/ @ProtocolInfo(protocolSide = ProtocolVersion.ProtocolSide.WEB) -public abstract class ProtocolWebServer extends ProtocolCustomServer { - /** - * Folder for web content. - */ - private final File contentFolder; - /** - * Folder for error pages. - */ - private final File errorsFolder; - /** - * A unique secret for session management. - */ - private final String uniqueSessionString; - /** - * The expiration time of a Session in minutes - */ - private final int sessionExpire; - /** - * The max upload size in MB - */ - private final int maxUploadSize; +public abstract class ProtocolWebServer extends ProtocolWebServer_1_0_0_B { + + private static final long LEGACY_TAB_ID = 1L; + private static final long LEGACY_PAGE_ID = 1L; /** * Initializes the web server with the given configuration, authentication, and rules files. * * @param authFile The authentication file. * @param rulesFile The rules file. - * @param sessionExpire The expiration time of a Session in minutes - * @param uploadSize The max upload size in MB + * @param sessionExpire The expiration time of a Session in minutes. + * @param uploadSize The max upload size in MB. * @throws Exception If an error occurs during initialization. */ public ProtocolWebServer(File authFile, File rulesFile, int sessionExpire, int uploadSize) throws Exception { - super("server", "server"); - - this.sessionExpire = sessionExpire; - this.maxUploadSize = uploadSize; - - // Set up content and error folders - contentFolder = new File("content"); - errorsFolder = new File("errors"); - - // Create folders if they don't exist - if (!contentFolder.exists()) contentFolder.mkdir(); - if (!errorsFolder.exists()) errorsFolder.mkdir(); - - // Create auth and rules files with default content if they don't exist - if (!authFile.exists()) { - authFile.createNewFile(); - FileUtils.writeFile(authFile, """ - admin:5e884898da28047151d0e56f8dc6292773603d0d6aabbddab8f91d8e5f99f6c7 - user:e99a18c428cb38d5f260853678922e03abd8335f - """); - } - - // Create default rules file if it doesn't exist - if (!rulesFile.exists()) { - rulesFile.createNewFile(); - FileUtils.writeFile(rulesFile, """ - { - "allow": [ - "index.html", - "css/*", - "private/info.php" - ], - "deny": [ - "private/*" - ], - "auth": [ - "private/*", - "admin/*" - ] - } - """); - } - - // Load authentication and rules - uniqueSessionString = AuthManager.sha256(new RandomString(new Random(System.currentTimeMillis()).nextInt(10, 20)).nextString()); - - AuthManager.loadAuthFile(authFile); - RuleManager.loadRules(rulesFile); - } - - public final File getContentFolder() { - return contentFolder; - } - - public final File getErrorsFolder() { - return errorsFolder; - } - - public final String getUniqueSessionString() { - return uniqueSessionString; - } - - public final int getSessionExpire() { - return sessionExpire; - } - - public final int getMaxUploadSize() { - return maxUploadSize; + super(authFile, rulesFile, sessionExpire, uploadSize); } /** - * Called when the server receives a WebRequestPacket from the client. + * Server-side dispatcher entry point for a navigation request (packet id 01). * - * @param client The connected web client (pipeline + web socket). - * @param request The full decoded request packet. - * @return The response packet that should be sent back to the client. + * @param client connected client + * @param request navigation request + * @return navigation ack to send back */ - public abstract WebResponsePacket onWebRequest(CustomConnectedClient client, WebRequestPacket request); -} + public final WebNavigateAckPacket handleNavigate(CustomConnectedClient client, WebNavigateRequestPacket request) { + Objects.requireNonNull(client, "client"); + Objects.requireNonNull(request, "request"); + + WebNavigateAckPacket ack = onNavigateRequest(client, request); + if (ack == null) { + ack = new WebNavigateAckPacket( + mirrorHeader(request.getHeader(), WebPacketFlags.NAVIGATION), + false, + "onNavigateRequest returned null" + ); + } + return ack; + } + + /** + * Server-side dispatcher entry point for a resource request (packet id 03). + * + *

Streaming: + *

    + *
  • If you want to stream the body, return a {@link WebResourceResponsePacket} with an empty body, + * and then send {@link WebStreamStartPacket_v1_0_1_B}/{@link WebStreamChunkPacket_v1_0_1_B}/{@link WebStreamEndPacket_v1_0_1_B} + * using the SAME {@code requestId}.
  • + *
+ *

+ * + * @param client connected client + * @param request resource request + * @return resource response packet to send back + */ + public final WebResourceResponsePacket handleResource(CustomConnectedClient client, WebResourceRequestPacket request) { + Objects.requireNonNull(client, "client"); + Objects.requireNonNull(request, "request"); + + WebResourceResponsePacket resp = onResourceRequest(client, request); + if (resp == null) { + resp = new WebResourceResponsePacket( + mirrorHeader(request.getHeader(), WebPacketFlags.RESOURCE), + 500, + "text/plain; charset=utf-8", + java.util.Collections.emptyMap(), + "onResourceRequest returned null".getBytes(StandardCharsets.UTF_8), + null + ); + } + return resp; + } + + /** + * Server-side dispatcher entry point for a document apply request (packet id 20). + * + * @param client connected client + * @param request apply request + * @return apply response + */ + public final WebDocumentApplyResponsePacket handleDocumentApply(CustomConnectedClient client, WebDocumentApplyRequestPacket request) { + Objects.requireNonNull(client, "client"); + Objects.requireNonNull(request, "request"); + + WebDocumentApplyResponsePacket resp = onDocumentApplyRequest(client, request); + if (resp == null) { + resp = new WebDocumentApplyResponsePacket( + mirrorHeader(request.getHeader(), 0), + false, + "onDocumentApplyRequest returned null" + ); + } + return resp; + } + + /** + * Hook: called when the server receives a navigation request. + * + * @param client connected client + * @param request navigation request + * @return ack packet (must include mirrored requestId/tabId/pageId/frameId) + */ + protected abstract WebNavigateAckPacket onNavigateRequest(CustomConnectedClient client, WebNavigateRequestPacket request); + + /** + * Hook: called when the server receives a resource request. + * + * @param client connected client + * @param request resource request + * @return response packet (must include mirrored requestId/tabId/pageId/frameId) + */ + protected abstract WebResourceResponsePacket onResourceRequest(CustomConnectedClient client, WebResourceRequestPacket request); + + /** + * Hook: called when the server receives a document apply request. + * + * @param client connected client + * @param request apply request + * @return apply response packet (must include mirrored requestId/tabId/pageId/frameId) + */ + protected abstract WebDocumentApplyResponsePacket onDocumentApplyRequest(CustomConnectedClient client, WebDocumentApplyRequestPacket request); + + /** + * v1.0.0 entry point. + * + *

This method is invoked by the v1.0.0 server pipeline when a 1.0.0 client sends a 1.0.0 request packet. + * We adapt it into the v1.0.1 resource pipeline.

+ * + * @param client connected client + * @param request 1.0.0 request packet + * @return 1.0.0 response packet + */ + @Deprecated(forRemoval = false, since = "1.0.1-BETA.0.1") + @Override + public WebResponsePacket onWebRequest(CustomConnectedClient client, WebRequestPacket request) { + Objects.requireNonNull(client, "client"); + Objects.requireNonNull(request, "request"); + + // 1.0.0 requests have no correlation fields. Create synthetic ones. + long requestId = WebCompatMapper.nextCompatRequestId(); + long tabId = LEGACY_TAB_ID; + long pageId = LEGACY_PAGE_ID; + + WebResourceRequestPacket mapped = WebCompatMapper.toResourceRequest(requestId, tabId, pageId, request); + WebResourceResponsePacket resp = onResourceRequest(client, mapped); + + if (resp == null) { + return new WebResponsePacket( + 500, + "text/plain; charset=utf-8", + java.util.Collections.emptyMap(), + "onResourceRequest returned null".getBytes(StandardCharsets.UTF_8) + ); + } + + return WebCompatMapper.to100BResponse(resp); + } + + /** + * Creates a response header that mirrors correlation fields from an incoming header. + * + * @param incoming incoming header + * @param extraFlags extra flags to OR into the mirrored header + * @return mirrored header + */ + protected final WebPacketHeader mirrorHeader(WebPacketHeader incoming, int extraFlags) { + Objects.requireNonNull(incoming, "incoming"); + return new WebPacketHeader( + incoming.getRequestId(), + incoming.getTabId(), + incoming.getPageId(), + incoming.getFrameId(), + incoming.getFlags() | extraFlags, + System.currentTimeMillis() + ); + } +} \ No newline at end of file diff --git a/src/main/java/org/openautonomousconnection/protocol/versions/ProtocolVersion.java b/src/main/java/org/openautonomousconnection/protocol/versions/ProtocolVersion.java index 1e1056b..b948702 100644 --- a/src/main/java/org/openautonomousconnection/protocol/versions/ProtocolVersion.java +++ b/src/main/java/org/openautonomousconnection/protocol/versions/ProtocolVersion.java @@ -12,14 +12,19 @@ import java.util.List; */ public enum ProtocolVersion implements Serializable { /** - * Support for old OAC-Project => *_old + * For classic OAC-Project => "classic"-branch */ + @Deprecated(forRemoval = false, since = "1.0.1-BETA.0.1") PV_1_0_0_CLASSIC("1.0.0", ProtocolType.CLASSIC, ProtocolSide.WEB_INS, List.of(Protocol.HTTP)), /** - * First Beta Version of OAC-Protocol + * Version {@link ProtocolVersion#PV_1_0_0_BETA} has many bugs and does not work as expected (occurred by incompleted packets). + * Use {@link ProtocolVersion#PV_1_0_1_BETA} or newer. */ - PV_1_0_0_BETA("1.0.0", ProtocolType.BETA, ProtocolSide.ALL, List.of(Protocol.OAC), PV_1_0_0_CLASSIC), + @Deprecated(forRemoval = false, since = "1.0.1-BETA.0.1") + PV_1_0_0_BETA("1.0.0", ProtocolType.BETA, ProtocolSide.ALL, List.of(Protocol.OAC)), + + PV_1_0_1_BETA("1.0.1", ProtocolType.BETA, ProtocolSide.ALL, List.of(Protocol.OAC), PV_1_0_0_BETA), ; /** diff --git a/src/main/java/org/openautonomousconnection/protocol/versions/v1_0_0/beta/INSRecordTools.java b/src/main/java/org/openautonomousconnection/protocol/versions/v1_0_0/beta/INSRecordTools.java index 9d18f60..4ea5dc5 100644 --- a/src/main/java/org/openautonomousconnection/protocol/versions/v1_0_0/beta/INSRecordTools.java +++ b/src/main/java/org/openautonomousconnection/protocol/versions/v1_0_0/beta/INSRecordTools.java @@ -149,9 +149,7 @@ public final class INSRecordTools { return record.ttl >= 0; } - // ------------------------------------------------------------------------- // Validation helpers - // ------------------------------------------------------------------------- /** * Validates a collection of records by applying {@link #isValidRecord(INSRecord)} diff --git a/src/main/java/org/openautonomousconnection/protocol/versions/v1_0_0/beta/ProtocolWebServer_1_0_0_B.java b/src/main/java/org/openautonomousconnection/protocol/versions/v1_0_0/beta/ProtocolWebServer_1_0_0_B.java new file mode 100644 index 0000000..d3bc391 --- /dev/null +++ b/src/main/java/org/openautonomousconnection/protocol/versions/v1_0_0/beta/ProtocolWebServer_1_0_0_B.java @@ -0,0 +1,133 @@ +package org.openautonomousconnection.protocol.versions.v1_0_0.beta; + +import dev.unlegitdqrk.unlegitlibrary.file.FileUtils; +import dev.unlegitdqrk.unlegitlibrary.string.RandomString; +import org.openautonomousconnection.protocol.annotations.ProtocolInfo; +import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.WebRequestPacket; +import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.WebResponsePacket; +import org.openautonomousconnection.protocol.side.server.CustomConnectedClient; +import org.openautonomousconnection.protocol.side.server.ProtocolCustomServer; +import org.openautonomousconnection.protocol.side.web.managers.AuthManager; +import org.openautonomousconnection.protocol.side.web.managers.RuleManager; +import org.openautonomousconnection.protocol.versions.ProtocolVersion; + +import java.io.File; +import java.util.Random; + +/** + * Represents the web server for the protocol. + */ +@Deprecated(forRemoval = false, since = "1.0.1-BETA.0.1") +@ProtocolInfo(protocolSide = ProtocolVersion.ProtocolSide.WEB) +public abstract class ProtocolWebServer_1_0_0_B extends ProtocolCustomServer { + /** + * Folder for web content. + */ + private final File contentFolder; + /** + * Folder for error pages. + */ + private final File errorsFolder; + /** + * A unique secret for session management. + */ + private final String uniqueSessionString; + /** + * The expiration time of a Session in minutes + */ + private final int sessionExpire; + /** + * The max upload size in MB + */ + private final int maxUploadSize; + + /** + * Initializes the web server with the given configuration, authentication, and rules files. + * + * @param authFile The authentication file. + * @param rulesFile The rules file. + * @param sessionExpire The expiration time of a Session in minutes + * @param uploadSize The max upload size in MB + * @throws Exception If an error occurs during initialization. + */ + public ProtocolWebServer_1_0_0_B(File authFile, File rulesFile, int sessionExpire, int uploadSize) throws Exception { + super("server", "server"); + + this.sessionExpire = sessionExpire; + this.maxUploadSize = uploadSize; + + // Set up content and error folders + contentFolder = new File("content"); + errorsFolder = new File("errors"); + + // Create folders if they don't exist + if (!contentFolder.exists()) contentFolder.mkdir(); + if (!errorsFolder.exists()) errorsFolder.mkdir(); + + // Create auth and rules files with default content if they don't exist + if (!authFile.exists()) { + authFile.createNewFile(); + FileUtils.writeFile(authFile, """ + admin:5e884898da28047151d0e56f8dc6292773603d0d6aabbddab8f91d8e5f99f6c7 + user:e99a18c428cb38d5f260853678922e03abd8335f + """); + } + + // Create default rules file if it doesn't exist + if (!rulesFile.exists()) { + rulesFile.createNewFile(); + FileUtils.writeFile(rulesFile, """ + { + "allow": [ + "index.html", + "css/*", + "private/info.php" + ], + "deny": [ + "private/*" + ], + "auth": [ + "private/*", + "admin/*" + ] + } + """); + } + + // Load authentication and rules + uniqueSessionString = AuthManager.sha256(new RandomString(new Random(System.currentTimeMillis()).nextInt(10, 20)).nextString()); + + AuthManager.loadAuthFile(authFile); + RuleManager.loadRules(rulesFile); + } + + public final File getContentFolder() { + return contentFolder; + } + + public final File getErrorsFolder() { + return errorsFolder; + } + + public final String getUniqueSessionString() { + return uniqueSessionString; + } + + public final int getSessionExpire() { + return sessionExpire; + } + + public final int getMaxUploadSize() { + return maxUploadSize; + } + + /** + * Called when the server receives a WebRequestPacket from the client. + * + * @param client The connected web client (pipeline + web socket). + * @param request The full decoded request packet. + * @return The response packet that should be sent back to the client. + */ + @Deprecated(forRemoval = false, since = "1.0.1-BETA.0.1") + public abstract WebResponsePacket onWebRequest(CustomConnectedClient client, WebRequestPacket request); +} diff --git a/src/main/java/org/openautonomousconnection/protocol/versions/v1_0_0/beta/WebRequestMethod.java b/src/main/java/org/openautonomousconnection/protocol/versions/v1_0_0/beta/WebRequestMethod.java index b98f18a..131c5f3 100644 --- a/src/main/java/org/openautonomousconnection/protocol/versions/v1_0_0/beta/WebRequestMethod.java +++ b/src/main/java/org/openautonomousconnection/protocol/versions/v1_0_0/beta/WebRequestMethod.java @@ -1,5 +1,5 @@ package org.openautonomousconnection.protocol.versions.v1_0_0.beta; public enum WebRequestMethod { - GET, POST + GET, POST, EXECUTE, SCRIPT } diff --git a/src/main/java/org/openautonomousconnection/protocol/versions/v1_0_0/classic/Classic_ProtocolVersion.java b/src/main/java/org/openautonomousconnection/protocol/versions/v1_0_0/classic/Classic_ProtocolVersion.java index 92a4883..7cb8c31 100644 --- a/src/main/java/org/openautonomousconnection/protocol/versions/v1_0_0/classic/Classic_ProtocolVersion.java +++ b/src/main/java/org/openautonomousconnection/protocol/versions/v1_0_0/classic/Classic_ProtocolVersion.java @@ -5,7 +5,7 @@ import java.io.Serializable; /** * Enum representing the protocol versions for the Classic protocol. */ -@Deprecated(forRemoval = false, since = "1.0.0-BETA.3") +@Deprecated(forRemoval = true, since = "1.0.1-BETA.0.1") public enum Classic_ProtocolVersion implements Serializable { PV_1_0_0("1.0.0"); public final String version; diff --git a/src/main/java/org/openautonomousconnection/protocol/versions/v1_0_1/beta/WebCacheMode.java b/src/main/java/org/openautonomousconnection/protocol/versions/v1_0_1/beta/WebCacheMode.java new file mode 100644 index 0000000..b6197d3 --- /dev/null +++ b/src/main/java/org/openautonomousconnection/protocol/versions/v1_0_1/beta/WebCacheMode.java @@ -0,0 +1,10 @@ +package org.openautonomousconnection.protocol.versions.v1_0_1.beta; + +/** + * Cache behavior for navigation/resource requests. + */ +public enum WebCacheMode { + DEFAULT, + BYPASS, + ONLY_CACHE +} \ No newline at end of file diff --git a/src/main/java/org/openautonomousconnection/protocol/versions/v1_0_1/beta/WebHeaderFactory.java b/src/main/java/org/openautonomousconnection/protocol/versions/v1_0_1/beta/WebHeaderFactory.java new file mode 100644 index 0000000..1e673de --- /dev/null +++ b/src/main/java/org/openautonomousconnection/protocol/versions/v1_0_1/beta/WebHeaderFactory.java @@ -0,0 +1,250 @@ +package org.openautonomousconnection.protocol.versions.v1_0_1.beta; + +import java.security.SecureRandom; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicLong; + +/** + * Factory for creating {@link WebPacketHeader} instances with stable tab/page identifiers and + * monotonic request identifiers. + * + *

This class is intended to be used by the WebClient/WebServer integration layer to ensure + * every packet carries a valid header and consistent correlation metadata.

+ * + *

Thread-safety: + *

    + *
  • Request id generation is thread-safe.
  • + *
  • Tab/page allocation methods are thread-safe.
  • + *
  • {@link WebTabContext} is thread-safe for reads; writes are synchronized.
  • + *
+ *

+ */ +public final class WebHeaderFactory { + + private static final long MIN_ID = 1L; + + private final AtomicLong requestIdSeq; + private final AtomicLong tabIdSeq; + private final AtomicLong pageIdSeq; + + /** + * Creates a factory with randomized id bases to reduce collisions across process restarts. + */ + public WebHeaderFactory() { + SecureRandom rnd = new SecureRandom(); + this.requestIdSeq = new AtomicLong(Math.max(MIN_ID, rnd.nextLong() & Long.MAX_VALUE)); + this.tabIdSeq = new AtomicLong(Math.max(MIN_ID, rnd.nextLong() & Long.MAX_VALUE)); + this.pageIdSeq = new AtomicLong(Math.max(MIN_ID, rnd.nextLong() & Long.MAX_VALUE)); + } + + /** + * Creates a new tab context with a fresh tab id and an initial page id. + * + * @return new tab context + */ + public WebTabContext createTab() { + long tabId = nextTabId(); + long pageId = nextPageId(); + return new WebTabContext(tabId, pageId); + } + + /** + * Starts a new navigation by allocating a new page id for the given tab context. + * + *

Call this when navigation is initiated (typed URL, link click, reload, back/forward).

+ * + * @param tab tab context + * @return the new page id + */ + public long beginNavigation(WebTabContext tab) { + Objects.requireNonNull(tab, "tab"); + long newPageId = nextPageId(); + tab.setPageId(newPageId); + return newPageId; + } + + /** + * Creates a header for a navigation request. + * + * @param tab tab context + * @param noStore if true, sets {@link WebPacketFlags#NO_STORE} + * @return header + */ + public WebPacketHeader navigation(WebTabContext tab, boolean noStore) { + Objects.requireNonNull(tab, "tab"); + int flags = WebPacketFlags.NAVIGATION; + if (noStore) flags |= WebPacketFlags.NO_STORE; + return header(nextRequestId(), tab.getTabId(), tab.getPageId(), 0L, flags); + } + + /** + * Creates a header for a resource request. + * + * @param tab tab context + * @param frameId frame id (0 = main) + * @param noStore if true, sets {@link WebPacketFlags#NO_STORE} + * @return header + */ + public WebPacketHeader resource(WebTabContext tab, long frameId, boolean noStore) { + Objects.requireNonNull(tab, "tab"); + int flags = WebPacketFlags.RESOURCE; + if (noStore) flags |= WebPacketFlags.NO_STORE; + return header(nextRequestId(), tab.getTabId(), tab.getPageId(), frameId, flags); + } + + /** + * Creates a header for a resource response correlated to a request id. + * + * @param requestId request id to correlate + * @param tabId tab id + * @param pageId page id + * @param frameId frame id + * @return header + */ + public WebPacketHeader resourceResponse(long requestId, long tabId, long pageId, long frameId) { + return header(requestId, tabId, pageId, frameId, WebPacketFlags.RESOURCE); + } + + /** + * Creates a header for a streamed payload associated with a request id. + * + * @param requestId correlated request id + * @param tabId tab id + * @param pageId page id + * @param frameId frame id + * @return header + */ + public WebPacketHeader stream(long requestId, long tabId, long pageId, long frameId) { + return header(requestId, tabId, pageId, frameId, WebPacketFlags.STREAM); + } + + /** + * Creates an event header (server->client or client->server). + * + * @param tabId tab id + * @param pageId page id + * @param frameId frame id + * @return header + */ + public WebPacketHeader event(long tabId, long pageId, long frameId) { + return header(nextRequestId(), tabId, pageId, frameId, WebPacketFlags.EVENT); + } + + /** + * Creates an event header correlated to an existing request id. + * + * @param requestId request id + * @param tabId tab id + * @param pageId page id + * @param frameId frame id + * @return header + */ + public WebPacketHeader correlatedEvent(long requestId, long tabId, long pageId, long frameId) { + return header(requestId, tabId, pageId, frameId, WebPacketFlags.EVENT); + } + + /** + * Creates a header for a document snapshot event. + * + * @param tab tab context + * @param frameId frame id + * @return header + */ + public WebPacketHeader documentSnapshotEvent(WebTabContext tab, long frameId) { + Objects.requireNonNull(tab, "tab"); + return header(nextRequestId(), tab.getTabId(), tab.getPageId(), frameId, WebPacketFlags.EVENT); + } + + /** + * Creates a header for a document apply request. + * + * @param tab tab context + * @param frameId frame id + * @return header + */ + public WebPacketHeader documentApply(WebTabContext tab, long frameId) { + Objects.requireNonNull(tab, "tab"); + return header(nextRequestId(), tab.getTabId(), tab.getPageId(), frameId, WebPacketFlags.RESOURCE); + } + + /** + * Checks whether an incoming packet is stale for a given tab context. + * + *

Stale means: the packet refers to an older pageId than the current tab pageId.

+ * + * @param tab tab context + * @param incoming incoming header + * @return true if stale, false otherwise + */ + public boolean isStale(WebTabContext tab, WebPacketHeader incoming) { + Objects.requireNonNull(tab, "tab"); + Objects.requireNonNull(incoming, "incoming"); + return incoming.getTabId() == tab.getTabId() && incoming.getPageId() != tab.getPageId(); + } + + private WebPacketHeader header(long requestId, long tabId, long pageId, long frameId, int flags) { + long ts = System.currentTimeMillis(); + return new WebPacketHeader(requestId, tabId, pageId, frameId, flags, ts); + } + + private long nextRequestId() { + long v = requestIdSeq.incrementAndGet(); + return (v < MIN_ID) ? MIN_ID : v; + } + + private long nextTabId() { + long v = tabIdSeq.incrementAndGet(); + return (v < MIN_ID) ? MIN_ID : v; + } + + private long nextPageId() { + long v = pageIdSeq.incrementAndGet(); + return (v < MIN_ID) ? MIN_ID : v; + } + + /** + * Represents per-tab state used for header creation and stale detection. + */ + public static final class WebTabContext { + + private final long tabId; + private volatile long pageId; + + /** + * Creates a tab context. + * + * @param tabId tab id + * @param pageId initial page id + */ + public WebTabContext(long tabId, long pageId) { + if (tabId < MIN_ID) throw new IllegalArgumentException("tabId must be >= 1"); + if (pageId < MIN_ID) throw new IllegalArgumentException("pageId must be >= 1"); + this.tabId = tabId; + this.pageId = pageId; + } + + /** + * @return stable tab id + */ + public long getTabId() { + return tabId; + } + + /** + * @return current page id + */ + public long getPageId() { + return pageId; + } + + /** + * Sets the current page id. + * + * @param pageId new page id + */ + public synchronized void setPageId(long pageId) { + if (pageId < MIN_ID) throw new IllegalArgumentException("pageId must be >= 1"); + this.pageId = pageId; + } + } +} \ No newline at end of file diff --git a/src/main/java/org/openautonomousconnection/protocol/versions/v1_0_1/beta/WebInitiatorType.java b/src/main/java/org/openautonomousconnection/protocol/versions/v1_0_1/beta/WebInitiatorType.java new file mode 100644 index 0000000..711c640 --- /dev/null +++ b/src/main/java/org/openautonomousconnection/protocol/versions/v1_0_1/beta/WebInitiatorType.java @@ -0,0 +1,16 @@ +package org.openautonomousconnection.protocol.versions.v1_0_1.beta; + +/** + * Identifies the initiator of a resource request. + */ +public enum WebInitiatorType { + DOCUMENT, + SCRIPT, + STYLE, + IMAGE, + MEDIA, + FONT, + XHR, + FETCH, + OTHER +} \ No newline at end of file diff --git a/src/main/java/org/openautonomousconnection/protocol/versions/v1_0_1/beta/WebPacketFlags.java b/src/main/java/org/openautonomousconnection/protocol/versions/v1_0_1/beta/WebPacketFlags.java new file mode 100644 index 0000000..933004d --- /dev/null +++ b/src/main/java/org/openautonomousconnection/protocol/versions/v1_0_1/beta/WebPacketFlags.java @@ -0,0 +1,46 @@ +package org.openautonomousconnection.protocol.versions.v1_0_1.beta; + +/** + * Bitset flags used in WebPacketHeader. + */ +public final class WebPacketFlags { + + /** + * Packet describes a top-level navigation action. + */ + public static final int NAVIGATION = 1 << 0; + /** + * Packet describes a resource request/response. + */ + public static final int RESOURCE = 1 << 1; + /** + * Packet is part of a streamed body transfer. + */ + public static final int STREAM = 1 << 2; + /** + * Packet is devtools-related. + */ + public static final int DEVTOOLS = 1 << 3; + /** + * Packet is an event (server->client / client->server). + */ + public static final int EVENT = 1 << 4; + /** + * Disable caching / do not store. + */ + public static final int NO_STORE = 1 << 5; + + private WebPacketFlags() { + } + + /** + * Checks if a flag is present. + * + * @param flags bitset + * @param flag flag constant + * @return true if present + */ + public static boolean has(int flags, int flag) { + return (flags & flag) == flag; + } +} \ No newline at end of file diff --git a/src/main/java/org/openautonomousconnection/protocol/versions/v1_0_1/beta/WebPacketHeader.java b/src/main/java/org/openautonomousconnection/protocol/versions/v1_0_1/beta/WebPacketHeader.java new file mode 100644 index 0000000..45b394a --- /dev/null +++ b/src/main/java/org/openautonomousconnection/protocol/versions/v1_0_1/beta/WebPacketHeader.java @@ -0,0 +1,81 @@ +package org.openautonomousconnection.protocol.versions.v1_0_1.beta; + +import lombok.Getter; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; + +/** + * Shared header for all Web v1.0.1-BETA packets. + * + *

This header MUST be written/read first in every web packet.

+ */ +@Getter +public final class WebPacketHeader { + + private long requestId; + private long tabId; + private long pageId; + private long frameId; + private int flags; + private long timestampMs; + + /** + * Empty constructor for decoding. + */ + public WebPacketHeader() { + } + + /** + * Creates a new web header. + * + * @param requestId correlation id (request/response/stream/events) + * @param tabId stable tab id + * @param pageId navigation instance id + * @param frameId frame id (0 = main frame) + * @param flags bitset flags + * @param timestampMs unix epoch millis (0 allowed) + */ + public WebPacketHeader(long requestId, long tabId, long pageId, long frameId, int flags, long timestampMs) { + this.requestId = requestId; + this.tabId = tabId; + this.pageId = pageId; + this.frameId = frameId; + this.flags = flags; + this.timestampMs = timestampMs; + } + + /** + * Reads a header from the stream. + * + * @param in stream + * @return decoded header + * @throws IOException on IO error + */ + public static WebPacketHeader read(DataInputStream in) throws IOException { + WebPacketHeader h = new WebPacketHeader(); + h.requestId = in.readLong(); + h.tabId = in.readLong(); + h.pageId = in.readLong(); + h.frameId = in.readLong(); + h.flags = in.readInt(); + h.timestampMs = in.readLong(); + return h; + } + + /** + * Writes the header to the stream. + * + * @param out stream + * @throws IOException on IO error + */ + public void write(DataOutputStream out) throws IOException { + out.writeLong(requestId); + out.writeLong(tabId); + out.writeLong(pageId); + out.writeLong(frameId); + out.writeInt(flags); + out.writeLong(timestampMs); + } +} \ No newline at end of file diff --git a/src/main/java/org/openautonomousconnection/protocol/versions/v1_0_1/beta/WebTransitionType.java b/src/main/java/org/openautonomousconnection/protocol/versions/v1_0_1/beta/WebTransitionType.java new file mode 100644 index 0000000..ce54c14 --- /dev/null +++ b/src/main/java/org/openautonomousconnection/protocol/versions/v1_0_1/beta/WebTransitionType.java @@ -0,0 +1,12 @@ +package org.openautonomousconnection.protocol.versions.v1_0_1.beta; + + +/** + * Describes how a navigation was triggered. + */ +public enum WebTransitionType { + LINK, + TYPED, + RELOAD, + BACK_FORWARD +} \ No newline at end of file diff --git a/src/main/java/org/openautonomousconnection/protocol/versions/v1_0_1/beta/compat/WebCompatMapper.java b/src/main/java/org/openautonomousconnection/protocol/versions/v1_0_1/beta/compat/WebCompatMapper.java new file mode 100644 index 0000000..55b1f20 --- /dev/null +++ b/src/main/java/org/openautonomousconnection/protocol/versions/v1_0_1/beta/compat/WebCompatMapper.java @@ -0,0 +1,281 @@ +package org.openautonomousconnection.protocol.versions.v1_0_1.beta.compat; + +import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.WebRequestPacket; +import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.WebResponsePacket; +import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.stream.WebStreamChunkPacket_v1_0_0_B; +import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.stream.WebStreamEndPacket_v1_0_0_B; +import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.stream.WebStreamStartPacket_v1_0_0_B; +import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.resource.WebResourceRequestPacket; +import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.resource.WebResourceResponsePacket; +import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.stream.WebStreamChunkPacket_v1_0_1_B; +import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.stream.WebStreamEndPacket_v1_0_1_B; +import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.stream.WebStreamStartPacket_v1_0_1_B; +import org.openautonomousconnection.protocol.versions.v1_0_0.beta.WebRequestMethod; +import org.openautonomousconnection.protocol.versions.v1_0_1.beta.WebCacheMode; +import org.openautonomousconnection.protocol.versions.v1_0_1.beta.WebInitiatorType; +import org.openautonomousconnection.protocol.versions.v1_0_1.beta.WebPacketFlags; +import org.openautonomousconnection.protocol.versions.v1_0_1.beta.WebPacketHeader; + +import java.util.Collections; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicLong; + +/** + * Compatibility mapper between Web protocol v1.0.0 + * and Web protocol v1.0.1 (beta). + * + *

This class provides pure mapping logic and contains no networking code.

+ * + *

Usage: + *

    + *
  • Server-side: map 1.0.0 {@link WebRequestPacket} to {@link WebResourceRequestPacket}
  • + *
  • Server-side: map {@link WebResourceResponsePacket} back to {@link WebResponsePacket}
  • + *
  • Client-side: map 1.0.0 responses/streams to v1.0.1 packets so v1.0.1 hooks can be reused
  • + *
+ *

+ */ +public final class WebCompatMapper { + + /** + * Synthetic request id generator for 1.0.0 requests. + */ + private static final AtomicLong COMPAT_REQUEST_ID = new AtomicLong(1L); + + private WebCompatMapper() { + } + + /** + * Generates a monotonic synthetic request id. + * + * @return next request id + */ + public static long nextCompatRequestId() { + return COMPAT_REQUEST_ID.getAndIncrement(); + } + + /** + * Maps a v1.0.0 {@link WebRequestPacket} to a v1.0.1 {@link WebResourceRequestPacket}. + * + *

Because v1.0.0 has no tab/page/frame/request correlation model, + * synthetic identifiers must be provided.

+ * + * @param requestId synthetic request id + * @param tabId synthetic tab id + * @param pageId synthetic page id + * @param requestPacket 1.0.0 request + * @return mapped v1.0.1 resource request + */ + public static WebResourceRequestPacket toResourceRequest( + long requestId, + long tabId, + long pageId, + WebRequestPacket requestPacket + ) { + Objects.requireNonNull(requestPacket, "requestPacket"); + + String path = requestPacket.getPath(); + String method = mapMethod(requestPacket.getMethod()); + Map headers = requestPacket.getHeaders() != null ? requestPacket.getHeaders() : Collections.emptyMap(); + byte[] body = requestPacket.getBody() != null ? requestPacket.getBody() : new byte[0]; + + WebPacketHeader header = new WebPacketHeader( + requestId, + tabId, + pageId, + 0L, + WebPacketFlags.RESOURCE, + System.currentTimeMillis() + ); + + return new WebResourceRequestPacket( + header, + path, + method, + headers, + body, + null, + WebInitiatorType.OTHER, + WebCacheMode.DEFAULT + ); + } + + /** + * Maps a v1.0.1 {@link WebResourceResponsePacket} back to a 1.0.0 {@link WebResponsePacket}. + * + *

Correlation information is lost because v1.0.0 does not support it.

+ * + * @param response v1.0.1 resource response + * @return 1.0.0 response packet + */ + public static WebResponsePacket to100BResponse(WebResourceResponsePacket response) { + Objects.requireNonNull(response, "response"); + + return new WebResponsePacket( + response.getStatusCode(), + response.getContentType(), + response.getHeaders(), + response.getBody() + ); + } + + /** + * Maps v1.0.1 resource response to 1.0.0 response with overridden body. + * + *

Useful when buffering streamed responses for 1.0.0 clients.

+ * + * @param response original v1.0.1 response + * @param body buffered full body + * @return 1.0.0 response + */ + public static WebResponsePacket to100BResponse(WebResourceResponsePacket response, byte[] body) { + Objects.requireNonNull(response, "response"); + + return new WebResponsePacket( + response.getStatusCode(), + response.getContentType(), + response.getHeaders(), + body != null ? body : new byte[0] + ); + } + + /** + * Maps a v1.0.0 {@link WebResponsePacket} to a v1.0.1 {@link WebResourceResponsePacket} + * using the provided synthetic correlation header. + * + * @param correlationHeader synthetic correlation header (requestId/tabId/pageId/frameId) + * @param responsePacket 1.0.0 response packet + * @return v1.0.1 resource response packet + */ + public static WebResourceResponsePacket toV101ResourceResponse(WebPacketHeader correlationHeader, WebResponsePacket responsePacket) { + Objects.requireNonNull(correlationHeader, "correlationHeader"); + Objects.requireNonNull(responsePacket, "responsePacket"); + + WebPacketHeader header = mirrorCorrelation(correlationHeader, WebPacketFlags.RESOURCE); + + return new WebResourceResponsePacket( + header, + responsePacket.getStatusCode(), + responsePacket.getContentType(), + responsePacket.getHeaders(), + responsePacket.getBody(), + null + ); + } + + /** + * Maps a v1.0.0 {@link WebStreamStartPacket_v1_0_0_B} to a v1.0.1 {@link WebStreamStartPacket_v1_0_1_B} + * using the provided synthetic correlation header. + * + * @param correlationHeader synthetic correlation header + * @param streamPacket 1.0.0 stream start + * @return v1.0.1 stream start + */ + public static WebStreamStartPacket_v1_0_1_B toV101StreamStart(WebPacketHeader correlationHeader, WebStreamStartPacket_v1_0_0_B streamPacket) { + Objects.requireNonNull(correlationHeader, "correlationHeader"); + Objects.requireNonNull(streamPacket, "streamPacket"); + + WebPacketHeader header = mirrorCorrelation(correlationHeader, WebPacketFlags.RESOURCE); + + return new WebStreamStartPacket_v1_0_1_B( + header, + streamPacket.getStatusCode(), + streamPacket.getContentType(), + streamPacket.getHeaders(), + streamPacket.getTotalLength() + ); + } + + /** + * Maps a v1.0.0 {@link WebStreamChunkPacket_v1_0_0_B} to a v1.0.1 {@link WebStreamChunkPacket_v1_0_1_B} + * using the provided synthetic correlation header. + * + * @param correlationHeader synthetic correlation header + * @param streamPacket 1.0.0 stream chunk + * @return v1.0.1 stream chunk + */ + public static WebStreamChunkPacket_v1_0_1_B toV101StreamChunk(WebPacketHeader correlationHeader, WebStreamChunkPacket_v1_0_0_B streamPacket) { + Objects.requireNonNull(correlationHeader, "correlationHeader"); + Objects.requireNonNull(streamPacket, "streamPacket"); + + WebPacketHeader header = mirrorCorrelation(correlationHeader, WebPacketFlags.RESOURCE); + + return new WebStreamChunkPacket_v1_0_1_B( + header, + streamPacket.getSeq(), + streamPacket.getData() + ); + } + + /** + * Maps a v1.0.0 {@link WebStreamEndPacket_v1_0_0_B} to a v1.0.1 {@link WebStreamEndPacket_v1_0_1_B} + * using the provided synthetic correlation header. + * + * @param correlationHeader synthetic correlation header + * @param streamPacket v1.0.0 stream end packet + * @return v1.0.1 stream end packet + */ + public static WebStreamEndPacket_v1_0_1_B toV101StreamEnd( + WebPacketHeader correlationHeader, + WebStreamEndPacket_v1_0_0_B streamPacket + ) { + Objects.requireNonNull(correlationHeader, "correlationHeader"); + Objects.requireNonNull(streamPacket, "streamPacket"); + + WebPacketHeader header = mirrorCorrelation(correlationHeader, WebPacketFlags.RESOURCE); + + // v1.0.0 has no error string -> null + return new WebStreamEndPacket_v1_0_1_B( + header, + streamPacket.isOk(), + null + ); + } + + /** + * Creates a header that mirrors correlation fields from the provided header and updates timestamp/flags. + * + * @param correlationHeader correlation header to mirror + * @param extraFlags flags to OR + * @return mirrored header + */ + public static WebPacketHeader mirrorCorrelation(WebPacketHeader correlationHeader, int extraFlags) { + Objects.requireNonNull(correlationHeader, "correlationHeader"); + return new WebPacketHeader( + correlationHeader.getRequestId(), + correlationHeader.getTabId(), + correlationHeader.getPageId(), + correlationHeader.getFrameId(), + correlationHeader.getFlags() | extraFlags, + System.currentTimeMillis() + ); + } + + /** + * Maps 1.0.0 {@link WebRequestMethod}. + * + * @param method 1.0.0 method + * @return method string + */ + public static String mapMethod(WebRequestMethod method) { + if (method == null) return "GET"; + return switch (method) { + case GET -> "GET"; + case POST, EXECUTE, SCRIPT -> "POST"; + }; + } + + /** + * Maps 1.0.1 to 1.0.0 {@link WebRequestMethod}. + * + * @param method 1.0.0 method + * @return method string + */ + public static WebRequestMethod map100BMethod(String method) { + if (method == null) return WebRequestMethod.GET; + return switch (method.toUpperCase()) { + case "POST" -> WebRequestMethod.POST; + default -> WebRequestMethod.GET; + }; + } +} \ No newline at end of file