Finished up WebServer
This commit is contained in:
2
pom.xml
2
pom.xml
@@ -70,7 +70,7 @@
|
||||
<dependency>
|
||||
<groupId>org.openautonomousconnection</groupId>
|
||||
<artifactId>protocol</artifactId>
|
||||
<version>1.0.0-BETA.2</version>
|
||||
<version>1.0.0-BETA.4</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
|
||||
@@ -82,7 +82,7 @@ default_ca = CA_default # The default ca section
|
||||
dir = ./demoCA # Where everything is kept
|
||||
certs = $dir/certs # Where the issued certs are kept
|
||||
crl_dir = $dir/crl # Where the issued crl are kept
|
||||
database = $dir/index.txt # database index file.
|
||||
database = $dir/org.openautonomousconnection.webserver.index.txt # database org.openautonomousconnection.webserver.index file.
|
||||
#unique_subject = no # Set to 'no' to allow creation of
|
||||
# several certs with same subject.
|
||||
new_certs_dir = $dir/newcerts # default place for new certs.
|
||||
|
||||
@@ -1155,7 +1155,7 @@ PHP NEWS
|
||||
ResourceBundle object now throw:
|
||||
- TypeError for invalid offset types
|
||||
- ValueError for an empty string
|
||||
- ValueError if the integer index does not fit in a signed 32 bit integer
|
||||
- ValueError if the integer org.openautonomousconnection.webserver.index does not fit in a signed 32 bit integer
|
||||
. ResourceBundle::get() now has a tentative return type of:
|
||||
ResourceBundle|array|string|int|null
|
||||
. Added the new Grapheme function grapheme_str_split. (youkidearitai)
|
||||
@@ -1441,7 +1441,7 @@ PHP NEWS
|
||||
. Added SOCK_NONBLOCK/SOCK_CLOEXEC constants for socket_create and
|
||||
socket_create_pair to apply O_NONBLOCK/O_CLOEXEC flags to the
|
||||
newly created sockets. (David Carlier)
|
||||
. Added SO_BINDTOIFINDEX to bind a socket to an interface index.
|
||||
. Added SO_BINDTOIFINDEX to bind a socket to an interface org.openautonomousconnection.webserver.index.
|
||||
(David Carlier)
|
||||
|
||||
- Sodium:
|
||||
|
||||
@@ -1455,7 +1455,7 @@ session.trans_sid_tags = "a=href,area=href,frame=src,form="
|
||||
; https://php.net/session.upload-progress.prefix
|
||||
;session.upload_progress.prefix = "upload_progress_"
|
||||
|
||||
; The index name (concatenated with the prefix) in $_SESSION
|
||||
; The org.openautonomousconnection.webserver.index name (concatenated with the prefix) in $_SESSION
|
||||
; containing the upload progress information
|
||||
; Default Value: "PHP_SESSION_UPLOAD_PROGRESS"
|
||||
; Development Value: "PHP_SESSION_UPLOAD_PROGRESS"
|
||||
|
||||
@@ -1457,7 +1457,7 @@ session.trans_sid_tags = "a=href,area=href,frame=src,form="
|
||||
; https://php.net/session.upload-progress.prefix
|
||||
;session.upload_progress.prefix = "upload_progress_"
|
||||
|
||||
; The index name (concatenated with the prefix) in $_SESSION
|
||||
; The org.openautonomousconnection.webserver.index name (concatenated with the prefix) in $_SESSION
|
||||
; containing the upload progress information
|
||||
; Default Value: "PHP_SESSION_UPLOAD_PROGRESS"
|
||||
; Development Value: "PHP_SESSION_UPLOAD_PROGRESS"
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
package github.openautonomousconnection.webserver;
|
||||
|
||||
public class Main {
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package github.openautonomousconnection.webserver.api;
|
||||
|
||||
public interface WebModule {
|
||||
|
||||
/**
|
||||
* @return Path this module handles (e.g. "/api/user")
|
||||
*/
|
||||
String path();
|
||||
|
||||
/**
|
||||
* Handle a web request.
|
||||
*/
|
||||
WebResponsePacket handle(ConnectedWebClient client, WebRequestPacket request) throws Exception;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package org.openautonomousconnection.webserver;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public final class ContentTypeResolver {
|
||||
|
||||
private static final Map<String, String> MAP = Map.ofEntries(
|
||||
Map.entry("html", "text/html"),
|
||||
Map.entry("css", "text/css"),
|
||||
Map.entry("json", "application/json"),
|
||||
Map.entry("xml", "application/xml"),
|
||||
Map.entry("yml", "application/x-yaml"),
|
||||
Map.entry("yaml", "application/x-yaml"),
|
||||
Map.entry("txt", "text/plain"),
|
||||
Map.entry("png", "image/png"),
|
||||
Map.entry("jpg", "image/jpeg"),
|
||||
Map.entry("jpeg", "image/jpeg"),
|
||||
Map.entry("gif", "image/gif"),
|
||||
Map.entry("svg", "image/svg+xml"),
|
||||
Map.entry("mp4", "video/mp4"),
|
||||
Map.entry("webm", "video/webm"),
|
||||
Map.entry("mp3", "audio/mpeg"),
|
||||
Map.entry("wav", "audio/wav"),
|
||||
|
||||
// explicitly NOT executed
|
||||
Map.entry("java", "text/plain"),
|
||||
Map.entry("js", "text/plain"),
|
||||
Map.entry("ts", "text/plain"),
|
||||
Map.entry("php", "text/plain"),
|
||||
Map.entry("py", "text/plain")
|
||||
);
|
||||
|
||||
public static String resolve(String name) {
|
||||
int i = name.lastIndexOf('.');
|
||||
if (i == -1) return "application/octet-stream";
|
||||
return MAP.getOrDefault(
|
||||
name.substring(i + 1).toLowerCase(),
|
||||
"application/octet-stream"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package org.openautonomousconnection.webserver;
|
||||
|
||||
import dev.unlegitdqrk.unlegitlibrary.command.CommandExecutor;
|
||||
import dev.unlegitdqrk.unlegitlibrary.command.CommandManager;
|
||||
import dev.unlegitdqrk.unlegitlibrary.command.CommandPermission;
|
||||
import dev.unlegitdqrk.unlegitlibrary.event.EventManager;
|
||||
import dev.unlegitdqrk.unlegitlibrary.network.system.packets.PacketHandler;
|
||||
import lombok.Getter;
|
||||
import org.openautonomousconnection.protocol.ProtocolBridge;
|
||||
import org.openautonomousconnection.protocol.ProtocolSettings;
|
||||
import org.openautonomousconnection.protocol.side.ins.ProtocolINSServer;
|
||||
import org.openautonomousconnection.protocol.versions.ProtocolVersion;
|
||||
|
||||
import javax.annotation.processing.Generated;
|
||||
import java.io.File;
|
||||
import java.util.Scanner;
|
||||
|
||||
public class Main {
|
||||
private static final CommandPermission PERMISSION_ALL = new CommandPermission("all", 1);
|
||||
private static final CommandExecutor commandExecutor = new CommandExecutor("DNS", PERMISSION_ALL) {};
|
||||
private static CommandManager commandManager;
|
||||
|
||||
@Getter
|
||||
private static ProtocolBridge protocolBridge;
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
ProtocolSettings settings = new ProtocolSettings();
|
||||
settings.packetHandler = new PacketHandler();
|
||||
settings.eventManager = new EventManager();
|
||||
settings.port = 9824;
|
||||
|
||||
protocolBridge = new ProtocolBridge(new WebServer(new File("config.properties"),
|
||||
new File("auth.ini"), new File("rules.ini")),
|
||||
settings, ProtocolVersion.PV_1_0_0_BETA, new File("logs"));
|
||||
|
||||
commandManager = new CommandManager(protocolBridge.getProtocolSettings().eventManager);
|
||||
Scanner scanner = new Scanner(System.in);
|
||||
|
||||
while (true) {
|
||||
System.out.println(commandExecutor.getName() + "> ");
|
||||
String line = scanner.nextLine();
|
||||
commandManager.execute(commandExecutor, line);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package org.openautonomousconnection.webserver;
|
||||
|
||||
import org.openautonomousconnection.protocol.annotations.ProtocolInfo;
|
||||
import org.openautonomousconnection.protocol.packets.v1_0_0.beta.*;
|
||||
import org.openautonomousconnection.protocol.side.web.ConnectedWebClient;
|
||||
import org.openautonomousconnection.protocol.side.web.ProtocolWebServer;
|
||||
import org.openautonomousconnection.protocol.side.web.managers.RuleManager;
|
||||
import org.openautonomousconnection.protocol.side.web.managers.SessionManager;
|
||||
import org.openautonomousconnection.protocol.versions.ProtocolVersion;
|
||||
import org.openautonomousconnection.webserver.api.SessionContext;
|
||||
import org.openautonomousconnection.webserver.runtime.JavaPageDispatcher;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.Files;
|
||||
import java.util.*;
|
||||
|
||||
@ProtocolInfo(protocolSide = ProtocolVersion.ProtocolSide.WEB)
|
||||
public final class WebServer extends ProtocolWebServer {
|
||||
|
||||
private static final int STREAM_CHUNK_SIZE = 64 * 1024;
|
||||
private static final long STREAM_THRESHOLD = 2L * 1024 * 1024;
|
||||
|
||||
public WebServer(File configFile, File authFile, File rulesFile) throws Exception {
|
||||
super(configFile, authFile, rulesFile);
|
||||
Main.getProtocolBridge().getProtocolSettings().port = getConfigurationManager().getInt("port.webserver");
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebResponsePacket onWebRequest(ConnectedWebClient client, WebRequestPacket request) {
|
||||
try {
|
||||
String path = request.getPath() == null ? "/" : request.getPath();
|
||||
|
||||
if (RuleManager.isDenied(path)) return new WebResponsePacket(403, "text/plain", Map.of(), "Forbidden".getBytes());
|
||||
|
||||
if (RuleManager.requiresAuth(path)) {
|
||||
SessionContext ctx = SessionContext.from(client, this, request.getHeaders());
|
||||
if (!ctx.isValid()) return new WebResponsePacket(401, "text/plain", Map.of(), "Authentication required".getBytes());
|
||||
}
|
||||
|
||||
WebResponsePacket javaResp = JavaPageDispatcher.dispatch(client, this, request);
|
||||
if (javaResp != null) return javaResp;
|
||||
|
||||
return serveFile(client, path);
|
||||
|
||||
} catch (Exception e) {
|
||||
return new WebResponsePacket(500, "text/plain", Map.of(), ("Internal Error: " + e.getMessage()).getBytes());
|
||||
}
|
||||
}
|
||||
|
||||
private WebResponsePacket serveFile(ConnectedWebClient client, String path) throws Exception {
|
||||
if (path.startsWith("/")) path = path.substring(1);
|
||||
if (path.isEmpty()) path = "index.html";
|
||||
|
||||
File root = getContentFolder().getCanonicalFile();
|
||||
File file = new File(root, path).getCanonicalFile();
|
||||
|
||||
if (!file.getPath().startsWith(root.getPath())) return new WebResponsePacket(403, "text/plain", Map.of(), "Forbidden".getBytes());
|
||||
if (!file.exists() || !file.isFile()) return new WebResponsePacket(404, "text/plain", Map.of(), "Not found".getBytes());
|
||||
|
||||
String contentType = ContentTypeResolver.resolve(file.getName());
|
||||
long size = file.length();
|
||||
|
||||
if (size >= STREAM_THRESHOLD) {
|
||||
streamFile(client, file, contentType);
|
||||
return null;
|
||||
}
|
||||
|
||||
byte[] data = Files.readAllBytes(file.toPath());
|
||||
return new WebResponsePacket(200, contentType, Map.of(), data);
|
||||
}
|
||||
|
||||
private void streamFile(ConnectedWebClient client, File file, String contentType) throws IOException {
|
||||
long total = file.length();
|
||||
|
||||
client.streamStart(new WebStreamStartPacket(200, contentType, Map.of("name", file.getName()), total));
|
||||
|
||||
try (InputStream in = new BufferedInputStream(new FileInputStream(file))) {
|
||||
byte[] buf = new byte[STREAM_CHUNK_SIZE];
|
||||
int seq = 0;
|
||||
int r;
|
||||
while ((r = in.read(buf)) != -1) {
|
||||
byte[] chunk = (r == buf.length) ? buf : Arrays.copyOf(buf, r);
|
||||
client.streamChunk(new WebStreamChunkPacket(seq++, chunk));
|
||||
}
|
||||
}
|
||||
|
||||
client.streamEnd(new WebStreamEndPacket(true));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package org.openautonomousconnection.webserver.api;
|
||||
|
||||
import org.openautonomousconnection.protocol.side.web.ConnectedWebClient;
|
||||
import org.openautonomousconnection.protocol.side.web.ProtocolWebServer;
|
||||
import org.openautonomousconnection.protocol.side.web.managers.SessionManager;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Provides session-related information for Java WebPages.
|
||||
* Thin layer on top of SessionManager.
|
||||
*/
|
||||
public final class SessionContext {
|
||||
|
||||
private final String sessionId;
|
||||
private final String user;
|
||||
private final boolean valid;
|
||||
|
||||
private SessionContext(String sessionId, String user, boolean valid) {
|
||||
this.sessionId = sessionId;
|
||||
this.user = user;
|
||||
this.valid = valid;
|
||||
}
|
||||
|
||||
public static SessionContext from(ConnectedWebClient client, ProtocolWebServer server, Map<String, String> headers) throws IOException {
|
||||
if (headers == null) return new SessionContext(null, null, false);
|
||||
|
||||
String sessionId = headers.get("session");
|
||||
if (sessionId == null) return new SessionContext(null, null, false);
|
||||
|
||||
String ip = (client.getWebSocket() != null && client.getWebSocket().getInetAddress() != null)
|
||||
? client.getWebSocket().getInetAddress().getHostAddress() : "";
|
||||
|
||||
String userAgent = headers.getOrDefault("user-agent", "");
|
||||
|
||||
boolean valid = SessionManager.isValid(sessionId, ip, userAgent, server);
|
||||
if (!valid) return new SessionContext(sessionId, null, false);
|
||||
|
||||
String user = SessionManager.getUser(sessionId);
|
||||
return new SessionContext(sessionId, user, true);
|
||||
}
|
||||
|
||||
public boolean isValid() { return valid; }
|
||||
public String getSessionId() { return sessionId; }
|
||||
public String getUser() { return user; }
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package org.openautonomousconnection.webserver.api;
|
||||
|
||||
import org.openautonomousconnection.protocol.packets.v1_0_0.beta.WebResponsePacket;
|
||||
|
||||
/**
|
||||
* Server-side Java page (PHP alternative).
|
||||
* Every .java page must implement this interface.
|
||||
*/
|
||||
public interface WebPage {
|
||||
|
||||
/**
|
||||
* Handles a web request.
|
||||
*
|
||||
* @param ctx context (client, request, session)
|
||||
* @return response packet
|
||||
*/
|
||||
WebResponsePacket handle(WebPageContext ctx) throws Exception;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package org.openautonomousconnection.webserver.api;
|
||||
|
||||
import org.openautonomousconnection.protocol.packets.v1_0_0.beta.WebRequestPacket;
|
||||
import org.openautonomousconnection.protocol.side.web.ConnectedWebClient;
|
||||
import org.openautonomousconnection.protocol.side.web.ProtocolWebServer;
|
||||
|
||||
/**
|
||||
* Context passed to Java WebPages (client, request, session).
|
||||
*/
|
||||
public final class WebPageContext {
|
||||
|
||||
public final ConnectedWebClient client;
|
||||
public final WebRequestPacket request;
|
||||
public final SessionContext session;
|
||||
|
||||
public WebPageContext(ConnectedWebClient client, ProtocolWebServer server, WebRequestPacket request) throws Exception {
|
||||
this.client = client;
|
||||
this.request = request;
|
||||
this.session = SessionContext.from(client, server, request.getHeaders());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package org.openautonomousconnection.webserver.runtime;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Caches compiled Java WebPages by file lastModified timestamp.
|
||||
*/
|
||||
public final class JavaPageCache {
|
||||
|
||||
private static final class Entry {
|
||||
final long lastModified;
|
||||
final Class<?> clazz;
|
||||
|
||||
Entry(long lastModified, Class<?> clazz) {
|
||||
this.lastModified = lastModified;
|
||||
this.clazz = clazz;
|
||||
}
|
||||
}
|
||||
|
||||
private final ConcurrentHashMap<String, Entry> cache = new ConcurrentHashMap<>();
|
||||
|
||||
public Class<?> getOrCompile(File javaFile) throws Exception {
|
||||
String key = javaFile.getAbsolutePath();
|
||||
long lm = javaFile.lastModified();
|
||||
|
||||
Entry e = cache.get(key);
|
||||
if (e != null && e.lastModified == lm) {
|
||||
return e.clazz;
|
||||
}
|
||||
|
||||
Class<?> compiled = JavaPageCompiler.compile(javaFile);
|
||||
cache.put(key, new Entry(lm, compiled));
|
||||
return compiled;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package org.openautonomousconnection.webserver.runtime;
|
||||
|
||||
import javax.tools.*;
|
||||
import java.io.File;
|
||||
import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Compiles and loads Java web pages at runtime.
|
||||
*
|
||||
* NOTE: Requires running with a JDK (ToolProvider.getSystemJavaCompiler != null).
|
||||
*/
|
||||
public final class JavaPageCompiler {
|
||||
|
||||
private JavaPageCompiler() {}
|
||||
|
||||
public static Class<?> compile(File javaFile) throws Exception {
|
||||
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
|
||||
if (compiler == null) throw new IllegalStateException("JDK required (JavaCompiler not available)");
|
||||
|
||||
StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);
|
||||
|
||||
Iterable<? extends JavaFileObject> units = fileManager.getJavaFileObjects(javaFile);
|
||||
|
||||
// Compile in-place (class next to the .java file)
|
||||
List<String> options = List.of("-classpath", System.getProperty("java.class.path"));
|
||||
|
||||
boolean success = compiler.getTask(null, fileManager, null, options, null, units).call();
|
||||
fileManager.close();
|
||||
|
||||
if (!success) throw new RuntimeException("Compilation failed: " + javaFile.getName());
|
||||
|
||||
URLClassLoader cl = new URLClassLoader(new URL[]{ javaFile.getParentFile().toURI().toURL() });
|
||||
|
||||
String className = javaFile.getName().replace(".java", "");
|
||||
return cl.loadClass(className);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package org.openautonomousconnection.webserver.runtime;
|
||||
|
||||
import org.openautonomousconnection.protocol.packets.v1_0_0.beta.WebRequestPacket;
|
||||
import org.openautonomousconnection.protocol.packets.v1_0_0.beta.WebResponsePacket;
|
||||
import org.openautonomousconnection.protocol.side.web.ConnectedWebClient;
|
||||
import org.openautonomousconnection.protocol.side.web.ProtocolWebServer;
|
||||
import org.openautonomousconnection.webserver.api.WebPage;
|
||||
import org.openautonomousconnection.webserver.api.WebPageContext;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public final class JavaPageDispatcher {
|
||||
|
||||
private static final JavaPageCache CACHE = new JavaPageCache();
|
||||
|
||||
private JavaPageDispatcher() {}
|
||||
|
||||
public static WebResponsePacket dispatch(
|
||||
ConnectedWebClient client,
|
||||
ProtocolWebServer server,
|
||||
WebRequestPacket request
|
||||
) throws Exception {
|
||||
|
||||
if (request.getPath() == null) return null;
|
||||
|
||||
String p = request.getPath().startsWith("/") ? request.getPath().substring(1) : request.getPath();
|
||||
File javaFile = new File(server.getContentFolder(), p + ".java");
|
||||
|
||||
if (!javaFile.exists() || !javaFile.isFile()) return null;
|
||||
|
||||
Class<?> clazz = CACHE.getOrCompile(javaFile);
|
||||
Object instance = clazz.getDeclaredConstructor().newInstance();
|
||||
|
||||
if (!(instance instanceof WebPage page))
|
||||
throw new IllegalStateException("Java page must implement WebPage");
|
||||
|
||||
WebPageContext ctx = new WebPageContext(client, server, request);
|
||||
return page.handle(ctx);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user