From 195bd58bea422bf3d0e19ebbed60204ddec70929 Mon Sep 17 00:00:00 2001 From: Finn Date: Fri, 12 Dec 2025 21:16:13 +0100 Subject: [PATCH] Finished up WebServer --- pom.xml | 2 +- run/php/extras/ssl/openssl.cnf | 2 +- run/php/news.txt | 4 +- run/php/php.ini-development | 2 +- run/php/php.ini-production | 2 +- .../webserver/Main.java | 9 -- .../webserver/api/WebModule.java | 15 ---- .../webserver/ContentTypeResolver.java | 41 +++++++++ .../webserver/Main.java | 46 ++++++++++ .../webserver/WebServer.java | 89 +++++++++++++++++++ .../webserver/api/SessionContext.java | 47 ++++++++++ .../webserver/api/WebPage.java | 18 ++++ .../webserver/api/WebPageContext.java | 21 +++++ .../webserver/runtime/JavaPageCache.java | 36 ++++++++ .../webserver/runtime/JavaPageCompiler.java | 39 ++++++++ .../webserver/runtime/JavaPageDispatcher.java | 40 +++++++++ 16 files changed, 383 insertions(+), 30 deletions(-) delete mode 100644 src/main/java/github/openautonomousconnection/webserver/Main.java delete mode 100644 src/main/java/github/openautonomousconnection/webserver/api/WebModule.java create mode 100644 src/main/java/org/openautonomousconnection/webserver/ContentTypeResolver.java create mode 100644 src/main/java/org/openautonomousconnection/webserver/Main.java create mode 100644 src/main/java/org/openautonomousconnection/webserver/WebServer.java create mode 100644 src/main/java/org/openautonomousconnection/webserver/api/SessionContext.java create mode 100644 src/main/java/org/openautonomousconnection/webserver/api/WebPage.java create mode 100644 src/main/java/org/openautonomousconnection/webserver/api/WebPageContext.java create mode 100644 src/main/java/org/openautonomousconnection/webserver/runtime/JavaPageCache.java create mode 100644 src/main/java/org/openautonomousconnection/webserver/runtime/JavaPageCompiler.java create mode 100644 src/main/java/org/openautonomousconnection/webserver/runtime/JavaPageDispatcher.java diff --git a/pom.xml b/pom.xml index 0c00ddf..8e4e750 100644 --- a/pom.xml +++ b/pom.xml @@ -70,7 +70,7 @@ org.openautonomousconnection protocol - 1.0.0-BETA.2 + 1.0.0-BETA.4 org.projectlombok diff --git a/run/php/extras/ssl/openssl.cnf b/run/php/extras/ssl/openssl.cnf index 12bc408..b1d3bd2 100644 --- a/run/php/extras/ssl/openssl.cnf +++ b/run/php/extras/ssl/openssl.cnf @@ -82,7 +82,7 @@ default_ca = CA_default # The default ca section dir = ./demoCA # Where everything is kept certs = $dir/certs # Where the issued certs are kept crl_dir = $dir/crl # Where the issued crl are kept -database = $dir/index.txt # database index file. +database = $dir/org.openautonomousconnection.webserver.index.txt # database org.openautonomousconnection.webserver.index file. #unique_subject = no # Set to 'no' to allow creation of # several certs with same subject. new_certs_dir = $dir/newcerts # default place for new certs. diff --git a/run/php/news.txt b/run/php/news.txt index 908ce09..1606bd5 100644 --- a/run/php/news.txt +++ b/run/php/news.txt @@ -1155,7 +1155,7 @@ PHP NEWS ResourceBundle object now throw: - TypeError for invalid offset types - ValueError for an empty string - - ValueError if the integer index does not fit in a signed 32 bit integer + - ValueError if the integer org.openautonomousconnection.webserver.index does not fit in a signed 32 bit integer . ResourceBundle::get() now has a tentative return type of: ResourceBundle|array|string|int|null . Added the new Grapheme function grapheme_str_split. (youkidearitai) @@ -1441,7 +1441,7 @@ PHP NEWS . Added SOCK_NONBLOCK/SOCK_CLOEXEC constants for socket_create and socket_create_pair to apply O_NONBLOCK/O_CLOEXEC flags to the newly created sockets. (David Carlier) - . Added SO_BINDTOIFINDEX to bind a socket to an interface index. + . Added SO_BINDTOIFINDEX to bind a socket to an interface org.openautonomousconnection.webserver.index. (David Carlier) - Sodium: diff --git a/run/php/php.ini-development b/run/php/php.ini-development index 6e5064d..abcac38 100644 --- a/run/php/php.ini-development +++ b/run/php/php.ini-development @@ -1455,7 +1455,7 @@ session.trans_sid_tags = "a=href,area=href,frame=src,form=" ; https://php.net/session.upload-progress.prefix ;session.upload_progress.prefix = "upload_progress_" -; The index name (concatenated with the prefix) in $_SESSION +; The org.openautonomousconnection.webserver.index name (concatenated with the prefix) in $_SESSION ; containing the upload progress information ; Default Value: "PHP_SESSION_UPLOAD_PROGRESS" ; Development Value: "PHP_SESSION_UPLOAD_PROGRESS" diff --git a/run/php/php.ini-production b/run/php/php.ini-production index c62faf5..cc40200 100644 --- a/run/php/php.ini-production +++ b/run/php/php.ini-production @@ -1457,7 +1457,7 @@ session.trans_sid_tags = "a=href,area=href,frame=src,form=" ; https://php.net/session.upload-progress.prefix ;session.upload_progress.prefix = "upload_progress_" -; The index name (concatenated with the prefix) in $_SESSION +; The org.openautonomousconnection.webserver.index name (concatenated with the prefix) in $_SESSION ; containing the upload progress information ; Default Value: "PHP_SESSION_UPLOAD_PROGRESS" ; Development Value: "PHP_SESSION_UPLOAD_PROGRESS" diff --git a/src/main/java/github/openautonomousconnection/webserver/Main.java b/src/main/java/github/openautonomousconnection/webserver/Main.java deleted file mode 100644 index 622cebc..0000000 --- a/src/main/java/github/openautonomousconnection/webserver/Main.java +++ /dev/null @@ -1,9 +0,0 @@ -package github.openautonomousconnection.webserver; - -public class Main { - - public static void main(String[] args) { - - } - -} diff --git a/src/main/java/github/openautonomousconnection/webserver/api/WebModule.java b/src/main/java/github/openautonomousconnection/webserver/api/WebModule.java deleted file mode 100644 index 598b5a1..0000000 --- a/src/main/java/github/openautonomousconnection/webserver/api/WebModule.java +++ /dev/null @@ -1,15 +0,0 @@ -package github.openautonomousconnection.webserver.api; - -public interface WebModule { - - /** - * @return Path this module handles (e.g. "/api/user") - */ - String path(); - - /** - * Handle a web request. - */ - WebResponsePacket handle(ConnectedWebClient client, WebRequestPacket request) throws Exception; -} - diff --git a/src/main/java/org/openautonomousconnection/webserver/ContentTypeResolver.java b/src/main/java/org/openautonomousconnection/webserver/ContentTypeResolver.java new file mode 100644 index 0000000..292e089 --- /dev/null +++ b/src/main/java/org/openautonomousconnection/webserver/ContentTypeResolver.java @@ -0,0 +1,41 @@ +package org.openautonomousconnection.webserver; + +import java.util.Map; + +public final class ContentTypeResolver { + + private static final Map MAP = Map.ofEntries( + Map.entry("html", "text/html"), + Map.entry("css", "text/css"), + Map.entry("json", "application/json"), + Map.entry("xml", "application/xml"), + Map.entry("yml", "application/x-yaml"), + Map.entry("yaml", "application/x-yaml"), + Map.entry("txt", "text/plain"), + Map.entry("png", "image/png"), + Map.entry("jpg", "image/jpeg"), + Map.entry("jpeg", "image/jpeg"), + Map.entry("gif", "image/gif"), + Map.entry("svg", "image/svg+xml"), + Map.entry("mp4", "video/mp4"), + Map.entry("webm", "video/webm"), + Map.entry("mp3", "audio/mpeg"), + Map.entry("wav", "audio/wav"), + + // explicitly NOT executed + Map.entry("java", "text/plain"), + Map.entry("js", "text/plain"), + Map.entry("ts", "text/plain"), + Map.entry("php", "text/plain"), + Map.entry("py", "text/plain") + ); + + public static String resolve(String name) { + int i = name.lastIndexOf('.'); + if (i == -1) return "application/octet-stream"; + return MAP.getOrDefault( + name.substring(i + 1).toLowerCase(), + "application/octet-stream" + ); + } +} diff --git a/src/main/java/org/openautonomousconnection/webserver/Main.java b/src/main/java/org/openautonomousconnection/webserver/Main.java new file mode 100644 index 0000000..74f8a13 --- /dev/null +++ b/src/main/java/org/openautonomousconnection/webserver/Main.java @@ -0,0 +1,46 @@ +package org.openautonomousconnection.webserver; + +import dev.unlegitdqrk.unlegitlibrary.command.CommandExecutor; +import dev.unlegitdqrk.unlegitlibrary.command.CommandManager; +import dev.unlegitdqrk.unlegitlibrary.command.CommandPermission; +import dev.unlegitdqrk.unlegitlibrary.event.EventManager; +import dev.unlegitdqrk.unlegitlibrary.network.system.packets.PacketHandler; +import lombok.Getter; +import org.openautonomousconnection.protocol.ProtocolBridge; +import org.openautonomousconnection.protocol.ProtocolSettings; +import org.openautonomousconnection.protocol.side.ins.ProtocolINSServer; +import org.openautonomousconnection.protocol.versions.ProtocolVersion; + +import javax.annotation.processing.Generated; +import java.io.File; +import java.util.Scanner; + +public class Main { + private static final CommandPermission PERMISSION_ALL = new CommandPermission("all", 1); + private static final CommandExecutor commandExecutor = new CommandExecutor("DNS", PERMISSION_ALL) {}; + private static CommandManager commandManager; + + @Getter + private static ProtocolBridge protocolBridge; + + public static void main(String[] args) throws Exception { + ProtocolSettings settings = new ProtocolSettings(); + settings.packetHandler = new PacketHandler(); + settings.eventManager = new EventManager(); + settings.port = 9824; + + protocolBridge = new ProtocolBridge(new WebServer(new File("config.properties"), + new File("auth.ini"), new File("rules.ini")), + settings, ProtocolVersion.PV_1_0_0_BETA, new File("logs")); + + commandManager = new CommandManager(protocolBridge.getProtocolSettings().eventManager); + Scanner scanner = new Scanner(System.in); + + while (true) { + System.out.println(commandExecutor.getName() + "> "); + String line = scanner.nextLine(); + commandManager.execute(commandExecutor, line); + } + + } +} \ No newline at end of file diff --git a/src/main/java/org/openautonomousconnection/webserver/WebServer.java b/src/main/java/org/openautonomousconnection/webserver/WebServer.java new file mode 100644 index 0000000..f8ab427 --- /dev/null +++ b/src/main/java/org/openautonomousconnection/webserver/WebServer.java @@ -0,0 +1,89 @@ +package org.openautonomousconnection.webserver; + +import org.openautonomousconnection.protocol.annotations.ProtocolInfo; +import org.openautonomousconnection.protocol.packets.v1_0_0.beta.*; +import org.openautonomousconnection.protocol.side.web.ConnectedWebClient; +import org.openautonomousconnection.protocol.side.web.ProtocolWebServer; +import org.openautonomousconnection.protocol.side.web.managers.RuleManager; +import org.openautonomousconnection.protocol.side.web.managers.SessionManager; +import org.openautonomousconnection.protocol.versions.ProtocolVersion; +import org.openautonomousconnection.webserver.api.SessionContext; +import org.openautonomousconnection.webserver.runtime.JavaPageDispatcher; + +import java.io.*; +import java.nio.file.Files; +import java.util.*; + +@ProtocolInfo(protocolSide = ProtocolVersion.ProtocolSide.WEB) +public final class WebServer extends ProtocolWebServer { + + private static final int STREAM_CHUNK_SIZE = 64 * 1024; + private static final long STREAM_THRESHOLD = 2L * 1024 * 1024; + + public WebServer(File configFile, File authFile, File rulesFile) throws Exception { + super(configFile, authFile, rulesFile); + Main.getProtocolBridge().getProtocolSettings().port = getConfigurationManager().getInt("port.webserver"); + } + + @Override + public WebResponsePacket onWebRequest(ConnectedWebClient client, WebRequestPacket request) { + try { + String path = request.getPath() == null ? "/" : request.getPath(); + + if (RuleManager.isDenied(path)) return new WebResponsePacket(403, "text/plain", Map.of(), "Forbidden".getBytes()); + + if (RuleManager.requiresAuth(path)) { + SessionContext ctx = SessionContext.from(client, this, request.getHeaders()); + if (!ctx.isValid()) return new WebResponsePacket(401, "text/plain", Map.of(), "Authentication required".getBytes()); + } + + WebResponsePacket javaResp = JavaPageDispatcher.dispatch(client, this, request); + if (javaResp != null) return javaResp; + + return serveFile(client, path); + + } catch (Exception e) { + return new WebResponsePacket(500, "text/plain", Map.of(), ("Internal Error: " + e.getMessage()).getBytes()); + } + } + + private WebResponsePacket serveFile(ConnectedWebClient client, String path) throws Exception { + if (path.startsWith("/")) path = path.substring(1); + if (path.isEmpty()) path = "index.html"; + + File root = getContentFolder().getCanonicalFile(); + File file = new File(root, path).getCanonicalFile(); + + if (!file.getPath().startsWith(root.getPath())) return new WebResponsePacket(403, "text/plain", Map.of(), "Forbidden".getBytes()); + if (!file.exists() || !file.isFile()) return new WebResponsePacket(404, "text/plain", Map.of(), "Not found".getBytes()); + + String contentType = ContentTypeResolver.resolve(file.getName()); + long size = file.length(); + + if (size >= STREAM_THRESHOLD) { + streamFile(client, file, contentType); + return null; + } + + byte[] data = Files.readAllBytes(file.toPath()); + return new WebResponsePacket(200, contentType, Map.of(), data); + } + + private void streamFile(ConnectedWebClient client, File file, String contentType) throws IOException { + long total = file.length(); + + client.streamStart(new WebStreamStartPacket(200, contentType, Map.of("name", file.getName()), total)); + + try (InputStream in = new BufferedInputStream(new FileInputStream(file))) { + byte[] buf = new byte[STREAM_CHUNK_SIZE]; + int seq = 0; + int r; + while ((r = in.read(buf)) != -1) { + byte[] chunk = (r == buf.length) ? buf : Arrays.copyOf(buf, r); + client.streamChunk(new WebStreamChunkPacket(seq++, chunk)); + } + } + + client.streamEnd(new WebStreamEndPacket(true)); + } +} diff --git a/src/main/java/org/openautonomousconnection/webserver/api/SessionContext.java b/src/main/java/org/openautonomousconnection/webserver/api/SessionContext.java new file mode 100644 index 0000000..2208e3b --- /dev/null +++ b/src/main/java/org/openautonomousconnection/webserver/api/SessionContext.java @@ -0,0 +1,47 @@ +package org.openautonomousconnection.webserver.api; + +import org.openautonomousconnection.protocol.side.web.ConnectedWebClient; +import org.openautonomousconnection.protocol.side.web.ProtocolWebServer; +import org.openautonomousconnection.protocol.side.web.managers.SessionManager; + +import java.io.IOException; +import java.util.Map; + +/** + * Provides session-related information for Java WebPages. + * Thin layer on top of SessionManager. + */ +public final class SessionContext { + + private final String sessionId; + private final String user; + private final boolean valid; + + private SessionContext(String sessionId, String user, boolean valid) { + this.sessionId = sessionId; + this.user = user; + this.valid = valid; + } + + public static SessionContext from(ConnectedWebClient client, ProtocolWebServer server, Map headers) throws IOException { + if (headers == null) return new SessionContext(null, null, false); + + String sessionId = headers.get("session"); + if (sessionId == null) return new SessionContext(null, null, false); + + String ip = (client.getWebSocket() != null && client.getWebSocket().getInetAddress() != null) + ? client.getWebSocket().getInetAddress().getHostAddress() : ""; + + String userAgent = headers.getOrDefault("user-agent", ""); + + boolean valid = SessionManager.isValid(sessionId, ip, userAgent, server); + if (!valid) return new SessionContext(sessionId, null, false); + + String user = SessionManager.getUser(sessionId); + return new SessionContext(sessionId, user, true); + } + + public boolean isValid() { return valid; } + public String getSessionId() { return sessionId; } + public String getUser() { return user; } +} diff --git a/src/main/java/org/openautonomousconnection/webserver/api/WebPage.java b/src/main/java/org/openautonomousconnection/webserver/api/WebPage.java new file mode 100644 index 0000000..6f8d0bb --- /dev/null +++ b/src/main/java/org/openautonomousconnection/webserver/api/WebPage.java @@ -0,0 +1,18 @@ +package org.openautonomousconnection.webserver.api; + +import org.openautonomousconnection.protocol.packets.v1_0_0.beta.WebResponsePacket; + +/** + * Server-side Java page (PHP alternative). + * Every .java page must implement this interface. + */ +public interface WebPage { + + /** + * Handles a web request. + * + * @param ctx context (client, request, session) + * @return response packet + */ + WebResponsePacket handle(WebPageContext ctx) throws Exception; +} diff --git a/src/main/java/org/openautonomousconnection/webserver/api/WebPageContext.java b/src/main/java/org/openautonomousconnection/webserver/api/WebPageContext.java new file mode 100644 index 0000000..1957b52 --- /dev/null +++ b/src/main/java/org/openautonomousconnection/webserver/api/WebPageContext.java @@ -0,0 +1,21 @@ +package org.openautonomousconnection.webserver.api; + +import org.openautonomousconnection.protocol.packets.v1_0_0.beta.WebRequestPacket; +import org.openautonomousconnection.protocol.side.web.ConnectedWebClient; +import org.openautonomousconnection.protocol.side.web.ProtocolWebServer; + +/** + * Context passed to Java WebPages (client, request, session). + */ +public final class WebPageContext { + + public final ConnectedWebClient client; + public final WebRequestPacket request; + public final SessionContext session; + + public WebPageContext(ConnectedWebClient client, ProtocolWebServer server, WebRequestPacket request) throws Exception { + this.client = client; + this.request = request; + this.session = SessionContext.from(client, server, request.getHeaders()); + } +} diff --git a/src/main/java/org/openautonomousconnection/webserver/runtime/JavaPageCache.java b/src/main/java/org/openautonomousconnection/webserver/runtime/JavaPageCache.java new file mode 100644 index 0000000..a318d95 --- /dev/null +++ b/src/main/java/org/openautonomousconnection/webserver/runtime/JavaPageCache.java @@ -0,0 +1,36 @@ +package org.openautonomousconnection.webserver.runtime; + +import java.io.File; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Caches compiled Java WebPages by file lastModified timestamp. + */ +public final class JavaPageCache { + + private static final class Entry { + final long lastModified; + final Class clazz; + + Entry(long lastModified, Class clazz) { + this.lastModified = lastModified; + this.clazz = clazz; + } + } + + private final ConcurrentHashMap cache = new ConcurrentHashMap<>(); + + public Class getOrCompile(File javaFile) throws Exception { + String key = javaFile.getAbsolutePath(); + long lm = javaFile.lastModified(); + + Entry e = cache.get(key); + if (e != null && e.lastModified == lm) { + return e.clazz; + } + + Class compiled = JavaPageCompiler.compile(javaFile); + cache.put(key, new Entry(lm, compiled)); + return compiled; + } +} diff --git a/src/main/java/org/openautonomousconnection/webserver/runtime/JavaPageCompiler.java b/src/main/java/org/openautonomousconnection/webserver/runtime/JavaPageCompiler.java new file mode 100644 index 0000000..d0ea48b --- /dev/null +++ b/src/main/java/org/openautonomousconnection/webserver/runtime/JavaPageCompiler.java @@ -0,0 +1,39 @@ +package org.openautonomousconnection.webserver.runtime; + +import javax.tools.*; +import java.io.File; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.List; + +/** + * Compiles and loads Java web pages at runtime. + * + * NOTE: Requires running with a JDK (ToolProvider.getSystemJavaCompiler != null). + */ +public final class JavaPageCompiler { + + private JavaPageCompiler() {} + + public static Class compile(File javaFile) throws Exception { + JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + if (compiler == null) throw new IllegalStateException("JDK required (JavaCompiler not available)"); + + StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null); + + Iterable units = fileManager.getJavaFileObjects(javaFile); + + // Compile in-place (class next to the .java file) + List options = List.of("-classpath", System.getProperty("java.class.path")); + + boolean success = compiler.getTask(null, fileManager, null, options, null, units).call(); + fileManager.close(); + + if (!success) throw new RuntimeException("Compilation failed: " + javaFile.getName()); + + URLClassLoader cl = new URLClassLoader(new URL[]{ javaFile.getParentFile().toURI().toURL() }); + + String className = javaFile.getName().replace(".java", ""); + return cl.loadClass(className); + } +} diff --git a/src/main/java/org/openautonomousconnection/webserver/runtime/JavaPageDispatcher.java b/src/main/java/org/openautonomousconnection/webserver/runtime/JavaPageDispatcher.java new file mode 100644 index 0000000..0d865c9 --- /dev/null +++ b/src/main/java/org/openautonomousconnection/webserver/runtime/JavaPageDispatcher.java @@ -0,0 +1,40 @@ +package org.openautonomousconnection.webserver.runtime; + +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.ConnectedWebClient; +import org.openautonomousconnection.protocol.side.web.ProtocolWebServer; +import org.openautonomousconnection.webserver.api.WebPage; +import org.openautonomousconnection.webserver.api.WebPageContext; + +import java.io.File; + +public final class JavaPageDispatcher { + + private static final JavaPageCache CACHE = new JavaPageCache(); + + private JavaPageDispatcher() {} + + public static WebResponsePacket dispatch( + ConnectedWebClient client, + ProtocolWebServer server, + WebRequestPacket request + ) throws Exception { + + if (request.getPath() == null) return null; + + String p = request.getPath().startsWith("/") ? request.getPath().substring(1) : request.getPath(); + File javaFile = new File(server.getContentFolder(), p + ".java"); + + if (!javaFile.exists() || !javaFile.isFile()) return null; + + Class clazz = CACHE.getOrCompile(javaFile); + Object instance = clazz.getDeclaredConstructor().newInstance(); + + if (!(instance instanceof WebPage page)) + throw new IllegalStateException("Java page must implement WebPage"); + + WebPageContext ctx = new WebPageContext(client, server, request); + return page.handle(ctx); + } +}