- Added comments

This commit is contained in:
2025-09-29 17:46:30 +02:00
parent fddf9d81ad
commit 1fe77f6076
44 changed files with 1775 additions and 185 deletions

View File

@@ -16,24 +16,50 @@ import org.openautonomousconnection.protocol.packets.v1_0_0.classic.Classic_Ping
import org.openautonomousconnection.protocol.versions.ProtocolVersion;
import org.openautonomousconnection.protocol.versions.v1_0_0.beta.DNSResponseCode;
import org.openautonomousconnection.protocol.versions.v1_0_0.beta.Domain;
import org.openautonomousconnection.protocol.versions.v1_0_0.classic.Classic_RequestDomain;
import org.openautonomousconnection.protocol.versions.v1_0_0.classic.objects.Classic_RequestDomain;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.security.cert.CertificateException;
/**
* Abstract class defining the client-side protocol operations and interactions with DNS and web servers.
*/
@ProtocolInfo(protocolSide = ProtocolVersion.ProtocolSide.DNS)
public abstract class ProtocolClient extends DefaultMethodsOverrider {
private final NetworkClient clientToDNS; // Handles everything with DNS-Connection
/**
* Handles everything with DNS-Connection.
*/
private final NetworkClient clientToDNS;
/**
* Manages the folder structure for client certificates.
*/
@Getter
private final ClientCertificateFolderStructure folderStructure;
/**
* Manages the web connection to the destination server.
*/
@Getter
private WebClient webClient;
/**
* Stores the protocol version of the connected server.
*/
private ProtocolVersion serverVersion = null;
/**
* Initializes the ProtocolClient, setting up certificate folders and the DNS client connection.
* @throws CertificateException if there are issues with the certificates.
* @throws IOException if there are I/O issues during initialization.
*/
public ProtocolClient() throws CertificateException, IOException {
// Initialize and verify certificate folders and files
folderStructure = new ClientCertificateFolderStructure();
// Initialize connection to DNS server
clientToDNS = new NetworkClient.ClientBuilder().setLogger(ProtocolBridge.getInstance().getLogger()).
setHost(ProtocolBridge.getInstance().getProtocolSettings().host).setPort(ProtocolBridge.getInstance().getProtocolSettings().port).
setPacketHandler(ProtocolBridge.getInstance().getProtocolSettings().packetHandler).setEventManager(ProtocolBridge.getInstance().getProtocolSettings().eventManager).
@@ -41,52 +67,98 @@ public abstract class ProtocolClient extends DefaultMethodsOverrider {
build();
}
/**
* Gets the DNS connection client.
* @return the NetworkClient handling the DNS connection.
*/
public final NetworkClient getClientDNSConnection() {
return clientToDNS;
}
/**
* Creates a web connection to the specified domain and ports.
* @param domain the target domain for the web connection.
* @param pipelinePort the port used for the pipeline connection.
* @param webPort the port used for the web connection.
* @throws Exception if there are issues creating the web connection or if the protocol is unsupported.
*/
public final void createWebConnection(Domain domain, int pipelinePort, int webPort) throws Exception {
// Ensure the protocol supports web connections
if (!ProtocolBridge.getInstance().isProtocolSupported(ProtocolVersion.Protocol.OAC))
throw new UnsupportedProtocolException();
// Check if web client is already connected and close it
if (webClient != null) {
try {
webClient.closeConnection();
} catch (IOException e) {
ProtocolBridge.getInstance().getLogger().exception("Failed to close connection to web server", e);
return;
}
}
// Verify necessary certificate files exist
webClient = new WebClient(domain, pipelinePort, webPort);
}
/**
* 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
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) || !file.getName().endsWith(extension))
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;
}
/**
* Handles DNS disconnection events, resetting the server version and closing the web client connection if necessary.
* @param event the ClientDisconnectedEvent triggered on DNS disconnection.
*/
public final void onDNSDisconnect(ClientDisconnectedEvent event) {
// Reset server version on DNS disconnect
serverVersion = null;
// Close web client connection if it exists
if (webClient != null) {
try {
webClient.closeConnection();
@@ -96,82 +168,167 @@ public abstract class ProtocolClient extends DefaultMethodsOverrider {
}
}
/**
* 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();
}
/**
* 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() {
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;
}
/**
* 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;
}
/**
* 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() {
boolean yes = false;
for (ProtocolVersion compatibleVersion : getServerVersion().getCompatibleVersions()) {
// Check if compatible version is beta
yes = compatibleVersion.getProtocolType() == ProtocolVersion.ProtocolType.BETA;
if (yes) break;
}
// Check if the server version is beta
return isBetaServer() || 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;
}
/**
* 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() {
boolean yes = false;
for (ProtocolVersion compatibleVersion : getServerVersion().getCompatibleVersions()) {
// Check if compatible version is classic
yes = compatibleVersion.getProtocolType() == ProtocolVersion.ProtocolType.CLASSIC;
if (yes) break;
}
// Check if the server version is classic
return isClassicServer() || 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());
}
/**
* 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);
}
/**
* 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) {
boolean yes = false;
for (ProtocolVersion compatibleVersion : getServerVersion().getCompatibleVersions()) {
// Check if compatible version supports the protocol
yes = compatibleVersion.getSupportedProtocols().contains(protocol);
if (yes) break;
}
// Check if the server version supports the protocol
return getServerVersion().getSupportedProtocols().contains(protocol) || yes;
}
/**
* Validates the specified domain by sending a validation request to the DNS server.
* @param domain the Domain to validate.
* @throws IOException if there are I/O issues during the validation process.
* @throws ClassNotFoundException if there are issues with class loading during packet handling.
*/
public final void validateDomain(Domain domain) throws IOException, ClassNotFoundException {
// Send Classic_PingPacket if classic protocol is supported
Classic_PingPacket cPingPacket = new Classic_PingPacket(new Classic_RequestDomain(domain.getName(), domain.getTopLevelName(), domain.getPath()), null, false);
if (ProtocolBridge.getInstance().isPacketSupported(cPingPacket)) clientToDNS.sendPacket(cPingPacket);
if (ProtocolBridge.getInstance().isClassicSupported()) clientToDNS.sendPacket(cPingPacket);
// Send ValidateDomainPacket
clientToDNS.sendPacket(new ValidateDomainPacket(domain));
}
/**
* Requests the destination for the specified domain from the DNS server.
* @param domain the Domain for which to request the destination.
* @param responseCode the expected DNSResponseCode for the request.
* @throws IOException if there are I/O issues during the request process.
* @throws ClassNotFoundException if there are issues with class loading during packet handling.
*/
public final void requestDestination(Domain domain, DNSResponseCode responseCode) throws IOException, ClassNotFoundException {
// Send Classic_DomainPacket if classic protocol is supported
Classic_DomainPacket cDomainPacket = new Classic_DomainPacket(0, new Classic_RequestDomain(domain.getName(), domain.getTopLevelName(), domain.getPath()), null);
if (ProtocolBridge.getInstance().isPacketSupported(cDomainPacket)) clientToDNS.sendPacket(cDomainPacket);
if (ProtocolBridge.getInstance().isClassicSupported()) clientToDNS.sendPacket(cDomainPacket);
// Send GetDestinationPacket
clientToDNS.sendPacket(new GetDestinationPacket(domain, responseCode));
}
/**
* Callback method invoked when domain validation is completed.
* @param domain the Domain that was validated.
* @param responseCode the DNSResponseCode resulting from the validation.
*/
public abstract void validationCompleted(Domain domain, DNSResponseCode responseCode);
/**
* Callback method invoked when the destination retrieval is completed.
* @param domain the Domain for which the destination was requested.
* @param destination the retrieved destination as a string.
* @param validationResponse the DNSResponseCode resulting from the destination retrieval.
*/
public abstract void getDestinationCompleted(Domain domain, String destination, DNSResponseCode validationResponse);
/**
* Manages the folder structure for client certificates.
*/
public final class ClientCertificateFolderStructure {
public final File certificatesFolder;

View File

@@ -17,29 +17,89 @@ import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.Socket;
/**
* WebClient handles secure connections to web servers through a pipeline connection.
* It manages SSL/TLS handshakes and data transmission over the established connection.
*/
@ProtocolInfo(protocolSide = ProtocolVersion.ProtocolSide.DNS)
public final class WebClient {
private final NetworkClient clientToWebPipeline; // Handles everything with Pipeline-Connection
private SSLSocket clientToWebServer; // Handles everything with Web-Connection
private ObjectOutputStream outputStream; private final Thread receiveThread = new Thread(this::receive);
/**
* NetworkClient instance for managing the pipeline connection to the web server.
*/
private final NetworkClient clientToWebPipeline;
/**
* SSLSocket for secure communication with the web server.
*/
private SSLSocket clientToWebServer;
/**
* Streams for object serialization over the SSL connection.
*/
private ObjectOutputStream outputStream;
/**
* Streams for object serialization over the SSL connection.
*/
private ObjectInputStream inputStream;
/**
* Thread for receiving data from the web server.
*/
private final Thread receiveThread = new Thread(this::receive);
/**
* Constructs a WebClient instance and establishes a secure connection to the web server.
* @param domain The domain information for the web server.
* @param pipelinePort The port for the pipeline connection.
* @param webPort The port for the web server connection.
* @throws Exception If an error occurs during connection setup.
*/
public WebClient(Domain domain, int pipelinePort, int webPort) throws Exception {
clientToWebPipeline = new NetworkClient.ClientBuilder().setLogger(ProtocolBridge.getInstance().getLogger()).
// Initialize and connect the pipeline client
clientToWebPipeline = new NetworkClient.ClientBuilder().
// Set logger from ProtocolBridge
setLogger(ProtocolBridge.getInstance().getLogger()).
// Set the destination and port for the pipeline connection
setHost(domain.getDestination()).setPort(pipelinePort).
setPacketHandler(ProtocolBridge.getInstance().getProtocolSettings().packetHandler).setEventManager(ProtocolBridge.getInstance().getProtocolSettings().eventManager).
setRootCAFolder(ProtocolBridge.getInstance().getProtocolClient().getFolderStructure().publicCAFolder).setClientCertificatesFolder(ProtocolBridge.getInstance().getProtocolClient().getFolderStructure().publicClientFolder, ProtocolBridge.getInstance().getProtocolClient().getFolderStructure().privateClientFolder).
// Configure packet handler and event manager
setPacketHandler(ProtocolBridge.getInstance().getProtocolSettings().packetHandler).
setEventManager(ProtocolBridge.getInstance().getProtocolSettings().eventManager).
// Set proxy and ssl parameters from DNS connection settings
setProxy(ProtocolBridge.getInstance().getProtocolClient().getClientDNSConnection().getProxy()).
setSSLParameters(ProtocolBridge.getInstance().getProtocolClient().getClientDNSConnection().getSocket().getSSLParameters()).
// Set certificates and folders for SSL
setRootCAFolder(ProtocolBridge.getInstance().getProtocolClient().getFolderStructure().publicCAFolder).
setClientCertificatesFolder(ProtocolBridge.getInstance().getProtocolClient().getFolderStructure().publicClientFolder,
ProtocolBridge.getInstance().getProtocolClient().getFolderStructure().privateClientFolder).
// Finalize the client setup
build();
// Connect to the pipeline
clientToWebPipeline.connect();
// Wait until the pipeline connection is established
while (!clientToWebPipeline.isConnected()) if (clientToWebPipeline.isConnected()) break;
SSLSocketFactory sslSocketFactory = NetworkClient.ClientBuilder.createSSLSocketFactory(ProtocolBridge.getInstance().getProtocolClient().getFolderStructure().publicCAFolder, ProtocolBridge.getInstance().getProtocolClient().getFolderStructure().publicClientFolder, ProtocolBridge.getInstance().getProtocolClient().getFolderStructure().privateClientFolder);
// Create SSL socket factory using client certificates
SSLSocketFactory sslSocketFactory = NetworkClient.ClientBuilder.
createSSLSocketFactory(ProtocolBridge.getInstance().getProtocolClient().getFolderStructure().publicCAFolder,
ProtocolBridge.getInstance().getProtocolClient().getFolderStructure().publicClientFolder,
ProtocolBridge.getInstance().getProtocolClient().getFolderStructure().privateClientFolder);
// Get proxy settings from the pipeline client
Proxy proxy = clientToWebPipeline.getProxy();
// Establish SSL connection to the web server
SSLSocket tempSocket;
if (sslSocketFactory == null) {
throw new ConnectException("SSL socket factory not set. Client certificate required!");
} else {
// Create raw socket and wrap it in SSL socket
if (proxy != null) {
Socket rawSocket = new Socket(proxy);
rawSocket.connect(new InetSocketAddress(domain.getDestination(), webPort), 0);
@@ -49,16 +109,20 @@ public final class WebClient {
}
clientToWebServer = tempSocket;
// Configure SSL parameters
SSLParameters sslParameters = clientToWebPipeline.getSocket().getSSLParameters();
if (sslParameters != null) {
clientToWebServer.setSSLParameters(sslParameters);
} else {
// Set default to TLSv1.3 if no parameters are provided
SSLParameters defaultParams = clientToWebServer.getSSLParameters();
defaultParams.setProtocols(new String[]{"TLSv1.3"});
clientToWebServer.setSSLParameters(defaultParams);
}
// Configure socket options
clientToWebServer.setTcpNoDelay(true);
clientToWebServer.setSoTimeout(0);
@@ -68,26 +132,46 @@ public final class WebClient {
throw new ConnectException("Handshake failed: " + handshakeEx.getMessage());
}
// Initialize object streams for communication
this.outputStream = new ObjectOutputStream(clientToWebServer.getOutputStream());
this.inputStream = new ObjectInputStream(clientToWebServer.getInputStream());
// Start the receive thread
this.receiveThread.start();
}
}
/**
* Gets the NetworkClient used for the pipeline connection to the web server.
* @return The NetworkClient connected to the web server pipeline.
*/
public NetworkClient getClientPipelineConnection() {
return clientToWebPipeline;
}
/**
* Gets the SSLSocket used for communication with the web server.
* @return The SSLSocket connected to the web server.
*/
public SSLSocket getClientWebConnection() {
return clientToWebServer;
}
/**
* Checks if the WebClient is currently connected to the web server.
* @return true if connected, false otherwise.
*/
public boolean isConnected() {
return this.clientToWebServer != null && this.clientToWebServer.isConnected() && !this.clientToWebServer.isClosed()
&& this.receiveThread.isAlive() && !this.receiveThread.isInterrupted() &&
ProtocolBridge.getInstance().getProtocolClient().getClientDNSConnection().isConnected() && clientToWebPipeline.isConnected();
}
/**
* Continuously receives data from the web server.
* This method runs in a separate thread and handles incoming objects.
* If an error occurs, it attempts to close the connection.
*/
private void receive() {
try {
while (this.isConnected()) {
@@ -102,6 +186,11 @@ public final class WebClient {
}
}
/**
* Closes the connection to the web server and releases resources.
* @return true if the connection was successfully closed, false if it was already closed.
* @throws IOException If an I/O error occurs during closure.
*/
public synchronized boolean closeConnection() throws IOException {
if (!this.isConnected()) {
return false;
@@ -109,7 +198,10 @@ public final class WebClient {
clientToWebPipeline.disconnect();
try {
// Interrupt the receive thread
this.receiveThread.interrupt();
// Close streams and socket
if (this.outputStream != null) {
this.outputStream.close();
}
@@ -122,6 +214,7 @@ public final class WebClient {
this.clientToWebServer.close();
}
} finally {
// Nullify references to help with garbage collection
this.clientToWebServer = null;
this.outputStream = null;
this.inputStream = null;

View File

@@ -4,6 +4,9 @@ import dev.unlegitdqrk.unlegitlibrary.event.impl.Event;
import org.openautonomousconnection.protocol.annotations.ProtocolInfo;
import org.openautonomousconnection.protocol.versions.ProtocolVersion;
/**
* Event triggered when a client successfully connects to a DNS protocol server.
*/
@ProtocolInfo(protocolSide = ProtocolVersion.ProtocolSide.DNS)
public class ConnectedToProtocolServer extends Event {
public class ConnectedToProtocolDNSServerEvent extends Event {
}

View File

@@ -6,83 +6,153 @@ 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 DNS server side.
*/
@ProtocolInfo(protocolSide = ProtocolVersion.ProtocolSide.DNS)
public final class ConnectedProtocolClient {
/**
* The connection handler associated with this protocol client.
*/
@Getter
private final ConnectionHandler connectionHandler;
/**
* The protocol version of the connected client.
*/
private ProtocolVersion clientVersion = null;
public ConnectedProtocolClient(ConnectionHandler connectionHandler) {
this.connectionHandler = connectionHandler;
}
/**
* 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.
*/
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 (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() {
// Check if the server version is classic
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 given packet's protocol version.
* @param packet The packet to check.
* @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.
* @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
return getClientVersion() == targetVersion || getClientVersion().getCompatibleVersions().contains(targetVersion);
}
/**
* Checks if the connected client supports the given protocol.
* @param protocol The protocol to check.
* @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;
}
}

View File

@@ -18,23 +18,50 @@ import java.security.cert.CertificateException;
import java.util.ArrayList;
import java.util.List;
/**
* Abstract class representing a DNS server in the protocol.
*/
@ProtocolInfo(protocolSide = ProtocolVersion.ProtocolSide.DNS)
public abstract class ProtocolDNSServer extends DefaultMethodsOverrider {
/**
* The network server instance.
*/
@Getter
private final NetworkServer networkServer;
/**
* The configuration manager for handling server configurations.
*/
private final ConfigurationManager configurationManager;
/**
* List of connected protocol clients.
*/
@Getter
private List<ConnectedProtocolClient> clients;
/**
* The folder structure for server certificates.
*/
@Getter
private ServerCertificateFolderStructure folderStructure;
/**
* Constructs a ProtocolDNSServer with the specified configuration file.
*
* @param configFile The configuration file for the DNS server.
* @throws IOException If an I/O error occurs.
* @throws CertificateException If a certificate error occurs.
*/
public ProtocolDNSServer(File configFile) throws IOException, CertificateException {
// 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", "DNS-SERVER INFO SITE IP");
configurationManager.saveProperties();
@@ -45,8 +72,10 @@ public abstract class ProtocolDNSServer extends DefaultMethodsOverrider {
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");
@@ -54,13 +83,17 @@ public abstract class ProtocolDNSServer extends DefaultMethodsOverrider {
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
ProtocolBridge protocolBridge = ProtocolBridge.getInstance();
this.clients = new ArrayList<>();
this.networkServer = new NetworkServer.ServerBuilder().setLogger(protocolBridge.getLogger()).
// 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).
@@ -68,51 +101,126 @@ public abstract class ProtocolDNSServer extends DefaultMethodsOverrider {
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) || !file.getName().endsWith(extension))
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;
}
/**
* Gets the DNS information site URL from the configuration.
* @return The DNS information site URL.
*/
public final String getDNSInfoSite() {
return configurationManager.getString("server.site.info");
}
/**
* Gets the DNS registration site URL from the configuration.
* @return The DNS registration site URL.
*/
public final String getDNSRegisterSite() {
return configurationManager.getString("server.site.register");
}
/**
* Abstract method to retrieve the list of domains managed by the DNS server.
* @return A list of Domain objects.
*/
public abstract List<Domain> getDomains();
/**
* @see Domain#getDestination()
* Abstract method to get the destination for a given domain.
* @param domain The domain to look up.
* @return The destination associated with the domain.
*/
public abstract String getDomainDestination(Domain domain);
/**
* @see Domain#getDestination()
* Abstract method to get the destination for a given subname under a specific domain.
* @param domain The parent domain.
* @param subname The subname to look up.
* @return The destination associated with the subname.
*/
public abstract String getSubnameDestination(Domain domain, String subname);
/**
* @see Domain#getDestination()
* Abstract method to get the top-level domain information site URL.
* @param topLevelName The top-level domain name.
* @return The information site URL for the specified top-level domain.
*/
public abstract String getTLNInfoSite(String topLevelName);
/**
* Abstract method to validate a requested domain.
* @param requestedDomain The domain to validate.
* @return A DNSResponseCode indicating the result of the validation.
*/
public abstract DNSResponseCode validateDomain(Domain requestedDomain);
/**
* Abstract method called when a validation packet fails to send.
* @param domain The domain associated with the validation.
* @param client The connected protocol client.
* @param exception The exception that occurred during sending.
*/
public abstract void validationPacketSendFailed(Domain domain, ConnectedProtocolClient client, Exception exception);
/**
* Abstract method called when a domain destination packet fails to send.
* @param client The connected protocol client.
* @param domain The domain associated with the packet.
* @param validationResponse The DNS response code from validation.
* @param exception The exception that occurred during sending.
*/
public abstract void domainDestinationPacketFailedSend(ConnectedProtocolClient client, Domain domain, DNSResponseCode validationResponse, Exception exception);
/**
* Class representing the folder structure for server certificates.
*/
public final class ServerCertificateFolderStructure {
public final File certificatesFolder;

View File

@@ -6,6 +6,9 @@ import org.openautonomousconnection.protocol.annotations.ProtocolInfo;
import org.openautonomousconnection.protocol.side.dns.ConnectedProtocolClient;
import org.openautonomousconnection.protocol.versions.ProtocolVersion;
/**
* Event triggered when a protocol client connects to the DNS server.
*/
@ProtocolInfo(protocolSide = ProtocolVersion.ProtocolSide.DNS)
public final class ConnectedProtocolClientEvent extends Event {

View File

@@ -18,64 +18,140 @@ 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;
private ProtocolVersion clientVersion = null; private final Thread receiveThread = new Thread(this::receive);
/**
* The protocol version of the connected client.
*/
private ProtocolVersion clientVersion = null;
/**
* Thread for receiving data from the client.
*/
private final Thread receiveThread = new Thread(this::receive);
/**
* Indicates if the client version has been loaded.
*/
@Getter
private boolean clientVersionLoaded = false;
/**
* 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 {
out.write(("HTTP/1.1 302 Found\r\n").getBytes());
// 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();
}
/**
* 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]);
}
@@ -83,17 +159,32 @@ public final class ConnectedWebClient {
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;
@@ -102,14 +193,17 @@ public final class ConnectedWebClient {
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=\"")) {
@@ -125,6 +219,7 @@ public final class ConnectedWebClient {
}
}
// 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);
@@ -135,54 +230,89 @@ public final class ConnectedWebClient {
}
}
private static String renderPHP(File file) throws IOException, InterruptedException {
ProcessBuilder pb = new ProcessBuilder("php", file.getAbsolutePath());
pb.redirectErrorStream(true);
Process p = pb.start();
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();
return output.toString(StandardCharsets.UTF_8);
}
/**
* 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 {
out.write(("HTTP/1.1 " + code + " " + getStatusText(code) + "\r\n").getBytes());
// 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";
@@ -197,6 +327,11 @@ public final class ConnectedWebClient {
};
}
/**
* 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";
@@ -213,11 +348,20 @@ public final class ConnectedWebClient {
};
}
/**
* 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];
@@ -227,14 +371,19 @@ public final class ConnectedWebClient {
}
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();
@@ -244,114 +393,194 @@ public final class ConnectedWebClient {
}
}
} 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()) {
@@ -447,6 +676,9 @@ public final class ConnectedWebClient {
}
}
/**
* Represents the response from a PHP script, including body and cookies.
*/
private static class PHPResponse {
String body;
Map<String, String> cookies;
@@ -457,5 +689,4 @@ public final class ConnectedWebClient {
}
}
}

View File

@@ -20,43 +20,96 @@ import java.security.cert.CertificateException;
import java.util.ArrayList;
import java.util.List;
/**
* Represents the web server for the protocol.
*/
@ProtocolInfo(protocolSide = ProtocolVersion.ProtocolSide.WEB)
public class ProtocolWebServer {
/**
* Folder for web content.
*/
@Getter
private final File contentFolder;
/**
* 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 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;
/**
* 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.
* @throws Exception If an error occurs during initialization.
*/
public ProtocolWebServer(File configFile, File authFile, File rulesFile) throws Exception {
// Initialize the list of connected clients
this.clients = new ArrayList<>();
// 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, """
@@ -65,6 +118,7 @@ public class ProtocolWebServer {
""");
}
// Create default rules file if it doesn't exist
if (!rulesFile.exists()) {
rulesFile.createNewFile();
FileUtils.writeFile(rulesFile, """
@@ -85,9 +139,11 @@ public class ProtocolWebServer {
""");
}
// Load authentication and rules
AuthManager.loadAuthFile(authFile);
RuleManager.loadRules(rulesFile);
// Initialize the pipeline server
pipelineServer = new NetworkServer.ServerBuilder().
setPort(configurationManager.getInt("port.pipeline")).setTimeout(0).
setPacketHandler(ProtocolBridge.getInstance().getProtocolSettings().packetHandler).setEventManager(ProtocolBridge.getInstance().getProtocolSettings().eventManager).
@@ -96,13 +152,26 @@ public class ProtocolWebServer {
build();
}
/**
* 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 ConnectedWebClient getClientByID(int clientID) {
for (ConnectedWebClient client : clients)
if (client.getPipelineConnection().getClientID() == clientID) return client;
return null;
}
/**
* Starts the web server to accept and handle client connections.
* @throws Exception If an error occurs while starting the server.
*/
public final void startWebServer() throws Exception {
// Start the pipeline server
pipelineServer.start();
// Create the SSL server socket for web connections
webServer = (SSLServerSocket) NetworkServer.ServerBuilder.
createSSLServerSocketFactory(folderStructure.publicCAFolder, certFile, keyFile).
createServerSocket(configurationManager.getInt("port"));
@@ -112,8 +181,10 @@ public class ProtocolWebServer {
// 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);
@@ -122,6 +193,11 @@ public class ProtocolWebServer {
Thread.currentThread().interrupt();
break;
}
try {
// Sleep for a while before checking again
Thread.sleep(1000);
} catch (InterruptedException ignored) {}
}
}).start();
@@ -129,40 +205,68 @@ public class ProtocolWebServer {
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);
}
}
} catch (IOException e) {
e.printStackTrace();
pipelineServer.getLogger().exception("Failed to accept WebClient", e);
}
}
}).start();
}
/**
* Handles the shutdown of the web server when the pipeline server stops.
* @throws IOException If an I/O error occurs while closing the web server.
*/
private void onPipelineStop() throws IOException {
webServer.close();
}
/**
* 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 {
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) || !file.getName().endsWith(extension))
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);
}
/**
* Loads and initializes the configuration manager with default settings if necessary.
* @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.
*/
private ConfigurationManager getConfigurationManager(File configFile) throws IOException {
if (!configFile.exists()) configFile.createNewFile();
@@ -192,6 +296,9 @@ public class ProtocolWebServer {
return configurationManager;
}
/**
* Represents the folder structure for server certificates.
*/
public final class ServerCertificateFolderStructure {
public final File certificatesFolder;

View File

@@ -6,9 +6,15 @@ 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;

View File

@@ -11,16 +11,34 @@ import java.security.MessageDigest;
import java.util.HashMap;
import java.util.Map;
/**
* Manages authentication for web clients.
* Loads user credentials from a file and verifies login attempts.
*/
@ProtocolInfo(protocolSide = ProtocolVersion.ProtocolSide.WEB)
public class AuthManager {
/**
* Map of usernames to their SHA-256 hashed passwords
*/
private static final Map<String, String> users = new HashMap<>();
/**
* Loads the authentication file and populates the users map.
* The file should contain lines in the format: username:hashed_password
* Lines starting with '#' are treated as comments and ignored.
* @param authFile The authentication file to load.
* @throws IOException If an I/O error occurs reading from the file.
*/
public static void loadAuthFile(File authFile) throws IOException {
// Create the file if it doesn't exist
if (!authFile.exists()) authFile.createNewFile();
for (String line : Files.readAllLines(authFile.toPath(), StandardCharsets.UTF_8)) {
// Trim whitespace and ignore comments/empty lines
line = line.trim();
if (line.isEmpty() || line.startsWith("#")) continue;
// Split the line into username and hashed password
String[] parts = line.split(":", 2);
if (parts.length == 2) {
users.put(parts[0], parts[1]);
@@ -28,19 +46,33 @@ public class AuthManager {
}
}
/**
* Checks if the provided login and password are valid.
* @param login The username to check.
* @param password The password to verify.
* @return True if the credentials are valid, false otherwise.
*/
public static boolean checkAuth(String login, String password) {
// Retrieve the stored hashed password for the given username
String storedHash = users.get(login);
if (storedHash == null) return false;
// Hash the provided password and compare it to the stored hash
String hash = sha256(password);
return storedHash.equalsIgnoreCase(hash);
}
/**
* Computes the SHA-256 hash of the given input string.
* @param input The input string to hash.
* @return The hexadecimal representation of the SHA-256 hash.
*/
private static String sha256(String input) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] digest = md.digest(input.getBytes(StandardCharsets.UTF_8));
// Convert the byte array to a hexadecimal string
StringBuilder sb = new StringBuilder();
for (byte b : digest) sb.append(String.format("%02x", b));
return sb.toString();

View File

@@ -10,33 +10,79 @@ import java.nio.file.Files;
import java.util.List;
import java.util.Map;
/**
* Manages access rules for web resources.
* Loads allow, deny, and auth rules from a JSON file and provides methods to check access.
*/
@ProtocolInfo(protocolSide = ProtocolVersion.ProtocolSide.WEB)
public class RuleManager {
/**
* Lists of path patterns for allow, deny, and auth rules
*/
private static List<String> allow;
/**
* 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", ...]
* }
* @param rulesFile The JSON file containing the rules.
* @throws Exception If an error occurs reading the file or parsing JSON.
*/
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());
Map<String, List<String>> map = new Gson().fromJson(json, new TypeToken<Map<String, List<String>>>() {}.getType());
// 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());
}
/** Checks if the given path is allowed based on the allow rules.
* @param path The path to check.
* @return True if the path is allowed, false otherwise.
*/
public static boolean isAllowed(String path) {
return allow.stream().anyMatch(p -> pathMatches(path, p));
}
/** Checks if the given path is denied based on the deny rules.
* @param path The path to check.
* @return True if the path is denied, false otherwise.
*/
public static boolean isDenied(String path) {
return deny.stream().anyMatch(p -> pathMatches(path, p));
}
/** Checks if the given path requires authentication based on the auth rules.
* @param path The path to check.
* @return True if the path requires authentication, false otherwise.
*/
public static boolean requiresAuth(String path) {
return auth.stream().anyMatch(p -> pathMatches(path, p));
}
/** Helper method to check if a path matches a pattern.
* Patterns can include '*' as a wildcard.
* @param path The path to check.
* @param pattern The pattern to match against.
* @return True if the path matches the pattern, false otherwise.
*/
private static boolean pathMatches(String path, String pattern) {
pattern = pattern.replace("/", File.separator).replace("*", ".*");
return path.matches(pattern);

View File

@@ -10,49 +10,106 @@ import java.util.Base64;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Manages user sessions for web clients.
* Provides methods to create, validate, and invalidate sessions.
*/
@ProtocolInfo(protocolSide = ProtocolVersion.ProtocolSide.WEB)
public class SessionManager {
/**
* Map of session IDs to Session objects.
*/
private static final Map<String, Session> sessions = new ConcurrentHashMap<>();
/**
* Secure random number generator for session ID creation.
*/
private static final SecureRandom secureRandom = new SecureRandom();
/**
* Creates a new session for the given user.
* @param login The username associated with the session.
* @param ip The IP address of the client.
* @param userAgent The User-Agent string of the client.
* @return The generated session ID.
* @throws IOException If an I/O error occurs.
*/
public static String create(String login, String ip, String userAgent) throws IOException {
// Generate a secure random session ID
byte[] bytes = new byte[32];
secureRandom.nextBytes(bytes);
// Encode the bytes to a URL-safe Base64 string
String sessionId = Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
// Create and store the new session
sessions.put(sessionId, new Session(login, ip, userAgent));
return sessionId;
}
/**
* Validates a session ID against the provided IP and User-Agent.
* @param sessionId The session ID to validate.
* @param ip The IP address of the client.
* @param userAgent The User-Agent string of the client.
* @return True if the session is valid, false otherwise.
* @throws IOException If an I/O error occurs.
*/
public static boolean isValid(String sessionId, String ip, String userAgent) throws IOException {
// Retrieve the session associated with the session ID
Session session = sessions.get(sessionId);
// Check if the session exists, is not expired, and matches the IP and User-Agent
if (session == null || session.isExpired() || !session.matches(ip, userAgent)) {
sessions.remove(sessionId);
return false;
}
// Refresh the session expiration time
session.refresh();
return true;
}
/**
* Invalidates a session, removing it from the active sessions.
* @param sessionId The session ID to invalidate.
*/
public static void invalidate(String sessionId) {
sessions.remove(sessionId);
}
/**
* Retrieves the username associated with a valid session ID.
* @param sessionId The session ID to look up.
* @return The username if the session is valid, null otherwise.
*/
public static String getUser(String sessionId) {
// Retrieve the session associated with the session ID
Session session = sessions.get(sessionId);
// Check if the session exists and is not expired
if (session == null || session.isExpired()) {
sessions.remove(sessionId);
return null;
}
// Return the username associated with the session
return session.getLogin();
}
/**
* Cleans up expired sessions from the session map.
* This method should be called periodically to prevent memory leaks.
*/
public static void cleanupExpiredSessions() {
long now = System.currentTimeMillis();
sessions.entrySet().removeIf(entry -> entry.getValue().isExpired());
}
/**
* Represents a user session with associated metadata.
*/
private static class Session {
@Getter
String login;
@@ -67,14 +124,28 @@ public class SessionManager {
this.expiresAt = System.currentTimeMillis() + Main.getConfigurationManager().getInt("sessionexpireminutes") * 60 * 1000;
}
/**
* Checks if the session has expired.
* @return True if the session is expired, false otherwise.
*/
boolean isExpired() {
return System.currentTimeMillis() > expiresAt;
}
/**
* Checks if the session matches the given IP and User-Agent.
* @param ip The IP address to check.
* @param userAgent The User-Agent string to check.
* @return True if both match, false otherwise.
*/
boolean matches(String ip, String userAgent) {
return this.ip.equals(ip) && this.userAgent.equals(userAgent);
}
/**
* Refreshes the session's expiration time.
* @throws IOException If an I/O error occurs.
*/
void refresh() throws IOException {
this.expiresAt = System.currentTimeMillis() + Main.getConfigurationManager().getInt("sessionexpireminutes") * 60 * 1000;
}