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 extends JavaFileObject> 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);
+ }
+}