Finished protocol
This commit is contained in:
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
package org.openautonomousconnection.protocol.versions.v1_0_0.beta;
|
||||
|
||||
public enum WebRequestMethod {
|
||||
GET, POST, EXECUTE, SCRIPT
|
||||
GET, POST
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user