Finished up WebServer-Protocol
This commit is contained in:
2
pom.xml
2
pom.xml
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
<groupId>org.openautonomousconnection</groupId>
|
<groupId>org.openautonomousconnection</groupId>
|
||||||
<artifactId>protocol</artifactId>
|
<artifactId>protocol</artifactId>
|
||||||
<version>1.0.0-BETA.1</version>
|
<version>1.0.0-BETA.2</version>
|
||||||
<organization>
|
<organization>
|
||||||
<name>Open Autonomous Connection</name>
|
<name>Open Autonomous Connection</name>
|
||||||
<url>https://open-autonomous-connection.org/</url>
|
<url>https://open-autonomous-connection.org/</url>
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ public final class INSQueryPacket extends OACPacket {
|
|||||||
* @param clientId Sender client ID for routing.
|
* @param clientId Sender client ID for routing.
|
||||||
*/
|
*/
|
||||||
public INSQueryPacket(String tln, String name, String sub, INSRecordType type, int clientId) {
|
public INSQueryPacket(String tln, String name, String sub, INSRecordType type, int clientId) {
|
||||||
super(6, ProtocolVersion.PV_1_0_0_BETA);
|
super(5, ProtocolVersion.PV_1_0_0_BETA);
|
||||||
this.tln = tln;
|
this.tln = tln;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.sub = sub;
|
this.sub = sub;
|
||||||
@@ -51,7 +51,7 @@ public final class INSQueryPacket extends OACPacket {
|
|||||||
* Registration constructor
|
* Registration constructor
|
||||||
*/
|
*/
|
||||||
public INSQueryPacket() {
|
public INSQueryPacket() {
|
||||||
super(6, ProtocolVersion.PV_1_0_0_BETA);
|
super(5, ProtocolVersion.PV_1_0_0_BETA);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ public final class INSResponsePacket extends OACPacket {
|
|||||||
* @param bridge Protocol runtime context.
|
* @param bridge Protocol runtime context.
|
||||||
*/
|
*/
|
||||||
public INSResponsePacket(INSResponseStatus status, List<INSRecord> records, int clientId, ProtocolBridge bridge) {
|
public INSResponsePacket(INSResponseStatus status, List<INSRecord> records, int clientId, ProtocolBridge bridge) {
|
||||||
super(7, ProtocolVersion.PV_1_0_0_BETA);
|
super(6, ProtocolVersion.PV_1_0_0_BETA);
|
||||||
this.status = status;
|
this.status = status;
|
||||||
this.records = records;
|
this.records = records;
|
||||||
this.clientId = clientId;
|
this.clientId = clientId;
|
||||||
@@ -54,7 +54,7 @@ public final class INSResponsePacket extends OACPacket {
|
|||||||
* @param bridge Protocol runtime context.
|
* @param bridge Protocol runtime context.
|
||||||
*/
|
*/
|
||||||
public INSResponsePacket(ProtocolBridge bridge) {
|
public INSResponsePacket(ProtocolBridge bridge) {
|
||||||
super(7, ProtocolVersion.PV_1_0_0_BETA);
|
super(6, ProtocolVersion.PV_1_0_0_BETA);
|
||||||
this.bridge = bridge;
|
this.bridge = bridge;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ public final class UnsupportedClassicPacket extends OACPacket {
|
|||||||
* Registration Constructor
|
* Registration Constructor
|
||||||
*/
|
*/
|
||||||
public UnsupportedClassicPacket() {
|
public UnsupportedClassicPacket() {
|
||||||
super(5, ProtocolVersion.PV_1_0_0_BETA);
|
super(7, ProtocolVersion.PV_1_0_0_BETA);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package org.openautonomousconnection.protocol.packets.v1_0_0.beta;
|
||||||
|
|
||||||
|
import dev.unlegitdqrk.unlegitlibrary.network.system.packets.PacketHandler;
|
||||||
|
import org.openautonomousconnection.protocol.packets.OACPacket;
|
||||||
|
import org.openautonomousconnection.protocol.versions.ProtocolVersion;
|
||||||
|
import org.openautonomousconnection.protocol.versions.v1_0_0.beta.WebRequestMethod;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.ObjectInputStream;
|
||||||
|
import java.io.ObjectOutputStream;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public final class WebRequestPacket extends OACPacket {
|
||||||
|
|
||||||
|
private String path;
|
||||||
|
private WebRequestMethod method;
|
||||||
|
private Map<String,String> headers;
|
||||||
|
private byte[] body;
|
||||||
|
|
||||||
|
public WebRequestPacket() {
|
||||||
|
super(8, ProtocolVersion.PV_1_0_0_BETA);
|
||||||
|
}
|
||||||
|
|
||||||
|
public WebRequestPacket(String path, WebRequestMethod method, Map<String, String> headers, byte[] body) {
|
||||||
|
super(8, ProtocolVersion.PV_1_0_0_BETA);
|
||||||
|
this.path = path;
|
||||||
|
this.method = method;
|
||||||
|
this.headers = headers;
|
||||||
|
this.body = (body != null) ? body : new byte[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onWrite(PacketHandler handler, ObjectOutputStream out) throws IOException {
|
||||||
|
out.writeUTF(path != null ? path : "/");
|
||||||
|
out.writeUTF(method != null ? method.name() : WebRequestMethod.GET.name());
|
||||||
|
out.writeObject(headers);
|
||||||
|
|
||||||
|
if (body == null) body = new byte[0];
|
||||||
|
out.writeInt(body.length);
|
||||||
|
out.write(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
@Override
|
||||||
|
public void onRead(PacketHandler handler, ObjectInputStream in) throws IOException, ClassNotFoundException {
|
||||||
|
this.path = in.readUTF();
|
||||||
|
this.method = WebRequestMethod.valueOf(in.readUTF());
|
||||||
|
this.headers = (Map<String, String>) in.readObject();
|
||||||
|
|
||||||
|
int len = in.readInt();
|
||||||
|
if (len < 0) {
|
||||||
|
throw new IOException("Negative body length in WebRequestPacket");
|
||||||
|
}
|
||||||
|
this.body = in.readNBytes(len);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package org.openautonomousconnection.protocol.packets.v1_0_0.beta;
|
||||||
|
|
||||||
|
import dev.unlegitdqrk.unlegitlibrary.network.system.packets.PacketHandler;
|
||||||
|
import org.openautonomousconnection.protocol.packets.OACPacket;
|
||||||
|
import org.openautonomousconnection.protocol.versions.ProtocolVersion;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.ObjectInputStream;
|
||||||
|
import java.io.ObjectOutputStream;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public final class WebResponsePacket extends OACPacket {
|
||||||
|
|
||||||
|
private int statusCode; // 200, 404, 500 ...
|
||||||
|
private String contentType; // text/ohtml, text/plain, application/json, text/py
|
||||||
|
private Map<String,String> headers;
|
||||||
|
private byte[] body;
|
||||||
|
|
||||||
|
public WebResponsePacket() {
|
||||||
|
super(9, ProtocolVersion.PV_1_0_0_BETA);
|
||||||
|
}
|
||||||
|
|
||||||
|
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.body = (body != null) ? body : new byte[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onWrite(PacketHandler handler, ObjectOutputStream out) throws IOException {
|
||||||
|
out.writeInt(statusCode);
|
||||||
|
out.writeUTF(contentType != null ? contentType : "text/plain");
|
||||||
|
out.writeObject(headers);
|
||||||
|
|
||||||
|
if (body == null) body = new byte[0];
|
||||||
|
out.writeInt(body.length);
|
||||||
|
out.write(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRead(PacketHandler handler, ObjectInputStream in) throws IOException, ClassNotFoundException {
|
||||||
|
this.statusCode = in.readInt();
|
||||||
|
this.contentType = in.readUTF();
|
||||||
|
this.headers = (Map<String, String>) in.readObject();
|
||||||
|
|
||||||
|
int len = in.readInt();
|
||||||
|
if (len < 0) {
|
||||||
|
throw new IOException("Negative body length in WebResponsePacket");
|
||||||
|
}
|
||||||
|
this.body = in.readNBytes(len);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,50 +1,32 @@
|
|||||||
package org.openautonomousconnection.protocol.side.web;
|
package org.openautonomousconnection.protocol.side.web;
|
||||||
|
|
||||||
import dev.unlegitdqrk.unlegitlibrary.network.system.server.ConnectionHandler;
|
import dev.unlegitdqrk.unlegitlibrary.network.system.server.ConnectionHandler;
|
||||||
|
import dev.unlegitdqrk.unlegitlibrary.network.system.packets.PacketHandler;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import org.openautonomousconnection.protocol.annotations.ProtocolInfo;
|
|
||||||
import org.openautonomousconnection.protocol.packets.OACPacket;
|
import org.openautonomousconnection.protocol.packets.OACPacket;
|
||||||
import org.openautonomousconnection.protocol.side.web.managers.AuthManager;
|
import org.openautonomousconnection.protocol.packets.v1_0_0.beta.WebRequestPacket;
|
||||||
import org.openautonomousconnection.protocol.side.web.managers.RuleManager;
|
import org.openautonomousconnection.protocol.packets.v1_0_0.beta.WebResponsePacket;
|
||||||
import org.openautonomousconnection.protocol.side.web.managers.SessionManager;
|
|
||||||
import org.openautonomousconnection.protocol.versions.ProtocolVersion;
|
import org.openautonomousconnection.protocol.versions.ProtocolVersion;
|
||||||
|
|
||||||
import javax.net.ssl.SSLSocket;
|
import javax.net.ssl.SSLSocket;
|
||||||
import java.io.*;
|
import java.io.IOException;
|
||||||
import java.net.URLDecoder;
|
import java.io.ObjectInputStream;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.io.ObjectOutputStream;
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a connected web client.
|
* A connected web client using pure OAC packets.
|
||||||
* Manages the connection, handles HTTP requests, and serves files.
|
* No HTTP, no GET/POST, no header parsing.
|
||||||
*/
|
*/
|
||||||
@ProtocolInfo(protocolSide = ProtocolVersion.ProtocolSide.WEB)
|
|
||||||
public final class ConnectedWebClient {
|
public final class ConnectedWebClient {
|
||||||
|
|
||||||
/**
|
|
||||||
* The connection handler associated with this web client.
|
|
||||||
*/
|
|
||||||
@Getter
|
@Getter
|
||||||
private final ConnectionHandler pipelineConnection;
|
private final ConnectionHandler pipelineConnection;
|
||||||
|
|
||||||
/**
|
|
||||||
* The SSL socket for the web client connection.
|
|
||||||
*/
|
|
||||||
@Getter
|
@Getter
|
||||||
private SSLSocket webSocket;
|
private SSLSocket webSocket;
|
||||||
|
|
||||||
/**
|
@Getter
|
||||||
* The output stream for sending data to the client.
|
private ProtocolWebServer server;
|
||||||
*/
|
|
||||||
private ObjectOutputStream outputStream;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The input stream for receiving data from the client.
|
|
||||||
*/
|
|
||||||
private ObjectInputStream inputStream;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The protocol version of the connected client.
|
* The protocol version of the connected client.
|
||||||
@@ -55,418 +37,76 @@ public final class ConnectedWebClient {
|
|||||||
*/
|
*/
|
||||||
@Getter
|
@Getter
|
||||||
private boolean clientVersionLoaded = false;
|
private boolean clientVersionLoaded = false;
|
||||||
/**
|
|
||||||
* The reference to the ProtocolWebServer Object
|
|
||||||
*/
|
|
||||||
@Getter
|
|
||||||
private ProtocolWebServer protocolWebServer;
|
|
||||||
|
|
||||||
/**
|
private Thread receiveThread;
|
||||||
* Constructs a ConnectedWebClient with the given connection handler.
|
|
||||||
*
|
|
||||||
* @param pipelineConnection The connection handler for the web client.
|
|
||||||
*/
|
|
||||||
public ConnectedWebClient(ConnectionHandler pipelineConnection) {
|
public ConnectedWebClient(ConnectionHandler pipelineConnection) {
|
||||||
this.pipelineConnection = pipelineConnection;
|
this.pipelineConnection = pipelineConnection;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public void attachWebServer(SSLSocket socket, ProtocolWebServer server) {
|
||||||
* Sends an HTTP redirect response to the client.
|
if (this.webSocket != null) return;
|
||||||
*
|
|
||||||
* @param out The output stream to send the response to.
|
|
||||||
* @param location The URL to redirect to.
|
|
||||||
* @param cookies Optional cookies to set in the response.
|
|
||||||
* @throws IOException If an I/O error occurs.
|
|
||||||
*/
|
|
||||||
private static void sendRedirect(OutputStream out, String location, Map<String, String> cookies) throws IOException {
|
|
||||||
// Send HTTP 302 Found response with Location header
|
|
||||||
out.write(("OAC 302 Found\r\n").getBytes());
|
|
||||||
out.write(("Location: " + location + "\r\n").getBytes());
|
|
||||||
|
|
||||||
// Set cookies if provided
|
this.webSocket = socket;
|
||||||
if (cookies != null) {
|
this.server = server;
|
||||||
for (var entry : cookies.entrySet()) {
|
|
||||||
out.write((entry.getKey() + ": " + entry.getValue() + "\r\n").getBytes());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// End of headers
|
this.receiveThread = new Thread(this::receiveLoop, "OAC-WebClient-Receiver");
|
||||||
out.write("\r\n".getBytes());
|
|
||||||
out.flush();
|
|
||||||
} /**
|
|
||||||
* Thread for receiving data from the client.
|
|
||||||
*/
|
|
||||||
private final Thread receiveThread = new Thread(this::receive);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses POST parameters from the input stream.
|
|
||||||
*
|
|
||||||
* @param in The input stream to read from.
|
|
||||||
* @return A map of POST parameter names to values.
|
|
||||||
* @throws IOException If an I/O error occurs.
|
|
||||||
*/
|
|
||||||
private static Map<String, String> parsePostParams(InputStream in) throws IOException {
|
|
||||||
// Read the entire input stream into a string
|
|
||||||
BufferedReader reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
while (reader.ready()) {
|
|
||||||
sb.append((char) reader.read());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Split the string into key-value pairs and decode them
|
|
||||||
Map<String, String> map = new HashMap<>();
|
|
||||||
String[] pairs = sb.toString().split("&");
|
|
||||||
for (String p : pairs) {
|
|
||||||
// Split each pair into key and value
|
|
||||||
String[] kv = p.split("=", 2);
|
|
||||||
if (kv.length == 2)
|
|
||||||
// Decode and store in the map
|
|
||||||
map.put(URLDecoder.decode(kv[0], StandardCharsets.UTF_8), URLDecoder.decode(kv[1], StandardCharsets.UTF_8));
|
|
||||||
}
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Normalizes a file path to prevent directory traversal attacks.
|
|
||||||
*
|
|
||||||
* @param path The raw file path.
|
|
||||||
* @return The normalized file path.
|
|
||||||
*/
|
|
||||||
private static String normalizePath(String path) {
|
|
||||||
// Replace backslashes with forward slashes and remove ".." segments
|
|
||||||
path = path.replace("/", File.separator).replace("\\", "/");
|
|
||||||
// Remove any ".." segments to prevent directory traversal
|
|
||||||
while (path.contains("..")) path = path.replace("..", "");
|
|
||||||
|
|
||||||
// Remove leading slashes
|
|
||||||
if (path.startsWith("/")) path = path.substring(1);
|
|
||||||
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses query parameters from a raw URL path.
|
|
||||||
*
|
|
||||||
* @param rawPath The raw URL path containing query parameters.
|
|
||||||
* @return A map of query parameter names to values.
|
|
||||||
*/
|
|
||||||
private static Map<String, String> parseQueryParams(String rawPath) {
|
|
||||||
// Extract query parameters from the URL path
|
|
||||||
Map<String, String> map = new HashMap<>();
|
|
||||||
if (rawPath.contains("?")) {
|
|
||||||
// Split the query string into key-value pairs
|
|
||||||
String[] params = rawPath.substring(rawPath.indexOf("?") + 1).split("&");
|
|
||||||
for (String p : params) {
|
|
||||||
// Split each pair into key and value
|
|
||||||
String[] kv = p.split("=");
|
|
||||||
if (kv.length == 2) map.put(kv[0], kv[1]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the request is a multipart/form-data request.
|
|
||||||
*
|
|
||||||
* @param headers The HTTP headers of the request.
|
|
||||||
* @return True if the request is multipart/form-data, false otherwise.
|
|
||||||
*/
|
|
||||||
private static boolean isMultipart(Map<String, String> headers) {
|
|
||||||
String contentType = headers.get("content-type");
|
|
||||||
return contentType != null && contentType.startsWith("multipart/form-data");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles a multipart/form-data request, saving uploaded files to the specified directory.
|
|
||||||
*
|
|
||||||
* @param in The input stream to read the request body from.
|
|
||||||
* @param headers The HTTP headers of the request.
|
|
||||||
* @param uploadDir The directory to save uploaded files to.
|
|
||||||
* @throws IOException If an I/O error occurs.
|
|
||||||
*/
|
|
||||||
private static void handleMultipart(InputStream in, Map<String, String> headers, File uploadDir) throws IOException {
|
|
||||||
// Ensure the upload directory exists
|
|
||||||
if (!uploadDir.exists()) uploadDir.mkdirs();
|
|
||||||
|
|
||||||
// Extract the boundary from the Content-Type header
|
|
||||||
String contentType = headers.get("content-type");
|
|
||||||
String boundary = "--" + contentType.split("boundary=")[1];
|
|
||||||
|
|
||||||
// Read the entire request body into a buffer
|
|
||||||
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
|
|
||||||
byte[] lineBuffer = new byte[8192];
|
|
||||||
int read;
|
|
||||||
while ((read = in.read(lineBuffer)) != -1) {
|
|
||||||
buffer.write(lineBuffer, 0, read);
|
|
||||||
if (buffer.size() > 10 * 1024 * 1024) break; // 10 MB max
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse the multipart data
|
|
||||||
String data = buffer.toString(StandardCharsets.UTF_8);
|
|
||||||
String[] parts = data.split(boundary);
|
|
||||||
|
|
||||||
// Process each part
|
|
||||||
for (String part : parts) {
|
|
||||||
if (part.contains("Content-Disposition")) {
|
|
||||||
String name = null;
|
|
||||||
String filename = null;
|
|
||||||
|
|
||||||
// Extract headers from the part
|
|
||||||
for (String headerLine : part.split("\r\n")) {
|
|
||||||
if (headerLine.startsWith("Content-Disposition")) {
|
|
||||||
if (headerLine.contains("filename=\"")) {
|
|
||||||
int start = headerLine.indexOf("filename=\"") + 10;
|
|
||||||
int end = headerLine.indexOf("\"", start);
|
|
||||||
filename = headerLine.substring(start, end);
|
|
||||||
}
|
|
||||||
if (headerLine.contains("name=\"")) {
|
|
||||||
int start = headerLine.indexOf("name=\"") + 6;
|
|
||||||
int end = headerLine.indexOf("\"", start);
|
|
||||||
name = headerLine.substring(start, end);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save the file if a filename is provided
|
|
||||||
if (filename != null && !filename.isEmpty()) {
|
|
||||||
int headerEnd = part.indexOf("\r\n\r\n");
|
|
||||||
byte[] fileData = part.substring(headerEnd + 4).getBytes(StandardCharsets.UTF_8);
|
|
||||||
File outFile = new File(uploadDir, filename);
|
|
||||||
Files.write(outFile.toPath(), fileData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends an response to the client.
|
|
||||||
*
|
|
||||||
* @param out
|
|
||||||
* @param code
|
|
||||||
* @param file
|
|
||||||
* @param headers
|
|
||||||
* @throws IOException
|
|
||||||
*/
|
|
||||||
private static void sendResponse(OutputStream out, int code, File file, Map<String, String> headers) throws IOException {
|
|
||||||
byte[] body = Files.readAllBytes(file.toPath());
|
|
||||||
sendResponse(out, code, body, "text/html", headers);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends an response to the client.
|
|
||||||
*
|
|
||||||
* @param out The output stream to send the response to.
|
|
||||||
* @param code The HTTP status code.
|
|
||||||
* @param file The file to read the response body from.
|
|
||||||
* @throws IOException If an I/O error occurs.
|
|
||||||
*/
|
|
||||||
private static void sendResponse(OutputStream out, int code, File file) throws IOException {
|
|
||||||
sendResponse(out, code, Files.readString(file.toPath()), "text/html");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends an response to the client.
|
|
||||||
*
|
|
||||||
* @param out The output stream to send the response to.
|
|
||||||
* @param code The HTTP status code.
|
|
||||||
* @param body The response body as a string.
|
|
||||||
* @param contentType The content type of the response.
|
|
||||||
* @throws IOException If an I/O error occurs.
|
|
||||||
*/
|
|
||||||
private static void sendResponse(OutputStream out, int code, String body, String contentType) throws IOException {
|
|
||||||
sendResponse(out, code, body.getBytes(StandardCharsets.UTF_8), contentType, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends an response to the client.
|
|
||||||
*
|
|
||||||
* @param out The output stream to send the response to.
|
|
||||||
* @param code The HTTP status code.
|
|
||||||
* @param file The file to read the response body from.
|
|
||||||
* @param contentType The content type of the response.
|
|
||||||
* @throws IOException If an I/O error occurs.
|
|
||||||
*/
|
|
||||||
private static void sendResponse(OutputStream out, int code, File file, String contentType) throws IOException {
|
|
||||||
byte[] bytes = Files.readAllBytes(file.toPath());
|
|
||||||
sendResponse(out, code, bytes, contentType, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends an response to the client.
|
|
||||||
*
|
|
||||||
* @param out The output stream to send the response to.
|
|
||||||
* @param code The HTTP status code.
|
|
||||||
* @param body The response body as a byte array.
|
|
||||||
* @param contentType The content type of the response.
|
|
||||||
* @param headers Additional headers to include in the response.
|
|
||||||
* @throws IOException If an I/O error occurs.
|
|
||||||
*/
|
|
||||||
private static void sendResponse(OutputStream out, int code, byte[] body, String contentType, Map<String, String> headers) throws IOException {
|
|
||||||
// Send response status line and headers
|
|
||||||
out.write(("OAC " + code + " " + getStatusText(code) + "\r\n").getBytes());
|
|
||||||
out.write(("Content-Type: " + contentType + "\r\n").getBytes());
|
|
||||||
out.write(("Content-Length: " + body.length + "\r\n").getBytes());
|
|
||||||
|
|
||||||
// Write additional headers if provided
|
|
||||||
if (headers != null) headers.forEach((k, v) -> {
|
|
||||||
try {
|
|
||||||
out.write((k + ": " + v + "\r\n").getBytes());
|
|
||||||
} catch (IOException ignored) {
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// End of headers
|
|
||||||
out.write("\r\n".getBytes());
|
|
||||||
out.write(body);
|
|
||||||
out.flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the standard status text for a given status code.
|
|
||||||
*
|
|
||||||
* @param code The status code.
|
|
||||||
* @return The corresponding status text.
|
|
||||||
*/
|
|
||||||
private static String getStatusText(int code) {
|
|
||||||
return switch (code) {
|
|
||||||
case 200 -> "OK";
|
|
||||||
case 301 -> "Moved Permanently";
|
|
||||||
case 302 -> "Found";
|
|
||||||
case 400 -> "Bad Request";
|
|
||||||
case 401 -> "Unauthorized";
|
|
||||||
case 403 -> "Forbidden";
|
|
||||||
case 404 -> "Not Found";
|
|
||||||
case 500 -> "Internal Server Error";
|
|
||||||
default -> "Unknown";
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the content type based on the file extension.
|
|
||||||
*
|
|
||||||
* @param name The file name.
|
|
||||||
* @return The corresponding content type.
|
|
||||||
*/
|
|
||||||
private static String getContentType(String name) {
|
|
||||||
return switch (name.substring(name.lastIndexOf('.') + 1).toLowerCase()) {
|
|
||||||
case "html", "php" -> "text/html";
|
|
||||||
case "js" -> "text/javascript";
|
|
||||||
case "css" -> "text/css";
|
|
||||||
case "json" -> "application/json";
|
|
||||||
case "png" -> "image/png";
|
|
||||||
case "jpg", "jpeg" -> "image/jpeg";
|
|
||||||
case "mp4" -> "video/mp4";
|
|
||||||
case "mp3" -> "audio/mpeg3";
|
|
||||||
case "wav" -> "audio/wav";
|
|
||||||
case "pdf" -> "application/pdf";
|
|
||||||
default -> "text/plain";
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Renders a PHP file by executing it with the PHP interpreter and captures cookies.
|
|
||||||
*
|
|
||||||
* @param file The PHP file to render.
|
|
||||||
* @return A PHPResponse containing the output and cookies.
|
|
||||||
* @throws IOException If an I/O error occurs.
|
|
||||||
* @throws InterruptedException If the process is interrupted.
|
|
||||||
*/
|
|
||||||
private static PHPResponse renderPHPWithCookies(File file) throws IOException, InterruptedException {
|
|
||||||
// Execute the PHP file using the PHP interpreter
|
|
||||||
ProcessBuilder pb = new ProcessBuilder("php", file.getAbsolutePath());
|
|
||||||
pb.redirectErrorStream(true);
|
|
||||||
Process p = pb.start();
|
|
||||||
|
|
||||||
// Capture the output of the PHP process
|
|
||||||
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
|
||||||
InputStream processIn = p.getInputStream();
|
|
||||||
byte[] buf = new byte[8192];
|
|
||||||
int read;
|
|
||||||
while ((read = processIn.read(buf)) != -1) {
|
|
||||||
output.write(buf, 0, read);
|
|
||||||
}
|
|
||||||
p.waitFor();
|
|
||||||
|
|
||||||
// Parse the output to separate headers and body, and extract cookies
|
|
||||||
String fullOutput = output.toString(StandardCharsets.UTF_8);
|
|
||||||
Map<String, String> cookies = new HashMap<>();
|
|
||||||
|
|
||||||
// Split headers and body
|
|
||||||
String[] parts = fullOutput.split("\r\n\r\n", 2);
|
|
||||||
String body;
|
|
||||||
if (parts.length == 2) {
|
|
||||||
// Get headers and body
|
|
||||||
String headers = parts[0];
|
|
||||||
body = parts[1];
|
|
||||||
|
|
||||||
// Extract cookies from headers
|
|
||||||
for (String headerLine : headers.split("\r\n")) {
|
|
||||||
if (headerLine.toLowerCase().startsWith("set-cookie:")) {
|
|
||||||
String cookie = headerLine.substring("set-cookie:".length()).trim();
|
|
||||||
String[] kv = cookie.split(";", 2);
|
|
||||||
String[] pair = kv[0].split("=", 2);
|
|
||||||
if (pair.length == 2) cookies.put(pair[0], pair[1]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// No headers, only body
|
|
||||||
body = fullOutput;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new PHPResponse(body, cookies);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the SSL socket for the web client and starts the receive thread.
|
|
||||||
*
|
|
||||||
* @param webSocket The SSL socket to set.
|
|
||||||
*/
|
|
||||||
public void setWebSocket(SSLSocket webSocket) {
|
|
||||||
if (webSocket != null) this.webSocket = webSocket;
|
|
||||||
this.receiveThread.start();
|
this.receiveThread.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private void receiveLoop() {
|
||||||
* Checks if the web client is currently connected.
|
try {
|
||||||
*
|
ObjectInputStream in = new ObjectInputStream(webSocket.getInputStream());
|
||||||
* @return True if connected, false otherwise.
|
ObjectOutputStream out = new ObjectOutputStream(webSocket.getOutputStream());
|
||||||
*/
|
|
||||||
|
PacketHandler handler = server.getProtocolBridge().getProtocolSettings().packetHandler;
|
||||||
|
|
||||||
|
while (!webSocket.isClosed() && pipelineConnection.isConnected()) {
|
||||||
|
|
||||||
|
Object obj = in.readObject();
|
||||||
|
if (!(obj instanceof OACPacket packet)) continue;
|
||||||
|
|
||||||
|
if (packet instanceof WebRequestPacket requestPacket) {
|
||||||
|
|
||||||
|
WebResponsePacket response =
|
||||||
|
server.onWebRequest(this, requestPacket);
|
||||||
|
|
||||||
|
if (response != null) {
|
||||||
|
out.writeObject(response);
|
||||||
|
out.flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
} finally {
|
||||||
|
disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isConnected() {
|
public boolean isConnected() {
|
||||||
return this.webSocket != null && this.webSocket.isConnected() && !this.webSocket.isClosed() && this.receiveThread.isAlive() && pipelineConnection.isConnected();
|
return webSocket != null &&
|
||||||
|
webSocket.isConnected() &&
|
||||||
|
!webSocket.isClosed() &&
|
||||||
|
pipelineConnection.isConnected();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public synchronized void disconnect() {
|
||||||
* Disconnects the web client, closing streams and the socket.
|
try {
|
||||||
*
|
server.onDisconnect(this);
|
||||||
* @return True if disconnection was successful, false if already disconnected.
|
clientVersionLoaded = false;
|
||||||
*/
|
clientVersion = null;
|
||||||
public synchronized boolean disconnect() {
|
} catch (Exception ignored) {}
|
||||||
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 {
|
try {
|
||||||
// Close streams and the socket
|
pipelineConnection.disconnect();
|
||||||
this.outputStream.close();
|
} catch (Exception ignored) {}
|
||||||
this.inputStream.close();
|
|
||||||
this.webSocket.close();
|
|
||||||
} catch (IOException var2) {
|
|
||||||
}
|
|
||||||
|
|
||||||
// Nullify references
|
try {
|
||||||
this.webSocket = null;
|
if (webSocket != null) webSocket.close();
|
||||||
this.outputStream = null;
|
} catch (IOException ignored) {}
|
||||||
this.inputStream = null;
|
|
||||||
|
|
||||||
return true;
|
webSocket = null;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -484,8 +124,11 @@ public final class ConnectedWebClient {
|
|||||||
* @param clientVersion The protocol version to set.
|
* @param clientVersion The protocol version to set.
|
||||||
*/
|
*/
|
||||||
public void setClientVersion(ProtocolVersion clientVersion) {
|
public void setClientVersion(ProtocolVersion clientVersion) {
|
||||||
if (!clientVersionLoaded) clientVersionLoaded = true;
|
if (clientVersionLoaded) return;
|
||||||
if (clientVersion == null) this.clientVersion = clientVersion;
|
if (clientVersion == null) return;
|
||||||
|
|
||||||
|
this.clientVersion = clientVersion;
|
||||||
|
this.clientVersionLoaded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -607,128 +250,4 @@ public final class ConnectedWebClient {
|
|||||||
return getClientVersion().getSupportedProtocols().contains(protocol) || yes;
|
return getClientVersion().getSupportedProtocols().contains(protocol) || yes;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Receives and processes requests from the client.
|
|
||||||
* Handles authentication, file serving, and PHP rendering.
|
|
||||||
*/
|
|
||||||
private void receive() {
|
|
||||||
try {
|
|
||||||
while (this.isConnected()) {
|
|
||||||
Object received = this.inputStream.readObject();
|
|
||||||
|
|
||||||
try (InputStream in = webSocket.getInputStream(); OutputStream out = webSocket.getOutputStream()) {
|
|
||||||
BufferedReader reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
|
|
||||||
String line;
|
|
||||||
String path = "/main.html";
|
|
||||||
Map<String, String> headers = new HashMap<>();
|
|
||||||
while ((line = reader.readLine()) != null && !line.isEmpty()) {
|
|
||||||
if (line.toLowerCase().startsWith("get") || line.toLowerCase().startsWith("post")) {
|
|
||||||
path = line.split(" ")[1];
|
|
||||||
}
|
|
||||||
if (line.contains(":")) {
|
|
||||||
String[] parts = line.split(":", 2);
|
|
||||||
headers.put(parts[0].trim().toLowerCase(), parts[1].trim());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
path = URLDecoder.decode(path, StandardCharsets.UTF_8);
|
|
||||||
path = normalizePath(path);
|
|
||||||
|
|
||||||
File file = new File(protocolWebServer.getProtocolBridge().getProtocolWebServer().getContentFolder(), path);
|
|
||||||
|
|
||||||
String sessionId = null;
|
|
||||||
if (headers.containsKey("cookie")) {
|
|
||||||
for (String cookie : headers.get("cookie").split(";")) {
|
|
||||||
cookie = cookie.trim();
|
|
||||||
if (cookie.startsWith("SESSIONID=")) {
|
|
||||||
sessionId = cookie.substring("SESSIONID=".length());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!file.exists() || !file.isFile()) {
|
|
||||||
sendResponse(out, 404, new File(protocolWebServer.getProtocolBridge().getProtocolWebServer().getErrorsFolder(), "404.html"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
String clientIp = webSocket.getInetAddress().getHostAddress();
|
|
||||||
String userAgent = headers.getOrDefault("user-agent", null);
|
|
||||||
|
|
||||||
boolean loggedIn = sessionId != null && SessionManager.isValid(sessionId, clientIp, userAgent, protocolWebServer);
|
|
||||||
|
|
||||||
if (path.equals("/403-login") && headers.getOrDefault("content-type", "").startsWith("application/x-www-form-urlencoded")) {
|
|
||||||
Map<String, String> postParams = parsePostParams(in);
|
|
||||||
String login = postParams.get("login");
|
|
||||||
String password = postParams.get("password");
|
|
||||||
|
|
||||||
if (AuthManager.checkAuth(login, password)) {
|
|
||||||
String newSessionId = SessionManager.create(login, clientIp, userAgent, protocolWebServer);
|
|
||||||
Map<String, String> cookies = Map.of("Set-Cookie", "SESSIONID=" + newSessionId + "; HttpOnly; Path=/");
|
|
||||||
sendRedirect(out, "/main.html", cookies);
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
sendRedirect(out, "/403.php", null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isMultipart(headers)) {
|
|
||||||
handleMultipart(in, headers, new File(protocolWebServer.getProtocolBridge().getProtocolWebServer().getContentFolder(), "uploads"));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (RuleManager.requiresAuth(path) && !loggedIn) {
|
|
||||||
PHPResponse phpResp = renderPHPWithCookies(new File(protocolWebServer.getProtocolBridge().getProtocolWebServer().getContentFolder(), "403.php"));
|
|
||||||
sendResponse(out, 200, phpResp.body.getBytes(StandardCharsets.UTF_8), "text/html", phpResp.cookies);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (RuleManager.isDenied(path) && !RuleManager.isAllowed(path)) {
|
|
||||||
sendResponse(out, 403, new File(protocolWebServer.getProtocolBridge().getProtocolWebServer().getErrorsFolder(), "403.php"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (path.endsWith(".php")) {
|
|
||||||
PHPResponse phpResp = renderPHPWithCookies(file);
|
|
||||||
sendResponse(out, 200, phpResp.body.getBytes(StandardCharsets.UTF_8), "text/html", phpResp.cookies);
|
|
||||||
} else {
|
|
||||||
sendResponse(out, 200, Files.readAllBytes(file.toPath()), getContentType(path), null);
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
} finally {
|
|
||||||
disconnect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception var2) {
|
|
||||||
this.disconnect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set protocol bridge
|
|
||||||
*
|
|
||||||
* @param protocolWebServer The ProtocolWebServer object
|
|
||||||
*/
|
|
||||||
public void setProtocolWebServer(ProtocolWebServer protocolWebServer) {
|
|
||||||
if (this.protocolWebServer == null) this.protocolWebServer = protocolWebServer;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents the response from a PHP script, including body and cookies.
|
|
||||||
*/
|
|
||||||
private static class PHPResponse {
|
|
||||||
String body;
|
|
||||||
Map<String, String> cookies;
|
|
||||||
|
|
||||||
public PHPResponse(String body, Map<String, String> cookies) {
|
|
||||||
this.body = body;
|
|
||||||
this.cookies = cookies;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import dev.unlegitdqrk.unlegitlibrary.string.RandomString;
|
|||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import org.openautonomousconnection.protocol.ProtocolBridge;
|
import org.openautonomousconnection.protocol.ProtocolBridge;
|
||||||
import org.openautonomousconnection.protocol.annotations.ProtocolInfo;
|
import org.openautonomousconnection.protocol.annotations.ProtocolInfo;
|
||||||
|
import org.openautonomousconnection.protocol.packets.v1_0_0.beta.WebRequestPacket;
|
||||||
|
import org.openautonomousconnection.protocol.packets.v1_0_0.beta.WebResponsePacket;
|
||||||
import org.openautonomousconnection.protocol.side.web.managers.AuthManager;
|
import org.openautonomousconnection.protocol.side.web.managers.AuthManager;
|
||||||
import org.openautonomousconnection.protocol.side.web.managers.RuleManager;
|
import org.openautonomousconnection.protocol.side.web.managers.RuleManager;
|
||||||
import org.openautonomousconnection.protocol.versions.ProtocolVersion;
|
import org.openautonomousconnection.protocol.versions.ProtocolVersion;
|
||||||
@@ -26,7 +28,7 @@ import java.util.Random;
|
|||||||
* Represents the web server for the protocol.
|
* Represents the web server for the protocol.
|
||||||
*/
|
*/
|
||||||
@ProtocolInfo(protocolSide = ProtocolVersion.ProtocolSide.WEB)
|
@ProtocolInfo(protocolSide = ProtocolVersion.ProtocolSide.WEB)
|
||||||
public final class ProtocolWebServer {
|
public abstract class ProtocolWebServer {
|
||||||
/**
|
/**
|
||||||
* Folder for web content.
|
* Folder for web content.
|
||||||
*/
|
*/
|
||||||
@@ -193,7 +195,7 @@ public final class ProtocolWebServer {
|
|||||||
* @param clientID The client ID to search for.
|
* @param clientID The client ID to search for.
|
||||||
* @return The connected web client with the specified ID, or null if not found.
|
* @return The connected web client with the specified ID, or null if not found.
|
||||||
*/
|
*/
|
||||||
public ConnectedWebClient getClientByID(int clientID) {
|
public final ConnectedWebClient getClientByID(int clientID) {
|
||||||
for (ConnectedWebClient client : clients)
|
for (ConnectedWebClient client : clients)
|
||||||
if (client.getPipelineConnection().getClientID() == clientID) return client;
|
if (client.getPipelineConnection().getClientID() == clientID) return client;
|
||||||
return null;
|
return null;
|
||||||
@@ -204,7 +206,7 @@ public final class ProtocolWebServer {
|
|||||||
*
|
*
|
||||||
* @throws Exception If an error occurs while starting the server.
|
* @throws Exception If an error occurs while starting the server.
|
||||||
*/
|
*/
|
||||||
public void startWebServer() throws Exception {
|
public final void startWebServer() throws Exception {
|
||||||
// Start the pipeline server
|
// Start the pipeline server
|
||||||
pipelineServer.start();
|
pipelineServer.start();
|
||||||
|
|
||||||
@@ -249,8 +251,7 @@ public final class ProtocolWebServer {
|
|||||||
for (ConnectedWebClient connectedWebClient : clients) {
|
for (ConnectedWebClient connectedWebClient : clients) {
|
||||||
if (connectedWebClient.getPipelineConnection().getClientID() != -1 && connectedWebClient.isClientVersionLoaded()) {
|
if (connectedWebClient.getPipelineConnection().getClientID() != -1 && connectedWebClient.isClientVersionLoaded()) {
|
||||||
// Assign socket to an existing connected client
|
// Assign socket to an existing connected client
|
||||||
connectedWebClient.setWebSocket(client);
|
connectedWebClient.attachWebServer(client, this);
|
||||||
connectedWebClient.setProtocolWebServer(this);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
@@ -260,6 +261,21 @@ public final class ProtocolWebServer {
|
|||||||
}).start();
|
}).start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional callback when the connection closes.
|
||||||
|
*/
|
||||||
|
public void onDisconnect(ConnectedWebClient client) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the server receives a WebRequestPacket from the client.
|
||||||
|
*
|
||||||
|
* @param client The connected web client (pipeline + web socket).
|
||||||
|
* @param request The full decoded request packet.
|
||||||
|
* @return The response packet that should be sent back to the client.
|
||||||
|
*/
|
||||||
|
public abstract WebResponsePacket onWebRequest(ConnectedWebClient client, WebRequestPacket request);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles the shutdown of the web server when the pipeline server stops.
|
* Handles the shutdown of the web server when the pipeline server stops.
|
||||||
*
|
*
|
||||||
@@ -308,7 +324,7 @@ public final class ProtocolWebServer {
|
|||||||
* @return The configuration manager.
|
* @return The configuration manager.
|
||||||
* @throws IOException If an I/O error occurs while loading or saving the configuration.
|
* @throws IOException If an I/O error occurs while loading or saving the configuration.
|
||||||
*/
|
*/
|
||||||
public ConfigurationManager getConfigurationManager() throws IOException {
|
public final ConfigurationManager getConfigurationManager() throws IOException {
|
||||||
return getConfigurationManager(configFile);
|
return getConfigurationManager(configFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package org.openautonomousconnection.protocol.versions.v1_0_0.beta;
|
||||||
|
|
||||||
|
public enum WebRequestMethod {
|
||||||
|
GET, POST, EXECUTE, SCRIPT
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user