master #1
2
.idea/misc.xml
generated
2
.idea/misc.xml
generated
@@ -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>
|
||||||
1
LICENSE
1
LICENSE
@@ -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
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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.1</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>
|
||||||
@@ -44,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>
|
||||||
@@ -109,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>
|
||||||
@@ -122,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>
|
||||||
|
|||||||
27
pom.xml
27
pom.xml
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
<groupId>org.openautonomousconnection</groupId>
|
<groupId>org.openautonomousconnection</groupId>
|
||||||
<artifactId>WebServer</artifactId>
|
<artifactId>WebServer</artifactId>
|
||||||
<version>1.0.0-BETA.1.1</version>
|
<version>1.0.1-BETA.0.4</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>
|
||||||
|
|
||||||
@@ -77,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.1</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>
|
||||||
@@ -94,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>
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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!");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
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;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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 implement this interface.</p>
|
||||||
*/
|
*/
|
||||||
public interface WebPage {
|
public interface WebPage {
|
||||||
|
|
||||||
@@ -12,7 +13,8 @@ public interface WebPage {
|
|||||||
* 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;
|
WebResourceResponsePacket handle(WebPageContext ctx) throws Exception;
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user