15 Commits

Author SHA1 Message Date
a9bcdb42bf Fixed mistake where document doesn't actually have the created body or head in WebPage.java 2026-03-02 17:30:54 +00:00
Tinglyyy
7fa378c236 Merge remote-tracking branch 'origin/dev' into dev 2026-03-02 17:42:05 +01:00
Tinglyyy
8ffc5fb5ec Fixed mistake where document doesn't actually have the created body or head in WebPage.java 2026-03-02 17:41:39 +01:00
1a7a4617be Update src/main/java/org/openautonomousconnection/webserver/api/WebPage.java 2026-03-02 16:32:42 +00:00
14e2af25d6 Update src/main/java/org/openautonomousconnection/webserver/api/WebPage.java 2026-03-02 16:32:28 +00:00
0a821fc8e5 Update README.MD 2026-03-02 16:32:06 +00:00
Tinglyyy
2d63489b86 Added Document utility to WebPage.java 2026-03-02 17:12:49 +01:00
93df6ac904 Merge pull request 'master' (#1) from master into dev
Reviewed-on: #1
2026-03-02 13:23:02 +00:00
UnlegitDqrk
6c8269441d Updated to latest Protocol Version 2026-02-28 15:44:31 +01:00
UnlegitDqrk
8369c6aaf2 Updated to latest Protocol Version 2026-02-28 15:44:11 +01:00
UnlegitDqrk
cfe178ae34 Updated to latest Protocol Version 2026-02-28 15:42:07 +01:00
UnlegitDqrk
aa5963378d Bug fixes 2026-02-27 20:31:10 +01:00
UnlegitDqrk
5642869097 Updated to latest Protocol Version 2026-02-22 17:26:22 +01:00
UnlegitDqrk
5058e41ce1 Removed unused licenses 2026-02-14 19:19:52 +01:00
UnlegitDqrk
98f9fa3a74 Removed unused licenses 2026-02-14 19:19:22 +01:00
17 changed files with 965 additions and 265 deletions

2
.idea/misc.xml generated
View File

@@ -8,7 +8,7 @@
</list> </list>
</option> </option>
</component> </component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_23" default="true" project-jdk-name="23" project-jdk-type="JavaSDK"> <component name="ProjectRootManager" version="2" languageLevel="JDK_25" default="true" project-jdk-name="25" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" /> <output url="file://$PROJECT_DIR$/out" />
</component> </component>
</project> </project>

View File

@@ -1 +1,2 @@
Please read the license here: https://open-autonomous-connection.org/license.html 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

View File

@@ -10,7 +10,8 @@ This project (OAC) is licensed under
the [Open Autonomous Public License (OAPL)](https://open-autonomous-connection.org/license.html). the [Open Autonomous Public License (OAPL)](https://open-autonomous-connection.org/license.html).
**Third-party components:** **Third-party components:**
<br />
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: - *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 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), the [GNU GPLv3](https://repo.unlegitdqrk.dev/UnlegitDqrk/unlegitlibrary/src/branch/master/LICENSE),
@@ -22,5 +23,3 @@ the [Open Autonomous Public License (OAPL)](https://open-autonomous-connection.o
# In progress # In progress
# TODO # TODO
everything

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.0-BETA.1.0</version> <version>1.0.1-BETA.0.4</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>
@@ -37,44 +37,6 @@
<name>Open Autonomous Public License (OAPL)</name> <name>Open Autonomous Public License (OAPL)</name>
<url>https://open-autonomous-connection.org/license.html</url> <url>https://open-autonomous-connection.org/license.html</url>
</license> </license>
<license>
<name>GNU General Public License v3.0</name>
<url>https://www.gnu.org/licenses/gpl-3.0.html</url>
<comments>Default license: Applies to all users and projects unless an explicit alternative license has been
granted.</comments>
</license>
<license>
<name>LPGL 3</name>
<url>https://www.gnu.org/licenses/lgpl-3.0.html#license-text</url>
</license>
<license>
<name>LPGL 2.1</name>
<url>https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.en#SEC1</url>
</license>
<license>
<name>WTPL License</name>
<url>https://github.com/ronmamo/reflections/tree/master?tab=WTFPL-1-ov-file</url>
</license>
<license>
<name>Apache License 2.0</name>
<url>https://www.apache.org/licenses/LICENSE-2.0.txt</url>
</license>
<license>
<name>MIT License</name>
<url>https://opensource.org/license/mit</url>
</license>
<license>
<name>javassist</name>
<url>https://github.com/jboss-javassist/javassist/blob/master/License.html</url>
</license>
<license>
<name>projectlombok</name>
<url>https://github.com/projectlombok/lombok?tab=License-1-ov-file</url>
</license>
<license>
<name>mariadb</name>
<url>https://mariadb.com/docs/general-resources/community/community/faq/licensing-questions/licensing-faq</url>
</license>
</licenses> </licenses>
<organization> <organization>
<name>Open Autonomous Connection</name> <name>Open Autonomous Connection</name>
@@ -82,6 +44,21 @@
</organization> </organization>
<build> <build>
<plugins> <plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.42</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin> <plugin>
<artifactId>maven-shade-plugin</artifactId> <artifactId>maven-shade-plugin</artifactId>
<version>3.6.0</version> <version>3.6.0</version>
@@ -147,7 +124,7 @@
<dependency> <dependency>
<groupId>org.projectlombok</groupId> <groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId> <artifactId>lombok</artifactId>
<version>1.18.38</version> <version>1.18.42</version>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
</dependencies> </dependencies>
@@ -160,7 +137,7 @@
</distributionManagement> </distributionManagement>
<properties> <properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.target>23</maven.compiler.target> <maven.compiler.target>25</maven.compiler.target>
<maven.compiler.source>23</maven.compiler.source> <maven.compiler.source>25</maven.compiler.source>
</properties> </properties>
</project> </project>

68
pom.xml
View File

@@ -6,7 +6,7 @@
<groupId>org.openautonomousconnection</groupId> <groupId>org.openautonomousconnection</groupId>
<artifactId>WebServer</artifactId> <artifactId>WebServer</artifactId>
<version>1.0.0-BETA.1.0</version> <version>1.0.1-BETA.0.5</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>
@@ -15,8 +15,8 @@
<description>The default DNS-Server</description> <description>The default DNS-Server</description>
<properties> <properties>
<maven.compiler.source>23</maven.compiler.source> <maven.compiler.source>25</maven.compiler.source>
<maven.compiler.target>23</maven.compiler.target> <maven.compiler.target>25</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties> </properties>
@@ -61,47 +61,6 @@
<name>Open Autonomous Public License (OAPL)</name> <name>Open Autonomous Public License (OAPL)</name>
<url>https://open-autonomous-connection.org/license.html</url> <url>https://open-autonomous-connection.org/license.html</url>
</license> </license>
<license>
<name>GNU General Public License v3.0</name>
<url>https://www.gnu.org/licenses/gpl-3.0.html</url>
<comments>
Default license: Applies to all users and projects unless an explicit alternative license has been
granted.
</comments>
</license>
<license>
<name>LPGL 3</name>
<url>https://www.gnu.org/licenses/lgpl-3.0.html#license-text</url>
</license>
<license>
<name>LPGL 2.1</name>
<url>https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.en#SEC1</url>
</license>
<license>
<name>WTPL License</name>
<url>https://github.com/ronmamo/reflections/tree/master?tab=WTFPL-1-ov-file</url>
</license>
<license>
<name>Apache License 2.0</name>
<url>https://www.apache.org/licenses/LICENSE-2.0.txt</url>
</license>
<license>
<name>MIT License</name>
<url>https://opensource.org/license/mit</url>
</license>
<license>
<name>javassist</name>
<url>https://github.com/jboss-javassist/javassist/blob/master/License.html</url>
</license>
<license>
<name>projectlombok</name>
<url>https://github.com/projectlombok/lombok?tab=License-1-ov-file</url>
</license>
<license>
<name>mariadb</name>
<url>https://mariadb.com/docs/general-resources/community/community/faq/licensing-questions/licensing-faq
</url>
</license>
</licenses> </licenses>
<repositories> <repositories>
@@ -118,12 +77,12 @@
<dependency> <dependency>
<groupId>org.openautonomousconnection</groupId> <groupId>org.openautonomousconnection</groupId>
<artifactId>Protocol</artifactId> <artifactId>Protocol</artifactId>
<version>1.0.0-BETA.1.0</version> <version>1.0.1-BETA.0.6</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.projectlombok</groupId> <groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId> <artifactId>lombok</artifactId>
<version>1.18.38</version> <version>1.18.42</version>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<dependency> <dependency>
@@ -135,6 +94,23 @@
<build> <build>
<plugins> <plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.42</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId> <artifactId>maven-shade-plugin</artifactId>

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

@@ -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().getProtocolValues().logger.error("Command '" + event.getName() + argsBuilder.toString() + "' not found!");
}
@dev.unlegitdqrk.unlegitlibrary.event.Listener
public void onMissingCommandPermission(CommandExecutorMissingPermissionEvent event) {
Main.getProtocolBridge().getProtocolValues().logger.error("You do not have enough permissions to execute this command!");
}
}

View File

@@ -1,5 +1,6 @@
package org.openautonomousconnection.webserver; package org.openautonomousconnection.webserver;
import dev.unlegitdqrk.unlegitlibrary.addon.AddonLoader;
import dev.unlegitdqrk.unlegitlibrary.command.CommandExecutor; import dev.unlegitdqrk.unlegitlibrary.command.CommandExecutor;
import dev.unlegitdqrk.unlegitlibrary.command.CommandManager; import dev.unlegitdqrk.unlegitlibrary.command.CommandManager;
import dev.unlegitdqrk.unlegitlibrary.command.CommandPermission; 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.file.ConfigurationManager;
import dev.unlegitdqrk.unlegitlibrary.network.system.packets.PacketHandler; import dev.unlegitdqrk.unlegitlibrary.network.system.packets.PacketHandler;
import dev.unlegitdqrk.unlegitlibrary.network.system.utils.ClientAuthMode; import dev.unlegitdqrk.unlegitlibrary.network.system.utils.ClientAuthMode;
import dev.unlegitdqrk.unlegitlibrary.utils.Logger;
import lombok.Getter; import lombok.Getter;
import org.openautonomousconnection.protocol.ProtocolBridge; import org.openautonomousconnection.protocol.ProtocolBridge;
import org.openautonomousconnection.protocol.ProtocolValues; import org.openautonomousconnection.protocol.ProtocolValues;
@@ -35,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());
@@ -74,13 +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; 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_0_BETA, new File("logs"));
protocolBridge.getProtocolServer().getNetwork().start(tcpPort, udpPort); protocolBridge.getProtocolServer().getNetwork().start(tcpPort, udpPort);

View File

@@ -1,168 +1,594 @@
package org.openautonomousconnection.webserver; package org.openautonomousconnection.webserver;
import dev.unlegitdqrk.unlegitlibrary.network.system.utils.TransportProtocol; import dev.unlegitdqrk.unlegitlibrary.network.system.utils.TransportProtocol;
import lombok.Getter;
import org.openautonomousconnection.protocol.annotations.ProtocolInfo; import org.openautonomousconnection.protocol.annotations.ProtocolInfo;
import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.WebRequestPacket; import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.document.WebDocumentApplyRequestPacket;
import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.WebResponsePacket; import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.document.WebDocumentApplyResponsePacket;
import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.stream.WebStreamChunkPacket; import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.navigate.WebNavigateAckPacket;
import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.stream.WebStreamEndPacket; import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.navigate.WebNavigateRequestPacket;
import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.stream.WebStreamStartPacket; 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.server.CustomConnectedClient;
import org.openautonomousconnection.protocol.side.web.ProtocolWebServer; import org.openautonomousconnection.protocol.side.web.ProtocolWebServer;
import org.openautonomousconnection.protocol.side.web.managers.RuleManager; import org.openautonomousconnection.protocol.side.web.managers.RuleManager;
import org.openautonomousconnection.protocol.versions.ProtocolVersion; 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.api.SessionContext;
import org.openautonomousconnection.webserver.runtime.JavaPageDispatcher; import org.openautonomousconnection.webserver.runtime.JavaPageDispatcher;
import org.openautonomousconnection.webserver.utils.HeaderMaps;
import org.openautonomousconnection.webserver.utils.WebHasher; import org.openautonomousconnection.webserver.utils.WebHasher;
import org.openautonomousconnection.webserver.utils.WebUrlUtil;
import java.io.*; import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.util.Arrays; import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
/** /**
* OAC WebServer implementation. * OAC WebServer implementation for WEB v1.0.1-BETA.
*
* <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>Therefore, request URLs are parsed using {@link java.net.URI} which accepts unknown schemes.</p>
*/ */
@ProtocolInfo(protocolSide = ProtocolVersion.ProtocolSide.WEB) @ProtocolInfo(protocolSide = ProtocolVersion.ProtocolSide.WEB)
public final class WebServer extends ProtocolWebServer { public final class WebServer extends ProtocolWebServer {
private static final int STREAM_CHUNK_SIZE = 64 * 1024; private static final int STREAM_CHUNK_SIZE = 64 * 1024;
private static final long STREAM_THRESHOLD = 2L * 1024 * 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 -> { private static final ExecutorService STREAM_EXECUTOR = Executors.newCachedThreadPool(r -> {
Thread t = new Thread(r, "oac-web-stream"); Thread t = new Thread(r, "oac-web-stream");
t.setDaemon(true); t.setDaemon(true);
return t; return t;
}); });
@Getter
private final WebHasher hasher; private final WebHasher hasher;
public WebServer( /**
File authFile, * Creates a WEB v1.0.1 server instance.
File rulesFile, *
int sessionExpire, * @param authFile authentication file
int maxUpload * @param rulesFile rules file
) throws Exception { * @param sessionExpire session expiration in minutes
super(authFile, rulesFile, sessionExpire, maxUpload); * @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, * @return server hasher instance
16, */
32 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 ParsedRequestUrl parsed;
try {
parsed = ParsedRequestUrl.parse(request.getUrl());
} catch (Exception e) {
return new WebNavigateAckPacket(
mirrorHeader(in, WebPacketFlags.NAVIGATION),
false,
"Invalid URL: " + e.getMessage()
);
}
String path = normalizePath(parsed.path());
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 @Override
public WebResponsePacket onWebRequest(CustomConnectedClient client, WebRequestPacket request) { protected WebResourceResponsePacket onResourceRequest(CustomConnectedClient client, WebResourceRequestPacket request) {
try { Objects.requireNonNull(client, "client");
String path = request.getPath() == null ? "/" : request.getPath(); Objects.requireNonNull(request, "request");
if (RuleManager.isDenied(path)) { final WebPacketHeader in = request.getHeader();
return new WebResponsePacket(403, "text/plain; charset=utf-8", HeaderMaps.mutable(), "Forbidden".getBytes());
final ParsedRequestUrl parsed;
try {
parsed = ParsedRequestUrl.parse(request.getUrl());
} catch (Exception e) {
return error(in, 400, "Invalid URL: " + e.getMessage());
}
String path = normalizePath(parsed.path());
if (RuleManager.isDenied(path) || !RuleManager.isAllowed(path)) {
return error(in, 403, "Forbidden");
} }
if (RuleManager.requiresAuth(path)) { if (RuleManager.requiresAuth(path)) {
SessionContext ctx = SessionContext.from(client, this, request.getHeaders()); try {
SessionContext ctx = SessionContext.from(client, this, safeHeaders(request));
if (!ctx.isValid()) { if (!ctx.isValid()) {
return new WebResponsePacket(401, "text/plain; charset=utf-8", HeaderMaps.mutable(), "Authentication required".getBytes()); return error(in, 401, "Authentication required");
} }
}
WebResponsePacket javaResp = JavaPageDispatcher.dispatch(client, this, request);
if (javaResp != null) return javaResp;
return serveFile(client, path);
} catch (Exception e) { } catch (Exception e) {
return new WebResponsePacket( return error(in, 500, "Auth check failed: " + e.getMessage());
500, }
"text/plain; charset=utf-8", }
HeaderMaps.mutable(),
("Internal Error: " + e.getClass().getName() + ": " + e.getMessage()).getBytes() try {
WebResourceResponsePacket javaResp = dispatchJavaPageAsResource(client, request);
if (javaResp != null) {
return javaResp;
}
} catch (Exception e) {
return error(in, 500, "JavaPage error: " + e.getClass().getSimpleName() + ": " + e.getMessage());
}
try {
return serveStaticFile(client, request, parsed);
} catch (Exception e) {
return error(in, 500, "Internal error: " + e.getClass().getSimpleName() + ": " + e.getMessage());
}
}
@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) throws Exception {
return JavaPageDispatcher.dispatch(client, this, req);
} }
private WebResponsePacket serveFile(CustomConnectedClient client, String path) throws Exception { private WebResourceResponsePacket serveStaticFile(CustomConnectedClient client, WebResourceRequestPacket request, ParsedRequestUrl parsed) throws Exception {
final WebPacketHeader in = request.getHeader();
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";
File root = getContentFolder().getCanonicalFile(); File root = getContentFolder().getCanonicalFile();
File file = new File(root, path).getCanonicalFile(); File file = new File(root, path).getCanonicalFile();
if (!RuleManager.isAllowed(path)) { if (!file.getPath().startsWith(root.getPath())) {
return new WebResponsePacket(403, "text/plain; charset=utf-8", HeaderMaps.mutable(), "Forbidden".getBytes()); return error(in, 403, "Forbidden");
} }
if (RuleManager.isDenied(path)) { if (!file.exists() || !file.isFile()) {
return new WebResponsePacket(403, "text/plain; charset=utf-8", HeaderMaps.mutable(), "Forbidden".getBytes()); return error(in, 404, "Not Found");
} }
String contentType = ContentTypeResolver.resolve(file.getName()); String contentType = ContentTypeResolver.resolve(file.getName());
long size = file.length(); long size = file.length();
if (size >= STREAM_THRESHOLD) { RangeSpec range = RangeSpec.parse(getHeaderIgnoreCase(safeHeaders(request), "range"), size);
startStreamingAsync(client, file, contentType);
// IMPORTANT: Never return null. If your client expects a normal response, this keeps the pipeline stable. boolean wantsRange = range != null && range.isValid();
// The actual bytes are delivered via WebStream* packets. boolean shouldStream = shouldStream(file.getName(), contentType, size, wantsRange);
return new WebResponsePacket(
202, if (shouldStream) {
"text/plain; charset=utf-8", // Return an empty response that indicates "stream follows" via flags.
HeaderMaps.mutable(Map.of("x-oac-stream", "1")), WebPacketHeader respHeader = mirrorHeader(in, WebPacketFlags.RESOURCE | WebPacketFlags.STREAM);
"Streaming started".getBytes()
Map<String, String> 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()); // Non-stream: either full or ranged (single shot)
return new WebResponsePacket(200, contentType, HeaderMaps.mutable(), data); byte[] data;
if (wantsRange) {
data = readRange(file, range);
Map<String, String> 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()
);
} }
private void startStreamingAsync(CustomConnectedClient client, File file, String contentType) { data = Files.readAllBytes(file.toPath());
Map<String, String> 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, WebPacketHeader incomingHeader, File file, String contentType, RangeSpec range) {
STREAM_EXECUTOR.execute(() -> { STREAM_EXECUTOR.execute(() -> {
try { try {
streamFile(client, file, contentType); streamFile(client, incomingHeader, file, contentType, range);
} catch (Exception e) { } catch (Exception e) {
// Never let streaming errors kill your server threads.
try { 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) { } catch (Exception ignored) {
// ignore: client may already be gone // ignore
} }
} }
}); });
} }
private void streamFile(CustomConnectedClient client, File file, String contentType) throws IOException, ClassNotFoundException { private void streamFile(CustomConnectedClient client, WebPacketHeader incomingHeader, File file, String contentType, RangeSpec range) throws Exception {
long total = file.length(); 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<String, String> 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( 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.TCP
); );
TransportProtocol chunkTransport = chooseChunkTransport(file.getName());
try (InputStream in = new BufferedInputStream(new FileInputStream(file))) { try (InputStream in = new BufferedInputStream(new FileInputStream(file))) {
skipFully(in, start);
byte[] buf = new byte[STREAM_CHUNK_SIZE]; byte[] buf = new byte[STREAM_CHUNK_SIZE];
int seq = 0; int seq = 0;
int r;
while ((r = in.read(buf)) != -1) { long remaining = toSend;
// Always copy: never hand out the reusable buffer reference. 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); byte[] chunk = Arrays.copyOf(buf, r);
client.getConnection().sendPacket( client.getConnection().sendPacket(
new WebStreamChunkPacket(seq++, chunk), new WebStreamChunkPacket_v1_0_1_B(
ContentTypeResolver.isVideoFile(file.getName()) ? TransportProtocol.UDP : TransportProtocol.TCP 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;
if (wantsRange && (ContentTypeResolver.isVideoFile(fileName) || ContentTypeResolver.isAudioFile(fileName))) {
return true;
}
return false;
}
private static TransportProtocol chooseChunkTransport(String fileName) {
return ContentTypeResolver.isVideoFile(fileName) ? TransportProtocol.UDP : TransportProtocol.TCP;
}
private static String normalizePath(String p) {
return WebUrlUtil.normalizeRequestPath(p);
}
private static String contentTypeOrDefault(String ct) {
if (ct == null || ct.isBlank()) return "application/octet-stream";
return ct;
}
private static Map<String, String> safeHeaders(WebResourceRequestPacket request) {
Map<String, String> h = request.getHeaders();
return h == null ? Map.of() : h;
}
private static Map<String, String> safeHeaders(WebNavigateRequestPacket request) {
return Map.of();
}
private static String getHeaderIgnoreCase(Map<String, String> headers, String key) {
if (headers == null || headers.isEmpty() || key == null) return null;
String needle = key.trim().toLowerCase(Locale.ROOT);
for (Map.Entry<String, String> 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<String, String> 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
);
}
/**
* 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".
*/
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;
}
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();
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);
}
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);
}
}
public boolean isValid() {
return valid;
}
public long startInclusive() {
return startInclusive;
}
public long endInclusive() {
return endInclusive;
}
public long length() {
return (endInclusive >= startInclusive) ? (endInclusive - startInclusive + 1) : 0L;
}
public String toContentRange(long total) {
return "bytes " + startInclusive + "-" + endInclusive + "/" + total;
}
} }
} }

View File

@@ -59,14 +59,13 @@ public final class SessionContext {
} }
private static String extractSessionId(Map<String, String> headers) { private static String extractSessionId(Map<String, String> headers) {
// 1) Cookie header preferred
String cookie = getHeaderIgnoreCase(headers, "cookie"); String cookie = getHeaderIgnoreCase(headers, "cookie");
String fromCookie = parseCookie(cookie, COOKIE_NAME); String fromCookie = parseCookie(cookie, COOKIE_NAME);
if (fromCookie != null && !fromCookie.isBlank()) return fromCookie; if (fromCookie != null && !fromCookie.isBlank()) return fromCookie;
// 2) Backward-compatible fallback: old custom header // Fallback
String legacy = getHeaderIgnoreCase(headers, "session"); String session = getHeaderIgnoreCase(headers, "session");
return (legacy == null || legacy.isBlank()) ? null : legacy.trim(); return (session == null || session.isBlank()) ? null : session.trim();
} }
private static String parseCookie(String cookieHeader, String name) { private static String parseCookie(String cookieHeader, String name) {

View File

@@ -1,18 +1,61 @@
package org.openautonomousconnection.webserver.api; 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;
import org.openautonomousconnection.webserver.Main;
import org.w3c.dom.Document;
import org.w3c.dom.html.HTMLBodyElement;
import org.w3c.dom.html.HTMLHeadElement;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
/** /**
* Server-side Java page (PHP alternative). * Server-side Java page (v1.0.1-BETA).
* Every .java page must implement this interface. *
* <p>Every .java page must extend this class.</p>
*/ */
public interface WebPage { public abstract class WebPage {
private static final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
private static final DocumentBuilder documentBuilder;
static {
DocumentBuilder temp = null;
try {
temp = factory.newDocumentBuilder();
} catch (ParserConfigurationException e) {
Main.getValues().logger.exception("Failed to create document builder", e);
temp = null;
}
documentBuilder = temp;
}
protected final Document document;
protected final HTMLHeadElement head;
protected final HTMLBodyElement body;
/**
* Default constructor that creates a new Document instance
*/
public WebPage() {
this.document = documentBuilder.newDocument();
this.head = (HTMLHeadElement) this.document.createElement("head");
this.body = (HTMLBodyElement) this.document.createElement("body");
this.document.appendChild(this.head);
this.document.appendChild(this.body);
}
/** /**
* Handles a web request. * Handles a web request.
* *
* @param ctx context (client, request, session) * @param ctx context (client, request, session)
* @return response packet * @return resource response packet
* @throws Exception on unexpected failures
*/ */
WebResponsePacket handle(WebPageContext ctx) throws Exception; public abstract WebResourceResponsePacket handle(WebPageContext ctx) throws Exception;
} }

View File

@@ -1,18 +1,18 @@
package org.openautonomousconnection.webserver.api; 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.server.CustomConnectedClient;
import org.openautonomousconnection.protocol.side.web.ProtocolWebServer; import org.openautonomousconnection.protocol.side.web.ProtocolWebServer;
import org.openautonomousconnection.webserver.utils.RequestParams; import org.openautonomousconnection.webserver.utils.RequestParams;
import org.openautonomousconnection.webserver.utils.WebHasher; 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 class WebPageContext {
public final CustomConnectedClient client; public final CustomConnectedClient client;
public final WebRequestPacket request; public final WebResourceRequestPacket request;
public final SessionContext session; public final SessionContext session;
public final RequestParams params; public final RequestParams params;
public final WebHasher hasher; public final WebHasher hasher;
@@ -20,7 +20,7 @@ public final class WebPageContext {
public WebPageContext( public WebPageContext(
CustomConnectedClient client, CustomConnectedClient client,
ProtocolWebServer server, ProtocolWebServer server,
WebRequestPacket request, WebResourceRequestPacket request,
RequestParams params, RequestParams params,
WebHasher hasher WebHasher hasher
) throws Exception { ) throws Exception {
@@ -30,4 +30,22 @@ public final class WebPageContext {
this.params = params; this.params = params;
this.hasher = hasher; 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);
}
} }

View File

@@ -1,7 +1,7 @@
package org.openautonomousconnection.webserver.runtime; package org.openautonomousconnection.webserver.runtime;
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.packets.v1_0_0.beta.web.WebResponsePacket; 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.server.CustomConnectedClient;
import org.openautonomousconnection.protocol.side.web.ProtocolWebServer; import org.openautonomousconnection.protocol.side.web.ProtocolWebServer;
import org.openautonomousconnection.webserver.WebServer; 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.HeaderMaps;
import org.openautonomousconnection.webserver.utils.RequestParams; import org.openautonomousconnection.webserver.utils.RequestParams;
import org.openautonomousconnection.webserver.utils.WebHasher; import org.openautonomousconnection.webserver.utils.WebHasher;
import org.openautonomousconnection.webserver.utils.WebUrlUtil;
import java.io.File;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
/** /**
* Dispatches Java WebPages using {@code @Route} annotation. * Dispatches Java WebPages using {@code @Route} annotation (v1.0.1-BETA).
*
* <p>This dispatcher relies on {@link JavaRouteRegistry} for route-to-source mapping
* and uses {@link JavaPageCache} to compile/load classes from the content tree.
*/ */
public final class JavaPageDispatcher { public final class JavaPageDispatcher {
@@ -34,25 +31,28 @@ public final class JavaPageDispatcher {
* @param client connected client * @param client connected client
* @param server protocol web server * @param server protocol web server
* @param request request packet * @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 * @throws Exception on unexpected failures
*/ */
public static WebResponsePacket dispatch( public static WebResourceResponsePacket dispatch(
CustomConnectedClient client, CustomConnectedClient client,
ProtocolWebServer server, ProtocolWebServer server,
WebRequestPacket request WebResourceRequestPacket request
) throws Exception { ) throws Exception {
if (request == null || request.getPath() == null) { if (request == null || request.getUrl() == null) {
return null; return null;
} }
String route = request.getPath(); String route = WebUrlUtil.extractPathAndQuery(request.getUrl());
if (!route.startsWith("/")) { if (route == null) return null;
route = "/" + route;
}
File contentRoot = server.getContentFolder(); int q = route.indexOf('?');
if (q >= 0) route = route.substring(0, q);
route = WebUrlUtil.normalizeRequestPath(route);
java.io.File contentRoot = server.getContentFolder();
ROUTES.refreshIfNeeded(contentRoot); ROUTES.refreshIfNeeded(contentRoot);
JavaRouteRegistry.RouteLookupResult found = ROUTES.find(route); JavaRouteRegistry.RouteLookupResult found = ROUTES.find(route);
@@ -65,9 +65,8 @@ public final class JavaPageDispatcher {
JavaPageCache.LoadedClass loaded = CACHE.getOrCompile(contentRoot, found.sourceFile(), contentLm); JavaPageCache.LoadedClass loaded = CACHE.getOrCompile(contentRoot, found.sourceFile(), contentLm);
Class<?> clazz = loaded.clazz(); Class<?> clazz = loaded.clazz();
// Verify that the loaded class is actually routable.
if (!WebPage.class.isAssignableFrom(clazz)) { 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(); Object instance = clazz.getDeclaredConstructor().newInstance();
@@ -75,19 +74,41 @@ public final class JavaPageDispatcher {
WebHasher hasher = (server instanceof WebServer ws) ? ws.getHasher() : null; WebHasher hasher = (server instanceof WebServer ws) ? ws.getHasher() : null;
if (hasher == 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); WebPageContext ctx = new WebPageContext(client, server, request, new RequestParams(request), hasher);
return page.handle(ctx); return page.handle(ctx);
} }
private static WebResponsePacket error(int code, String msg) { private static WebResourceResponsePacket error(WebResourceRequestPacket req, int code, String msg) {
return new WebResponsePacket( 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, code,
"text/plain; charset=utf-8", "text/plain; charset=utf-8",
HeaderMaps.mutable(), HeaderMaps.mutable(),
msg.getBytes(StandardCharsets.UTF_8) body,
null
); );
} }
} }

View File

@@ -1,6 +1,9 @@
package org.openautonomousconnection.webserver.utils; package org.openautonomousconnection.webserver.utils;
import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.WebResponsePacket; 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 org.openautonomousconnection.webserver.api.WebPageContext;
import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.HttpsURLConnection;
@@ -8,15 +11,16 @@ import java.io.ByteArrayOutputStream;
import java.io.InputStream; import java.io.InputStream;
import java.net.URL; import java.net.URL;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.LinkedHashMap;
import java.util.Map;
/** /**
* Simple HTTPS -> OAC proxy helper. * Simple HTTPS -> OAC proxy helper.
* *
* <p>Limitations: * <p>v1.0.1 entry returns {@link WebResourceResponsePacket} and mirrors correlation header fields
* <ul> * from {@link WebPageContext#request}.</p>
* <li>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.</li> * <p>v1.0.0 method is kept for older call sites.</p>
* </ul>
*/ */
public final class HttpsProxy { 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> out, String name) {
String v = con.getHeaderField(name);
if (v != null && !v.isBlank()) out.put(name, v);
}
private static String getHeaderIgnoreCase(Map<String, String> headers, String key) {
if (headers == null || headers.isEmpty() || key == null) return null;
String needle = key.trim().toLowerCase(java.util.Locale.ROOT);
for (Map.Entry<String, String> 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 ctx the current web page context (for optional user-agent forwarding)
* @param url the target HTTPS URL * @param url the target HTTPS URL
* @return proxied response * @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 { try {
return proxyGetInternal(ctx, url, 0); return proxyGetInternalV100B(ctx, url, 0);
} catch (Exception e) { } catch (Exception e) {
return new WebResponsePacket( return new WebResponsePacket(
502, 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) { if (depth > MAX_REDIRECTS) {
return new WebResponsePacket( return new WebResponsePacket(
508, 508,
@@ -64,16 +204,14 @@ public final class HttpsProxy {
con.setConnectTimeout(CONNECT_TIMEOUT_MS); con.setConnectTimeout(CONNECT_TIMEOUT_MS);
con.setReadTimeout(READ_TIMEOUT_MS); con.setReadTimeout(READ_TIMEOUT_MS);
// Forward a user-agent if you have one (optional).
String ua = null; String ua = null;
if (ctx != null && ctx.request != null && ctx.request.getHeaders() != 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"); con.setRequestProperty("User-Agent", ua != null ? ua : "OAC-HttpsProxy/1.0");
int code = con.getResponseCode(); 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) { if (code == 301 || code == 302 || code == 303 || code == 307 || code == 308) {
String location = con.getHeaderField("Location"); String location = con.getHeaderField("Location");
if (location == null || location.isBlank()) { if (location == null || location.isBlank()) {
@@ -84,16 +222,13 @@ public final class HttpsProxy {
("Bad Gateway: redirect without Location (code=" + code + ")").getBytes(StandardCharsets.UTF_8) ("Bad Gateway: redirect without Location (code=" + code + ")").getBytes(StandardCharsets.UTF_8)
); );
} }
// Resolve relative redirects
URL resolved = new URL(target, location); URL resolved = new URL(target, location);
con.disconnect(); con.disconnect();
return proxyGetInternal(ctx, resolved.toString(), depth + 1); return proxyGetInternalV100B(ctx, resolved.toString(), depth + 1);
} }
String contentType = con.getContentType(); String contentType = con.getContentType();
if (contentType == null || contentType.isBlank()) { if (contentType == null || contentType.isBlank()) contentType = "application/octet-stream";
contentType = "application/octet-stream";
}
byte[] body; byte[] body;
try (InputStream in = (code >= 400 ? con.getErrorStream() : con.getInputStream())) { 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); 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();
}
} }

View File

@@ -33,20 +33,20 @@ public final class MergedRequestParams {
public static MergedRequestParams from(String rawTarget, Map<String, String> headers, byte[] body) { public static MergedRequestParams from(String rawTarget, Map<String, String> headers, byte[] body) {
Map<String, List<String>> merged = new LinkedHashMap<>(); Map<String, List<String>> merged = new LinkedHashMap<>();
// 1) Query string // Query string
String query = extractQuery(rawTarget); String query = extractQuery(rawTarget);
if (query != null && !query.isBlank()) { if (query != null && !query.isBlank()) {
mergeInto(merged, parseUrlEncoded(query, StandardCharsets.UTF_8), false); mergeInto(merged, parseUrlEncoded(query, StandardCharsets.UTF_8), false);
} }
// 2) Body // Body
if (body != null && body.length > 0) { if (body != null && body.length > 0) {
String contentType = header(headers, "content-type"); String contentType = header(headers, "content-type");
Map<String, List<String>> bodyParams = parseBody(contentType, body); Map<String, List<String>> bodyParams = parseBody(contentType, body);
mergeInto(merged, bodyParams, true); mergeInto(merged, bodyParams, true);
} }
return new MergedRequestParams(merged); return new MergedRequestParams(Collections.unmodifiableMap(merged));
} }
private static String extractQuery(String rawTarget) { private static String extractQuery(String rawTarget) {
@@ -70,16 +70,20 @@ public final class MergedRequestParams {
private static void mergeInto(Map<String, List<String>> target, Map<String, List<String>> src, boolean override) { private static void mergeInto(Map<String, List<String>> target, Map<String, List<String>> src, boolean override) {
if (src == null || src.isEmpty()) return; if (src == null || src.isEmpty()) return;
for (Map.Entry<String, List<String>> e : src.entrySet()) { for (Map.Entry<String, List<String>> e : src.entrySet()) {
if (e.getKey() == null) continue; if (e.getKey() == null) continue;
String k = e.getKey(); String k = e.getKey();
List<String> vals = e.getValue() == null ? List.of() : e.getValue(); List<String> vals = (e.getValue() == null) ? List.of() : e.getValue();
if (!override && target.containsKey(k)) { if (!override && target.containsKey(k)) {
// append // Always keep ArrayList in target to allow appends safely.
target.get(k).addAll(vals); target.get(k).addAll(vals);
continue; continue;
} }
// override or insert
// Insert/override with a mutable list to preserve later merge behavior.
target.put(k, new ArrayList<>(vals)); target.put(k, new ArrayList<>(vals));
} }
} }
@@ -166,6 +170,7 @@ public final class MergedRequestParams {
/** /**
* Minimal multipart parser for text fields only. * Minimal multipart parser for text fields only.
*
* <p>Ignores file uploads and binary content.</p> * <p>Ignores file uploads and binary content.</p>
*/ */
private static Map<String, List<String>> parseMultipartTextFields(byte[] body, String boundary, Charset charset) { private static Map<String, List<String>> parseMultipartTextFields(byte[] body, String boundary, Charset charset) {
@@ -293,13 +298,8 @@ public final class MergedRequestParams {
continue; continue;
} }
} }
// ISO-8859-1 safe char -> byte
if (c <= 0xFF) baos.write((byte) c); if (c <= 0xFF) baos.write((byte) c);
else { else baos.writeBytes(String.valueOf(c).getBytes(charset));
// for non-latin chars, fall back to UTF-8 bytes of that char
byte[] b = String.valueOf(c).getBytes(charset);
baos.writeBytes(b);
}
} }
return baos.toString(charset); return baos.toString(charset);
} }

View File

@@ -1,25 +1,47 @@
package org.openautonomousconnection.webserver.utils; package org.openautonomousconnection.webserver.utils;
import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.WebRequestPacket; 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.Collections;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
/** /**
* Reads parameters from WebRequestPacket headers (case-insensitive). * Reads parameters from request headers (case-insensitive).
* *
* <p>Additionally provides hashing helpers via a supplied {@link WebHasher}. * <p>This utility is used by server-side pages to access request metadata.</p>
*/ */
public final class RequestParams { public final class RequestParams {
private final Map<String, String> headers; private final Map<String, String> 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<String, String> 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<String, String> 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) { public RequestParams(WebRequestPacket request) {
Map<String, String> h = request != null ? request.getHeaders() : null; Map<String, String> h = request != null ? request.getHeaders() : null;
this.headers = (h == null) ? Collections.emptyMap() : h; this.headers = (h == null) ? Collections.emptyMap() : h;
@@ -92,6 +114,7 @@ public final class RequestParams {
* @return sha256 hex (or null if missing) * @return sha256 hex (or null if missing)
*/ */
public String getSha256Hex(WebHasher hasher, String key) { public String getSha256Hex(WebHasher hasher, String key) {
if (hasher == null) throw new IllegalArgumentException("hasher is null");
String v = getTrimmed(key); String v = getTrimmed(key);
if (v == null) return null; if (v == null) return null;
return hasher.sha256Hex(v); return hasher.sha256Hex(v);

View File

@@ -0,0 +1,69 @@
package org.openautonomousconnection.webserver.utils;
import java.net.URI;
/**
* URL utilities for the server.
*/
public final class WebUrlUtil {
private static final String DEFAULT_DOCUMENT_PATH = "/index.html";
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 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.
*
* @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);
p = normalizeRequestPath(p);
if (p.startsWith("/")) p = p.substring(1);
if (p.isBlank()) return "index.html";
return p;
}
}