Bug fixes

This commit is contained in:
UnlegitDqrk
2026-02-27 20:31:10 +01:00
parent 5642869097
commit aa5963378d
8 changed files with 113 additions and 79 deletions

View File

@@ -3,7 +3,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<groupId>org.openautonomousconnection</groupId> <groupId>org.openautonomousconnection</groupId>
<artifactId>WebServer</artifactId> <artifactId>WebServer</artifactId>
<version>1.0.1-BETA.0.1</version> <version>1.0.1-BETA.0.3</version>
<description>The default DNS-Server</description> <description>The default DNS-Server</description>
<url>https://open-autonomous-connection.org/</url> <url>https://open-autonomous-connection.org/</url>
<issueManagement> <issueManagement>

View File

@@ -6,7 +6,7 @@
<groupId>org.openautonomousconnection</groupId> <groupId>org.openautonomousconnection</groupId>
<artifactId>WebServer</artifactId> <artifactId>WebServer</artifactId>
<version>1.0.1-BETA.0.1</version> <version>1.0.1-BETA.0.3</version>
<organization> <organization>
<name>Open Autonomous Connection</name> <name>Open Autonomous Connection</name>
<url>https://open-autonomous-connection.org/</url> <url>https://open-autonomous-connection.org/</url>
@@ -77,7 +77,7 @@
<dependency> <dependency>
<groupId>org.openautonomousconnection</groupId> <groupId>org.openautonomousconnection</groupId>
<artifactId>Protocol</artifactId> <artifactId>Protocol</artifactId>
<version>1.0.1-BETA.0.3</version> <version>1.0.1-BETA.0.5</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.projectlombok</groupId> <groupId>org.projectlombok</groupId>

View File

@@ -39,7 +39,7 @@ public final class ContentTypeResolver {
return isFile(name, "image"); return isFile(name, "image");
} }
public static boolean isAudio(String name) { public static boolean isAudioFile(String name) {
return isFile(name, "audio"); return isFile(name, "audio");
} }

View File

@@ -10,11 +10,11 @@ public class Listener extends EventListener {
public void onCommandNotFound(CommandNotFoundEvent event) { public void onCommandNotFound(CommandNotFoundEvent event) {
StringBuilder argsBuilder = new StringBuilder(); StringBuilder argsBuilder = new StringBuilder();
for (String arg : event.getArgs()) argsBuilder.append(arg).append(" "); 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 @dev.unlegitdqrk.unlegitlibrary.event.Listener
public void onMissingCommandPermission(CommandExecutorMissingPermissionEvent event) { 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!");
} }
} }

View File

@@ -37,6 +37,9 @@ public class Main {
values = new ProtocolValues(); values = new ProtocolValues();
values.packetHandler = new PacketHandler(); values.packetHandler = new PacketHandler();
values.eventManager = new EventManager(); 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())) if (!Files.exists(new File("config.properties").toPath()))
Files.createFile(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 sessionExpire = config.getInt("sessionexpiremin");
int maxUpload = config.getInt("maxuploadmb"); int maxUpload = config.getInt("maxuploadmb");
ClientAuthMode authMode = ClientAuthMode.valueOf(config.getString("clientauth").toUpperCase()); values.authMode = ClientAuthMode.valueOf(config.getString("clientauth").toUpperCase());
values.authMode = authMode;
Logger logger = new Logger(new File("logs"), false, true);
values.eventManager.registerListener(Listener.class); values.eventManager.registerListener(Listener.class);
protocolBridge = new ProtocolBridge(new WebServer( protocolBridge = new ProtocolBridge(new WebServer(
new File("auth.ini"), new File("rules.ini"), new File("auth.ini"), new File("rules.ini"),
sessionExpire, maxUpload), sessionExpire, maxUpload), values);
values, ProtocolVersion.PV_1_0_1_BETA, logger, new AddonLoader(values.eventManager, logger));
protocolBridge.getProtocolServer().getNetwork().start(tcpPort, udpPort); protocolBridge.getProtocolServer().getNetwork().start(tcpPort, udpPort);

View File

@@ -20,12 +20,13 @@ import org.openautonomousconnection.protocol.versions.v1_0_1.beta.WebPacketHeade
import org.openautonomousconnection.webserver.api.SessionContext; import org.openautonomousconnection.webserver.api.SessionContext;
import org.openautonomousconnection.webserver.runtime.JavaPageDispatcher; import org.openautonomousconnection.webserver.runtime.JavaPageDispatcher;
import org.openautonomousconnection.webserver.utils.WebHasher; import org.openautonomousconnection.webserver.utils.WebHasher;
import org.openautonomousconnection.webserver.utils.WebUrlUtil;
import java.io.BufferedInputStream; import java.io.BufferedInputStream;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.InputStream; import java.io.InputStream;
import java.net.URL; import java.net.URI;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.util.Arrays; import java.util.Arrays;
@@ -33,27 +34,16 @@ import java.util.LinkedHashMap;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
/** /**
* OAC WebServer implementation for WEB v1.0.1-BETA. * OAC WebServer implementation for WEB v1.0.1-BETA.
* *
* <p>Implements the three v1.0.1 entry points:</p> * <p>Important: Server-side parsing MUST NOT use {@link java.net.URL} for "web://" because
* <ul> * the server JVM does not need (and typically does not have) a URLStreamHandler installed.</p>
* <li>Navigation: {@link #onNavigateRequest(CustomConnectedClient, WebNavigateRequestPacket)}</li>
* <li>Resource: {@link #onResourceRequest(CustomConnectedClient, WebResourceRequestPacket)}</li>
* <li>Document apply: {@link #onDocumentApplyRequest(CustomConnectedClient, WebDocumentApplyRequestPacket)}</li>
* </ul>
* *
* <p>Supports:</p> * <p>Therefore, request URLs are parsed using {@link java.net.URI} which accepts unknown schemes.</p>
* <ul>
* <li>Static file serving</li>
* <li>Java-page dispatch (server-side pages)</li>
* <li>Range requests ("bytes=start-end")</li>
* <li>Streaming for large responses (best-effort over UDP for video chunks)</li>
* </ul>
*/ */
@ProtocolInfo(protocolSide = ProtocolVersion.ProtocolSide.WEB) @ProtocolInfo(protocolSide = ProtocolVersion.ProtocolSide.WEB)
public final class WebServer extends ProtocolWebServer { public final class WebServer extends ProtocolWebServer {
@@ -95,9 +85,10 @@ public final class WebServer extends ProtocolWebServer {
Objects.requireNonNull(request, "request"); Objects.requireNonNull(request, "request");
final WebPacketHeader in = request.getHeader(); final WebPacketHeader in = request.getHeader();
final URL url;
final ParsedRequestUrl parsed;
try { try {
url = new URL(request.getUrl()); parsed = ParsedRequestUrl.parse(request.getUrl());
} catch (Exception e) { } catch (Exception e) {
return new WebNavigateAckPacket( return new WebNavigateAckPacket(
mirrorHeader(in, WebPacketFlags.NAVIGATION), 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)) { if (RuleManager.isDenied(path)) {
return new WebNavigateAckPacket( return new WebNavigateAckPacket(
mirrorHeader(in, WebPacketFlags.NAVIGATION), mirrorHeader(in, WebPacketFlags.NAVIGATION),
@@ -147,14 +138,15 @@ public final class WebServer extends ProtocolWebServer {
Objects.requireNonNull(request, "request"); Objects.requireNonNull(request, "request");
final WebPacketHeader in = request.getHeader(); final WebPacketHeader in = request.getHeader();
final URL url;
final ParsedRequestUrl parsed;
try { try {
url = new URL(request.getUrl()); parsed = ParsedRequestUrl.parse(request.getUrl());
} catch (Exception e) { } catch (Exception e) {
return error(in, 400, "Invalid URL: " + e.getMessage()); 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)) { if (RuleManager.isDenied(path) || !RuleManager.isAllowed(path)) {
return error(in, 403, "Forbidden"); return error(in, 403, "Forbidden");
@@ -172,7 +164,7 @@ public final class WebServer extends ProtocolWebServer {
} }
try { try {
WebResourceResponsePacket javaResp = dispatchJavaPageAsResource(client, request, url); WebResourceResponsePacket javaResp = dispatchJavaPageAsResource(client, request);
if (javaResp != null) { if (javaResp != null) {
return javaResp; return javaResp;
} }
@@ -181,7 +173,7 @@ public final class WebServer extends ProtocolWebServer {
} }
try { try {
return serveStaticFile(client, request, url); return serveStaticFile(client, request, parsed);
} catch (Exception e) { } catch (Exception e) {
return error(in, 500, "Internal error: " + e.getClass().getSimpleName() + ": " + e.getMessage()); 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); 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(); final WebPacketHeader in = request.getHeader();
String path = normalizePath(url.getPath()); String path = normalizePath(parsed.path());
if (path.startsWith("/")) path = path.substring(1); if (path.startsWith("/")) path = path.substring(1);
if (path.isEmpty()) path = "index.html"; 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) { private static boolean shouldStream(String fileName, String contentType, long size, boolean wantsRange) {
if (size >= STREAM_THRESHOLD) return true; if (size >= STREAM_THRESHOLD) return true;
// Many video/audio players use Range for progressive playback. if (wantsRange && (ContentTypeResolver.isVideoFile(fileName) || ContentTypeResolver.isAudioFile(fileName))) {
// 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 true;
} }
@@ -422,14 +412,11 @@ public final class WebServer extends ProtocolWebServer {
} }
private static TransportProtocol chooseChunkTransport(String fileName) { private static TransportProtocol chooseChunkTransport(String fileName) {
// best-effort: chunks via UDP for video, TCP otherwise
return ContentTypeResolver.isVideoFile(fileName) ? TransportProtocol.UDP : TransportProtocol.TCP; return ContentTypeResolver.isVideoFile(fileName) ? TransportProtocol.UDP : TransportProtocol.TCP;
} }
private static String normalizePath(String p) { private static String normalizePath(String p) {
if (p == null || p.isBlank()) return "/"; return WebUrlUtil.normalizeRequestPath(p);
String t = p.trim();
return t.isEmpty() ? "/" : t;
} }
private static String contentTypeOrDefault(String ct) { private static String contentTypeOrDefault(String ct) {
@@ -443,7 +430,6 @@ public final class WebServer extends ProtocolWebServer {
} }
private static Map<String, String> safeHeaders(WebNavigateRequestPacket request) { private static Map<String, String> safeHeaders(WebNavigateRequestPacket request) {
// Navigate packet currently has no headers in your definition; keep empty.
return Map.of(); 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.
*
* <p>Uses {@link URI} because it supports unknown schemes such as "web".</p>
*/
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". * 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; 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) { public static RangeSpec parse(String header, long size) {
if (header == null || header.isBlank()) return null; if (header == null || header.isBlank()) return null;
String h = header.trim().toLowerCase(Locale.ROOT); String h = header.trim().toLowerCase(Locale.ROOT);
if (!h.startsWith("bytes=")) return null; if (!h.startsWith("bytes=")) return null;
String v = h.substring("bytes=".length()).trim(); String v = h.substring("bytes=".length()).trim();
// Only single-range supported
int comma = v.indexOf(','); int comma = v.indexOf(',');
if (comma >= 0) v = v.substring(0, comma).trim(); 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); return new RangeSpec(start, end, true);
} }
// suffix range: "-N" (last N bytes)
if (!b.isEmpty()) { if (!b.isEmpty()) {
long suffix = Long.parseLong(b); long suffix = Long.parseLong(b);
if (suffix <= 0) return new RangeSpec(0, 0, false); 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() { public boolean isValid() {
return valid; return valid;
} }
/**
* @return inclusive start
*/
public long startInclusive() { public long startInclusive() {
return startInclusive; return startInclusive;
} }
/**
* @return inclusive end
*/
public long endInclusive() { public long endInclusive() {
return endInclusive; return endInclusive;
} }
/**
* @return byte length represented by this range
*/
public long length() { public long length() {
return (endInclusive >= startInclusive) ? (endInclusive - startInclusive + 1) : 0L; return (endInclusive >= startInclusive) ? (endInclusive - startInclusive + 1) : 0L;
} }
/**
* @param total total size
* @return Content-Range header value
*/
public String toContentRange(long total) { public String toContentRange(long total) {
return "bytes " + startInclusive + "-" + endInclusive + "/" + total; return "bytes " + startInclusive + "-" + endInclusive + "/" + total;
} }
} }
} }

View File

@@ -50,9 +50,7 @@ public final class JavaPageDispatcher {
int q = route.indexOf('?'); int q = route.indexOf('?');
if (q >= 0) route = route.substring(0, q); if (q >= 0) route = route.substring(0, q);
if (!route.startsWith("/")) { route = WebUrlUtil.normalizeRequestPath(route);
route = "/" + route;
}
java.io.File contentRoot = server.getContentFolder(); java.io.File contentRoot = server.getContentFolder();
ROUTES.refreshIfNeeded(contentRoot); ROUTES.refreshIfNeeded(contentRoot);
@@ -113,4 +111,4 @@ public final class JavaPageDispatcher {
null null
); );
} }
} }

View File

@@ -7,6 +7,8 @@ import java.net.URI;
*/ */
public final class WebUrlUtil { public final class WebUrlUtil {
private static final String DEFAULT_DOCUMENT_PATH = "/index.html";
private WebUrlUtil() { private WebUrlUtil() {
} }
@@ -28,6 +30,25 @@ public final class WebUrlUtil {
} }
} }
/**
* Normalizes a request path for route/rule lookup.
*
* <p>Blank paths and "/" resolve to the default document.</p>
*
* @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. * Normalizes a requested path to a content-root relative path.
* *
@@ -39,12 +60,10 @@ public final class WebUrlUtil {
int q = p.indexOf('?'); int q = p.indexOf('?');
if (q >= 0) p = p.substring(0, q); if (q >= 0) p = p.substring(0, q);
if (p == null || p.isBlank() || "/".equals(p)) { p = normalizeRequestPath(p);
return "index.html";
}
if (p.startsWith("/")) p = p.substring(1); if (p.startsWith("/")) p = p.substring(1);
if (p.isBlank()) return "index.html"; if (p.isBlank()) return "index.html";
return p; return p;
} }
} }