From aa5963378d1b03f80c9ea17b8929e93fb32b186f Mon Sep 17 00:00:00 2001 From: UnlegitDqrk Date: Fri, 27 Feb 2026 20:31:10 +0100 Subject: [PATCH] Bug fixes --- dependency-reduced-pom.xml | 2 +- pom.xml | 4 +- .../webserver/ContentTypeResolver.java | 2 +- .../webserver/Listener.java | 4 +- .../webserver/Main.java | 10 +- .../webserver/WebServer.java | 137 ++++++++++-------- .../webserver/runtime/JavaPageDispatcher.java | 6 +- .../webserver/utils/WebUrlUtil.java | 27 +++- 8 files changed, 113 insertions(+), 79 deletions(-) diff --git a/dependency-reduced-pom.xml b/dependency-reduced-pom.xml index 95dfd2f..eb071ae 100644 --- a/dependency-reduced-pom.xml +++ b/dependency-reduced-pom.xml @@ -3,7 +3,7 @@ 4.0.0 org.openautonomousconnection WebServer - 1.0.1-BETA.0.1 + 1.0.1-BETA.0.3 The default DNS-Server https://open-autonomous-connection.org/ diff --git a/pom.xml b/pom.xml index 85ef37e..6aa19c6 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ org.openautonomousconnection WebServer - 1.0.1-BETA.0.1 + 1.0.1-BETA.0.3 Open Autonomous Connection https://open-autonomous-connection.org/ @@ -77,7 +77,7 @@ org.openautonomousconnection Protocol - 1.0.1-BETA.0.3 + 1.0.1-BETA.0.5 org.projectlombok diff --git a/src/main/java/org/openautonomousconnection/webserver/ContentTypeResolver.java b/src/main/java/org/openautonomousconnection/webserver/ContentTypeResolver.java index d039f56..0da7df2 100644 --- a/src/main/java/org/openautonomousconnection/webserver/ContentTypeResolver.java +++ b/src/main/java/org/openautonomousconnection/webserver/ContentTypeResolver.java @@ -39,7 +39,7 @@ public final class ContentTypeResolver { return isFile(name, "image"); } - public static boolean isAudio(String name) { + public static boolean isAudioFile(String name) { return isFile(name, "audio"); } diff --git a/src/main/java/org/openautonomousconnection/webserver/Listener.java b/src/main/java/org/openautonomousconnection/webserver/Listener.java index 1d7a6d4..12f7ea0 100644 --- a/src/main/java/org/openautonomousconnection/webserver/Listener.java +++ b/src/main/java/org/openautonomousconnection/webserver/Listener.java @@ -10,11 +10,11 @@ public class Listener extends EventListener { public void onCommandNotFound(CommandNotFoundEvent event) { StringBuilder argsBuilder = new StringBuilder(); for (String arg : event.getArgs()) argsBuilder.append(arg).append(" "); - Main.getProtocolBridge().getLogger().error("Command '" + event.getName() + argsBuilder.toString() + "' not found!"); + Main.getProtocolBridge().getProtocolValues().logger.error("Command '" + event.getName() + argsBuilder.toString() + "' not found!"); } @dev.unlegitdqrk.unlegitlibrary.event.Listener public void onMissingCommandPermission(CommandExecutorMissingPermissionEvent event) { - Main.getProtocolBridge().getLogger().error("You do not have enough permissions to execute this command!"); + Main.getProtocolBridge().getProtocolValues().logger.error("You do not have enough permissions to execute this command!"); } } diff --git a/src/main/java/org/openautonomousconnection/webserver/Main.java b/src/main/java/org/openautonomousconnection/webserver/Main.java index 47e3cb5..efaf480 100644 --- a/src/main/java/org/openautonomousconnection/webserver/Main.java +++ b/src/main/java/org/openautonomousconnection/webserver/Main.java @@ -37,6 +37,9 @@ public class Main { values = new ProtocolValues(); values.packetHandler = new PacketHandler(); values.eventManager = new EventManager(); + values.logger = new Logger(new File("logs"), false, true); + values.protocolVersion = ProtocolVersion.PV_1_0_1_BETA; + values.addonLoader = new AddonLoader(values.eventManager, values.logger); if (!Files.exists(new File("config.properties").toPath())) Files.createFile(new File("config.properties").toPath()); @@ -76,15 +79,12 @@ public class Main { int sessionExpire = config.getInt("sessionexpiremin"); int maxUpload = config.getInt("maxuploadmb"); - ClientAuthMode authMode = ClientAuthMode.valueOf(config.getString("clientauth").toUpperCase()); - values.authMode = authMode; - Logger logger = new Logger(new File("logs"), false, true); + values.authMode = ClientAuthMode.valueOf(config.getString("clientauth").toUpperCase()); values.eventManager.registerListener(Listener.class); protocolBridge = new ProtocolBridge(new WebServer( new File("auth.ini"), new File("rules.ini"), - sessionExpire, maxUpload), - values, ProtocolVersion.PV_1_0_1_BETA, logger, new AddonLoader(values.eventManager, logger)); + sessionExpire, maxUpload), values); protocolBridge.getProtocolServer().getNetwork().start(tcpPort, udpPort); diff --git a/src/main/java/org/openautonomousconnection/webserver/WebServer.java b/src/main/java/org/openautonomousconnection/webserver/WebServer.java index bb90728..ea90d90 100644 --- a/src/main/java/org/openautonomousconnection/webserver/WebServer.java +++ b/src/main/java/org/openautonomousconnection/webserver/WebServer.java @@ -20,12 +20,13 @@ import org.openautonomousconnection.protocol.versions.v1_0_1.beta.WebPacketHeade import org.openautonomousconnection.webserver.api.SessionContext; import org.openautonomousconnection.webserver.runtime.JavaPageDispatcher; import org.openautonomousconnection.webserver.utils.WebHasher; +import org.openautonomousconnection.webserver.utils.WebUrlUtil; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.InputStream; -import java.net.URL; +import java.net.URI; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.util.Arrays; @@ -33,27 +34,16 @@ import java.util.LinkedHashMap; import java.util.Locale; import java.util.Map; import java.util.Objects; -import java.util.Optional; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * OAC WebServer implementation for WEB v1.0.1-BETA. * - *

Implements the three v1.0.1 entry points:

- *
    - *
  • Navigation: {@link #onNavigateRequest(CustomConnectedClient, WebNavigateRequestPacket)}
  • - *
  • Resource: {@link #onResourceRequest(CustomConnectedClient, WebResourceRequestPacket)}
  • - *
  • Document apply: {@link #onDocumentApplyRequest(CustomConnectedClient, WebDocumentApplyRequestPacket)}
  • - *
+ *

Important: Server-side parsing MUST NOT use {@link java.net.URL} for "web://" because + * the server JVM does not need (and typically does not have) a URLStreamHandler installed.

* - *

Supports:

- *
    - *
  • Static file serving
  • - *
  • Java-page dispatch (server-side pages)
  • - *
  • Range requests ("bytes=start-end")
  • - *
  • Streaming for large responses (best-effort over UDP for video chunks)
  • - *
+ *

Therefore, request URLs are parsed using {@link java.net.URI} which accepts unknown schemes.

*/ @ProtocolInfo(protocolSide = ProtocolVersion.ProtocolSide.WEB) public final class WebServer extends ProtocolWebServer { @@ -95,9 +85,10 @@ public final class WebServer extends ProtocolWebServer { Objects.requireNonNull(request, "request"); final WebPacketHeader in = request.getHeader(); - final URL url; + + final ParsedRequestUrl parsed; try { - url = new URL(request.getUrl()); + parsed = ParsedRequestUrl.parse(request.getUrl()); } catch (Exception e) { return new WebNavigateAckPacket( mirrorHeader(in, WebPacketFlags.NAVIGATION), @@ -106,7 +97,7 @@ public final class WebServer extends ProtocolWebServer { ); } - String path = normalizePath(url.getPath()); + String path = normalizePath(parsed.path()); if (RuleManager.isDenied(path)) { return new WebNavigateAckPacket( mirrorHeader(in, WebPacketFlags.NAVIGATION), @@ -147,14 +138,15 @@ public final class WebServer extends ProtocolWebServer { Objects.requireNonNull(request, "request"); final WebPacketHeader in = request.getHeader(); - final URL url; + + final ParsedRequestUrl parsed; try { - url = new URL(request.getUrl()); + parsed = ParsedRequestUrl.parse(request.getUrl()); } catch (Exception e) { return error(in, 400, "Invalid URL: " + e.getMessage()); } - String path = normalizePath(url.getPath()); + String path = normalizePath(parsed.path()); if (RuleManager.isDenied(path) || !RuleManager.isAllowed(path)) { return error(in, 403, "Forbidden"); @@ -172,7 +164,7 @@ public final class WebServer extends ProtocolWebServer { } try { - WebResourceResponsePacket javaResp = dispatchJavaPageAsResource(client, request, url); + WebResourceResponsePacket javaResp = dispatchJavaPageAsResource(client, request); if (javaResp != null) { return javaResp; } @@ -181,7 +173,7 @@ public final class WebServer extends ProtocolWebServer { } try { - return serveStaticFile(client, request, url); + return serveStaticFile(client, request, parsed); } catch (Exception e) { return error(in, 500, "Internal error: " + e.getClass().getSimpleName() + ": " + e.getMessage()); } @@ -200,14 +192,14 @@ public final class WebServer extends ProtocolWebServer { ); } - private WebResourceResponsePacket dispatchJavaPageAsResource(CustomConnectedClient client, WebResourceRequestPacket req, URL url) throws Exception { + private WebResourceResponsePacket dispatchJavaPageAsResource(CustomConnectedClient client, WebResourceRequestPacket req) throws Exception { return JavaPageDispatcher.dispatch(client, this, req); } - private WebResourceResponsePacket serveStaticFile(CustomConnectedClient client, WebResourceRequestPacket request, URL url) throws Exception { + private WebResourceResponsePacket serveStaticFile(CustomConnectedClient client, WebResourceRequestPacket request, ParsedRequestUrl parsed) throws Exception { final WebPacketHeader in = request.getHeader(); - String path = normalizePath(url.getPath()); + String path = normalizePath(parsed.path()); if (path.startsWith("/")) path = path.substring(1); if (path.isEmpty()) path = "index.html"; @@ -412,9 +404,7 @@ public final class WebServer extends ProtocolWebServer { private static boolean shouldStream(String fileName, String contentType, long size, boolean wantsRange) { if (size >= STREAM_THRESHOLD) return true; - // Many video/audio players use Range for progressive playback. - // If Range is present and it's a video/audio, prefer stream to avoid big memory spikes. - if (wantsRange && (ContentTypeResolver.isVideoFile(fileName) || ContentTypeResolver.isAudio(fileName))) { + if (wantsRange && (ContentTypeResolver.isVideoFile(fileName) || ContentTypeResolver.isAudioFile(fileName))) { return true; } @@ -422,14 +412,11 @@ public final class WebServer extends ProtocolWebServer { } private static TransportProtocol chooseChunkTransport(String fileName) { - // best-effort: chunks via UDP for video, TCP otherwise return ContentTypeResolver.isVideoFile(fileName) ? TransportProtocol.UDP : TransportProtocol.TCP; } private static String normalizePath(String p) { - if (p == null || p.isBlank()) return "/"; - String t = p.trim(); - return t.isEmpty() ? "/" : t; + return WebUrlUtil.normalizeRequestPath(p); } private static String contentTypeOrDefault(String ct) { @@ -443,7 +430,6 @@ public final class WebServer extends ProtocolWebServer { } private static Map safeHeaders(WebNavigateRequestPacket request) { - // Navigate packet currently has no headers in your definition; keep empty. return Map.of(); } @@ -475,6 +461,62 @@ public final class WebServer extends ProtocolWebServer { ); } + /** + * Small helper that parses an incoming request URL string without requiring a URLStreamHandler. + * + *

Uses {@link URI} because it supports unknown schemes such as "web".

+ */ + private record ParsedRequestUrl(String raw, String scheme, String host, String path, String query) { + + /** + * Parses an input URL string into components. + * + * @param raw raw URL string from packet + * @return parsed representation + */ + public static ParsedRequestUrl parse(String raw) { + if (raw == null || raw.isBlank()) { + throw new IllegalArgumentException("URL is empty"); + } + + final URI uri; + try { + uri = URI.create(raw.trim()); + } catch (Exception e) { + throw new IllegalArgumentException(e.getMessage(), e); + } + + String scheme = uri.getScheme(); + if (scheme == null || scheme.isBlank()) { + throw new IllegalArgumentException("Missing scheme"); + } + + // For hierarchical URIs like "web://host/path", getHost() is expected to work. + String host = uri.getHost(); + + // Fallback: some inputs might be like "web:host/path" or malformed variants. + if ((host == null || host.isBlank()) && uri.getRawSchemeSpecificPart() != null) { + // best-effort: extract after "//" + String ssp = uri.getRawSchemeSpecificPart(); + int idx = ssp.indexOf("//"); + if (idx >= 0) { + String rest = ssp.substring(idx + 2); + int slash = rest.indexOf('/'); + host = (slash >= 0) ? rest.substring(0, slash) : rest; + if (host != null) host = host.trim(); + if (host != null && host.isEmpty()) host = null; + } + } + + String path = uri.getPath(); + if (path == null || path.isBlank()) path = "/"; + + String query = uri.getQuery(); + + return new ParsedRequestUrl(raw.trim(), scheme, host, path, query); + } + } + /** * Minimal byte-range parser supporting "bytes=start-end", "bytes=start-", and "bytes=-suffixLen". */ @@ -489,20 +531,12 @@ public final class WebServer extends ProtocolWebServer { this.valid = valid; } - /** - * Parses a Range header for a resource of the given size. - * - * @param header Range header value - * @param size total resource size - * @return range spec or null if header missing/blank - */ public static RangeSpec parse(String header, long size) { if (header == null || header.isBlank()) return null; String h = header.trim().toLowerCase(Locale.ROOT); if (!h.startsWith("bytes=")) return null; String v = h.substring("bytes=".length()).trim(); - // Only single-range supported int comma = v.indexOf(','); if (comma >= 0) v = v.substring(0, comma).trim(); @@ -522,7 +556,6 @@ public final class WebServer extends ProtocolWebServer { return new RangeSpec(start, end, true); } - // suffix range: "-N" (last N bytes) if (!b.isEmpty()) { long suffix = Long.parseLong(b); if (suffix <= 0) return new RangeSpec(0, 0, false); @@ -538,40 +571,24 @@ public final class WebServer extends ProtocolWebServer { } } - /** - * @return true if range is usable - */ public boolean isValid() { return valid; } - /** - * @return inclusive start - */ public long startInclusive() { return startInclusive; } - /** - * @return inclusive end - */ public long endInclusive() { return endInclusive; } - /** - * @return byte length represented by this range - */ public long length() { return (endInclusive >= startInclusive) ? (endInclusive - startInclusive + 1) : 0L; } - /** - * @param total total size - * @return Content-Range header value - */ public String toContentRange(long total) { return "bytes " + startInclusive + "-" + endInclusive + "/" + total; } } -} \ 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 fc0a146..b47ebd0 100644 --- a/src/main/java/org/openautonomousconnection/webserver/runtime/JavaPageDispatcher.java +++ b/src/main/java/org/openautonomousconnection/webserver/runtime/JavaPageDispatcher.java @@ -50,9 +50,7 @@ public final class JavaPageDispatcher { int q = route.indexOf('?'); if (q >= 0) route = route.substring(0, q); - if (!route.startsWith("/")) { - route = "/" + route; - } + route = WebUrlUtil.normalizeRequestPath(route); java.io.File contentRoot = server.getContentFolder(); ROUTES.refreshIfNeeded(contentRoot); @@ -113,4 +111,4 @@ public final class JavaPageDispatcher { null ); } -} \ No newline at end of file +} diff --git a/src/main/java/org/openautonomousconnection/webserver/utils/WebUrlUtil.java b/src/main/java/org/openautonomousconnection/webserver/utils/WebUrlUtil.java index dc0df2b..f82bb84 100644 --- a/src/main/java/org/openautonomousconnection/webserver/utils/WebUrlUtil.java +++ b/src/main/java/org/openautonomousconnection/webserver/utils/WebUrlUtil.java @@ -7,6 +7,8 @@ import java.net.URI; */ public final class WebUrlUtil { + private static final String DEFAULT_DOCUMENT_PATH = "/index.html"; + private WebUrlUtil() { } @@ -28,6 +30,25 @@ public final class WebUrlUtil { } } + /** + * Normalizes a request path for route/rule lookup. + * + *

Blank paths and "/" resolve to the default document.

+ * + * @param path request path + * @return normalized absolute path + */ + public static String normalizeRequestPath(String path) { + if (path == null) return DEFAULT_DOCUMENT_PATH; + + String p = path.trim(); + if (p.isEmpty() || "/".equals(p)) { + return DEFAULT_DOCUMENT_PATH; + } + + return p.startsWith("/") ? p : ("/" + p); + } + /** * Normalizes a requested path to a content-root relative path. * @@ -39,12 +60,10 @@ public final class WebUrlUtil { int q = p.indexOf('?'); if (q >= 0) p = p.substring(0, q); - if (p == null || p.isBlank() || "/".equals(p)) { - return "index.html"; - } + p = normalizeRequestPath(p); if (p.startsWith("/")) p = p.substring(1); if (p.isBlank()) return "index.html"; return p; } -} \ No newline at end of file +}