Compare commits
52 Commits
7321d06ee9
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
700cff0294 | ||
|
|
8ef2f0291f | ||
|
|
d5b880d70c | ||
|
|
e20ee03682 | ||
|
|
d715c0758d | ||
|
|
abc989a675 | ||
|
|
632e71707d | ||
|
|
dc757d4fcb | ||
|
|
a7ce9982c9 | ||
|
|
0841992363 | ||
|
|
6107cf85be | ||
|
|
9e54fe4b46 | ||
|
|
cd58d37ea2 | ||
|
|
fb8ff372d3 | ||
|
|
ae98225043 | ||
|
|
23a3293060 | ||
|
|
c2e47a8a1e | ||
|
|
8f00dcb5df | ||
|
|
c855877530 | ||
|
|
566cddc33f | ||
|
|
0e5e0b5668 | ||
|
|
68aa9c1df2 | ||
|
|
cdf83958c9 | ||
|
|
e7954cfb0b | ||
|
|
238214c1e9 | ||
|
|
3ea6723cf9 | ||
|
|
0a0618a680 | ||
|
|
9958306572 | ||
|
|
b244633131 | ||
|
|
a2c5d67e8a | ||
|
|
a345b81846 | ||
|
|
fb56d47b16 | ||
|
|
50cd7b57ac | ||
|
|
da254a6c8e | ||
|
|
50eac7dbd5 | ||
|
|
c408c94288 | ||
|
|
5d028a45a6 | ||
|
|
3f58bf2c01 | ||
| 5e596a4cf7 | |||
|
|
bf978f48b0 | ||
|
|
6cb9f55804 | ||
|
|
0347274af3 | ||
|
|
4fb1488c9c | ||
|
|
ebccdf5b36 | ||
|
|
a00a3b319f | ||
|
|
d5b5a7d8b0 | ||
|
|
e37a76af56 | ||
|
|
4108b04582 | ||
|
|
c2b72da849 | ||
|
|
fb3086c28e | ||
|
|
95dd467634 | ||
|
|
8c64f67538 |
2
.idea/misc.xml
generated
2
.idea/misc.xml
generated
@@ -13,7 +13,7 @@
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_23" default="true" project-jdk-name="23" project-jdk-type="JavaSDK">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_25" default="true" project-jdk-name="25" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
</project>
|
||||
70
LICENSE
70
LICENSE
@@ -1,68 +1,2 @@
|
||||
Open Autonomous Public License (OAPL) v1.0
|
||||
Copyright (C) 2024-2025 Open Autonomous Connection (OAC)
|
||||
Projekt-URL: https://open-autonomous-connection.org/
|
||||
|
||||
---
|
||||
|
||||
1. Nutzungsrechte
|
||||
Diese Software darf sowohl für private als auch kommerzielle Zwecke genutzt werden. Die Nutzung ist unter den Bedingungen dieser Lizenz gestattet.
|
||||
|
||||
---
|
||||
|
||||
2. Verkaufsverbot
|
||||
Es ist nicht gestattet, diese Software oder abgeleitete Werke davon zu verkaufen oder kommerziell zu vertreiben.
|
||||
Dies umfasst auch jede Form der direkten oder indirekten Monetarisierung der Software selbst.
|
||||
Es ist gestattet, die Software im Rahmen von Dienstleistungen kommerziell zu nutzen, solange der Quellcode und die Originaldateien kostenlos verfügbar bleiben und nicht gegen Entgelt verkauft oder monetarisiert werden.
|
||||
Jede Form der Monetarisierung der Software selbst, wie der Verkauf oder die Lizenzierung der Software, ist untersagt.
|
||||
|
||||
---
|
||||
|
||||
3. Offenlegung des Quellcodes
|
||||
Die Software ist dauerhaft quelloffen. Der vollständige Quellcode muss bei jeder Verbreitung, auch in geänderter Form, mitgeliefert oder öffentlich zugänglich gemacht werden.
|
||||
Jede Version, auch veränderte, muss einen klar sichtbaren Verweis auf das Originalprojekt enthalten:
|
||||
→ https://github.com/Open-Autonomous-Connection/
|
||||
|
||||
---
|
||||
|
||||
4. Weitergabe & Lizenzvererbung
|
||||
Die Software darf frei kopiert, verteilt und verändert werden, unter folgenden Bedingungen:
|
||||
|
||||
- Die Original-Lizenz (OAPL v1.0) muss vollständig und unverändert mitgeliefert werden.
|
||||
- Alle Änderungen am Quellcode müssen klar kenntlich gemacht werden (z.B. durch Kommentare oder Änderungsprotokolle).
|
||||
- Abgeleitete Werke müssen unter derselben Lizenz (OAPL v1.0) veröffentlicht werden, es sei denn, die Lizenzierung erfolgt in einem Kontext,
|
||||
in dem dies durch geltendes Recht oder technische Einschränkungen nicht möglich ist. In diesem Fall muss der Quellcode der Änderungen weiterhin offen und zugänglich gemacht werden.
|
||||
- Es dürfen keine zusätzlichen Einschränkungen oder Bedingungen auferlegt werden, die den Bedingungen dieser Lizenz widersprechen.
|
||||
|
||||
---
|
||||
|
||||
5. Keine proprietäre Nutzung
|
||||
Die Software oder ihre abgeleiteten Werke dürfen nicht in proprietäre Software integriert oder unter einer Lizenz weitergegeben werden, die die Bedingungen dieser Lizenz einschränkt oder die Offenlegung des Quellcodes unterlässt.
|
||||
|
||||
---
|
||||
|
||||
6. Keine Veränderung dieser Lizenz
|
||||
Diese Lizenz darf nicht verändert oder durch andere Lizenzen ersetzt werden. Eine Modifikation oder Re-Lizenzierung ist ausdrücklich untersagt.
|
||||
|
||||
---
|
||||
|
||||
7. Haftungsausschluss ("as-is")
|
||||
DIE SOFTWARE WIRD 'WIE BESEHEN' BEREITGESTELLT, OHNE AUSDRÜCKLICHE ODER IMPLIZIERTE GARANTIEN, EINSCHLIESSLICH, ABER NICHT BESCHRÄNKT AUF GARANTIEN DER MARKTGÄNGIGKEIT,
|
||||
DER EIGNUNG FÜR EINEN BESTIMMTEN ZWECK UND DER NICHTVERLETZUNG VON RECHTEN DRITTER. SOWEIT ES GESCHÄFTSRECHTLICH ZULÄSSIG IST,
|
||||
WIRD DIE HAFTUNG DER AUTOREN FÜR SCHÄDEN ODER VERLUSTE AUFGRUND DER NUTZUNG DER SOFTWARE AUSGESCHLOSSEN.
|
||||
|
||||
---
|
||||
|
||||
8. Salvatorische Klausel
|
||||
Sollte eine Bestimmung dieser Lizenz als unwirksam, undurchsetzbar oder nicht durchsetzbar erklärt werden,
|
||||
bleibt die Gültigkeit der übrigen Bestimmungen davon unberührt. In diesem Fall wird die unwirksame Klausel durch eine wirksame und durchsetzbare Bestimmung ersetzt,
|
||||
die dem ursprünglichen wirtschaftlichen Zweck der unwirksamen Bestimmung am nächsten kommt.
|
||||
|
||||
---
|
||||
|
||||
Ende der Lizenzbedingungen.
|
||||
|
||||
Additional Notice regarding UnlegitLibrary:
|
||||
UnlegitLibrary is primarily distributed under the GNU GPLv3.
|
||||
For the purposes of the Open Autonomous Connection (OAC) project,
|
||||
the author has also licensed UnlegitLibrary under the Open Autonomous Public License (OAPL).
|
||||
Within OAC, the OAPL terms apply to UnlegitLibrary.
|
||||
Please read the license here: https://open-autonomous-connection.org/license.html
|
||||
Download all third parties licenses here: https://open-autonomous-connection.org/assets/licenses.zip
|
||||
|
||||
25
README.MD
25
README.MD
@@ -5,30 +5,17 @@ You can easily implement this Protocol via Maven.<br />
|
||||
Feel free to join our Discord.
|
||||
<br />
|
||||
|
||||
## License Notice
|
||||
|
||||
This project (OAC) is licensed under
|
||||
the [Open Autonomous Public License (OAPL)](https://open-autonomous-connection.org/license.html).
|
||||
|
||||
**Third-party components:**
|
||||
|
||||
<br />
|
||||
Download all license here: https://open-autonomous-connection.org/assets/licenses.zip
|
||||
- *UnlegitLibrary* is authored by the same copyright holder and is used here under a special agreement:
|
||||
While [UnlegitLibrary](https://repo.unlegitdqrk.dev/UnlegitDqrk/unlegitlibrary/) is generally distributed under
|
||||
the [GNU GPLv3](https://repo.unlegitdqrk.dev/UnlegitDqrk/unlegitlibrary/src/branch/master/LICENSE),
|
||||
it is additionally licensed under OAPL **exclusively for the OAC project**.
|
||||
Therefore, within OAC, the OAPL terms apply to UnlegitLibrary as well.<br/>
|
||||
<br/>
|
||||
Therefore, within OAC, the OAPL terms apply to UnlegitLibrary as well.
|
||||
|
||||
### Take a look into [Certificate Generation](https://repo.unlegitdqrk.dev/UnlegitDqrk/unlegitlibrary/src/branch/master#certificate-generation-for-networksystem).
|
||||
# Bugs/Problems
|
||||
- Project not tested yet
|
||||
# In progress
|
||||
- WebServer
|
||||
# TODO
|
||||
- Finishing WebServer
|
||||
- Writing INS Backend and Frontend
|
||||
- Writing WebClient
|
||||
- Writing WebServer
|
||||
- Documentation / Wiki
|
||||
|
||||
# Maven
|
||||
|
||||
### pom.xml
|
||||
@@ -36,7 +23,7 @@ the [Open Autonomous Public License (OAPL)](https://open-autonomous-connection.o
|
||||
```
|
||||
<dependency>
|
||||
<groupId>org.openautonomousconnection</groupId>
|
||||
<artifactId>protocol</artifactId>
|
||||
<artifactId>Protocol</artifactId>
|
||||
<version>VERSION</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
91
pom.xml
91
pom.xml
@@ -5,8 +5,8 @@
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>org.openautonomousconnection</groupId>
|
||||
<artifactId>protocol</artifactId>
|
||||
<version>1.0.0-BETA.11</version>
|
||||
<artifactId>Protocol</artifactId>
|
||||
<version>1.0.1-BETA.0.6</version>
|
||||
<organization>
|
||||
<name>Open Autonomous Connection</name>
|
||||
<url>https://open-autonomous-connection.org/</url>
|
||||
@@ -15,20 +15,11 @@
|
||||
<description>The Protocol for Server and Client</description>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>23</maven.compiler.source>
|
||||
<maven.compiler.target>23</maven.compiler.target>
|
||||
<maven.compiler.source>25</maven.compiler.source>
|
||||
<maven.compiler.target>25</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>github</id>
|
||||
<activation>
|
||||
<activeByDefault>true</activeByDefault>
|
||||
</activation>
|
||||
</profile>
|
||||
</profiles>
|
||||
|
||||
<developers>
|
||||
<developer>
|
||||
<name>UnlegitDqrk</name>
|
||||
@@ -65,6 +56,13 @@
|
||||
<enabled>true</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>oac</id>
|
||||
<url>https://repo.open-autonomous-connection.org/api/packages/open-autonomous-connection/maven</url>
|
||||
<snapshots>
|
||||
<enabled>true</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
<distributionManagement>
|
||||
@@ -77,9 +75,8 @@
|
||||
|
||||
<licenses>
|
||||
<license>
|
||||
<name>Open Autonomous Public License</name>
|
||||
<name>Open Autonomous Public License (OAPL)</name>
|
||||
<url>https://open-autonomous-connection.org/license.html</url>
|
||||
<distribution>repo</distribution>
|
||||
</license>
|
||||
</licenses>
|
||||
|
||||
@@ -87,12 +84,12 @@
|
||||
<dependency>
|
||||
<groupId>dev.unlegitdqrk</groupId>
|
||||
<artifactId>unlegitlibrary</artifactId>
|
||||
<version>1.6.6</version>
|
||||
<version>1.8.3</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>1.18.38</version>
|
||||
<version>1.18.42</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
@@ -100,47 +97,40 @@
|
||||
<artifactId>gson</artifactId>
|
||||
<version>2.13.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-fileupload2-jakarta-servlet6</artifactId>
|
||||
<version>2.0.0-M4</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-fileupload2</artifactId>
|
||||
<version>2.0.0-M4</version>
|
||||
<type>pom</type>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-fileupload2-core</artifactId>
|
||||
<version>2.0.0-M4</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-io</groupId>
|
||||
<artifactId>commons-io</artifactId>
|
||||
<version>2.20.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>jakarta.servlet</groupId>
|
||||
<artifactId>jakarta.servlet-api</artifactId>
|
||||
<version>6.1.0</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>net.bytebuddy</groupId>
|
||||
<artifactId>byte-buddy</artifactId>
|
||||
<version>LATEST</version>
|
||||
<version>1.18.5</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>net.bytebuddy</groupId>
|
||||
<artifactId>byte-buddy-agent</artifactId>
|
||||
<version>LATEST</version>
|
||||
<version>1.18.5</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.13.0</version>
|
||||
<configuration>
|
||||
<source>${maven.compiler.source}</source>
|
||||
<target>${maven.compiler.target}</target>
|
||||
<compilerArgs>
|
||||
<arg>-proc:full</arg>
|
||||
</compilerArgs>
|
||||
|
||||
<annotationProcessorPaths>
|
||||
<path>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>1.18.42</version>
|
||||
</path>
|
||||
</annotationProcessorPaths>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-source-plugin</artifactId>
|
||||
@@ -158,6 +148,15 @@
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-javadoc-plugin</artifactId>
|
||||
<version>3.6.3</version>
|
||||
<configuration>
|
||||
<failOnError>false</failOnError>
|
||||
<failOnWarnings>false</failOnWarnings>
|
||||
<doclint>none</doclint>
|
||||
<locale>en_US</locale>
|
||||
<encoding>UTF-8</encoding>
|
||||
<docencoding>UTF-8</docencoding>
|
||||
<charset>UTF-8</charset>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-javadocs</id>
|
||||
|
||||
@@ -1,32 +1,43 @@
|
||||
package org.openautonomousconnection.protocol;
|
||||
|
||||
import dev.unlegitdqrk.unlegitlibrary.utils.Logger;
|
||||
import dev.unlegitdqrk.unlegitlibrary.file.FileUtils;
|
||||
import dev.unlegitdqrk.unlegitlibrary.network.system.utils.ClientAuthMode;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.openautonomousconnection.protocol.annotations.ProtocolInfo;
|
||||
import org.openautonomousconnection.protocol.listeners.ClientListener;
|
||||
import org.openautonomousconnection.protocol.listeners.INSServerListener;
|
||||
import org.openautonomousconnection.protocol.listeners.WebServerListener;
|
||||
import org.openautonomousconnection.protocol.listeners.CustomServerListener;
|
||||
import org.openautonomousconnection.protocol.packets.OACPacket;
|
||||
import org.openautonomousconnection.protocol.packets.v1_0_0.beta.AuthPacket;
|
||||
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.UnsupportedClassicPacket;
|
||||
import org.openautonomousconnection.protocol.packets.v1_0_0.classic.Classic_DomainPacket;
|
||||
import org.openautonomousconnection.protocol.packets.v1_0_0.classic.Classic_MessagePacket;
|
||||
import org.openautonomousconnection.protocol.packets.v1_0_0.classic.Classic_PingPacket;
|
||||
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.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.urlhandler.v1_0_0.beta.OacWebUrlInstaller_v1_0_0_B;
|
||||
import org.openautonomousconnection.protocol.urlhandler.v1_0_1.beta.LibClientImpl_v1_0_1_B;
|
||||
import org.openautonomousconnection.protocol.urlhandler.v1_0_1.beta.OacUrlHandlerInstaller_v1_0_1_B;
|
||||
import org.openautonomousconnection.protocol.versions.ProtocolVersion;
|
||||
import org.openautonomousconnection.protocol.versions.v1_0_0.classic.handlers.ClassicHandlerClient;
|
||||
import org.openautonomousconnection.protocol.versions.v1_0_0.classic.handlers.ClassicHandlerINSServer;
|
||||
import org.openautonomousconnection.protocol.versions.v1_0_0.classic.handlers.ClassicHandlerWebServer;
|
||||
import org.openautonomousconnection.protocol.versions.v1_0_0.classic.utils.Classic_ClientListener;
|
||||
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.
|
||||
@@ -37,25 +48,7 @@ public final class ProtocolBridge {
|
||||
* The protocol settings for the current connection
|
||||
*/
|
||||
@Getter
|
||||
private final ProtocolSettings protocolSettings;
|
||||
|
||||
/**
|
||||
* The protocol version for the current connection
|
||||
*/
|
||||
@Getter
|
||||
private final ProtocolVersion protocolVersion;
|
||||
|
||||
/**
|
||||
* The logger instance for logging events and errors
|
||||
*/
|
||||
@Getter
|
||||
private Logger logger;
|
||||
|
||||
/**
|
||||
* The protocol side instances
|
||||
*/
|
||||
@Getter
|
||||
private ProtocolINSServer protocolINSServer;
|
||||
private final ProtocolValues protocolValues;
|
||||
|
||||
/**
|
||||
* The protocol side instances
|
||||
@@ -63,84 +56,32 @@ public final class ProtocolBridge {
|
||||
@Getter
|
||||
private ProtocolClient protocolClient;
|
||||
|
||||
|
||||
/**
|
||||
* The protocol side instances
|
||||
*/
|
||||
@Getter
|
||||
private ProtocolWebServer protocolWebServer;
|
||||
private ProtocolCustomServer protocolServer;
|
||||
|
||||
/**
|
||||
* The classic protocol handlers for INS server side
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
private ClassicHandlerINSServer classicHandlerINSServer;
|
||||
|
||||
/**
|
||||
* The classic protocol handlers for web server side
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
private ClassicHandlerWebServer classicHandlerWebServer;
|
||||
|
||||
/**
|
||||
* The classic protocol handlers for client side
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
private ClassicHandlerClient classicHandlerClient;
|
||||
|
||||
/**
|
||||
* The proxy for client side
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
private Proxy proxy;
|
||||
|
||||
/**
|
||||
* Initialize the ProtocolBridge instance for the INS server side
|
||||
* Initialize the ProtocolBridge instance for the client side
|
||||
*
|
||||
* @param protocolINSServer The ProtocolINSServer instance
|
||||
* @param protocolSettings The ProtocolSettings instance
|
||||
* @param protocolVersion The ProtocolVersion instance
|
||||
* @param logFolder The folder to store the log files
|
||||
* @param protocolServer The ProtocolCustomServer instance
|
||||
* @param protocolValues The ProtocolSettings instance
|
||||
* @throws Exception if an error occurs while initializing the ProtocolBridge
|
||||
*/
|
||||
@ProtocolInfo(protocolSide = ProtocolVersion.ProtocolSide.INS)
|
||||
public ProtocolBridge(ProtocolINSServer protocolINSServer, ProtocolSettings protocolSettings, ProtocolVersion protocolVersion, File logFolder) throws Exception {
|
||||
public ProtocolBridge(ProtocolCustomServer protocolServer, ProtocolValues protocolValues) throws Exception {
|
||||
// Assign the parameters to the class fields
|
||||
this.protocolINSServer = protocolINSServer;
|
||||
this.protocolSettings = protocolSettings;
|
||||
this.protocolVersion = protocolVersion;
|
||||
this.protocolServer = protocolServer;
|
||||
this.protocolValues = protocolValues;
|
||||
|
||||
if (protocolServer instanceof ProtocolINSServer)
|
||||
protocolServer.attachBridge(this, null, false, ClientAuthMode.NONE);
|
||||
else
|
||||
protocolServer.attachBridge(this, protocolValues.keyPass, protocolValues.ssl, protocolValues.authMode);
|
||||
|
||||
// Initialize the logger and protocol version
|
||||
initializeLogger(logFolder);
|
||||
initializeProtocolVersion();
|
||||
|
||||
// Register the appropriate listeners and packets
|
||||
registerListeners();
|
||||
registerPackets();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the ProtocolBridge instance for the web server side
|
||||
*
|
||||
* @param protocolWebServer The ProtocolWebServer instance
|
||||
* @param protocolSettings The ProtocolSettings instance
|
||||
* @param protocolVersion The ProtocolVersion instance
|
||||
* @param logFolder The folder to store the log files
|
||||
* @throws Exception if an error occurs while initializing the ProtocolBridge
|
||||
*/
|
||||
@ProtocolInfo(protocolSide = ProtocolVersion.ProtocolSide.WEB)
|
||||
public ProtocolBridge(ProtocolWebServer protocolWebServer, ProtocolSettings protocolSettings, ProtocolVersion protocolVersion, File logFolder) throws Exception {
|
||||
// Assign the parameters to the class fields
|
||||
this.protocolWebServer = protocolWebServer;
|
||||
this.protocolSettings = protocolSettings;
|
||||
this.protocolVersion = protocolVersion;
|
||||
|
||||
// Initialize the logger and protocol version
|
||||
initializeLogger(logFolder);
|
||||
initializeProtocolVersion();
|
||||
downloadLicenses();
|
||||
|
||||
// Register the appropriate listeners and packets
|
||||
registerListeners();
|
||||
@@ -150,54 +91,109 @@ public final class ProtocolBridge {
|
||||
/**
|
||||
* Initialize the ProtocolBridge instance for the client side
|
||||
*
|
||||
* @param protocolClient The ProtocolClient instance
|
||||
* @param protocolSettings The ProtocolSettings instance
|
||||
* @param protocolVersion The ProtocolVersion instance
|
||||
* @param logFolder The folder to store the log files
|
||||
* @param protocolClient The ProtocolClient instance
|
||||
* @param protocolValues The ProtocolSettings instance
|
||||
* @throws Exception if an error occurs while initializing the ProtocolBridge
|
||||
*/
|
||||
@ProtocolInfo(protocolSide = ProtocolVersion.ProtocolSide.CLIENT)
|
||||
public ProtocolBridge(ProtocolClient protocolClient, ProtocolSettings protocolSettings, ProtocolVersion protocolVersion, File logFolder) throws Exception {
|
||||
public ProtocolBridge(ProtocolClient protocolClient, LibClientImpl_v1_0_1_B libClientImpl,
|
||||
ProtocolValues protocolValues) throws Exception {
|
||||
// Assign the parameters to the class fields
|
||||
this.protocolClient = protocolClient;
|
||||
this.protocolSettings = protocolSettings;
|
||||
this.protocolVersion = protocolVersion;
|
||||
this.protocolValues = protocolValues;
|
||||
|
||||
// Initialize the logger and protocol version
|
||||
initializeLogger(logFolder);
|
||||
protocolClient.attachBridge(this);
|
||||
initializeProtocolVersion();
|
||||
downloadLicenses();
|
||||
|
||||
|
||||
// Register the appropriate listeners and packets
|
||||
registerListeners();
|
||||
registerPackets();
|
||||
installUrl(libClientImpl);
|
||||
}
|
||||
|
||||
private void installUrl(LibClientImpl_v1_0_1_B libClientImpl) {
|
||||
if (protocolValues.protocolVersion == ProtocolVersion.PV_1_0_0_BETA) {
|
||||
OacWebUrlInstaller_v1_0_0_B.installOnce(this, libClientImpl);
|
||||
}
|
||||
|
||||
if (protocolValues.protocolVersion == ProtocolVersion.PV_1_0_1_BETA) {
|
||||
OacUrlHandlerInstaller_v1_0_1_B.installOnce(this, libClientImpl, libClientImpl, libClientImpl);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
FileUtils.unzip(output, licensesFolder.getParent());
|
||||
output.deleteOnExit();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the appropriate packets based on the current protocol version
|
||||
* Register the appropriate packets
|
||||
*
|
||||
* <p>Overview of all Packets
|
||||
*
|
||||
* <table border="2">
|
||||
* <tr><th>ID</th><th>Packet</th><th>ProtocolVersion</th></tr>
|
||||
*
|
||||
* <tr><td>8</td><td>{@link org.openautonomousconnection.protocol.packets.v1_0_0.beta.AuthPacket AuthPacket}</td><td>{@link org.openautonomousconnection.protocol.versions.ProtocolVersion#PV_1_0_0_BETA PV_1_0_0_BETA}</td></tr>
|
||||
* <tr><td>7</td><td>{@link org.openautonomousconnection.protocol.packets.v1_0_0.beta.INSQueryPacket INSQueryPacket}</td><td>{@link org.openautonomousconnection.protocol.versions.ProtocolVersion#PV_1_0_0_BETA PV_1_0_0_BETA}</td></tr>
|
||||
* <tr><td>6</td><td>{@link org.openautonomousconnection.protocol.packets.v1_0_0.beta.INSResponsePacket INSResponsePacket}</td><td>{@link org.openautonomousconnection.protocol.versions.ProtocolVersion#PV_1_0_0_BETA PV_1_0_0_BETA}</td></tr>
|
||||
* <tr><td>10</td><td>{@link org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.WebRequestPacket WebRequestPacket}</td><td>{@link org.openautonomousconnection.protocol.versions.ProtocolVersion#PV_1_0_0_BETA PV_1_0_0_BETA}</td></tr>
|
||||
* <tr><td>9</td><td>{@link org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.WebResponsePacket WebResponsePacket}</td><td>{@link org.openautonomousconnection.protocol.versions.ProtocolVersion#PV_1_0_0_BETA PV_1_0_0_BETA}</td></tr>
|
||||
* <tr><td>11</td><td>{@link WebStreamStartPacket_v1_0_0_B v1_0_0.beta.web.stream.WebStreamStartPacket}</td><td>{@link org.openautonomousconnection.protocol.versions.ProtocolVersion#PV_1_0_0_BETA PV_1_0_0_BETA}</td></tr>
|
||||
* <tr><td>13</td><td>{@link WebStreamChunkPacket_v1_0_0_B v1_0_0.beta.web.stream.WebStreamChunkPacket}</td><td>{@link org.openautonomousconnection.protocol.versions.ProtocolVersion#PV_1_0_0_BETA PV_1_0_0_BETA}</td></tr>
|
||||
* <tr><td>12</td><td>{@link WebStreamEndPacket_v1_0_0_B v1_0_0.beta.web.stream.WebStreamEndPacket}</td><td>{@link org.openautonomousconnection.protocol.versions.ProtocolVersion#PV_1_0_0_BETA PV_1_0_0_BETA}</td></tr>
|
||||
*
|
||||
* <tr><td>1</td><td>{@link org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.navigate.WebNavigateRequestPacket WebNavigateRequestPacket}</td><td>{@link org.openautonomousconnection.protocol.versions.ProtocolVersion#PV_1_0_1_BETA PV_1_0_1_BETA}</td></tr>
|
||||
* <tr><td>2</td><td>{@link org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.navigate.WebNavigateAckPacket WebNavigateAckPacket}</td><td>{@link org.openautonomousconnection.protocol.versions.ProtocolVersion#PV_1_0_1_BETA PV_1_0_1_BETA}</td></tr>
|
||||
* <tr><td>3</td><td>{@link org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.resource.WebResourceRequestPacket WebResourceRequestPacket}</td><td>{@link org.openautonomousconnection.protocol.versions.ProtocolVersion#PV_1_0_1_BETA PV_1_0_1_BETA}</td></tr>
|
||||
* <tr><td>4</td><td>{@link org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.resource.WebResourceResponsePacket WebResourceResponsePacket}</td><td>{@link org.openautonomousconnection.protocol.versions.ProtocolVersion#PV_1_0_1_BETA PV_1_0_1_BETA}</td></tr>
|
||||
* <tr><td>14</td><td>{@link WebStreamStartPacket_v1_0_1_B v1_0_1.beta.web.impl.stream.WebStreamStartPacket}</td><td>{@link org.openautonomousconnection.protocol.versions.ProtocolVersion#PV_1_0_1_BETA PV_1_0_1_BETA}</td></tr>
|
||||
* <tr><td>15</td><td>{@link WebStreamChunkPacket_v1_0_1_B v1_0_1.beta.web.impl.stream.WebStreamChunkPacket}</td><td>{@link org.openautonomousconnection.protocol.versions.ProtocolVersion#PV_1_0_1_BETA PV_1_0_1_BETA}</td></tr>
|
||||
* <tr><td>16</td><td>{@link WebStreamEndPacket_v1_0_1_B v1_0_1.beta.web.impl.stream.WebStreamEndPacket}</td><td>{@link org.openautonomousconnection.protocol.versions.ProtocolVersion#PV_1_0_1_BETA PV_1_0_1_BETA}</td></tr>
|
||||
* <tr><td>17</td><td>{@link org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.document.WebDocumentSnapshotEventPacket WebDocumentSnapshotEventPacket}</td><td>{@link org.openautonomousconnection.protocol.versions.ProtocolVersion#PV_1_0_1_BETA PV_1_0_1_BETA}</td></tr>
|
||||
* <tr><td>5</td><td>{@link org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.document.WebDocumentApplyResponsePacket WebDocumentApplyResponsePacket}</td><td>{@link org.openautonomousconnection.protocol.versions.ProtocolVersion#PV_1_0_1_BETA PV_1_0_1_BETA}</td></tr>
|
||||
* <tr><td>18</td><td>{@link org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.document.WebDocumentApplyRequestPacket WebDocumentApplyRequestPacket}</td><td>{@link org.openautonomousconnection.protocol.versions.ProtocolVersion#PV_1_0_1_BETA PV_1_0_1_BETA}</td></tr>
|
||||
* </table>
|
||||
*
|
||||
* @see org.openautonomousconnection.protocol.versions.ProtocolVersion
|
||||
*/
|
||||
private void registerPackets() {
|
||||
// Classic packets
|
||||
Classic_DomainPacket cDomainPacket = new Classic_DomainPacket();
|
||||
Classic_MessagePacket cMessagePacket = new Classic_MessagePacket();
|
||||
Classic_PingPacket cPingPacket = new Classic_PingPacket();
|
||||
|
||||
if (isPacketSupported(cDomainPacket)) protocolSettings.packetHandler.registerPacket(cDomainPacket);
|
||||
if (isPacketSupported(cMessagePacket)) protocolSettings.packetHandler.registerPacket(cMessagePacket);
|
||||
if (isPacketSupported(cPingPacket)) protocolSettings.packetHandler.registerPacket(cPingPacket);
|
||||
|
||||
// 1.0.0-BETA packets
|
||||
AuthPacket v100bAuthPath = new AuthPacket();
|
||||
UnsupportedClassicPacket v100bUnsupportedClassicPacket = new UnsupportedClassicPacket();
|
||||
INSQueryPacket v100BINSQueryPacket = new INSQueryPacket();
|
||||
INSResponsePacket v100BINSResponsePacket = new INSResponsePacket(this);
|
||||
registerPacket(() -> new AuthPacket(this));
|
||||
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);
|
||||
|
||||
if (isPacketSupported(v100bAuthPath)) protocolSettings.packetHandler.registerPacket(v100bAuthPath);
|
||||
if (isPacketSupported(v100bUnsupportedClassicPacket))
|
||||
protocolSettings.packetHandler.registerPacket(v100bUnsupportedClassicPacket);
|
||||
if (isPacketSupported(v100BINSQueryPacket))
|
||||
protocolSettings.packetHandler.registerPacket(v100BINSQueryPacket);
|
||||
if (isPacketSupported(v100BINSResponsePacket))
|
||||
protocolSettings.packetHandler.registerPacket(v100BINSResponsePacket);
|
||||
// 1.0.1-BETA Packets
|
||||
registerPacket(() -> new WebDocumentApplyRequestPacket(this));
|
||||
registerPacket(WebDocumentApplyResponsePacket::new);
|
||||
registerPacket(WebDocumentSnapshotEventPacket::new);
|
||||
registerPacket(() -> new WebNavigateRequestPacket(this));
|
||||
registerPacket(WebNavigateAckPacket::new);
|
||||
registerPacket(() -> new WebResourceRequestPacket(this));
|
||||
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<? extends OACPacket> factory) {
|
||||
if (isPacketSupported(factory.get())) protocolValues.packetHandler.registerPacket(factory);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -206,60 +202,17 @@ public final class ProtocolBridge {
|
||||
* @throws Exception if an error occurs while registering the listeners
|
||||
*/
|
||||
private void registerListeners() throws Exception {
|
||||
// Classic listeners
|
||||
if (isClassicSupported()) {
|
||||
Classic_ClientListener classicListener = new Classic_ClientListener();
|
||||
classicListener.setProtocolBridge(this);
|
||||
protocolSettings.eventManager.registerListener(classicListener.getClass());
|
||||
} else protocolSettings.eventManager.unregisterListener(Classic_ClientListener.class);
|
||||
|
||||
// INS Listeners
|
||||
if (isRunningAsINSServer()) {
|
||||
INSServerListener serverListener = new INSServerListener();
|
||||
serverListener.setINSServer(protocolINSServer);
|
||||
protocolSettings.eventManager.registerListener(serverListener.getClass());
|
||||
protocolSettings.eventManager.unregisterListener(WebServerListener.class);
|
||||
protocolSettings.eventManager.unregisterListener(ClientListener.class);
|
||||
}
|
||||
|
||||
// Web Listeners
|
||||
if (isRunningAsWebServer()) {
|
||||
WebServerListener serverListener = new WebServerListener();
|
||||
serverListener.setWebServer(protocolWebServer);
|
||||
protocolSettings.eventManager.registerListener(serverListener.getClass());
|
||||
protocolSettings.eventManager.unregisterListener(INSServerListener.class);
|
||||
protocolSettings.eventManager.unregisterListener(ClientListener.class);
|
||||
}
|
||||
|
||||
// Client Listeners
|
||||
if (isRunningAsClient()) {
|
||||
ClientListener clientListener = new ClientListener();
|
||||
clientListener.setClient(protocolClient);
|
||||
protocolSettings.eventManager.registerListener(clientListener.getClass());
|
||||
protocolSettings.eventManager.unregisterListener(INSServerListener.class);
|
||||
protocolSettings.eventManager.unregisterListener(WebServerListener.class);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
protocolValues.eventManager.registerListener(new ClientListener(protocolClient));
|
||||
protocolValues.eventManager.unregisterListener(CustomServerListener.class);
|
||||
}
|
||||
|
||||
// Assign the temporary logger to the final field
|
||||
this.logger = tmpLogger;
|
||||
// Server Listeners
|
||||
if (isRunningAsServer()) {
|
||||
protocolValues.eventManager.registerListener(new CustomServerListener(protocolServer));
|
||||
protocolValues.eventManager.unregisterListener(ClientListener.class);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -271,7 +224,7 @@ public final class ProtocolBridge {
|
||||
// Check if the protocol version is valid for the current side
|
||||
// If not, log an error and exit the application
|
||||
if (!validateProtocolSide()) {
|
||||
this.logger.error("Invalid protocol version '" + protocolVersion.toString() + "'!");
|
||||
protocolValues.logger.error("Invalid protocol version '" + protocolValues.protocolVersion.toString() + "'!");
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -283,14 +236,14 @@ public final class ProtocolBridge {
|
||||
*/
|
||||
public boolean isClassicSupported() {
|
||||
boolean yes = false;
|
||||
for (ProtocolVersion compatibleVersion : protocolVersion.getCompatibleVersions()) {
|
||||
for (ProtocolVersion compatibleVersion : protocolValues.protocolVersion.getCompatibleVersions()) {
|
||||
// Check if the compatible version is classic
|
||||
yes = compatibleVersion.getProtocolType() == ProtocolVersion.ProtocolType.CLASSIC;
|
||||
if (yes) break;
|
||||
}
|
||||
|
||||
// Check if the current protocol version is classic or if it is supported by any of the compatible versions
|
||||
return protocolVersion.getProtocolType() == ProtocolVersion.ProtocolType.CLASSIC || yes;
|
||||
return protocolValues.protocolVersion.getProtocolType() == ProtocolVersion.ProtocolType.CLASSIC || yes;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -301,14 +254,15 @@ public final class ProtocolBridge {
|
||||
*/
|
||||
public boolean isProtocolSupported(ProtocolVersion.Protocol protocol) {
|
||||
boolean yes = false;
|
||||
for (ProtocolVersion compatibleVersion : protocolVersion.getCompatibleVersions()) {
|
||||
|
||||
for (ProtocolVersion compatibleVersion : protocolValues.protocolVersion.getCompatibleVersions()) {
|
||||
// Check if the compatible version supports the target protocol
|
||||
yes = compatibleVersion.getSupportedProtocols().contains(protocol);
|
||||
if (yes) break;
|
||||
}
|
||||
|
||||
// Check if the current protocol version supports the target protocol or if it is supported by any of the compatible versions
|
||||
return protocolVersion.getSupportedProtocols().contains(protocol) || yes;
|
||||
return protocolValues.protocolVersion.getSupportedProtocols().contains(protocol) || yes;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -318,7 +272,13 @@ public final class ProtocolBridge {
|
||||
* @return true if the target packet is supported, false otherwise
|
||||
*/
|
||||
public boolean isPacketSupported(OACPacket packet) {
|
||||
return isVersionSupported(packet.getProtocolVersion());
|
||||
boolean compatible = false;
|
||||
|
||||
for (ProtocolVersion compatibleVersion : packet.getCompatibleVersions()) {
|
||||
if (!compatible) compatible = isVersionSupported(compatibleVersion);
|
||||
}
|
||||
|
||||
return compatible;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -329,7 +289,7 @@ public final class ProtocolBridge {
|
||||
*/
|
||||
public boolean isVersionSupported(ProtocolVersion targetVersion) {
|
||||
// Check if the target protocol version is the same as the current protocol version or if it is in the list of compatible versions
|
||||
return protocolVersion == targetVersion || protocolVersion.getCompatibleVersions().contains(targetVersion);
|
||||
return protocolValues.protocolVersion == targetVersion || protocolValues.protocolVersion.getCompatibleVersions().contains(targetVersion);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -339,20 +299,22 @@ public final class ProtocolBridge {
|
||||
*/
|
||||
private boolean validateProtocolSide() {
|
||||
return
|
||||
(isRunningAsClient() && protocolVersion.getProtocolSide() == ProtocolVersion.ProtocolSide.CLIENT) ||
|
||||
(isRunningAsClient() && protocolVersion.getProtocolSide() == ProtocolVersion.ProtocolSide.CLIENT_WEB) ||
|
||||
(isRunningAsClient() && protocolVersion.getProtocolSide() == ProtocolVersion.ProtocolSide.CLIENT_INS) ||
|
||||
(isRunningAsClient() && protocolVersion.getProtocolSide() == ProtocolVersion.ProtocolSide.ALL) ||
|
||||
(isRunningAsClient() && protocolValues.protocolVersion.getProtocolSide() == ProtocolVersion.ProtocolSide.CLIENT) ||
|
||||
(isRunningAsClient() && protocolValues.protocolVersion.getProtocolSide() == ProtocolVersion.ProtocolSide.CLIENT_WEB) ||
|
||||
(isRunningAsClient() && protocolValues.protocolVersion.getProtocolSide() == ProtocolVersion.ProtocolSide.CLIENT_INS) ||
|
||||
(isRunningAsClient() && protocolValues.protocolVersion.getProtocolSide() == ProtocolVersion.ProtocolSide.ALL) ||
|
||||
|
||||
(isRunningAsWebServer() && protocolVersion.getProtocolSide() == ProtocolVersion.ProtocolSide.WEB) ||
|
||||
(isRunningAsWebServer() && protocolVersion.getProtocolSide() == ProtocolVersion.ProtocolSide.CLIENT_WEB) ||
|
||||
(isRunningAsWebServer() && protocolVersion.getProtocolSide() == ProtocolVersion.ProtocolSide.WEB_INS) ||
|
||||
(isRunningAsWebServer() && protocolVersion.getProtocolSide() == ProtocolVersion.ProtocolSide.ALL) ||
|
||||
(isRunningAsWebServer() && protocolValues.protocolVersion.getProtocolSide() == ProtocolVersion.ProtocolSide.WEB) ||
|
||||
(isRunningAsWebServer() && protocolValues.protocolVersion.getProtocolSide() == ProtocolVersion.ProtocolSide.CLIENT_WEB) ||
|
||||
(isRunningAsWebServer() && protocolValues.protocolVersion.getProtocolSide() == ProtocolVersion.ProtocolSide.WEB_INS) ||
|
||||
(isRunningAsWebServer() && protocolValues.protocolVersion.getProtocolSide() == ProtocolVersion.ProtocolSide.ALL) ||
|
||||
|
||||
(isRunningAsINSServer() && protocolVersion.getProtocolSide() == ProtocolVersion.ProtocolSide.INS) ||
|
||||
(isRunningAsINSServer() && protocolVersion.getProtocolSide() == ProtocolVersion.ProtocolSide.WEB_INS) ||
|
||||
(isRunningAsINSServer() && protocolVersion.getProtocolSide() == ProtocolVersion.ProtocolSide.CLIENT_INS) ||
|
||||
(isRunningAsINSServer() && protocolVersion.getProtocolSide() == ProtocolVersion.ProtocolSide.ALL);
|
||||
(isRunningAsServer() && protocolValues.protocolVersion.getProtocolSide() == ProtocolVersion.ProtocolSide.ALL) ||
|
||||
|
||||
(isRunningAsINSServer() && protocolValues.protocolVersion.getProtocolSide() == ProtocolVersion.ProtocolSide.INS) ||
|
||||
(isRunningAsINSServer() && protocolValues.protocolVersion.getProtocolSide() == ProtocolVersion.ProtocolSide.WEB_INS) ||
|
||||
(isRunningAsINSServer() && protocolValues.protocolVersion.getProtocolSide() == ProtocolVersion.ProtocolSide.CLIENT_INS) ||
|
||||
(isRunningAsINSServer() && protocolValues.protocolVersion.getProtocolSide() == ProtocolVersion.ProtocolSide.ALL);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -361,7 +323,7 @@ public final class ProtocolBridge {
|
||||
* @return true if the current instance is running as a INS server, false otherwise
|
||||
*/
|
||||
public boolean isRunningAsINSServer() {
|
||||
return protocolINSServer != null;
|
||||
return isRunningAsServer() && protocolServer instanceof ProtocolINSServer;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -379,6 +341,17 @@ public final class ProtocolBridge {
|
||||
* @return true if the current instance is running as a web server, false otherwise
|
||||
*/
|
||||
public boolean isRunningAsWebServer() {
|
||||
return protocolWebServer != null;
|
||||
if (protocolValues.protocolVersion == ProtocolVersion.PV_1_0_0_BETA)
|
||||
return isRunningAsServer() && protocolServer instanceof ProtocolWebServer_1_0_0_B;
|
||||
return isRunningAsServer() && protocolServer instanceof ProtocolWebServer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current instance is running as a custom server
|
||||
*
|
||||
* @return true if the current instance is running as a custom server, false otherwise
|
||||
*/
|
||||
public boolean isRunningAsServer() {
|
||||
return protocolServer != null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
package org.openautonomousconnection.protocol;
|
||||
|
||||
import dev.unlegitdqrk.unlegitlibrary.event.EventManager;
|
||||
import dev.unlegitdqrk.unlegitlibrary.network.system.packets.PacketHandler;
|
||||
import dev.unlegitdqrk.unlegitlibrary.utils.DefaultMethodsOverrider;
|
||||
|
||||
/**
|
||||
* Settings for the protocol connection.
|
||||
*/
|
||||
public final class ProtocolSettings extends DefaultMethodsOverrider {
|
||||
|
||||
/**
|
||||
* The host to connect to.
|
||||
*/
|
||||
public String host;
|
||||
|
||||
/**
|
||||
* The port to connect to.
|
||||
*/
|
||||
public int port;
|
||||
|
||||
/**
|
||||
* The protocol version to use.
|
||||
*/
|
||||
public PacketHandler packetHandler;
|
||||
|
||||
/**
|
||||
* The event manager to use.
|
||||
*/
|
||||
public EventManager eventManager;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package org.openautonomousconnection.protocol;
|
||||
|
||||
import dev.unlegitdqrk.unlegitlibrary.addon.AddonLoader;
|
||||
import dev.unlegitdqrk.unlegitlibrary.event.EventManager;
|
||||
import dev.unlegitdqrk.unlegitlibrary.network.system.packets.PacketHandler;
|
||||
import dev.unlegitdqrk.unlegitlibrary.network.system.utils.ClientAuthMode;
|
||||
import dev.unlegitdqrk.unlegitlibrary.utils.DefaultMethodsOverrider;
|
||||
import dev.unlegitdqrk.unlegitlibrary.utils.Logger;
|
||||
import org.openautonomousconnection.protocol.versions.ProtocolVersion;
|
||||
|
||||
/**
|
||||
* Settings for the protocol connection.
|
||||
*/
|
||||
public final class ProtocolValues extends DefaultMethodsOverrider {
|
||||
/**
|
||||
* The packet handler to use.
|
||||
*/
|
||||
public PacketHandler packetHandler;
|
||||
|
||||
/**
|
||||
* The event manager to use.
|
||||
*/
|
||||
public EventManager eventManager;
|
||||
|
||||
public AddonLoader addonLoader;
|
||||
|
||||
public Logger logger;
|
||||
|
||||
public String keyPass = null;
|
||||
|
||||
public ProtocolVersion protocolVersion;
|
||||
|
||||
public boolean ssl = true;
|
||||
public ClientAuthMode authMode = ClientAuthMode.NONE;
|
||||
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import java.lang.annotation.RetentionPolicy;
|
||||
/**
|
||||
* Annotation to provide metadata about protocol handlers or classes.
|
||||
*/
|
||||
@Deprecated(forRemoval = false, since = "1.0.1-BETA.0.5")
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface ProtocolInfo {
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import net.bytebuddy.agent.ByteBuddyAgent;
|
||||
import net.bytebuddy.agent.builder.AgentBuilder;
|
||||
import net.bytebuddy.asm.Advice;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.instrument.Instrumentation;
|
||||
import java.lang.reflect.Method;
|
||||
@@ -73,6 +72,6 @@ public class CallTracker<A extends Annotation> extends GenericReflectClass<A> {
|
||||
/**
|
||||
* Code executed on any method call
|
||||
*/
|
||||
public abstract void onCall(Method method, @Nullable StackTraceElement callerMethod);
|
||||
public abstract void onCall(Method method, StackTraceElement callerMethod);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import org.openautonomousconnection.protocol.annotations.ProtocolInfo;
|
||||
import org.openautonomousconnection.protocol.exceptions.IncompatibleProtocolSideException;
|
||||
import org.openautonomousconnection.protocol.versions.ProtocolVersion;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
@@ -36,7 +35,7 @@ public class ProtocolInfoProcessing extends AnnotationProcessor<ProtocolInfo> {
|
||||
|
||||
this.tracker = new CallTracker<>(new CallTracker.CallInterceptor() {
|
||||
@Override
|
||||
public void onCall(Method method, @Nullable StackTraceElement callerMethod) {
|
||||
public void onCall(Method method, StackTraceElement callerMethod) {
|
||||
ProtocolVersion.ProtocolSide side, callerSide;
|
||||
Object o;
|
||||
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
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.ClientConnectedEvent;
|
||||
import dev.unlegitdqrk.unlegitlibrary.network.system.client.events.ClientDisconnectedEvent;
|
||||
import dev.unlegitdqrk.unlegitlibrary.network.system.client.events.state.ClientConnectedEvent;
|
||||
import dev.unlegitdqrk.unlegitlibrary.network.system.utils.TransportProtocol;
|
||||
import lombok.Getter;
|
||||
import org.openautonomousconnection.protocol.annotations.ProtocolInfo;
|
||||
import org.openautonomousconnection.protocol.packets.v1_0_0.beta.AuthPacket;
|
||||
import org.openautonomousconnection.protocol.side.client.ProtocolClient;
|
||||
import org.openautonomousconnection.protocol.versions.ProtocolVersion;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Listener for client-side events such as connection and disconnection.
|
||||
*/
|
||||
@@ -22,15 +21,14 @@ public final class ClientListener extends EventListener {
|
||||
* The reference to the ProtocolClient object
|
||||
*/
|
||||
@Getter
|
||||
private ProtocolClient client;
|
||||
private final ProtocolClient client;
|
||||
|
||||
/**
|
||||
* Sets the client variable
|
||||
*
|
||||
* @param client The Instance of the ProtocolClient
|
||||
*/
|
||||
public void setClient(ProtocolClient client) {
|
||||
if (this.client != null) return;
|
||||
public ClientListener(ProtocolClient client) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
@@ -40,25 +38,14 @@ 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()));
|
||||
} catch (IOException | ClassNotFoundException exception) {
|
||||
event.getClient().getLogger().exception("Failed to send auth packet", exception);
|
||||
event.getClient().sendPacket(new AuthPacket(client.getProtocolBridge()), TransportProtocol.TCP);
|
||||
} catch (Exception exception) {
|
||||
client.getProtocolBridge().getProtocolValues().logger.exception("Failed to send auth packet", exception);
|
||||
event.getClient().disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the event when a client disconnects.
|
||||
* Notifies the protocol client of the disconnection.
|
||||
*
|
||||
* @param event The client disconnected event.
|
||||
*/
|
||||
@Listener
|
||||
public void onDisconnect(ClientDisconnectedEvent event) {
|
||||
client.onINSDisconnect(event);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
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.server.events.client.S_ClientConnectedEvent;
|
||||
import dev.unlegitdqrk.unlegitlibrary.network.system.server.events.packets.S_PacketReadEvent;
|
||||
import dev.unlegitdqrk.unlegitlibrary.network.system.utils.TransportProtocol;
|
||||
import lombok.Getter;
|
||||
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.side.ins.ProtocolINSServer;
|
||||
import org.openautonomousconnection.protocol.side.server.CustomConnectedClient;
|
||||
import org.openautonomousconnection.protocol.side.server.ProtocolCustomServer;
|
||||
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;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Listener for web server connection events.
|
||||
*/
|
||||
public final class CustomServerListener extends EventListener {
|
||||
|
||||
/**
|
||||
* The reference to the CustomProtocolServer object
|
||||
*/
|
||||
@Getter
|
||||
private final ProtocolCustomServer server;
|
||||
|
||||
/**
|
||||
* Sets the webServer variable
|
||||
*
|
||||
* @param server The Instance of the CustomProtocolServer
|
||||
*/
|
||||
public CustomServerListener(ProtocolCustomServer server) {
|
||||
this.server = server;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the event when a connection is established.
|
||||
* Adds the connected client to the protocol web server's client list.
|
||||
*
|
||||
* @param event The connection handler connected event.
|
||||
*/
|
||||
@Listener(priority = EventPriority.HIGHEST)
|
||||
public void onConnect(S_ClientConnectedEvent event) {
|
||||
try {
|
||||
server.getClients().add(new CustomConnectedClient(event.getClient(), server));
|
||||
} catch (Exception e) {
|
||||
server.getProtocolBridge().getProtocolValues().logger.exception("Failed to add client to server", e);
|
||||
event.getClient().disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
@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_1_0_0_B) server.getProtocolBridge().getProtocolServer()).
|
||||
onWebRequest(server.getClientByID(event.getClient().getUniqueID()), (WebRequestPacket) event.getPacket()),
|
||||
TransportProtocol.TCP);
|
||||
} catch (IOException e) {
|
||||
server.getProtocolBridge().getProtocolValues().logger.exception("Failed to send web response", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles incoming INS query packets from connected clients.
|
||||
* <p>
|
||||
* This method performs the following steps:
|
||||
* <ul>
|
||||
* <li>Checks whether the received packet is an {@link INSQueryPacket}</li>
|
||||
* <li>Notifies the INS server through {@code onQueryReceived}</li>
|
||||
* <li>Resolves the request via {@code resolve()}</li>
|
||||
* <li>Wraps the result in an {@link INSResponsePacket}</li>
|
||||
* <li>Sends the response back to the requesting client</li>
|
||||
* </ul>
|
||||
* If sending the response fails, the INS server receives an
|
||||
* {@code onResponseSentFailed(...)} callback.
|
||||
*
|
||||
* @param event The packet event received by the network system.
|
||||
*/
|
||||
@Listener(priority = EventPriority.HIGHEST)
|
||||
public void onPacketINS(S_PacketReadEvent event) {
|
||||
if (!(event.getPacket() instanceof INSQueryPacket q)) return;
|
||||
if (!server.getProtocolBridge().isRunningAsINSServer()) return;
|
||||
|
||||
ProtocolINSServer insServer = (ProtocolINSServer) server;
|
||||
|
||||
insServer.onQueryReceived(q.getTLN(), q.getName(), q.getSub(), q.getType());
|
||||
List<INSRecord> resolved = new ArrayList<>();
|
||||
INSResponseStatus status = null;
|
||||
|
||||
if (q.getSub() == null && q.getTLN().equalsIgnoreCase("oac")) {
|
||||
if (q.getName().equalsIgnoreCase("info")) {
|
||||
// Return INS server info site
|
||||
String insInfo = insServer.getInsInfoSite();
|
||||
if (!insInfo.contains(":")) insInfo = insInfo + ":1028";
|
||||
|
||||
String[] hostPort = insInfo.split(":");
|
||||
resolved = List.of(new INSRecord(q.getType(), hostPort[0], -1, -1, Integer.parseInt(hostPort[1]), 0));
|
||||
} else if (q.getName().equalsIgnoreCase("register")) {
|
||||
// Return INS frontend site
|
||||
String insFrontend = insServer.getInsFrontendSite();
|
||||
if (!insFrontend.contains(":")) insFrontend = insFrontend + ":1028";
|
||||
|
||||
String[] hostPort = insFrontend.split(":");
|
||||
resolved = List.of(new INSRecord(q.getType(), hostPort[0], -1, -1, Integer.parseInt(hostPort[1]), 0));
|
||||
} else {
|
||||
// Not a special name → use normal resolving
|
||||
resolved = insServer.resolve(q.getTLN(), q.getName(), q.getName(), q.getType());
|
||||
}
|
||||
} else if (q.getSub() == null && q.getName().equalsIgnoreCase("info")) {
|
||||
// Return TLN server info site
|
||||
String resolve = insServer.resolveTLNInfoSite(q.getTLN());
|
||||
if (resolve == null) status = INSResponseStatus.INVALID_REQUEST;
|
||||
else {
|
||||
if (!resolve.contains(":")) resolve = resolve + ":1028";
|
||||
String[] hostPort = resolve.split(":");
|
||||
resolved = List.of(new INSRecord(q.getType(), hostPort[0], -1, -1, Integer.parseInt(hostPort[1]), 0));
|
||||
}
|
||||
} else {
|
||||
// Normal resolving
|
||||
resolved = insServer.resolve(q.getTLN(), q.getName(), q.getSub(), q.getType());
|
||||
}
|
||||
|
||||
status = status == null && resolved.isEmpty() ? INSResponseStatus.NOT_FOUND : INSResponseStatus.OK;
|
||||
INSResponsePacket response = new INSResponsePacket(status, resolved, q.getClientId(), insServer.getProtocolBridge());
|
||||
|
||||
try {
|
||||
event.getClient().sendPacket(response, TransportProtocol.TCP);
|
||||
insServer.onResponseSent(q.getTLN(), q.getName(), q.getSub(), q.getType(), resolved);
|
||||
} catch (Exception e) {
|
||||
insServer.onResponseSentFailed(q.getTLN(), q.getName(), q.getSub(), q.getType(), resolved, e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
package org.openautonomousconnection.protocol.listeners;
|
||||
|
||||
import dev.unlegitdqrk.unlegitlibrary.event.EventListener;
|
||||
import dev.unlegitdqrk.unlegitlibrary.event.Listener;
|
||||
import dev.unlegitdqrk.unlegitlibrary.network.system.server.events.ConnectionHandlerConnectedEvent;
|
||||
import dev.unlegitdqrk.unlegitlibrary.network.system.server.events.ConnectionHandlerDisconnectedEvent;
|
||||
import dev.unlegitdqrk.unlegitlibrary.network.system.server.events.S_PacketReceivedEvent;
|
||||
import lombok.Getter;
|
||||
import org.openautonomousconnection.protocol.annotations.ProtocolInfo;
|
||||
import org.openautonomousconnection.protocol.packets.v1_0_0.beta.INSQueryPacket;
|
||||
import org.openautonomousconnection.protocol.packets.v1_0_0.beta.INSResponsePacket;
|
||||
import org.openautonomousconnection.protocol.side.ins.ConnectedProtocolClient;
|
||||
import org.openautonomousconnection.protocol.side.ins.ProtocolINSServer;
|
||||
import org.openautonomousconnection.protocol.versions.ProtocolVersion;
|
||||
import org.openautonomousconnection.protocol.versions.v1_0_0.beta.INSRecord;
|
||||
import org.openautonomousconnection.protocol.versions.v1_0_0.beta.INSRecordType;
|
||||
import org.openautonomousconnection.protocol.versions.v1_0_0.beta.INSResponseStatus;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Listener for INS server connection events.
|
||||
*/
|
||||
@ProtocolInfo(protocolSide = ProtocolVersion.ProtocolSide.INS)
|
||||
public final class INSServerListener extends EventListener {
|
||||
|
||||
/**
|
||||
* The reference to the INSServer object
|
||||
*/
|
||||
@Getter
|
||||
private ProtocolINSServer insServer;
|
||||
|
||||
/**
|
||||
* Sets the insServer variable
|
||||
*
|
||||
* @param insServer The Instance of the INSServer
|
||||
*/
|
||||
public void setINSServer(ProtocolINSServer insServer) {
|
||||
if (this.insServer != null) return;
|
||||
this.insServer = insServer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the event when a connection handler connects to the INS server.
|
||||
* Adds the connected client to the ProtocolBridge's INS server client list.
|
||||
*
|
||||
* @param event The connection handler connected event.
|
||||
*/
|
||||
@Listener
|
||||
public void onConnect(ConnectionHandlerConnectedEvent event) {
|
||||
insServer.getClients().add(new ConnectedProtocolClient(event.getConnectionHandler(), insServer));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the event when a connection handler disconnects from the INS server.
|
||||
* Removes the disconnected client from the ProtocolBridge's INS server client list.
|
||||
*
|
||||
* @param event The connection handler disconnected event.
|
||||
*/
|
||||
@Listener
|
||||
public void onDisconnect(ConnectionHandlerDisconnectedEvent event) {
|
||||
insServer.getClients().removeIf(client -> client.getConnectionHandler().getClientID() == -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles incoming INS query packets from connected clients.
|
||||
* <p>
|
||||
* This method performs the following steps:
|
||||
* <ul>
|
||||
* <li>Checks whether the received packet is an {@link INSQueryPacket}</li>
|
||||
* <li>Notifies the INS server through {@code onQueryReceived}</li>
|
||||
* <li>Resolves the request via {@code resolve()}</li>
|
||||
* <li>Wraps the result in an {@link INSResponsePacket}</li>
|
||||
* <li>Sends the response back to the requesting client</li>
|
||||
* </ul>
|
||||
* If sending the response fails, the INS server receives an
|
||||
* {@code onResponseSentFailed(...)} callback.
|
||||
*
|
||||
* @param event The packet event received by the network system.
|
||||
*/
|
||||
@Listener
|
||||
public void onPacket(S_PacketReceivedEvent event) {
|
||||
if (!(event.getPacket() instanceof INSQueryPacket q)) return;
|
||||
|
||||
insServer.onQueryReceived(q.tln, q.name, q.sub, q.type);
|
||||
List<INSRecord> resolved = new ArrayList<>();
|
||||
INSResponseStatus status = null;
|
||||
|
||||
if (q.sub == null && q.tln.equalsIgnoreCase("oac")) {
|
||||
if (q.name.equalsIgnoreCase("info")) {
|
||||
// Return INS server info site
|
||||
String[] hostPort = insServer.getINSInfoSite().split(":");
|
||||
resolved = List.of(new INSRecord(q.type, hostPort[0], -1, -1, Integer.parseInt(hostPort[1]), 0));
|
||||
} else if (q.name.equalsIgnoreCase("register")) {
|
||||
// Return INS frontend site
|
||||
String[] hostPort = insServer.getINSFrontendSite().split(":");
|
||||
resolved = List.of(new INSRecord(q.type, hostPort[0], -1, -1, Integer.parseInt(hostPort[1]), 0));
|
||||
} else {
|
||||
// Not a special name → use normal resolving
|
||||
resolved = insServer.resolve(q.tln, q.name, q.sub, q.type);
|
||||
}
|
||||
} else if (q.sub == null && q.name.equalsIgnoreCase("info")) {
|
||||
// Return TLN server info site
|
||||
String resolve = insServer.resolveTLNInfoSite(q.tln);
|
||||
if (resolve == null) status = INSResponseStatus.INVALID_REQUEST;
|
||||
else {
|
||||
String[] hostPort = resolve.split(":");
|
||||
resolved = List.of(new INSRecord(q.type, hostPort[0], -1, -1, Integer.parseInt(hostPort[1]), 0));
|
||||
}
|
||||
} else {
|
||||
// Normal resolving
|
||||
resolved = insServer.resolve(q.tln, q.name, q.sub, q.type);
|
||||
}
|
||||
|
||||
status = status == null && resolved.isEmpty() ? INSResponseStatus.NOT_FOUND : INSResponseStatus.OK;
|
||||
INSResponsePacket response = new INSResponsePacket(status, resolved, q.clientId, insServer.getProtocolBridge());
|
||||
|
||||
try {
|
||||
event.getConnectionHandler().sendPacket(response);
|
||||
insServer.onResponseSent(q.tln, q.name, q.sub, q.type, resolved);
|
||||
} catch (IOException | ClassNotFoundException e) {
|
||||
insServer.onResponseSentFailed(q.tln, q.name, q.sub, q.type, resolved, e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
package org.openautonomousconnection.protocol.listeners;
|
||||
|
||||
import dev.unlegitdqrk.unlegitlibrary.event.EventListener;
|
||||
import dev.unlegitdqrk.unlegitlibrary.event.Listener;
|
||||
import dev.unlegitdqrk.unlegitlibrary.network.system.server.events.ConnectionHandlerConnectedEvent;
|
||||
import dev.unlegitdqrk.unlegitlibrary.network.system.server.events.ConnectionHandlerDisconnectedEvent;
|
||||
import lombok.Getter;
|
||||
import org.openautonomousconnection.protocol.annotations.ProtocolInfo;
|
||||
import org.openautonomousconnection.protocol.side.web.ConnectedWebClient;
|
||||
import org.openautonomousconnection.protocol.side.web.ProtocolWebServer;
|
||||
import org.openautonomousconnection.protocol.versions.ProtocolVersion;
|
||||
|
||||
/**
|
||||
* Listener for web server connection events.
|
||||
*/
|
||||
@ProtocolInfo(protocolSide = ProtocolVersion.ProtocolSide.WEB)
|
||||
public final class WebServerListener extends EventListener {
|
||||
|
||||
/**
|
||||
* The reference to the ProtocolWebServer object
|
||||
*/
|
||||
@Getter
|
||||
private ProtocolWebServer webServer;
|
||||
|
||||
/**
|
||||
* Sets the webServer variable
|
||||
*
|
||||
* @param webServer The Instance of the ProtocolWebServer
|
||||
*/
|
||||
public void setWebServer(ProtocolWebServer webServer) {
|
||||
if (this.webServer != null) return;
|
||||
this.webServer = webServer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the event when a connection is established.
|
||||
* Adds the connected client to the protocol web server's client list.
|
||||
*
|
||||
* @param event The connection handler connected event.
|
||||
*/
|
||||
@Listener
|
||||
public void onConnect(ConnectionHandlerConnectedEvent event) {
|
||||
webServer.getClients().add(new ConnectedWebClient(event.getConnectionHandler()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the event when a connection is disconnected.
|
||||
* Removes the disconnected client from the protocol web server's client list.
|
||||
*
|
||||
* @param event The connection handler disconnected event.
|
||||
*/
|
||||
@Listener
|
||||
public void onDisconnect(ConnectionHandlerDisconnectedEvent event) {
|
||||
webServer.getClients().removeIf(client -> client.getPipelineConnection().getClientID() == -1);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
package org.openautonomousconnection.protocol.packets;
|
||||
|
||||
import dev.unlegitdqrk.unlegitlibrary.network.system.packets.Packet;
|
||||
import dev.unlegitdqrk.unlegitlibrary.network.system.packets.PacketHandler;
|
||||
import lombok.Getter;
|
||||
import org.openautonomousconnection.protocol.versions.ProtocolVersion;
|
||||
import org.openautonomousconnection.protocol.versions.v1_0_0.beta.INSResponseStatus;
|
||||
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.io.ObjectOutputStream;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Abstract class representing a packet in the Open Autonomous Connection (OAC) protocol.
|
||||
@@ -20,8 +20,8 @@ public abstract class OACPacket extends Packet {
|
||||
* The protocol version associated with this packet.
|
||||
*/
|
||||
@Getter
|
||||
private final ProtocolVersion protocolVersion;
|
||||
|
||||
private final List<ProtocolVersion> compatibleVersions;
|
||||
private final int id;
|
||||
/**
|
||||
* The response code for the packet, defaulting to RESPONSE_NOT_REQUIRED.
|
||||
*/
|
||||
@@ -31,11 +31,16 @@ public abstract class OACPacket extends Packet {
|
||||
* Constructor for OACPacket.
|
||||
*
|
||||
* @param id The unique identifier for the packet.
|
||||
* @param protocolVersion The protocol version associated with this packet.
|
||||
* @param supportedVersions The protocol version associated with this packet.
|
||||
*/
|
||||
public OACPacket(int id, ProtocolVersion protocolVersion) {
|
||||
super(id);
|
||||
this.protocolVersion = protocolVersion;
|
||||
public OACPacket(int id, ProtocolVersion... supportedVersions) {
|
||||
this.id = id;
|
||||
this.compatibleVersions = List.of(supportedVersions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPacketID() {
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -59,61 +64,100 @@ public abstract class OACPacket extends Packet {
|
||||
/**
|
||||
* Writes the packet data to the output stream.
|
||||
*
|
||||
* @param packetHandler The packet handler managing the packet.
|
||||
* @param objectOutputStream The output stream to write the packet data to.
|
||||
* @throws IOException If an I/O error occurs.
|
||||
* @throws ClassNotFoundException If a class cannot be found during serialization.
|
||||
* @param outputStream The output stream to write the packet data to.
|
||||
* @throws IOException If an I/O error occurs.
|
||||
*/
|
||||
@Override
|
||||
public final void write(PacketHandler packetHandler, ObjectOutputStream objectOutputStream) throws IOException, ClassNotFoundException {
|
||||
public final void write(DataOutputStream outputStream) throws IOException {
|
||||
// Write the specific packet data
|
||||
onWrite(packetHandler, objectOutputStream);
|
||||
onWrite(outputStream);
|
||||
|
||||
// Write the response code if the protocol version is not classic
|
||||
if (protocolVersion != ProtocolVersion.PV_1_0_0_CLASSIC) objectOutputStream.writeObject(responseCode);
|
||||
if (!compatibleVersions.contains(ProtocolVersion.PV_1_0_0_CLASSIC)) outputStream.writeUTF(responseCode.name());
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void read(PacketHandler packetHandler, ObjectInputStream objectInputStream) throws IOException, ClassNotFoundException {
|
||||
public final void read(DataInputStream inputStream, UUID clientID) throws IOException {
|
||||
// Read the specific packet data
|
||||
onRead(packetHandler, objectInputStream);
|
||||
onRead(inputStream, clientID);
|
||||
|
||||
// Read the response code if the protocol version is not classic
|
||||
if (protocolVersion != ProtocolVersion.PV_1_0_0_CLASSIC)
|
||||
responseCode = (INSResponseStatus) objectInputStream.readObject();
|
||||
if (!compatibleVersions.contains(ProtocolVersion.PV_1_0_0_CLASSIC))
|
||||
responseCode = INSResponseStatus.valueOf(inputStream.readUTF());
|
||||
else responseCode = INSResponseStatus.RESPONSE_NOT_REQUIRED;
|
||||
|
||||
// Call the response code read handler
|
||||
onResponseCodeRead(packetHandler, objectInputStream);
|
||||
onResponseCodeRead(inputStream, clientID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract method to be implemented by subclasses for writing specific packet data.
|
||||
*
|
||||
* @param packetHandler The packet handler managing the packet.
|
||||
* @param objectOutputStream The output stream to write the packet data to.
|
||||
* @throws IOException If an I/O error occurs.
|
||||
* @throws ClassNotFoundException If a class cannot be found during serialization.
|
||||
* @param outputStream The output stream to write the packet data to.
|
||||
* @throws IOException If an I/O error occurs.
|
||||
*/
|
||||
public abstract void onWrite(PacketHandler packetHandler, ObjectOutputStream objectOutputStream) throws IOException, ClassNotFoundException;
|
||||
public abstract void onWrite(DataOutputStream outputStream) throws IOException;
|
||||
|
||||
/**
|
||||
* Abstract method to be implemented by subclasses for reading specific packet data.
|
||||
*
|
||||
* @param packetHandler The packet handler managing the packet.
|
||||
* @param objectInputStream The input stream to read the packet data from.
|
||||
* @throws IOException If an I/O error occurs.
|
||||
* @throws ClassNotFoundException If a class cannot be found during deserialization.
|
||||
* @param inputStream The input stream to read the packet data from.
|
||||
* @throws IOException If an I/O error occurs.
|
||||
*/
|
||||
public abstract void onRead(PacketHandler packetHandler, ObjectInputStream objectInputStream) throws IOException, ClassNotFoundException;
|
||||
public abstract void onRead(DataInputStream inputStream, UUID clientID) throws IOException;
|
||||
|
||||
/**
|
||||
* Method called after the response code has been read from the input stream.
|
||||
* Subclasses can override this method to handle any additional logic based on the response code.
|
||||
*
|
||||
* @param packetHandler The packet handler managing the packet.
|
||||
* @param objectInputStream The input stream from which the response code was read.
|
||||
* @param inputStream The input stream from which the response code was read.
|
||||
*/
|
||||
protected void onResponseCodeRead(PacketHandler packetHandler, ObjectInputStream objectInputStream) {
|
||||
protected void onResponseCodeRead(DataInputStream inputStream, UUID clientID) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a string map in a deterministic way (no Java object serialization).
|
||||
*
|
||||
* @param out output stream
|
||||
* @param map map to write (may be null)
|
||||
* @throws IOException on I/O errors
|
||||
*/
|
||||
protected final void writeStringMap(DataOutputStream out, Map<String, String> map) throws IOException {
|
||||
if (map == null || map.isEmpty()) {
|
||||
out.writeInt(0);
|
||||
return;
|
||||
}
|
||||
|
||||
out.writeInt(map.size());
|
||||
for (Map.Entry<String, String> e : map.entrySet()) {
|
||||
// Null keys/values are normalized to empty strings to keep the wire format stable.
|
||||
out.writeUTF((e.getKey() != null) ? e.getKey() : "");
|
||||
out.writeUTF((e.getValue() != null) ? e.getValue() : "");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a string map in a deterministic way (no Java object serialization).
|
||||
*
|
||||
* @param in input stream
|
||||
* @return headers map (never null)
|
||||
* @throws IOException on I/O errors / invalid sizes
|
||||
*/
|
||||
protected final Map<String, String> readStringMap(DataInputStream in) throws IOException {
|
||||
int size = in.readInt();
|
||||
if (size < 0) {
|
||||
throw new IOException("Negative map size");
|
||||
}
|
||||
if (size == 0) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
Map<String, String> map = new LinkedHashMap<>(Math.max(16, size * 2));
|
||||
for (int i = 0; i < size; i++) {
|
||||
String key = in.readUTF();
|
||||
String value = in.readUTF();
|
||||
map.put(key, value);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +1,33 @@
|
||||
package org.openautonomousconnection.protocol.packets.v1_0_0.beta;
|
||||
|
||||
import dev.unlegitdqrk.unlegitlibrary.file.FileUtils;
|
||||
import dev.unlegitdqrk.unlegitlibrary.network.system.packets.PacketHandler;
|
||||
import dev.unlegitdqrk.unlegitlibrary.network.system.server.ConnectionHandler;
|
||||
import dev.unlegitdqrk.unlegitlibrary.network.system.server.ConnectedClient;
|
||||
import dev.unlegitdqrk.unlegitlibrary.network.system.server.NetworkServer;
|
||||
import dev.unlegitdqrk.unlegitlibrary.network.system.utils.TransportProtocol;
|
||||
import dev.unlegitdqrk.unlegitlibrary.network.utils.NetworkUtils;
|
||||
import org.openautonomousconnection.protocol.ProtocolBridge;
|
||||
import org.openautonomousconnection.protocol.packets.OACPacket;
|
||||
import org.openautonomousconnection.protocol.side.client.events.ConnectedToProtocolINSServerEvent;
|
||||
import org.openautonomousconnection.protocol.side.ins.ConnectedProtocolClient;
|
||||
import org.openautonomousconnection.protocol.side.ins.events.ConnectedProtocolClientEvent;
|
||||
import org.openautonomousconnection.protocol.side.web.ConnectedWebClient;
|
||||
import org.openautonomousconnection.protocol.side.client.events.ConnectedToProtocolServerEvent;
|
||||
import org.openautonomousconnection.protocol.side.server.CustomConnectedClient;
|
||||
import org.openautonomousconnection.protocol.side.server.events.S_CustomClientConnectedEvent;
|
||||
import org.openautonomousconnection.protocol.versions.ProtocolVersion;
|
||||
import org.openautonomousconnection.protocol.versions.v1_0_0.beta.INSResponseStatus;
|
||||
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.io.ObjectOutputStream;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Authentication packet used between client and INS/Web servers.
|
||||
* <p>
|
||||
* Responsibilities:
|
||||
*
|
||||
* <p>Responsibilities:
|
||||
* <ul>
|
||||
* <li>Client → Server: Sends client ID and protocol version</li>
|
||||
* <li>Server → Client: Sends CA key, CA certificate and CA serial files</li>
|
||||
* <li>Client → Server: Sends client connection id and protocol version</li>
|
||||
* <li>INS Server → Client: Sends CA key, CA certificate and CA serial files</li>
|
||||
* <li>Performs version compatibility validation</li>
|
||||
* <li>Triggers authentication callbacks on both sides</li>
|
||||
* </ul>
|
||||
@@ -39,134 +42,182 @@ public final class AuthPacket extends OACPacket {
|
||||
* @param protocolBridge The protocol context of the current instance.
|
||||
*/
|
||||
public AuthPacket(ProtocolBridge protocolBridge) {
|
||||
this();
|
||||
super(8, ProtocolVersion.PV_1_0_0_BETA, ProtocolVersion.PV_1_0_1_BETA);
|
||||
this.protocolBridge = protocolBridge;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registration constructor
|
||||
*/
|
||||
public AuthPacket() {
|
||||
super(4, ProtocolVersion.PV_1_0_0_BETA);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes authentication data to the output stream.
|
||||
* <p>
|
||||
* Behavior differs based on the running side:
|
||||
* <ul>
|
||||
* <li>INS Server → sends CA bundle to the client</li>
|
||||
* <li>Client → sends client ID + protocol version</li>
|
||||
* </ul>
|
||||
*/
|
||||
@Override
|
||||
public void onWrite(PacketHandler packetHandler, ObjectOutputStream objectOutputStream) throws IOException, ClassNotFoundException {
|
||||
public void onWrite(DataOutputStream objectOutputStream) throws IOException {
|
||||
if (protocolBridge.isRunningAsINSServer()) {
|
||||
objectOutputStream.writeObject(protocolBridge.getProtocolVersion());
|
||||
objectOutputStream.writeBoolean(true);
|
||||
objectOutputStream.writeUTF(protocolBridge.getProtocolValues().protocolVersion.name());
|
||||
|
||||
// Read ca files
|
||||
String caKey = "N/A";
|
||||
String caPem = "N/A";
|
||||
String caSrl = "N/A";
|
||||
try {
|
||||
objectOutputStream.writeUTF(protocolBridge.getProtocolINSServer().getFolderStructure().caPrefix + NetworkUtils.getPublicIPAddress());
|
||||
|
||||
caKey = FileUtils.readFileFull(new File(
|
||||
protocolBridge.getProtocolINSServer().getFolderStructure().privateCAFolder,
|
||||
protocolBridge.getProtocolINSServer().getFolderStructure().caPrefix + NetworkUtils.getPublicIPAddress() + ".key"));
|
||||
try {
|
||||
String caPrefix = protocolBridge.getProtocolServer().getFolderStructure().getCaPrefix()
|
||||
+ NetworkUtils.getPublicIPAddress();
|
||||
|
||||
objectOutputStream.writeUTF(caPrefix);
|
||||
|
||||
caPem = FileUtils.readFileFull(new File(
|
||||
protocolBridge.getProtocolINSServer().getFolderStructure().publicCAFolder,
|
||||
protocolBridge.getProtocolINSServer().getFolderStructure().caPrefix + NetworkUtils.getPublicIPAddress() + ".pem"));
|
||||
|
||||
caSrl = FileUtils.readFileFull(new File(
|
||||
protocolBridge.getProtocolINSServer().getFolderStructure().publicCAFolder,
|
||||
protocolBridge.getProtocolINSServer().getFolderStructure().caPrefix + NetworkUtils.getPublicIPAddress() + ".srl"));
|
||||
protocolBridge.getProtocolServer().getFolderStructure().publicCAFolder,
|
||||
caPrefix + ".pem"));
|
||||
} catch (Exception exception) {
|
||||
protocolBridge.getLogger().exception("Failed to read ca-files", exception);
|
||||
protocolBridge.getProtocolValues().logger.exception("Failed to read ca-files", exception);
|
||||
setResponseCode(INSResponseStatus.RESPONSE_AUTH_FAILED);
|
||||
}
|
||||
|
||||
// Send ca data
|
||||
objectOutputStream.writeUTF(caKey);
|
||||
objectOutputStream.writeUTF(caPem);
|
||||
objectOutputStream.writeUTF(caSrl);
|
||||
} else if (protocolBridge.isRunningAsClient()) {
|
||||
objectOutputStream.writeInt(protocolBridge.getProtocolClient().getClientINSConnection().getClientID());
|
||||
objectOutputStream.writeObject(protocolBridge.getProtocolVersion());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads authentication data and updates protocol state.
|
||||
* <p>
|
||||
* Behavior:
|
||||
* <ul>
|
||||
* <li>Server validates version and registers new connected client</li>
|
||||
* <li>Client saves received CA files and completes TLS initialization</li>
|
||||
* </ul>
|
||||
*/
|
||||
@Override
|
||||
public void onRead(PacketHandler packetHandler, ObjectInputStream objectInputStream) throws IOException, ClassNotFoundException {
|
||||
if (protocolBridge.isRunningAsINSServer() || protocolBridge.isRunningAsWebServer()) {
|
||||
int clientID = objectInputStream.readInt();
|
||||
ProtocolVersion clientVersion = (ProtocolVersion) objectInputStream.readObject();
|
||||
ConnectionHandler connectionHandler = protocolBridge.getProtocolINSServer().getNetworkServer().getConnectionHandlerByID(clientID);
|
||||
if (protocolBridge.isRunningAsServer()) {
|
||||
objectOutputStream.writeBoolean(false);
|
||||
objectOutputStream.writeUTF(protocolBridge.getProtocolValues().protocolVersion.name());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!protocolBridge.isVersionSupported(clientVersion)) {
|
||||
setResponseCode(INSResponseStatus.RESPONSE_AUTH_FAILED);
|
||||
connectionHandler.disconnect();
|
||||
return;
|
||||
} else setResponseCode(INSResponseStatus.RESPONSE_AUTH_SUCCESS);
|
||||
if (protocolBridge.isRunningAsClient()) {
|
||||
UUID clientConnectionId = null;
|
||||
|
||||
|
||||
if (protocolBridge.isRunningAsINSServer()) {
|
||||
ConnectedProtocolClient client = protocolBridge.getProtocolINSServer().getClientByID(clientID);
|
||||
client.setClientVersion(clientVersion);
|
||||
protocolBridge.getProtocolSettings().eventManager.executeEvent(new ConnectedProtocolClientEvent(client));
|
||||
} else {
|
||||
ConnectedWebClient client = protocolBridge.getProtocolWebServer().getClientByID(clientID);
|
||||
client.setClientVersion(clientVersion);
|
||||
}
|
||||
} else if (protocolBridge.isRunningAsClient()) {
|
||||
ProtocolVersion serverVersion = (ProtocolVersion) objectInputStream.readObject();
|
||||
|
||||
if (!protocolBridge.isVersionSupported(serverVersion)) {
|
||||
setResponseCode(INSResponseStatus.RESPONSE_AUTH_FAILED);
|
||||
protocolBridge.getProtocolClient().getClientINSConnection().disconnect();
|
||||
return;
|
||||
} else setResponseCode(INSResponseStatus.RESPONSE_AUTH_SUCCESS);
|
||||
|
||||
String caPrefix = objectInputStream.readUTF();
|
||||
|
||||
String caKey = objectInputStream.readUTF();
|
||||
String caPem = objectInputStream.readUTF();
|
||||
String caSrl = objectInputStream.readUTF();
|
||||
|
||||
if (caKey.equalsIgnoreCase("N/A") || caPem.equalsIgnoreCase("N/A") || caSrl.equalsIgnoreCase("N/A"))
|
||||
setResponseCode(INSResponseStatus.RESPONSE_AUTH_FAILED);
|
||||
else {
|
||||
|
||||
File caPemFile = new File(protocolBridge.getProtocolClient().getFolderStructure().publicCAFolder, caPrefix + ".pem");
|
||||
File caSrlFile = new File(protocolBridge.getProtocolClient().getFolderStructure().publicCAFolder, caPrefix + ".srl");
|
||||
File caKeyFile = new File(protocolBridge.getProtocolClient().getFolderStructure().privateCAFolder, caPrefix + ".key");
|
||||
|
||||
try {
|
||||
if (!caPemFile.exists()) caPemFile.createNewFile();
|
||||
if (!caSrlFile.exists()) caSrlFile.createNewFile();
|
||||
if (!caKeyFile.exists()) caKeyFile.createNewFile();
|
||||
|
||||
FileUtils.writeFile(caPemFile, caPem);
|
||||
FileUtils.writeFile(caSrlFile, caKey);
|
||||
FileUtils.writeFile(caKeyFile, caSrl);
|
||||
} catch (Exception exception) {
|
||||
protocolBridge.getLogger().exception("Failed to create/save ca-files", exception);
|
||||
setResponseCode(INSResponseStatus.RESPONSE_AUTH_FAILED);
|
||||
if (protocolBridge.getProtocolClient() != null) {
|
||||
if (protocolBridge.getProtocolClient().getClientINSConnection() != null && (protocolBridge.getProtocolClient().getClientServerConnection() == null)) {
|
||||
clientConnectionId = protocolBridge.getProtocolClient().getClientINSConnection().getUniqueID();
|
||||
} else if (protocolBridge.getProtocolClient().getClientServerConnection() != null) {
|
||||
clientConnectionId = protocolBridge.getProtocolClient().getClientServerConnection().getUniqueID();
|
||||
} else if (protocolBridge.getProtocolClient().getClientINSConnection() != null) {
|
||||
clientConnectionId = protocolBridge.getProtocolClient().getClientINSConnection().getUniqueID();
|
||||
}
|
||||
}
|
||||
|
||||
protocolBridge.getProtocolClient().setServerVersion(serverVersion);
|
||||
protocolBridge.getProtocolSettings().eventManager.executeEvent(new ConnectedToProtocolINSServerEvent(protocolBridge.getProtocolClient()));
|
||||
objectOutputStream.writeUTF(clientConnectionId.toString());
|
||||
objectOutputStream.writeUTF(protocolBridge.getProtocolValues().protocolVersion.name());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRead(DataInputStream objectInputStream, UUID id) throws IOException {
|
||||
if (protocolBridge.isRunningAsServer()) {
|
||||
UUID clientID = UUID.fromString(objectInputStream.readUTF());
|
||||
ProtocolVersion clientVersion = ProtocolVersion.valueOf(objectInputStream.readUTF());
|
||||
|
||||
ConnectedClient connectionHandler = getConnection(protocolBridge.getProtocolServer().getNetwork(), clientID);
|
||||
if (connectionHandler == null) {
|
||||
setResponseCode(INSResponseStatus.RESPONSE_AUTH_FAILED);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!protocolBridge.isVersionSupported(clientVersion)) {
|
||||
setResponseCode(INSResponseStatus.RESPONSE_AUTH_FAILED);
|
||||
try {
|
||||
connectionHandler.disconnect();
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
setResponseCode(INSResponseStatus.RESPONSE_AUTH_SUCCESS);
|
||||
|
||||
CustomConnectedClient client = protocolBridge.getProtocolServer().getClientByID(clientID);
|
||||
client.setClientVersion(clientVersion);
|
||||
protocolBridge.getProtocolValues().eventManager.executeEvent(new S_CustomClientConnectedEvent(client));
|
||||
client.getConnection().sendPacket(new AuthPacket(protocolBridge), TransportProtocol.TCP);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (protocolBridge.isRunningAsClient()) {
|
||||
boolean fromINS = objectInputStream.readBoolean();
|
||||
ProtocolVersion serverVersion = ProtocolVersion.valueOf(objectInputStream.readUTF());
|
||||
|
||||
if (!fromINS) {
|
||||
protocolBridge.getProtocolClient().setServerVersion(serverVersion);
|
||||
protocolBridge.getProtocolValues().eventManager.executeEvent(
|
||||
new ConnectedToProtocolServerEvent(protocolBridge.getProtocolClient())
|
||||
);
|
||||
} else {
|
||||
if (!protocolBridge.isVersionSupported(serverVersion)) {
|
||||
setResponseCode(INSResponseStatus.RESPONSE_AUTH_FAILED);
|
||||
if (protocolBridge.getProtocolClient() != null && protocolBridge.getProtocolClient().getClientINSConnection() != null) {
|
||||
protocolBridge.getProtocolClient().getClientINSConnection().disconnect();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setResponseCode(INSResponseStatus.RESPONSE_AUTH_SUCCESS);
|
||||
|
||||
String caPrefix = objectInputStream.readUTF();
|
||||
String caPem = objectInputStream.readUTF();
|
||||
|
||||
if (caPem.equalsIgnoreCase("N/A")) {
|
||||
setResponseCode(INSResponseStatus.RESPONSE_AUTH_FAILED);
|
||||
protocolBridge.getProtocolClient().getClientINSConnection().disconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
byte[] caBytes = caPem.getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
||||
String fp = "N/A";
|
||||
|
||||
try {
|
||||
java.security.MessageDigest md = java.security.MessageDigest.getInstance("SHA-256");
|
||||
fp = java.util.HexFormat.of().formatHex(md.digest(caBytes));
|
||||
} catch (NoSuchAlgorithmException ignored) {
|
||||
setResponseCode(INSResponseStatus.RESPONSE_AUTH_FAILED);
|
||||
protocolBridge.getProtocolClient().getClientINSConnection().disconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
File caPemFile = new File(protocolBridge.getProtocolClient().getFolderStructure().publicCAFolder, caPrefix + ".pem");
|
||||
|
||||
File fpFile = new File(
|
||||
protocolBridge.getProtocolClient().getFolderStructure().publicCAFolder,
|
||||
caPrefix + ".fp");
|
||||
|
||||
if (!fpFile.exists()) {
|
||||
if (!protocolBridge.getProtocolClient().trustINS(fp)) {
|
||||
setResponseCode(INSResponseStatus.RESPONSE_AUTH_FAILED);
|
||||
protocolBridge.getProtocolClient().getClientINSConnection().disconnect();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
String existing = FileUtils.readFileLines(fpFile).getFirst();
|
||||
if (!existing.equalsIgnoreCase(fp)) {
|
||||
if (!protocolBridge.getProtocolClient().trustNewINSFingerprint(existing, fp)) {
|
||||
setResponseCode(INSResponseStatus.RESPONSE_AUTH_FAILED);
|
||||
protocolBridge.getProtocolClient().getClientINSConnection().disconnect();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FileUtils.writeFile(fpFile, fp + System.lineSeparator());
|
||||
|
||||
try {
|
||||
FileUtils.writeFile(caPemFile, caPem);
|
||||
} catch (Exception exception) {
|
||||
protocolBridge.getProtocolValues().logger.exception("Failed to create/save ca-files", exception);
|
||||
setResponseCode(INSResponseStatus.RESPONSE_AUTH_FAILED);
|
||||
}
|
||||
|
||||
protocolBridge.getProtocolClient().setInsVersion(serverVersion);
|
||||
protocolBridge.getProtocolValues().eventManager.executeEvent(
|
||||
new ConnectedToProtocolINSServerEvent(protocolBridge.getProtocolClient())
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ConnectedClient getConnection(NetworkServer server, UUID connectionId) {
|
||||
if (server == null || connectionId == null) return null;
|
||||
|
||||
for (ConnectedClient connection : server.getConnectedClients()) {
|
||||
if (connection != null && connection.getUniqueID().equals(connectionId)) {
|
||||
return connection;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
package org.openautonomousconnection.protocol.packets.v1_0_0.beta;
|
||||
|
||||
import dev.unlegitdqrk.unlegitlibrary.network.system.packets.PacketHandler;
|
||||
import lombok.Getter;
|
||||
import org.openautonomousconnection.protocol.packets.OACPacket;
|
||||
import org.openautonomousconnection.protocol.versions.ProtocolVersion;
|
||||
import org.openautonomousconnection.protocol.versions.v1_0_0.beta.INSRecordType;
|
||||
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.io.ObjectOutputStream;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Packet used by clients to query INS records from an INS server.
|
||||
@@ -23,11 +24,16 @@ import java.io.ObjectOutputStream;
|
||||
*/
|
||||
public final class INSQueryPacket extends OACPacket {
|
||||
|
||||
public String tln;
|
||||
public String name;
|
||||
public String sub;
|
||||
public INSRecordType type;
|
||||
public int clientId;
|
||||
@Getter
|
||||
private String TLN;
|
||||
@Getter
|
||||
private String name;
|
||||
@Getter
|
||||
private String sub;
|
||||
@Getter
|
||||
private INSRecordType type;
|
||||
@Getter
|
||||
private UUID clientId;
|
||||
|
||||
/**
|
||||
* Creates a new INS query packet with all required parameters.
|
||||
@@ -38,9 +44,9 @@ public final class INSQueryPacket extends OACPacket {
|
||||
* @param type Record type requested.
|
||||
* @param clientId Sender client ID for routing.
|
||||
*/
|
||||
public INSQueryPacket(String tln, String name, String sub, INSRecordType type, int clientId) {
|
||||
super(6, ProtocolVersion.PV_1_0_0_BETA);
|
||||
this.tln = tln;
|
||||
public INSQueryPacket(String tln, String name, String sub, INSRecordType type, UUID clientId) {
|
||||
this();
|
||||
this.TLN = tln;
|
||||
this.name = name;
|
||||
this.sub = sub;
|
||||
this.type = type;
|
||||
@@ -51,36 +57,36 @@ public final class INSQueryPacket extends OACPacket {
|
||||
* Registration constructor
|
||||
*/
|
||||
public INSQueryPacket() {
|
||||
super(6, ProtocolVersion.PV_1_0_0_BETA);
|
||||
super(7, ProtocolVersion.PV_1_0_0_BETA, ProtocolVersion.PV_1_0_1_BETA);
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes the INS query into the stream.
|
||||
*/
|
||||
@Override
|
||||
public void onWrite(PacketHandler handler, ObjectOutputStream out) throws IOException {
|
||||
out.writeUTF(tln);
|
||||
public void onWrite(DataOutputStream out) throws IOException {
|
||||
out.writeUTF(TLN);
|
||||
out.writeUTF(name);
|
||||
|
||||
out.writeBoolean(sub != null);
|
||||
if (sub != null) out.writeUTF(sub);
|
||||
|
||||
out.writeObject(type);
|
||||
out.writeInt(clientId);
|
||||
out.writeUTF(type.name());
|
||||
out.writeUTF(clientId.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserializes the INS query from the stream.
|
||||
*/
|
||||
@Override
|
||||
public void onRead(PacketHandler handler, ObjectInputStream in) throws IOException, ClassNotFoundException {
|
||||
tln = in.readUTF();
|
||||
public void onRead(DataInputStream in, UUID clientID) throws IOException {
|
||||
TLN = in.readUTF();
|
||||
name = in.readUTF();
|
||||
|
||||
boolean hasSub = in.readBoolean();
|
||||
sub = hasSub ? in.readUTF() : null;
|
||||
|
||||
type = (INSRecordType) in.readObject();
|
||||
clientId = in.readInt();
|
||||
type = INSRecordType.valueOf(in.readUTF());
|
||||
clientId = UUID.fromString(in.readUTF());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
package org.openautonomousconnection.protocol.packets.v1_0_0.beta;
|
||||
|
||||
import dev.unlegitdqrk.unlegitlibrary.network.system.packets.PacketHandler;
|
||||
import lombok.Getter;
|
||||
import org.openautonomousconnection.protocol.ProtocolBridge;
|
||||
import org.openautonomousconnection.protocol.packets.OACPacket;
|
||||
import org.openautonomousconnection.protocol.versions.ProtocolVersion;
|
||||
import org.openautonomousconnection.protocol.versions.v1_0_0.beta.INSRecord;
|
||||
import org.openautonomousconnection.protocol.versions.v1_0_0.beta.INSResponseStatus;
|
||||
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.io.ObjectOutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Response packet returned by an INS server after resolving a query.
|
||||
@@ -27,10 +28,14 @@ import java.util.List;
|
||||
*/
|
||||
public final class INSResponsePacket extends OACPacket {
|
||||
|
||||
@Getter
|
||||
private final ProtocolBridge bridge;
|
||||
public INSResponseStatus status;
|
||||
public List<INSRecord> records;
|
||||
public int clientId;
|
||||
@Getter
|
||||
private INSResponseStatus status;
|
||||
@Getter
|
||||
private List<INSRecord> records;
|
||||
@Getter
|
||||
private UUID clientId;
|
||||
|
||||
/**
|
||||
* Creates a populated response packet.
|
||||
@@ -40,12 +45,11 @@ public final class INSResponsePacket extends OACPacket {
|
||||
* @param clientId ID of requesting client.
|
||||
* @param bridge Protocol runtime context.
|
||||
*/
|
||||
public INSResponsePacket(INSResponseStatus status, List<INSRecord> records, int clientId, ProtocolBridge bridge) {
|
||||
super(7, ProtocolVersion.PV_1_0_0_BETA);
|
||||
public INSResponsePacket(INSResponseStatus status, List<INSRecord> records, UUID clientId, ProtocolBridge bridge) {
|
||||
this(bridge);
|
||||
this.status = status;
|
||||
this.records = records;
|
||||
this.clientId = clientId;
|
||||
this.bridge = bridge;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -54,7 +58,7 @@ public final class INSResponsePacket extends OACPacket {
|
||||
* @param bridge Protocol runtime context.
|
||||
*/
|
||||
public INSResponsePacket(ProtocolBridge bridge) {
|
||||
super(7, ProtocolVersion.PV_1_0_0_BETA);
|
||||
super(6, ProtocolVersion.PV_1_0_0_BETA, ProtocolVersion.PV_1_0_1_BETA);
|
||||
this.bridge = bridge;
|
||||
}
|
||||
|
||||
@@ -62,32 +66,32 @@ public final class INSResponsePacket extends OACPacket {
|
||||
* Serializes the response status, records and client ID.
|
||||
*/
|
||||
@Override
|
||||
public void onWrite(PacketHandler handler, ObjectOutputStream out) throws IOException {
|
||||
out.writeObject(status);
|
||||
public void onWrite(DataOutputStream out) throws IOException {
|
||||
out.writeUTF(status.name());
|
||||
out.writeInt(records.size());
|
||||
|
||||
for (INSRecord rec : records) {
|
||||
out.writeObject(rec);
|
||||
writeObject(out, rec);
|
||||
}
|
||||
|
||||
out.writeInt(clientId);
|
||||
out.writeUTF(clientId.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserializes the response, reconstructing the record list.
|
||||
*/
|
||||
@Override
|
||||
public void onRead(PacketHandler handler, ObjectInputStream in) throws IOException, ClassNotFoundException {
|
||||
status = (INSResponseStatus) in.readObject();
|
||||
public void onRead(DataInputStream in, UUID clientID) throws IOException {
|
||||
status = INSResponseStatus.valueOf(in.readUTF());
|
||||
|
||||
int size = in.readInt();
|
||||
records = new ArrayList<>(size);
|
||||
|
||||
for (int i = 0; i < size; i++) {
|
||||
records.add((INSRecord) in.readObject());
|
||||
records.add((INSRecord) readObject(in));
|
||||
}
|
||||
|
||||
clientId = in.readInt();
|
||||
clientId = UUID.fromString(in.readUTF());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -96,7 +100,7 @@ public final class INSResponsePacket extends OACPacket {
|
||||
* If running on a client, forwards the result to the client-side API.
|
||||
*/
|
||||
@Override
|
||||
protected void onResponseCodeRead(PacketHandler handler, ObjectInputStream in) {
|
||||
protected void onResponseCodeRead(DataInputStream in, UUID clientID) {
|
||||
if (bridge.isRunningAsClient()) {
|
||||
bridge.getProtocolClient().onResponse(status, records);
|
||||
}
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
package org.openautonomousconnection.protocol.packets.v1_0_0.beta;
|
||||
|
||||
import dev.unlegitdqrk.unlegitlibrary.network.system.packets.PacketHandler;
|
||||
import org.openautonomousconnection.protocol.ProtocolBridge;
|
||||
import org.openautonomousconnection.protocol.packets.OACPacket;
|
||||
import org.openautonomousconnection.protocol.versions.ProtocolVersion;
|
||||
import org.openautonomousconnection.protocol.versions.v1_0_0.beta.INSResponseStatus;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.io.ObjectOutputStream;
|
||||
|
||||
/**
|
||||
* Internal compatibility packet used when a classic-protocol packet is received
|
||||
* but not supported by the current protocol version.
|
||||
* <p>
|
||||
* Instead of rejecting the packet entirely, the content and class name are forwarded
|
||||
* to the appropriate compatibility handler:
|
||||
* <ul>
|
||||
* <li>{@link org.openautonomousconnection.protocol.versions.v1_0_0.classic.handlers.ClassicHandlerINSServer}</li>
|
||||
* <li>{@link org.openautonomousconnection.protocol.versions.v1_0_0.classic.handlers.ClassicHandlerClient}</li>
|
||||
* <li>{@link org.openautonomousconnection.protocol.versions.v1_0_0.classic.handlers.ClassicHandlerWebServer}</li>
|
||||
* </ul>
|
||||
*/
|
||||
public final class UnsupportedClassicPacket extends OACPacket {
|
||||
private Class<? extends OACPacket> unsupportedClassicPacket;
|
||||
private Object[] content;
|
||||
private ProtocolBridge protocolBridge;
|
||||
|
||||
/**
|
||||
* Constructs a packet describing the unsupported classic packet and its content.
|
||||
*
|
||||
* @param unsupportedClassicPacket The packet class that was not understood.
|
||||
* @param content Serialized field values.
|
||||
* @param protocolBridge The protocol context.
|
||||
*/
|
||||
public UnsupportedClassicPacket(Class<? extends OACPacket> unsupportedClassicPacket, Object[] content, ProtocolBridge protocolBridge) {
|
||||
this();
|
||||
this.unsupportedClassicPacket = unsupportedClassicPacket;
|
||||
this.content = content;
|
||||
this.protocolBridge = protocolBridge;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registration Constructor
|
||||
*/
|
||||
public UnsupportedClassicPacket() {
|
||||
super(5, ProtocolVersion.PV_1_0_0_BETA);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the class name and serialized object fields to the stream.
|
||||
*/
|
||||
@Override
|
||||
public void onWrite(PacketHandler packetHandler, ObjectOutputStream objectOutputStream) throws IOException, ClassNotFoundException {
|
||||
if (protocolBridge.isRunningAsClient())
|
||||
objectOutputStream.writeInt(protocolBridge.getProtocolClient().getClientINSConnection().getClientID());
|
||||
|
||||
objectOutputStream.writeUTF(unsupportedClassicPacket.getName());
|
||||
objectOutputStream.writeInt(content.length);
|
||||
for (Object o : content) objectOutputStream.writeObject(o);
|
||||
setResponseCode(INSResponseStatus.RESPONSE_NOT_REQUIRED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the unsupported packet data and forwards it to the appropriate compatibility handler.
|
||||
*/
|
||||
@Override
|
||||
public void onRead(PacketHandler packetHandler, ObjectInputStream objectInputStream) throws IOException, ClassNotFoundException {
|
||||
int clientID = 0;
|
||||
if (protocolBridge.isRunningAsINSServer()) clientID = objectInputStream.readInt();
|
||||
String className = objectInputStream.readUTF();
|
||||
int size = objectInputStream.readInt();
|
||||
content = new Object[size];
|
||||
|
||||
for (int i = 0; i < size; i++) {
|
||||
content[i] = objectInputStream.readObject();
|
||||
}
|
||||
|
||||
if (protocolBridge.isRunningAsINSServer())
|
||||
protocolBridge.getClassicHandlerINSServer().unsupportedClassicPacket(className, content, protocolBridge.getProtocolINSServer().getClientByID(clientID));
|
||||
else if (protocolBridge.isRunningAsClient())
|
||||
protocolBridge.getClassicHandlerClient().unsupportedClassicPacket(className, content);
|
||||
else if (protocolBridge.isRunningAsWebServer())
|
||||
protocolBridge.getClassicHandlerWebServer().unsupportedClassicPacket(className, content, protocolBridge.getProtocolINSServer().getClientByID(clientID));
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package org.openautonomousconnection.protocol.packets.v1_0_0.beta.web;
|
||||
|
||||
import lombok.Getter;
|
||||
import org.openautonomousconnection.protocol.packets.OACPacket;
|
||||
import org.openautonomousconnection.protocol.versions.ProtocolVersion;
|
||||
import org.openautonomousconnection.protocol.versions.v1_0_0.beta.WebRequestMethod;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* Represents a web request in OAC Web protocol.
|
||||
*
|
||||
* <p>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.</p>
|
||||
*/
|
||||
public final class WebRequestPacket extends OACPacket {
|
||||
|
||||
@Getter
|
||||
private String path;
|
||||
|
||||
@Getter
|
||||
private WebRequestMethod method;
|
||||
|
||||
@Getter
|
||||
private Map<String, String> headers;
|
||||
|
||||
@Getter
|
||||
private byte[] body;
|
||||
|
||||
/**
|
||||
* Creates an empty request packet (used by PacketHandler factory).
|
||||
*/
|
||||
public WebRequestPacket() {
|
||||
super(10, ProtocolVersion.PV_1_0_0_BETA);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a request packet.
|
||||
*
|
||||
* @param path request path (e.g. "/index.html")
|
||||
* @param method request method
|
||||
* @param headers request headers (may be null)
|
||||
* @param body request body (may be null)
|
||||
*/
|
||||
public WebRequestPacket(String path, WebRequestMethod method, Map<String, String> headers, byte[] body) {
|
||||
this();
|
||||
this.path = (path != null) ? path : "/";
|
||||
this.method = (method != null) ? method : WebRequestMethod.GET;
|
||||
this.headers = (headers != null) ? new LinkedHashMap<>(headers) : Collections.emptyMap();
|
||||
this.body = (body != null) ? body : new byte[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWrite(DataOutputStream out) throws IOException {
|
||||
out.writeUTF((path != null) ? path : "/");
|
||||
out.writeUTF((method != null) ? method.name() : WebRequestMethod.GET.name());
|
||||
|
||||
writeStringMap(out, headers);
|
||||
|
||||
byte[] b = (body != null) ? body : new byte[0];
|
||||
out.writeInt(b.length);
|
||||
out.write(b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRead(DataInputStream in, UUID clientID) throws IOException {
|
||||
this.path = in.readUTF();
|
||||
this.method = WebRequestMethod.valueOf(in.readUTF());
|
||||
|
||||
this.headers = readStringMap(in);
|
||||
|
||||
int len = in.readInt();
|
||||
if (len < 0) {
|
||||
throw new IOException("Negative body length in WebRequestPacket");
|
||||
}
|
||||
this.body = in.readNBytes(len);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package org.openautonomousconnection.protocol.packets.v1_0_0.beta.web;
|
||||
|
||||
import lombok.Getter;
|
||||
import org.openautonomousconnection.protocol.packets.OACPacket;
|
||||
import org.openautonomousconnection.protocol.versions.ProtocolVersion;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* Represents a web response in OAC Web protocol.
|
||||
*
|
||||
* <p>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.</p>
|
||||
*/
|
||||
public final class WebResponsePacket extends OACPacket {
|
||||
|
||||
@Getter
|
||||
private int statusCode;
|
||||
|
||||
@Getter
|
||||
private String contentType;
|
||||
|
||||
@Getter
|
||||
private Map<String, String> headers;
|
||||
|
||||
@Getter
|
||||
private byte[] body;
|
||||
|
||||
/**
|
||||
* Creates an empty response packet (used by PacketHandler factory).
|
||||
*/
|
||||
public WebResponsePacket() {
|
||||
super(9, ProtocolVersion.PV_1_0_0_BETA);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a response packet.
|
||||
*
|
||||
* @param statusCode HTTP-like status code (e.g. 200, 404, 500)
|
||||
* @param contentType MIME type (e.g. "text/html")
|
||||
* @param headers response headers (may be null)
|
||||
* @param body response body (may be null)
|
||||
*/
|
||||
public WebResponsePacket(int statusCode, String contentType, Map<String, String> headers, byte[] body) {
|
||||
this();
|
||||
this.statusCode = statusCode;
|
||||
this.contentType = (contentType != null) ? contentType : "text/plain";
|
||||
this.headers = (headers != null) ? new LinkedHashMap<>(headers) : Collections.emptyMap();
|
||||
this.body = (body != null) ? body : new byte[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWrite(DataOutputStream out) throws IOException {
|
||||
out.writeInt(statusCode);
|
||||
out.writeUTF((contentType != null) ? contentType : "text/plain");
|
||||
|
||||
writeStringMap(out, headers);
|
||||
|
||||
byte[] b = (body != null) ? body : new byte[0];
|
||||
out.writeInt(b.length);
|
||||
out.write(b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRead(DataInputStream in, UUID clientID) throws IOException {
|
||||
this.statusCode = in.readInt();
|
||||
this.contentType = in.readUTF();
|
||||
|
||||
this.headers = readStringMap(in);
|
||||
|
||||
int len = in.readInt();
|
||||
if (len < 0) {
|
||||
throw new IOException("Negative body length in WebResponsePacket");
|
||||
}
|
||||
this.body = in.readNBytes(len);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.stream;
|
||||
|
||||
import lombok.Getter;
|
||||
import org.openautonomousconnection.protocol.packets.OACPacket;
|
||||
import org.openautonomousconnection.protocol.versions.ProtocolVersion;
|
||||
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
|
||||
public final class WebStreamChunkPacket_v1_0_0_B extends OACPacket {
|
||||
|
||||
@Getter
|
||||
private int seq;
|
||||
@Getter
|
||||
private byte[] data;
|
||||
|
||||
public WebStreamChunkPacket_v1_0_0_B() {
|
||||
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];
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWrite(DataOutputStream out) throws IOException {
|
||||
out.writeInt(seq);
|
||||
out.writeInt(data.length);
|
||||
out.write(data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRead(DataInputStream in, UUID clientID) throws IOException {
|
||||
seq = in.readInt();
|
||||
int len = in.readInt();
|
||||
if (len < 0) throw new IOException("Negative chunk length");
|
||||
data = in.readNBytes(len);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.stream;
|
||||
|
||||
import lombok.Getter;
|
||||
import org.openautonomousconnection.protocol.packets.OACPacket;
|
||||
import org.openautonomousconnection.protocol.versions.ProtocolVersion;
|
||||
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
|
||||
public final class WebStreamEndPacket_v1_0_0_B extends OACPacket {
|
||||
|
||||
@Getter
|
||||
private boolean ok;
|
||||
|
||||
public WebStreamEndPacket_v1_0_0_B() {
|
||||
super(13, ProtocolVersion.PV_1_0_0_BETA);
|
||||
}
|
||||
|
||||
public WebStreamEndPacket_v1_0_0_B(boolean ok) {
|
||||
this();
|
||||
this.ok = ok;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWrite(DataOutputStream out) throws IOException {
|
||||
out.writeBoolean(ok);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRead(DataInputStream in, UUID clientID) throws IOException {
|
||||
ok = in.readBoolean();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.stream;
|
||||
|
||||
import lombok.Getter;
|
||||
import org.openautonomousconnection.protocol.packets.OACPacket;
|
||||
import org.openautonomousconnection.protocol.versions.ProtocolVersion;
|
||||
|
||||
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 streaming response.
|
||||
*
|
||||
* <p>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.</p>
|
||||
*/
|
||||
public final class WebStreamStartPacket_v1_0_0_B extends OACPacket {
|
||||
|
||||
@Getter
|
||||
private int statusCode;
|
||||
|
||||
@Getter
|
||||
private String contentType;
|
||||
|
||||
@Getter
|
||||
private Map<String, String> headers;
|
||||
|
||||
@Getter
|
||||
private long totalLength;
|
||||
|
||||
/**
|
||||
* Creates an empty start packet (used by PacketHandler factory).
|
||||
*/
|
||||
public WebStreamStartPacket_v1_0_0_B() {
|
||||
super(11, ProtocolVersion.PV_1_0_0_BETA);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a start packet.
|
||||
*
|
||||
* @param statusCode status code
|
||||
* @param contentType content type
|
||||
* @param headers headers (may be null)
|
||||
* @param totalLength total length of the stream (may be -1 if unknown)
|
||||
*/
|
||||
public WebStreamStartPacket_v1_0_0_B(int statusCode, String contentType, Map<String, String> headers, long totalLength) {
|
||||
this();
|
||||
this.statusCode = statusCode;
|
||||
this.contentType = (contentType != null) ? contentType : "application/octet-stream";
|
||||
this.headers = (headers != null) ? new LinkedHashMap<>(headers) : Collections.emptyMap();
|
||||
this.totalLength = totalLength;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWrite(DataOutputStream out) throws IOException {
|
||||
out.writeInt(statusCode);
|
||||
out.writeUTF((contentType != null) ? contentType : "application/octet-stream");
|
||||
|
||||
writeStringMap(out, headers);
|
||||
|
||||
out.writeLong(totalLength);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRead(DataInputStream in, UUID clientID) throws IOException {
|
||||
this.statusCode = in.readInt();
|
||||
this.contentType = in.readUTF();
|
||||
|
||||
this.headers = readStringMap(in);
|
||||
|
||||
this.totalLength = in.readLong();
|
||||
}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
package org.openautonomousconnection.protocol.packets.v1_0_0.classic;
|
||||
|
||||
import dev.unlegitdqrk.unlegitlibrary.network.system.packets.PacketHandler;
|
||||
import org.openautonomousconnection.protocol.ProtocolBridge;
|
||||
import org.openautonomousconnection.protocol.packets.OACPacket;
|
||||
import org.openautonomousconnection.protocol.packets.v1_0_0.beta.UnsupportedClassicPacket;
|
||||
import org.openautonomousconnection.protocol.versions.ProtocolVersion;
|
||||
import org.openautonomousconnection.protocol.versions.v1_0_0.classic.events.Classic_DomainPacketReceivedEvent;
|
||||
import org.openautonomousconnection.protocol.versions.v1_0_0.classic.objects.Classic_Domain;
|
||||
import org.openautonomousconnection.protocol.versions.v1_0_0.classic.objects.Classic_RequestDomain;
|
||||
import org.openautonomousconnection.protocol.versions.v1_0_0.classic.utils.Classic_ProtocolVersion;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.io.ObjectOutputStream;
|
||||
import java.sql.SQLException;
|
||||
|
||||
public final class Classic_DomainPacket extends OACPacket {
|
||||
private Classic_RequestDomain requestDomain;
|
||||
private Classic_Domain domain;
|
||||
private int clientID;
|
||||
private ProtocolBridge bridge;
|
||||
|
||||
public Classic_DomainPacket(int toClient, Classic_RequestDomain requestDomain, Classic_Domain domain, ProtocolBridge protocolBridge) {
|
||||
this();
|
||||
this.clientID = toClient;
|
||||
this.bridge = protocolBridge;
|
||||
|
||||
this.requestDomain = requestDomain;
|
||||
this.domain = domain;
|
||||
}
|
||||
|
||||
// Registration constructor
|
||||
public Classic_DomainPacket() {
|
||||
super(2, ProtocolVersion.PV_1_0_0_CLASSIC);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWrite(PacketHandler packetHandler, ObjectOutputStream objectOutputStream) throws IOException, ClassNotFoundException {
|
||||
if (bridge.isRunningAsINSServer()) {
|
||||
objectOutputStream.writeInt(clientID);
|
||||
objectOutputStream.writeObject(requestDomain);
|
||||
objectOutputStream.writeObject(domain);
|
||||
} else if (bridge.isRunningAsClient()) {
|
||||
clientID = bridge.getProtocolClient().getClientINSConnection().getClientID();
|
||||
objectOutputStream.writeInt(clientID);
|
||||
objectOutputStream.writeObject(requestDomain);
|
||||
}
|
||||
|
||||
objectOutputStream.writeObject(Classic_ProtocolVersion.PV_1_0_0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRead(PacketHandler packetHandler, ObjectInputStream objectInputStream) throws IOException, ClassNotFoundException {
|
||||
if (bridge.isRunningAsINSServer()) {
|
||||
clientID = objectInputStream.readInt();
|
||||
requestDomain = (Classic_RequestDomain) objectInputStream.readObject();
|
||||
Classic_ProtocolVersion protocolVersion = (Classic_ProtocolVersion) objectInputStream.readObject();
|
||||
|
||||
try {
|
||||
domain = bridge.getClassicHandlerINSServer().getDomain(requestDomain);
|
||||
} catch (SQLException exception) {
|
||||
exception.printStackTrace();
|
||||
}
|
||||
|
||||
bridge.getProtocolINSServer().getNetworkServer().getEventManager().executeEvent(new Classic_DomainPacketReceivedEvent(protocolVersion, domain, requestDomain, clientID));
|
||||
|
||||
if (bridge.getProtocolINSServer().getClientByID(clientID).supportClientClassic())
|
||||
bridge.getProtocolINSServer().getNetworkServer().getConnectionHandlerByID(clientID).sendPacket(new Classic_DomainPacket(clientID, requestDomain, domain, bridge));
|
||||
else
|
||||
bridge.getProtocolINSServer().getNetworkServer().getConnectionHandlerByID(clientID).sendPacket(new UnsupportedClassicPacket(Classic_PingPacket.class, new Object[]{clientID, requestDomain, domain}, bridge));
|
||||
} else if (bridge.isRunningAsClient()) {
|
||||
clientID = objectInputStream.readInt();
|
||||
requestDomain = (Classic_RequestDomain) objectInputStream.readObject();
|
||||
domain = (Classic_Domain) objectInputStream.readObject();
|
||||
Classic_ProtocolVersion protocolVersion = (Classic_ProtocolVersion) objectInputStream.readObject();
|
||||
|
||||
bridge.getProtocolClient().getClientINSConnection().getEventManager().executeEvent(new Classic_DomainPacketReceivedEvent(protocolVersion, domain, requestDomain, clientID));
|
||||
} else if (bridge.isRunningAsWebServer()) {
|
||||
clientID = objectInputStream.readInt();
|
||||
requestDomain = (Classic_RequestDomain) objectInputStream.readObject();
|
||||
Classic_ProtocolVersion protocolVersion = (Classic_ProtocolVersion) objectInputStream.readObject();
|
||||
|
||||
bridge.getProtocolWebServer().getPipelineServer().getConnectionHandlerByID(clientID).sendPacket(new UnsupportedClassicPacket(Classic_PingPacket.class, new Object[]{clientID, requestDomain, domain}, bridge));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
package org.openautonomousconnection.protocol.packets.v1_0_0.classic;
|
||||
|
||||
import dev.unlegitdqrk.unlegitlibrary.network.system.packets.PacketHandler;
|
||||
import org.openautonomousconnection.protocol.ProtocolBridge;
|
||||
import org.openautonomousconnection.protocol.packets.OACPacket;
|
||||
import org.openautonomousconnection.protocol.versions.ProtocolVersion;
|
||||
import org.openautonomousconnection.protocol.versions.v1_0_0.classic.utils.Classic_ProtocolVersion;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.io.ObjectOutputStream;
|
||||
|
||||
public final class Classic_MessagePacket extends OACPacket {
|
||||
private String message;
|
||||
private int clientID;
|
||||
private ProtocolBridge bridge;
|
||||
|
||||
// Constructor with message and client id
|
||||
public Classic_MessagePacket(String message, int toClient, ProtocolBridge bridge) {
|
||||
this();
|
||||
this.message = message;
|
||||
this.clientID = toClient;
|
||||
this.bridge = bridge;
|
||||
}
|
||||
|
||||
public Classic_MessagePacket() {
|
||||
super(3, ProtocolVersion.PV_1_0_0_CLASSIC);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWrite(PacketHandler packetHandler, ObjectOutputStream objectOutputStream) throws IOException, ClassNotFoundException {
|
||||
if (bridge.isRunningAsINSServer() || bridge.isRunningAsWebServer())
|
||||
objectOutputStream.writeInt(clientID);
|
||||
else if (bridge.isRunningAsClient()) {
|
||||
clientID = bridge.getProtocolClient().getClientINSConnection().getClientID();
|
||||
objectOutputStream.writeInt(clientID);
|
||||
}
|
||||
|
||||
objectOutputStream.writeUTF(message);
|
||||
objectOutputStream.writeObject(Classic_ProtocolVersion.PV_1_0_0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRead(PacketHandler packetHandler, ObjectInputStream objectInputStream) throws IOException, ClassNotFoundException {
|
||||
if (bridge.isRunningAsINSServer()) {
|
||||
clientID = objectInputStream.readInt();
|
||||
String message = objectInputStream.readUTF();
|
||||
Classic_ProtocolVersion protocolVersion = (Classic_ProtocolVersion) objectInputStream.readObject();
|
||||
|
||||
bridge.getClassicHandlerINSServer().handleMessage(bridge.getProtocolINSServer().getClientByID(clientID), message, protocolVersion);
|
||||
} else if (bridge.isRunningAsClient()) {
|
||||
clientID = objectInputStream.readInt();
|
||||
String message = objectInputStream.readUTF();
|
||||
Classic_ProtocolVersion protocolVersion = (Classic_ProtocolVersion) objectInputStream.readObject();
|
||||
|
||||
bridge.getClassicHandlerClient().handleMessage(message, protocolVersion);
|
||||
} else if (bridge.isRunningAsWebServer()) {
|
||||
clientID = objectInputStream.readInt();
|
||||
String message = objectInputStream.readUTF();
|
||||
Classic_ProtocolVersion protocolVersion = (Classic_ProtocolVersion) objectInputStream.readObject();
|
||||
|
||||
bridge.getClassicHandlerWebServer().handleMessage(bridge.getProtocolINSServer().getClientByID(clientID), message, protocolVersion);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
package org.openautonomousconnection.protocol.packets.v1_0_0.classic;
|
||||
|
||||
import dev.unlegitdqrk.unlegitlibrary.network.system.packets.PacketHandler;
|
||||
import org.openautonomousconnection.protocol.ProtocolBridge;
|
||||
import org.openautonomousconnection.protocol.packets.OACPacket;
|
||||
import org.openautonomousconnection.protocol.packets.v1_0_0.beta.UnsupportedClassicPacket;
|
||||
import org.openautonomousconnection.protocol.versions.ProtocolVersion;
|
||||
import org.openautonomousconnection.protocol.versions.v1_0_0.beta.INSResponseStatus;
|
||||
import org.openautonomousconnection.protocol.versions.v1_0_0.classic.events.Classic_PingPacketReceivedEvent;
|
||||
import org.openautonomousconnection.protocol.versions.v1_0_0.classic.objects.Classic_Domain;
|
||||
import org.openautonomousconnection.protocol.versions.v1_0_0.classic.objects.Classic_RequestDomain;
|
||||
import org.openautonomousconnection.protocol.versions.v1_0_0.classic.utils.Classic_ProtocolVersion;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.io.ObjectOutputStream;
|
||||
import java.sql.SQLException;
|
||||
|
||||
public final class Classic_PingPacket extends OACPacket {
|
||||
private Classic_RequestDomain requestDomain;
|
||||
private Classic_Domain domain;
|
||||
private int clientID;
|
||||
private boolean reachable;
|
||||
private Classic_ProtocolVersion protocolVersion;
|
||||
private ProtocolBridge bridge;
|
||||
|
||||
public Classic_PingPacket(Classic_RequestDomain requestDomain, Classic_Domain domain, boolean reachable, ProtocolBridge bridge) {
|
||||
this();
|
||||
this.bridge = bridge;
|
||||
|
||||
this.requestDomain = requestDomain;
|
||||
this.domain = domain;
|
||||
this.reachable = reachable;
|
||||
this.protocolVersion = Classic_ProtocolVersion.PV_1_0_0;
|
||||
}
|
||||
|
||||
public Classic_PingPacket() {
|
||||
super(1, ProtocolVersion.PV_1_0_0_CLASSIC);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWrite(PacketHandler packetHandler, ObjectOutputStream objectOutputStream) throws IOException, ClassNotFoundException {
|
||||
if (bridge.isRunningAsINSServer()) {
|
||||
objectOutputStream.writeInt(clientID);
|
||||
objectOutputStream.writeObject(requestDomain);
|
||||
objectOutputStream.writeObject(domain);
|
||||
objectOutputStream.writeBoolean(reachable);
|
||||
} else if (bridge.isRunningAsClient()) {
|
||||
clientID = bridge.getProtocolClient().getClientINSConnection().getClientID();
|
||||
objectOutputStream.writeInt(clientID);
|
||||
objectOutputStream.writeObject(requestDomain);
|
||||
}
|
||||
|
||||
objectOutputStream.writeObject(protocolVersion);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRead(PacketHandler packetHandler, ObjectInputStream objectInputStream) throws IOException, ClassNotFoundException {
|
||||
if (bridge.isRunningAsINSServer()) {
|
||||
clientID = objectInputStream.readInt();
|
||||
requestDomain = (Classic_RequestDomain) objectInputStream.readObject();
|
||||
protocolVersion = (Classic_ProtocolVersion) objectInputStream.readObject();
|
||||
|
||||
try {
|
||||
domain = bridge.getClassicHandlerINSServer().ping(requestDomain);
|
||||
} catch (SQLException exception) {
|
||||
exception.printStackTrace();
|
||||
}
|
||||
|
||||
reachable = domain != null;
|
||||
bridge.getProtocolINSServer().getNetworkServer().getEventManager().executeEvent(new Classic_PingPacketReceivedEvent(protocolVersion, domain, requestDomain, reachable, clientID));
|
||||
if (bridge.getProtocolINSServer().getClientByID(clientID).supportClientClassic())
|
||||
bridge.getProtocolINSServer().getNetworkServer().getConnectionHandlerByID(clientID).sendPacket(new Classic_PingPacket(requestDomain, domain, reachable, bridge));
|
||||
else
|
||||
bridge.getProtocolINSServer().getNetworkServer().getConnectionHandlerByID(clientID).sendPacket(new UnsupportedClassicPacket(Classic_PingPacket.class, new Object[]{requestDomain, domain, reachable}, bridge));
|
||||
} else if (bridge.isRunningAsClient()) {
|
||||
clientID = objectInputStream.readInt();
|
||||
requestDomain = (Classic_RequestDomain) objectInputStream.readObject();
|
||||
domain = (Classic_Domain) objectInputStream.readObject();
|
||||
boolean reachable = objectInputStream.readBoolean();
|
||||
Classic_ProtocolVersion protocolVersion = (Classic_ProtocolVersion) objectInputStream.readObject();
|
||||
|
||||
bridge.getClassicHandlerClient().validationCompleted(domain, reachable ? INSResponseStatus.OK : INSResponseStatus.NOT_FOUND);
|
||||
bridge.getProtocolClient().getClientINSConnection().getEventManager().executeEvent(new Classic_PingPacketReceivedEvent(protocolVersion, domain, requestDomain, reachable, clientID));
|
||||
} else if (bridge.isRunningAsWebServer()) {
|
||||
clientID = objectInputStream.readInt();
|
||||
requestDomain = (Classic_RequestDomain) objectInputStream.readObject();
|
||||
protocolVersion = (Classic_ProtocolVersion) objectInputStream.readObject();
|
||||
|
||||
bridge.getProtocolWebServer().getPipelineServer().getConnectionHandlerByID(clientID).sendPacket(new UnsupportedClassicPacket(Classic_PingPacket.class, new Object[]{requestDomain}, bridge));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
* <p>Ensures the {@link WebPacketHeader} is always serialized first.</p>
|
||||
*/
|
||||
@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;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.document;
|
||||
|
||||
import dev.unlegitdqrk.unlegitlibrary.network.system.utils.TransportProtocol;
|
||||
import lombok.Getter;
|
||||
import org.openautonomousconnection.protocol.ProtocolBridge;
|
||||
import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.WebPacket;
|
||||
import org.openautonomousconnection.protocol.side.server.CustomConnectedClient;
|
||||
import org.openautonomousconnection.protocol.side.web.ProtocolWebServer;
|
||||
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;
|
||||
private final ProtocolBridge protocolBridge;
|
||||
|
||||
/**
|
||||
* Registration constructor.
|
||||
*/
|
||||
public WebDocumentApplyRequestPacket(ProtocolBridge protocolBridge) {
|
||||
super(18, ProtocolVersion.PV_1_0_1_BETA);
|
||||
this.protocolBridge = protocolBridge;
|
||||
}
|
||||
|
||||
public WebDocumentApplyRequestPacket(WebPacketHeader header, String fullHtml, ProtocolBridge protocolBridge) {
|
||||
this(protocolBridge);
|
||||
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();
|
||||
|
||||
if (protocolBridge.isRunningAsWebServer()) {
|
||||
ProtocolWebServer server = (ProtocolWebServer) protocolBridge.getProtocolServer();
|
||||
CustomConnectedClient client = server.getClientByID(clientID);
|
||||
client.getConnection().sendPacket(server.handleDocumentApply(client, this), TransportProtocol.TCP);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.navigate;
|
||||
|
||||
import dev.unlegitdqrk.unlegitlibrary.network.system.utils.TransportProtocol;
|
||||
import lombok.Getter;
|
||||
import org.openautonomousconnection.protocol.ProtocolBridge;
|
||||
import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.WebPacket;
|
||||
import org.openautonomousconnection.protocol.side.server.CustomConnectedClient;
|
||||
import org.openautonomousconnection.protocol.side.web.ProtocolWebServer;
|
||||
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.Objects;
|
||||
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;
|
||||
private final ProtocolBridge protocolBridge;
|
||||
|
||||
/**
|
||||
* Registration constructor.
|
||||
*/
|
||||
public WebNavigateRequestPacket(ProtocolBridge protocolBridge) {
|
||||
super(1, ProtocolVersion.PV_1_0_1_BETA);
|
||||
this.protocolBridge = Objects.requireNonNull(protocolBridge, "protocolBridge");
|
||||
}
|
||||
|
||||
public WebNavigateRequestPacket(
|
||||
WebPacketHeader header,
|
||||
String url,
|
||||
String referrer,
|
||||
WebTransitionType transitionType,
|
||||
long headerProfileId,
|
||||
WebCacheMode cacheMode, ProtocolBridge protocolBridge
|
||||
) {
|
||||
this(protocolBridge);
|
||||
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());
|
||||
|
||||
if (protocolBridge.isRunningAsWebServer()) {
|
||||
ProtocolWebServer server = (ProtocolWebServer) protocolBridge.getProtocolServer();
|
||||
CustomConnectedClient client = server.getClientByID(clientID);
|
||||
client.getConnection().sendPacket(server.handleNavigate(client, this), TransportProtocol.TCP);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.resource;
|
||||
|
||||
import dev.unlegitdqrk.unlegitlibrary.network.system.utils.TransportProtocol;
|
||||
import lombok.Getter;
|
||||
import org.openautonomousconnection.protocol.ProtocolBridge;
|
||||
import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.WebPacket;
|
||||
import org.openautonomousconnection.protocol.side.server.CustomConnectedClient;
|
||||
import org.openautonomousconnection.protocol.side.web.ProtocolWebServer;
|
||||
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<String, String> headers;
|
||||
private byte[] body;
|
||||
private String bodyContentType;
|
||||
private WebInitiatorType initiatorType;
|
||||
private WebCacheMode cacheMode;
|
||||
private final ProtocolBridge protocolBridge;
|
||||
|
||||
/**
|
||||
* Registration constructor.
|
||||
*/
|
||||
public WebResourceRequestPacket(ProtocolBridge protocolBridge) {
|
||||
super(3, ProtocolVersion.PV_1_0_1_BETA);
|
||||
this.protocolBridge = protocolBridge;
|
||||
}
|
||||
|
||||
public WebResourceRequestPacket(
|
||||
WebPacketHeader header,
|
||||
String url,
|
||||
String method,
|
||||
Map<String, String> headers,
|
||||
byte[] body,
|
||||
String bodyContentType,
|
||||
WebInitiatorType initiatorType,
|
||||
WebCacheMode cacheMode, ProtocolBridge protocolBridge
|
||||
) {
|
||||
this(protocolBridge);
|
||||
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;
|
||||
|
||||
try {
|
||||
this.initiatorType = WebInitiatorType.valueOf(in.readUTF());
|
||||
} catch (IllegalArgumentException e) {
|
||||
getProtocolBridge().getProtocolValues().logger.exception("Invalid initiator type: " + initiatorType, e);
|
||||
this.initiatorType = WebInitiatorType.OTHER;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
if (protocolBridge.isRunningAsWebServer()) {
|
||||
ProtocolWebServer server = (ProtocolWebServer) protocolBridge.getProtocolServer();
|
||||
CustomConnectedClient client = server.getClientByID(clientID);
|
||||
if (client == null) return;
|
||||
client.getConnection().sendPacket(server.handleResource(client, this), TransportProtocol.TCP);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
*
|
||||
* <p>If the response body is streamed, send this packet with an empty body and set STREAM flag in the header.</p>
|
||||
*/
|
||||
@Getter
|
||||
public final class WebResourceResponsePacket extends WebPacket {
|
||||
|
||||
private int statusCode;
|
||||
private String contentType;
|
||||
private Map<String, String> 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<String, String> 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);
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
* <p>Correlation is done via {@link WebPacketHeader#getRequestId()}.</p>
|
||||
*/
|
||||
@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);
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
* <p>Correlation is done via {@link WebPacketHeader#getRequestId()}.</p>
|
||||
*/
|
||||
@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;
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
* <p>Correlation is done via {@link WebPacketHeader#getRequestId()}.</p>
|
||||
*/
|
||||
@Getter
|
||||
public final class WebStreamStartPacket_v1_0_1_B extends WebPacket {
|
||||
private int statusCode;
|
||||
private String contentType;
|
||||
private Map<String, String> 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<String, String> 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();
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,13 @@
|
||||
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;
|
||||
import dev.unlegitdqrk.unlegitlibrary.network.system.client.events.ClientDisconnectedEvent;
|
||||
import dev.unlegitdqrk.unlegitlibrary.network.utils.NetworkUtils;
|
||||
import dev.unlegitdqrk.unlegitlibrary.utils.DefaultMethodsOverrider;
|
||||
import dev.unlegitdqrk.unlegitlibrary.network.system.client.events.state.ClientDisconnectedEvent;
|
||||
import dev.unlegitdqrk.unlegitlibrary.network.system.utils.TransportProtocol;
|
||||
import dev.unlegitdqrk.unlegitlibrary.network.utils.PemUtils;
|
||||
import lombok.Getter;
|
||||
import org.openautonomousconnection.protocol.ProtocolBridge;
|
||||
import org.openautonomousconnection.protocol.annotations.ProtocolInfo;
|
||||
@@ -17,291 +21,341 @@ import org.openautonomousconnection.protocol.versions.v1_0_0.beta.INSResponseSta
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.security.KeyStore;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Abstract class defining the client-side protocol operations and interactions with INS and web servers.
|
||||
* Abstract class defining the client-side protocol operations and interactions with INS and servers.
|
||||
*/
|
||||
@ProtocolInfo(protocolSide = ProtocolVersion.ProtocolSide.INS)
|
||||
public abstract class ProtocolClient extends DefaultMethodsOverrider {
|
||||
/**
|
||||
* Handles everything with INS-Connection.
|
||||
*/
|
||||
private final NetworkClient clientToINS;
|
||||
@ProtocolInfo(protocolSide = ProtocolVersion.ProtocolSide.CLIENT)
|
||||
public abstract class ProtocolClient extends EventListener {
|
||||
|
||||
/**
|
||||
* Manages the folder structure for client certificates.
|
||||
*/
|
||||
@Getter
|
||||
private final ClientCertificateFolderStructure folderStructure;
|
||||
/**
|
||||
* The reference to the ProtocolBridge Object
|
||||
*/
|
||||
|
||||
private NetworkClient clientToINS;
|
||||
private NetworkClient clientToServer;
|
||||
|
||||
@Getter
|
||||
private final ProtocolBridge protocolBridge;
|
||||
/**
|
||||
* Stores the protocol version of the connected server.
|
||||
*/
|
||||
private ProtocolBridge protocolBridge;
|
||||
|
||||
private ProtocolVersion insVersion = null;
|
||||
private ProtocolVersion serverVersion = null;
|
||||
|
||||
/**
|
||||
* Initializes the ProtocolClient, setting up certificate folders and the INS client connection.
|
||||
*
|
||||
* @throws CertificateException if there are issues with the certificates.
|
||||
* @throws IOException if there are I/O issues during initialization.
|
||||
*/
|
||||
public ProtocolClient(ProtocolBridge protocolBridge) throws CertificateException, IOException {
|
||||
this.protocolBridge = protocolBridge;
|
||||
|
||||
// Initialize and verify certificate folders and files
|
||||
public ProtocolClient() {
|
||||
folderStructure = new ClientCertificateFolderStructure();
|
||||
|
||||
// Initialize connection to INS server
|
||||
clientToINS = new NetworkClient.ClientBuilder().setLogger(protocolBridge.getLogger()).setProxy(protocolBridge.getProxy()).
|
||||
setHost(protocolBridge.getProtocolSettings().host).setPort(protocolBridge.getProtocolSettings().port).
|
||||
setPacketHandler(protocolBridge.getProtocolSettings().packetHandler).setEventManager(protocolBridge.getProtocolSettings().eventManager).
|
||||
setRootCAFolder(folderStructure.publicCAFolder).setClientCertificatesFolder(folderStructure.publicClientFolder, folderStructure.privateClientFolder).
|
||||
build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the INS connection client.
|
||||
*
|
||||
* @return the NetworkClient handling the INS connection.
|
||||
*/
|
||||
public final void buildINSConnection() {
|
||||
if (!protocolBridge.isRunningAsClient())
|
||||
throw new IllegalStateException("Not running as client");
|
||||
|
||||
if (clientToINS != null &&
|
||||
(clientToINS.isConnected() || clientToINS.isTCPConnected() || clientToINS.isUDPConnected()))
|
||||
return;
|
||||
|
||||
clientToINS = new NetworkClient.Builder().sslEnabled(false).
|
||||
packetHandler(protocolBridge.getProtocolValues().packetHandler)
|
||||
.eventManager(protocolBridge.getProtocolValues().eventManager)
|
||||
.build();
|
||||
}
|
||||
|
||||
public final void buildServerConnection(String keyPassword, boolean ssl) throws Exception {
|
||||
if (!protocolBridge.isRunningAsClient())
|
||||
throw new IllegalStateException("Not running as client");
|
||||
|
||||
if (clientToServer != null &&
|
||||
(clientToServer.isConnected() || clientToServer.isTCPConnected() || clientToServer.isUDPConnected()))
|
||||
return;
|
||||
|
||||
if (ssl) {
|
||||
KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
|
||||
trustStore.load(null, null);
|
||||
|
||||
int caIndex = 0;
|
||||
for (File caPem : FileUtils.listFiles(folderStructure.publicCAFolder, ".pem", ".crt", ".cer")) {
|
||||
if (caPem.getName().endsWith(".srl")) continue;
|
||||
X509Certificate caCert = PemUtils.loadCertificate(caPem);
|
||||
trustStore.setCertificateEntry("root-ca-" + (caIndex++), caCert);
|
||||
}
|
||||
|
||||
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
|
||||
keyStore.load(null, null);
|
||||
char[] keyPass = new char[0];
|
||||
if (keyPassword != null) keyPass = keyPassword.toCharArray();
|
||||
|
||||
Map<String, File> keyFiles = new HashMap<>();
|
||||
Map<String, File> certFiles = new HashMap<>();
|
||||
|
||||
for (File f : FileUtils.listFiles(folderStructure.privateClientFolder, ".key", ".pem")) {
|
||||
if (f.getName().endsWith(".srl")) continue;
|
||||
String base = FileUtils.stripExt(f.getName());
|
||||
if (f.getName().endsWith(".key")) keyFiles.put(base, f);
|
||||
if (f.getName().endsWith(".pem")) certFiles.put(base, f);
|
||||
}
|
||||
|
||||
int clientIndex = 0;
|
||||
for (String base : keyFiles.keySet()) {
|
||||
File keyFile = keyFiles.get(base);
|
||||
File certFile = certFiles.get(base);
|
||||
if (certFile == null) {
|
||||
File alt = new File(folderStructure.publicClientFolder, base + ".pem");
|
||||
if (alt.exists()) certFile = alt;
|
||||
}
|
||||
if (certFile == null) continue;
|
||||
|
||||
PrivateKey key = PemUtils.loadPrivateKey(keyFile);
|
||||
X509Certificate cert = PemUtils.loadCertificate(certFile);
|
||||
|
||||
String alias = "client-" + (clientIndex++);
|
||||
keyStore.setKeyEntry(alias, key, keyPass, new X509Certificate[]{cert});
|
||||
}
|
||||
|
||||
clientToServer = new NetworkClient.Builder().sslEnabled(ssl).
|
||||
packetHandler(protocolBridge.getProtocolValues().packetHandler)
|
||||
.eventManager(protocolBridge.getProtocolValues().eventManager)
|
||||
.keyStore(keyStore, keyPass)
|
||||
.trustStore(trustStore)
|
||||
.build();
|
||||
} else {
|
||||
clientToServer = new NetworkClient.Builder().sslEnabled(ssl).
|
||||
packetHandler(protocolBridge.getProtocolValues().packetHandler)
|
||||
.eventManager(protocolBridge.getProtocolValues().eventManager)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
public final NetworkClient getClientServerConnection() {
|
||||
return clientToServer;
|
||||
}
|
||||
|
||||
public final void attachBridge(ProtocolBridge bridge) throws Exception {
|
||||
if (this.protocolBridge != null)
|
||||
throw new IllegalStateException("ProtocolBridge already attached!");
|
||||
this.protocolBridge = bridge;
|
||||
protocolBridge.getProtocolValues().eventManager.registerListener(this);
|
||||
}
|
||||
|
||||
public final NetworkClient getClientINSConnection() {
|
||||
return clientToINS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the required certificate files exist in the specified folder.
|
||||
*
|
||||
* @param folder the folder to check for certificate files.
|
||||
* @param prefix the prefix of the certificate files.
|
||||
* @param extension the extension of the certificate files.
|
||||
* @throws CertificateException if any required certificate file is missing or invalid.
|
||||
* @throws IOException if there are I/O issues during the check.
|
||||
*/
|
||||
private final void checkFileExists(File folder, String prefix, String extension) throws CertificateException, IOException {
|
||||
boolean found = false;
|
||||
// Check if folder exists
|
||||
private void checkFileExists(File folder, String prefix, String extension) throws CertificateException, IOException {
|
||||
if (folder == null) throw new FileNotFoundException("Folder does not exist");
|
||||
|
||||
// List files in the folder
|
||||
File[] files = folder.listFiles();
|
||||
|
||||
// Check if folder is empty
|
||||
if (files == null || files.length == 0)
|
||||
throw new FileNotFoundException("Folder " + folder.getAbsolutePath() + " is empty");
|
||||
|
||||
// Validate each file in the folder
|
||||
for (File file : files) {
|
||||
if (!file.getName().startsWith(prefix))
|
||||
throw new CertificateException(file.getAbsolutePath() + " is not valid");
|
||||
|
||||
// Check for specific files
|
||||
if (!found) found = file.getName().equalsIgnoreCase(prefix + NetworkUtils.getPublicIPAddress() + extension);
|
||||
}
|
||||
|
||||
// If the specific file is not found, throw an exception
|
||||
if (!found) throw new CertificateException("Missing " + prefix + NetworkUtils.getPublicIPAddress() + extension);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the protocol version of the connected server.
|
||||
*
|
||||
* @return the ProtocolVersion of the server, or PV_1_0_0_CLASSIC if not set.
|
||||
*/
|
||||
public final ProtocolVersion getServerVersion() {
|
||||
return serverVersion == null ? ProtocolVersion.PV_1_0_0_CLASSIC : serverVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the protocol version of the connected server.
|
||||
*
|
||||
* @param serverVersion the ProtocolVersion to set for the server.
|
||||
*/
|
||||
public final void setServerVersion(ProtocolVersion serverVersion) {
|
||||
if (serverVersion == null) this.serverVersion = serverVersion;
|
||||
if (this.serverVersion == null) this.serverVersion = serverVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles INS disconnection events, resetting the server version and closing the web client connection if necessary.
|
||||
*
|
||||
* @param event the ClientDisconnectedEvent triggered on INS disconnection.
|
||||
*/
|
||||
public final void onINSDisconnect(ClientDisconnectedEvent event) {
|
||||
// Reset server version on INS disconnect
|
||||
serverVersion = null;
|
||||
public final ProtocolVersion getInsVersion() {
|
||||
return insVersion == null ? ProtocolVersion.PV_1_0_0_CLASSIC : insVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the connected server is a stable server.
|
||||
*
|
||||
* @return true if the server is stable, false otherwise.
|
||||
*/
|
||||
public final boolean isStableServer() {
|
||||
// Check if the server version is stable
|
||||
return !isBetaServer() && !isClassicServer();
|
||||
public final void setInsVersion(ProtocolVersion insVersion) {
|
||||
if (this.insVersion == null) this.insVersion = insVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the connected server or its compatible versions support stable protocol.
|
||||
*
|
||||
* @return true if stable protocol is supported, false otherwise.
|
||||
*/
|
||||
public final boolean supportServerStable() {
|
||||
@Listener(priority = EventPriority.HIGHEST)
|
||||
public final void onDisconnect(ClientDisconnectedEvent event) {
|
||||
if (clientToINS == null || !clientToINS.isConnected()) {
|
||||
insVersion = null;
|
||||
clientToINS = null;
|
||||
disconnectFromServer();
|
||||
}
|
||||
|
||||
if (clientToServer == null || !clientToServer.isConnected()) {
|
||||
serverVersion = null;
|
||||
clientToServer = null;
|
||||
}
|
||||
}
|
||||
|
||||
public final boolean isINSStableServer() {
|
||||
return !isINSBetaServer() && !isINSClassicServer();
|
||||
}
|
||||
|
||||
public final boolean supportINSServerStable() {
|
||||
boolean yes = false;
|
||||
for (ProtocolVersion compatibleVersion : getServerVersion().getCompatibleVersions()) {
|
||||
// Check if compatible version is stable
|
||||
yes = compatibleVersion.getProtocolType() == ProtocolVersion.ProtocolType.STABLE;
|
||||
if (yes) break;
|
||||
}
|
||||
|
||||
// Check if the server version is stable
|
||||
return isStableServer() || yes;
|
||||
return isINSStableServer() || yes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the connected server is a beta server.
|
||||
*
|
||||
* @return true if the server is beta, false otherwise.
|
||||
*/
|
||||
public final boolean isBetaServer() {
|
||||
// Check if the server version is beta
|
||||
return getServerVersion().getProtocolType() == ProtocolVersion.ProtocolType.BETA;
|
||||
public final boolean isINSBetaServer() {
|
||||
return getInsVersion().getProtocolType() == ProtocolVersion.ProtocolType.BETA;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the connected server or its compatible versions support beta protocol.
|
||||
*
|
||||
* @return true if beta protocol is supported, false otherwise.
|
||||
*/
|
||||
public final boolean supportServerBeta() {
|
||||
public final boolean supportINSServerBeta() {
|
||||
boolean yes = false;
|
||||
for (ProtocolVersion compatibleVersion : getServerVersion().getCompatibleVersions()) {
|
||||
// Check if compatible version is beta
|
||||
for (ProtocolVersion compatibleVersion : getInsVersion().getCompatibleVersions()) {
|
||||
yes = compatibleVersion.getProtocolType() == ProtocolVersion.ProtocolType.BETA;
|
||||
if (yes) break;
|
||||
}
|
||||
|
||||
// Check if the server version is beta
|
||||
return isBetaServer() || yes;
|
||||
return isINSBetaServer() || yes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the connected server is a classic server.
|
||||
*
|
||||
* @return true if the server is classic, false otherwise.
|
||||
*/
|
||||
public final boolean isClassicServer() {
|
||||
// Check if the server version is classic
|
||||
return getServerVersion().getProtocolType() == ProtocolVersion.ProtocolType.CLASSIC;
|
||||
public final boolean isINSClassicServer() {
|
||||
return getInsVersion().getProtocolType() == ProtocolVersion.ProtocolType.CLASSIC;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the connected server or its compatible versions support classic protocol.
|
||||
*
|
||||
* @return true if classic protocol is supported, false otherwise.
|
||||
*/
|
||||
public final boolean supportServerClassic() {
|
||||
public final boolean supportINSServerClassic() {
|
||||
boolean yes = false;
|
||||
for (ProtocolVersion compatibleVersion : getServerVersion().getCompatibleVersions()) {
|
||||
// Check if compatible version is classic
|
||||
for (ProtocolVersion compatibleVersion : getInsVersion().getCompatibleVersions()) {
|
||||
yes = compatibleVersion.getProtocolType() == ProtocolVersion.ProtocolType.CLASSIC;
|
||||
if (yes) break;
|
||||
}
|
||||
|
||||
// Check if the server version is classic
|
||||
return isClassicServer() || yes;
|
||||
return isINSClassicServer() || yes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the connected server supports the protocol version of the given packet.
|
||||
*
|
||||
* @param packet the OACPacket to check against the server's supported protocol version.
|
||||
* @return true if the server supports the packet's protocol version, false otherwise.
|
||||
*/
|
||||
public final boolean supportServerPacket(OACPacket packet) {
|
||||
// Check if the server supports the protocol version of the packet
|
||||
return supportServerVersion(packet.getProtocolVersion());
|
||||
public final boolean supportINSServerPacket(OACPacket packet) {
|
||||
boolean compatible = false;
|
||||
|
||||
for (ProtocolVersion compatibleVersion : packet.getCompatibleVersions()) {
|
||||
if (!compatible) compatible = supportINSServerVersion(compatibleVersion);
|
||||
}
|
||||
|
||||
return compatible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the connected server or its compatible versions support the specified protocol version.
|
||||
*
|
||||
* @param targetVersion the ProtocolVersion to check for support.
|
||||
* @return true if the server or its compatible versions support the target version, false otherwise.
|
||||
*/
|
||||
public final boolean supportServerVersion(ProtocolVersion targetVersion) {
|
||||
// Directly check if the server version matches or is in the list of compatible versions
|
||||
return getServerVersion() == targetVersion || getServerVersion().getCompatibleVersions().contains(targetVersion);
|
||||
public final boolean supportINSServerVersion(ProtocolVersion targetVersion) {
|
||||
return getInsVersion() == targetVersion || getInsVersion().getCompatibleVersions().contains(targetVersion);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the connected server or its compatible versions support the specified protocol.
|
||||
*
|
||||
* @param protocol the Protocol to check for support.
|
||||
* @return true if the server or its compatible versions support the protocol, false otherwise.
|
||||
*/
|
||||
public final boolean supportServerProtocol(ProtocolVersion.Protocol protocol) {
|
||||
public final boolean supportINSServerProtocol(ProtocolVersion.Protocol protocol) {
|
||||
boolean yes = false;
|
||||
for (ProtocolVersion compatibleVersion : getServerVersion().getCompatibleVersions()) {
|
||||
// Check if compatible version supports the protocol
|
||||
for (ProtocolVersion compatibleVersion : getInsVersion().getCompatibleVersions()) {
|
||||
yes = compatibleVersion.getSupportedProtocols().contains(protocol);
|
||||
if (yes) break;
|
||||
}
|
||||
return getInsVersion().getSupportedProtocols().contains(protocol) || yes;
|
||||
}
|
||||
|
||||
// Check if the server version supports the protocol
|
||||
public final boolean isStableServer() {
|
||||
return !isBetaServer() && !isClassicServer();
|
||||
}
|
||||
|
||||
public final boolean supportServerStable() {
|
||||
boolean yes = false;
|
||||
for (ProtocolVersion compatibleVersion : getServerVersion().getCompatibleVersions()) {
|
||||
yes = compatibleVersion.getProtocolType() == ProtocolVersion.ProtocolType.STABLE;
|
||||
if (yes) break;
|
||||
}
|
||||
return isStableServer() || yes;
|
||||
}
|
||||
|
||||
public final boolean isBetaServer() {
|
||||
return getServerVersion().getProtocolType() == ProtocolVersion.ProtocolType.BETA;
|
||||
}
|
||||
|
||||
public final boolean supportServerBeta() {
|
||||
boolean yes = false;
|
||||
for (ProtocolVersion compatibleVersion : getServerVersion().getCompatibleVersions()) {
|
||||
yes = compatibleVersion.getProtocolType() == ProtocolVersion.ProtocolType.BETA;
|
||||
if (yes) break;
|
||||
}
|
||||
return isBetaServer() || yes;
|
||||
}
|
||||
|
||||
public final boolean isClassicServer() {
|
||||
return getServerVersion().getProtocolType() == ProtocolVersion.ProtocolType.CLASSIC;
|
||||
}
|
||||
|
||||
public final boolean supportServerClassic() {
|
||||
boolean yes = false;
|
||||
for (ProtocolVersion compatibleVersion : getServerVersion().getCompatibleVersions()) {
|
||||
yes = compatibleVersion.getProtocolType() == ProtocolVersion.ProtocolType.CLASSIC;
|
||||
if (yes) break;
|
||||
}
|
||||
return isClassicServer() || yes;
|
||||
}
|
||||
|
||||
public final boolean supportServerPacket(OACPacket packet) {
|
||||
boolean compatible = false;
|
||||
|
||||
for (ProtocolVersion compatibleVersion : packet.getCompatibleVersions()) {
|
||||
if (!compatible) compatible = supportServerVersion(compatibleVersion);
|
||||
}
|
||||
|
||||
return compatible;
|
||||
}
|
||||
|
||||
public final boolean supportServerVersion(ProtocolVersion targetVersion) {
|
||||
return getServerVersion() == targetVersion || getServerVersion().getCompatibleVersions().contains(targetVersion);
|
||||
}
|
||||
|
||||
public final boolean supportServerProtocol(ProtocolVersion.Protocol protocol) {
|
||||
boolean yes = false;
|
||||
for (ProtocolVersion compatibleVersion : getServerVersion().getCompatibleVersions()) {
|
||||
yes = compatibleVersion.getSupportedProtocols().contains(protocol);
|
||||
if (yes) break;
|
||||
}
|
||||
return getServerVersion().getSupportedProtocols().contains(protocol) || yes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an INS query to the INS server.
|
||||
* <p>
|
||||
* The client requests a specific record type for a given TLN, name and optional subname.
|
||||
* After sending the packet, {@link #onQuerySent(String, String, String, INSRecordType)} is invoked.
|
||||
*
|
||||
* @param tln The TLN.
|
||||
* @param name The InfoName.
|
||||
* @param sub Optional subname or {@code null}.
|
||||
* @param type The requested record type.
|
||||
* @throws IOException If sending the packet fails.
|
||||
* @throws ClassNotFoundException If packet serialization fails.
|
||||
*/
|
||||
public final void sendINSQuery(String tln, String name, String sub, INSRecordType type) throws IOException, ClassNotFoundException {
|
||||
public final void sendINSQuery(String tln, String name, String sub, INSRecordType type) throws Exception {
|
||||
if (!protocolBridge.isRunningAsClient()) return;
|
||||
|
||||
getClientINSConnection().sendPacket(new INSQueryPacket(tln, name, sub, type, getClientINSConnection().getClientID()));
|
||||
getClientINSConnection().sendPacket(
|
||||
new INSQueryPacket(tln, name, sub, type, getClientINSConnection().getUniqueID()),
|
||||
TransportProtocol.TCP
|
||||
);
|
||||
onQuerySent(tln, name, sub, type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the client receives an INS response from the server.
|
||||
* <p>
|
||||
*
|
||||
* @param status The response status from the server.
|
||||
* @param records The list of records returned by the server.
|
||||
*/
|
||||
public abstract void onResponse(INSResponseStatus status, List<INSRecord> records);
|
||||
private final void disconnectFromServer() {
|
||||
if (clientToServer != null) {
|
||||
if (clientToINS == null || !clientToINS.isConnected())
|
||||
protocolBridge.getProtocolValues().eventManager.unregisterListener(this);
|
||||
clientToServer.disconnect();
|
||||
clientToServer = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void onResponse(INSResponseStatus status, List<INSRecord> records) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after a query was sent to the INS server.
|
||||
* <p>
|
||||
*
|
||||
* @param tln The TLN requested.
|
||||
* @param name The InfoName.
|
||||
* @param sub Optional subname.
|
||||
* @param type The requested record type.
|
||||
*/
|
||||
public void onQuerySent(String tln, String name, String sub, INSRecordType type) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages the folder structure for client certificates.
|
||||
* Called when no stored INS fingerprint exists yet (trust-on-first-use).
|
||||
*
|
||||
* @param caFingerprint received fingerprint of the INS CA certificate
|
||||
* @return {@code true} to allow the connection; {@code false} to reject it
|
||||
*/
|
||||
public final class ClientCertificateFolderStructure {
|
||||
public abstract boolean trustINS(String caFingerprint);
|
||||
|
||||
/**
|
||||
* Called when a stored INS fingerprint does not match the received one.
|
||||
*
|
||||
* @param oldCAFingerprint previously stored fingerprint
|
||||
* @param newCAFingerprint received fingerprint of the INS CA certificate
|
||||
* @return {@code true} to accept the new fingerprint; {@code false} to reject it
|
||||
*/
|
||||
public abstract boolean trustNewINSFingerprint(String oldCAFingerprint, String newCAFingerprint);
|
||||
|
||||
public static final class ClientCertificateFolderStructure {
|
||||
public final File certificatesFolder;
|
||||
|
||||
public final File publicFolder;
|
||||
@@ -338,3 +392,4 @@ public abstract class ProtocolClient extends DefaultMethodsOverrider {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.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}.
|
||||
*
|
||||
* <p>Compatibility strategy:</p>
|
||||
* <ul>
|
||||
* <li>In Web v1.0.0 mode, correlation is only possible if there is exactly ONE in-flight request.</li>
|
||||
* <li>This client therefore enforces single in-flight request and maps responses/streams
|
||||
* onto that single active {@link WebPacketHeader}.</li>
|
||||
* </ul>
|
||||
*/
|
||||
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<Long, WebHeaderFactory.WebTabContext> tabs = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* Single in-flight correlation header.
|
||||
*
|
||||
* <p>v1.0.0 has no correlation fields; without changing the v1.0.0 server this is the only deterministic mapping.</p>
|
||||
*/
|
||||
private final AtomicReference<WebPacketHeader> 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, getProtocolBridge()
|
||||
);
|
||||
|
||||
getClientServerConnection().sendPacket(pkt, TransportProtocol.TCP);
|
||||
onWebRequestSent(pkt);
|
||||
}
|
||||
|
||||
public final long sendResource(
|
||||
WebHeaderFactory.WebTabContext tab,
|
||||
long frameId,
|
||||
String url,
|
||||
String method,
|
||||
Map<String, String> 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, getProtocolBridge()
|
||||
);
|
||||
|
||||
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, getProtocolBridge());
|
||||
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.
|
||||
*
|
||||
* <p>Enforces single in-flight 1.0.0 request.</p>
|
||||
*
|
||||
* @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.
|
||||
*
|
||||
* <p>Your v1.0.0 WebServer uses: header 'x-oac-stream' = '1' for the 202 response before streaming.</p>
|
||||
*
|
||||
* @param resp 1.0.0 response
|
||||
* @return true if likely stream handshake
|
||||
*/
|
||||
private boolean looksLike100BStreamHandshake(WebResponsePacket resp) {
|
||||
if (resp == null) return false;
|
||||
Map<String, String> h = resp.getHeaders();
|
||||
if (h == null) return false;
|
||||
|
||||
// Case-insensitive lookup without allocating
|
||||
for (Map.Entry<String, String> e : h.entrySet()) {
|
||||
if (e.getKey() != null && e.getValue() != null
|
||||
&& e.getKey().equalsIgnoreCase("x-oac-stream")
|
||||
&& e.getValue().equals("1")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package org.openautonomousconnection.protocol.side.client.events;
|
||||
|
||||
import dev.unlegitdqrk.unlegitlibrary.event.impl.Event;
|
||||
import lombok.Getter;
|
||||
import org.openautonomousconnection.protocol.annotations.ProtocolInfo;
|
||||
import org.openautonomousconnection.protocol.side.client.ProtocolClient;
|
||||
import org.openautonomousconnection.protocol.versions.ProtocolVersion;
|
||||
|
||||
/**
|
||||
* Event triggered when a client successfully connects to a INS protocol server.
|
||||
*/
|
||||
@ProtocolInfo(protocolSide = ProtocolVersion.ProtocolSide.INS)
|
||||
public final class ConnectedToProtocolServerEvent extends Event {
|
||||
|
||||
/**
|
||||
* Reference to the ProtocolClient object.
|
||||
*/
|
||||
@Getter
|
||||
private final ProtocolClient client;
|
||||
|
||||
public ConnectedToProtocolServerEvent(ProtocolClient client) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,164 +1,71 @@
|
||||
package org.openautonomousconnection.protocol.side.ins;
|
||||
|
||||
import dev.unlegitdqrk.unlegitlibrary.file.ConfigurationManager;
|
||||
import dev.unlegitdqrk.unlegitlibrary.network.system.server.NetworkServer;
|
||||
import dev.unlegitdqrk.unlegitlibrary.file.FileUtils;
|
||||
import dev.unlegitdqrk.unlegitlibrary.network.utils.NetworkUtils;
|
||||
import dev.unlegitdqrk.unlegitlibrary.utils.DefaultMethodsOverrider;
|
||||
import lombok.Getter;
|
||||
import org.openautonomousconnection.protocol.ProtocolBridge;
|
||||
import org.openautonomousconnection.protocol.annotations.ProtocolInfo;
|
||||
import org.openautonomousconnection.protocol.side.server.ProtocolCustomServer;
|
||||
import org.openautonomousconnection.protocol.versions.ProtocolVersion;
|
||||
import org.openautonomousconnection.protocol.versions.v1_0_0.beta.INSRecord;
|
||||
import org.openautonomousconnection.protocol.versions.v1_0_0.beta.INSRecordType;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Abstract class representing a INS server in the protocol.
|
||||
*/
|
||||
@ProtocolInfo(protocolSide = ProtocolVersion.ProtocolSide.INS)
|
||||
public abstract class ProtocolINSServer extends DefaultMethodsOverrider {
|
||||
/**
|
||||
* The network server instance.
|
||||
*/
|
||||
@Getter
|
||||
private final NetworkServer networkServer;
|
||||
|
||||
/**
|
||||
* The configuration manager for handling server configurations.
|
||||
*/
|
||||
private final ConfigurationManager configurationManager;
|
||||
/**
|
||||
* The reference to the ProtocolBridge Object
|
||||
*/
|
||||
@Getter
|
||||
private final ProtocolBridge protocolBridge;
|
||||
/**
|
||||
* List of connected protocol clients.
|
||||
*/
|
||||
@Getter
|
||||
private List<ConnectedProtocolClient> clients;
|
||||
/**
|
||||
* The folder structure for server certificates.
|
||||
*/
|
||||
@Getter
|
||||
private ServerCertificateFolderStructure folderStructure;
|
||||
|
||||
/**
|
||||
* Constructs a ProtocolINSServer with the specified configuration file.
|
||||
*
|
||||
* @param configFile The configuration file for the INS server.
|
||||
* @throws IOException If an I/O error occurs.
|
||||
* @throws CertificateException If a certificate error occurs.
|
||||
*/
|
||||
public ProtocolINSServer(File configFile, ProtocolBridge protocolBridge) throws IOException, CertificateException {
|
||||
this.protocolBridge = protocolBridge;
|
||||
// Ensure the configuration file exists
|
||||
if (!configFile.exists()) configFile.createNewFile();
|
||||
|
||||
// Load the configuration properties
|
||||
configurationManager = new ConfigurationManager(configFile);
|
||||
configurationManager.loadProperties();
|
||||
|
||||
// Set default values for configuration properties if not already set
|
||||
if (!configurationManager.isSet("server.site.info")) {
|
||||
configurationManager.set("server.site.info", "INS-SERVER INFO SITE IP:PORT");
|
||||
configurationManager.saveProperties();
|
||||
}
|
||||
|
||||
if (!configurationManager.isSet("server.site.frontend")) {
|
||||
configurationManager.set("server.site.frontend", "SERVER IP TO INS-FRONTEND:PORT");
|
||||
configurationManager.saveProperties();
|
||||
}
|
||||
|
||||
// Initialize the folder structure
|
||||
folderStructure = new ServerCertificateFolderStructure();
|
||||
|
||||
// Check for the existence of necessary certificate files
|
||||
checkFileExists(folderStructure.publicCAFolder, folderStructure.caPrefix, ".pem");
|
||||
checkFileExists(folderStructure.publicCAFolder, folderStructure.caPrefix, ".srl");
|
||||
checkFileExists(folderStructure.privateCAFolder, folderStructure.caPrefix, ".key");
|
||||
|
||||
checkFileExists(folderStructure.publicServerFolder, folderStructure.certPrefix, ".crt");
|
||||
checkFileExists(folderStructure.privateServerFolder, folderStructure.certPrefix, ".key");
|
||||
|
||||
// Define the certificate and key files based on the public IP address
|
||||
File certFile = new File(folderStructure.publicServerFolder, folderStructure.certPrefix + NetworkUtils.getPublicIPAddress() + ".crt");
|
||||
File keyFile = new File(folderStructure.privateServerFolder, folderStructure.certPrefix + NetworkUtils.getPublicIPAddress() + ".key");
|
||||
|
||||
// Initialize the protocol bridge and clients list
|
||||
this.clients = new ArrayList<>();
|
||||
|
||||
// Build the network server with the specified settings
|
||||
this.networkServer = new NetworkServer.ServerBuilder().
|
||||
setLogger(protocolBridge.getLogger()).
|
||||
setEventManager(protocolBridge.getProtocolSettings().eventManager).
|
||||
setPacketHandler(protocolBridge.getProtocolSettings().packetHandler).
|
||||
setPort(protocolBridge.getProtocolSettings().port).
|
||||
setRequireClientCertificate(false).setRootCAFolder(folderStructure.publicCAFolder).setServerCertificate(certFile, keyFile).
|
||||
build();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks if the required certificate files exist in the specified folder.
|
||||
*
|
||||
* @param folder The folder to check for certificate files.
|
||||
* @param prefix The prefix of the certificate files.
|
||||
* @param extension The extension of the certificate files.
|
||||
* @throws CertificateException If a certificate error occurs.
|
||||
* @throws IOException If an I/O error occurs.
|
||||
*/
|
||||
private final void checkFileExists(File folder, String prefix, String extension) throws CertificateException, IOException {
|
||||
boolean found = false;
|
||||
|
||||
// Check if the folder exists
|
||||
if (folder == null) throw new FileNotFoundException("Folder does not exist");
|
||||
|
||||
// List all files in the folder
|
||||
File[] files = folder.listFiles();
|
||||
|
||||
// Check if the folder is empty
|
||||
if (files == null || files.length == 0)
|
||||
throw new FileNotFoundException("Folder " + folder.getAbsolutePath() + " is empty");
|
||||
|
||||
// Validate each file in the folder
|
||||
for (File file : files) {
|
||||
if (!file.getName().startsWith(prefix))
|
||||
throw new CertificateException(file.getAbsolutePath() + " is not valid");
|
||||
|
||||
// Check if the file matches the expected naming convention
|
||||
if (!found) found = file.getName().equalsIgnoreCase(prefix + NetworkUtils.getPublicIPAddress() + extension);
|
||||
}
|
||||
|
||||
// If the required file is not found, throw an exception
|
||||
if (!found) throw new CertificateException("Missing " + prefix + NetworkUtils.getPublicIPAddress() + extension);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a connected protocol client by its client ID.
|
||||
*
|
||||
* @param clientID The ID of the client to retrieve.
|
||||
* @return The ConnectedProtocolClient with the specified ID, or null if not found.
|
||||
*/
|
||||
public final ConnectedProtocolClient getClientByID(int clientID) {
|
||||
for (ConnectedProtocolClient client : clients)
|
||||
if (client.getConnectionHandler().getClientID() == clientID) return client;
|
||||
return null;
|
||||
}
|
||||
public abstract class ProtocolINSServer extends ProtocolCustomServer {
|
||||
|
||||
/**
|
||||
* Gets the INS information site URL from the configuration.
|
||||
*
|
||||
* @return The INS information site URL.
|
||||
*/
|
||||
public final String getINSInfoSite() {
|
||||
return configurationManager.getString("server.site.info");
|
||||
private final String insInfoSite;
|
||||
/**
|
||||
* Gets the INS registration site URL from the configuration.
|
||||
*
|
||||
* @return The INS registration site URL.
|
||||
*/
|
||||
private final String insFrontendSite;
|
||||
|
||||
/**
|
||||
* Constructs a ProtocolINSServer with the specified configuration file.
|
||||
*
|
||||
* @param insInfoSite The INS-InfoSize (IP:PORT)
|
||||
* @param insFrontendSite The INS-InfoSize (IP:PORT)
|
||||
* @throws IOException If an I/O error occurs.
|
||||
* @throws CertificateException If a certificate error occurs.
|
||||
*/
|
||||
public ProtocolINSServer(String insInfoSite, String insFrontendSite) throws Exception {
|
||||
super("ins", "ins");
|
||||
this.insInfoSite = insInfoSite;
|
||||
this.insFrontendSite = insFrontendSite;
|
||||
}
|
||||
|
||||
public final String getInsInfoSite() {
|
||||
return insInfoSite;
|
||||
}
|
||||
|
||||
public final String getInsFrontendSite() {
|
||||
return insFrontendSite;
|
||||
}
|
||||
|
||||
public void start(int tcpPort) throws IOException, InterruptedException, NoSuchAlgorithmException {
|
||||
getNetwork().start(tcpPort, -1);
|
||||
|
||||
String caPrefix = getFolderStructure().getCaPrefix() + NetworkUtils.getPublicIPAddress();
|
||||
File caPemFile = new File(getFolderStructure().publicCAFolder, caPrefix + ".pem");
|
||||
|
||||
byte[] caBytes = FileUtils.readFileFull(caPemFile).getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
||||
java.security.MessageDigest md = java.security.MessageDigest.getInstance("SHA-256");
|
||||
String fp = java.util.HexFormat.of().formatHex(md.digest(caBytes));
|
||||
|
||||
getProtocolBridge().getProtocolValues().logger.info("CA Fingerprint: " + fp);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -188,7 +95,8 @@ public abstract class ProtocolINSServer extends DefaultMethodsOverrider {
|
||||
* @param sub An optional subname, or {@code null}.
|
||||
* @param type The record type requested.
|
||||
*/
|
||||
public void onQueryReceived(String tln, String name, String sub, INSRecordType type) {}
|
||||
public void onQueryReceived(String tln, String name, String sub, INSRecordType type) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback fired after an INS response was successfully sent to the client.
|
||||
@@ -199,7 +107,8 @@ public abstract class ProtocolINSServer extends DefaultMethodsOverrider {
|
||||
* @param type The requested record type.
|
||||
* @param results The records returned to the client.
|
||||
*/
|
||||
public void onResponseSent(String tln, String name, String sub, INSRecordType type, List<INSRecord> results) {}
|
||||
public void onResponseSent(String tln, String name, String sub, INSRecordType type, List<INSRecord> results) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback fired when an INS response could not be delivered to the client.
|
||||
@@ -211,7 +120,8 @@ public abstract class ProtocolINSServer extends DefaultMethodsOverrider {
|
||||
* @param results The records that would have been sent.
|
||||
* @param exception The exception describing the failure.
|
||||
*/
|
||||
public void onResponseSentFailed(String tln, String name, String sub, INSRecordType type, List<INSRecord> results, Exception exception) {}
|
||||
public void onResponseSentFailed(String tln, String name, String sub, INSRecordType type, List<INSRecord> results, Exception exception) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the information endpoint for a given Top-Level Name (TLN).
|
||||
@@ -239,60 +149,8 @@ public abstract class ProtocolINSServer extends DefaultMethodsOverrider {
|
||||
*
|
||||
* @param tln the top-level name for which the info site should be resolved.
|
||||
* Must not be null. Case-insensitive.
|
||||
*
|
||||
* @return a string in <code>"host:port"</code> format representing the TLN's info endpoint,
|
||||
* or <code>null</code> if the TLN has no registered info site.
|
||||
* or <code>null</code> if the TLN has no registered info site.
|
||||
*/
|
||||
public abstract String resolveTLNInfoSite(String tln);
|
||||
|
||||
/**
|
||||
* Gets the INS registration site URL from the configuration.
|
||||
*
|
||||
* @return The INS registration site URL.
|
||||
*/
|
||||
public final String getINSFrontendSite() {
|
||||
return configurationManager.getString("server.site.frontend");
|
||||
}
|
||||
|
||||
/**
|
||||
* Class representing the folder structure for server certificates.
|
||||
*/
|
||||
public final class ServerCertificateFolderStructure {
|
||||
public final File certificatesFolder;
|
||||
|
||||
public final File publicFolder;
|
||||
public final File privateFolder;
|
||||
|
||||
public final File privateCAFolder;
|
||||
public final File privateServerFolder;
|
||||
|
||||
public final File publicCAFolder;
|
||||
public final File publicServerFolder;
|
||||
public final String caPrefix = "ca_ins_";
|
||||
public final String certPrefix = "cert_ins_";
|
||||
|
||||
public ServerCertificateFolderStructure() {
|
||||
certificatesFolder = new File("certificates");
|
||||
|
||||
publicFolder = new File(certificatesFolder, "public");
|
||||
privateFolder = new File(certificatesFolder, "private");
|
||||
|
||||
privateCAFolder = new File(privateFolder, "ca");
|
||||
privateServerFolder = new File(privateFolder, "server");
|
||||
|
||||
publicCAFolder = new File(publicFolder, "ca");
|
||||
publicServerFolder = new File(publicFolder, "server");
|
||||
|
||||
if (!certificatesFolder.exists()) certificatesFolder.mkdirs();
|
||||
|
||||
if (!publicFolder.exists()) publicFolder.mkdirs();
|
||||
if (!privateFolder.exists()) privateFolder.mkdirs();
|
||||
|
||||
if (!privateCAFolder.exists()) privateCAFolder.mkdirs();
|
||||
if (!privateServerFolder.exists()) privateServerFolder.mkdirs();
|
||||
|
||||
if (!publicCAFolder.exists()) publicCAFolder.mkdirs();
|
||||
if (!publicServerFolder.exists()) publicServerFolder.mkdirs();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
package org.openautonomousconnection.protocol.side.ins.events;
|
||||
|
||||
import dev.unlegitdqrk.unlegitlibrary.event.impl.Event;
|
||||
import lombok.Getter;
|
||||
import org.openautonomousconnection.protocol.annotations.ProtocolInfo;
|
||||
import org.openautonomousconnection.protocol.side.ins.ConnectedProtocolClient;
|
||||
import org.openautonomousconnection.protocol.versions.ProtocolVersion;
|
||||
|
||||
/**
|
||||
* Event triggered when a protocol client connects to the INS server.
|
||||
*/
|
||||
@ProtocolInfo(protocolSide = ProtocolVersion.ProtocolSide.INS)
|
||||
public final class ConnectedProtocolClientEvent extends Event {
|
||||
|
||||
@Getter
|
||||
private final ConnectedProtocolClient protocolClient;
|
||||
|
||||
public ConnectedProtocolClientEvent(ConnectedProtocolClient protocolClient) {
|
||||
this.protocolClient = protocolClient;
|
||||
}
|
||||
}
|
||||
@@ -1,44 +1,56 @@
|
||||
package org.openautonomousconnection.protocol.side.ins;
|
||||
package org.openautonomousconnection.protocol.side.server;
|
||||
|
||||
import dev.unlegitdqrk.unlegitlibrary.network.system.server.ConnectionHandler;
|
||||
import dev.unlegitdqrk.unlegitlibrary.event.EventListener;
|
||||
import dev.unlegitdqrk.unlegitlibrary.event.EventPriority;
|
||||
import dev.unlegitdqrk.unlegitlibrary.event.Listener;
|
||||
import dev.unlegitdqrk.unlegitlibrary.network.system.server.ConnectedClient;
|
||||
import dev.unlegitdqrk.unlegitlibrary.network.system.server.events.client.S_ClientDisconnectedEvent;
|
||||
import lombok.Getter;
|
||||
import org.openautonomousconnection.protocol.annotations.ProtocolInfo;
|
||||
import org.openautonomousconnection.protocol.packets.OACPacket;
|
||||
import org.openautonomousconnection.protocol.versions.ProtocolVersion;
|
||||
|
||||
/**
|
||||
* Represents a connected protocol client on the INS server side.
|
||||
*/
|
||||
@ProtocolInfo(protocolSide = ProtocolVersion.ProtocolSide.INS)
|
||||
public final class ConnectedProtocolClient {
|
||||
public class CustomConnectedClient extends EventListener {
|
||||
|
||||
/**
|
||||
* The connection handler associated with this protocol client.
|
||||
*/
|
||||
@Getter
|
||||
private final ConnectionHandler connectionHandler;
|
||||
private final ConnectedClient connection;
|
||||
|
||||
/**
|
||||
* The Protocol Server associated with this protocol client.
|
||||
*/
|
||||
@Getter
|
||||
private final ProtocolINSServer protocolINSServer;
|
||||
private final ProtocolCustomServer server;
|
||||
|
||||
/**
|
||||
* The protocol version of the connected client.
|
||||
*/
|
||||
private ProtocolVersion clientVersion = null;
|
||||
|
||||
public ConnectedProtocolClient(ConnectionHandler connectionHandler, ProtocolINSServer protocolINSServer) {
|
||||
this.connectionHandler = connectionHandler;
|
||||
this.protocolINSServer = protocolINSServer;
|
||||
@Getter
|
||||
private boolean clientVersionLoaded = false;
|
||||
|
||||
public CustomConnectedClient(ConnectedClient connection, ProtocolCustomServer protocolServer) throws Exception {
|
||||
this.connection = connection;
|
||||
this.server = protocolServer;
|
||||
protocolServer.getProtocolBridge().getProtocolValues().eventManager.registerListener(this);
|
||||
}
|
||||
|
||||
public synchronized void disconnect() {
|
||||
|
||||
try {
|
||||
connection.disconnect();
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
clientVersion = null;
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the protocol version of the connected client.
|
||||
* Defaults to PV_1_0_0_CLASSIC if not set.
|
||||
*
|
||||
* @return The protocol version of the client.
|
||||
* @return The protocol version of the client, defaults to PV_1_0_0_CLASSIC if not set.
|
||||
*/
|
||||
public ProtocolVersion getClientVersion() {
|
||||
return clientVersion == null ? ProtocolVersion.PV_1_0_0_CLASSIC : clientVersion;
|
||||
@@ -50,7 +62,11 @@ public final class ConnectedProtocolClient {
|
||||
* @param clientVersion The protocol version to set.
|
||||
*/
|
||||
public void setClientVersion(ProtocolVersion clientVersion) {
|
||||
if (clientVersion == null) this.clientVersion = clientVersion;
|
||||
if (clientVersionLoaded) return;
|
||||
if (this.clientVersion != null) return;
|
||||
|
||||
this.clientVersion = clientVersion;
|
||||
this.clientVersionLoaded = true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -113,11 +129,9 @@ public final class ConnectedProtocolClient {
|
||||
* @return True if the client is classic, false otherwise.
|
||||
*/
|
||||
public boolean isClassicClient() {
|
||||
// Check if the server version is classic
|
||||
return getClientVersion().getProtocolType() == ProtocolVersion.ProtocolType.CLASSIC;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks if the connected client supports classic protocol versions.
|
||||
*
|
||||
@@ -136,30 +150,36 @@ public final class ConnectedProtocolClient {
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the connected client supports the given packet's protocol version.
|
||||
* Checks if the connected client supports the protocol version of the given packet.
|
||||
*
|
||||
* @param packet The packet to check.
|
||||
* @param packet The packet to check support for.
|
||||
* @return True if the client supports the packet's protocol version, false otherwise.
|
||||
*/
|
||||
public boolean supportClientPacket(OACPacket packet) {
|
||||
return supportClientVersion(packet.getProtocolVersion());
|
||||
boolean compatible = false;
|
||||
|
||||
for (ProtocolVersion compatibleVersion : packet.getCompatibleVersions()) {
|
||||
if (!compatible) compatible = supportClientVersion(compatibleVersion);
|
||||
}
|
||||
|
||||
return compatible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the connected client supports the given protocol version.
|
||||
*
|
||||
* @param targetVersion The protocol version to check.
|
||||
* @param targetVersion The protocol version to check support for.
|
||||
* @return True if the client supports the target version, false otherwise.
|
||||
*/
|
||||
public boolean supportClientVersion(ProtocolVersion targetVersion) {
|
||||
// Check if the client version matches or is compatible with the target version
|
||||
// Check if the client version matches the target version or is compatible
|
||||
return getClientVersion() == targetVersion || getClientVersion().getCompatibleVersions().contains(targetVersion);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the connected client supports the given protocol.
|
||||
*
|
||||
* @param protocol The protocol to check.
|
||||
* @param protocol The protocol to check support for.
|
||||
* @return True if the client supports the protocol, false otherwise.
|
||||
*/
|
||||
public boolean supportClientProtocol(ProtocolVersion.Protocol protocol) {
|
||||
@@ -174,3 +194,4 @@ public final class ConnectedProtocolClient {
|
||||
return getClientVersion().getSupportedProtocols().contains(protocol) || yes;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,332 @@
|
||||
package org.openautonomousconnection.protocol.side.server;
|
||||
|
||||
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.server.NetworkServer;
|
||||
import dev.unlegitdqrk.unlegitlibrary.network.system.server.events.client.S_ClientDisconnectedEvent;
|
||||
import dev.unlegitdqrk.unlegitlibrary.network.system.server.events.state.ServerStoppedEvent;
|
||||
import dev.unlegitdqrk.unlegitlibrary.network.system.utils.ClientAuthMode;
|
||||
import dev.unlegitdqrk.unlegitlibrary.network.utils.NetworkUtils;
|
||||
import dev.unlegitdqrk.unlegitlibrary.network.utils.PemUtils;
|
||||
import lombok.Getter;
|
||||
import org.openautonomousconnection.protocol.ProtocolBridge;
|
||||
import org.openautonomousconnection.protocol.side.server.events.S_CustomClientDisconnectedEvent;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.security.KeyStore;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.*;
|
||||
|
||||
public abstract class ProtocolCustomServer extends EventListener {
|
||||
|
||||
/**
|
||||
* Structure for server.
|
||||
*/
|
||||
private final ServerCertificateFolderStructure folderStructure;
|
||||
/**
|
||||
* Certificate files for SSL.
|
||||
*/
|
||||
private final File certFile;
|
||||
/**
|
||||
* Certificate files for SSL.
|
||||
*/
|
||||
private final File keyFile;
|
||||
/**
|
||||
* List of connected web clients.
|
||||
*/
|
||||
private final List<CustomConnectedClient> clients;
|
||||
/**
|
||||
* The reference to the ProtocolBridge Object
|
||||
*/
|
||||
private ProtocolBridge protocolBridge;
|
||||
/**
|
||||
* The network server handling pipeline connections.
|
||||
*/
|
||||
private NetworkServer network;
|
||||
|
||||
/**
|
||||
* Initializes the web server with the given configuration, authentication, and rules files.
|
||||
*
|
||||
* @throws Exception If an error occurs during initialization.
|
||||
*/
|
||||
public ProtocolCustomServer(String caPrefix, String certPrefix) throws Exception {
|
||||
// Initialize the list of connected clients
|
||||
this.clients = new ArrayList<>();
|
||||
|
||||
// Set up folder structure for certificates
|
||||
folderStructure = new ServerCertificateFolderStructure(caPrefix, certPrefix);
|
||||
|
||||
// Check for necessary certificate files
|
||||
checkFileExists(folderStructure.publicServerFolder, folderStructure.certPrefix, ".crt");
|
||||
checkFileExists(folderStructure.privateServerFolder, folderStructure.certPrefix, ".key");
|
||||
|
||||
// Set up certificate files based on public IP address
|
||||
this.certFile = new File(folderStructure.publicServerFolder, folderStructure.certPrefix + NetworkUtils.getPublicIPAddress() + ".crt");
|
||||
this.keyFile = new File(folderStructure.privateServerFolder, folderStructure.certPrefix + NetworkUtils.getPublicIPAddress() + ".key");
|
||||
}
|
||||
|
||||
public final ServerCertificateFolderStructure getFolderStructure() {
|
||||
return folderStructure;
|
||||
}
|
||||
|
||||
public final ProtocolBridge getProtocolBridge() {
|
||||
return protocolBridge;
|
||||
}
|
||||
|
||||
public final List<CustomConnectedClient> getClients() {
|
||||
return clients;
|
||||
}
|
||||
|
||||
public final NetworkServer getNetwork() {
|
||||
return network;
|
||||
}
|
||||
|
||||
protected final void setNetwork(NetworkServer network) {
|
||||
this.network = network;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects the ProtocolBridge.
|
||||
*
|
||||
* @param bridge the Bridge instance.
|
||||
* @throws IOException when NetworkServer failed to create.
|
||||
*/
|
||||
public final void attachBridge(ProtocolBridge bridge, String keyPassword, boolean ssl, ClientAuthMode authMode) throws Exception {
|
||||
if (this.protocolBridge != null)
|
||||
throw new IllegalStateException("ProtocolBridge already attached!");
|
||||
|
||||
this.protocolBridge = bridge;
|
||||
bridge.getProtocolValues().eventManager.registerListener(this);
|
||||
|
||||
if (ssl) {
|
||||
char[] keyPass = new char[0];
|
||||
if (keyPassword != null) keyPass = keyPassword.toCharArray();
|
||||
|
||||
KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
|
||||
trustStore.load(null, null);
|
||||
|
||||
int caIndex = 0;
|
||||
for (File caPem : FileUtils.listFiles(folderStructure.publicCAFolder, ".pem", ".crt", ".cer")) {
|
||||
if (caPem.getName().endsWith(".srl")) continue;
|
||||
if (!caPem.getName().startsWith(folderStructure.getCaPrefix())) continue;
|
||||
|
||||
X509Certificate caCert = PemUtils.loadCertificate(caPem);
|
||||
trustStore.setCertificateEntry("root-ca-" + (caIndex++), caCert);
|
||||
}
|
||||
|
||||
// Build server key material (private key + certificate chain)
|
||||
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
|
||||
keyStore.load(null, null);
|
||||
|
||||
Map<String, File> keyFiles = new HashMap<>();
|
||||
Map<String, File> certFiles = new HashMap<>();
|
||||
|
||||
for (File f : FileUtils.listFiles(folderStructure.privateServerFolder, ".key")) {
|
||||
if (f.getName().endsWith(".srl")) continue;
|
||||
if (!f.getName().startsWith(folderStructure.getCertPrefix())) continue;
|
||||
keyFiles.put(FileUtils.stripExt(f.getName()), f);
|
||||
}
|
||||
|
||||
// Allow cert files in public server folder as .pem/.crt/.cer
|
||||
for (File f : FileUtils.listFiles(folderStructure.publicServerFolder, ".pem", ".crt", ".cer")) {
|
||||
if (f.getName().endsWith(".srl")) continue;
|
||||
if (!f.getName().startsWith(folderStructure.getCertPrefix())) continue;
|
||||
certFiles.put(FileUtils.stripExt(f.getName()), f);
|
||||
}
|
||||
|
||||
// Load all CA certs once (for chain building)
|
||||
List<X509Certificate> caCerts = new ArrayList<>();
|
||||
for (File caPem : FileUtils.listFiles(folderStructure.publicCAFolder, ".pem", ".crt", ".cer")) {
|
||||
if (caPem.getName().endsWith(".srl")) continue;
|
||||
if (!caPem.getName().startsWith(folderStructure.getCaPrefix())) continue;
|
||||
caCerts.add(PemUtils.loadCertificate(caPem));
|
||||
}
|
||||
|
||||
int serverIndex = 0;
|
||||
for (Map.Entry<String, File> e : keyFiles.entrySet()) {
|
||||
String base = e.getKey();
|
||||
File keyFile = e.getValue();
|
||||
|
||||
File certFile = certFiles.get(base);
|
||||
if (certFile == null) {
|
||||
// If your naming differs between private key and public cert, log it and skip
|
||||
continue;
|
||||
}
|
||||
|
||||
PrivateKey privateKey = PemUtils.loadPrivateKey(keyFile);
|
||||
X509Certificate leaf = PemUtils.loadCertificate(certFile);
|
||||
|
||||
// Build a minimal chain: leaf + issuer CA if found (or all CAs as fallback)
|
||||
List<X509Certificate> chain = new ArrayList<>();
|
||||
chain.add(leaf);
|
||||
|
||||
// Try to find matching issuer CA(s) by Subject/Issuer DN
|
||||
X509Certificate current = leaf;
|
||||
for (int depth = 0; depth < 5; depth++) {
|
||||
X509Certificate issuer = findIssuer(current, caCerts);
|
||||
if (issuer == null) break;
|
||||
chain.add(issuer);
|
||||
if (issuer.getSubjectX500Principal().equals(issuer.getIssuerX500Principal())) {
|
||||
// self-signed root reached
|
||||
break;
|
||||
}
|
||||
current = issuer;
|
||||
}
|
||||
|
||||
String alias = "server-" + (serverIndex++);
|
||||
keyStore.setKeyEntry(alias, privateKey, keyPass, chain.toArray(new X509Certificate[0]));
|
||||
}
|
||||
|
||||
// If still empty, fail fast with a clear error instead of handshake_failure later
|
||||
if (!keyStore.aliases().hasMoreElements()) {
|
||||
throw new IllegalStateException(
|
||||
"No server key entries loaded. Ensure private .key and public .crt/.pem names match and start with prefix "
|
||||
+ folderStructure.getCertPrefix()
|
||||
);
|
||||
}
|
||||
|
||||
network = new NetworkServer.Builder()
|
||||
.packetHandler(bridge.getProtocolValues().packetHandler)
|
||||
.eventManager(bridge.getProtocolValues().eventManager)
|
||||
.sslEnabled(true)
|
||||
.clientAuthMode(authMode)
|
||||
.keyStore(keyStore, keyPass)
|
||||
.trustStore(trustStore)
|
||||
.build();
|
||||
} else {
|
||||
network = new NetworkServer.Builder()
|
||||
.packetHandler(bridge.getProtocolValues().packetHandler)
|
||||
.eventManager(bridge.getProtocolValues().eventManager)
|
||||
.sslEnabled(false)
|
||||
.clientAuthMode(ClientAuthMode.NONE)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@Listener(priority = EventPriority.HIGHEST)
|
||||
public final void onStop(ServerStoppedEvent event) {
|
||||
if (event.getServer() == network) {
|
||||
protocolBridge.getProtocolValues().eventManager.unregisterListener(this);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieves a connected web client by its client ID.
|
||||
*
|
||||
* @param clientID The client ID to search for.
|
||||
* @return The connected web client with the specified ID, or null if not found.
|
||||
*/
|
||||
public final CustomConnectedClient getClientByID(UUID clientID) {
|
||||
for (CustomConnectedClient client : clients)
|
||||
if (client.getConnection().getUniqueID().equals(clientID)) return client;
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to find the issuer certificate of {@code cert} within {@code candidates}.
|
||||
*
|
||||
* @param cert the certificate whose issuer should be found
|
||||
* @param candidates possible issuer certificates
|
||||
* @return issuer certificate if found, otherwise null
|
||||
*/
|
||||
private X509Certificate findIssuer(X509Certificate cert, List<X509Certificate> candidates) {
|
||||
for (X509Certificate ca : candidates) {
|
||||
if (cert.getIssuerX500Principal().equals(ca.getSubjectX500Principal())) {
|
||||
return ca;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the required certificate files exist in the specified folder.
|
||||
*
|
||||
* @param folder The folder to check for certificate files.
|
||||
* @param prefix The prefix of the certificate files.
|
||||
* @param extension The extension of the certificate files.
|
||||
* @throws CertificateException If a required certificate file is missing or invalid.
|
||||
* @throws IOException If an I/O error occurs while checking the files.
|
||||
*/
|
||||
private void checkFileExists(File folder, String prefix, String extension) throws CertificateException, IOException {
|
||||
// Ensure the folder exists
|
||||
if (folder == null) throw new FileNotFoundException("Folder does not exist");
|
||||
|
||||
// List all files in the folder
|
||||
File[] files = folder.listFiles();
|
||||
if (files == null || files.length == 0)
|
||||
throw new FileNotFoundException("Folder " + folder.getAbsolutePath() + " is empty");
|
||||
|
||||
// Check for the required certificate file
|
||||
for (File file : files) {
|
||||
if (!file.getName().startsWith(prefix))
|
||||
throw new CertificateException(file.getAbsolutePath() + " is not valid");
|
||||
}
|
||||
}
|
||||
|
||||
@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())) {
|
||||
protocolBridge.getProtocolValues().eventManager.executeEvent(new S_CustomClientDisconnectedEvent(client));
|
||||
clients.remove(client);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the folder structure for server certificates.
|
||||
*/
|
||||
public final class ServerCertificateFolderStructure {
|
||||
public final File certificatesFolder;
|
||||
|
||||
public final File publicFolder;
|
||||
public final File privateFolder;
|
||||
|
||||
public final File privateCAFolder;
|
||||
public final File privateServerFolder;
|
||||
|
||||
public final File publicCAFolder;
|
||||
public final File publicServerFolder;
|
||||
|
||||
@Getter
|
||||
private String caPrefix = "ca_server_";
|
||||
@Getter
|
||||
private String certPrefix = "cert_server_";
|
||||
|
||||
public ServerCertificateFolderStructure(String caPrefix, String certPrefix) {
|
||||
this.caPrefix = "ca_" + caPrefix + "_";
|
||||
this.certPrefix = "cert_" + certPrefix + "_";
|
||||
|
||||
certificatesFolder = new File("certificates");
|
||||
|
||||
publicFolder = new File(certificatesFolder, "public");
|
||||
privateFolder = new File(certificatesFolder, "private");
|
||||
|
||||
privateCAFolder = new File(privateFolder, "ca");
|
||||
privateServerFolder = new File(privateFolder, "server");
|
||||
|
||||
publicCAFolder = new File(publicFolder, "ca");
|
||||
publicServerFolder = new File(publicFolder, "server");
|
||||
|
||||
if (!certificatesFolder.exists()) certificatesFolder.mkdirs();
|
||||
|
||||
if (!publicFolder.exists()) publicFolder.mkdirs();
|
||||
if (!privateFolder.exists()) privateFolder.mkdirs();
|
||||
|
||||
if (!privateCAFolder.exists()) privateCAFolder.mkdirs();
|
||||
if (!privateServerFolder.exists()) privateServerFolder.mkdirs();
|
||||
|
||||
if (!publicCAFolder.exists()) publicCAFolder.mkdirs();
|
||||
if (!publicServerFolder.exists()) publicServerFolder.mkdirs();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package org.openautonomousconnection.protocol.side.server.events;
|
||||
|
||||
import dev.unlegitdqrk.unlegitlibrary.event.impl.Event;
|
||||
import lombok.Getter;
|
||||
import org.openautonomousconnection.protocol.side.server.CustomConnectedClient;
|
||||
|
||||
/**
|
||||
* Event triggered when a web client connects to the web server.
|
||||
*/
|
||||
public final class S_CustomClientConnectedEvent extends Event {
|
||||
|
||||
/**
|
||||
* The connected web client.
|
||||
*/
|
||||
@Getter
|
||||
private final CustomConnectedClient client;
|
||||
|
||||
public S_CustomClientConnectedEvent(CustomConnectedClient client) {
|
||||
this.client = client;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package org.openautonomousconnection.protocol.side.server.events;
|
||||
|
||||
import dev.unlegitdqrk.unlegitlibrary.event.impl.Event;
|
||||
import lombok.Getter;
|
||||
import org.openautonomousconnection.protocol.side.server.CustomConnectedClient;
|
||||
|
||||
/**
|
||||
* Event triggered when a web client connects to the web server.
|
||||
*/
|
||||
public final class S_CustomClientDisconnectedEvent extends Event {
|
||||
|
||||
/**
|
||||
* The connected web client.
|
||||
*/
|
||||
@Getter
|
||||
private final CustomConnectedClient client;
|
||||
|
||||
public S_CustomClientDisconnectedEvent(CustomConnectedClient client) {
|
||||
this.client = client;
|
||||
}
|
||||
}
|
||||
@@ -1,734 +0,0 @@
|
||||
package org.openautonomousconnection.protocol.side.web;
|
||||
|
||||
import dev.unlegitdqrk.unlegitlibrary.network.system.server.ConnectionHandler;
|
||||
import lombok.Getter;
|
||||
import org.openautonomousconnection.protocol.annotations.ProtocolInfo;
|
||||
import org.openautonomousconnection.protocol.packets.OACPacket;
|
||||
import org.openautonomousconnection.protocol.side.web.managers.AuthManager;
|
||||
import org.openautonomousconnection.protocol.side.web.managers.RuleManager;
|
||||
import org.openautonomousconnection.protocol.side.web.managers.SessionManager;
|
||||
import org.openautonomousconnection.protocol.versions.ProtocolVersion;
|
||||
|
||||
import javax.net.ssl.SSLSocket;
|
||||
import java.io.*;
|
||||
import java.net.URLDecoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Represents a connected web client.
|
||||
* Manages the connection, handles HTTP requests, and serves files.
|
||||
*/
|
||||
@ProtocolInfo(protocolSide = ProtocolVersion.ProtocolSide.WEB)
|
||||
public final class ConnectedWebClient {
|
||||
|
||||
/**
|
||||
* The connection handler associated with this web client.
|
||||
*/
|
||||
@Getter
|
||||
private final ConnectionHandler pipelineConnection;
|
||||
|
||||
/**
|
||||
* The SSL socket for the web client connection.
|
||||
*/
|
||||
@Getter
|
||||
private SSLSocket webSocket;
|
||||
|
||||
/**
|
||||
* The output stream for sending data to the client.
|
||||
*/
|
||||
private ObjectOutputStream outputStream;
|
||||
|
||||
/**
|
||||
* The input stream for receiving data from the client.
|
||||
*/
|
||||
private ObjectInputStream inputStream;
|
||||
|
||||
/**
|
||||
* The protocol version of the connected client.
|
||||
*/
|
||||
private ProtocolVersion clientVersion = null;
|
||||
/**
|
||||
* Indicates if the client version has been loaded.
|
||||
*/
|
||||
@Getter
|
||||
private boolean clientVersionLoaded = false;
|
||||
/**
|
||||
* The reference to the ProtocolWebServer Object
|
||||
*/
|
||||
@Getter
|
||||
private ProtocolWebServer protocolWebServer;
|
||||
|
||||
/**
|
||||
* Constructs a ConnectedWebClient with the given connection handler.
|
||||
*
|
||||
* @param pipelineConnection The connection handler for the web client.
|
||||
*/
|
||||
public ConnectedWebClient(ConnectionHandler pipelineConnection) {
|
||||
this.pipelineConnection = pipelineConnection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an HTTP redirect response to the client.
|
||||
*
|
||||
* @param out The output stream to send the response to.
|
||||
* @param location The URL to redirect to.
|
||||
* @param cookies Optional cookies to set in the response.
|
||||
* @throws IOException If an I/O error occurs.
|
||||
*/
|
||||
private static void sendRedirect(OutputStream out, String location, Map<String, String> cookies) throws IOException {
|
||||
// Send HTTP 302 Found response with Location header
|
||||
out.write(("OAC 302 Found\r\n").getBytes());
|
||||
out.write(("Location: " + location + "\r\n").getBytes());
|
||||
|
||||
// Set cookies if provided
|
||||
if (cookies != null) {
|
||||
for (var entry : cookies.entrySet()) {
|
||||
out.write((entry.getKey() + ": " + entry.getValue() + "\r\n").getBytes());
|
||||
}
|
||||
}
|
||||
|
||||
// End of headers
|
||||
out.write("\r\n".getBytes());
|
||||
out.flush();
|
||||
} /**
|
||||
* Thread for receiving data from the client.
|
||||
*/
|
||||
private final Thread receiveThread = new Thread(this::receive);
|
||||
|
||||
/**
|
||||
* Parses POST parameters from the input stream.
|
||||
*
|
||||
* @param in The input stream to read from.
|
||||
* @return A map of POST parameter names to values.
|
||||
* @throws IOException If an I/O error occurs.
|
||||
*/
|
||||
private static Map<String, String> parsePostParams(InputStream in) throws IOException {
|
||||
// Read the entire input stream into a string
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
|
||||
StringBuilder sb = new StringBuilder();
|
||||
while (reader.ready()) {
|
||||
sb.append((char) reader.read());
|
||||
}
|
||||
|
||||
// Split the string into key-value pairs and decode them
|
||||
Map<String, String> map = new HashMap<>();
|
||||
String[] pairs = sb.toString().split("&");
|
||||
for (String p : pairs) {
|
||||
// Split each pair into key and value
|
||||
String[] kv = p.split("=", 2);
|
||||
if (kv.length == 2)
|
||||
// Decode and store in the map
|
||||
map.put(URLDecoder.decode(kv[0], StandardCharsets.UTF_8), URLDecoder.decode(kv[1], StandardCharsets.UTF_8));
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes a file path to prevent directory traversal attacks.
|
||||
*
|
||||
* @param path The raw file path.
|
||||
* @return The normalized file path.
|
||||
*/
|
||||
private static String normalizePath(String path) {
|
||||
// Replace backslashes with forward slashes and remove ".." segments
|
||||
path = path.replace("/", File.separator).replace("\\", "/");
|
||||
// Remove any ".." segments to prevent directory traversal
|
||||
while (path.contains("..")) path = path.replace("..", "");
|
||||
|
||||
// Remove leading slashes
|
||||
if (path.startsWith("/")) path = path.substring(1);
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses query parameters from a raw URL path.
|
||||
*
|
||||
* @param rawPath The raw URL path containing query parameters.
|
||||
* @return A map of query parameter names to values.
|
||||
*/
|
||||
private static Map<String, String> parseQueryParams(String rawPath) {
|
||||
// Extract query parameters from the URL path
|
||||
Map<String, String> map = new HashMap<>();
|
||||
if (rawPath.contains("?")) {
|
||||
// Split the query string into key-value pairs
|
||||
String[] params = rawPath.substring(rawPath.indexOf("?") + 1).split("&");
|
||||
for (String p : params) {
|
||||
// Split each pair into key and value
|
||||
String[] kv = p.split("=");
|
||||
if (kv.length == 2) map.put(kv[0], kv[1]);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the request is a multipart/form-data request.
|
||||
*
|
||||
* @param headers The HTTP headers of the request.
|
||||
* @return True if the request is multipart/form-data, false otherwise.
|
||||
*/
|
||||
private static boolean isMultipart(Map<String, String> headers) {
|
||||
String contentType = headers.get("content-type");
|
||||
return contentType != null && contentType.startsWith("multipart/form-data");
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a multipart/form-data request, saving uploaded files to the specified directory.
|
||||
*
|
||||
* @param in The input stream to read the request body from.
|
||||
* @param headers The HTTP headers of the request.
|
||||
* @param uploadDir The directory to save uploaded files to.
|
||||
* @throws IOException If an I/O error occurs.
|
||||
*/
|
||||
private static void handleMultipart(InputStream in, Map<String, String> headers, File uploadDir) throws IOException {
|
||||
// Ensure the upload directory exists
|
||||
if (!uploadDir.exists()) uploadDir.mkdirs();
|
||||
|
||||
// Extract the boundary from the Content-Type header
|
||||
String contentType = headers.get("content-type");
|
||||
String boundary = "--" + contentType.split("boundary=")[1];
|
||||
|
||||
// Read the entire request body into a buffer
|
||||
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
|
||||
byte[] lineBuffer = new byte[8192];
|
||||
int read;
|
||||
while ((read = in.read(lineBuffer)) != -1) {
|
||||
buffer.write(lineBuffer, 0, read);
|
||||
if (buffer.size() > 10 * 1024 * 1024) break; // 10 MB max
|
||||
}
|
||||
|
||||
// Parse the multipart data
|
||||
String data = buffer.toString(StandardCharsets.UTF_8);
|
||||
String[] parts = data.split(boundary);
|
||||
|
||||
// Process each part
|
||||
for (String part : parts) {
|
||||
if (part.contains("Content-Disposition")) {
|
||||
String name = null;
|
||||
String filename = null;
|
||||
|
||||
// Extract headers from the part
|
||||
for (String headerLine : part.split("\r\n")) {
|
||||
if (headerLine.startsWith("Content-Disposition")) {
|
||||
if (headerLine.contains("filename=\"")) {
|
||||
int start = headerLine.indexOf("filename=\"") + 10;
|
||||
int end = headerLine.indexOf("\"", start);
|
||||
filename = headerLine.substring(start, end);
|
||||
}
|
||||
if (headerLine.contains("name=\"")) {
|
||||
int start = headerLine.indexOf("name=\"") + 6;
|
||||
int end = headerLine.indexOf("\"", start);
|
||||
name = headerLine.substring(start, end);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save the file if a filename is provided
|
||||
if (filename != null && !filename.isEmpty()) {
|
||||
int headerEnd = part.indexOf("\r\n\r\n");
|
||||
byte[] fileData = part.substring(headerEnd + 4).getBytes(StandardCharsets.UTF_8);
|
||||
File outFile = new File(uploadDir, filename);
|
||||
Files.write(outFile.toPath(), fileData);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an response to the client.
|
||||
*
|
||||
* @param out
|
||||
* @param code
|
||||
* @param file
|
||||
* @param headers
|
||||
* @throws IOException
|
||||
*/
|
||||
private static void sendResponse(OutputStream out, int code, File file, Map<String, String> headers) throws IOException {
|
||||
byte[] body = Files.readAllBytes(file.toPath());
|
||||
sendResponse(out, code, body, "text/html", headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an response to the client.
|
||||
*
|
||||
* @param out The output stream to send the response to.
|
||||
* @param code The HTTP status code.
|
||||
* @param file The file to read the response body from.
|
||||
* @throws IOException If an I/O error occurs.
|
||||
*/
|
||||
private static void sendResponse(OutputStream out, int code, File file) throws IOException {
|
||||
sendResponse(out, code, Files.readString(file.toPath()), "text/html");
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an response to the client.
|
||||
*
|
||||
* @param out The output stream to send the response to.
|
||||
* @param code The HTTP status code.
|
||||
* @param body The response body as a string.
|
||||
* @param contentType The content type of the response.
|
||||
* @throws IOException If an I/O error occurs.
|
||||
*/
|
||||
private static void sendResponse(OutputStream out, int code, String body, String contentType) throws IOException {
|
||||
sendResponse(out, code, body.getBytes(StandardCharsets.UTF_8), contentType, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an response to the client.
|
||||
*
|
||||
* @param out The output stream to send the response to.
|
||||
* @param code The HTTP status code.
|
||||
* @param file The file to read the response body from.
|
||||
* @param contentType The content type of the response.
|
||||
* @throws IOException If an I/O error occurs.
|
||||
*/
|
||||
private static void sendResponse(OutputStream out, int code, File file, String contentType) throws IOException {
|
||||
byte[] bytes = Files.readAllBytes(file.toPath());
|
||||
sendResponse(out, code, bytes, contentType, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an response to the client.
|
||||
*
|
||||
* @param out The output stream to send the response to.
|
||||
* @param code The HTTP status code.
|
||||
* @param body The response body as a byte array.
|
||||
* @param contentType The content type of the response.
|
||||
* @param headers Additional headers to include in the response.
|
||||
* @throws IOException If an I/O error occurs.
|
||||
*/
|
||||
private static void sendResponse(OutputStream out, int code, byte[] body, String contentType, Map<String, String> headers) throws IOException {
|
||||
// Send response status line and headers
|
||||
out.write(("OAC " + code + " " + getStatusText(code) + "\r\n").getBytes());
|
||||
out.write(("Content-Type: " + contentType + "\r\n").getBytes());
|
||||
out.write(("Content-Length: " + body.length + "\r\n").getBytes());
|
||||
|
||||
// Write additional headers if provided
|
||||
if (headers != null) headers.forEach((k, v) -> {
|
||||
try {
|
||||
out.write((k + ": " + v + "\r\n").getBytes());
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
});
|
||||
|
||||
// End of headers
|
||||
out.write("\r\n".getBytes());
|
||||
out.write(body);
|
||||
out.flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the standard status text for a given status code.
|
||||
*
|
||||
* @param code The status code.
|
||||
* @return The corresponding status text.
|
||||
*/
|
||||
private static String getStatusText(int code) {
|
||||
return switch (code) {
|
||||
case 200 -> "OK";
|
||||
case 301 -> "Moved Permanently";
|
||||
case 302 -> "Found";
|
||||
case 400 -> "Bad Request";
|
||||
case 401 -> "Unauthorized";
|
||||
case 403 -> "Forbidden";
|
||||
case 404 -> "Not Found";
|
||||
case 500 -> "Internal Server Error";
|
||||
default -> "Unknown";
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the content type based on the file extension.
|
||||
*
|
||||
* @param name The file name.
|
||||
* @return The corresponding content type.
|
||||
*/
|
||||
private static String getContentType(String name) {
|
||||
return switch (name.substring(name.lastIndexOf('.') + 1).toLowerCase()) {
|
||||
case "html", "php" -> "text/html";
|
||||
case "js" -> "text/javascript";
|
||||
case "css" -> "text/css";
|
||||
case "json" -> "application/json";
|
||||
case "png" -> "image/png";
|
||||
case "jpg", "jpeg" -> "image/jpeg";
|
||||
case "mp4" -> "video/mp4";
|
||||
case "mp3" -> "audio/mpeg3";
|
||||
case "wav" -> "audio/wav";
|
||||
case "pdf" -> "application/pdf";
|
||||
default -> "text/plain";
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a PHP file by executing it with the PHP interpreter and captures cookies.
|
||||
*
|
||||
* @param file The PHP file to render.
|
||||
* @return A PHPResponse containing the output and cookies.
|
||||
* @throws IOException If an I/O error occurs.
|
||||
* @throws InterruptedException If the process is interrupted.
|
||||
*/
|
||||
private static PHPResponse renderPHPWithCookies(File file) throws IOException, InterruptedException {
|
||||
// Execute the PHP file using the PHP interpreter
|
||||
ProcessBuilder pb = new ProcessBuilder("php", file.getAbsolutePath());
|
||||
pb.redirectErrorStream(true);
|
||||
Process p = pb.start();
|
||||
|
||||
// Capture the output of the PHP process
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||
InputStream processIn = p.getInputStream();
|
||||
byte[] buf = new byte[8192];
|
||||
int read;
|
||||
while ((read = processIn.read(buf)) != -1) {
|
||||
output.write(buf, 0, read);
|
||||
}
|
||||
p.waitFor();
|
||||
|
||||
// Parse the output to separate headers and body, and extract cookies
|
||||
String fullOutput = output.toString(StandardCharsets.UTF_8);
|
||||
Map<String, String> cookies = new HashMap<>();
|
||||
|
||||
// Split headers and body
|
||||
String[] parts = fullOutput.split("\r\n\r\n", 2);
|
||||
String body;
|
||||
if (parts.length == 2) {
|
||||
// Get headers and body
|
||||
String headers = parts[0];
|
||||
body = parts[1];
|
||||
|
||||
// Extract cookies from headers
|
||||
for (String headerLine : headers.split("\r\n")) {
|
||||
if (headerLine.toLowerCase().startsWith("set-cookie:")) {
|
||||
String cookie = headerLine.substring("set-cookie:".length()).trim();
|
||||
String[] kv = cookie.split(";", 2);
|
||||
String[] pair = kv[0].split("=", 2);
|
||||
if (pair.length == 2) cookies.put(pair[0], pair[1]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No headers, only body
|
||||
body = fullOutput;
|
||||
}
|
||||
|
||||
return new PHPResponse(body, cookies);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the SSL socket for the web client and starts the receive thread.
|
||||
*
|
||||
* @param webSocket The SSL socket to set.
|
||||
*/
|
||||
public void setWebSocket(SSLSocket webSocket) {
|
||||
if (webSocket != null) this.webSocket = webSocket;
|
||||
this.receiveThread.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the web client is currently connected.
|
||||
*
|
||||
* @return True if connected, false otherwise.
|
||||
*/
|
||||
public boolean isConnected() {
|
||||
return this.webSocket != null && this.webSocket.isConnected() && !this.webSocket.isClosed() && this.receiveThread.isAlive() && pipelineConnection.isConnected();
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnects the web client, closing streams and the socket.
|
||||
*
|
||||
* @return True if disconnection was successful, false if already disconnected.
|
||||
*/
|
||||
public synchronized boolean disconnect() {
|
||||
if (!this.isConnected()) {
|
||||
return false;
|
||||
} else {
|
||||
// Disconnect the underlying connection handler
|
||||
pipelineConnection.disconnect();
|
||||
|
||||
// Interrupt the receive thread if it's still alive
|
||||
if (this.receiveThread.isAlive()) {
|
||||
this.receiveThread.interrupt();
|
||||
}
|
||||
|
||||
try {
|
||||
// Close streams and the socket
|
||||
this.outputStream.close();
|
||||
this.inputStream.close();
|
||||
this.webSocket.close();
|
||||
} catch (IOException var2) {
|
||||
}
|
||||
|
||||
// Nullify references
|
||||
this.webSocket = null;
|
||||
this.outputStream = null;
|
||||
this.inputStream = null;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the protocol version of the connected client.
|
||||
*
|
||||
* @return The protocol version of the client, defaults to PV_1_0_0_CLASSIC if not set.
|
||||
*/
|
||||
public ProtocolVersion getClientVersion() {
|
||||
return clientVersion == null ? ProtocolVersion.PV_1_0_0_CLASSIC : clientVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the protocol version of the connected client.
|
||||
*
|
||||
* @param clientVersion The protocol version to set.
|
||||
*/
|
||||
public void setClientVersion(ProtocolVersion clientVersion) {
|
||||
if (!clientVersionLoaded) clientVersionLoaded = true;
|
||||
if (clientVersion == null) this.clientVersion = clientVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the connected client is a stable client.
|
||||
*
|
||||
* @return True if the client is stable, false otherwise.
|
||||
*/
|
||||
public boolean isStableClient() {
|
||||
// Check if the server version is stable
|
||||
return !isBetaClient() && !isClassicClient();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the connected client supports stable protocol versions.
|
||||
*
|
||||
* @return True if the client supports stable versions, false otherwise.
|
||||
*/
|
||||
public boolean supportClientStable() {
|
||||
boolean yes = false;
|
||||
for (ProtocolVersion compatibleVersion : getClientVersion().getCompatibleVersions()) {
|
||||
// Check if compatible version is stable
|
||||
yes = compatibleVersion.getProtocolType() == ProtocolVersion.ProtocolType.STABLE;
|
||||
if (yes) break;
|
||||
}
|
||||
|
||||
// Check if the client version is stable
|
||||
return isStableClient() || yes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the connected client is a beta client.
|
||||
*
|
||||
* @return True if the client is beta, false otherwise.
|
||||
*/
|
||||
public boolean isBetaClient() {
|
||||
// Check if the server version is beta
|
||||
return getClientVersion().getProtocolType() == ProtocolVersion.ProtocolType.BETA;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the connected client supports beta protocol versions.
|
||||
*
|
||||
* @return True if the client supports beta versions, false otherwise.
|
||||
*/
|
||||
public boolean supportClientBeta() {
|
||||
boolean yes = false;
|
||||
for (ProtocolVersion compatibleVersion : getClientVersion().getCompatibleVersions()) {
|
||||
// Check if compatible version is beta
|
||||
yes = compatibleVersion.getProtocolType() == ProtocolVersion.ProtocolType.BETA;
|
||||
if (yes) break;
|
||||
}
|
||||
|
||||
// Check if the client version is beta
|
||||
return isBetaClient() || yes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the connected client is a classic client.
|
||||
*
|
||||
* @return True if the client is classic, false otherwise.
|
||||
*/
|
||||
public boolean isClassicClient() {
|
||||
return getClientVersion().getProtocolType() == ProtocolVersion.ProtocolType.CLASSIC;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the connected client supports classic protocol versions.
|
||||
*
|
||||
* @return True if the client supports classic versions, false otherwise.
|
||||
*/
|
||||
public boolean supportClientClassic() {
|
||||
boolean yes = false;
|
||||
for (ProtocolVersion compatibleVersion : getClientVersion().getCompatibleVersions()) {
|
||||
// Check if compatible version is classic
|
||||
yes = compatibleVersion.getProtocolType() == ProtocolVersion.ProtocolType.CLASSIC;
|
||||
if (yes) break;
|
||||
}
|
||||
|
||||
// Check if the client version is classic
|
||||
return isClassicClient() || yes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the connected client supports the protocol version of the given packet.
|
||||
*
|
||||
* @param packet The packet to check support for.
|
||||
* @return True if the client supports the packet's protocol version, false otherwise.
|
||||
*/
|
||||
public boolean supportClientPacket(OACPacket packet) {
|
||||
return supportClientVersion(packet.getProtocolVersion());
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the connected client supports the given protocol version.
|
||||
*
|
||||
* @param targetVersion The protocol version to check support for.
|
||||
* @return True if the client supports the target version, false otherwise.
|
||||
*/
|
||||
public boolean supportClientVersion(ProtocolVersion targetVersion) {
|
||||
// Check if the client version matches the target version or is compatible
|
||||
return getClientVersion() == targetVersion || getClientVersion().getCompatibleVersions().contains(targetVersion);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the connected client supports the given protocol.
|
||||
*
|
||||
* @param protocol The protocol to check support for.
|
||||
* @return True if the client supports the protocol, false otherwise.
|
||||
*/
|
||||
public boolean supportClientProtocol(ProtocolVersion.Protocol protocol) {
|
||||
boolean yes = false;
|
||||
for (ProtocolVersion compatibleVersion : getClientVersion().getCompatibleVersions()) {
|
||||
// Check if compatible version supports the protocol
|
||||
yes = compatibleVersion.getSupportedProtocols().contains(protocol);
|
||||
if (yes) break;
|
||||
}
|
||||
|
||||
// Check if the client version supports the protocol
|
||||
return getClientVersion().getSupportedProtocols().contains(protocol) || yes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Receives and processes requests from the client.
|
||||
* Handles authentication, file serving, and PHP rendering.
|
||||
*/
|
||||
private void receive() {
|
||||
try {
|
||||
while (this.isConnected()) {
|
||||
Object received = this.inputStream.readObject();
|
||||
|
||||
try (InputStream in = webSocket.getInputStream(); OutputStream out = webSocket.getOutputStream()) {
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
|
||||
String line;
|
||||
String path = "/main.html";
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
while ((line = reader.readLine()) != null && !line.isEmpty()) {
|
||||
if (line.toLowerCase().startsWith("get") || line.toLowerCase().startsWith("post")) {
|
||||
path = line.split(" ")[1];
|
||||
}
|
||||
if (line.contains(":")) {
|
||||
String[] parts = line.split(":", 2);
|
||||
headers.put(parts[0].trim().toLowerCase(), parts[1].trim());
|
||||
}
|
||||
}
|
||||
|
||||
path = URLDecoder.decode(path, StandardCharsets.UTF_8);
|
||||
path = normalizePath(path);
|
||||
|
||||
File file = new File(protocolWebServer.getProtocolBridge().getProtocolWebServer().getContentFolder(), path);
|
||||
|
||||
String sessionId = null;
|
||||
if (headers.containsKey("cookie")) {
|
||||
for (String cookie : headers.get("cookie").split(";")) {
|
||||
cookie = cookie.trim();
|
||||
if (cookie.startsWith("SESSIONID=")) {
|
||||
sessionId = cookie.substring("SESSIONID=".length());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!file.exists() || !file.isFile()) {
|
||||
sendResponse(out, 404, new File(protocolWebServer.getProtocolBridge().getProtocolWebServer().getErrorsFolder(), "404.html"));
|
||||
return;
|
||||
}
|
||||
|
||||
String clientIp = webSocket.getInetAddress().getHostAddress();
|
||||
String userAgent = headers.getOrDefault("user-agent", null);
|
||||
|
||||
boolean loggedIn = sessionId != null && SessionManager.isValid(sessionId, clientIp, userAgent, protocolWebServer);
|
||||
|
||||
if (path.equals("/403-login") && headers.getOrDefault("content-type", "").startsWith("application/x-www-form-urlencoded")) {
|
||||
Map<String, String> postParams = parsePostParams(in);
|
||||
String login = postParams.get("login");
|
||||
String password = postParams.get("password");
|
||||
|
||||
if (AuthManager.checkAuth(login, password)) {
|
||||
String newSessionId = SessionManager.create(login, clientIp, userAgent, protocolWebServer);
|
||||
Map<String, String> cookies = Map.of("Set-Cookie", "SESSIONID=" + newSessionId + "; HttpOnly; Path=/");
|
||||
sendRedirect(out, "/main.html", cookies);
|
||||
return;
|
||||
} else {
|
||||
sendRedirect(out, "/403.php", null);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (isMultipart(headers)) {
|
||||
handleMultipart(in, headers, new File(protocolWebServer.getProtocolBridge().getProtocolWebServer().getContentFolder(), "uploads"));
|
||||
}
|
||||
|
||||
if (RuleManager.requiresAuth(path) && !loggedIn) {
|
||||
PHPResponse phpResp = renderPHPWithCookies(new File(protocolWebServer.getProtocolBridge().getProtocolWebServer().getContentFolder(), "403.php"));
|
||||
sendResponse(out, 200, phpResp.body.getBytes(StandardCharsets.UTF_8), "text/html", phpResp.cookies);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (RuleManager.isDenied(path) && !RuleManager.isAllowed(path)) {
|
||||
sendResponse(out, 403, new File(protocolWebServer.getProtocolBridge().getProtocolWebServer().getErrorsFolder(), "403.php"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (path.endsWith(".php")) {
|
||||
PHPResponse phpResp = renderPHPWithCookies(file);
|
||||
sendResponse(out, 200, phpResp.body.getBytes(StandardCharsets.UTF_8), "text/html", phpResp.cookies);
|
||||
} else {
|
||||
sendResponse(out, 200, Files.readAllBytes(file.toPath()), getContentType(path), null);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
disconnect();
|
||||
}
|
||||
}
|
||||
} catch (Exception var2) {
|
||||
this.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set protocol bridge
|
||||
*
|
||||
* @param protocolWebServer The ProtocolWebServer object
|
||||
*/
|
||||
public void setProtocolWebServer(ProtocolWebServer protocolWebServer) {
|
||||
if (this.protocolWebServer == null) this.protocolWebServer = protocolWebServer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the response from a PHP script, including body and cookies.
|
||||
*/
|
||||
private static class PHPResponse {
|
||||
String body;
|
||||
Map<String, String> cookies;
|
||||
|
||||
public PHPResponse(String body, Map<String, String> cookies) {
|
||||
this.body = body;
|
||||
this.cookies = cookies;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -1,377 +1,256 @@
|
||||
package org.openautonomousconnection.protocol.side.web;
|
||||
|
||||
import dev.unlegitdqrk.unlegitlibrary.file.ConfigurationManager;
|
||||
import dev.unlegitdqrk.unlegitlibrary.file.FileUtils;
|
||||
import dev.unlegitdqrk.unlegitlibrary.network.system.server.NetworkServer;
|
||||
import dev.unlegitdqrk.unlegitlibrary.network.utils.NetworkUtils;
|
||||
import dev.unlegitdqrk.unlegitlibrary.string.RandomString;
|
||||
import lombok.Getter;
|
||||
import org.openautonomousconnection.protocol.ProtocolBridge;
|
||||
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.utils.TransportProtocol;
|
||||
import org.openautonomousconnection.protocol.annotations.ProtocolInfo;
|
||||
import org.openautonomousconnection.protocol.side.web.managers.AuthManager;
|
||||
import org.openautonomousconnection.protocol.side.web.managers.RuleManager;
|
||||
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.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.WebCompatMapper;
|
||||
|
||||
import javax.net.ssl.SSLServerSocket;
|
||||
import javax.net.ssl.SSLSocket;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
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.
|
||||
*
|
||||
* <p>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}.</p>
|
||||
*
|
||||
* <p>Important:
|
||||
* <ul>
|
||||
* <li>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}.</li>
|
||||
* <li>Responses/streams must preserve correlation via {@link WebPacketHeader#getRequestId()} for v1.0.1 clients.</li>
|
||||
* <li>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.</li>
|
||||
* </ul>
|
||||
* </p>
|
||||
*/
|
||||
@ProtocolInfo(protocolSide = ProtocolVersion.ProtocolSide.WEB)
|
||||
public final class ProtocolWebServer {
|
||||
/**
|
||||
* Folder for web content.
|
||||
*/
|
||||
@Getter
|
||||
private final File contentFolder;
|
||||
public abstract class ProtocolWebServer extends ProtocolWebServer_1_0_0_B {
|
||||
|
||||
/**
|
||||
* Folder for error pages.
|
||||
*/
|
||||
@Getter
|
||||
private final File errorsFolder;
|
||||
|
||||
/**
|
||||
* Structure for server.
|
||||
*/
|
||||
@Getter
|
||||
private final ServerCertificateFolderStructure folderStructure;
|
||||
|
||||
/**
|
||||
* Configuration manager for server settings.
|
||||
*/
|
||||
private final ConfigurationManager configurationManager;
|
||||
|
||||
/**
|
||||
* Certificate files for SSL.
|
||||
*/
|
||||
private final File certFile;
|
||||
/**
|
||||
* Certificate files for SSL.
|
||||
*/
|
||||
private final File keyFile;
|
||||
/**
|
||||
* The configuration file for the web server.
|
||||
*/
|
||||
private final File configFile;
|
||||
/**
|
||||
* The reference to the ProtocolBridge Object
|
||||
*/
|
||||
@Getter
|
||||
private final ProtocolBridge protocolBridge;
|
||||
/**
|
||||
* The network server handling pipeline connections.
|
||||
*/
|
||||
@Getter
|
||||
private NetworkServer pipelineServer;
|
||||
/**
|
||||
* The SSL server socket for web connections.
|
||||
*/
|
||||
@Getter
|
||||
private SSLServerSocket webServer;
|
||||
/**
|
||||
* List of connected web clients.
|
||||
*/
|
||||
@Getter
|
||||
private List<ConnectedWebClient> clients;
|
||||
/**
|
||||
* A unique secret for session management.
|
||||
*/
|
||||
@Getter
|
||||
private String uniqueSessionString;
|
||||
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 configFile The configuration file.
|
||||
* @param authFile The authentication file.
|
||||
* @param rulesFile The rules file.
|
||||
* @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(File configFile, File authFile, File rulesFile, ProtocolBridge protocolBridge) throws Exception {
|
||||
this.protocolBridge = protocolBridge;
|
||||
|
||||
// Initialize the list of connected clients
|
||||
this.clients = new ArrayList<>();
|
||||
|
||||
// Store the configuration file
|
||||
this.configFile = configFile;
|
||||
|
||||
// Set up folder structure for certificates
|
||||
folderStructure = new ServerCertificateFolderStructure();
|
||||
|
||||
// Check for necessary certificate files
|
||||
checkFileExists(folderStructure.publicServerFolder, folderStructure.certPrefix, ".crt");
|
||||
checkFileExists(folderStructure.privateServerFolder, folderStructure.certPrefix, ".key");
|
||||
|
||||
// Load configuration settings
|
||||
this.configurationManager = getConfigurationManager(configFile);
|
||||
|
||||
// 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();
|
||||
|
||||
// Set up certificate files based on public IP address
|
||||
this.certFile = new File(folderStructure.publicServerFolder, folderStructure.certPrefix + NetworkUtils.getPublicIPAddress() + ".crt");
|
||||
this.keyFile = new File(folderStructure.privateServerFolder, folderStructure.certPrefix + NetworkUtils.getPublicIPAddress() + ".key");
|
||||
|
||||
// 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);
|
||||
|
||||
// Initialize the pipeline server
|
||||
pipelineServer = new NetworkServer.ServerBuilder().
|
||||
setPort(configurationManager.getInt("port.pipeline")).setTimeout(0).
|
||||
setPacketHandler(protocolBridge.getProtocolSettings().packetHandler).setEventManager(protocolBridge.getProtocolSettings().eventManager).
|
||||
setLogger(protocolBridge.getLogger()).
|
||||
setServerCertificate(certFile, keyFile).setRootCAFolder(folderStructure.publicCAFolder).
|
||||
build();
|
||||
public ProtocolWebServer(File authFile, File rulesFile, int sessionExpire, int uploadSize) throws Exception {
|
||||
super(authFile, rulesFile, sessionExpire, uploadSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a connected web client by its client ID.
|
||||
*
|
||||
* @param clientID The client ID to search for.
|
||||
* @return The connected web client with the specified ID, or null if not found.
|
||||
*/
|
||||
public ConnectedWebClient getClientByID(int clientID) {
|
||||
for (ConnectedWebClient client : clients)
|
||||
if (client.getPipelineConnection().getClientID() == clientID) return client;
|
||||
return null;
|
||||
}
|
||||
@Listener(priority = EventPriority.HIGHEST)
|
||||
public final void onPacketRead(C_PacketReadEvent event) {
|
||||
Objects.requireNonNull(event, "event");
|
||||
|
||||
/**
|
||||
* Starts the web server to accept and handle client connections.
|
||||
*
|
||||
* @throws Exception If an error occurs while starting the server.
|
||||
*/
|
||||
public void startWebServer() throws Exception {
|
||||
// Start the pipeline server
|
||||
pipelineServer.start();
|
||||
Object packet = event.getPacket();
|
||||
if (packet == null) return;
|
||||
|
||||
// Create the SSL server socket for web connections
|
||||
webServer = (SSLServerSocket) NetworkServer.ServerBuilder.
|
||||
createSSLServerSocketFactory(folderStructure.publicCAFolder, certFile, keyFile).
|
||||
createServerSocket(configurationManager.getInt("port"));
|
||||
webServer.setSoTimeout(0);
|
||||
webServer.setEnabledProtocols(pipelineServer.getServerSocket().getEnabledProtocols());
|
||||
CustomConnectedClient client = getClientByID(event.getClient().getUniqueID());
|
||||
if (client == null) return;
|
||||
|
||||
// Stop WebServer if pipelineServer dies
|
||||
new Thread(() -> {
|
||||
while (true) {
|
||||
// Check if the pipeline server is still running
|
||||
if (pipelineServer == null || !pipelineServer.getServerSocket().isBound()) {
|
||||
try {
|
||||
// Stop the web server
|
||||
onPipelineStop();
|
||||
} catch (IOException e) {
|
||||
pipelineServer.getLogger().exception("Failed to stop WebServer", e);
|
||||
}
|
||||
|
||||
Thread.currentThread().interrupt();
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
// Sleep for a while before checking again
|
||||
Thread.sleep(1000);
|
||||
} catch (InterruptedException ignored) {
|
||||
}
|
||||
try {
|
||||
if (packet instanceof WebNavigateRequestPacket nav) {
|
||||
WebNavigateAckPacket ack = handleNavigate(client, nav);
|
||||
client.getConnection().sendPacket(ack, TransportProtocol.TCP);
|
||||
return;
|
||||
}
|
||||
}).start();
|
||||
|
||||
// Handle every client
|
||||
new Thread(() -> {
|
||||
while (true) {
|
||||
try {
|
||||
// Accept incoming client connections
|
||||
SSLSocket client = (SSLSocket) webServer.accept();
|
||||
|
||||
for (ConnectedWebClient connectedWebClient : clients) {
|
||||
if (connectedWebClient.getPipelineConnection().getClientID() != -1 && connectedWebClient.isClientVersionLoaded()) {
|
||||
// Assign socket to an existing connected client
|
||||
connectedWebClient.setWebSocket(client);
|
||||
connectedWebClient.setProtocolWebServer(this);
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
pipelineServer.getLogger().exception("Failed to accept WebClient", e);
|
||||
}
|
||||
if (packet instanceof WebResourceRequestPacket req) {
|
||||
WebResourceResponsePacket resp = handleResource(client, req);
|
||||
client.getConnection().sendPacket(resp, TransportProtocol.TCP);
|
||||
return;
|
||||
}
|
||||
}).start();
|
||||
|
||||
if (packet instanceof WebDocumentApplyRequestPacket apply) {
|
||||
WebDocumentApplyResponsePacket resp = handleDocumentApply(client, apply);
|
||||
client.getConnection().sendPacket(resp, TransportProtocol.TCP);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
getProtocolBridge().getProtocolValues().logger.exception("Failed to handle packet", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the shutdown of the web server when the pipeline server stops.
|
||||
* Server-side dispatcher entry point for a navigation request (packet id 01).
|
||||
*
|
||||
* @throws IOException If an I/O error occurs while closing the web server.
|
||||
* @param client connected client
|
||||
* @param request navigation request
|
||||
* @return navigation ack to send back
|
||||
*/
|
||||
private void onPipelineStop() throws IOException {
|
||||
webServer.close();
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the required certificate files exist in the specified folder.
|
||||
* Server-side dispatcher entry point for a resource request (packet id 03).
|
||||
*
|
||||
* @param folder The folder to check for certificate files.
|
||||
* @param prefix The prefix of the certificate files.
|
||||
* @param extension The extension of the certificate files.
|
||||
* @throws CertificateException If a required certificate file is missing or invalid.
|
||||
* @throws IOException If an I/O error occurs while checking the files.
|
||||
*/
|
||||
private void checkFileExists(File folder, String prefix, String extension) throws CertificateException, IOException {
|
||||
boolean found = false;
|
||||
|
||||
// Ensure the folder exists
|
||||
if (folder == null) throw new FileNotFoundException("Folder does not exist");
|
||||
|
||||
// List all files in the folder
|
||||
File[] files = folder.listFiles();
|
||||
if (files == null || files.length == 0)
|
||||
throw new FileNotFoundException("Folder " + folder.getAbsolutePath() + " is empty");
|
||||
|
||||
// Check for the required certificate file
|
||||
for (File file : files) {
|
||||
if (!file.getName().startsWith(prefix))
|
||||
throw new CertificateException(file.getAbsolutePath() + " is not valid");
|
||||
|
||||
// Check for file matching the public IP address
|
||||
if (!found) found = file.getName().equalsIgnoreCase(prefix + NetworkUtils.getPublicIPAddress() + extension);
|
||||
}
|
||||
|
||||
// Throw exception if the required file is not found
|
||||
if (!found) throw new CertificateException("Missing " + prefix + NetworkUtils.getPublicIPAddress() + extension);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the configuration manager for the web server.
|
||||
* <p>Streaming:
|
||||
* <ul>
|
||||
* <li>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}.</li>
|
||||
* </ul>
|
||||
* </p>
|
||||
*
|
||||
* @return The configuration manager.
|
||||
* @throws IOException If an I/O error occurs while loading or saving the configuration.
|
||||
* @param client connected client
|
||||
* @param request resource request
|
||||
* @return resource response packet to send back
|
||||
*/
|
||||
public ConfigurationManager getConfigurationManager() throws IOException {
|
||||
return getConfigurationManager(configFile);
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads and initializes the configuration manager with default settings if necessary.
|
||||
* Server-side dispatcher entry point for a document apply request (packet id 20).
|
||||
*
|
||||
* @param configFile The configuration file to load.
|
||||
* @return The initialized configuration manager.
|
||||
* @throws IOException If an I/O error occurs while loading or saving the configuration.
|
||||
* @param client connected client
|
||||
* @param request apply request
|
||||
* @return apply response
|
||||
*/
|
||||
private ConfigurationManager getConfigurationManager(File configFile) throws IOException {
|
||||
if (!configFile.exists()) configFile.createNewFile();
|
||||
public final WebDocumentApplyResponsePacket handleDocumentApply(CustomConnectedClient client, WebDocumentApplyRequestPacket request) {
|
||||
Objects.requireNonNull(client, "client");
|
||||
Objects.requireNonNull(request, "request");
|
||||
|
||||
ConfigurationManager configurationManager = new ConfigurationManager(configFile);
|
||||
configurationManager.loadProperties();
|
||||
|
||||
if (!configurationManager.isSet("port.webserver")) {
|
||||
configurationManager.set("port.webserver", 9824);
|
||||
configurationManager.saveProperties();
|
||||
WebDocumentApplyResponsePacket resp = onDocumentApplyRequest(client, request);
|
||||
if (resp == null) {
|
||||
resp = new WebDocumentApplyResponsePacket(
|
||||
mirrorHeader(request.getHeader(), 0),
|
||||
false,
|
||||
"onDocumentApplyRequest returned null"
|
||||
);
|
||||
}
|
||||
|
||||
if (!configurationManager.isSet("port.pipeline")) {
|
||||
configurationManager.set("port.pipeline", 9389);
|
||||
configurationManager.saveProperties();
|
||||
}
|
||||
|
||||
if (!configurationManager.isSet("filemaxuploadmb")) {
|
||||
configurationManager.set("filemaxuploadmb", 1000);
|
||||
configurationManager.saveProperties();
|
||||
}
|
||||
|
||||
if (!configurationManager.isSet("sessionexpireminutes")) {
|
||||
configurationManager.set("sessionexpireminutes", 60);
|
||||
configurationManager.saveProperties();
|
||||
}
|
||||
|
||||
return configurationManager;
|
||||
return resp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the folder structure for server certificates.
|
||||
* 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)
|
||||
*/
|
||||
public final class ServerCertificateFolderStructure {
|
||||
public final File certificatesFolder;
|
||||
protected abstract WebNavigateAckPacket onNavigateRequest(CustomConnectedClient client, WebNavigateRequestPacket request);
|
||||
|
||||
public final File publicFolder;
|
||||
public final File privateFolder;
|
||||
/**
|
||||
* 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);
|
||||
|
||||
public final File privateCAFolder;
|
||||
public final File privateServerFolder;
|
||||
/**
|
||||
* 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);
|
||||
|
||||
public final File publicCAFolder;
|
||||
public final File publicServerFolder;
|
||||
public final String caPrefix = "ca_server_";
|
||||
public final String certPrefix = "cert_server_";
|
||||
/**
|
||||
* v1.0.0 entry point.
|
||||
*
|
||||
* <p>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.</p>
|
||||
*
|
||||
* @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");
|
||||
|
||||
public ServerCertificateFolderStructure() {
|
||||
certificatesFolder = new File("certificates");
|
||||
// 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;
|
||||
|
||||
publicFolder = new File(certificatesFolder, "public");
|
||||
privateFolder = new File(certificatesFolder, "private");
|
||||
WebResourceRequestPacket mapped = WebCompatMapper.toResourceRequest(requestId, tabId, pageId, request, getProtocolBridge());
|
||||
WebResourceResponsePacket resp = onResourceRequest(client, mapped);
|
||||
|
||||
privateCAFolder = new File(privateFolder, "ca");
|
||||
privateServerFolder = new File(privateFolder, "server");
|
||||
|
||||
publicCAFolder = new File(publicFolder, "ca");
|
||||
publicServerFolder = new File(publicFolder, "server");
|
||||
|
||||
if (!certificatesFolder.exists()) certificatesFolder.mkdirs();
|
||||
|
||||
if (!publicFolder.exists()) publicFolder.mkdirs();
|
||||
if (!privateFolder.exists()) privateFolder.mkdirs();
|
||||
|
||||
if (!privateCAFolder.exists()) privateCAFolder.mkdirs();
|
||||
if (!privateServerFolder.exists()) privateServerFolder.mkdirs();
|
||||
|
||||
if (!publicCAFolder.exists()) publicCAFolder.mkdirs();
|
||||
if (!publicServerFolder.exists()) publicServerFolder.mkdirs();
|
||||
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()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package org.openautonomousconnection.protocol.side.web.events;
|
||||
|
||||
import dev.unlegitdqrk.unlegitlibrary.event.impl.Event;
|
||||
import lombok.Getter;
|
||||
import org.openautonomousconnection.protocol.annotations.ProtocolInfo;
|
||||
import org.openautonomousconnection.protocol.side.web.ConnectedWebClient;
|
||||
import org.openautonomousconnection.protocol.versions.ProtocolVersion;
|
||||
|
||||
/**
|
||||
* Event triggered when a web client connects to the web server.
|
||||
*/
|
||||
@ProtocolInfo(protocolSide = ProtocolVersion.ProtocolSide.WEB)
|
||||
public final class ConnectedWebClientEvent extends Event {
|
||||
|
||||
/**
|
||||
* The connected web client.
|
||||
*/
|
||||
@Getter
|
||||
private final ConnectedWebClient webClient;
|
||||
|
||||
public ConnectedWebClientEvent(ConnectedWebClient webClient) {
|
||||
this.webClient = webClient;
|
||||
}
|
||||
}
|
||||
@@ -6,95 +6,168 @@ import org.openautonomousconnection.protocol.annotations.ProtocolInfo;
|
||||
import org.openautonomousconnection.protocol.versions.ProtocolVersion;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Manages access rules for web resources.
|
||||
* Loads allow, deny, and auth rules from a JSON file and provides methods to check access.
|
||||
*
|
||||
* <p>Rules are loaded from a JSON file with the structure:
|
||||
* <pre>
|
||||
* {
|
||||
* "allow": ["index.html", "css/*"],
|
||||
* "deny": ["private/*"],
|
||||
* "auth": ["private/*", "admin/*"]
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* <p>Matching is performed against normalized web paths (forward slashes).
|
||||
* Patterns are treated as glob patterns where '*' matches any sequence.</p>
|
||||
*/
|
||||
@ProtocolInfo(protocolSide = ProtocolVersion.ProtocolSide.WEB)
|
||||
public final class RuleManager {
|
||||
/**
|
||||
* Lists of path patterns for allow, deny, and auth rules
|
||||
*/
|
||||
private static List<String> allow;
|
||||
|
||||
private static List<Pattern> allow = List.of();
|
||||
private static List<Pattern> deny = List.of();
|
||||
private static List<Pattern> auth = List.of();
|
||||
|
||||
private RuleManager() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists of path patterns for allow, deny, and auth rules
|
||||
*/
|
||||
private static List<String> deny;
|
||||
|
||||
/**
|
||||
* Lists of path patterns for allow, deny, and auth rules
|
||||
*/
|
||||
private static List<String> auth;
|
||||
|
||||
/**
|
||||
* Loads rules from a JSON file.
|
||||
* The JSON should have the structure:
|
||||
* {
|
||||
* "allow": ["pattern1", "pattern2", ...],
|
||||
* "deny": ["pattern1", "pattern2", ...],
|
||||
* "auth": ["pattern1", "pattern2", ...]
|
||||
* }
|
||||
* Loads rules from a JSON file and compiles patterns.
|
||||
*
|
||||
* @param rulesFile The JSON file containing the rules.
|
||||
* @throws Exception If an error occurs reading the file or parsing JSON.
|
||||
* @param rulesFile JSON rules file
|
||||
* @throws Exception if reading/parsing fails
|
||||
*/
|
||||
public static void loadRules(File rulesFile) throws Exception {
|
||||
// Load and parse the JSON file
|
||||
String json = new String(Files.readAllBytes(rulesFile.toPath()));
|
||||
Map<String, List<String>> map = new Gson().fromJson(json, new TypeToken<Map<String, List<String>>>() {
|
||||
}.getType());
|
||||
String json = Files.readString(rulesFile.toPath(), StandardCharsets.UTF_8);
|
||||
|
||||
// Default to empty lists if keys are missing
|
||||
allow = map.getOrDefault("allow", List.of());
|
||||
deny = map.getOrDefault("deny", List.of());
|
||||
auth = map.getOrDefault("auth", List.of());
|
||||
Map<String, List<String>> map = new Gson().fromJson(
|
||||
json,
|
||||
new TypeToken<Map<String, List<String>>>() {
|
||||
}.getType()
|
||||
);
|
||||
|
||||
allow = compileList(map.getOrDefault("allow", List.of()));
|
||||
deny = compileList(map.getOrDefault("deny", List.of()));
|
||||
auth = compileList(map.getOrDefault("auth", List.of()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given path is allowed based on the allow rules.
|
||||
* Returns true if the path is allowed by allow rules.
|
||||
*
|
||||
* @param path The path to check.
|
||||
* @return True if the path is allowed, false otherwise.
|
||||
* <p>Important: If allow list is empty, everything is allowed (default-open).</p>
|
||||
*
|
||||
* @param path web path (e.g. "/index.html" or "index.html")
|
||||
* @return true if allowed
|
||||
*/
|
||||
public static boolean isAllowed(String path) {
|
||||
return allow.stream().anyMatch(p -> pathMatches(path, p));
|
||||
String p = normalizePath(path);
|
||||
|
||||
// Default-open behavior if allow list is empty
|
||||
if (allow.isEmpty()) return true;
|
||||
|
||||
return matchesAny(p, allow);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given path is denied based on the deny rules.
|
||||
* Returns true if the path is denied.
|
||||
*
|
||||
* @param path The path to check.
|
||||
* @return True if the path is denied, false otherwise.
|
||||
* @param path web path
|
||||
* @return true if denied
|
||||
*/
|
||||
public static boolean isDenied(String path) {
|
||||
return deny.stream().anyMatch(p -> pathMatches(path, p));
|
||||
String p = normalizePath(path);
|
||||
return matchesAny(p, deny);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given path requires authentication based on the auth rules.
|
||||
* Returns true if the path requires authentication.
|
||||
*
|
||||
* @param path The path to check.
|
||||
* @return True if the path requires authentication, false otherwise.
|
||||
* @param path web path
|
||||
* @return true if auth required
|
||||
*/
|
||||
public static boolean requiresAuth(String path) {
|
||||
return auth.stream().anyMatch(p -> pathMatches(path, p));
|
||||
String p = normalizePath(path);
|
||||
return matchesAny(p, auth);
|
||||
}
|
||||
|
||||
private static boolean matchesAny(String normalizedPath, List<Pattern> patterns) {
|
||||
for (Pattern pat : patterns) {
|
||||
if (pat.matcher(normalizedPath).matches()) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static List<Pattern> compileList(List<String> globs) {
|
||||
if (globs == null || globs.isEmpty()) return List.of();
|
||||
return globs.stream()
|
||||
.map(RuleManager::compileGlob)
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to check if a path matches a pattern.
|
||||
* Patterns can include '*' as a wildcard.
|
||||
* Compiles a glob pattern to a full-match regex Pattern.
|
||||
*
|
||||
* @param path The path to check.
|
||||
* @param pattern The pattern to match against.
|
||||
* @return True if the path matches the pattern, false otherwise.
|
||||
* <p>Rules:
|
||||
* <ul>
|
||||
* <li>'*' matches any sequence of characters (including '/')</li>
|
||||
* <li>All other regex meta chars are escaped</li>
|
||||
* <li>Matching is performed against the entire normalized path</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param glob glob pattern, e.g. "css/*" or "private/*"
|
||||
* @return compiled Pattern
|
||||
*/
|
||||
private static boolean pathMatches(String path, String pattern) {
|
||||
pattern = pattern.replace("/", File.separator).replace("*", ".*");
|
||||
return path.matches(pattern);
|
||||
private static Pattern compileGlob(String glob) {
|
||||
String g = normalizePath(glob);
|
||||
|
||||
StringBuilder regex = new StringBuilder();
|
||||
regex.append("^");
|
||||
|
||||
for (int i = 0; i < g.length(); i++) {
|
||||
char c = g.charAt(i);
|
||||
if (c == '*') {
|
||||
regex.append(".*");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Escape regex metacharacters
|
||||
if ("\\.[]{}()+-^$|?".indexOf(c) >= 0) {
|
||||
regex.append("\\");
|
||||
}
|
||||
regex.append(c);
|
||||
}
|
||||
|
||||
regex.append("$");
|
||||
return Pattern.compile(regex.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes a web path:
|
||||
* <ul>
|
||||
* <li>null -> ""</li>
|
||||
* <li>backslashes -> forward slashes</li>
|
||||
* <li>leading '/' removed</li>
|
||||
* <li>no URL decoding (must be done at parser level if needed)</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param path input path
|
||||
* @return normalized path
|
||||
*/
|
||||
private static String normalizePath(String path) {
|
||||
if (path == null) return "";
|
||||
|
||||
String p = path.trim().replace('\\', '/');
|
||||
|
||||
while (p.startsWith("/")) {
|
||||
p = p.substring(1);
|
||||
}
|
||||
|
||||
return p;
|
||||
}
|
||||
}
|
||||
@@ -128,7 +128,7 @@ public final class SessionManager {
|
||||
this.login = login;
|
||||
this.ip = ip;
|
||||
this.userAgent = userAgent;
|
||||
this.expiresAt = System.currentTimeMillis() + (long) protocolWebServer.getConfigurationManager().getInt("sessionexpireminutes") * 60 * 1000;
|
||||
this.expiresAt = System.currentTimeMillis() + (long) protocolWebServer.getSessionExpire() * 60 * 1000;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -158,7 +158,7 @@ public final class SessionManager {
|
||||
* @throws IOException If an I/O error occurs.
|
||||
*/
|
||||
void refresh(ProtocolWebServer protocolWebServer) throws IOException {
|
||||
this.expiresAt = System.currentTimeMillis() + (long) protocolWebServer.getConfigurationManager().getInt("sessionexpireminutes") * 60 * 1000;
|
||||
this.expiresAt = System.currentTimeMillis() + (long) protocolWebServer.getSessionExpire() * 60 * 1000;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package org.openautonomousconnection.protocol.urlhandler.v1_0_0.beta;
|
||||
/**
|
||||
* Installs the "web://" protocol handler using java.protocol.handler.pkgs.
|
||||
* Recommended to use Version v1.0.0-BETA or newer
|
||||
*/
|
||||
@Deprecated(forRemoval = false, since = "1.0.1-BETA.0.3")
|
||||
public abstract class LibClientImpl_v1_0_0_B {
|
||||
/**
|
||||
* Called when connecting to the resolved server endpoint fails.
|
||||
*
|
||||
* @param exception the connection failure
|
||||
*/
|
||||
public abstract void serverConnectionFailed(Exception exception);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package org.openautonomousconnection.protocol.urlhandler.v1_0_0.beta;
|
||||
|
||||
/**
|
||||
* Stores the current session token across multiple HttpURLConnection instances.
|
||||
*
|
||||
* <p>JavaFX WebView creates a new connection per navigation, so headers must be re-injected
|
||||
* for every request.</p>
|
||||
*/
|
||||
public final class OacSessionJar {
|
||||
|
||||
private volatile String session;
|
||||
|
||||
/**
|
||||
* Stores a session token (e.g. from response header "session").
|
||||
*
|
||||
* @param session session token
|
||||
*/
|
||||
public void store(String session) {
|
||||
String s = (session == null) ? null : session.trim();
|
||||
this.session = (s == null || s.isEmpty()) ? null : s;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return stored session token or null
|
||||
*/
|
||||
public String get() {
|
||||
return session;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
package org.openautonomousconnection.protocol.urlhandler.v1_0_0.beta;
|
||||
|
||||
import org.openautonomousconnection.protocol.versions.v1_0_0.beta.WebRequestMethod;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.ProtocolException;
|
||||
import java.net.URL;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* HttpURLConnection implementation that maps "web://" URLs to OAC WebRequestPacket/WebResponsePacket.
|
||||
*
|
||||
* <p>Important semantics for JavaFX WebView:</p>
|
||||
* <ul>
|
||||
* <li>WebView may call {@link #connect()} before {@link #getOutputStream()}.</li>
|
||||
* <li>WebView creates a NEW connection instance per navigation; therefore persistent headers
|
||||
* (like session) must be injected from an external store per request.</li>
|
||||
* </ul>
|
||||
*/
|
||||
public final class OacWebHttpURLConnection extends HttpURLConnection {
|
||||
|
||||
private static final int MAX_REDIRECTS = 8;
|
||||
|
||||
private final OacWebRequestBroker broker;
|
||||
private final OacSessionJar sessionJar;
|
||||
|
||||
private final Map<String, String> requestHeaders = new LinkedHashMap<>();
|
||||
private final ByteArrayOutputStream requestBody = new ByteArrayOutputStream(1024);
|
||||
|
||||
private boolean connected;
|
||||
private boolean requestSent;
|
||||
|
||||
private OacWebResponse response;
|
||||
|
||||
/**
|
||||
* Creates a new OAC HttpURLConnection.
|
||||
*
|
||||
* @param url the web:// URL
|
||||
* @param broker request broker
|
||||
* @param sessionJar shared session store (must be shared across connections)
|
||||
*/
|
||||
public OacWebHttpURLConnection(URL url, OacWebRequestBroker broker, OacSessionJar sessionJar) {
|
||||
super(url);
|
||||
this.broker = Objects.requireNonNull(broker, "broker");
|
||||
this.sessionJar = Objects.requireNonNull(sessionJar, "sessionJar");
|
||||
this.method = "GET";
|
||||
setInstanceFollowRedirects(true);
|
||||
}
|
||||
|
||||
private static String headerValue(Map<String, String> headers, String nameLower) {
|
||||
if (headers == null || headers.isEmpty() || nameLower == null) return null;
|
||||
String needle = nameLower.trim().toLowerCase(Locale.ROOT);
|
||||
for (Map.Entry<String, String> e : headers.entrySet()) {
|
||||
if (e.getKey() == null) continue;
|
||||
if (e.getKey().trim().toLowerCase(Locale.ROOT).equals(needle)) {
|
||||
return e.getValue();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRequestProperty(String key, String value) {
|
||||
if (key == null) return;
|
||||
// DO NOT block after connect(): WebView may call connect() early.
|
||||
// Only block once the request was actually sent.
|
||||
if (requestSent) throw new IllegalStateException("Request already sent");
|
||||
if (value == null) requestHeaders.remove(key);
|
||||
else requestHeaders.put(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRequestProperty(String key) {
|
||||
if (key == null) return null;
|
||||
for (Map.Entry<String, String> e : requestHeaders.entrySet()) {
|
||||
if (e.getKey() != null && e.getKey().equalsIgnoreCase(key)) return e.getValue();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, List<String>> getRequestProperties() {
|
||||
Map<String, List<String>> out = new LinkedHashMap<>();
|
||||
for (Map.Entry<String, String> e : requestHeaders.entrySet()) {
|
||||
out.put(e.getKey(), e.getValue() == null ? List.of() : List.of(e.getValue()));
|
||||
}
|
||||
return Collections.unmodifiableMap(out);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRequestMethod(String method) throws ProtocolException {
|
||||
if (method == null) throw new ProtocolException("method is null");
|
||||
if (requestSent) throw new ProtocolException("Request already sent");
|
||||
|
||||
String m = method.trim().toUpperCase(Locale.ROOT);
|
||||
if (!m.equals("GET") && !m.equals("POST")) {
|
||||
throw new ProtocolException("Unsupported method: " + method);
|
||||
}
|
||||
this.method = m;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OutputStream getOutputStream() throws IOException {
|
||||
// WebView may call connect() first, so do NOT throw "Already connected".
|
||||
if (requestSent) throw new IllegalStateException("Request already sent");
|
||||
|
||||
setDoOutput(true);
|
||||
return requestBody;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void connect() {
|
||||
// MUST NOT send here. WebView may call connect() before writing the POST body.
|
||||
connected = true;
|
||||
}
|
||||
|
||||
private void ensureResponse() throws IOException {
|
||||
if (requestSent) return;
|
||||
|
||||
URL cur = this.url;
|
||||
|
||||
String methodStr = (this.method == null) ? "GET" : this.method.trim().toUpperCase(Locale.ROOT);
|
||||
WebRequestMethod reqMethod = "POST".equals(methodStr) ? WebRequestMethod.POST : WebRequestMethod.GET;
|
||||
|
||||
// Snapshot headers/body at send time.
|
||||
Map<String, String> carryHeaders = new LinkedHashMap<>(requestHeaders);
|
||||
|
||||
// Each navigation creates a new connection, so we re-add the session for every request.
|
||||
String session = sessionJar.get();
|
||||
if (session != null && !session.isBlank() && headerValue(carryHeaders, "session") == null) {
|
||||
carryHeaders.put("session", session);
|
||||
}
|
||||
|
||||
byte[] carryBody = null;
|
||||
if (getDoOutput()) {
|
||||
carryBody = requestBody.toByteArray();
|
||||
if (reqMethod == WebRequestMethod.POST && carryBody == null) carryBody = new byte[0];
|
||||
}
|
||||
|
||||
if (reqMethod == WebRequestMethod.POST && headerValue(carryHeaders, "content-type") == null) {
|
||||
carryHeaders.put("content-type", "application/x-www-form-urlencoded; charset=utf-8");
|
||||
}
|
||||
|
||||
OacWebResponse resp = null;
|
||||
WebRequestMethod carryMethod = reqMethod;
|
||||
|
||||
for (int i = 0; i <= MAX_REDIRECTS; i++) {
|
||||
resp = broker.fetch(cur, carryMethod, carryHeaders, carryBody);
|
||||
|
||||
String newSession = headerValue(resp.headers(), "session");
|
||||
if (newSession != null && !newSession.isBlank()) {
|
||||
sessionJar.store(newSession);
|
||||
// keep it for the next request in this redirect chain too
|
||||
carryHeaders.put("session", newSession);
|
||||
}
|
||||
|
||||
int code = resp.statusCode();
|
||||
if (!getInstanceFollowRedirects()) break;
|
||||
|
||||
if (code == 301 || code == 302 || code == 303 || code == 307 || code == 308) {
|
||||
String loc = headerValue(resp.headers(), "location");
|
||||
if (loc == null || loc.isBlank()) break;
|
||||
|
||||
try {
|
||||
cur = new URL(cur, loc);
|
||||
} catch (Exception ex) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (code == 303) {
|
||||
carryMethod = WebRequestMethod.GET;
|
||||
carryBody = null;
|
||||
} else if ((code == 301 || code == 302) && carryMethod == WebRequestMethod.POST) {
|
||||
carryMethod = WebRequestMethod.GET;
|
||||
carryBody = null;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
this.response = resp;
|
||||
this.requestSent = true;
|
||||
this.connected = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getInputStream() throws IOException {
|
||||
ensureResponse();
|
||||
if (response == null) return new ByteArrayInputStream(new byte[0]);
|
||||
return response.bodyStream();
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getErrorStream() {
|
||||
try {
|
||||
ensureResponse();
|
||||
if (response == null) return null;
|
||||
int code = response.statusCode();
|
||||
return (code >= 400) ? response.bodyStream() : null;
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getResponseCode() throws IOException {
|
||||
ensureResponse();
|
||||
return response == null ? -1 : response.statusCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getContentType() {
|
||||
try {
|
||||
ensureResponse();
|
||||
} catch (IOException e) {
|
||||
return "application/octet-stream";
|
||||
}
|
||||
String ct = (response == null) ? null : response.contentType();
|
||||
return (ct == null || ct.isBlank()) ? "application/octet-stream" : ct;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getContentLength() {
|
||||
try {
|
||||
ensureResponse();
|
||||
} catch (IOException e) {
|
||||
return -1;
|
||||
}
|
||||
long len = (response == null) ? -1L : response.contentLength();
|
||||
return (len <= 0 || len > Integer.MAX_VALUE) ? -1 : (int) len;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getContentLengthLong() {
|
||||
try {
|
||||
ensureResponse();
|
||||
} catch (IOException e) {
|
||||
return -1L;
|
||||
}
|
||||
return (response == null) ? -1L : response.contentLength();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, List<String>> getHeaderFields() {
|
||||
try {
|
||||
ensureResponse();
|
||||
} catch (IOException e) {
|
||||
return Map.of();
|
||||
}
|
||||
if (response == null) return Map.of();
|
||||
|
||||
Map<String, List<String>> out = new LinkedHashMap<>();
|
||||
for (Map.Entry<String, String> e : response.headers().entrySet()) {
|
||||
String k = e.getKey();
|
||||
String v = e.getValue();
|
||||
if (k == null) continue;
|
||||
out.put(k, v == null ? List.of() : List.of(v));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHeaderField(String name) {
|
||||
if (name == null) return null;
|
||||
try {
|
||||
ensureResponse();
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
if (response == null) return null;
|
||||
return headerValue(response.headers(), name.trim().toLowerCase(Locale.ROOT));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disconnect() {
|
||||
// No persistent socket owned by this object.
|
||||
connected = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean usingProxy() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
package org.openautonomousconnection.protocol.urlhandler.v1_0_0.beta;
|
||||
|
||||
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.packets.C_PacketReadEvent;
|
||||
import org.openautonomousconnection.protocol.packets.v1_0_0.beta.INSResponsePacket;
|
||||
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.side.client.ProtocolClient;
|
||||
import org.openautonomousconnection.protocol.side.client.events.ConnectedToProtocolServerEvent;
|
||||
import org.openautonomousconnection.protocol.versions.v1_0_0.beta.INSRecord;
|
||||
import org.openautonomousconnection.protocol.versions.v1_0_0.beta.INSResponseStatus;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Receives incoming OAC web response packets and forwards them into the request broker.
|
||||
*
|
||||
* <p>Important:</p>
|
||||
* <ul>
|
||||
* <li>The shown protocol types do not contain any correlation id.</li>
|
||||
* <li>Therefore, the broker must treat the connection as single-flight (one in-flight request).</li>
|
||||
* </ul>
|
||||
*/
|
||||
public final class OacWebPacketListener extends EventListener {
|
||||
|
||||
private final OacWebRequestBroker broker;
|
||||
private final ProtocolClient client;
|
||||
private final LibClientImpl_v1_0_0_B impl;
|
||||
|
||||
/**
|
||||
* Creates a new listener bound to the given broker.
|
||||
*
|
||||
* @param broker broker instance
|
||||
* @param client protocol client
|
||||
*/
|
||||
public OacWebPacketListener(OacWebRequestBroker broker, ProtocolClient client, LibClientImpl_v1_0_0_B impl) {
|
||||
this.broker = Objects.requireNonNull(broker, "broker");
|
||||
this.client = Objects.requireNonNull(client, "client");
|
||||
this.impl = Objects.requireNonNull(impl, "impl");
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the broker that the server connection is established.
|
||||
*
|
||||
* @param event connected event
|
||||
*/
|
||||
@Listener(priority = EventPriority.HIGHEST)
|
||||
public void onConnected(ConnectedToProtocolServerEvent event) {
|
||||
broker.notifyServerConnected();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles packets coming from INS and the web server side.
|
||||
*
|
||||
* @param event packet event
|
||||
*/
|
||||
@Listener(priority = EventPriority.HIGHEST)
|
||||
public void onPacketRead(C_PacketReadEvent event) {
|
||||
Object p = event.getPacket();
|
||||
|
||||
if (p instanceof INSResponsePacket resp) {
|
||||
onInsResponse(resp);
|
||||
return;
|
||||
}
|
||||
|
||||
if (p instanceof WebResponsePacket resp) {
|
||||
broker.onWebResponse(resp.getStatusCode(), resp.getContentType(), resp.getHeaders(), resp.getBody());
|
||||
return;
|
||||
}
|
||||
|
||||
if (p instanceof WebStreamStartPacket_v1_0_0_B start) {
|
||||
broker.onStreamStart(start.getStatusCode(), start.getContentType(), start.getHeaders(), start.getTotalLength());
|
||||
return;
|
||||
}
|
||||
|
||||
if (p instanceof WebStreamChunkPacket_v1_0_0_B chunk) {
|
||||
broker.onStreamChunk(chunk.getSeq(), chunk.getData());
|
||||
return;
|
||||
}
|
||||
|
||||
if (p instanceof WebStreamEndPacket_v1_0_0_B end) {
|
||||
broker.onStreamEnd(end.isOk());
|
||||
}
|
||||
}
|
||||
|
||||
private void onInsResponse(INSResponsePacket resp) {
|
||||
INSResponseStatus status = resp.getStatus();
|
||||
List<INSRecord> records = resp.getRecords();
|
||||
|
||||
if (status != INSResponseStatus.OK) {
|
||||
broker.invalidateCurrentInfoName();
|
||||
throw new IllegalStateException("INS resolution failed: " + status);
|
||||
}
|
||||
|
||||
if (records == null || records.isEmpty() || records.getFirst() == null || records.getFirst().value == null) {
|
||||
broker.invalidateCurrentInfoName();
|
||||
throw new IllegalStateException("INS resolution returned no usable records");
|
||||
}
|
||||
|
||||
String host = records.getFirst().value.trim();
|
||||
if (host.isEmpty()) {
|
||||
broker.invalidateCurrentInfoName();
|
||||
throw new IllegalStateException("INS record value is empty");
|
||||
}
|
||||
|
||||
String hostname;
|
||||
int port;
|
||||
|
||||
if (!host.contains(":")) {
|
||||
hostname = host;
|
||||
|
||||
if (records.getFirst().port == 0) port = 1028;
|
||||
else port = records.getFirst().port;
|
||||
} else {
|
||||
String[] split = host.split(":", 2);
|
||||
hostname = split[0].trim();
|
||||
String p1 = split[1].trim();
|
||||
if (hostname.isEmpty() || p1.isEmpty()) {
|
||||
broker.invalidateCurrentInfoName();
|
||||
throw new IllegalStateException("Invalid INS host:port value: " + host);
|
||||
}
|
||||
try {
|
||||
port = Integer.parseInt(p1);
|
||||
} catch (NumberFormatException e) {
|
||||
broker.invalidateCurrentInfoName();
|
||||
throw new IllegalStateException("Invalid port in INS record: " + host, e);
|
||||
}
|
||||
}
|
||||
|
||||
Thread t = new Thread(() -> connectServer(hostname, port), "oac-web-server-connect");
|
||||
t.setDaemon(true);
|
||||
t.start();
|
||||
}
|
||||
|
||||
private void connectServer(String hostname, int port) {
|
||||
try {
|
||||
if (client.getClientServerConnection() != null && client.getClientServerConnection().isConnected()) {
|
||||
client.getClientServerConnection().disconnect();
|
||||
}
|
||||
|
||||
client.buildServerConnection(null, client.getProtocolBridge().getProtocolValues().ssl);
|
||||
client.getClientServerConnection().connect(hostname, port);
|
||||
} catch (Exception e) {
|
||||
broker.invalidateCurrentInfoName();
|
||||
impl.serverConnectionFailed(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,520 @@
|
||||
package org.openautonomousconnection.protocol.urlhandler.v1_0_0.beta;
|
||||
|
||||
import dev.unlegitdqrk.unlegitlibrary.network.system.utils.TransportProtocol;
|
||||
import org.openautonomousconnection.protocol.packets.v1_0_0.beta.INSQueryPacket;
|
||||
import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.WebRequestPacket;
|
||||
import org.openautonomousconnection.protocol.side.client.ProtocolClient;
|
||||
import org.openautonomousconnection.protocol.versions.v1_0_0.beta.INSRecordType;
|
||||
import org.openautonomousconnection.protocol.versions.v1_0_0.beta.WebRequestMethod;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.net.URL;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Central broker that translates {@code web://} URLs into OAC protocol traffic.
|
||||
*
|
||||
* <p>Protocol limitation: no correlation id -> single-flight (one in-flight request at a time).</p>
|
||||
*
|
||||
* <p>UDP streaming semantics (best-effort):</p>
|
||||
* <ul>
|
||||
* <li>Chunks may arrive out of order or be lost.</li>
|
||||
* <li>We accept gaps and assemble what we have after {@code WebStreamEndPacket}.</li>
|
||||
* <li>We wait a short grace window after stream end to allow late UDP packets.</li>
|
||||
* </ul>
|
||||
*/
|
||||
public final class OacWebRequestBroker {
|
||||
|
||||
private static final OacWebRequestBroker INSTANCE = new OacWebRequestBroker();
|
||||
|
||||
private static final long CONNECT_TIMEOUT_SECONDS = 10;
|
||||
private static final long RESPONSE_TIMEOUT_SECONDS = 25;
|
||||
|
||||
/**
|
||||
* Grace time after receiving WebStreamEndPacket to allow late UDP packets (reordering).
|
||||
*/
|
||||
private static final long UDP_END_GRACE_MILLIS = 150;
|
||||
|
||||
private final Object responseLock = new Object();
|
||||
|
||||
private volatile ProtocolClient client;
|
||||
private volatile CountDownLatch connectionLatch;
|
||||
private volatile String currentInfoName;
|
||||
private volatile ResponseState responseState;
|
||||
|
||||
private OacWebRequestBroker() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the singleton broker.
|
||||
*
|
||||
* @return broker
|
||||
*/
|
||||
public static OacWebRequestBroker get() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
private static String safeContentType(String ct) {
|
||||
return (ct == null || ct.isBlank()) ? "application/octet-stream" : ct;
|
||||
}
|
||||
|
||||
private static Map<String, String> safeHeaders(Map<String, String> headers) {
|
||||
return (headers == null || headers.isEmpty()) ? Map.of() : Map.copyOf(headers);
|
||||
}
|
||||
|
||||
private static String normalizePathWithQuery(String path, String query) {
|
||||
String p;
|
||||
if (path == null || path.isBlank() || "/".equals(path)) {
|
||||
p = "index.html";
|
||||
} else {
|
||||
p = path.startsWith("/") ? path.substring(1) : path;
|
||||
if (p.isBlank()) p = "index.html";
|
||||
}
|
||||
|
||||
if (query != null && !query.isBlank()) {
|
||||
return p + "?" + query;
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assembles the response body from received chunks in ascending seq order.
|
||||
*
|
||||
* <p>Best-effort UDP behavior: gaps are ignored.</p>
|
||||
*/
|
||||
private static void assembleBestEffortLocked(ResponseState st) {
|
||||
st.body.reset();
|
||||
|
||||
if (st.chunkBuffer.isEmpty() || st.maxSeqSeen < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (int seq = 0; seq <= st.maxSeqSeen; seq++) {
|
||||
byte[] chunk = st.chunkBuffer.get(seq);
|
||||
if (chunk == null) continue; // gap accepted
|
||||
st.body.write(chunk, 0, chunk.length);
|
||||
}
|
||||
}
|
||||
|
||||
private static void sleepSilently(long millis) {
|
||||
try {
|
||||
Thread.sleep(millis);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches the client used to send INS/Web packets.
|
||||
*
|
||||
* @param client protocol client
|
||||
*/
|
||||
public void attachClient(ProtocolClient client) {
|
||||
this.client = Objects.requireNonNull(client, "client");
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy overload (GET with no body).
|
||||
*
|
||||
* @param url web:// URL
|
||||
* @param headers request headers
|
||||
* @return response
|
||||
*/
|
||||
public OacWebResponse fetch(URL url, Map<String, String> headers) {
|
||||
return fetch(url, WebRequestMethod.GET, headers, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a URL via OAC protocol (used by {@link java.net.URLConnection}).
|
||||
*
|
||||
* @param url web:// URL
|
||||
* @param method request method
|
||||
* @param headers request headers
|
||||
* @param body request body (may be null)
|
||||
* @return response
|
||||
*/
|
||||
public OacWebResponse fetch(URL url, WebRequestMethod method, Map<String, String> headers, byte[] body) {
|
||||
Objects.requireNonNull(url, "url");
|
||||
Objects.requireNonNull(method, "method");
|
||||
|
||||
ProtocolClient c = this.client;
|
||||
if (c == null) {
|
||||
throw new IllegalStateException("ProtocolClient not attached. Call OacWebUrlInstaller.installOnce(..., client) first.");
|
||||
}
|
||||
|
||||
Response r = openAndAwait(c, url, method, headers, body);
|
||||
|
||||
byte[] respBody = (r.body() == null) ? new byte[0] : r.body();
|
||||
long len = respBody.length;
|
||||
|
||||
return new OacWebResponse(
|
||||
r.statusCode(),
|
||||
r.contentType(),
|
||||
OacWebResponse.safeHeaders(r.headers()),
|
||||
new ByteArrayInputStream(respBody),
|
||||
len
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a resource and blocks until the current single-flight response completes.
|
||||
*/
|
||||
public Response openAndAwait(ProtocolClient client, URL url, WebRequestMethod method, Map<String, String> headers, byte[] body) {
|
||||
Objects.requireNonNull(client, "client");
|
||||
Objects.requireNonNull(url, "url");
|
||||
Objects.requireNonNull(method, "method");
|
||||
|
||||
open(client, url, method, headers, body);
|
||||
return awaitResponse(RESPONSE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends required packets for a {@code web://} URL.
|
||||
*/
|
||||
public synchronized void open(ProtocolClient client, URL url, WebRequestMethod method, Map<String, String> headers, byte[] body) {
|
||||
Objects.requireNonNull(client, "client");
|
||||
Objects.requireNonNull(url, "url");
|
||||
Objects.requireNonNull(method, "method");
|
||||
|
||||
if (!"web".equalsIgnoreCase(url.getProtocol())) {
|
||||
throw new IllegalArgumentException("Unsupported protocol: " + url.getProtocol());
|
||||
}
|
||||
|
||||
String infoName = url.getHost();
|
||||
if (infoName == null || infoName.isBlank()) {
|
||||
throw new IllegalArgumentException("Missing InfoName in URL: " + url);
|
||||
}
|
||||
|
||||
String path = normalizePathWithQuery(url.getPath(), url.getQuery());
|
||||
|
||||
beginNewResponse();
|
||||
|
||||
if (!infoName.equals(currentInfoName)) {
|
||||
resolveAndConnect(client, infoName);
|
||||
currentInfoName = infoName;
|
||||
} else {
|
||||
awaitConnectionIfPending();
|
||||
}
|
||||
|
||||
sendWebRequest(client, path, method, headers, body);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by packet listener when server connection is established.
|
||||
*/
|
||||
public void notifyServerConnected() {
|
||||
CountDownLatch latch = connectionLatch;
|
||||
if (latch != null) {
|
||||
latch.countDown();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates cached InfoName, forcing a new INS resolution on next request.
|
||||
*/
|
||||
public synchronized void invalidateCurrentInfoName() {
|
||||
currentInfoName = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a non-streamed WebResponsePacket.
|
||||
*
|
||||
* @param statusCode status code
|
||||
* @param contentType content-type
|
||||
* @param headers headers
|
||||
* @param body body
|
||||
*/
|
||||
public void onWebResponse(int statusCode, String contentType, Map<String, String> headers, byte[] body) {
|
||||
ResponseState st = responseState;
|
||||
if (st == null) return;
|
||||
|
||||
synchronized (responseLock) {
|
||||
if (st.completed) return;
|
||||
|
||||
st.statusCode = statusCode;
|
||||
st.contentType = safeContentType(contentType);
|
||||
st.headers = safeHeaders(headers);
|
||||
|
||||
byte[] b = (body == null) ? new byte[0] : body;
|
||||
st.body.reset();
|
||||
st.body.write(b, 0, b.length);
|
||||
|
||||
st.completed = true;
|
||||
st.success = true;
|
||||
st.done.countDown();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the beginning of a streamed response.
|
||||
*
|
||||
* @param statusCode status code
|
||||
* @param contentType content-type
|
||||
* @param headers headers
|
||||
* @param totalLength total length (may be -1)
|
||||
*/
|
||||
public void onStreamStart(int statusCode, String contentType, Map<String, String> headers, long totalLength) {
|
||||
ResponseState st = responseState;
|
||||
if (st == null) return;
|
||||
|
||||
synchronized (responseLock) {
|
||||
if (st.completed) return;
|
||||
|
||||
st.statusCode = statusCode;
|
||||
st.contentType = safeContentType(contentType);
|
||||
st.headers = safeHeaders(headers);
|
||||
st.totalLength = totalLength;
|
||||
|
||||
st.streamStarted = true;
|
||||
st.maxSeqSeen = -1;
|
||||
st.endReceived = false;
|
||||
st.endReceivedAtMillis = 0L;
|
||||
|
||||
// Streaming body will be assembled on end (best-effort UDP)
|
||||
st.body.reset();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a streamed chunk.
|
||||
*
|
||||
* <p>UDP best-effort: store by seq and assemble later; accept gaps.</p>
|
||||
*
|
||||
* @param seq chunk sequence number
|
||||
* @param data chunk bytes
|
||||
*/
|
||||
public void onStreamChunk(int seq, byte[] data) {
|
||||
ResponseState st = responseState;
|
||||
if (st == null) return;
|
||||
if (data == null || data.length == 0) return;
|
||||
|
||||
synchronized (responseLock) {
|
||||
if (st.completed) return;
|
||||
|
||||
if (!st.streamStarted) {
|
||||
failLocked(st, "Stream chunk received before stream start");
|
||||
return;
|
||||
}
|
||||
|
||||
if (seq < 0) return;
|
||||
|
||||
st.chunkBuffer.put(seq, Arrays.copyOf(data, data.length));
|
||||
if (seq > st.maxSeqSeen) st.maxSeqSeen = seq;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles stream end.
|
||||
*
|
||||
* <p>UDP best-effort: do not complete immediately; allow late UDP packets and assemble after grace.</p>
|
||||
*
|
||||
* @param ok end status
|
||||
*/
|
||||
public void onStreamEnd(boolean ok) {
|
||||
ResponseState st = responseState;
|
||||
if (st == null) return;
|
||||
|
||||
synchronized (responseLock) {
|
||||
if (st.completed) return;
|
||||
|
||||
if (!st.streamStarted) {
|
||||
st.streamStarted = true;
|
||||
}
|
||||
|
||||
st.success = ok;
|
||||
st.endReceived = true;
|
||||
st.endReceivedAtMillis = System.currentTimeMillis();
|
||||
// completion + assembly happens in awaitResponse() after grace window
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for the current response (single-flight).
|
||||
*
|
||||
* @param timeout timeout
|
||||
* @param unit unit
|
||||
* @return response
|
||||
*/
|
||||
public Response awaitResponse(long timeout, TimeUnit unit) {
|
||||
Objects.requireNonNull(unit, "unit");
|
||||
|
||||
ResponseState st = responseState;
|
||||
if (st == null) {
|
||||
throw new IllegalStateException("No in-flight request");
|
||||
}
|
||||
|
||||
long deadlineNanos = System.nanoTime() + unit.toNanos(timeout);
|
||||
|
||||
for (; ; ) {
|
||||
synchronized (responseLock) {
|
||||
if (st.completed) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Non-stream response already completed by onWebResponse()
|
||||
if (!st.streamStarted && st.done.getCount() == 0) {
|
||||
st.completed = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (st.endReceived) {
|
||||
long now = System.currentTimeMillis();
|
||||
if (now - st.endReceivedAtMillis >= UDP_END_GRACE_MILLIS) {
|
||||
// Assemble best-effort body from received chunks
|
||||
assembleBestEffortLocked(st);
|
||||
|
||||
st.completed = true;
|
||||
|
||||
if (!st.success) {
|
||||
st.errorMessage = (st.errorMessage == null) ? "Streaming failed" : st.errorMessage;
|
||||
}
|
||||
|
||||
st.done.countDown();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (System.nanoTime() >= deadlineNanos) {
|
||||
throw new IllegalStateException("Timeout while waiting for Web response");
|
||||
}
|
||||
|
||||
sleepSilently(10);
|
||||
}
|
||||
|
||||
synchronized (responseLock) {
|
||||
if (!st.success) {
|
||||
throw new IllegalStateException(st.errorMessage == null ? "Request failed" : st.errorMessage);
|
||||
}
|
||||
return new Response(
|
||||
st.statusCode,
|
||||
st.contentType,
|
||||
st.headers,
|
||||
st.body.toByteArray()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void resolveAndConnect(ProtocolClient client, String infoName) {
|
||||
if (client.getClientINSConnection() == null || !client.getClientINSConnection().isConnected()) return;
|
||||
|
||||
String[] parts = infoName.split("\\.");
|
||||
if (parts.length < 2 || parts.length > 3) {
|
||||
throw new IllegalArgumentException(
|
||||
"Invalid INS address format: " + infoName + " (expected name.tln or sub.name.tln)"
|
||||
);
|
||||
}
|
||||
|
||||
String tln = parts[parts.length - 1];
|
||||
String name = parts[parts.length - 2];
|
||||
String sub = (parts.length == 3) ? parts[0] : null;
|
||||
|
||||
connectionLatch = new CountDownLatch(1);
|
||||
|
||||
try {
|
||||
client.sendINSQuery(tln, name, sub, INSRecordType.A);
|
||||
awaitConnectionIfPending();
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("Failed to send INSQueryPacket for " + infoName, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void awaitConnectionIfPending() {
|
||||
CountDownLatch latch = connectionLatch;
|
||||
if (latch == null || client == null || client.getClientServerConnection() == null || client.getClientINSConnection() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!latch.await(CONNECT_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
|
||||
throw new IllegalStateException("Timeout while waiting for ServerConnection");
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IllegalStateException("Interrupted while waiting for ServerConnection", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendWebRequest(ProtocolClient client, String path, WebRequestMethod method, Map<String, String> headers, byte[] body) {
|
||||
Objects.requireNonNull(client, "client");
|
||||
Objects.requireNonNull(path, "path");
|
||||
Objects.requireNonNull(method, "method");
|
||||
|
||||
if (client.getClientServerConnection() == null || !client.getClientServerConnection().isConnected()) {
|
||||
awaitConnectionIfPending();
|
||||
}
|
||||
|
||||
if (client.getClientServerConnection() == null || !client.getClientServerConnection().isConnected()) {
|
||||
throw new IllegalStateException("ServerConnection is not connected after waiting");
|
||||
}
|
||||
|
||||
WebRequestPacket packet = new WebRequestPacket(
|
||||
path,
|
||||
method,
|
||||
(headers == null ? Map.of() : headers),
|
||||
body
|
||||
);
|
||||
|
||||
try {
|
||||
client.getClientServerConnection().sendPacket(packet, TransportProtocol.TCP);
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("Failed to send WebRequestPacket for path " + path, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void beginNewResponse() {
|
||||
synchronized (responseLock) {
|
||||
responseState = new ResponseState();
|
||||
}
|
||||
}
|
||||
|
||||
private void failLocked(ResponseState st, String message) {
|
||||
st.completed = true;
|
||||
st.success = false;
|
||||
st.errorMessage = message;
|
||||
st.done.countDown();
|
||||
}
|
||||
|
||||
/**
|
||||
* Response DTO.
|
||||
*
|
||||
* @param statusCode status code
|
||||
* @param contentType content type
|
||||
* @param headers headers
|
||||
* @param body body bytes
|
||||
*/
|
||||
public record Response(int statusCode, String contentType, Map<String, String> headers, byte[] body) {
|
||||
}
|
||||
|
||||
/**
|
||||
* In-flight state for a single request (single-flight).
|
||||
*/
|
||||
private static final class ResponseState {
|
||||
private final CountDownLatch done = new CountDownLatch(1);
|
||||
|
||||
private final ByteArrayOutputStream body = new ByteArrayOutputStream(64 * 1024);
|
||||
private final Map<Integer, byte[]> chunkBuffer = new HashMap<>();
|
||||
|
||||
private int statusCode = 0;
|
||||
private String contentType = "application/octet-stream";
|
||||
private Map<String, String> headers = Map.of();
|
||||
|
||||
private boolean streamStarted;
|
||||
private long totalLength;
|
||||
|
||||
private int maxSeqSeen = -1;
|
||||
|
||||
private boolean endReceived;
|
||||
private long endReceivedAtMillis;
|
||||
|
||||
private boolean completed;
|
||||
private boolean success;
|
||||
private String errorMessage;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package org.openautonomousconnection.protocol.urlhandler.v1_0_0.beta;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Represents a resolved web response for the JavaFX WebView.
|
||||
*
|
||||
* @param statusCode HTTP-like status code
|
||||
* @param contentType response content-type (as sent by server)
|
||||
* @param headers response headers
|
||||
* @param bodyStream body stream (may be streaming)
|
||||
* @param contentLength content length if known, else -1
|
||||
*/
|
||||
public record OacWebResponse(
|
||||
int statusCode,
|
||||
String contentType,
|
||||
Map<String, String> headers,
|
||||
InputStream bodyStream,
|
||||
long contentLength
|
||||
) {
|
||||
public OacWebResponse {
|
||||
Objects.requireNonNull(headers, "headers");
|
||||
Objects.requireNonNull(bodyStream, "bodyStream");
|
||||
}
|
||||
|
||||
public static Map<String, String> safeHeaders(Map<String, String> h) {
|
||||
return (h == null) ? Collections.emptyMap() : Collections.unmodifiableMap(h);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package org.openautonomousconnection.protocol.urlhandler.v1_0_0.beta;
|
||||
|
||||
import org.openautonomousconnection.protocol.ProtocolBridge;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* Installs the "web://" protocol handler using java.protocol.handler.pkgs.
|
||||
* Recommended to use Version v1.0.0-BETA or newer
|
||||
*/
|
||||
@Deprecated(forRemoval = false, since = "1.0.1-BETA.0.3")
|
||||
public final class OacWebUrlInstaller_v1_0_0_B {
|
||||
|
||||
private static final AtomicBoolean INSTALLED = new AtomicBoolean(false);
|
||||
|
||||
private OacWebUrlInstaller_v1_0_0_B() {
|
||||
}
|
||||
|
||||
public static void installOnce(ProtocolBridge protocolBridge, LibClientImpl_v1_0_0_B impl) {
|
||||
Objects.requireNonNull(protocolBridge, "protocolBridge");
|
||||
Objects.requireNonNull(impl, "impl");
|
||||
|
||||
if (!INSTALLED.compareAndSet(false, true)) return;
|
||||
|
||||
OacWebRequestBroker.get().attachClient(protocolBridge.getProtocolClient());
|
||||
protocolBridge.getProtocolValues().eventManager.
|
||||
registerListener(new OacWebPacketListener(OacWebRequestBroker.get(), protocolBridge.getProtocolClient(), impl));
|
||||
|
||||
ProtocolHandlerPackages.installPackage("org.openautonomousconnection.urlhandler");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package org.openautonomousconnection.protocol.urlhandler.v1_0_0.beta;
|
||||
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Utility to safely append packages to "java.protocol.handler.pkgs".
|
||||
*/
|
||||
public final class ProtocolHandlerPackages {
|
||||
|
||||
private static final String KEY = "java.protocol.handler.pkgs";
|
||||
|
||||
private ProtocolHandlerPackages() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends a package prefix to the protocol handler search path.
|
||||
*
|
||||
* @param pkg package prefix (e.g. "com.example.protocols")
|
||||
*/
|
||||
public static void installPackage(String pkg) {
|
||||
Objects.requireNonNull(pkg, "pkg");
|
||||
String p = pkg.trim();
|
||||
if (p.isEmpty()) return;
|
||||
|
||||
String existing = System.getProperty(KEY, "");
|
||||
Set<String> parts = new LinkedHashSet<>();
|
||||
|
||||
if (!existing.isBlank()) {
|
||||
for (String s : existing.split("\\|")) {
|
||||
String t = s.trim();
|
||||
if (!t.isEmpty()) parts.add(t);
|
||||
}
|
||||
}
|
||||
|
||||
parts.add(p);
|
||||
|
||||
String merged = String.join("|", parts);
|
||||
System.setProperty(KEY, merged);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package org.openautonomousconnection.protocol.urlhandler.v1_0_0.beta.web;
|
||||
|
||||
import org.openautonomousconnection.protocol.urlhandler.v1_0_0.beta.OacSessionJar;
|
||||
import org.openautonomousconnection.protocol.urlhandler.v1_0_0.beta.OacWebHttpURLConnection;
|
||||
import org.openautonomousconnection.protocol.urlhandler.v1_0_0.beta.OacWebRequestBroker;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.net.URLStreamHandler;
|
||||
|
||||
/**
|
||||
* URLStreamHandler for the "web" protocol (loaded via java.protocol.handler.pkgs).
|
||||
*/
|
||||
public final class Handler extends URLStreamHandler {
|
||||
private static final OacSessionJar SESSION_JAR = new OacSessionJar();
|
||||
|
||||
@Override
|
||||
protected URLConnection openConnection(URL u) throws IOException {
|
||||
return new OacWebHttpURLConnection(u, OacWebRequestBroker.get(), SESSION_JAR);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,326 @@
|
||||
package org.openautonomousconnection.protocol.urlhandler.v1_0_1.beta;
|
||||
|
||||
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.packets.C_PacketReadEvent;
|
||||
import dev.unlegitdqrk.unlegitlibrary.network.system.client.events.state.ClientDisconnectedEvent;
|
||||
import org.openautonomousconnection.protocol.ProtocolBridge;
|
||||
import org.openautonomousconnection.protocol.packets.v1_0_0.beta.INSResponsePacket;
|
||||
import org.openautonomousconnection.protocol.side.client.ProtocolClient;
|
||||
import org.openautonomousconnection.protocol.side.client.events.ConnectedToProtocolINSServerEvent;
|
||||
import org.openautonomousconnection.protocol.side.client.events.ConnectedToProtocolServerEvent;
|
||||
import org.openautonomousconnection.protocol.urlhandler.v1_0_1.beta.web.OacWebRequestBroker;
|
||||
import org.openautonomousconnection.protocol.versions.v1_0_0.beta.INSRecord;
|
||||
import org.openautonomousconnection.protocol.versions.v1_0_0.beta.INSRecordType;
|
||||
import org.openautonomousconnection.protocol.versions.v1_0_0.beta.INSResponseStatus;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Coordinates INS resolution for WEB requests and connects to the resolved server endpoint.
|
||||
*
|
||||
* <p>No {@code sendIns()} exists. This coordinator sends INS queries directly:</p>
|
||||
* <ul>
|
||||
* <li>{@link #requestResolve(String)} is called on every WEB request.</li>
|
||||
* <li>Requests for the currently connected InfoName reuse the active server connection.</li>
|
||||
* <li>If INS is connected, unresolved hosts immediately trigger {@code sendINSQuery(tln,name,sub,A)}.</li>
|
||||
* <li>If INS is not connected yet, it stores the pending host and sends on {@link ConnectedToProtocolINSServerEvent}.</li>
|
||||
* <li>On {@link INSResponsePacket} it connects the server connection.</li>
|
||||
* <li>On {@link ConnectedToProtocolServerEvent} it opens the broker gate.</li>
|
||||
* </ul>
|
||||
*/
|
||||
public final class InsResolutionCoordinator_v1_0_1_B extends EventListener {
|
||||
|
||||
private static final InsResolutionCoordinator_v1_0_1_B INSTANCE = new InsResolutionCoordinator_v1_0_1_B();
|
||||
|
||||
private final Object lock = new Object();
|
||||
|
||||
private volatile ProtocolBridge bridge;
|
||||
|
||||
private volatile ProtocolClient insClient; // from ConnectedToProtocolINSServerEvent
|
||||
private volatile boolean insConnected;
|
||||
private volatile Object activeServerConnection;
|
||||
|
||||
private volatile String activeInfoName;
|
||||
private volatile String pendingInfoName;
|
||||
|
||||
private InsResolutionCoordinator_v1_0_1_B() {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return global singleton instance
|
||||
*/
|
||||
public static InsResolutionCoordinator_v1_0_1_B get() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches runtime dependencies.
|
||||
*
|
||||
* @param protocolBridge protocol bridge
|
||||
*/
|
||||
public synchronized void attach(ProtocolBridge protocolBridge) {
|
||||
Objects.requireNonNull(protocolBridge, "protocolBridge");
|
||||
|
||||
if (this.bridge == null) {
|
||||
this.bridge = protocolBridge;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.bridge == protocolBridge) return;
|
||||
|
||||
throw new IllegalStateException("InsResolutionCoordinator runtime already initialized with different instances");
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the WEB broker for EVERY outgoing request.
|
||||
*
|
||||
* <p>If the requested InfoName is already connected, the current server connection is reused.
|
||||
* Otherwise an INS query is triggered for the URL host. If INS is not connected yet, the host is stored
|
||||
* and will be sent once INS connects.</p>
|
||||
*
|
||||
* @param infoName host from {@code web://<infoName>/...}
|
||||
*/
|
||||
public void requestResolve(String infoName) {
|
||||
String in = (infoName == null) ? "" : infoName.trim();
|
||||
if (in.isEmpty()) {
|
||||
throw new IllegalArgumentException("InfoName is empty");
|
||||
}
|
||||
|
||||
if (canReuseCurrentServerConnection(in)) {
|
||||
OacWebRequestBroker.get().notifyServerConnected();
|
||||
return;
|
||||
}
|
||||
|
||||
synchronized (lock) {
|
||||
pendingInfoName = in;
|
||||
}
|
||||
|
||||
// Unresolved or changed hosts reset the server gate and trigger INS -> connect -> request.
|
||||
OacWebRequestBroker.get().beginServerConnectAttempt();
|
||||
|
||||
// If INS already connected, send immediately
|
||||
trySendPendingInsNow();
|
||||
}
|
||||
|
||||
/**
|
||||
* INS is now connected -> we can send pending INS immediately.
|
||||
*
|
||||
* @param event ins connected event
|
||||
*/
|
||||
@Listener(priority = EventPriority.HIGHEST)
|
||||
public void onConnectedToIns(ConnectedToProtocolINSServerEvent event) {
|
||||
Objects.requireNonNull(event, "event");
|
||||
|
||||
this.insClient = Objects.requireNonNull(event.getClient(), "event.getClient()");
|
||||
this.insConnected = true;
|
||||
|
||||
// Send pending (if any) now that INS is connected
|
||||
trySendPendingInsNow();
|
||||
}
|
||||
|
||||
/**
|
||||
* Receives INS resolution response and starts the WEB server connect attempt.
|
||||
*
|
||||
* @param event packet read event
|
||||
*/
|
||||
@Listener(priority = EventPriority.HIGHEST)
|
||||
public void onInsResponse(C_PacketReadEvent event) {
|
||||
Objects.requireNonNull(event, "event");
|
||||
if (!(event.getPacket() instanceof INSResponsePacket packet)) return;
|
||||
|
||||
final String infoName;
|
||||
final String hostname;
|
||||
final int port;
|
||||
|
||||
try {
|
||||
synchronized (lock) {
|
||||
infoName = pendingInfoName;
|
||||
}
|
||||
if (infoName == null || infoName.isBlank()) {
|
||||
throw new IllegalStateException("INS response received without pending info name");
|
||||
}
|
||||
|
||||
INSResponseStatus status = packet.getStatus();
|
||||
List<INSRecord> records = packet.getRecords();
|
||||
|
||||
if (status != INSResponseStatus.OK) {
|
||||
throw new IllegalStateException("INS resolution failed: " + status);
|
||||
}
|
||||
|
||||
if (records == null || records.isEmpty() || records.getFirst() == null || records.getFirst().value == null) {
|
||||
throw new IllegalStateException("INS resolution returned no usable records");
|
||||
}
|
||||
|
||||
String host = records.getFirst().value.trim();
|
||||
if (host.isEmpty()) {
|
||||
throw new IllegalStateException("INS record value is empty");
|
||||
}
|
||||
|
||||
if (!host.contains(":")) {
|
||||
hostname = host;
|
||||
|
||||
int recordPort = records.getFirst().port;
|
||||
port = (recordPort == 0) ? 1028 : recordPort;
|
||||
} else {
|
||||
String[] split = host.split(":", 2);
|
||||
String h = split[0].trim();
|
||||
String p1 = split[1].trim();
|
||||
|
||||
if (h.isEmpty() || p1.isEmpty()) {
|
||||
throw new IllegalStateException("Invalid INS host:port value: " + host);
|
||||
}
|
||||
|
||||
try {
|
||||
port = Integer.parseInt(p1);
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IllegalStateException("Invalid port in INS record: " + host, e);
|
||||
}
|
||||
|
||||
hostname = h;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
OacWebRequestBroker.get().notifyServerConnectionFailed(e);
|
||||
return;
|
||||
}
|
||||
|
||||
Thread t = new Thread(() -> connectServer(infoName, hostname, port), "oac-web-server-connect");
|
||||
t.setDaemon(true);
|
||||
t.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Server connected -> open broker gate.
|
||||
*
|
||||
* @param event server connected event
|
||||
*/
|
||||
@Listener(priority = EventPriority.HIGHEST)
|
||||
public void onConnectedToServer(ConnectedToProtocolServerEvent event) {
|
||||
Objects.requireNonNull(event, "event");
|
||||
ProtocolClient client = event.getClient();
|
||||
if (client != null) {
|
||||
this.activeServerConnection = client.getClientServerConnection();
|
||||
}
|
||||
OacWebRequestBroker.get().notifyServerConnected();
|
||||
}
|
||||
|
||||
@Listener(priority = EventPriority.HIGHEST)
|
||||
public void onDisconnected(ClientDisconnectedEvent event) {
|
||||
Objects.requireNonNull(event, "event");
|
||||
|
||||
Object disconnected = event.getClient();
|
||||
if (disconnected == null) return;
|
||||
|
||||
ProtocolBridge b = this.bridge;
|
||||
ProtocolClient client = (b == null) ? null : b.getProtocolClient();
|
||||
|
||||
if (disconnected == activeServerConnection) {
|
||||
activeServerConnection = null;
|
||||
activeInfoName = null;
|
||||
OacWebRequestBroker.get().notifyServerConnectionFailed(
|
||||
new IllegalStateException("Server connection was lost")
|
||||
);
|
||||
}
|
||||
|
||||
if (client != null && disconnected == client.getClientINSConnection()) {
|
||||
this.insConnected = false;
|
||||
this.pendingInfoName = null;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean canReuseCurrentServerConnection(String infoName) {
|
||||
ProtocolBridge b = this.bridge;
|
||||
if (b == null) return false;
|
||||
|
||||
ProtocolClient client = b.getProtocolClient();
|
||||
if (client == null || client.getClientServerConnection() == null || !client.getClientServerConnection().isConnected()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return infoName.equalsIgnoreCase(activeInfoName);
|
||||
}
|
||||
|
||||
private void trySendPendingInsNow() {
|
||||
ProtocolClient c = this.insClient;
|
||||
if (c == null) return;
|
||||
|
||||
final String infoName;
|
||||
synchronized (lock) {
|
||||
infoName = pendingInfoName;
|
||||
}
|
||||
|
||||
if (infoName == null || infoName.isBlank()) return;
|
||||
|
||||
InsParts parts = InsParts.parse(infoName);
|
||||
|
||||
try {
|
||||
c.sendINSQuery(parts.tln(), parts.name(), parts.sub(), INSRecordType.A);
|
||||
} catch (Exception e) {
|
||||
OacWebRequestBroker.get().notifyServerConnectionFailed(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void connectServer(String infoName, String hostname, int port) {
|
||||
ProtocolBridge b = this.bridge;
|
||||
if (b == null) return;
|
||||
if (canReuseCurrentServerConnection(infoName)) {
|
||||
OacWebRequestBroker.get().notifyServerConnected();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
ProtocolClient client = b.getProtocolClient();
|
||||
|
||||
if (client.getClientServerConnection() != null && client.getClientServerConnection().isConnected()) {
|
||||
client.getClientServerConnection().disconnect();
|
||||
}
|
||||
|
||||
client.buildServerConnection(
|
||||
null,
|
||||
client.getProtocolBridge().getProtocolValues().ssl
|
||||
);
|
||||
|
||||
client.getClientServerConnection().connect(hostname, port);
|
||||
this.activeServerConnection = client.getClientServerConnection();
|
||||
this.activeInfoName = infoName;
|
||||
// Broker gate is opened by ConnectedToProtocolServerEvent.
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
this.activeServerConnection = null;
|
||||
this.activeInfoName = null;
|
||||
OacWebRequestBroker.get().notifyServerConnectionFailed(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses name.tln or sub.name.tln.
|
||||
*/
|
||||
public record InsParts(String tln, String name, String sub) {
|
||||
|
||||
/**
|
||||
* Parses an InfoName into INS parts.
|
||||
*
|
||||
* @param infoName infoName (name.tln or sub.name.tln)
|
||||
* @return parts
|
||||
*/
|
||||
public static InsParts parse(String infoName) {
|
||||
String in = Objects.requireNonNull(infoName, "infoName").trim();
|
||||
if (in.isEmpty()) throw new IllegalArgumentException("infoName is empty");
|
||||
|
||||
String[] parts = in.split("\\.");
|
||||
if (parts.length < 2 || parts.length > 3) {
|
||||
throw new IllegalArgumentException(
|
||||
"Invalid INS address format: " + infoName + " (expected name.tln or sub.name.tln)"
|
||||
);
|
||||
}
|
||||
|
||||
String tln = parts[parts.length - 1];
|
||||
String name = parts[parts.length - 2];
|
||||
String sub = (parts.length == 3) ? parts[0] : null;
|
||||
|
||||
return new InsParts(tln, name, sub);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package org.openautonomousconnection.protocol.urlhandler.v1_0_1.beta;
|
||||
|
||||
import org.openautonomousconnection.protocol.urlhandler.v1_0_0.beta.LibClientImpl_v1_0_0_B;
|
||||
import org.openautonomousconnection.protocol.urlhandler.v1_0_1.beta.web.WebFlagInspector;
|
||||
import org.openautonomousconnection.protocol.urlhandler.v1_0_1.beta.web.WebRequestContextProvider;
|
||||
import org.openautonomousconnection.protocol.versions.v1_0_1.beta.WebPacketHeader;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Callback surface for URL-handler related streaming events (v1.0.1-BETA).
|
||||
*/
|
||||
public abstract class LibClientImpl_v1_0_1_B extends LibClientImpl_v1_0_0_B implements WebRequestContextProvider, WebFlagInspector {
|
||||
|
||||
/**
|
||||
* Called when a streamed response begins.
|
||||
*/
|
||||
public void streamStart(
|
||||
WebPacketHeader header,
|
||||
int statusCode,
|
||||
String contentType,
|
||||
Map<String, String> headers,
|
||||
long totalLength
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called for each streamed chunk.
|
||||
*/
|
||||
public void streamChunk(
|
||||
WebPacketHeader header,
|
||||
int seq,
|
||||
byte[] data
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the stream ends.
|
||||
*/
|
||||
public void streamEnd(
|
||||
WebPacketHeader header,
|
||||
boolean ok,
|
||||
String error
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after full best-effort assembly (only if ok=true).
|
||||
*/
|
||||
public void streamFinish(
|
||||
WebPacketHeader header,
|
||||
byte[] content
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package org.openautonomousconnection.protocol.urlhandler.v1_0_1.beta;
|
||||
|
||||
import org.openautonomousconnection.protocol.ProtocolBridge;
|
||||
import org.openautonomousconnection.protocol.urlhandler.v1_0_1.beta.web.OacWebProtocolModule_v1_0_1_B;
|
||||
import org.openautonomousconnection.protocol.urlhandler.v1_0_1.beta.web.OacWebRequestBroker;
|
||||
import org.openautonomousconnection.protocol.urlhandler.v1_0_1.beta.web.WebFlagInspector;
|
||||
import org.openautonomousconnection.protocol.urlhandler.v1_0_1.beta.web.WebRequestContextProvider;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* Installs OAC URL protocol modules for v1.0.1-BETA.
|
||||
*
|
||||
* <p>Runtime behavior required by this installer:</p>
|
||||
* <ul>
|
||||
* <li>Every WEB request triggers an INS resolve attempt (via {@link InsResolutionCoordinator_v1_0_1_B}).</li>
|
||||
* <li>Once an INS response arrives, the client connects to the resolved server endpoint.</li>
|
||||
* <li>Only after the server connection is established, WEB packets may be sent (broker gate).</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>Call once during startup (before creating {@link java.net.URL} instances).</p>
|
||||
*/
|
||||
public final class OacUrlHandlerInstaller_v1_0_1_B {
|
||||
|
||||
private static final AtomicBoolean INSTALLED = new AtomicBoolean(false);
|
||||
|
||||
private OacUrlHandlerInstaller_v1_0_1_B() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs the URL handler package path and registers the WEB module.
|
||||
*
|
||||
* @param protocolBridge protocol bridge
|
||||
* @param impl callback implementation
|
||||
* @param ctxProvider provides tab/page/frame correlation for each request
|
||||
* @param flagInspector interprets {@code WebPacketHeader.flags} (e.g. stream bit)
|
||||
*/
|
||||
public static void installOnce(
|
||||
ProtocolBridge protocolBridge,
|
||||
LibClientImpl_v1_0_1_B impl,
|
||||
WebRequestContextProvider ctxProvider,
|
||||
WebFlagInspector flagInspector
|
||||
) {
|
||||
Objects.requireNonNull(protocolBridge, "protocolBridge");
|
||||
Objects.requireNonNull(impl, "impl");
|
||||
Objects.requireNonNull(ctxProvider, "ctxProvider");
|
||||
Objects.requireNonNull(flagInspector, "flagInspector");
|
||||
|
||||
if (!INSTALLED.compareAndSet(false, true)) return;
|
||||
|
||||
ProtocolHandlerPackages.installPackage("org.openautonomousconnection.protocol.urlhandler.v1_0_1.beta");
|
||||
|
||||
// WEB module (URLConnection + web packet listener -> broker)
|
||||
OacWebProtocolModule_v1_0_1_B web = new OacWebProtocolModule_v1_0_1_B(ctxProvider, flagInspector, protocolBridge);
|
||||
OacUrlProtocolRegistry.register(web);
|
||||
web.install(protocolBridge, impl);
|
||||
|
||||
// INS coordination (request -> send INS -> connect server -> open broker gate)
|
||||
InsResolutionCoordinator_v1_0_1_B coordinator = InsResolutionCoordinator_v1_0_1_B.get();
|
||||
coordinator.attach(protocolBridge);
|
||||
|
||||
protocolBridge.getProtocolValues().eventManager.registerListener(coordinator);
|
||||
|
||||
// Broker needs to wait for "server connected" gate. Coordinator will open/fail this gate.
|
||||
OacWebRequestBroker.get().attachCoordinator(coordinator);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package org.openautonomousconnection.protocol.urlhandler.v1_0_1.beta;
|
||||
|
||||
import org.openautonomousconnection.protocol.ProtocolBridge;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
|
||||
/**
|
||||
* Pluggable module for a URL protocol scheme (e.g. "web", "ftp").
|
||||
*
|
||||
* <p>Each module is responsible for providing {@link URLConnection} instances for its scheme and
|
||||
* wiring packet listeners into the {@link ProtocolBridge} runtime.</p>
|
||||
*/
|
||||
public interface OacUrlProtocolModule {
|
||||
|
||||
/**
|
||||
* @return the URL scheme handled by this module (e.g. "web", "ftp")
|
||||
*/
|
||||
String scheme();
|
||||
|
||||
/**
|
||||
* Opens a connection for the given URL.
|
||||
*
|
||||
* @param url the URL
|
||||
* @return the connection
|
||||
* @throws IOException if opening fails
|
||||
*/
|
||||
URLConnection openConnection(URL url) throws IOException;
|
||||
|
||||
/**
|
||||
* Installs the module into the given protocol bridge (listeners, brokers, etc.).
|
||||
*
|
||||
* @param protocolBridge protocol bridge
|
||||
* @param impl callback implementation
|
||||
*/
|
||||
void install(ProtocolBridge protocolBridge, LibClientImpl_v1_0_1_B impl);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package org.openautonomousconnection.protocol.urlhandler.v1_0_1.beta;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Global registry for URL protocol modules.
|
||||
*/
|
||||
public final class OacUrlProtocolRegistry {
|
||||
|
||||
private static final ConcurrentHashMap<String, OacUrlProtocolModule> MODULES = new ConcurrentHashMap<>();
|
||||
|
||||
private OacUrlProtocolRegistry() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a module for its scheme.
|
||||
*
|
||||
* @param module the module
|
||||
* @throws IllegalStateException if a different module is already registered for the scheme
|
||||
*/
|
||||
public static void register(OacUrlProtocolModule module) {
|
||||
Objects.requireNonNull(module, "module");
|
||||
|
||||
String scheme = normalizeScheme(module.scheme());
|
||||
OacUrlProtocolModule prev = MODULES.putIfAbsent(scheme, module);
|
||||
|
||||
if (prev != null && prev != module) {
|
||||
throw new IllegalStateException(
|
||||
"Module already registered for scheme '" + scheme + "': " + prev.getClass().getName()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the module for the given scheme.
|
||||
*
|
||||
* @param scheme the scheme
|
||||
* @return module or null if not registered
|
||||
*/
|
||||
public static OacUrlProtocolModule get(String scheme) {
|
||||
if (scheme == null) return null;
|
||||
return MODULES.get(normalizeScheme(scheme));
|
||||
}
|
||||
|
||||
private static String normalizeScheme(String scheme) {
|
||||
String s = Objects.requireNonNull(scheme, "scheme").trim();
|
||||
if (s.isEmpty()) throw new IllegalArgumentException("scheme is empty");
|
||||
return s.toLowerCase(Locale.ROOT);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package org.openautonomousconnection.protocol.urlhandler.v1_0_1.beta;
|
||||
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Utility to safely append packages to "java.protocol.handler.pkgs".
|
||||
*
|
||||
* <p>JVM lookup rule: it searches {@code <pkgprefix>.<protocol>.Handler}.</p>
|
||||
*/
|
||||
public final class ProtocolHandlerPackages {
|
||||
|
||||
private static final String KEY = "java.protocol.handler.pkgs";
|
||||
|
||||
private ProtocolHandlerPackages() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends a package prefix to the protocol handler search path.
|
||||
*
|
||||
* @param pkg package prefix (e.g. "org.openautonomousconnection.protocol.urlhandler.v1_0_1.beta")
|
||||
*/
|
||||
public static void installPackage(String pkg) {
|
||||
Objects.requireNonNull(pkg, "pkg");
|
||||
String p = pkg.trim();
|
||||
if (p.isEmpty()) return;
|
||||
|
||||
String existing = System.getProperty(KEY, "");
|
||||
Set<String> parts = new LinkedHashSet<>();
|
||||
|
||||
if (!existing.isBlank()) {
|
||||
for (String s : existing.split("\\|")) {
|
||||
String t = s.trim();
|
||||
if (!t.isEmpty()) parts.add(t);
|
||||
}
|
||||
}
|
||||
|
||||
parts.add(p);
|
||||
|
||||
String merged = String.join("|", parts);
|
||||
System.setProperty(KEY, merged);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package org.openautonomousconnection.protocol.urlhandler.v1_0_1.beta.web;
|
||||
|
||||
import java.io.FilterInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.*;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* InputStream wrapper that deletes one or more paths (files/directories) on close.
|
||||
*
|
||||
* <p>Useful for temporary downloads/streams that must be cleaned up automatically.</p>
|
||||
*/
|
||||
final class DeleteOnCloseInputStream extends FilterInputStream {
|
||||
|
||||
private final Path[] deletePaths;
|
||||
private volatile boolean closed;
|
||||
|
||||
DeleteOnCloseInputStream(InputStream in, Path... deletePaths) {
|
||||
super(Objects.requireNonNull(in, "in"));
|
||||
this.deletePaths = (deletePaths == null) ? new Path[0] : deletePaths.clone();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
if (closed) return;
|
||||
closed = true;
|
||||
|
||||
IOException io = null;
|
||||
try {
|
||||
super.close();
|
||||
} catch (IOException e) {
|
||||
io = e;
|
||||
}
|
||||
|
||||
for (Path p : deletePaths) {
|
||||
if (p == null) continue;
|
||||
try {
|
||||
deleteRecursivelyIfExists(p);
|
||||
} catch (IOException e) {
|
||||
if (io == null) io = e;
|
||||
}
|
||||
}
|
||||
|
||||
if (io != null) throw io;
|
||||
}
|
||||
|
||||
private static void deleteRecursivelyIfExists(Path path) throws IOException {
|
||||
if (!Files.exists(path)) return;
|
||||
|
||||
if (Files.isDirectory(path)) {
|
||||
Files.walkFileTree(path, new SimpleFileVisitor<>() {
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
|
||||
Files.deleteIfExists(file);
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
|
||||
Files.deleteIfExists(dir);
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
Files.deleteIfExists(path);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package org.openautonomousconnection.protocol.urlhandler.v1_0_1.beta.web;
|
||||
|
||||
import org.openautonomousconnection.protocol.urlhandler.v1_0_1.beta.OacUrlProtocolModule;
|
||||
import org.openautonomousconnection.protocol.urlhandler.v1_0_1.beta.OacUrlProtocolRegistry;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.net.URLStreamHandler;
|
||||
|
||||
/**
|
||||
* URLStreamHandler for the "web" protocol.
|
||||
*
|
||||
* <p>Loaded by JVM via {@code java.protocol.handler.pkgs} and delegates to the registered module.</p>
|
||||
*/
|
||||
public final class Handler extends URLStreamHandler {
|
||||
|
||||
@Override
|
||||
protected URLConnection openConnection(URL u) throws IOException {
|
||||
OacUrlProtocolModule module = OacUrlProtocolRegistry.get("web");
|
||||
if (module == null) {
|
||||
throw new IOException("No module registered for scheme 'web'. Did you call OacUrlHandlerInstaller_v1_0_1_B.installOnce(...)?");
|
||||
}
|
||||
return module.openConnection(u);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package org.openautonomousconnection.protocol.urlhandler.v1_0_1.beta.web;
|
||||
|
||||
/**
|
||||
* Stores the current session token across multiple URLConnection instances.
|
||||
*/
|
||||
public final class OacSessionJar {
|
||||
|
||||
private volatile String session;
|
||||
|
||||
/**
|
||||
* Stores a session token (e.g. from response header "session").
|
||||
*
|
||||
* @param session session token
|
||||
*/
|
||||
public void store(String session) {
|
||||
String s = (session == null) ? null : session.trim();
|
||||
this.session = (s == null || s.isEmpty()) ? null : s;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return stored session token or null
|
||||
*/
|
||||
public String get() {
|
||||
return session;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
package org.openautonomousconnection.protocol.urlhandler.v1_0_1.beta.web;
|
||||
|
||||
import org.openautonomousconnection.protocol.ProtocolBridge;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.ProtocolException;
|
||||
import java.net.URL;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* HttpURLConnection implementation that maps "web://" URLs to WEB v1.0.1 protocol requests.
|
||||
*
|
||||
* <p>This implementation is designed for JavaFX WebView semantics.</p>
|
||||
*/
|
||||
public final class OacWebHttpURLConnection extends HttpURLConnection {
|
||||
|
||||
private static final int MAX_REDIRECTS = 8;
|
||||
|
||||
private final OacWebRequestBroker broker;
|
||||
private final OacSessionJar sessionJar;
|
||||
private final WebRequestContextProvider ctxProvider;
|
||||
private final ProtocolBridge bridge;
|
||||
|
||||
private final Map<String, String> requestHeaders = new LinkedHashMap<>();
|
||||
private final ByteArrayOutputStream requestBody = new ByteArrayOutputStream(1024);
|
||||
|
||||
private boolean requestSent;
|
||||
private OacWebResponse response;
|
||||
|
||||
public OacWebHttpURLConnection(URL url, OacWebRequestBroker broker, OacSessionJar sessionJar, WebRequestContextProvider ctxProvider, ProtocolBridge protocolBridge) {
|
||||
super(url);
|
||||
this.broker = Objects.requireNonNull(broker, "broker");
|
||||
this.sessionJar = Objects.requireNonNull(sessionJar, "sessionJar");
|
||||
this.ctxProvider = Objects.requireNonNull(ctxProvider, "ctxProvider");
|
||||
this.bridge = Objects.requireNonNull(protocolBridge, "bridge");
|
||||
this.method = "GET";
|
||||
setInstanceFollowRedirects(true);
|
||||
}
|
||||
|
||||
private static String headerValue(Map<String, String> headers, String nameLower) {
|
||||
if (headers == null || headers.isEmpty() || nameLower == null) return null;
|
||||
String needle = nameLower.trim().toLowerCase(Locale.ROOT);
|
||||
for (Map.Entry<String, String> e : headers.entrySet()) {
|
||||
if (e.getKey() == null) continue;
|
||||
if (e.getKey().trim().toLowerCase(Locale.ROOT).equals(needle)) {
|
||||
return e.getValue();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRequestProperty(String key, String value) {
|
||||
if (key == null) return;
|
||||
if (requestSent) throw new IllegalStateException("Request already sent");
|
||||
if (value == null) requestHeaders.remove(key);
|
||||
else requestHeaders.put(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRequestProperty(String key) {
|
||||
if (key == null) return null;
|
||||
for (Map.Entry<String, String> e : requestHeaders.entrySet()) {
|
||||
if (e.getKey() != null && e.getKey().equalsIgnoreCase(key)) return e.getValue();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, List<String>> getRequestProperties() {
|
||||
Map<String, List<String>> out = new LinkedHashMap<>();
|
||||
for (Map.Entry<String, String> e : requestHeaders.entrySet()) {
|
||||
out.put(e.getKey(), e.getValue() == null ? List.of() : List.of(e.getValue()));
|
||||
}
|
||||
return Collections.unmodifiableMap(out);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRequestMethod(String method) throws ProtocolException {
|
||||
if (method == null) throw new ProtocolException("method is null");
|
||||
if (requestSent) throw new ProtocolException("Request already sent");
|
||||
|
||||
String m = method.trim().toUpperCase(Locale.ROOT);
|
||||
if (!m.equals("GET") && !m.equals("POST")) {
|
||||
throw new ProtocolException("Unsupported method: " + method);
|
||||
}
|
||||
this.method = m;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OutputStream getOutputStream() {
|
||||
if (requestSent) throw new IllegalStateException("Request already sent");
|
||||
setDoOutput(true);
|
||||
return requestBody;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void connect() {
|
||||
// Intentionally no-op: JavaFX WebView may call connect() before writing POST body.
|
||||
}
|
||||
|
||||
private void ensureResponse() throws IOException {
|
||||
if (requestSent) return;
|
||||
|
||||
URL cur = this.url;
|
||||
String methodStr = (this.method == null) ? "GET" : this.method.trim().toUpperCase(Locale.ROOT);
|
||||
|
||||
Map<String, String> carryHeaders = new LinkedHashMap<>(requestHeaders);
|
||||
|
||||
String session = sessionJar.get();
|
||||
if (session != null && !session.isBlank() && headerValue(carryHeaders, "session") == null) {
|
||||
carryHeaders.put("session", session);
|
||||
}
|
||||
|
||||
byte[] carryBody = null;
|
||||
if (getDoOutput()) {
|
||||
carryBody = requestBody.toByteArray();
|
||||
if ("POST".equals(methodStr) && carryBody == null) carryBody = new byte[0];
|
||||
}
|
||||
|
||||
if ("POST".equals(methodStr) && headerValue(carryHeaders, "content-type") == null) {
|
||||
carryHeaders.put("content-type", "application/x-www-form-urlencoded; charset=utf-8");
|
||||
}
|
||||
|
||||
OacWebResponse resp = null;
|
||||
|
||||
for (int i = 0; i <= MAX_REDIRECTS; i++) {
|
||||
WebRequestContextProvider.WebRequestContext ctx = ctxProvider.contextFor(cur);
|
||||
|
||||
resp = broker.fetch(
|
||||
cur,
|
||||
methodStr,
|
||||
carryHeaders,
|
||||
carryBody,
|
||||
ctx.tabId(),
|
||||
ctx.pageId(),
|
||||
ctx.frameId(), bridge
|
||||
);
|
||||
|
||||
String newSession = broker.extractSession(resp.headers());
|
||||
if (newSession != null && !newSession.isBlank()) {
|
||||
sessionJar.store(newSession);
|
||||
carryHeaders.put("session", newSession);
|
||||
}
|
||||
|
||||
int code = resp.statusCode();
|
||||
if (!getInstanceFollowRedirects()) break;
|
||||
|
||||
if (code == 301 || code == 302 || code == 303 || code == 307 || code == 308) {
|
||||
String loc = headerValue(resp.headers(), "location");
|
||||
if (loc == null || loc.isBlank()) break;
|
||||
|
||||
try {
|
||||
cur = new URL(cur, loc);
|
||||
} catch (Exception ex) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (code == 303) {
|
||||
methodStr = "GET";
|
||||
carryBody = null;
|
||||
} else if ((code == 301 || code == 302) && "POST".equals(methodStr)) {
|
||||
methodStr = "GET";
|
||||
carryBody = null;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
this.response = resp;
|
||||
this.requestSent = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getInputStream() throws IOException {
|
||||
ensureResponse();
|
||||
if (response == null) return new ByteArrayInputStream(new byte[0]);
|
||||
return response.bodyStream();
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getErrorStream() {
|
||||
try {
|
||||
ensureResponse();
|
||||
if (response == null) return null;
|
||||
return (response.statusCode() >= 400) ? response.bodyStream() : null;
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getResponseCode() throws IOException {
|
||||
ensureResponse();
|
||||
return response == null ? -1 : response.statusCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getContentType() {
|
||||
try {
|
||||
ensureResponse();
|
||||
} catch (IOException e) {
|
||||
return "application/octet-stream";
|
||||
}
|
||||
String ct = (response == null) ? null : response.contentType();
|
||||
return (ct == null || ct.isBlank()) ? "application/octet-stream" : ct;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getContentLength() {
|
||||
try {
|
||||
ensureResponse();
|
||||
} catch (IOException e) {
|
||||
return -1;
|
||||
}
|
||||
long len = (response == null) ? -1L : response.contentLength();
|
||||
return (len <= 0 || len > Integer.MAX_VALUE) ? -1 : (int) len;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getContentLengthLong() {
|
||||
try {
|
||||
ensureResponse();
|
||||
} catch (IOException e) {
|
||||
return -1L;
|
||||
}
|
||||
return (response == null) ? -1L : response.contentLength();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, List<String>> getHeaderFields() {
|
||||
try {
|
||||
ensureResponse();
|
||||
} catch (IOException e) {
|
||||
return Map.of();
|
||||
}
|
||||
if (response == null) return Map.of();
|
||||
|
||||
Map<String, List<String>> out = new LinkedHashMap<>();
|
||||
for (Map.Entry<String, String> e : response.headers().entrySet()) {
|
||||
String k = e.getKey();
|
||||
String v = e.getValue();
|
||||
if (k == null) continue;
|
||||
out.put(k, v == null ? List.of() : List.of(v));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHeaderField(String name) {
|
||||
if (name == null) return null;
|
||||
try {
|
||||
ensureResponse();
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
if (response == null) return null;
|
||||
return headerValue(response.headers(), name.trim().toLowerCase(Locale.ROOT));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disconnect() {
|
||||
// No persistent socket owned by this object.
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean usingProxy() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package org.openautonomousconnection.protocol.urlhandler.v1_0_1.beta.web;
|
||||
|
||||
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.packets.C_PacketReadEvent;
|
||||
import dev.unlegitdqrk.unlegitlibrary.network.system.packets.Packet;
|
||||
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.urlhandler.v1_0_1.beta.LibClientImpl_v1_0_1_B;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Receives incoming WEB packets and forwards them into the broker and client callbacks.
|
||||
*/
|
||||
public final class OacWebPacketListener extends EventListener {
|
||||
|
||||
private final OacWebRequestBroker broker;
|
||||
private final LibClientImpl_v1_0_1_B impl;
|
||||
|
||||
/**
|
||||
* Creates a listener bound to the given broker.
|
||||
*
|
||||
* @param broker broker instance
|
||||
* @param impl callback implementation
|
||||
*/
|
||||
public OacWebPacketListener(OacWebRequestBroker broker, LibClientImpl_v1_0_1_B impl) {
|
||||
this.broker = Objects.requireNonNull(broker, "broker");
|
||||
this.impl = Objects.requireNonNull(impl, "impl");
|
||||
}
|
||||
|
||||
@Listener(priority = EventPriority.HIGHEST)
|
||||
public void onPacketRead(C_PacketReadEvent event) {
|
||||
Packet p = event.getPacket();
|
||||
if (p instanceof WebResourceResponsePacket resp) {
|
||||
broker.onResourceResponse(resp);
|
||||
return;
|
||||
}
|
||||
|
||||
if (p instanceof WebStreamStartPacket_v1_0_1_B start) {
|
||||
impl.streamStart(
|
||||
start.getHeader(),
|
||||
start.getStatusCode(),
|
||||
start.getContentType(),
|
||||
start.getHeaders() == null ? Map.of() : start.getHeaders(),
|
||||
start.getTotalLength()
|
||||
);
|
||||
broker.onStreamStart(start);
|
||||
return;
|
||||
}
|
||||
|
||||
if (p instanceof WebStreamChunkPacket_v1_0_1_B chunk) {
|
||||
byte[] data = (chunk.getData() == null) ? new byte[0] : chunk.getData();
|
||||
impl.streamChunk(chunk.getHeader(), chunk.getSeq(), data);
|
||||
broker.onStreamChunk(chunk);
|
||||
return;
|
||||
}
|
||||
|
||||
if (p instanceof WebStreamEndPacket_v1_0_1_B end) {
|
||||
impl.streamEnd(end.getHeader(), end.isOk(), end.getError());
|
||||
byte[] full = broker.onStreamEndAndAssemble(end);
|
||||
if (full != null) {
|
||||
impl.streamFinish(end.getHeader(), full);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package org.openautonomousconnection.protocol.urlhandler.v1_0_1.beta.web;
|
||||
|
||||
import org.openautonomousconnection.protocol.ProtocolBridge;
|
||||
import org.openautonomousconnection.protocol.urlhandler.v1_0_1.beta.LibClientImpl_v1_0_1_B;
|
||||
import org.openautonomousconnection.protocol.urlhandler.v1_0_1.beta.OacUrlProtocolModule;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* WEB protocol module implementation for v1.0.1-BETA.
|
||||
*/
|
||||
public final class OacWebProtocolModule_v1_0_1_B implements OacUrlProtocolModule {
|
||||
|
||||
private final OacSessionJar sessionJar = new OacSessionJar();
|
||||
private final WebRequestContextProvider ctxProvider;
|
||||
private final WebFlagInspector flagInspector;
|
||||
private final ProtocolBridge bridge;
|
||||
|
||||
public OacWebProtocolModule_v1_0_1_B(WebRequestContextProvider ctxProvider, WebFlagInspector flagInspector, ProtocolBridge protocolBridge) {
|
||||
this.ctxProvider = Objects.requireNonNull(ctxProvider, "ctxProvider");
|
||||
this.flagInspector = Objects.requireNonNull(flagInspector, "flagInspector");
|
||||
this.bridge = Objects.requireNonNull(protocolBridge, "protocolBridge");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String scheme() {
|
||||
return "web";
|
||||
}
|
||||
|
||||
@Override
|
||||
public URLConnection openConnection(URL url) throws IOException {
|
||||
Objects.requireNonNull(url, "url");
|
||||
if (!"web".equalsIgnoreCase(url.getProtocol())) {
|
||||
throw new IOException("Unsupported scheme for this module: " + url.getProtocol());
|
||||
}
|
||||
return new OacWebHttpURLConnection(url, OacWebRequestBroker.get(), sessionJar, ctxProvider, bridge);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void install(ProtocolBridge protocolBridge, LibClientImpl_v1_0_1_B impl) {
|
||||
Objects.requireNonNull(protocolBridge, "protocolBridge");
|
||||
Objects.requireNonNull(impl, "impl");
|
||||
|
||||
OacWebRequestBroker.get().attachRuntime(protocolBridge.getProtocolClient(), flagInspector);
|
||||
|
||||
protocolBridge.getProtocolValues().eventManager.registerListener(
|
||||
new OacWebPacketListener(OacWebRequestBroker.get(), impl)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,614 @@
|
||||
package org.openautonomousconnection.protocol.urlhandler.v1_0_1.beta.web;
|
||||
|
||||
import dev.unlegitdqrk.unlegitlibrary.network.system.utils.TransportProtocol;
|
||||
import org.openautonomousconnection.protocol.ProtocolBridge;
|
||||
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.urlhandler.v1_0_1.beta.InsResolutionCoordinator_v1_0_1_B;
|
||||
import org.openautonomousconnection.protocol.versions.v1_0_1.beta.WebPacketFlags;
|
||||
import org.openautonomousconnection.protocol.versions.v1_0_1.beta.WebPacketHeader;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.URL;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
/**
|
||||
* Multi-flight broker for WEB v1.0.1-BETA with best-effort streaming and temp-file assembly.
|
||||
*
|
||||
* <p><b>Gate behavior:</b> Before any WEB packet is sent, the broker triggers INS resolve (every request),
|
||||
* then blocks until server connection is established (opened by {@link #notifyServerConnected()}).</p>
|
||||
*/
|
||||
public final class OacWebRequestBroker {
|
||||
|
||||
private static final OacWebRequestBroker INSTANCE = new OacWebRequestBroker();
|
||||
|
||||
private static final long RESPONSE_TIMEOUT_SECONDS = 30;
|
||||
private static final long CONNECT_TIMEOUT_SECONDS = 30;
|
||||
|
||||
private final ConcurrentHashMap<Long, ResponseState> inFlight = new ConcurrentHashMap<>();
|
||||
private final AtomicLong requestCounter = new AtomicLong(1);
|
||||
|
||||
private final Object connectLock = new Object();
|
||||
private volatile CompletableFuture<Void> serverReady = new CompletableFuture<>();
|
||||
private volatile Throwable serverFailure;
|
||||
|
||||
private volatile ProtocolClient client;
|
||||
private volatile WebFlagInspector flagInspector;
|
||||
|
||||
private volatile InsResolutionCoordinator_v1_0_1_B coordinator;
|
||||
|
||||
private OacWebRequestBroker() {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return global singleton instance
|
||||
*/
|
||||
public static OacWebRequestBroker get() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches runtime dependencies required for dispatching requests and interpreting flags.
|
||||
*
|
||||
* @param client protocol client used to send packets
|
||||
* @param flagInspector inspector for header flags (STREAM bit)
|
||||
*/
|
||||
public synchronized void attachRuntime(ProtocolClient client, WebFlagInspector flagInspector) {
|
||||
Objects.requireNonNull(client, "client");
|
||||
Objects.requireNonNull(flagInspector, "flagInspector");
|
||||
|
||||
if (this.client == null && this.flagInspector == null) {
|
||||
this.client = client;
|
||||
this.flagInspector = flagInspector;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.client == client && this.flagInspector == flagInspector) return;
|
||||
|
||||
throw new IllegalStateException("OacWebRequestBroker runtime already initialized with different instances");
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches the INS coordinator used for per-request INS resolution.
|
||||
*
|
||||
* @param coordinator coordinator
|
||||
*/
|
||||
public synchronized void attachCoordinator(InsResolutionCoordinator_v1_0_1_B coordinator) {
|
||||
Objects.requireNonNull(coordinator, "coordinator");
|
||||
if (this.coordinator == null) {
|
||||
this.coordinator = coordinator;
|
||||
return;
|
||||
}
|
||||
if (this.coordinator == coordinator) return;
|
||||
throw new IllegalStateException("Coordinator already attached with different instance");
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the server connection gate to "not ready".
|
||||
*/
|
||||
public void beginServerConnectAttempt() {
|
||||
synchronized (connectLock) {
|
||||
this.serverFailure = null;
|
||||
this.serverReady = new CompletableFuture<>();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the server connection gate.
|
||||
*/
|
||||
public void notifyServerConnected() {
|
||||
synchronized (connectLock) {
|
||||
this.serverFailure = null;
|
||||
CompletableFuture<Void> f = this.serverReady;
|
||||
if (!f.isDone()) f.complete(null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fails the server connection gate and unblocks waiters.
|
||||
*
|
||||
* @param t failure cause
|
||||
*/
|
||||
public void notifyServerConnectionFailed(Throwable t) {
|
||||
Objects.requireNonNull(t, "t");
|
||||
synchronized (connectLock) {
|
||||
this.serverFailure = t;
|
||||
CompletableFuture<Void> f = this.serverReady;
|
||||
if (!f.isDone()) f.completeExceptionally(t);
|
||||
}
|
||||
}
|
||||
|
||||
private void awaitServerConnection() throws IOException {
|
||||
ProtocolClient c = this.client;
|
||||
if (c == null) {
|
||||
throw new IllegalStateException("ProtocolClient not attached. Call OacWebRequestBroker.attachRuntime(...) during install.");
|
||||
}
|
||||
|
||||
if (c.getClientServerConnection() != null && c.getClientServerConnection().isConnected()) {
|
||||
return;
|
||||
}
|
||||
|
||||
CompletableFuture<Void> f = this.serverReady;
|
||||
try {
|
||||
f.get(CONNECT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
} catch (TimeoutException e) {
|
||||
throw new IOException("Timeout waiting for server connection after " + CONNECT_TIMEOUT_SECONDS + "s", e);
|
||||
} catch (ExecutionException e) {
|
||||
Throwable cause = (e.getCause() == null) ? e : e.getCause();
|
||||
throw new IOException("Server connection failed: " + cause.getMessage(), cause);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IOException("Interrupted while waiting for server connection", e);
|
||||
}
|
||||
|
||||
if (c.getClientServerConnection() == null || !c.getClientServerConnection().isConnected()) {
|
||||
Throwable fail = this.serverFailure;
|
||||
if (fail != null) throw new IOException("Server connection failed: " + fail.getMessage(), fail);
|
||||
throw new IOException("Server gate opened but connection is not connected");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a resource request and blocks until completion.
|
||||
*
|
||||
* <p><b>Required flow:</b></p>
|
||||
* <ol>
|
||||
* <li>Trigger INS query for URL host (EVERY request).</li>
|
||||
* <li>Wait for INS response -> connect -> server connected gate.</li>
|
||||
* <li>Send WEB request packet.</li>
|
||||
* </ol>
|
||||
*/
|
||||
public OacWebResponse fetch(
|
||||
URL url,
|
||||
String method,
|
||||
Map<String, String> headers,
|
||||
byte[] body,
|
||||
long tabId,
|
||||
long pageId,
|
||||
long frameId, ProtocolBridge bridge
|
||||
) throws IOException {
|
||||
Objects.requireNonNull(url, "url");
|
||||
|
||||
String host = url.getHost();
|
||||
if (host == null || host.isBlank()) {
|
||||
throw new IOException("Missing InfoName in URL host: " + url);
|
||||
}
|
||||
|
||||
InsResolutionCoordinator_v1_0_1_B coord = this.coordinator;
|
||||
if (coord == null) {
|
||||
throw new IllegalStateException("INS coordinator not attached. Ensure installer was called.");
|
||||
}
|
||||
|
||||
// Requirement: trigger INS on EVERY request.
|
||||
coord.requestResolve(host);
|
||||
|
||||
// Block until server is connected
|
||||
awaitServerConnection();
|
||||
|
||||
ProtocolClient c = this.client;
|
||||
if (c == null) {
|
||||
throw new IllegalStateException("ProtocolClient not attached. Call OacWebRequestBroker.attachRuntime(...) during install.");
|
||||
}
|
||||
|
||||
String m = (method == null || method.isBlank()) ? "GET" : method.trim().toUpperCase(Locale.ROOT);
|
||||
|
||||
long requestId = requestCounter.getAndIncrement();
|
||||
int flags = WebPacketFlags.RESOURCE;
|
||||
|
||||
WebPacketHeader header = new WebPacketHeader(
|
||||
requestId,
|
||||
tabId,
|
||||
pageId,
|
||||
frameId,
|
||||
flags,
|
||||
System.currentTimeMillis()
|
||||
);
|
||||
|
||||
ResponseState st = new ResponseState(requestId);
|
||||
inFlight.put(requestId, st);
|
||||
|
||||
Map<String, String> safeHeaders = (headers == null) ? Map.of() : new LinkedHashMap<>(headers);
|
||||
byte[] safeBody = (body == null) ? new byte[0] : body;
|
||||
WebResourceRequestPacket packet = new WebResourceRequestPacket(
|
||||
header,
|
||||
url.toString(),
|
||||
m,
|
||||
safeHeaders,
|
||||
safeBody,
|
||||
safeHeaders.get("content-type"),
|
||||
null,
|
||||
null, bridge
|
||||
);
|
||||
|
||||
try {
|
||||
if (c.getClientServerConnection() == null || !c.getClientServerConnection().isConnected()) {
|
||||
inFlight.remove(requestId);
|
||||
throw new IOException("ServerConnection is not connected");
|
||||
}
|
||||
|
||||
c.getClientServerConnection().sendPacket(packet, TransportProtocol.TCP);
|
||||
} catch (Exception e) {
|
||||
inFlight.remove(requestId);
|
||||
throw new IOException("Failed to send WebResourceRequestPacket", e);
|
||||
}
|
||||
|
||||
return awaitAndBuildResponse(st);
|
||||
}
|
||||
|
||||
public String extractSession(Map<String, String> headers) {
|
||||
return headerValue(headers, "session");
|
||||
}
|
||||
|
||||
public void onResourceResponse(WebResourceResponsePacket p) {
|
||||
if (p == null || p.getHeader() == null) return;
|
||||
|
||||
ResponseState st = inFlight.get(p.getHeader().getRequestId());
|
||||
if (st == null) return;
|
||||
|
||||
synchronized (st.lock) {
|
||||
if (st.completed) return;
|
||||
|
||||
st.statusCode = p.getStatusCode();
|
||||
st.contentType = safeContentType(p.getContentType());
|
||||
st.headers = safeHeaders(p.getHeaders());
|
||||
|
||||
boolean stream = false;
|
||||
WebFlagInspector inspector = this.flagInspector;
|
||||
if (inspector != null) {
|
||||
stream = inspector.isStream(p.getHeader());
|
||||
}
|
||||
|
||||
if (!stream) {
|
||||
byte[] b = (p.getBody() == null) ? new byte[0] : p.getBody();
|
||||
st.memoryBody = b;
|
||||
st.success = true;
|
||||
st.completed = true;
|
||||
st.done.countDown();
|
||||
return;
|
||||
}
|
||||
|
||||
st.streamExpected = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void onStreamStart(WebStreamStartPacket_v1_0_1_B p) {
|
||||
if (p == null || p.getHeader() == null) return;
|
||||
|
||||
ResponseState st = inFlight.get(p.getHeader().getRequestId());
|
||||
if (st == null) return;
|
||||
|
||||
synchronized (st.lock) {
|
||||
if (st.completed) return;
|
||||
|
||||
st.statusCode = p.getStatusCode();
|
||||
st.contentType = safeContentType(p.getContentType());
|
||||
st.headers = safeHeaders(p.getHeaders());
|
||||
st.totalLength = p.getTotalLength();
|
||||
|
||||
st.streamExpected = true;
|
||||
|
||||
if (st.spooler == null) {
|
||||
try {
|
||||
st.spooler = TempChunkSpooler.create(st.requestId);
|
||||
st.spoolDir = st.spooler.dir();
|
||||
} catch (IOException e) {
|
||||
failLocked(st, "Failed to create stream spooler: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void onStreamChunk(WebStreamChunkPacket_v1_0_1_B p) {
|
||||
if (p == null || p.getHeader() == null) return;
|
||||
|
||||
ResponseState st = inFlight.get(p.getHeader().getRequestId());
|
||||
if (st == null) return;
|
||||
|
||||
int seq = p.getSeq();
|
||||
if (seq < 0) return;
|
||||
|
||||
byte[] data = (p.getData() == null) ? new byte[0] : p.getData();
|
||||
if (data.length == 0) return;
|
||||
|
||||
synchronized (st.lock) {
|
||||
if (st.completed) return;
|
||||
|
||||
st.streamExpected = true;
|
||||
|
||||
if (st.spooler == null) {
|
||||
try {
|
||||
st.spooler = TempChunkSpooler.create(st.requestId);
|
||||
st.spoolDir = st.spooler.dir();
|
||||
} catch (IOException e) {
|
||||
failLocked(st, "Failed to create stream spooler: " + e.getMessage());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
st.spooler.writeChunk(seq, data);
|
||||
if (seq > st.maxSeqSeen) st.maxSeqSeen = seq;
|
||||
} catch (IOException e) {
|
||||
failLocked(st, "Failed to spool stream chunk: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] onStreamEndAndAssemble(WebStreamEndPacket_v1_0_1_B p) {
|
||||
if (p == null || p.getHeader() == null) return null;
|
||||
|
||||
ResponseState st = inFlight.get(p.getHeader().getRequestId());
|
||||
if (st == null) return null;
|
||||
|
||||
synchronized (st.lock) {
|
||||
if (st.completed) return null;
|
||||
|
||||
st.success = p.isOk();
|
||||
st.errorMessage = p.getError();
|
||||
|
||||
if (!st.success) {
|
||||
st.completed = true;
|
||||
st.done.countDown();
|
||||
return null;
|
||||
}
|
||||
|
||||
if (st.spooler == null) {
|
||||
st.finalFile = null;
|
||||
st.completed = true;
|
||||
st.done.countDown();
|
||||
return new byte[0];
|
||||
}
|
||||
|
||||
st.spoolDir = st.spooler.dir();
|
||||
|
||||
try {
|
||||
st.finalFile = st.spooler.assembleBestEffort(st.maxSeqSeen);
|
||||
} catch (IOException e) {
|
||||
failLocked(st, "Failed to assemble stream: " + e.getMessage());
|
||||
return null;
|
||||
} finally {
|
||||
try {
|
||||
st.spooler.close();
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
st.completed = true;
|
||||
st.done.countDown();
|
||||
|
||||
if (st.finalFile == null) return new byte[0];
|
||||
|
||||
try (InputStream in = Files.newInputStream(st.finalFile)) {
|
||||
return in.readAllBytes();
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private OacWebResponse awaitAndBuildResponse(ResponseState st) throws IOException {
|
||||
try {
|
||||
if (!st.done.await(RESPONSE_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
|
||||
removeAndCleanup(st);
|
||||
throw new IOException("Timeout waiting for web response (requestId=" + st.requestId + ")");
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
removeAndCleanup(st);
|
||||
throw new IOException("Interrupted while waiting for web response (requestId=" + st.requestId + ")", e);
|
||||
}
|
||||
|
||||
inFlight.remove(st.requestId);
|
||||
|
||||
synchronized (st.lock) {
|
||||
if (!st.success) {
|
||||
cleanupLocked(st);
|
||||
String msg = (st.errorMessage == null || st.errorMessage.isBlank()) ? "Request failed" : st.errorMessage;
|
||||
throw new IOException(msg);
|
||||
}
|
||||
|
||||
if (!st.streamExpected && st.finalFile == null) {
|
||||
byte[] b = (st.memoryBody == null) ? new byte[0] : st.memoryBody;
|
||||
return new OacWebResponse(
|
||||
st.statusCode,
|
||||
safeContentType(st.contentType),
|
||||
OacWebResponse.safeHeaders(st.headers),
|
||||
new ByteArrayInputStream(b),
|
||||
b.length
|
||||
);
|
||||
}
|
||||
|
||||
if (st.finalFile == null) {
|
||||
return new OacWebResponse(
|
||||
st.statusCode,
|
||||
safeContentType(st.contentType),
|
||||
OacWebResponse.safeHeaders(st.headers),
|
||||
new ByteArrayInputStream(new byte[0]),
|
||||
0L
|
||||
);
|
||||
}
|
||||
|
||||
InputStream fileIn = Files.newInputStream(st.finalFile, StandardOpenOption.READ);
|
||||
return new OacWebResponse(
|
||||
st.statusCode,
|
||||
safeContentType(st.contentType),
|
||||
OacWebResponse.safeHeaders(st.headers),
|
||||
new DeleteOnCloseInputStream(fileIn, st.finalFile, st.spoolDir),
|
||||
safeFileSize(st.finalFile)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void removeAndCleanup(ResponseState st) {
|
||||
inFlight.remove(st.requestId);
|
||||
synchronized (st.lock) {
|
||||
cleanupLocked(st);
|
||||
}
|
||||
}
|
||||
|
||||
private static void cleanupLocked(ResponseState st) {
|
||||
if (st.spooler != null) {
|
||||
try {
|
||||
st.spooler.close();
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
st.spooler = null;
|
||||
}
|
||||
if (st.finalFile != null) {
|
||||
try {
|
||||
Files.deleteIfExists(st.finalFile);
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
st.finalFile = null;
|
||||
}
|
||||
if (st.spoolDir != null) {
|
||||
try {
|
||||
if (Files.exists(st.spoolDir)) {
|
||||
try (DirectoryStream<Path> ds = Files.newDirectoryStream(st.spoolDir)) {
|
||||
for (Path p : ds) {
|
||||
try {
|
||||
Files.deleteIfExists(p);
|
||||
} catch (IOException ignored2) {
|
||||
}
|
||||
}
|
||||
}
|
||||
try {
|
||||
Files.deleteIfExists(st.spoolDir);
|
||||
} catch (IOException ignored2) {
|
||||
}
|
||||
}
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
st.spoolDir = null;
|
||||
}
|
||||
}
|
||||
|
||||
private static void failLocked(ResponseState st, String message) {
|
||||
st.success = false;
|
||||
st.errorMessage = message;
|
||||
st.completed = true;
|
||||
st.done.countDown();
|
||||
}
|
||||
|
||||
private static long safeFileSize(Path p) {
|
||||
try {
|
||||
return Files.size(p);
|
||||
} catch (IOException e) {
|
||||
return -1L;
|
||||
}
|
||||
}
|
||||
|
||||
private static String safeContentType(String ct) {
|
||||
return (ct == null || ct.isBlank()) ? "application/octet-stream" : ct;
|
||||
}
|
||||
|
||||
private static Map<String, String> safeHeaders(Map<String, String> headers) {
|
||||
return (headers == null || headers.isEmpty()) ? Map.of() : Map.copyOf(headers);
|
||||
}
|
||||
|
||||
private static String headerValue(Map<String, String> headers, String nameLower) {
|
||||
if (headers == null || headers.isEmpty() || nameLower == null) return null;
|
||||
String needle = nameLower.trim().toLowerCase(Locale.ROOT);
|
||||
for (Map.Entry<String, String> e : headers.entrySet()) {
|
||||
if (e.getKey() == null) continue;
|
||||
if (e.getKey().trim().toLowerCase(Locale.ROOT).equals(needle)) {
|
||||
return e.getValue();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static final class ResponseState {
|
||||
private final long requestId;
|
||||
private final Object lock = new Object();
|
||||
private final CountDownLatch done = new CountDownLatch(1);
|
||||
|
||||
private int statusCode = 0;
|
||||
private String contentType = "application/octet-stream";
|
||||
private Map<String, String> headers = Map.of();
|
||||
|
||||
private boolean streamExpected;
|
||||
private long totalLength = -1L;
|
||||
|
||||
private int maxSeqSeen = -1;
|
||||
|
||||
private boolean completed;
|
||||
private boolean success;
|
||||
private String errorMessage;
|
||||
|
||||
private byte[] memoryBody;
|
||||
|
||||
private TempChunkSpooler spooler;
|
||||
private Path spoolDir;
|
||||
private Path finalFile;
|
||||
|
||||
private ResponseState(long requestId) {
|
||||
this.requestId = requestId;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class TempChunkSpooler implements Closeable {
|
||||
|
||||
private final Path dir;
|
||||
|
||||
private TempChunkSpooler(Path dir) {
|
||||
this.dir = dir;
|
||||
}
|
||||
|
||||
static TempChunkSpooler create(long requestId) throws IOException {
|
||||
Path dir = Files.createTempDirectory("oac-web-stream-" + requestId + "-");
|
||||
return new TempChunkSpooler(dir);
|
||||
}
|
||||
|
||||
void writeChunk(int seq, byte[] data) throws IOException {
|
||||
Path p = dir.resolve(seq + ".chunk");
|
||||
Files.write(p, data, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE);
|
||||
}
|
||||
|
||||
Path assembleBestEffort(int maxSeqSeen) throws IOException {
|
||||
Path out = Files.createTempFile("oac-web-stream-final-", ".bin");
|
||||
|
||||
try (OutputStream os = Files.newOutputStream(out, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE)) {
|
||||
for (int seq = 0; seq <= maxSeqSeen; seq++) {
|
||||
Path p = dir.resolve(seq + ".chunk");
|
||||
if (!Files.exists(p)) continue;
|
||||
try (InputStream in = Files.newInputStream(p, StandardOpenOption.READ)) {
|
||||
in.transferTo(os);
|
||||
}
|
||||
Files.deleteIfExists(p);
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
Path dir() {
|
||||
return dir;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
if (!Files.exists(dir)) return;
|
||||
|
||||
try (DirectoryStream<Path> ds = Files.newDirectoryStream(dir)) {
|
||||
for (Path p : ds) {
|
||||
try {
|
||||
Files.deleteIfExists(p);
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
try {
|
||||
Files.deleteIfExists(dir);
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package org.openautonomousconnection.protocol.urlhandler.v1_0_1.beta.web;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Represents a resolved web response for the JavaFX WebView.
|
||||
*
|
||||
* @param statusCode HTTP-like status code
|
||||
* @param contentType response content-type
|
||||
* @param headers response headers
|
||||
* @param bodyStream body stream
|
||||
* @param contentLength content length if known, else -1
|
||||
*/
|
||||
public record OacWebResponse(
|
||||
int statusCode,
|
||||
String contentType,
|
||||
Map<String, String> headers,
|
||||
InputStream bodyStream,
|
||||
long contentLength
|
||||
) {
|
||||
public OacWebResponse {
|
||||
Objects.requireNonNull(headers, "headers");
|
||||
Objects.requireNonNull(bodyStream, "bodyStream");
|
||||
}
|
||||
|
||||
public static Map<String, String> safeHeaders(Map<String, String> h) {
|
||||
return (h == null) ? Collections.emptyMap() : Collections.unmodifiableMap(h);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package org.openautonomousconnection.protocol.urlhandler.v1_0_1.beta.web;
|
||||
|
||||
import org.openautonomousconnection.protocol.versions.v1_0_1.beta.WebPacketFlags;
|
||||
import org.openautonomousconnection.protocol.versions.v1_0_1.beta.WebPacketHeader;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Interprets {@link WebPacketHeader#getFlags()} into semantic booleans.
|
||||
*/
|
||||
public interface WebFlagInspector {
|
||||
|
||||
/**
|
||||
* @param header web packet header
|
||||
* @return true if the packet is part of a streamed body transfer
|
||||
*/
|
||||
boolean isStream(WebPacketHeader header);
|
||||
|
||||
/**
|
||||
* Default implementation based on {@link WebPacketFlags#STREAM}.
|
||||
*/
|
||||
final class Default implements WebFlagInspector {
|
||||
|
||||
@Override
|
||||
public boolean isStream(WebPacketHeader header) {
|
||||
Objects.requireNonNull(header, "header");
|
||||
return WebPacketFlags.has(header.getFlags(), WebPacketFlags.STREAM);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package org.openautonomousconnection.protocol.urlhandler.v1_0_1.beta.web;
|
||||
|
||||
import java.net.URL;
|
||||
|
||||
/**
|
||||
* Provides per-request WEB correlation context (tab/page/frame).
|
||||
*/
|
||||
public interface WebRequestContextProvider {
|
||||
|
||||
/**
|
||||
* Returns the context to use for this URL request.
|
||||
*
|
||||
* @param url requested URL
|
||||
* @return context (never null)
|
||||
*/
|
||||
WebRequestContext contextFor(URL url);
|
||||
|
||||
/**
|
||||
* Immutable context DTO.
|
||||
*
|
||||
* @param tabId stable tab id
|
||||
* @param pageId navigation instance id
|
||||
* @param frameId frame id (0 = main frame)
|
||||
*/
|
||||
record WebRequestContext(long tabId, long pageId, long frameId) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Default context provider (tab/page/frame = 0).
|
||||
*/
|
||||
final class Default implements WebRequestContextProvider {
|
||||
@Override
|
||||
public WebRequestContext contextFor(URL url) {
|
||||
return new WebRequestContext(0L, 0L, 0L);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,14 +12,20 @@ import java.util.List;
|
||||
*/
|
||||
public enum ProtocolVersion implements Serializable {
|
||||
/**
|
||||
* Support for old OAC-Project => <a href="https://repo.open-autonomous-connection.org/Open-Autonomous-Connection/">*_old</a>
|
||||
* For classic OAC-Project => <a href="https://repo.open-autonomous-connection.org/Open-Autonomous-Connection/">"classic"-branch</a>
|
||||
*/
|
||||
@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));
|
||||
@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),
|
||||
;
|
||||
|
||||
/**
|
||||
* The version string of the protocol version.
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
package org.openautonomousconnection.protocol.versions.v1_0_0.beta;
|
||||
|
||||
import org.openautonomousconnection.protocol.side.ins.ProtocolINSServer;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Utility methods for working with {@link INSRecord} instances.
|
||||
* <p>
|
||||
* Features:
|
||||
* <ul>
|
||||
* <li>Recursive CNAME resolution with loop protection</li>
|
||||
* <li>Basic record validation helpers</li>
|
||||
* </ul>
|
||||
*/
|
||||
public final class INSRecordTools {
|
||||
|
||||
/**
|
||||
* Maximum number of CNAME hops before resolution is aborted.
|
||||
*/
|
||||
public static final int MAX_CNAME_DEPTH = 10;
|
||||
|
||||
/**
|
||||
* Resolves records for the given name and type, following CNAME chains if necessary.
|
||||
* <p>
|
||||
* Resolution strategy:
|
||||
* <ol>
|
||||
* <li>Try to resolve the requested {@code type} directly via {@link ProtocolINSServer#resolve}</li>
|
||||
* <li>If no records are found:
|
||||
* <ul>
|
||||
* <li>Resolve {@link INSRecordType#CNAME} for the same (tln, name, sub)</li>
|
||||
* <li>For each CNAME target, follow the chain up to {@link #MAX_CNAME_DEPTH}</li>
|
||||
* </ul>
|
||||
* </li>
|
||||
* <li>Return the first non-empty resolution result</li>
|
||||
* </ol>
|
||||
*
|
||||
* @param server The INS server implementation to use for lookups.
|
||||
* @param tln Top-level-name.
|
||||
* @param name InfoName.
|
||||
* @param sub Optional sub-name, may be {@code null}.
|
||||
* @param type Desired record type.
|
||||
* @return A list of resolved records. May be empty if nothing could be resolved.
|
||||
*/
|
||||
public static List<INSRecord> resolveWithCNAME(ProtocolINSServer server,
|
||||
String tln,
|
||||
String name,
|
||||
String sub,
|
||||
INSRecordType type) {
|
||||
|
||||
// If we explicitly ask for CNAME, just return raw CNAME records
|
||||
if (type == INSRecordType.CNAME) return server.resolve(tln, name, sub, INSRecordType.CNAME);
|
||||
|
||||
// Try direct resolution first
|
||||
List<INSRecord> direct = server.resolve(tln, name, sub, type);
|
||||
if (!direct.isEmpty()) return direct;
|
||||
|
||||
// Otherwise follow CNAME chain
|
||||
Set<String> visited = new HashSet<>();
|
||||
return followCNAME(server, tln, name, sub, type, 0, visited);
|
||||
}
|
||||
|
||||
private static List<INSRecord> followCNAME(ProtocolINSServer server, String tln, String name, String sub, INSRecordType targetType, int depth, Set<String> visited) {
|
||||
|
||||
if (depth > MAX_CNAME_DEPTH) {
|
||||
server.getProtocolBridge().getProtocolValues().logger.warn("Max CNAME depth exceeded for " + fqdn(tln, name, sub));
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
String key = fqdn(tln, name, sub);
|
||||
if (!visited.add(key)) {
|
||||
// Loop detected
|
||||
server.getProtocolBridge().getProtocolValues().logger.warn("CNAME loop detected for " + key);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
List<INSRecord> cnames = server.resolve(tln, name, sub, INSRecordType.CNAME);
|
||||
if (cnames.isEmpty()) return Collections.emptyList();
|
||||
|
||||
for (INSRecord cname : cnames) {
|
||||
if (!isValidRecord(cname)) continue;
|
||||
|
||||
// Target might be "host.tln" or full "sub.name.tln"
|
||||
ParsedName target = parseTargetName(cname.value);
|
||||
if (target == null) continue;
|
||||
|
||||
List<INSRecord> resolved = server.resolve(target.tln, target.name, target.sub, targetType);
|
||||
|
||||
if (!resolved.isEmpty()) return resolved;
|
||||
|
||||
// Try next CNAME hop
|
||||
List<INSRecord> deeper = followCNAME(server, target.tln, target.name, target.sub, targetType, depth + 1, visited);
|
||||
|
||||
if (!deeper.isEmpty()) return deeper;
|
||||
}
|
||||
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
private static String fqdn(String tln, String name, String sub) {
|
||||
if (sub == null || sub.isEmpty()) return name + "." + tln;
|
||||
return sub + "." + name + "." + tln;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a CNAME target into (tln, name, sub) assuming the last label is the TLN
|
||||
* and the one before that is the InfoName.
|
||||
*
|
||||
* @param target FQDN string.
|
||||
* @return ParsedName or {@code null} if it cannot be parsed.
|
||||
*/
|
||||
private static ParsedName parseTargetName(String target) {
|
||||
if (target == null) return null;
|
||||
|
||||
String hostname = target.trim();
|
||||
if (hostname.endsWith(".")) hostname = hostname.substring(0, hostname.length() - 1);
|
||||
|
||||
String[] parts = hostname.split("\\.");
|
||||
if (parts.length < 2) return null;
|
||||
|
||||
String tln = parts[parts.length - 1];
|
||||
String name = parts[parts.length - 2];
|
||||
String sub = null;
|
||||
|
||||
if (parts.length > 2) sub = String.join(".", Arrays.copyOfRange(parts, 0, parts.length - 2));
|
||||
return new ParsedName(tln, name, sub);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs basic validation on a single {@link INSRecord}.
|
||||
* <p>
|
||||
* Checks:
|
||||
* <ul>
|
||||
* <li>type is non-null</li>
|
||||
* <li>value is non-null and non-empty</li>
|
||||
* <li>port is in range 0–65535</li>
|
||||
* <li>ttl is non-negative</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param record The record to validate.
|
||||
* @return {@code true} if the record passes all checks, {@code false} otherwise.
|
||||
*/
|
||||
public static boolean isValidRecord(INSRecord record) {
|
||||
if (record == null) return false;
|
||||
if (record.type == null) return false;
|
||||
if (record.value == null || record.value.isEmpty()) return false;
|
||||
|
||||
if (record.port < 0 || record.port > 65535) return false;
|
||||
return record.ttl >= 0;
|
||||
}
|
||||
|
||||
// Validation helpers
|
||||
|
||||
/**
|
||||
* Validates a collection of records by applying {@link #isValidRecord(INSRecord)}
|
||||
* to each element.
|
||||
*
|
||||
* @param records Records to validate.
|
||||
* @return A new list containing only valid records.
|
||||
*/
|
||||
public static List<INSRecord> filterValid(List<INSRecord> records) {
|
||||
if (records == null || records.isEmpty()) return Collections.emptyList();
|
||||
List<INSRecord> out = new ArrayList<>(records.size());
|
||||
for (INSRecord r : records) {
|
||||
if (isValidRecord(r)) out.add(r);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private record ParsedName(String tln, String name, String sub) {
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package org.openautonomousconnection.protocol.versions.v1_0_0.beta;
|
||||
|
||||
public enum WebRequestMethod {
|
||||
GET, POST, EXECUTE, SCRIPT
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
package org.openautonomousconnection.protocol.versions.v1_0_0.classic.utils;
|
||||
package org.openautonomousconnection.protocol.versions.v1_0_0.classic;
|
||||
|
||||
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;
|
||||
@@ -1,51 +0,0 @@
|
||||
package org.openautonomousconnection.protocol.versions.v1_0_0.classic.events;
|
||||
|
||||
import dev.unlegitdqrk.unlegitlibrary.event.impl.Event;
|
||||
import org.openautonomousconnection.protocol.versions.v1_0_0.classic.handlers.ClassicHandlerINSServer;
|
||||
import org.openautonomousconnection.protocol.versions.v1_0_0.classic.objects.Classic_Domain;
|
||||
import org.openautonomousconnection.protocol.versions.v1_0_0.classic.objects.Classic_RequestDomain;
|
||||
import org.openautonomousconnection.protocol.versions.v1_0_0.classic.utils.Classic_ProtocolVersion;
|
||||
|
||||
/**
|
||||
* This event is fired when a classic domain packet is received.
|
||||
* This event is deprecated and will be marked for removal in future versions.
|
||||
*
|
||||
* @see ClassicHandlerINSServer
|
||||
* @see org.openautonomousconnection.protocol.versions.v1_0_0.classic.handlers.ClassicHandlerClient
|
||||
* @see org.openautonomousconnection.protocol.versions.v1_0_0.classic.handlers.ClassicHandlerWebServer
|
||||
*/
|
||||
@Deprecated(forRemoval = false, since = "1.0.0-BETA.3")
|
||||
public final class Classic_DomainPacketReceivedEvent extends Event {
|
||||
|
||||
public final Classic_ProtocolVersion protocolVersion;
|
||||
public final Classic_Domain domain;
|
||||
public final Classic_RequestDomain requestDomain;
|
||||
public final int clientID;
|
||||
|
||||
public Classic_DomainPacketReceivedEvent(Classic_ProtocolVersion protocolVersion, Classic_Domain domain, Classic_RequestDomain requestDomain, int clientID) {
|
||||
this.protocolVersion = protocolVersion;
|
||||
this.domain = domain;
|
||||
this.requestDomain = requestDomain;
|
||||
this.clientID = clientID;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object clone() throws CloneNotSupportedException {
|
||||
return super.clone();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
return super.equals(obj);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return super.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return super.hashCode();
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
package org.openautonomousconnection.protocol.versions.v1_0_0.classic.events;
|
||||
|
||||
import dev.unlegitdqrk.unlegitlibrary.event.impl.Event;
|
||||
import org.openautonomousconnection.protocol.versions.v1_0_0.classic.handlers.ClassicHandlerINSServer;
|
||||
import org.openautonomousconnection.protocol.versions.v1_0_0.classic.objects.Classic_Domain;
|
||||
import org.openautonomousconnection.protocol.versions.v1_0_0.classic.objects.Classic_RequestDomain;
|
||||
import org.openautonomousconnection.protocol.versions.v1_0_0.classic.utils.Classic_ProtocolVersion;
|
||||
|
||||
/**
|
||||
* This event is fired when a classic ping packet is received.
|
||||
* This event is deprecated and will be marked for removal in future versions.
|
||||
*
|
||||
* @see ClassicHandlerINSServer
|
||||
* @see org.openautonomousconnection.protocol.versions.v1_0_0.classic.handlers.ClassicHandlerClient
|
||||
* @see org.openautonomousconnection.protocol.versions.v1_0_0.classic.handlers.ClassicHandlerWebServer
|
||||
*/
|
||||
@Deprecated(forRemoval = false, since = "1.0.0-BETA.3")
|
||||
public final class Classic_PingPacketReceivedEvent extends Event {
|
||||
public final Classic_ProtocolVersion protocolVersion;
|
||||
public final Classic_Domain domain;
|
||||
public final Classic_RequestDomain requestDomain;
|
||||
public final boolean reachable;
|
||||
public final int clientID;
|
||||
|
||||
public Classic_PingPacketReceivedEvent(Classic_ProtocolVersion protocolVersion, Classic_Domain domain, Classic_RequestDomain requestDomain, boolean reachable, int clientID) {
|
||||
this.protocolVersion = protocolVersion;
|
||||
this.domain = domain;
|
||||
this.requestDomain = requestDomain;
|
||||
this.reachable = reachable;
|
||||
this.clientID = clientID;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object clone() throws CloneNotSupportedException {
|
||||
return super.clone();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
return super.equals(obj);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return super.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return super.hashCode();
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
package org.openautonomousconnection.protocol.versions.v1_0_0.classic.handlers;
|
||||
|
||||
import lombok.Getter;
|
||||
import org.openautonomousconnection.protocol.annotations.ProtocolInfo;
|
||||
import org.openautonomousconnection.protocol.packets.v1_0_0.classic.Classic_MessagePacket;
|
||||
import org.openautonomousconnection.protocol.side.client.ProtocolClient;
|
||||
import org.openautonomousconnection.protocol.versions.ProtocolVersion;
|
||||
import org.openautonomousconnection.protocol.versions.v1_0_0.beta.INSResponseStatus;
|
||||
import org.openautonomousconnection.protocol.versions.v1_0_0.classic.objects.Classic_Domain;
|
||||
import org.openautonomousconnection.protocol.versions.v1_0_0.classic.site.Classic_SiteType;
|
||||
import org.openautonomousconnection.protocol.versions.v1_0_0.classic.utils.Classic_ProtocolVersion;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Abstract class defining the client-side handler for Classic protocol operations.
|
||||
*/
|
||||
@ProtocolInfo(protocolSide = ProtocolVersion.ProtocolSide.CLIENT)
|
||||
public abstract class ClassicHandlerClient {
|
||||
|
||||
/**
|
||||
* Reference to the ProtocolClient
|
||||
*/
|
||||
@Getter
|
||||
private final ProtocolClient client;
|
||||
|
||||
/**
|
||||
* Sets the client variable
|
||||
*
|
||||
* @param client The ProtocolClient Object
|
||||
*/
|
||||
public ClassicHandlerClient(ProtocolClient client) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
public abstract void unsupportedClassicPacket(String classicPacketClassName, Object[] content);
|
||||
|
||||
public abstract void handleHTMLContent(Classic_SiteType siteType, Classic_Domain domain, String html);
|
||||
|
||||
public abstract void handleMessage(String message, Classic_ProtocolVersion protocolVersion);
|
||||
|
||||
public final void sendMessage(String message) throws IOException, ClassNotFoundException {
|
||||
client.getClientINSConnection().sendPacket(new Classic_MessagePacket(message, 0, client.getProtocolBridge()));
|
||||
}
|
||||
|
||||
public abstract void validationCompleted(Classic_Domain domain, INSResponseStatus insResponseStatus);
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package org.openautonomousconnection.protocol.versions.v1_0_0.classic.handlers;
|
||||
|
||||
import org.openautonomousconnection.protocol.annotations.ProtocolInfo;
|
||||
import org.openautonomousconnection.protocol.side.ins.ConnectedProtocolClient;
|
||||
import org.openautonomousconnection.protocol.versions.ProtocolVersion;
|
||||
import org.openautonomousconnection.protocol.versions.v1_0_0.classic.objects.Classic_Domain;
|
||||
import org.openautonomousconnection.protocol.versions.v1_0_0.classic.objects.Classic_RequestDomain;
|
||||
import org.openautonomousconnection.protocol.versions.v1_0_0.classic.utils.Classic_ProtocolVersion;
|
||||
|
||||
import java.sql.SQLException;
|
||||
|
||||
/**
|
||||
* Abstract class defining the INS server-side handler for Classic protocol operations.
|
||||
*/
|
||||
@ProtocolInfo(protocolSide = ProtocolVersion.ProtocolSide.INS)
|
||||
public abstract class ClassicHandlerINSServer {
|
||||
public abstract void handleMessage(ConnectedProtocolClient client, String message, Classic_ProtocolVersion protocolVersion);
|
||||
|
||||
public abstract Classic_Domain getDomain(Classic_RequestDomain requestDomain) throws SQLException;
|
||||
|
||||
public abstract Classic_Domain ping(Classic_RequestDomain requestDomain) throws SQLException;
|
||||
|
||||
public abstract void unsupportedClassicPacket(String className, Object[] content, ConnectedProtocolClient client);
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package org.openautonomousconnection.protocol.versions.v1_0_0.classic.handlers;
|
||||
|
||||
import org.openautonomousconnection.protocol.annotations.ProtocolInfo;
|
||||
import org.openautonomousconnection.protocol.side.ins.ConnectedProtocolClient;
|
||||
import org.openautonomousconnection.protocol.versions.ProtocolVersion;
|
||||
import org.openautonomousconnection.protocol.versions.v1_0_0.classic.utils.Classic_ProtocolVersion;
|
||||
|
||||
/**
|
||||
* Abstract class defining the web server-side handler for Classic protocol operations.
|
||||
*/
|
||||
@ProtocolInfo(protocolSide = ProtocolVersion.ProtocolSide.WEB)
|
||||
public abstract class ClassicHandlerWebServer {
|
||||
public abstract void handleMessage(ConnectedProtocolClient client, String message, Classic_ProtocolVersion protocolVersion);
|
||||
|
||||
public abstract void unsupportedClassicPacket(String className, Object[] content, ConnectedProtocolClient client);
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
package org.openautonomousconnection.protocol.versions.v1_0_0.classic.objects;
|
||||
|
||||
import lombok.Getter;
|
||||
import org.openautonomousconnection.protocol.ProtocolBridge;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* Classic_Domain is an old representation of a InfoName, maintained for backward compatibility.
|
||||
* It encapsulates the InfoName's name, top-level name, path, and destination.
|
||||
* This class is deprecated and users are encouraged to use the InfoName class instead.
|
||||
*/
|
||||
@Deprecated(forRemoval = false, since = "1.0.0-BETA.3")
|
||||
public class Classic_Domain implements Serializable {
|
||||
|
||||
/**
|
||||
* The name of the domain.
|
||||
*/
|
||||
@Deprecated(forRemoval = false, since = "1.0.0-BETA.3")
|
||||
public final String name;
|
||||
|
||||
/**
|
||||
* The top-level domain.
|
||||
*/
|
||||
@Deprecated(forRemoval = false, since = "1.0.0-BETA.3")
|
||||
public final String topLevelDomain;
|
||||
|
||||
|
||||
/**
|
||||
* The path component of the domain.
|
||||
*/
|
||||
@Deprecated(forRemoval = false, since = "1.0.0-BETA.3")
|
||||
public final String path;
|
||||
|
||||
/**
|
||||
* The destination of the domain, typically the full URL or address.
|
||||
*/
|
||||
@Deprecated(forRemoval = false, since = "1.0.0-BETA.3")
|
||||
public final String destination;
|
||||
|
||||
/**
|
||||
* The ProtocolBridge reference.
|
||||
*/
|
||||
@Deprecated(forRemoval = false, since = "1.0.0-BETA.3")
|
||||
public final ProtocolBridge protocolBridge;
|
||||
|
||||
public Classic_Domain(String name, String topLevelDomain, String destination, String path, ProtocolBridge bridge) {
|
||||
this.protocolBridge = bridge;
|
||||
this.name = name;
|
||||
this.topLevelDomain = topLevelDomain;
|
||||
this.destination = destination;
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected final Object clone() throws CloneNotSupportedException {
|
||||
return new Classic_Domain(name, topLevelDomain, destination, path, protocolBridge);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final boolean equals(Object obj) {
|
||||
if (!(obj instanceof Classic_Domain other)) return false;
|
||||
return other.name.equalsIgnoreCase(name) && other.topLevelDomain.equalsIgnoreCase(topLevelDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final int hashCode() {
|
||||
return super.hashCode();
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package org.openautonomousconnection.protocol.versions.v1_0_0.classic.objects;
|
||||
|
||||
import org.openautonomousconnection.protocol.ProtocolBridge;
|
||||
|
||||
/**
|
||||
* Class representing a local domain in the Classic protocol.
|
||||
* This class extends Classic_Domain and is used for local domain representation.
|
||||
*/
|
||||
@Deprecated(forRemoval = false, since = "1.0.0-BETA.3")
|
||||
public final class Classic_LocalDomain extends Classic_Domain {
|
||||
public Classic_LocalDomain(String name, String endName, String path, ProtocolBridge protocolBridge) {
|
||||
super(name, endName, null, path, protocolBridge);
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
package org.openautonomousconnection.protocol.versions.v1_0_0.classic.objects;
|
||||
|
||||
import org.openautonomousconnection.protocol.ProtocolBridge;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* Class representing a request for a domain in the Classic protocol.
|
||||
* This class extends Classic_Domain and is used for requesting domain information.
|
||||
*/
|
||||
@Deprecated(forRemoval = false, since = "1.0.0-BETA.3")
|
||||
public final class Classic_RequestDomain extends Classic_Domain implements Serializable {
|
||||
|
||||
public Classic_RequestDomain(String name, String topLevelDomain, String path, ProtocolBridge protocolBridge) {
|
||||
super(name, topLevelDomain, null, path, protocolBridge);
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
package org.openautonomousconnection.protocol.versions.v1_0_0.classic.site;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* Enum representing different types of sites in the Classic protocol.
|
||||
*/
|
||||
@Deprecated(forRemoval = false, since = "1.0.0-BETA.3")
|
||||
public enum Classic_SiteType implements Serializable {
|
||||
/**
|
||||
* Client site type.
|
||||
*/
|
||||
CLIENT("oac-client"),
|
||||
|
||||
/**
|
||||
* Web server site type.
|
||||
*/
|
||||
SERVER("oac-server"),
|
||||
|
||||
/**
|
||||
* INS server site type.
|
||||
*/
|
||||
PUBLIC("oac"),
|
||||
|
||||
/**
|
||||
* Protocol site type.
|
||||
*/
|
||||
PROTOCOL("oac-protocol"),
|
||||
|
||||
/**
|
||||
* Local site type.
|
||||
*/
|
||||
LOCAL("oac-local");
|
||||
|
||||
/**
|
||||
* The name of the site type.
|
||||
*/
|
||||
@Deprecated(forRemoval = false, since = "1.0.0-BETA.3")
|
||||
public final String name;
|
||||
|
||||
Classic_SiteType(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
package org.openautonomousconnection.protocol.versions.v1_0_0.classic.site;
|
||||
|
||||
import dev.unlegitdqrk.unlegitlibrary.utils.DefaultMethodsOverrider;
|
||||
|
||||
/**
|
||||
* This class contains predefined HTML content for various website responses in the Classic protocol.
|
||||
*/
|
||||
@Deprecated(forRemoval = false, since = "1.0.0-BETA.3")
|
||||
public final class Classic_WebsitesContent extends DefaultMethodsOverrider {
|
||||
|
||||
public static final String DOMAIN_NOT_FOUND = """
|
||||
<html>
|
||||
<head>
|
||||
<title>404 - Domain not found</title>
|
||||
<meta content="UTF-8" name="charset"/>
|
||||
<meta content="Open Autonomous Connection" name="author"/>
|
||||
<meta content="Domain not found" name="description"/>
|
||||
</head>
|
||||
<body>
|
||||
<h1>404 - This infoName was not found</h1>
|
||||
</body>
|
||||
</html>
|
||||
""";
|
||||
|
||||
public static final String FILE_NOT_FOUND = """
|
||||
<html>
|
||||
<head>
|
||||
<title>404 - File not found</title>
|
||||
<meta content="UTF-8" name="charset"/>
|
||||
<meta content="Open Autonomous Connection" name="author"/>
|
||||
<meta content="File not found" name="description"/>
|
||||
</head>
|
||||
<body>
|
||||
<h1>404 - This file was not found</h1>
|
||||
</body>
|
||||
</html>
|
||||
""";
|
||||
|
||||
public static final String DOMAIN_NOT_REACHABLE = """
|
||||
<html>
|
||||
<head>
|
||||
<title>504 - Site not reachable</title>
|
||||
<meta content="UTF-8" name="charset"/>
|
||||
<meta content="Open Autonomous Connection" name="author"/>
|
||||
<meta content="Site not reached" name="description"/>
|
||||
</head>
|
||||
<body>
|
||||
<h1>504 - This site is currently not reachable</h1>
|
||||
</body>
|
||||
</html>
|
||||
""";
|
||||
public static String ERROR_OCCURRED = ERROR_OCCURRED("No specified details!");
|
||||
|
||||
public static String ERROR_OCCURRED(String errorDetails) {
|
||||
return """
|
||||
<html>
|
||||
<head>
|
||||
<title>500 - Error occurred</title>
|
||||
<meta content="UTF-8" name="charset"/>
|
||||
<meta content="Open Autonomous Connection" name="author"/>
|
||||
<meta content="Site not reached" name="description"/>
|
||||
</head>
|
||||
<body>
|
||||
<h1>500 - Error occured while resolving infoName!</h1>
|
||||
<h4>Details:</h2>
|
||||
<h5>""" + errorDetails + "</h5>" + """
|
||||
</body>
|
||||
</html>
|
||||
""";
|
||||
}
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
package org.openautonomousconnection.protocol.versions.v1_0_0.classic.utils;
|
||||
|
||||
import dev.unlegitdqrk.unlegitlibrary.event.EventListener;
|
||||
import dev.unlegitdqrk.unlegitlibrary.event.Listener;
|
||||
import lombok.Getter;
|
||||
import org.openautonomousconnection.protocol.ProtocolBridge;
|
||||
import org.openautonomousconnection.protocol.packets.v1_0_0.classic.Classic_PingPacket;
|
||||
import org.openautonomousconnection.protocol.versions.v1_0_0.classic.events.Classic_DomainPacketReceivedEvent;
|
||||
import org.openautonomousconnection.protocol.versions.v1_0_0.classic.events.Classic_PingPacketReceivedEvent;
|
||||
import org.openautonomousconnection.protocol.versions.v1_0_0.classic.objects.Classic_LocalDomain;
|
||||
import org.openautonomousconnection.protocol.versions.v1_0_0.classic.site.Classic_SiteType;
|
||||
import org.openautonomousconnection.protocol.versions.v1_0_0.classic.site.Classic_WebsitesContent;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
|
||||
/**
|
||||
* This class listens for events related to Classic protocol operations on the client side.
|
||||
* It handles domain resolution and ping responses, facilitating communication with the INS server
|
||||
* and web content retrieval.
|
||||
*/
|
||||
@Deprecated(forRemoval = false, since = "1.0.0-BETA.3")
|
||||
public final class Classic_ClientListener extends EventListener {
|
||||
|
||||
/**
|
||||
* Reference to the ProtocolBridge
|
||||
*/
|
||||
@Getter
|
||||
private ProtocolBridge protocolBridge;
|
||||
|
||||
/**
|
||||
* Set protocol bridge
|
||||
*
|
||||
* @param protocolBridge The ProtocolBridge object
|
||||
*/
|
||||
public void setProtocolBridge(ProtocolBridge protocolBridge) {
|
||||
if (this.protocolBridge != null) return;
|
||||
this.protocolBridge = protocolBridge;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the event when a domain packet is received.
|
||||
* It checks if the domain exists and sends a ping request to the INS server.
|
||||
* If the domain does not exist, it handles the error accordingly.
|
||||
*
|
||||
* @param event The event containing domain information.
|
||||
*/
|
||||
@Listener
|
||||
public void onDomain(Classic_DomainPacketReceivedEvent event) {
|
||||
// Check if the domain exists
|
||||
boolean exists = event.domain != null;
|
||||
|
||||
if (exists) {
|
||||
try {
|
||||
// Send a ping request to the INS server
|
||||
if (!protocolBridge.getProtocolClient().getClientINSConnection().sendPacket(new Classic_PingPacket(event.requestDomain, event.domain, false, protocolBridge))) {
|
||||
// If sending the packet fails, handle the error
|
||||
protocolBridge.getClassicHandlerClient().handleHTMLContent(Classic_SiteType.PROTOCOL, new Classic_LocalDomain("error-occurred", "html", "", protocolBridge),
|
||||
Classic_WebsitesContent.ERROR_OCCURRED(event.domain + "/" + event.domain.path));
|
||||
}
|
||||
} catch (IOException | ClassNotFoundException e) {
|
||||
// Handle any exceptions that occur during the process
|
||||
protocolBridge.getClassicHandlerClient().handleHTMLContent(Classic_SiteType.PROTOCOL, new Classic_LocalDomain("error-occurred", "html", "", protocolBridge),
|
||||
Classic_WebsitesContent.ERROR_OCCURRED(event.domain + "/" + event.domain.path + ":\n" + e.getMessage()));
|
||||
}
|
||||
} else
|
||||
// If the domain does not exist, handle the error
|
||||
protocolBridge.getClassicHandlerClient().handleHTMLContent(Classic_SiteType.PROTOCOL, new Classic_LocalDomain("domain-not-found", "html", "", protocolBridge), Classic_WebsitesContent.DOMAIN_NOT_FOUND);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the event when a ping packet is received.
|
||||
* If the domain is reachable, it fetches the HTML content from the domain.
|
||||
* If not reachable, it handles the error accordingly.
|
||||
*
|
||||
* @param event The event containing ping response information.
|
||||
*/
|
||||
@Listener
|
||||
public void onPing(Classic_PingPacketReceivedEvent event) {
|
||||
// If the domain is reachable, fetch the HTML content
|
||||
if (event.reachable) {
|
||||
String destination = event.domain.destination;
|
||||
|
||||
try {
|
||||
// Create a URL object
|
||||
URL url = new URL(destination);
|
||||
HttpURLConnection connection2 = (HttpURLConnection) url.openConnection();
|
||||
connection2.setRequestMethod("GET");
|
||||
|
||||
// Read the response
|
||||
StringBuilder content = new StringBuilder();
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection2.getInputStream()))) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) content.append(line);
|
||||
}
|
||||
|
||||
// Handle the HTML content
|
||||
protocolBridge.getClassicHandlerClient().handleHTMLContent(Classic_SiteType.PUBLIC, event.domain, content.toString());
|
||||
} catch (IOException exception) {
|
||||
// Handle any exceptions that occur during the process
|
||||
protocolBridge.getClassicHandlerClient().handleHTMLContent(Classic_SiteType.PROTOCOL, new Classic_LocalDomain("error-occurred", "html", "", protocolBridge),
|
||||
Classic_WebsitesContent.ERROR_OCCURRED(exception.getMessage().replace(event.domain.destination, event.domain + "/" + event.domain.path)));
|
||||
}
|
||||
} else
|
||||
// If the domain is not reachable, handle the error
|
||||
protocolBridge.getClassicHandlerClient().handleHTMLContent(Classic_SiteType.PROTOCOL, new Classic_LocalDomain("error-not-reached", "html", "", protocolBridge), Classic_WebsitesContent.DOMAIN_NOT_REACHABLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object clone() throws CloneNotSupportedException {
|
||||
return super.clone();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
return super.equals(obj);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return super.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return super.hashCode();
|
||||
}
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
package org.openautonomousconnection.protocol.versions.v1_0_0.classic.utils;
|
||||
|
||||
import dev.unlegitdqrk.unlegitlibrary.utils.DefaultMethodsOverrider;
|
||||
import org.openautonomousconnection.protocol.versions.v1_0_0.classic.site.Classic_SiteType;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
|
||||
/**
|
||||
* Utility class for domain-related operations in the Classic protocol.
|
||||
*/
|
||||
@Deprecated(forRemoval = false, since = "1.0.0-BETA.3")
|
||||
class Classic_DomainUtils extends DefaultMethodsOverrider {
|
||||
|
||||
/**
|
||||
* Extracts the top-level domain (TLD) from a given URL.
|
||||
*
|
||||
* @param url The URL from which to extract the TLD.
|
||||
* @return The top-level domain as a string.
|
||||
* @throws MalformedURLException If the URL is malformed.
|
||||
*/
|
||||
public static String getTopLevelDomain(String url) throws MalformedURLException {
|
||||
URL uri = null;
|
||||
String tldString = null;
|
||||
|
||||
if (url.startsWith(Classic_SiteType.PUBLIC.name + "://"))
|
||||
url = url.substring((Classic_SiteType.PUBLIC.name + "://").length());
|
||||
if (url.startsWith(Classic_SiteType.CLIENT.name + "://"))
|
||||
url = url.substring((Classic_SiteType.CLIENT.name + "://").length());
|
||||
if (url.startsWith(Classic_SiteType.SERVER.name + "://"))
|
||||
url = url.substring((Classic_SiteType.SERVER.name + "://").length());
|
||||
if (url.startsWith(Classic_SiteType.PROTOCOL.name + "://"))
|
||||
url = url.substring((Classic_SiteType.PROTOCOL.name + "://").length());
|
||||
if (url.startsWith(Classic_SiteType.LOCAL.name + "://"))
|
||||
url = url.substring((Classic_SiteType.LOCAL.name + "://").length());
|
||||
|
||||
if (!url.startsWith("https://") && !url.startsWith("http://")) url = "https://" + url;
|
||||
|
||||
uri = new URL(url);
|
||||
String[] domainNameParts = uri.getHost().split("\\.");
|
||||
tldString = domainNameParts[domainNameParts.length - 1];
|
||||
|
||||
return tldString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the domain name (excluding the TLD) from a given URL.
|
||||
*
|
||||
* @param url The URL from which to extract the domain name.
|
||||
* @return The domain name as a string.
|
||||
* @throws URISyntaxException If the URL syntax is incorrect.
|
||||
* @throws MalformedURLException If the URL is malformed.
|
||||
*/
|
||||
public static String getDomainName(String url) throws URISyntaxException, MalformedURLException {
|
||||
if (url.startsWith(Classic_SiteType.PUBLIC.name + "://"))
|
||||
url = url.substring((Classic_SiteType.PUBLIC.name + "://").length());
|
||||
if (url.startsWith(Classic_SiteType.CLIENT.name + "://"))
|
||||
url = url.substring((Classic_SiteType.CLIENT.name + "://").length());
|
||||
if (url.startsWith(Classic_SiteType.SERVER.name + "://"))
|
||||
url = url.substring((Classic_SiteType.SERVER.name + "://").length());
|
||||
if (url.startsWith(Classic_SiteType.PROTOCOL.name + "://"))
|
||||
url = url.substring((Classic_SiteType.PROTOCOL.name + "://").length());
|
||||
if (url.startsWith(Classic_SiteType.LOCAL.name + "://"))
|
||||
url = url.substring((Classic_SiteType.LOCAL.name + "://").length());
|
||||
|
||||
if (!url.startsWith("https://") && !url.startsWith("http://")) url = "https://" + url;
|
||||
|
||||
URI uri = new URI(url);
|
||||
return uri.getHost().replace("." + getTopLevelDomain(url), "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the path component from a given URL.
|
||||
*
|
||||
* @param url The URL from which to extract the path.
|
||||
* @return The path as a string.
|
||||
*/
|
||||
public static String getPath(String url) {
|
||||
if (!url.startsWith(Classic_SiteType.PUBLIC.name + "://") && !url.startsWith(Classic_SiteType.CLIENT.name + "://") &&
|
||||
!url.startsWith(Classic_SiteType.SERVER.name + "://") && !url.startsWith(Classic_SiteType.PROTOCOL.name + "://") &&
|
||||
!url.startsWith(Classic_SiteType.LOCAL.name + "://") && !url.startsWith("http") && !url.startsWith("https")) {
|
||||
url = Classic_SiteType.PUBLIC.name + "://" + url;
|
||||
}
|
||||
|
||||
String[] split = url.split("/");
|
||||
if (split.length <= 3) return "";
|
||||
|
||||
StringBuilder path = new StringBuilder();
|
||||
|
||||
for (int i = 3; i < split.length; i++) path.append(split[i]).append("/");
|
||||
|
||||
String pathStr = path.toString();
|
||||
if (pathStr.startsWith("/")) pathStr = pathStr.substring("/".length());
|
||||
if (pathStr.endsWith("/")) pathStr = pathStr.substring(0, pathStr.length() - "/".length());
|
||||
|
||||
return pathStr;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package org.openautonomousconnection.protocol.versions.v1_0_1.beta;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Parsed INS address components.
|
||||
*
|
||||
* <p>Supported formats:</p>
|
||||
* <ul>
|
||||
* <li>{@code name.tln}</li>
|
||||
* <li>{@code sub.name.tln}</li>
|
||||
* </ul>
|
||||
*/
|
||||
public record InsParts(String tln, String name, String sub) {
|
||||
|
||||
/**
|
||||
* Parses an InfoName into INS query parts.
|
||||
*
|
||||
* @param infoName InfoName host string
|
||||
* @return parsed parts
|
||||
* @throws IllegalArgumentException if format is invalid
|
||||
*/
|
||||
public static InsParts parse(String infoName) {
|
||||
String in = Objects.requireNonNull(infoName, "infoName").trim();
|
||||
if (in.isEmpty()) {
|
||||
throw new IllegalArgumentException("InfoName is empty");
|
||||
}
|
||||
|
||||
String[] parts = in.split("\\.");
|
||||
if (parts.length < 2 || parts.length > 3) {
|
||||
throw new IllegalArgumentException(
|
||||
"Invalid INS address format: " + infoName + " (expected name.tln or sub.name.tln)"
|
||||
);
|
||||
}
|
||||
|
||||
String tln = parts[parts.length - 1].trim();
|
||||
String name = parts[parts.length - 2].trim();
|
||||
String sub = (parts.length == 3) ? parts[0].trim() : null;
|
||||
|
||||
if (tln.isEmpty() || name.isEmpty() || (sub != null && sub.isEmpty())) {
|
||||
throw new IllegalArgumentException("Invalid INS address parts in: " + infoName);
|
||||
}
|
||||
|
||||
return new InsParts(tln, name, sub);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
package org.openautonomousconnection.protocol.versions.v1_0_1.beta;
|
||||
|
||||
import org.openautonomousconnection.protocol.ProtocolBridge;
|
||||
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 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).
|
||||
*
|
||||
* <p>This class provides pure mapping logic and contains no networking code.</p>
|
||||
*
|
||||
* <p>Usage:
|
||||
* <ul>
|
||||
* <li>Server-side: map 1.0.0 {@link WebRequestPacket} to {@link WebResourceRequestPacket}</li>
|
||||
* <li>Server-side: map {@link WebResourceResponsePacket} back to {@link WebResponsePacket}</li>
|
||||
* <li>Client-side: map 1.0.0 responses/streams to v1.0.1 packets so v1.0.1 hooks can be reused</li>
|
||||
* </ul>
|
||||
* </p>
|
||||
*/
|
||||
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}.
|
||||
*
|
||||
* <p>Because v1.0.0 has no tab/page/frame/request correlation model,
|
||||
* synthetic identifiers must be provided.</p>
|
||||
*
|
||||
* @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, ProtocolBridge bridge
|
||||
) {
|
||||
Objects.requireNonNull(requestPacket, "requestPacket");
|
||||
|
||||
String path = requestPacket.getPath();
|
||||
String method = mapMethod(requestPacket.getMethod());
|
||||
Map<String, String> 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, bridge
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a v1.0.1 {@link WebResourceResponsePacket} back to a 1.0.0 {@link WebResponsePacket}.
|
||||
*
|
||||
* <p>Correlation information is lost because v1.0.0 does not support it.</p>
|
||||
*
|
||||
* @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.
|
||||
*
|
||||
* <p>Useful when buffering streamed responses for 1.0.0 clients.</p>
|
||||
*
|
||||
* @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";
|
||||
default -> "GET";
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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", "SCRIPT", "EXECUTE" -> WebRequestMethod.POST;
|
||||
default -> WebRequestMethod.GET;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
* <p>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.</p>
|
||||
*
|
||||
* <p>Thread-safety:
|
||||
* <ul>
|
||||
* <li>Request id generation is thread-safe.</li>
|
||||
* <li>Tab/page allocation methods are thread-safe.</li>
|
||||
* <li>{@link WebTabContext} is thread-safe for reads; writes are synchronized.</li>
|
||||
* </ul>
|
||||
* </p>
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* <p>Call this when navigation is initiated (typed URL, link click, reload, back/forward).</p>
|
||||
*
|
||||
* @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.
|
||||
*
|
||||
* <p>Stale means: the packet refers to an older pageId than the current tab pageId.</p>
|
||||
*
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user