Finished up WebServer

This commit is contained in:
Finn
2025-12-12 21:16:13 +01:00
parent 4e8e8e3e82
commit 195bd58bea
16 changed files with 383 additions and 30 deletions

View File

@@ -70,7 +70,7 @@
<dependency>
<groupId>org.openautonomousconnection</groupId>
<artifactId>protocol</artifactId>
<version>1.0.0-BETA.2</version>
<version>1.0.0-BETA.4</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>

View File

@@ -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.

View File

@@ -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:

View File

@@ -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"

View File

@@ -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"

View File

@@ -1,9 +0,0 @@
package github.openautonomousconnection.webserver;
public class Main {
public static void main(String[] args) {
}
}

View File

@@ -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;
}

View File

@@ -0,0 +1,41 @@
package org.openautonomousconnection.webserver;
import java.util.Map;
public final class ContentTypeResolver {
private static final Map<String, String> 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"
);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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));
}
}

View File

@@ -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<String, String> 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; }
}

View File

@@ -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;
}

View File

@@ -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());
}
}

View File

@@ -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<String, Entry> 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;
}
}

View File

@@ -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<String> 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);
}
}

View File

@@ -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);
}
}