diff --git a/dependency-reduced-pom.xml b/dependency-reduced-pom.xml index 49d0442..0d456eb 100644 --- a/dependency-reduced-pom.xml +++ b/dependency-reduced-pom.xml @@ -3,7 +3,7 @@ 4.0.0 org.openautonomousconnection WebServer - 1.0.0-BETA.1.8 + 1.0.0-BETA.1.9 The default DNS-Server https://open-autonomous-connection.org/ diff --git a/pom.xml b/pom.xml index 52e39dc..e92d580 100644 --- a/pom.xml +++ b/pom.xml @@ -112,7 +112,7 @@ org.openautonomousconnection Protocol - 1.0.0-BETA.7.6 + 1.0.0-BETA.7.7 org.projectlombok diff --git a/src/main/java/org/openautonomousconnection/webserver/Main.java b/src/main/java/org/openautonomousconnection/webserver/Main.java index 51ad5c7..3bca1e1 100644 --- a/src/main/java/org/openautonomousconnection/webserver/Main.java +++ b/src/main/java/org/openautonomousconnection/webserver/Main.java @@ -3,17 +3,26 @@ 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.EventListener; import dev.unlegitdqrk.unlegitlibrary.event.EventManager; +import dev.unlegitdqrk.unlegitlibrary.event.Listener; import dev.unlegitdqrk.unlegitlibrary.file.ConfigurationManager; +import dev.unlegitdqrk.unlegitlibrary.network.system.client.events.state.ClientConnectedEvent; import dev.unlegitdqrk.unlegitlibrary.network.system.packets.PacketHandler; +import dev.unlegitdqrk.unlegitlibrary.network.system.server.NetworkServer; +import dev.unlegitdqrk.unlegitlibrary.network.system.server.events.packets.S_PacketSendEvent; +import dev.unlegitdqrk.unlegitlibrary.network.system.server.events.state.ServerStoppedEvent; import dev.unlegitdqrk.unlegitlibrary.network.system.utils.ClientAuthMode; import lombok.Getter; import org.openautonomousconnection.protocol.ProtocolBridge; import org.openautonomousconnection.protocol.ProtocolValues; +import org.openautonomousconnection.protocol.side.server.events.S_CustomClientConnectedEvent; import org.openautonomousconnection.protocol.versions.ProtocolVersion; import org.openautonomousconnection.webserver.commands.StopCommand; import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.Scanner; public class Main { @@ -35,6 +44,8 @@ public class Main { values.packetHandler = new PacketHandler(); values.eventManager = new EventManager(); + if (!Files.exists(new File("config.properties").toPath())) Files.createFile(new File("config.properties").toPath()); + ConfigurationManager config = new ConfigurationManager(new File("config.properties")); config.loadProperties(); @@ -85,11 +96,12 @@ public class Main { Scanner scanner = new Scanner(System.in); - while (true) { - System.out.print(commandExecutor.getName() + "> "); - String line = scanner.nextLine(); - commandManager.execute(commandExecutor, line); - } - +// while (protocolBridge.getProtocolServer().getNetwork().isServerOnline()) { +// System.out.print(commandExecutor.getName() + "> "); +// String line = scanner.nextLine(); +// commandManager.execute(commandExecutor, line); +// } +// +// System.exit(0); } } \ 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 index 4a2da0f..2d49274 100644 --- a/src/main/java/org/openautonomousconnection/webserver/WebServer.java +++ b/src/main/java/org/openautonomousconnection/webserver/WebServer.java @@ -1,5 +1,7 @@ package org.openautonomousconnection.webserver; +import dev.unlegitdqrk.unlegitlibrary.event.Listener; +import dev.unlegitdqrk.unlegitlibrary.network.system.server.events.packets.S_PacketReadEvent; import dev.unlegitdqrk.unlegitlibrary.network.system.utils.TransportProtocol; import lombok.Getter; import org.openautonomousconnection.protocol.annotations.ProtocolInfo; @@ -14,19 +16,34 @@ import org.openautonomousconnection.protocol.side.web.managers.RuleManager; import org.openautonomousconnection.protocol.versions.ProtocolVersion; import org.openautonomousconnection.webserver.api.SessionContext; import org.openautonomousconnection.webserver.runtime.JavaPageDispatcher; +import org.openautonomousconnection.webserver.utils.HeaderMaps; import org.openautonomousconnection.webserver.utils.WebHasher; import java.io.*; import java.nio.file.Files; import java.util.Arrays; import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +/** + * OAC WebServer implementation. + */ @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; + /** + * Dedicated executor for streaming to avoid blocking network receive thread. + */ + private static final ExecutorService STREAM_EXECUTOR = Executors.newCachedThreadPool(r -> { + Thread t = new Thread(r, "oac-web-stream"); + t.setDaemon(true); + return t; + }); + @Getter private final WebHasher hasher; @@ -38,28 +55,27 @@ public final class WebServer extends ProtocolWebServer { ) throws Exception { super(authFile, rulesFile, sessionExpire, maxUpload); - // NOTE: Values chosen as safe defaults. - // move them to Main and pass them in here (no hidden assumptions). this.hasher = new WebHasher( - 120_000, // PBKDF2 iterations - 16, // salt bytes - 32 // key bytes + 120_000, + 16, + 32 ); } @Override public WebResponsePacket onWebRequest(CustomConnectedClient client, WebRequestPacket request) { + try { String path = request.getPath() == null ? "/" : request.getPath(); if (RuleManager.isDenied(path)) { - return new WebResponsePacket(403, "text/plain; charset=utf-8", Map.of(), "Forbidden".getBytes()); + return new WebResponsePacket(403, "text/plain; charset=utf-8", HeaderMaps.mutable(), "Forbidden".getBytes()); } if (RuleManager.requiresAuth(path)) { SessionContext ctx = SessionContext.from(client, this, request.getHeaders()); if (!ctx.isValid()) { - return new WebResponsePacket(401, "text/plain; charset=utf-8", Map.of(), "Authentication required".getBytes()); + return new WebResponsePacket(401, "text/plain; charset=utf-8", HeaderMaps.mutable(), "Authentication required".getBytes()); } } @@ -69,8 +85,12 @@ public final class WebServer extends ProtocolWebServer { return serveFile(client, path); } catch (Exception e) { - return new WebResponsePacket(500, "text/plain; charset=utf-8", Map.of(), - ("Internal Error: " + e.getMessage()).getBytes()); + return new WebResponsePacket( + 500, + "text/plain; charset=utf-8", + HeaderMaps.mutable(), + ("Internal Error: " + e.getClass().getName() + ": " + e.getMessage()).getBytes() + ); } } @@ -81,34 +101,64 @@ public final class WebServer extends ProtocolWebServer { File root = getContentFolder().getCanonicalFile(); File file = new File(root, path).getCanonicalFile(); - if (!file.getPath().startsWith(root.getPath())) - return new WebResponsePacket(403, "text/plain; charset=utf-8", Map.of(), "Forbidden".getBytes()); - if (!file.exists() || !file.isFile()) - return new WebResponsePacket(404, "text/plain; charset=utf-8", Map.of(), "Not found".getBytes()); + if (!file.getPath().startsWith(root.getPath())) { + return new WebResponsePacket(403, "text/plain; charset=utf-8", HeaderMaps.mutable(), "Forbidden".getBytes()); + } + if (!file.exists() || !file.isFile()) { + return new WebResponsePacket(404, "text/plain; charset=utf-8", HeaderMaps.mutable(), "Not found".getBytes()); + } String contentType = ContentTypeResolver.resolve(file.getName()); long size = file.length(); if (size >= STREAM_THRESHOLD) { - streamFile(client, file, contentType); - return null; + startStreamingAsync(client, file, contentType); + + // IMPORTANT: Never return null. If your client expects a normal response, this keeps the pipeline stable. + // The actual bytes are delivered via WebStream* packets. + return new WebResponsePacket( + 202, + "text/plain; charset=utf-8", + HeaderMaps.mutable(Map.of("x-oac-stream", "1")), + "Streaming started".getBytes() + ); } byte[] data = Files.readAllBytes(file.toPath()); - return new WebResponsePacket(200, contentType, Map.of(), data); + return new WebResponsePacket(200, contentType, HeaderMaps.mutable(), data); + } + + private void startStreamingAsync(CustomConnectedClient client, File file, String contentType) { + STREAM_EXECUTOR.execute(() -> { + try { + streamFile(client, file, contentType); + } catch (Exception e) { + // Never let streaming errors kill your server threads. + try { + client.getConnection().sendPacket(new WebStreamEndPacket(false), TransportProtocol.TCP); + } catch (Exception ignored) { + // ignore: client may already be gone + } + } + }); } private void streamFile(CustomConnectedClient client, File file, String contentType) throws IOException, ClassNotFoundException { long total = file.length(); - client.getConnection().sendPacket(new WebStreamStartPacket(200, contentType, Map.of("name", file.getName()), total), TransportProtocol.TCP); + client.getConnection().sendPacket( + new WebStreamStartPacket(200, contentType, Map.of("name", file.getName()), total), + TransportProtocol.TCP + ); 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); + // Always copy: never hand out the reusable buffer reference. + byte[] chunk = Arrays.copyOf(buf, r); + client.getConnection().sendPacket( new WebStreamChunkPacket(seq++, chunk), ContentTypeResolver.isVideoFile(file.getName()) ? TransportProtocol.UDP : TransportProtocol.TCP @@ -118,4 +168,4 @@ public final class WebServer extends ProtocolWebServer { client.getConnection().sendPacket(new WebStreamEndPacket(true), TransportProtocol.TCP); } -} +} \ No newline at end of file diff --git a/src/main/java/org/openautonomousconnection/webserver/runtime/JavaPageDispatcher.java b/src/main/java/org/openautonomousconnection/webserver/runtime/JavaPageDispatcher.java index b7857d2..34b58ed 100644 --- a/src/main/java/org/openautonomousconnection/webserver/runtime/JavaPageDispatcher.java +++ b/src/main/java/org/openautonomousconnection/webserver/runtime/JavaPageDispatcher.java @@ -7,6 +7,7 @@ import org.openautonomousconnection.protocol.side.web.ProtocolWebServer; import org.openautonomousconnection.webserver.WebServer; import org.openautonomousconnection.webserver.api.WebPage; import org.openautonomousconnection.webserver.api.WebPageContext; +import org.openautonomousconnection.webserver.utils.HeaderMaps; import org.openautonomousconnection.webserver.utils.RequestParams; import org.openautonomousconnection.webserver.utils.WebHasher; @@ -77,7 +78,7 @@ public final class JavaPageDispatcher { return new WebResponsePacket( code, "text/plain; charset=utf-8", - Map.of(), + HeaderMaps.mutable(), msg.getBytes(StandardCharsets.UTF_8) ); } diff --git a/src/main/java/org/openautonomousconnection/webserver/utils/HeaderMaps.java b/src/main/java/org/openautonomousconnection/webserver/utils/HeaderMaps.java new file mode 100644 index 0000000..d7a2209 --- /dev/null +++ b/src/main/java/org/openautonomousconnection/webserver/utils/HeaderMaps.java @@ -0,0 +1,32 @@ +package org.openautonomousconnection.webserver.utils; + +import java.util.HashMap; +import java.util.Map; + +/** + * Small helper utilities for mutable header maps. + */ +public final class HeaderMaps { + + private HeaderMaps() { + } + + /** + * Creates an empty mutable header map. + * + * @return mutable map + */ + public static Map mutable() { + return new HashMap<>(); + } + + /** + * Creates a mutable header map initialized with the given entries. + * + * @param initial initial entries (may be null) + * @return mutable map + */ + public static Map mutable(Map initial) { + return (initial == null || initial.isEmpty()) ? new HashMap<>() : new HashMap<>(initial); + } +} \ No newline at end of file diff --git a/src/main/java/org/openautonomousconnection/webserver/utils/HttpsProxy.java b/src/main/java/org/openautonomousconnection/webserver/utils/HttpsProxy.java index c579bbc..b203c64 100644 --- a/src/main/java/org/openautonomousconnection/webserver/utils/HttpsProxy.java +++ b/src/main/java/org/openautonomousconnection/webserver/utils/HttpsProxy.java @@ -43,7 +43,7 @@ public final class HttpsProxy { return new WebResponsePacket( 502, "text/plain; charset=utf-8", - Map.of(), + HeaderMaps.mutable(), ("Bad Gateway: " + e.getClass().getName() + ": " + e.getMessage()).getBytes(StandardCharsets.UTF_8) ); } @@ -54,7 +54,7 @@ public final class HttpsProxy { return new WebResponsePacket( 508, "text/plain; charset=utf-8", - Map.of(), + HeaderMaps.mutable(), "Too many redirects".getBytes(StandardCharsets.UTF_8) ); } @@ -82,7 +82,7 @@ public final class HttpsProxy { return new WebResponsePacket( 502, "text/plain; charset=utf-8", - Map.of(), + HeaderMaps.mutable(), ("Bad Gateway: redirect without Location (code=" + code + ")").getBytes(StandardCharsets.UTF_8) ); } @@ -104,8 +104,7 @@ public final class HttpsProxy { con.disconnect(); } - Map headers = new HashMap<>(); - return new WebResponsePacket(code, contentType, headers, body); + return new WebResponsePacket(code, contentType, HeaderMaps.mutable(), body); } private static byte[] readAllBytes(InputStream in) throws Exception {