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