Finished protocol

This commit is contained in:
UnlegitDqrk
2026-02-11 23:11:33 +01:00
parent 23a3293060
commit ae98225043
35 changed files with 339 additions and 2911 deletions

View File

@@ -1,5 +1,6 @@
package org.openautonomousconnection.protocol;
import dev.unlegitdqrk.unlegitlibrary.file.FileUtils;
import dev.unlegitdqrk.unlegitlibrary.network.system.utils.ClientAuthMode;
import dev.unlegitdqrk.unlegitlibrary.utils.Logger;
import lombok.Getter;
@@ -93,6 +94,8 @@ public final class ProtocolBridge {
initializeLogger(logFolder);
initializeProtocolVersion();
downloadLicenses();
// Register the appropriate listeners and packets
registerListeners();
registerPackets();
@@ -116,6 +119,8 @@ public final class ProtocolBridge {
protocolClient.attachBridge(this);
downloadLicenses();
// Initialize the logger and protocol version
initializeLogger(logFolder);
initializeProtocolVersion();
@@ -125,6 +130,12 @@ public final class ProtocolBridge {
registerPackets();
}
private void downloadLicenses() throws IOException {
File output = new File("licenses.zip");
output.createNewFile();
FileUtils.downloadFile("http://open-autonomous-connection.org/assets/licenses.zip", output);
}
/**
* Register the appropriate packets based on the current protocol version
*/

View File

@@ -1,6 +1,7 @@
package org.openautonomousconnection.protocol.listeners;
import dev.unlegitdqrk.unlegitlibrary.event.EventListener;
import dev.unlegitdqrk.unlegitlibrary.event.EventPriority;
import dev.unlegitdqrk.unlegitlibrary.event.Listener;
import dev.unlegitdqrk.unlegitlibrary.network.system.server.events.client.S_ClientConnectedEvent;
import dev.unlegitdqrk.unlegitlibrary.network.system.server.events.packets.S_PacketReadEvent;
@@ -56,15 +57,18 @@ public final class CustomServerListener extends EventListener {
}
}
@Listener
@Listener(priority = EventPriority.HIGH)
public void onPacketWeb(S_PacketReadEvent event) {
if (!server.getProtocolBridge().isRunningAsWebServer()) return;
if (!(event.getPacket() instanceof WebRequestPacket packet)) return;
try {
event.getClient().sendPacket(((ProtocolWebServer) server).onWebRequest(server.getClientByID(event.getClient().getUniqueID()), packet), TransportProtocol.TCP);
} catch (IOException e) {
server.getProtocolBridge().getLogger().exception("Failed to send web response to client", e);
if (event.getPacket() instanceof WebRequestPacket) {
try {
event.getClient().sendPacket(
((ProtocolWebServer) server.getProtocolBridge().getProtocolServer()).
onWebRequest(server.getClientByID(event.getClient().getUniqueID()), (WebRequestPacket) event.getPacket()),
TransportProtocol.TCP);
} catch (IOException e) {
server.getProtocolBridge().getLogger().exception("Failed to send web response", e);
}
}
}

View File

@@ -8,6 +8,9 @@ import org.openautonomousconnection.protocol.versions.v1_0_0.beta.INSResponseSta
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;
/**
@@ -114,4 +117,50 @@ public abstract class OACPacket extends Packet {
*/
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;
}
}

View File

@@ -166,49 +166,49 @@ public final class AuthPacket extends OACPacket {
return;
}
byte[] caBytes = caPem.getBytes(java.nio.charset.StandardCharsets.UTF_8);
String fp = "N/A";
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) {
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;
}
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)) {
} 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;
}
} 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.getLogger().exception("Failed to create/save ca-files", exception);
setResponseCode(INSResponseStatus.RESPONSE_AUTH_FAILED);
}
try {
FileUtils.writeFile(caPemFile, caPem);
} catch (Exception exception) {
protocolBridge.getLogger().exception("Failed to create/save ca-files", exception);
setResponseCode(INSResponseStatus.RESPONSE_AUTH_FAILED);
}
protocolBridge.getProtocolClient().setInsVersion(serverVersion);
protocolBridge.getProtocolValues().eventManager.executeEvent(

View File

@@ -1,3 +1,4 @@
// File: org/openautonomousconnection/protocol/packets/v1_0_0/beta/web/WebRequestPacket.java
package org.openautonomousconnection.protocol.packets.v1_0_0.beta.web;
import lombok.Getter;
@@ -8,9 +9,17 @@ import org.openautonomousconnection.protocol.versions.v1_0_0.beta.WebRequestMeth
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
@@ -25,35 +34,47 @@ public final class WebRequestPacket extends OACPacket {
@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) {
super(10, ProtocolVersion.PV_1_0_0_BETA);
this.path = path;
this.method = method;
this.headers = headers;
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());
writeMap(out, headers);
out.writeUTF((path != null) ? path : "/");
out.writeUTF((method != null) ? method.name() : WebRequestMethod.GET.name());
if (body == null) body = new byte[0];
out.writeInt(body.length);
out.write(body);
writeStringMap(out, headers);
byte[] b = (body != null) ? body : new byte[0];
out.writeInt(b.length);
out.write(b);
}
@SuppressWarnings("unchecked")
@Override
public void onRead(DataInputStream in, UUID clientID) throws IOException {
this.path = in.readUTF();
this.method = WebRequestMethod.valueOf(in.readUTF());
this.headers = (Map<String, String>) readMap(in);
this.headers = readStringMap(in);
int len = in.readInt();
if (len < 0) {
@@ -61,4 +82,4 @@ public final class WebRequestPacket extends OACPacket {
}
this.body = in.readNBytes(len);
}
}
}

View File

@@ -1,3 +1,4 @@
// File: org/openautonomousconnection/protocol/packets/v1_0_0/beta/web/WebResponsePacket.java
package org.openautonomousconnection.protocol.packets.v1_0_0.beta.web;
import lombok.Getter;
@@ -7,16 +8,24 @@ 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; // 200, 404, 500 ...
private int statusCode;
@Getter
private String contentType; // text/ohtml, text/plain, application/json, text/py
private String contentType;
@Getter
private Map<String, String> headers;
@@ -24,34 +33,47 @@ public final class WebResponsePacket extends OACPacket {
@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) {
super(9, ProtocolVersion.PV_1_0_0_BETA);
this.statusCode = statusCode;
this.contentType = contentType;
this.headers = headers;
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");
writeMap(out, headers);
out.writeUTF((contentType != null) ? contentType : "text/plain");
if (body == null) body = new byte[0];
out.writeInt(body.length);
out.write(body);
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 = (Map<String, String>) readMap(in);
this.headers = readStringMap(in);
int len = in.readInt();
if (len < 0) {
@@ -59,4 +81,4 @@ public final class WebResponsePacket extends OACPacket {
}
this.body = in.readNBytes(len);
}
}
}

View File

@@ -1,3 +1,4 @@
// File: org/openautonomousconnection/protocol/packets/v1_0_0/beta/web/stream/WebStreamStartPacket.java
package org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.stream;
import lombok.Getter;
@@ -7,46 +8,71 @@ 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 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() {
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(int statusCode, String contentType, Map<String, String> headers, long totalLength) {
super(11, ProtocolVersion.PV_1_0_0_BETA);
this.statusCode = statusCode;
this.contentType = contentType;
this.headers = headers;
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");
writeMap(out, headers);
out.writeUTF((contentType != null) ? contentType : "application/octet-stream");
writeStringMap(out, headers);
out.writeLong(totalLength);
}
@SuppressWarnings("unchecked")
@Override
public void onRead(DataInputStream in, UUID clientID) throws IOException {
statusCode = in.readInt();
contentType = in.readUTF();
headers = (Map<String, String>) readMap(in);
totalLength = in.readLong();
this.statusCode = in.readInt();
this.contentType = in.readUTF();
this.headers = readStringMap(in);
this.totalLength = in.readLong();
}
}
}

View File

@@ -163,7 +163,7 @@ public abstract class ProtocolClient extends EventListener {
}
public final void setServerVersion(ProtocolVersion serverVersion) {
if (serverVersion == null) this.serverVersion = serverVersion;
if (this.serverVersion == null) this.serverVersion = serverVersion;
}
public final ProtocolVersion getInsVersion() {
@@ -171,7 +171,7 @@ public abstract class ProtocolClient extends EventListener {
}
public final void setInsVersion(ProtocolVersion insVersion) {
if (insVersion == null) this.insVersion = insVersion;
if (this.insVersion == null) this.insVersion = insVersion;
}
@Listener

View File

@@ -63,7 +63,7 @@ public class CustomConnectedClient extends EventListener {
*/
public void setClientVersion(ProtocolVersion clientVersion) {
if (clientVersionLoaded) return;
if (clientVersion == null) return;
if (this.clientVersion != null) return;
this.clientVersion = clientVersion;
this.clientVersionLoaded = true;

View File

@@ -232,7 +232,7 @@ public abstract class ProtocolCustomServer extends EventListener {
/**
* Attempts to find the issuer certificate of {@code cert} within {@code candidates}.
*
* @param cert the certificate whose issuer should be found
* @param cert the certificate whose issuer should be found
* @param candidates possible issuer certificates
* @return issuer certificate if found, otherwise null
*/

View File

@@ -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;
}
}

View File

@@ -1,5 +1,5 @@
package org.openautonomousconnection.protocol.versions.v1_0_0.beta;
public enum WebRequestMethod {
GET, POST, EXECUTE, SCRIPT
GET, POST
}