From 564286909786068c49ab33395deb0a6ab484f66c Mon Sep 17 00:00:00 2001 From: UnlegitDqrk Date: Sun, 22 Feb 2026 17:26:22 +0100 Subject: [PATCH] Updated to latest Protocol Version --- .idea/misc.xml | 2 +- LICENSE | 3 +- README.MD | 3 +- dependency-reduced-pom.xml | 23 +- pom.xml | 27 +- .../webserver/Listener.java | 20 + .../webserver/Main.java | 6 +- .../webserver/WebServer.java | 569 +++++++++++++++--- .../webserver/api/SessionContext.java | 7 +- .../webserver/api/WebPage.java | 14 +- .../webserver/api/WebPageContext.java | 28 +- .../webserver/runtime/JavaPageDispatcher.java | 61 +- .../webserver/utils/HttpsProxy.java | 182 +++++- .../webserver/utils/MergedRequestParams.java | 24 +- .../webserver/utils/RequestParams.java | 33 +- .../webserver/utils/WebUrlUtil.java | 50 ++ 16 files changed, 879 insertions(+), 173 deletions(-) create mode 100644 src/main/java/org/openautonomousconnection/webserver/Listener.java create mode 100644 src/main/java/org/openautonomousconnection/webserver/utils/WebUrlUtil.java diff --git a/.idea/misc.xml b/.idea/misc.xml index 001e756..0c04b52 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -8,7 +8,7 @@ - + \ No newline at end of file diff --git a/LICENSE b/LICENSE index 3f64855..a7b8645 100644 --- a/LICENSE +++ b/LICENSE @@ -1 +1,2 @@ -Please read the license here: https://open-autonomous-connection.org/license.html \ No newline at end of file +Please read the license here: https://open-autonomous-connection.org/license.html +Download all third parties licenses here: https://open-autonomous-connection.org/assets/licenses.zip diff --git a/README.MD b/README.MD index 440d3fc..07774dd 100644 --- a/README.MD +++ b/README.MD @@ -10,7 +10,8 @@ This project (OAC) is licensed under the [Open Autonomous Public License (OAPL)](https://open-autonomous-connection.org/license.html). **Third-party components:** - +
+Download all license here: https://open-autonomous-connection.org/assets/licenses.zip - *UnlegitLibrary* is authored by the same copyright holder and is used here under a special agreement: While [UnlegitLibrary](https://repo.unlegitdqrk.dev/UnlegitDqrk/unlegitlibrary/) is generally distributed under the [GNU GPLv3](https://repo.unlegitdqrk.dev/UnlegitDqrk/unlegitlibrary/src/branch/master/LICENSE), diff --git a/dependency-reduced-pom.xml b/dependency-reduced-pom.xml index 25ea61b..95dfd2f 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.1 + 1.0.1-BETA.0.1 The default DNS-Server https://open-autonomous-connection.org/ @@ -44,6 +44,21 @@ + + maven-compiler-plugin + 3.13.0 + + ${maven.compiler.source} + ${maven.compiler.target} + + + org.projectlombok + lombok + 1.18.42 + + + + maven-shade-plugin 3.6.0 @@ -109,7 +124,7 @@ org.projectlombok lombok - 1.18.38 + 1.18.42 provided @@ -122,7 +137,7 @@ UTF-8 - 23 - 23 + 25 + 25 diff --git a/pom.xml b/pom.xml index 17b8e7c..85ef37e 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ org.openautonomousconnection WebServer - 1.0.0-BETA.1.1 + 1.0.1-BETA.0.1 Open Autonomous Connection https://open-autonomous-connection.org/ @@ -15,8 +15,8 @@ The default DNS-Server - 23 - 23 + 25 + 25 UTF-8 @@ -77,12 +77,12 @@ org.openautonomousconnection Protocol - 1.0.0-BETA.1.1 + 1.0.1-BETA.0.3 org.projectlombok lombok - 1.18.38 + 1.18.42 provided @@ -94,6 +94,23 @@ + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + ${maven.compiler.source} + ${maven.compiler.target} + + + + org.projectlombok + lombok + 1.18.42 + + + + org.apache.maven.plugins maven-shade-plugin diff --git a/src/main/java/org/openautonomousconnection/webserver/Listener.java b/src/main/java/org/openautonomousconnection/webserver/Listener.java new file mode 100644 index 0000000..1d7a6d4 --- /dev/null +++ b/src/main/java/org/openautonomousconnection/webserver/Listener.java @@ -0,0 +1,20 @@ +package org.openautonomousconnection.webserver; + +import dev.unlegitdqrk.unlegitlibrary.command.events.CommandExecutorMissingPermissionEvent; +import dev.unlegitdqrk.unlegitlibrary.command.events.CommandNotFoundEvent; +import dev.unlegitdqrk.unlegitlibrary.event.EventListener; + +public class Listener extends EventListener { + + @dev.unlegitdqrk.unlegitlibrary.event.Listener + 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!"); + } + + @dev.unlegitdqrk.unlegitlibrary.event.Listener + public void onMissingCommandPermission(CommandExecutorMissingPermissionEvent event) { + Main.getProtocolBridge().getLogger().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 5a4c8eb..47e3cb5 100644 --- a/src/main/java/org/openautonomousconnection/webserver/Main.java +++ b/src/main/java/org/openautonomousconnection/webserver/Main.java @@ -1,5 +1,6 @@ package org.openautonomousconnection.webserver; +import dev.unlegitdqrk.unlegitlibrary.addon.AddonLoader; import dev.unlegitdqrk.unlegitlibrary.command.CommandExecutor; import dev.unlegitdqrk.unlegitlibrary.command.CommandManager; import dev.unlegitdqrk.unlegitlibrary.command.CommandPermission; @@ -7,6 +8,7 @@ import dev.unlegitdqrk.unlegitlibrary.event.EventManager; import dev.unlegitdqrk.unlegitlibrary.file.ConfigurationManager; import dev.unlegitdqrk.unlegitlibrary.network.system.packets.PacketHandler; import dev.unlegitdqrk.unlegitlibrary.network.system.utils.ClientAuthMode; +import dev.unlegitdqrk.unlegitlibrary.utils.Logger; import lombok.Getter; import org.openautonomousconnection.protocol.ProtocolBridge; import org.openautonomousconnection.protocol.ProtocolValues; @@ -76,11 +78,13 @@ public class Main { ClientAuthMode authMode = ClientAuthMode.valueOf(config.getString("clientauth").toUpperCase()); values.authMode = authMode; + Logger logger = new Logger(new File("logs"), false, true); + 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_0_BETA, new File("logs")); + values, ProtocolVersion.PV_1_0_1_BETA, logger, new AddonLoader(values.eventManager, logger)); 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 7907b99..bb90728 100644 --- a/src/main/java/org/openautonomousconnection/webserver/WebServer.java +++ b/src/main/java/org/openautonomousconnection/webserver/WebServer.java @@ -1,168 +1,577 @@ package org.openautonomousconnection.webserver; import dev.unlegitdqrk.unlegitlibrary.network.system.utils.TransportProtocol; -import lombok.Getter; import org.openautonomousconnection.protocol.annotations.ProtocolInfo; -import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.WebRequestPacket; -import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.WebResponsePacket; -import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.stream.WebStreamChunkPacket; -import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.stream.WebStreamEndPacket; -import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.stream.WebStreamStartPacket; +import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.document.WebDocumentApplyRequestPacket; +import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.document.WebDocumentApplyResponsePacket; +import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.navigate.WebNavigateAckPacket; +import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.navigate.WebNavigateRequestPacket; +import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.resource.WebResourceRequestPacket; +import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.resource.WebResourceResponsePacket; +import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.stream.WebStreamChunkPacket_v1_0_1_B; +import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.stream.WebStreamEndPacket_v1_0_1_B; +import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.stream.WebStreamStartPacket_v1_0_1_B; import org.openautonomousconnection.protocol.side.server.CustomConnectedClient; import org.openautonomousconnection.protocol.side.web.ProtocolWebServer; import org.openautonomousconnection.protocol.side.web.managers.RuleManager; import org.openautonomousconnection.protocol.versions.ProtocolVersion; +import org.openautonomousconnection.protocol.versions.v1_0_1.beta.WebPacketFlags; +import org.openautonomousconnection.protocol.versions.v1_0_1.beta.WebPacketHeader; 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.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.net.URL; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.util.Arrays; +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. + * 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)}
  • + *
+ * + *

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)
  • + *
*/ @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; - public WebServer( - File authFile, - File rulesFile, - int sessionExpire, - int maxUpload - ) throws Exception { - super(authFile, rulesFile, sessionExpire, maxUpload); + /** + * Creates a WEB v1.0.1 server instance. + * + * @param authFile authentication file + * @param rulesFile rules file + * @param sessionExpire session expiration in minutes + * @param uploadSize max upload size in MB + * @throws Exception on init errors + */ + public WebServer(File authFile, File rulesFile, int sessionExpire, int uploadSize) throws Exception { + super(authFile, rulesFile, sessionExpire, uploadSize); + this.hasher = new WebHasher(120_000, 16, 32); + } - this.hasher = new WebHasher( - 120_000, - 16, - 32 + /** + * @return server hasher instance + */ + public WebHasher getHasher() { + return hasher; + } + + @Override + protected WebNavigateAckPacket onNavigateRequest(CustomConnectedClient client, WebNavigateRequestPacket request) { + Objects.requireNonNull(client, "client"); + Objects.requireNonNull(request, "request"); + + final WebPacketHeader in = request.getHeader(); + final URL url; + try { + url = new URL(request.getUrl()); + } catch (Exception e) { + return new WebNavigateAckPacket( + mirrorHeader(in, WebPacketFlags.NAVIGATION), + false, + "Invalid URL: " + e.getMessage() + ); + } + + String path = normalizePath(url.getPath()); + if (RuleManager.isDenied(path)) { + return new WebNavigateAckPacket( + mirrorHeader(in, WebPacketFlags.NAVIGATION), + false, + "Forbidden" + ); + } + + if (RuleManager.requiresAuth(path)) { + try { + SessionContext ctx = SessionContext.from(client, this, safeHeaders(request)); + if (!ctx.isValid()) { + return new WebNavigateAckPacket( + mirrorHeader(in, WebPacketFlags.NAVIGATION), + false, + "Authentication required" + ); + } + } catch (Exception e) { + return new WebNavigateAckPacket( + mirrorHeader(in, WebPacketFlags.NAVIGATION), + false, + "Auth check failed: " + e.getMessage() + ); + } + } + + return new WebNavigateAckPacket( + mirrorHeader(in, WebPacketFlags.NAVIGATION), + true, + null ); } @Override - public WebResponsePacket onWebRequest(CustomConnectedClient client, WebRequestPacket request) { + protected WebResourceResponsePacket onResourceRequest(CustomConnectedClient client, WebResourceRequestPacket request) { + Objects.requireNonNull(client, "client"); + Objects.requireNonNull(request, "request"); + + final WebPacketHeader in = request.getHeader(); + final URL url; try { - String path = request.getPath() == null ? "/" : request.getPath(); - - if (RuleManager.isDenied(path)) { - 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", HeaderMaps.mutable(), "Authentication required".getBytes()); - } - } - - WebResponsePacket javaResp = JavaPageDispatcher.dispatch(client, this, request); - if (javaResp != null) return javaResp; - - return serveFile(client, path); - + url = new URL(request.getUrl()); } catch (Exception e) { - return new WebResponsePacket( - 500, - "text/plain; charset=utf-8", - HeaderMaps.mutable(), - ("Internal Error: " + e.getClass().getName() + ": " + e.getMessage()).getBytes() - ); + return error(in, 400, "Invalid URL: " + e.getMessage()); + } + + String path = normalizePath(url.getPath()); + + if (RuleManager.isDenied(path) || !RuleManager.isAllowed(path)) { + return error(in, 403, "Forbidden"); + } + + if (RuleManager.requiresAuth(path)) { + try { + SessionContext ctx = SessionContext.from(client, this, safeHeaders(request)); + if (!ctx.isValid()) { + return error(in, 401, "Authentication required"); + } + } catch (Exception e) { + return error(in, 500, "Auth check failed: " + e.getMessage()); + } + } + + try { + WebResourceResponsePacket javaResp = dispatchJavaPageAsResource(client, request, url); + if (javaResp != null) { + return javaResp; + } + } catch (Exception e) { + return error(in, 500, "JavaPage error: " + e.getClass().getSimpleName() + ": " + e.getMessage()); + } + + try { + return serveStaticFile(client, request, url); + } catch (Exception e) { + return error(in, 500, "Internal error: " + e.getClass().getSimpleName() + ": " + e.getMessage()); } } - private WebResponsePacket serveFile(CustomConnectedClient client, String path) throws Exception { + @Override + protected WebDocumentApplyResponsePacket onDocumentApplyRequest(CustomConnectedClient client, WebDocumentApplyRequestPacket request) { + Objects.requireNonNull(client, "client"); + Objects.requireNonNull(request, "request"); + + // Not wired in this server implementation yet. + return new WebDocumentApplyResponsePacket( + mirrorHeader(request.getHeader(), WebPacketFlags.DEVTOOLS), + false, + "Document apply is not implemented on server side" + ); + } + + private WebResourceResponsePacket dispatchJavaPageAsResource(CustomConnectedClient client, WebResourceRequestPacket req, URL url) throws Exception { + return JavaPageDispatcher.dispatch(client, this, req); + } + + private WebResourceResponsePacket serveStaticFile(CustomConnectedClient client, WebResourceRequestPacket request, URL url) throws Exception { + final WebPacketHeader in = request.getHeader(); + + String path = normalizePath(url.getPath()); 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 (!RuleManager.isAllowed(path)) { - return new WebResponsePacket(403, "text/plain; charset=utf-8", HeaderMaps.mutable(), "Forbidden".getBytes()); + if (!file.getPath().startsWith(root.getPath())) { + return error(in, 403, "Forbidden"); } - if (RuleManager.isDenied(path)) { - return new WebResponsePacket(403, "text/plain; charset=utf-8", HeaderMaps.mutable(), "Forbidden".getBytes()); + if (!file.exists() || !file.isFile()) { + return error(in, 404, "Not Found"); } String contentType = ContentTypeResolver.resolve(file.getName()); long size = file.length(); - if (size >= STREAM_THRESHOLD) { - startStreamingAsync(client, file, contentType); + RangeSpec range = RangeSpec.parse(getHeaderIgnoreCase(safeHeaders(request), "range"), size); - // 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() + boolean wantsRange = range != null && range.isValid(); + boolean shouldStream = shouldStream(file.getName(), contentType, size, wantsRange); + + if (shouldStream) { + // Return an empty response that indicates "stream follows" via flags. + WebPacketHeader respHeader = mirrorHeader(in, WebPacketFlags.RESOURCE | WebPacketFlags.STREAM); + + Map headers = new LinkedHashMap<>(); + headers.put("x-oac-stream", "1"); + headers.put("accept-ranges", "bytes"); + + if (wantsRange) { + headers.put("content-range", range.toContentRange(size)); + } + + // Start streaming asynchronously; chunks best-effort for video (UDP). + startStreamingAsync(client, request.getHeader(), file, contentType, range); + + return new WebResourceResponsePacket( + respHeader, + wantsRange ? 206 : 200, + contentTypeOrDefault(contentType), + headers, + new byte[0], + file.toURI().toString() ); } - byte[] data = Files.readAllBytes(file.toPath()); - return new WebResponsePacket(200, contentType, HeaderMaps.mutable(), data); + // Non-stream: either full or ranged (single shot) + byte[] data; + if (wantsRange) { + data = readRange(file, range); + Map headers = new LinkedHashMap<>(); + headers.put("accept-ranges", "bytes"); + headers.put("content-range", range.toContentRange(size)); + headers.put("content-length", String.valueOf(data.length)); + + return new WebResourceResponsePacket( + mirrorHeader(in, WebPacketFlags.RESOURCE), + 206, + contentTypeOrDefault(contentType), + headers, + data, + file.toURI().toString() + ); + } + + data = Files.readAllBytes(file.toPath()); + Map headers = new LinkedHashMap<>(); + headers.put("accept-ranges", "bytes"); + headers.put("content-length", String.valueOf(data.length)); + + return new WebResourceResponsePacket( + mirrorHeader(in, WebPacketFlags.RESOURCE), + 200, + contentTypeOrDefault(contentType), + headers, + data, + file.toURI().toString() + ); } - private void startStreamingAsync(CustomConnectedClient client, File file, String contentType) { + private void startStreamingAsync(CustomConnectedClient client, WebPacketHeader incomingHeader, File file, String contentType, RangeSpec range) { STREAM_EXECUTOR.execute(() -> { try { - streamFile(client, file, contentType); + streamFile(client, incomingHeader, file, contentType, range); } catch (Exception e) { - // Never let streaming errors kill your server threads. try { - client.getConnection().sendPacket(new WebStreamEndPacket(false), TransportProtocol.TCP); + client.getConnection().sendPacket( + new WebStreamEndPacket_v1_0_1_B( + mirrorHeader(incomingHeader, WebPacketFlags.STREAM | WebPacketFlags.RESOURCE), + false, + e.getClass().getSimpleName() + ": " + e.getMessage() + ), + TransportProtocol.TCP + ); } catch (Exception ignored) { - // ignore: client may already be gone + // ignore } } }); } - private void streamFile(CustomConnectedClient client, File file, String contentType) throws IOException, ClassNotFoundException { - long total = file.length(); + private void streamFile(CustomConnectedClient client, WebPacketHeader incomingHeader, File file, String contentType, RangeSpec range) throws Exception { + long totalSize = file.length(); + boolean hasRange = range != null && range.isValid(); + + long start = hasRange ? range.startInclusive() : 0L; + long end = hasRange ? range.endInclusive() : (totalSize - 1); + long toSend = (end >= start) ? (end - start + 1) : 0L; + + Map startHeaders = new LinkedHashMap<>(); + startHeaders.put("name", file.getName()); + startHeaders.put("accept-ranges", "bytes"); + if (hasRange) { + startHeaders.put("content-range", range.toContentRange(totalSize)); + } client.getConnection().sendPacket( - new WebStreamStartPacket(200, contentType, Map.of("name", file.getName()), total), + new WebStreamStartPacket_v1_0_1_B( + mirrorHeader(incomingHeader, WebPacketFlags.STREAM | WebPacketFlags.RESOURCE), + hasRange ? 206 : 200, + contentTypeOrDefault(contentType), + startHeaders, + toSend + ), TransportProtocol.TCP ); + TransportProtocol chunkTransport = chooseChunkTransport(file.getName()); + try (InputStream in = new BufferedInputStream(new FileInputStream(file))) { + skipFully(in, start); + byte[] buf = new byte[STREAM_CHUNK_SIZE]; int seq = 0; - int r; - while ((r = in.read(buf)) != -1) { - // Always copy: never hand out the reusable buffer reference. + + long remaining = toSend; + while (remaining > 0) { + int want = (int) Math.min(buf.length, remaining); + int r = in.read(buf, 0, want); + if (r == -1) break; + byte[] chunk = Arrays.copyOf(buf, r); client.getConnection().sendPacket( - new WebStreamChunkPacket(seq++, chunk), - ContentTypeResolver.isVideoFile(file.getName()) ? TransportProtocol.UDP : TransportProtocol.TCP + new WebStreamChunkPacket_v1_0_1_B( + mirrorHeader(incomingHeader, WebPacketFlags.STREAM | WebPacketFlags.RESOURCE), + seq++, + chunk + ), + chunkTransport ); + + remaining -= r; } } - client.getConnection().sendPacket(new WebStreamEndPacket(true), TransportProtocol.TCP); + client.getConnection().sendPacket( + new WebStreamEndPacket_v1_0_1_B( + mirrorHeader(incomingHeader, WebPacketFlags.STREAM | WebPacketFlags.RESOURCE), + true, + null + ), + TransportProtocol.TCP + ); + } + + private static void skipFully(InputStream in, long n) throws Exception { + long remaining = n; + while (remaining > 0) { + long skipped = in.skip(remaining); + if (skipped <= 0) { + int b = in.read(); + if (b == -1) break; + skipped = 1; + } + remaining -= skipped; + } + } + + private static byte[] readRange(File file, RangeSpec range) throws Exception { + long len = range.length(); + if (len > Integer.MAX_VALUE) { + throw new IllegalStateException("Range too large for non-stream response: " + len); + } + + byte[] out = new byte[(int) len]; + try (InputStream in = new BufferedInputStream(new FileInputStream(file))) { + skipFully(in, range.startInclusive()); + int off = 0; + while (off < out.length) { + int r = in.read(out, off, out.length - off); + if (r == -1) break; + off += r; + } + if (off != out.length) { + return Arrays.copyOf(out, off); + } + } + return out; + } + + 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))) { + return true; + } + + return false; + } + + 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; + } + + private static String contentTypeOrDefault(String ct) { + if (ct == null || ct.isBlank()) return "application/octet-stream"; + return ct; + } + + private static Map safeHeaders(WebResourceRequestPacket request) { + Map h = request.getHeaders(); + return h == null ? Map.of() : h; + } + + private static Map safeHeaders(WebNavigateRequestPacket request) { + // Navigate packet currently has no headers in your definition; keep empty. + return Map.of(); + } + + private static String getHeaderIgnoreCase(Map headers, String key) { + if (headers == null || headers.isEmpty() || key == null) return null; + String needle = key.trim().toLowerCase(Locale.ROOT); + for (Map.Entry e : headers.entrySet()) { + if (e.getKey() == null) continue; + if (e.getKey().trim().toLowerCase(Locale.ROOT).equals(needle)) { + return e.getValue(); + } + } + return null; + } + + private WebResourceResponsePacket error(WebPacketHeader in, int code, String message) { + byte[] body = (message == null ? "" : message).getBytes(StandardCharsets.UTF_8); + Map headers = new LinkedHashMap<>(); + headers.put("content-length", String.valueOf(body.length)); + headers.put("accept-ranges", "bytes"); + + return new WebResourceResponsePacket( + mirrorHeader(in, WebPacketFlags.RESOURCE), + code, + "text/plain; charset=utf-8", + headers, + body, + null + ); + } + + /** + * Minimal byte-range parser supporting "bytes=start-end", "bytes=start-", and "bytes=-suffixLen". + */ + private static final class RangeSpec { + private final long startInclusive; + private final long endInclusive; + private final boolean valid; + + private RangeSpec(long startInclusive, long endInclusive, boolean valid) { + this.startInclusive = startInclusive; + this.endInclusive = endInclusive; + 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(); + + int dash = v.indexOf('-'); + if (dash < 0) return new RangeSpec(0, 0, false); + + String a = v.substring(0, dash).trim(); + String b = v.substring(dash + 1).trim(); + + try { + if (!a.isEmpty()) { + long start = Long.parseLong(a); + long end = b.isEmpty() ? (size - 1) : Long.parseLong(b); + if (start < 0 || end < start) return new RangeSpec(0, 0, false); + if (start >= size) return new RangeSpec(0, 0, false); + end = Math.min(end, size - 1); + 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); + long start = Math.max(0, size - suffix); + long end = size > 0 ? (size - 1) : 0; + if (size <= 0) return new RangeSpec(0, 0, false); + return new RangeSpec(start, end, true); + } + + return new RangeSpec(0, 0, false); + } catch (NumberFormatException e) { + return new RangeSpec(0, 0, false); + } + } + + /** + * @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/api/SessionContext.java b/src/main/java/org/openautonomousconnection/webserver/api/SessionContext.java index f7a4bf5..1531673 100644 --- a/src/main/java/org/openautonomousconnection/webserver/api/SessionContext.java +++ b/src/main/java/org/openautonomousconnection/webserver/api/SessionContext.java @@ -59,14 +59,13 @@ public final class SessionContext { } private static String extractSessionId(Map headers) { - // 1) Cookie header preferred String cookie = getHeaderIgnoreCase(headers, "cookie"); String fromCookie = parseCookie(cookie, COOKIE_NAME); if (fromCookie != null && !fromCookie.isBlank()) return fromCookie; - // 2) Backward-compatible fallback: old custom header - String legacy = getHeaderIgnoreCase(headers, "session"); - return (legacy == null || legacy.isBlank()) ? null : legacy.trim(); + // Fallback + String session = getHeaderIgnoreCase(headers, "session"); + return (session == null || session.isBlank()) ? null : session.trim(); } private static String parseCookie(String cookieHeader, String name) { diff --git a/src/main/java/org/openautonomousconnection/webserver/api/WebPage.java b/src/main/java/org/openautonomousconnection/webserver/api/WebPage.java index ef68d80..1345555 100644 --- a/src/main/java/org/openautonomousconnection/webserver/api/WebPage.java +++ b/src/main/java/org/openautonomousconnection/webserver/api/WebPage.java @@ -1,10 +1,11 @@ package org.openautonomousconnection.webserver.api; -import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.WebResponsePacket; +import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.resource.WebResourceResponsePacket; /** - * Server-side Java page (PHP alternative). - * Every .java page must implement this interface. + * Server-side Java page (v1.0.1-BETA). + * + *

Every .java page must implement this interface.

*/ public interface WebPage { @@ -12,7 +13,8 @@ public interface WebPage { * Handles a web request. * * @param ctx context (client, request, session) - * @return response packet + * @return resource response packet + * @throws Exception on unexpected failures */ - WebResponsePacket handle(WebPageContext ctx) throws Exception; -} + WebResourceResponsePacket handle(WebPageContext ctx) throws Exception; +} \ No newline at end of file diff --git a/src/main/java/org/openautonomousconnection/webserver/api/WebPageContext.java b/src/main/java/org/openautonomousconnection/webserver/api/WebPageContext.java index 6f069ec..49bcecf 100644 --- a/src/main/java/org/openautonomousconnection/webserver/api/WebPageContext.java +++ b/src/main/java/org/openautonomousconnection/webserver/api/WebPageContext.java @@ -1,18 +1,18 @@ package org.openautonomousconnection.webserver.api; -import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.WebRequestPacket; +import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.resource.WebResourceRequestPacket; import org.openautonomousconnection.protocol.side.server.CustomConnectedClient; import org.openautonomousconnection.protocol.side.web.ProtocolWebServer; import org.openautonomousconnection.webserver.utils.RequestParams; import org.openautonomousconnection.webserver.utils.WebHasher; /** - * Context passed to Java WebPages (client, request, session, params, hasher). + * Context passed to Java WebPages (v1.0.1-BETA). */ public final class WebPageContext { public final CustomConnectedClient client; - public final WebRequestPacket request; + public final WebResourceRequestPacket request; public final SessionContext session; public final RequestParams params; public final WebHasher hasher; @@ -20,7 +20,7 @@ public final class WebPageContext { public WebPageContext( CustomConnectedClient client, ProtocolWebServer server, - WebRequestPacket request, + WebResourceRequestPacket request, RequestParams params, WebHasher hasher ) throws Exception { @@ -30,4 +30,22 @@ public final class WebPageContext { this.params = params; this.hasher = hasher; } -} + + /** + * Convenience constructor: creates {@link RequestParams} from request headers. + * + * @param client client + * @param server server + * @param request request + * @param hasher hasher + * @throws Exception on errors + */ + public WebPageContext( + CustomConnectedClient client, + ProtocolWebServer server, + WebResourceRequestPacket request, + WebHasher hasher + ) throws Exception { + this(client, server, request, new RequestParams(request), hasher); + } +} \ 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 4b3aeda..fc0a146 100644 --- a/src/main/java/org/openautonomousconnection/webserver/runtime/JavaPageDispatcher.java +++ b/src/main/java/org/openautonomousconnection/webserver/runtime/JavaPageDispatcher.java @@ -1,7 +1,7 @@ package org.openautonomousconnection.webserver.runtime; -import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.WebRequestPacket; -import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.WebResponsePacket; +import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.resource.WebResourceRequestPacket; +import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.resource.WebResourceResponsePacket; import org.openautonomousconnection.protocol.side.server.CustomConnectedClient; import org.openautonomousconnection.protocol.side.web.ProtocolWebServer; import org.openautonomousconnection.webserver.WebServer; @@ -10,15 +10,12 @@ import org.openautonomousconnection.webserver.api.WebPageContext; import org.openautonomousconnection.webserver.utils.HeaderMaps; import org.openautonomousconnection.webserver.utils.RequestParams; import org.openautonomousconnection.webserver.utils.WebHasher; +import org.openautonomousconnection.webserver.utils.WebUrlUtil; -import java.io.File; import java.nio.charset.StandardCharsets; /** - * Dispatches Java WebPages using {@code @Route} annotation. - * - *

This dispatcher relies on {@link JavaRouteRegistry} for route-to-source mapping - * and uses {@link JavaPageCache} to compile/load classes from the content tree. + * Dispatches Java WebPages using {@code @Route} annotation (v1.0.1-BETA). */ public final class JavaPageDispatcher { @@ -34,25 +31,30 @@ public final class JavaPageDispatcher { * @param client connected client * @param server protocol web server * @param request request packet - * @return response packet or {@code null} if no Java route matches and static file handling should proceed + * @return response packet or {@code null} if no Java route matches * @throws Exception on unexpected failures */ - public static WebResponsePacket dispatch( + public static WebResourceResponsePacket dispatch( CustomConnectedClient client, ProtocolWebServer server, - WebRequestPacket request + WebResourceRequestPacket request ) throws Exception { - if (request == null || request.getPath() == null) { + if (request == null || request.getUrl() == null) { return null; } - String route = request.getPath(); + String route = WebUrlUtil.extractPathAndQuery(request.getUrl()); + if (route == null) return null; + + int q = route.indexOf('?'); + if (q >= 0) route = route.substring(0, q); + if (!route.startsWith("/")) { route = "/" + route; } - File contentRoot = server.getContentFolder(); + java.io.File contentRoot = server.getContentFolder(); ROUTES.refreshIfNeeded(contentRoot); JavaRouteRegistry.RouteLookupResult found = ROUTES.find(route); @@ -65,9 +67,8 @@ public final class JavaPageDispatcher { JavaPageCache.LoadedClass loaded = CACHE.getOrCompile(contentRoot, found.sourceFile(), contentLm); Class clazz = loaded.clazz(); - // Verify that the loaded class is actually routable. if (!WebPage.class.isAssignableFrom(clazz)) { - return error(500, "Class has @Route but is not a WebPage: " + found.fqcn()); + return error(request, 500, "Class has @Route but is not a WebPage: " + found.fqcn()); } Object instance = clazz.getDeclaredConstructor().newInstance(); @@ -75,19 +76,41 @@ public final class JavaPageDispatcher { WebHasher hasher = (server instanceof WebServer ws) ? ws.getHasher() : null; if (hasher == null) { - return error(500, "WebHasher missing on server instance."); + return error(request, 500, "WebHasher missing on server instance."); } WebPageContext ctx = new WebPageContext(client, server, request, new RequestParams(request), hasher); return page.handle(ctx); } - private static WebResponsePacket error(int code, String msg) { - return new WebResponsePacket( + private static WebResourceResponsePacket error(WebResourceRequestPacket req, int code, String msg) { + byte[] body = (msg == null ? "" : msg).getBytes(StandardCharsets.UTF_8); + + // Mirror correlation from the incoming request if possible. + org.openautonomousconnection.protocol.versions.v1_0_1.beta.WebPacketHeader in = + (req != null && req.getHeader() != null) + ? req.getHeader() + : new org.openautonomousconnection.protocol.versions.v1_0_1.beta.WebPacketHeader( + 0, 0, 0, 0, org.openautonomousconnection.protocol.versions.v1_0_1.beta.WebPacketFlags.RESOURCE, System.currentTimeMillis() + ); + + org.openautonomousconnection.protocol.versions.v1_0_1.beta.WebPacketHeader out = + new org.openautonomousconnection.protocol.versions.v1_0_1.beta.WebPacketHeader( + in.getRequestId(), + in.getTabId(), + in.getPageId(), + in.getFrameId(), + in.getFlags() | org.openautonomousconnection.protocol.versions.v1_0_1.beta.WebPacketFlags.RESOURCE, + System.currentTimeMillis() + ); + + return new WebResourceResponsePacket( + out, code, "text/plain; charset=utf-8", HeaderMaps.mutable(), - msg.getBytes(StandardCharsets.UTF_8) + body, + null ); } } \ 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 0c140a1..f0c2756 100644 --- a/src/main/java/org/openautonomousconnection/webserver/utils/HttpsProxy.java +++ b/src/main/java/org/openautonomousconnection/webserver/utils/HttpsProxy.java @@ -1,6 +1,9 @@ package org.openautonomousconnection.webserver.utils; import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.WebResponsePacket; +import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.resource.WebResourceResponsePacket; +import org.openautonomousconnection.protocol.versions.v1_0_1.beta.WebPacketFlags; +import org.openautonomousconnection.protocol.versions.v1_0_1.beta.WebPacketHeader; import org.openautonomousconnection.webserver.api.WebPageContext; import javax.net.ssl.HttpsURLConnection; @@ -8,15 +11,16 @@ import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.net.URL; import java.nio.charset.StandardCharsets; +import java.util.LinkedHashMap; +import java.util.Map; /** * Simple HTTPS -> OAC proxy helper. * - *

Limitations: - *

    - *
  • Does not rewrite HTML/CSS URLs. If you need full offline/proxied subresources, - * implement URL rewriting and route subresource paths through this proxy as well.
  • - *
+ *

v1.0.1 entry returns {@link WebResourceResponsePacket} and mirrors correlation header fields + * from {@link WebPageContext#request}.

+ * + *

v1.0.0 method is kept for older call sites.

*/ public final class HttpsProxy { @@ -28,15 +32,151 @@ public final class HttpsProxy { } /** - * Fetches an HTTPS URL and returns it as a WebResponsePacket. + * Fetches an HTTPS URL and returns it as a v1.0.1 {@link WebResourceResponsePacket}. + * + * @param ctx the current web page context + * @param url the target HTTPS URL + * @return proxied response (never null) + */ + public static WebResourceResponsePacket proxyGet(WebPageContext ctx, String url) { + WebPacketHeader in = (ctx != null && ctx.request != null) ? ctx.request.getHeader() : null; + WebPacketHeader baseHeader = (in == null) + ? new WebPacketHeader(0, 0, 0, 0, WebPacketFlags.RESOURCE, System.currentTimeMillis()) + : new WebPacketHeader( + in.getRequestId(), + in.getTabId(), + in.getPageId(), + in.getFrameId(), + in.getFlags() | WebPacketFlags.RESOURCE, + System.currentTimeMillis() + ); + + try { + return proxyGetInternalV101(ctx, url, baseHeader, 0); + } catch (Exception e) { + byte[] body = ("Bad Gateway: " + e.getClass().getName() + ": " + e.getMessage()) + .getBytes(StandardCharsets.UTF_8); + + Map headers = new LinkedHashMap<>(); + headers.put("content-length", String.valueOf(body.length)); + + return new WebResourceResponsePacket( + baseHeader, + 502, + "text/plain; charset=utf-8", + headers, + body, + null + ); + } + } + + private static WebResourceResponsePacket proxyGetInternalV101(WebPageContext ctx, String url, WebPacketHeader header, int depth) throws Exception { + if (depth > MAX_REDIRECTS) { + byte[] body = "Too many redirects".getBytes(StandardCharsets.UTF_8); + Map headers = new LinkedHashMap<>(); + headers.put("content-length", String.valueOf(body.length)); + return new WebResourceResponsePacket(header, 508, "text/plain; charset=utf-8", headers, body, null); + } + + URL target = new URL(url); + HttpsURLConnection con = (HttpsURLConnection) target.openConnection(); + con.setInstanceFollowRedirects(false); + con.setRequestMethod("GET"); + con.setConnectTimeout(CONNECT_TIMEOUT_MS); + con.setReadTimeout(READ_TIMEOUT_MS); + + // Forward a user-agent if present (optional). + String ua = null; + if (ctx != null && ctx.request != null && ctx.request.getHeaders() != null) { + ua = getHeaderIgnoreCase(ctx.request.getHeaders(), "user-agent"); + } + con.setRequestProperty("User-Agent", ua != null ? ua : "OAC-HttpsProxy/1.0.1"); + + int code = con.getResponseCode(); + + // Manual redirect handling + if (code == 301 || code == 302 || code == 303 || code == 307 || code == 308) { + String location = con.getHeaderField("Location"); + if (location == null || location.isBlank()) { + con.disconnect(); + byte[] body = ("Bad Gateway: redirect without Location (code=" + code + ")") + .getBytes(StandardCharsets.UTF_8); + Map headers = new LinkedHashMap<>(); + headers.put("content-length", String.valueOf(body.length)); + return new WebResourceResponsePacket(header, 502, "text/plain; charset=utf-8", headers, body, null); + } + + URL resolved = new URL(target, location); + con.disconnect(); + return proxyGetInternalV101(ctx, resolved.toString(), header, depth + 1); + } + + String contentType = con.getContentType(); + if (contentType == null || contentType.isBlank()) contentType = "application/octet-stream"; + + byte[] body; + Map outHeaders = new LinkedHashMap<>(); + try (InputStream in = (code >= 400 ? con.getErrorStream() : con.getInputStream())) { + body = readAllBytes(in); + outHeaders.put("content-length", String.valueOf(body.length)); + + // Pass through a few useful headers (safe subset) + copyHeaderIfPresent(con, outHeaders, "cache-control"); + copyHeaderIfPresent(con, outHeaders, "etag"); + copyHeaderIfPresent(con, outHeaders, "last-modified"); + } finally { + con.disconnect(); + } + + return new WebResourceResponsePacket( + header, + code, + contentType, + outHeaders, + body, + null + ); + } + + private static void copyHeaderIfPresent(HttpsURLConnection con, Map out, String name) { + String v = con.getHeaderField(name); + if (v != null && !v.isBlank()) out.put(name, v); + } + + private static String getHeaderIgnoreCase(Map headers, String key) { + if (headers == null || headers.isEmpty() || key == null) return null; + String needle = key.trim().toLowerCase(java.util.Locale.ROOT); + for (Map.Entry e : headers.entrySet()) { + if (e.getKey() == null) continue; + if (e.getKey().trim().toLowerCase(java.util.Locale.ROOT).equals(needle)) return e.getValue(); + } + return null; + } + + private static byte[] readAllBytes(InputStream in) throws Exception { + if (in == null) return new byte[0]; + ByteArrayOutputStream out = new ByteArrayOutputStream(Math.max(1024, in.available())); + byte[] buf = new byte[32 * 1024]; + int r; + while ((r = in.read(buf)) != -1) { + out.write(buf, 0, r); + } + return out.toByteArray(); + } + + /** + * Fetches an HTTPS URL and returns it as a v1.0.0 {@link WebResponsePacket}. * * @param ctx the current web page context (for optional user-agent forwarding) * @param url the target HTTPS URL * @return proxied response + * @deprecated v1.0.1 code should call {@link #proxyGet(WebPageContext, String)} returning {@link WebResourceResponsePacket}. */ - public static WebResponsePacket proxyGet(WebPageContext ctx, String url) { + @Deprecated(forRemoval = false, since = "1.0.1-BETA.0.1") + public static WebResponsePacket proxyGetV100B(WebPageContext ctx, String url) { try { - return proxyGetInternal(ctx, url, 0); + return proxyGetInternalV100B(ctx, url, 0); } catch (Exception e) { return new WebResponsePacket( 502, @@ -47,7 +187,7 @@ public final class HttpsProxy { } } - private static WebResponsePacket proxyGetInternal(WebPageContext ctx, String url, int depth) throws Exception { + private static WebResponsePacket proxyGetInternalV100B(WebPageContext ctx, String url, int depth) throws Exception { if (depth > MAX_REDIRECTS) { return new WebResponsePacket( 508, @@ -64,16 +204,14 @@ public final class HttpsProxy { con.setConnectTimeout(CONNECT_TIMEOUT_MS); con.setReadTimeout(READ_TIMEOUT_MS); - // Forward a user-agent if you have one (optional). String ua = null; if (ctx != null && ctx.request != null && ctx.request.getHeaders() != null) { - ua = ctx.request.getHeaders().get("user-agent"); + ua = getHeaderIgnoreCase(ctx.request.getHeaders(), "user-agent"); } con.setRequestProperty("User-Agent", ua != null ? ua : "OAC-HttpsProxy/1.0"); int code = con.getResponseCode(); - // Handle redirects manually to preserve content and avoid silent issues if (code == 301 || code == 302 || code == 303 || code == 307 || code == 308) { String location = con.getHeaderField("Location"); if (location == null || location.isBlank()) { @@ -84,16 +222,13 @@ public final class HttpsProxy { ("Bad Gateway: redirect without Location (code=" + code + ")").getBytes(StandardCharsets.UTF_8) ); } - // Resolve relative redirects URL resolved = new URL(target, location); con.disconnect(); - return proxyGetInternal(ctx, resolved.toString(), depth + 1); + return proxyGetInternalV100B(ctx, resolved.toString(), depth + 1); } String contentType = con.getContentType(); - if (contentType == null || contentType.isBlank()) { - contentType = "application/octet-stream"; - } + if (contentType == null || contentType.isBlank()) contentType = "application/octet-stream"; byte[] body; try (InputStream in = (code >= 400 ? con.getErrorStream() : con.getInputStream())) { @@ -104,15 +239,4 @@ public final class HttpsProxy { return new WebResponsePacket(code, contentType, HeaderMaps.mutable(), body); } - - private static byte[] readAllBytes(InputStream in) throws Exception { - if (in == null) return new byte[0]; - ByteArrayOutputStream out = new ByteArrayOutputStream(Math.max(1024, in.available())); - byte[] buf = new byte[32 * 1024]; - int r; - while ((r = in.read(buf)) != -1) { - out.write(buf, 0, r); - } - return out.toByteArray(); - } -} +} \ No newline at end of file diff --git a/src/main/java/org/openautonomousconnection/webserver/utils/MergedRequestParams.java b/src/main/java/org/openautonomousconnection/webserver/utils/MergedRequestParams.java index a4c75f2..0180c70 100644 --- a/src/main/java/org/openautonomousconnection/webserver/utils/MergedRequestParams.java +++ b/src/main/java/org/openautonomousconnection/webserver/utils/MergedRequestParams.java @@ -33,20 +33,20 @@ public final class MergedRequestParams { public static MergedRequestParams from(String rawTarget, Map headers, byte[] body) { Map> merged = new LinkedHashMap<>(); - // 1) Query string + // Query string String query = extractQuery(rawTarget); if (query != null && !query.isBlank()) { mergeInto(merged, parseUrlEncoded(query, StandardCharsets.UTF_8), false); } - // 2) Body + // Body if (body != null && body.length > 0) { String contentType = header(headers, "content-type"); Map> bodyParams = parseBody(contentType, body); mergeInto(merged, bodyParams, true); } - return new MergedRequestParams(merged); + return new MergedRequestParams(Collections.unmodifiableMap(merged)); } private static String extractQuery(String rawTarget) { @@ -70,16 +70,20 @@ public final class MergedRequestParams { private static void mergeInto(Map> target, Map> src, boolean override) { if (src == null || src.isEmpty()) return; + for (Map.Entry> e : src.entrySet()) { if (e.getKey() == null) continue; + String k = e.getKey(); - List vals = e.getValue() == null ? List.of() : e.getValue(); + List vals = (e.getValue() == null) ? List.of() : e.getValue(); + if (!override && target.containsKey(k)) { - // append + // Always keep ArrayList in target to allow appends safely. target.get(k).addAll(vals); continue; } - // override or insert + + // Insert/override with a mutable list to preserve later merge behavior. target.put(k, new ArrayList<>(vals)); } } @@ -166,6 +170,7 @@ public final class MergedRequestParams { /** * Minimal multipart parser for text fields only. + * *

Ignores file uploads and binary content.

*/ private static Map> parseMultipartTextFields(byte[] body, String boundary, Charset charset) { @@ -293,13 +298,8 @@ public final class MergedRequestParams { continue; } } - // ISO-8859-1 safe char -> byte if (c <= 0xFF) baos.write((byte) c); - else { - // for non-latin chars, fall back to UTF-8 bytes of that char - byte[] b = String.valueOf(c).getBytes(charset); - baos.writeBytes(b); - } + else baos.writeBytes(String.valueOf(c).getBytes(charset)); } return baos.toString(charset); } diff --git a/src/main/java/org/openautonomousconnection/webserver/utils/RequestParams.java b/src/main/java/org/openautonomousconnection/webserver/utils/RequestParams.java index f58f1bd..f0ba521 100644 --- a/src/main/java/org/openautonomousconnection/webserver/utils/RequestParams.java +++ b/src/main/java/org/openautonomousconnection/webserver/utils/RequestParams.java @@ -1,25 +1,47 @@ package org.openautonomousconnection.webserver.utils; import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.WebRequestPacket; +import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.resource.WebResourceRequestPacket; import java.util.Collections; import java.util.Locale; import java.util.Map; /** - * Reads parameters from WebRequestPacket headers (case-insensitive). + * Reads parameters from request headers (case-insensitive). * - *

Additionally provides hashing helpers via a supplied {@link WebHasher}. + *

This utility is used by server-side pages to access request metadata.

*/ public final class RequestParams { private final Map headers; /** - * Creates a param reader. + * Creates a param reader from v1.0.1 resource request headers. * - * @param request request + * @param request v1.0.1 resource request (may be null) */ + public RequestParams(WebResourceRequestPacket request) { + Map h = request != null ? request.getHeaders() : null; + this.headers = (h == null) ? Collections.emptyMap() : h; + } + + /** + * Creates a param reader from a header map. + * + * @param headers headers (may be null) + */ + public RequestParams(Map headers) { + this.headers = (headers == null) ? Collections.emptyMap() : headers; + } + + /** + * v1.0.0 constructor (v1.0.0). + * + * @param request v1.0.0 request (may be null) + * @deprecated v1.0.1 uses {@link WebResourceRequestPacket}. Keep only for older modules still compiled in. + */ + @Deprecated(forRemoval = false, since = "1.0.1-BETA.0.1") public RequestParams(WebRequestPacket request) { Map h = request != null ? request.getHeaders() : null; this.headers = (h == null) ? Collections.emptyMap() : h; @@ -92,8 +114,9 @@ public final class RequestParams { * @return sha256 hex (or null if missing) */ public String getSha256Hex(WebHasher hasher, String key) { + if (hasher == null) throw new IllegalArgumentException("hasher is null"); String v = getTrimmed(key); if (v == null) return null; return hasher.sha256Hex(v); } -} +} \ 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 new file mode 100644 index 0000000..dc0df2b --- /dev/null +++ b/src/main/java/org/openautonomousconnection/webserver/utils/WebUrlUtil.java @@ -0,0 +1,50 @@ +package org.openautonomousconnection.webserver.utils; + +import java.net.URI; + +/** + * URL utilities for the server. + */ +public final class WebUrlUtil { + + private WebUrlUtil() { + } + + /** + * Extracts path + query from an absolute URL string. + * + * @param url absolute URL + * @return path + optional query (e.g. "/a/b?x=1") or null + */ + public static String extractPathAndQuery(String url) { + try { + URI u = URI.create(url); + String p = u.getPath(); + if (p == null || p.isBlank()) p = "/"; + String q = u.getRawQuery(); + return (q == null || q.isBlank()) ? p : (p + "?" + q); + } catch (Exception e) { + return null; + } + } + + /** + * Normalizes a requested path to a content-root relative path. + * + * @param pathWithQuery "/foo/bar?x=1" + * @return "foo/bar" (default: "index.html") + */ + public static String normalizeToContentPath(String pathWithQuery) { + String p = pathWithQuery; + int q = p.indexOf('?'); + if (q >= 0) p = p.substring(0, q); + + if (p == null || p.isBlank() || "/".equals(p)) { + return "index.html"; + } + + if (p.startsWith("/")) p = p.substring(1); + if (p.isBlank()) return "index.html"; + return p; + } +} \ No newline at end of file