Implements the three v1.0.1 entry points:
- *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:
- *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 MapUses {@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 +}