Bug fixes
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.openautonomousconnection</groupId>
|
||||
<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>
|
||||
<url>https://open-autonomous-connection.org/</url>
|
||||
<issueManagement>
|
||||
|
||||
4
pom.xml
4
pom.xml
@@ -6,7 +6,7 @@
|
||||
|
||||
<groupId>org.openautonomousconnection</groupId>
|
||||
<artifactId>WebServer</artifactId>
|
||||
<version>1.0.1-BETA.0.1</version>
|
||||
<version>1.0.1-BETA.0.3</version>
|
||||
<organization>
|
||||
<name>Open Autonomous Connection</name>
|
||||
<url>https://open-autonomous-connection.org/</url>
|
||||
@@ -77,7 +77,7 @@
|
||||
<dependency>
|
||||
<groupId>org.openautonomousconnection</groupId>
|
||||
<artifactId>Protocol</artifactId>
|
||||
<version>1.0.1-BETA.0.3</version>
|
||||
<version>1.0.1-BETA.0.5</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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!");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
* <p>Implements the three v1.0.1 entry points:</p>
|
||||
* <ul>
|
||||
* <li>Navigation: {@link #onNavigateRequest(CustomConnectedClient, WebNavigateRequestPacket)}</li>
|
||||
* <li>Resource: {@link #onResourceRequest(CustomConnectedClient, WebResourceRequestPacket)}</li>
|
||||
* <li>Document apply: {@link #onDocumentApplyRequest(CustomConnectedClient, WebDocumentApplyRequestPacket)}</li>
|
||||
* </ul>
|
||||
* <p>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.</p>
|
||||
*
|
||||
* <p>Supports:</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>
|
||||
* <p>Therefore, request URLs are parsed using {@link java.net.URI} which accepts unknown schemes.</p>
|
||||
*/
|
||||
@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<String, String> 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.
|
||||
*
|
||||
* <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".
|
||||
*/
|
||||
@@ -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,38 +571,22 @@ 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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
* <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.
|
||||
*
|
||||
@@ -39,9 +60,7 @@ 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";
|
||||
|
||||
Reference in New Issue
Block a user