Updated to new WebServer-Version

This commit is contained in:
Finn
2026-01-19 17:02:46 +01:00
parent 42b1d69a5f
commit 348476cf0a
19 changed files with 1163 additions and 122 deletions

53
.idea/misc.xml generated
View File

@@ -1,9 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="DiscordProjectSettings">
<option name="show" value="ASK" />
<option name="description" value="" />
</component>
<component name="ExternalStorageConfigurationManager" enabled="true" /> <component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="MavenProjectsManager"> <component name="MavenProjectsManager">
<option name="originalFiles"> <option name="originalFiles">
@@ -12,6 +8,55 @@
</list> </list>
</option> </option>
</component> </component>
<component name="NullableNotNullManager">
<option name="myDefaultNullable" value="org.jetbrains.annotations.Nullable" />
<option name="myDefaultNotNull" value="org.jetbrains.annotations.NotNull" />
<option name="myOrdered" value="false" />
<option name="myNullables">
<value>
<list size="16">
<item index="0" class="java.lang.String" itemvalue="org.jspecify.annotations.Nullable" />
<item index="1" class="java.lang.String" itemvalue="org.jetbrains.annotations.Nullable" />
<item index="2" class="java.lang.String" itemvalue="android.annotation.Nullable" />
<item index="3" class="java.lang.String" itemvalue="androidx.annotation.Nullable" />
<item index="4" class="java.lang.String" itemvalue="com.android.annotations.Nullable" />
<item index="5" class="java.lang.String" itemvalue="android.support.annotation.Nullable" />
<item index="6" class="java.lang.String" itemvalue="androidx.annotation.RecentlyNullable" />
<item index="7" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.Nullable" />
<item index="8" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableDecl" />
<item index="9" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableType" />
<item index="10" class="java.lang.String" itemvalue="org.eclipse.jdt.annotation.Nullable" />
<item index="11" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.Nullable" />
<item index="12" class="java.lang.String" itemvalue="jakarta.annotation.Nullable" />
<item index="13" class="java.lang.String" itemvalue="javax.annotation.Nullable" />
<item index="14" class="java.lang.String" itemvalue="javax.annotation.CheckForNull" />
<item index="15" class="java.lang.String" itemvalue="org.springframework.lang.Nullable" />
</list>
</value>
</option>
<option name="myNotNulls">
<value>
<list size="16">
<item index="0" class="java.lang.String" itemvalue="org.jspecify.annotations.NonNull" />
<item index="1" class="java.lang.String" itemvalue="org.jetbrains.annotations.NotNull" />
<item index="2" class="java.lang.String" itemvalue="android.annotation.NonNull" />
<item index="3" class="java.lang.String" itemvalue="androidx.annotation.NonNull" />
<item index="4" class="java.lang.String" itemvalue="com.android.annotations.NonNull" />
<item index="5" class="java.lang.String" itemvalue="android.support.annotation.NonNull" />
<item index="6" class="java.lang.String" itemvalue="androidx.annotation.RecentlyNonNull" />
<item index="7" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.NonNull" />
<item index="8" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullDecl" />
<item index="9" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullType" />
<item index="10" class="java.lang.String" itemvalue="org.eclipse.jdt.annotation.NonNull" />
<item index="11" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.NonNull" />
<item index="12" class="java.lang.String" itemvalue="jakarta.annotation.Nonnull" />
<item index="13" class="java.lang.String" itemvalue="javax.annotation.Nonnull" />
<item index="14" class="java.lang.String" itemvalue="lombok.NonNull" />
<item index="15" class="java.lang.String" itemvalue="org.springframework.lang.NonNull" />
</list>
</value>
</option>
</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_23" default="true" project-jdk-name="23" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" /> <output url="file://$PROJECT_DIR$/out" />
</component> </component>

View File

@@ -104,7 +104,7 @@
<dependency> <dependency>
<groupId>org.openautonomousconnection</groupId> <groupId>org.openautonomousconnection</groupId>
<artifactId>WebServer</artifactId> <artifactId>WebServer</artifactId>
<version>1.0.0-BETA.1.3</version> <version>1.0.0-BETA.1.5</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.projectlombok</groupId> <groupId>org.projectlombok</groupId>

View File

@@ -3,6 +3,7 @@ package org.openautonomousconnection.oac2web;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
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.versions.v1_0_0.beta.WebRequestMethod; import org.openautonomousconnection.protocol.versions.v1_0_0.beta.WebRequestMethod;
import org.openautonomousconnection.webserver.api.Route;
import org.openautonomousconnection.webserver.api.WebPage; import org.openautonomousconnection.webserver.api.WebPage;
import org.openautonomousconnection.webserver.api.WebPageContext; import org.openautonomousconnection.webserver.api.WebPageContext;
@@ -18,16 +19,19 @@ import java.nio.charset.StandardCharsets;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
@Route(path = "api")
public class api implements WebPage { public class api implements WebPage {
@Override @Override
public WebResponsePacket handle(WebPageContext webPageContext) throws Exception { public WebResponsePacket handle(WebPageContext webPageContext) throws Exception {
if (webPageContext.request.getMethod() == WebRequestMethod.POST) { if (webPageContext.request.getMethod() == WebRequestMethod.POST) {
String hostIp = webPageContext.request.getHeaders().get("hostip"); String hostIp = webPageContext.request.getHeaders().get("ip");
String description = webPageContext.request.getHeaders().get("description"); String description = webPageContext.request.getHeaders().get("description");
int port = 0; int tcp = 0;
int udp = 0;
try { try {
port = Integer.parseInt(webPageContext.request.getHeaders().get("port")); tcp = Integer.parseInt(webPageContext.request.getHeaders().get("tcp"));
udp = Integer.parseInt(webPageContext.request.getHeaders().get("udp"));
} catch (NumberFormatException exception) { } catch (NumberFormatException exception) {
return new WebResponsePacket(400, "text/plain", webPageContext.request.getHeaders(), return new WebResponsePacket(400, "text/plain", webPageContext.request.getHeaders(),
("Bad request:\n" + exception.getMessage()).getBytes()); ("Bad request:\n" + exception.getMessage()).getBytes());
@@ -38,9 +42,10 @@ public class api implements WebPage {
try { try {
HashMap<String, String> dataMap = new HashMap<>(); HashMap<String, String> dataMap = new HashMap<>();
dataMap.put("insIp", hostIp); dataMap.put("ip", hostIp);
dataMap.put("insPort", String.valueOf(port)); dataMap.put("tcp", String.valueOf(tcp));
dataMap.put("insDesc", description); dataMap.put("udp", String.valueOf(udp));
dataMap.put("desc", description);
URL url = new URL("https://open-autonomous-connection.org/api/add.php"); URL url = new URL("https://open-autonomous-connection.org/api/add.php");
HttpsURLConnection connection = (HttpsURLConnection) url.openConnection(); HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();

View File

@@ -1,32 +1,21 @@
package org.openautonomousconnection.oac2web; package org.openautonomousconnection.oac2web;
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.webserver.api.Route;
import org.openautonomousconnection.webserver.api.WebPage; import org.openautonomousconnection.webserver.api.WebPage;
import org.openautonomousconnection.webserver.api.WebPageContext; import org.openautonomousconnection.webserver.api.WebPageContext;
import org.openautonomousconnection.webserver.utils.HttpsProxy;
import java.net.URL; /**
import java.net.URLConnection; * Proxies the HTTPS download page through OAC.
import java.nio.charset.StandardCharsets; */
import java.util.HashMap; @Route(path = "download.html")
import java.util.Scanner;
public class download implements WebPage { public class download implements WebPage {
@Override @Override
public WebResponsePacket handle(WebPageContext webPageContext) throws Exception { public WebResponsePacket handle(WebPageContext webPageContext) {
String content = null; if (webPageContext.request.getPath().equalsIgnoreCase("download.html"))
URLConnection connection = null; return HttpsProxy.proxyGet(webPageContext, "https://open-autonomous-connection.org/download.html");
return null;
try {
connection = new URL("https://open-autonomous-connection.org/download.html").openConnection();
Scanner scanner = new Scanner(connection.getInputStream());
scanner.useDelimiter("\\Z");
content = scanner.next();
scanner.close();
}catch ( Exception ex ) {
content = ex.getMessage();
ex.printStackTrace();
}
return new WebResponsePacket(308, "text/html", new HashMap<>(), content.getBytes(StandardCharsets.UTF_8));
} }
} }

View File

@@ -0,0 +1,175 @@
package org.openautonomousconnection.oac2web.frontend;
import org.openautonomousconnection.oac2web.utils.Oac2WebApp;
import org.openautonomousconnection.oac2web.utils.RegistrarDao;
import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.WebResponsePacket;
import org.openautonomousconnection.protocol.side.web.ProtocolWebServer;
import org.openautonomousconnection.protocol.versions.v1_0_0.beta.WebRequestMethod;
import org.openautonomousconnection.webserver.api.Route;
import org.openautonomousconnection.webserver.api.SessionContext;
import org.openautonomousconnection.webserver.api.WebPage;
import org.openautonomousconnection.webserver.api.WebPageContext;
import org.openautonomousconnection.webserver.utils.Html;
import org.openautonomousconnection.webserver.utils.RequestParams;
import java.util.HashMap;
import java.util.Map;
/**
* Dashboard page: lists owned infonames and allows modifications.
*
* Requires valid session.
*
* POST headers (action-based):
* - action=create_infoname, tln, infoname
* - action=delete_infoname, id
* - action=add_record, id (infoname_id), sub, type, value, ttl, priority, port, weight
*/
@Route(path = "ins/dashboard")
public final class dashboard implements WebPage {
@Override
public WebResponsePacket handle(WebPageContext ctx) throws Exception {
SessionContext session = SessionContext.from(ctx.client, (ProtocolWebServer) ctx.client.getServer(), ctx.request.getHeaders());
if (!session.isValid() || session.getUser() == null) {
return new WebResponsePacket(401, "text/plain", new HashMap<>(), Html.utf8("Authentication required (session)."));
}
int userId;
try {
userId = Integer.parseInt(session.getUser());
} catch (Exception e) {
return new WebResponsePacket(401, "text/plain", new HashMap<>(), Html.utf8("Invalid session user."));
}
String msg = null;
String err = null;
if (ctx.request.getMethod() == WebRequestMethod.POST) {
RequestParams p = new RequestParams(ctx.request);
String action = p.getOr("action", "").trim();
try {
Oac2WebApp app = Oac2WebApp.get();
RegistrarDao dao = app.dao();
if ("create_infoname".equalsIgnoreCase(action)) {
String tln = p.get("tln");
String info = p.get("infoname");
if (tln == null || tln.isBlank() || info == null || info.isBlank()) {
err = "Missing tln / infoname.";
} else {
Integer tlnId = dao.findTlnId(tln.trim()).orElse(null);
if (tlnId == null) {
err = "Unknown TLN: " + tln;
} else {
int newId = dao.createInfoName(tlnId, info.trim(), userId);
msg = "Created infoname id=" + newId + " (" + info + "." + tln + ")";
}
}
} else if ("delete_infoname".equalsIgnoreCase(action)) {
int id = p.getInt("id", -1);
if (id <= 0) {
err = "Invalid id.";
} else if (!dao.isOwnerOfInfoName(id, userId)) {
err = "Not owner (edit/delete requires ownership).";
} else {
dao.deleteInfoName(id);
msg = "Deleted infoname id=" + id;
}
} else if ("add_record".equalsIgnoreCase(action)) {
int infonameId = p.getInt("id", -1);
if (infonameId <= 0) {
err = "Invalid infoname id.";
} else if (!dao.isOwnerOfInfoName(infonameId, userId)) {
err = "Not owner (edit/delete requires ownership).";
} else {
String sub = p.get("sub");
String type = p.getOr("type", "").trim().toUpperCase();
String value = p.get("value");
int ttl = p.getInt("ttl", 3600);
Integer priority = (p.get("priority") == null) ? null : p.getInt("priority", 0);
Integer port = (p.get("port") == null) ? null : p.getInt("port", 0);
Integer weight = (p.get("weight") == null) ? null : p.getInt("weight", 0);
if (type.isBlank() || value == null || value.isBlank()) {
err = "Missing type/value.";
} else {
Integer subId = dao.ensureSubname(infonameId, sub);
int rid = dao.addRecord(infonameId, subId, type, value.trim(), ttl, priority, port, weight);
msg = "Added record id=" + rid + " type=" + type;
}
}
} else if (!action.isBlank()) {
err = "Unknown action: " + action;
}
} catch (Exception e) {
err = "Action failed: " + e.getMessage();
}
}
return render(ctx, userId, msg, err);
}
private WebResponsePacket render(WebPageContext ctx, int userId, String msg, String err) throws Exception {
Oac2WebApp app = Oac2WebApp.get();
RegistrarDao.InfoNameRow[] owned = app.dao().listOwnedInfoNames(userId);
StringBuilder list = new StringBuilder();
if (owned.length == 0) {
list.append("<p class='muted'>No infonames yet.</p>");
} else {
list.append("<ul>");
for (RegistrarDao.InfoNameRow r : owned) {
list.append("<li>")
.append("<code>").append(Html.esc(r.info())).append(".").append(Html.esc(r.tln())).append("</code>")
.append(" (id=").append(r.id()).append(")")
.append("</li>");
}
list.append("</ul>");
}
String body = """
<div class="card">
<h2>Dashboard</h2>
<p class="muted">Owned by users.id = <code>%d</code></p>
%s
%s
<h3>Create InfoName</h3>
<p class="muted">POST headers: <code>action=create_infoname</code>, <code>tln</code>, <code>infoname</code></p>
<h3>Add Record</h3>
<p class="muted">POST headers: <code>action=add_record</code>, <code>id</code> (infoname id), optional <code>sub</code>, <code>type</code>, <code>value</code>, optional <code>ttl</code>, <code>priority</code>, <code>port</code>, <code>weight</code></p>
<h3>Delete InfoName</h3>
<p class="muted">POST headers: <code>action=delete_infoname</code>, <code>id</code></p>
<h3>Your InfoNames</h3>
%s
<div class="row">
<div class="col"><a href="/">Home</a></div>
</div>
</div>
""".formatted(
userId,
msg == null ? "" : "<p class='ok'>" + Html.esc(msg) + "</p>",
err == null ? "" : "<p class='err'>" + Html.esc(err) + "</p>",
list
);
String html = Html.page("Dashboard", body);
Map<String, String> headers = new HashMap<>();
// keep the same session header visible to client, if needed
if (ctx.request.getHeaders() != null) {
String sess = ctx.request.getHeaders().get("session");
if (sess != null) headers.put("session", sess);
}
return new WebResponsePacket(200, "text/html", headers, Html.utf8(html));
}
}

View File

@@ -0,0 +1,34 @@
package org.openautonomousconnection.oac2web.frontend;
import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.WebResponsePacket;
import org.openautonomousconnection.webserver.api.Route;
import org.openautonomousconnection.webserver.api.WebPage;
import org.openautonomousconnection.webserver.api.WebPageContext;
import org.openautonomousconnection.webserver.utils.Html;
import java.util.HashMap;
/**
* Landing page for the registrar frontend.
*/
@Route(path = "ins/index.html")
public final class index implements WebPage {
@Override
public WebResponsePacket handle(WebPageContext ctx) {
String html = Html.page("OAC INS Registrar", """
<div class="card">
<h2>OAC INS Registrar</h2>
<p class="muted">Server-side pages (oac2web). No extra endpoints.</p>
<div class="row">
<div class="col"><a href="/login">Login</a></div>
<div class="col"><a href="/register">Register</a></div>
<div class="col"><a href="/dashboard">Dashboard</a></div>
</div>
<p class="muted">POST parameters are expected via headers (e.g. <code>username</code>, <code>password</code>, <code>action</code>).</p>
</div>
""");
return new WebResponsePacket(200, "text/html", new HashMap<>(), Html.utf8(html));
}
}

View File

@@ -0,0 +1,82 @@
package org.openautonomousconnection.oac2web.frontend;
import org.openautonomousconnection.oac2web.utils.*;
import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.WebResponsePacket;
import org.openautonomousconnection.protocol.side.web.ProtocolWebServer;
import org.openautonomousconnection.protocol.side.web.managers.SessionManager;
import org.openautonomousconnection.protocol.versions.v1_0_0.beta.WebRequestMethod;
import org.openautonomousconnection.webserver.api.Route;
import org.openautonomousconnection.webserver.api.WebPage;
import org.openautonomousconnection.webserver.api.WebPageContext;
import org.openautonomousconnection.webserver.utils.Html;
import org.openautonomousconnection.webserver.utils.RequestParams;
import java.util.HashMap;
import java.util.Map;
/**
* Login page.
*
* POST headers expected:
* - username
* - password
*
* Creates a session with SessionManager.
*/
@Route(path = "ins/login")
public final class login implements WebPage {
@Override
public WebResponsePacket handle(WebPageContext ctx) throws Exception {
if (ctx.request.getMethod() != WebRequestMethod.POST) {
return renderForm(null);
}
RequestParams p = new RequestParams(ctx.request);
String username = p.get("username");
String password = p.get("password");
if (username == null || username.isBlank() || password == null || password.isBlank()) {
return renderForm("Missing username/password (send via headers).");
}
Oac2WebApp app = Oac2WebApp.get();
String usernameHash = Sha256.hex(username.trim());
RegistrarDao.UserRow u = app.dao().findUserByUsernameHash(usernameHash).orElse(null);
if (u == null) return renderForm("Invalid credentials.");
boolean ok = app.passwordHasher().verify(password, u.passwordEncoded());
if (!ok) return renderForm("Invalid credentials.");
String ip = (ctx.client.getConnection().getSocket() != null && ctx.client.getConnection().getSocket().getInetAddress() != null)
? ctx.client.getConnection().getSocket().getInetAddress().getHostAddress()
: "";
String ua = ctx.request.getHeaders() != null ? ctx.request.getHeaders().getOrDefault("user-agent", "") : "";
String session = SessionManager.create(String.valueOf(u.id()), ip, ua, (ProtocolWebServer) ctx.client.getServer());
Map<String, String> headers = new HashMap<>();
headers.put("session", session);
headers.put("location", "/dashboard");
return new WebResponsePacket(302, "text/plain", headers, new byte[0]);
}
private WebResponsePacket renderForm(String err) {
String body = """
<div class="card">
<h2>Login</h2>
%s
<p class="muted">Send a POST request with headers <code>username</code> and <code>password</code>.</p>
<div class="row">
<div class="col"><a href="/register">Register</a></div>
<div class="col"><a href="/">Home</a></div>
</div>
</div>
""".formatted(err == null ? "" : "<p class='err'>" + Html.esc(err) + "</p>");
String html = Html.page("Login", body);
return new WebResponsePacket(200, "text/html", new HashMap<>(), Html.utf8(html));
}
}

View File

@@ -0,0 +1,96 @@
package org.openautonomousconnection.oac2web.frontend;
import org.openautonomousconnection.oac2web.utils.Oac2WebApp;
import org.openautonomousconnection.oac2web.utils.Sha256;
import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.WebResponsePacket;
import org.openautonomousconnection.protocol.side.web.ProtocolWebServer;
import org.openautonomousconnection.protocol.side.web.managers.SessionManager;
import org.openautonomousconnection.protocol.versions.v1_0_0.beta.WebRequestMethod;
import org.openautonomousconnection.webserver.api.Route;
import org.openautonomousconnection.webserver.api.WebPage;
import org.openautonomousconnection.webserver.api.WebPageContext;
import org.openautonomousconnection.webserver.utils.Html;
import org.openautonomousconnection.webserver.utils.RequestParams;
import java.util.HashMap;
import java.util.Map;
/**
* Registration page.
*
* POST headers expected:
* - username
* - password
*
* Stores:
* - users.username = sha256(username)
* - users.password = PBKDF2$sha256$...
*/
@Route(path = "ins/register")
public final class register implements WebPage {
@Override
public WebResponsePacket handle(WebPageContext ctx) throws Exception {
if (ctx.request.getMethod() != WebRequestMethod.POST) {
return renderForm(null, null);
}
RequestParams p = new RequestParams(ctx.request);
String username = p.get("username");
String password = p.get("password");
if (username == null || username.isBlank() || password == null || password.isBlank()) {
return renderForm("Missing username/password (send via headers).", null);
}
Oac2WebApp app = Oac2WebApp.get();
String usernameHash = Sha256.hex(username.trim());
String passwordEnc = app.passwordHasher().hash(password);
try {
int userId = app.dao().createUser(usernameHash, passwordEnc);
String ip = (ctx.client.getConnection().getSocket() != null && ctx.client.getConnection().getSocket().getInetAddress() != null)
? ctx.client.getConnection().getSocket().getInetAddress().getHostAddress()
: "";
String ua = ctx.request.getHeaders() != null ? ctx.request.getHeaders().getOrDefault("user-agent", "") : "";
// SessionManager user string: we store numeric users.id as string.
String session = SessionManager.create(String.valueOf(userId), ip, ua, (ProtocolWebServer) ctx.client.getServer());
Map<String, String> headers = new HashMap<>();
headers.put("session", session);
headers.put("location", "/dashboard");
return new WebResponsePacket(302, "text/plain", headers, new byte[0]);
} catch (Exception e) {
// likely UNIQUE violation on users.username (hashed)
return renderForm("Register failed: " + e.getMessage(), null);
}
}
private WebResponsePacket renderForm(String err, String ok) {
String body = """
<div class="card">
<h2>Register</h2>
%s
%s
<p class="muted">Send a POST request with headers <code>username</code> and <code>password</code>.</p>
<div class="row">
<div class="col"><a href="/login">Login</a></div>
<div class="col"><a href="/">Home</a></div>
</div>
</div>
""".formatted(
err == null ? "" : "<p class='err'>" + Html.esc(err) + "</p>",
ok == null ? "" : "<p class='ok'>" + Html.esc(ok) + "</p>"
);
String html = Html.page("Register", body);
return new WebResponsePacket(200, "text/html", new HashMap<>(), Html.utf8(html));
}
}

View File

@@ -1,33 +1,21 @@
package org.openautonomousconnection.oac2web; package org.openautonomousconnection.oac2web;
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.webserver.api.Route;
import org.openautonomousconnection.webserver.api.WebPage; import org.openautonomousconnection.webserver.api.WebPage;
import org.openautonomousconnection.webserver.api.WebPageContext; import org.openautonomousconnection.webserver.api.WebPageContext;
import org.openautonomousconnection.webserver.utils.HttpsProxy;
import java.net.URL; /**
import java.net.URLConnection; * Proxies the HTTPS wiki page through OAC.
import java.nio.charset.StandardCharsets; */
import java.util.HashMap; @Route(path = "index.html")
import java.util.Map;
import java.util.Scanner;
public class index implements WebPage { public class index implements WebPage {
@Override @Override
public WebResponsePacket handle(WebPageContext webPageContext) throws Exception { public WebResponsePacket handle(WebPageContext webPageContext) {
String content = null; if (webPageContext.request.getPath().equalsIgnoreCase("index.html"))
URLConnection connection = null; return HttpsProxy.proxyGet(webPageContext, "https://open-autonomous-connection.org/index.html");
return null;
try {
connection = new URL("https://open-autonomous-connection.org/index.html").openConnection();
Scanner scanner = new Scanner(connection.getInputStream());
scanner.useDelimiter("\\Z");
content = scanner.next();
scanner.close();
}catch ( Exception ex ) {
content = ex.getMessage();
ex.printStackTrace();
}
return new WebResponsePacket(308, "text/html", new HashMap<>(), content.getBytes(StandardCharsets.UTF_8));
} }
} }

View File

@@ -1,32 +1,21 @@
package org.openautonomousconnection.oac2web; package org.openautonomousconnection.oac2web;
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.webserver.api.Route;
import org.openautonomousconnection.webserver.api.WebPage; import org.openautonomousconnection.webserver.api.WebPage;
import org.openautonomousconnection.webserver.api.WebPageContext; import org.openautonomousconnection.webserver.api.WebPageContext;
import org.openautonomousconnection.webserver.utils.HttpsProxy;
import java.net.URL; /**
import java.net.URLConnection; * Proxies the HTTPS INS page through OAC.
import java.nio.charset.StandardCharsets; */
import java.util.HashMap; @Route(path = "ins.php")
import java.util.Scanner;
public class ins implements WebPage { public class ins implements WebPage {
@Override @Override
public WebResponsePacket handle(WebPageContext webPageContext) throws Exception { public WebResponsePacket handle(WebPageContext webPageContext) {
String content = null; if (webPageContext.request.getPath().equalsIgnoreCase("ins.php"))
URLConnection connection = null; return HttpsProxy.proxyGet(webPageContext, "https://open-autonomous-connection.org/ins.php");
return null;
try {
connection = new URL("https://open-autonomous-connection.org/ins.php").openConnection();
Scanner scanner = new Scanner(connection.getInputStream());
scanner.useDelimiter("\\Z");
content = scanner.next();
scanner.close();
}catch ( Exception ex ) {
content = ex.getMessage();
ex.printStackTrace();
}
return new WebResponsePacket(308, "text/html", new HashMap<>(), content.getBytes(StandardCharsets.UTF_8));
} }
} }

View File

@@ -1,32 +1,21 @@
package org.openautonomousconnection.oac2web; package org.openautonomousconnection.oac2web;
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.webserver.api.Route;
import org.openautonomousconnection.webserver.api.WebPage; import org.openautonomousconnection.webserver.api.WebPage;
import org.openautonomousconnection.webserver.api.WebPageContext; import org.openautonomousconnection.webserver.api.WebPageContext;
import org.openautonomousconnection.webserver.utils.HttpsProxy;
import java.net.URL; /**
import java.net.URLConnection; * Proxies the HTTPS license page through OAC.
import java.nio.charset.StandardCharsets; */
import java.util.HashMap; @Route(path = "license.html")
import java.util.Scanner;
public class license implements WebPage { public class license implements WebPage {
@Override @Override
public WebResponsePacket handle(WebPageContext webPageContext) throws Exception { public WebResponsePacket handle(WebPageContext webPageContext) {
String content = null; if (webPageContext.request.getPath().equalsIgnoreCase("license.html"))
URLConnection connection = null; return HttpsProxy.proxyGet(webPageContext, "https://open-autonomous-connection.org/license.html");
return null;
try {
connection = new URL("https://open-autonomous-connection.org/license.html").openConnection();
Scanner scanner = new Scanner(connection.getInputStream());
scanner.useDelimiter("\\Z");
content = scanner.next();
scanner.close();
}catch ( Exception ex ) {
content = ex.getMessage();
ex.printStackTrace();
}
return new WebResponsePacket(308, "text/html", new HashMap<>(), content.getBytes(StandardCharsets.UTF_8));
} }
} }

View File

@@ -0,0 +1,48 @@
package org.openautonomousconnection.oac2web.utils;
/**
* Hex encoding helper.
*/
public final class Hex {
private static final char[] HEX = "0123456789abcdef".toCharArray();
private Hex() {}
/**
* Encodes bytes to lowercase hex.
*
* @param data bytes
* @return hex
*/
public static String lower(byte[] data) {
if (data == null) return "";
char[] out = new char[data.length * 2];
int i = 0;
for (byte b : data) {
out[i++] = HEX[(b >>> 4) & 0x0F];
out[i++] = HEX[b & 0x0F];
}
return new String(out);
}
/**
* Decodes lowercase/uppercase hex to bytes.
*
* @param hex hex string
* @return bytes
*/
public static byte[] decode(String hex) {
if (hex == null) return new byte[0];
String s = hex.trim();
if (s.length() % 2 != 0) throw new IllegalArgumentException("Invalid hex length");
byte[] out = new byte[s.length() / 2];
for (int i = 0; i < out.length; i++) {
int hi = Character.digit(s.charAt(i * 2), 16);
int lo = Character.digit(s.charAt(i * 2 + 1), 16);
if (hi < 0 || lo < 0) throw new IllegalArgumentException("Invalid hex char");
out[i] = (byte) ((hi << 4) | lo);
}
return out;
}
}

View File

@@ -0,0 +1,58 @@
package org.openautonomousconnection.oac2web.utils;
import java.io.File;
import java.io.IOException;
/**
* Global app context for oac2web pages (config + dao + crypto).
*/
public final class Oac2WebApp {
private static volatile Oac2WebApp INSTANCE;
private final Oac2WebConfig config;
private final Oac2WebDb db;
private final RegistrarDao dao;
private final Pbkdf2PasswordHasher passwordHasher;
private Oac2WebApp(Oac2WebConfig config) {
this.config = config;
this.db = new Oac2WebDb(config.jdbcUrl(), config.jdbcUser(), config.jdbcPassword());
this.dao = new RegistrarDao(db);
this.passwordHasher = new Pbkdf2PasswordHasher(config.pbkdf2Iterations(), config.pbkdf2SaltBytes(), config.pbkdf2KeyBytes());
}
/**
* Initializes or returns global instance.
*
* @return singleton
*/
public static Oac2WebApp get() {
Oac2WebApp v = INSTANCE;
if (v != null) return v;
synchronized (Oac2WebApp.class) {
if (INSTANCE == null) {
try {
INSTANCE = new Oac2WebApp(new Oac2WebConfig(new File("ins.properties")));
} catch (IOException e) {
throw new IllegalStateException("Failed to load ins.properties", e);
}
}
return INSTANCE;
}
}
/**
* @return dao
*/
public RegistrarDao dao() {
return dao;
}
/**
* @return password hasher
*/
public Pbkdf2PasswordHasher passwordHasher() {
return passwordHasher;
}
}

View File

@@ -0,0 +1,91 @@
package org.openautonomousconnection.oac2web.utils;
import dev.unlegitdqrk.unlegitlibrary.file.ConfigurationManager;
import java.io.File;
import java.io.IOException;
/**
* Reads configuration for the oac2web registrar frontend.
*/
public final class Oac2WebConfig {
private final ConfigurationManager cfg;
/**
* Creates a config wrapper backed by the given properties file.
*
* @param file config file
* @throws IOException if loading fails
*/
public Oac2WebConfig(File file) throws IOException {
this.cfg = new ConfigurationManager(file);
this.cfg.loadProperties();
ensureDefaults();
this.cfg.saveProperties();
}
private void ensureDefaults() {
if (!cfg.isSet("db.url")) {
cfg.set("db.url", "jdbc:mysql://localhost:3306/oac_ins?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC");
}
if (!cfg.isSet("db.user")) {
cfg.set("db.user", "username");
}
if (!cfg.isSet("db.password")) {
cfg.set("db.password", "password");
}
if (!cfg.isSet("security.pbkdf2.iterations")) {
cfg.set("security.pbkdf2.iterations", 120000);
}
if (!cfg.isSet("security.pbkdf2.saltBytes")) {
cfg.set("security.pbkdf2.saltBytes", 16);
}
if (!cfg.isSet("security.pbkdf2.keyBytes")) {
cfg.set("security.pbkdf2.keyBytes", 32);
}
}
/**
* @return JDBC url
*/
public String jdbcUrl() {
return cfg.getString("db.url");
}
/**
* @return JDBC user
*/
public String jdbcUser() {
return cfg.getString("db.user");
}
/**
* @return JDBC password
*/
public String jdbcPassword() {
return cfg.getString("db.password");
}
/**
* @return PBKDF2 iterations
*/
public int pbkdf2Iterations() {
return cfg.getInt("security.pbkdf2.iterations");
}
/**
* @return PBKDF2 salt length in bytes
*/
public int pbkdf2SaltBytes() {
return cfg.getInt("security.pbkdf2.saltBytes");
}
/**
* @return PBKDF2 derived key length in bytes
*/
public int pbkdf2KeyBytes() {
return cfg.getInt("security.pbkdf2.keyBytes");
}
}

View File

@@ -0,0 +1,39 @@
package org.openautonomousconnection.oac2web.utils;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Objects;
/**
* Very small JDBC helper for the oac2web registrar frontend.
*/
public final class Oac2WebDb {
private final String url;
private final String user;
private final String password;
/**
* Creates a JDBC helper.
*
* @param url JDBC url
* @param user JDBC username
* @param password JDBC password
*/
public Oac2WebDb(String url, String user, String password) {
this.url = Objects.requireNonNull(url, "url");
this.user = Objects.requireNonNull(user, "user");
this.password = Objects.requireNonNull(password, "password");
}
/**
* Opens a new JDBC connection.
*
* @return connection
* @throws SQLException if open fails
*/
public Connection open() throws SQLException {
return DriverManager.getConnection(url, user, password);
}
}

View File

@@ -0,0 +1,104 @@
package org.openautonomousconnection.oac2web.utils;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.security.SecureRandom;
import java.util.Objects;
/**
* PBKDF2 password hashing (PBKDF2WithHmacSHA256).
*
* Storage format:
* PBKDF2$sha256$<iterations>$<saltHex>$<hashHex>
*/
public final class Pbkdf2PasswordHasher {
private final SecureRandom rng = new SecureRandom();
private final int iterations;
private final int saltBytes;
private final int keyBytes;
/**
* Creates a hasher.
*
* @param iterations iterations
* @param saltBytes salt bytes
* @param keyBytes derived key bytes
*/
public Pbkdf2PasswordHasher(int iterations, int saltBytes, int keyBytes) {
if (iterations < 10_000) throw new IllegalArgumentException("iterations too low");
if (saltBytes < 8) throw new IllegalArgumentException("saltBytes too low");
if (keyBytes < 16) throw new IllegalArgumentException("keyBytes too low");
this.iterations = iterations;
this.saltBytes = saltBytes;
this.keyBytes = keyBytes;
}
/**
* Hashes a password into the storage format.
*
* @param password raw password
* @return encoded string
*/
public String hash(String password) {
Objects.requireNonNull(password, "password");
byte[] salt = new byte[saltBytes];
rng.nextBytes(salt);
byte[] dk = derive(password.toCharArray(), salt, iterations, keyBytes);
return "PBKDF2$sha256$" + iterations + "$" + Hex.lower(salt) + "$" + Hex.lower(dk);
}
/**
* Verifies a password against stored format.
*
* @param password raw password
* @param stored stored string
* @return true if valid
*/
public boolean verify(String password, String stored) {
if (password == null || stored == null) return false;
String[] parts = stored.split("\\$");
if (parts.length != 5) return false;
if (!"PBKDF2".equals(parts[0])) return false;
if (!"sha256".equalsIgnoreCase(parts[1])) return false;
int it;
try {
it = Integer.parseInt(parts[2]);
} catch (NumberFormatException e) {
return false;
}
byte[] salt;
byte[] expected;
try {
salt = Hex.decode(parts[3]);
expected = Hex.decode(parts[4]);
} catch (Exception e) {
return false;
}
byte[] actual = derive(password.toCharArray(), salt, it, expected.length);
return constantTimeEquals(expected, actual);
}
private static byte[] derive(char[] password, byte[] salt, int iterations, int keyBytes) {
try {
PBEKeySpec spec = new PBEKeySpec(password, salt, iterations, keyBytes * 8);
SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
return skf.generateSecret(spec).getEncoded();
} catch (Exception e) {
throw new IllegalStateException("PBKDF2 not available", e);
}
}
private static boolean constantTimeEquals(byte[] a, byte[] b) {
if (a == null || b == null) return false;
if (a.length != b.length) return false;
int r = 0;
for (int i = 0; i < a.length; i++) r |= (a[i] ^ b[i]);
return r == 0;
}
}

View File

@@ -0,0 +1,291 @@
package org.openautonomousconnection.oac2web.utils;
import java.sql.*;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
/**
* Data access for users and INS registrar data.
*
* Uses the provided oac_ins SQL schema:
* - users(id, uid, username, password)
* - infonames(id, info, tln_id, uid)
* - tln(id, name, info, owner_id, is_public, allow_subdomains)
* - subnames(id, name, infoname_id)
* - records(id, infoname_id, subname_id, type, value, ttl, priority, port, weight)
*/
public final class RegistrarDao {
private final Oac2WebDb db;
/**
* Creates the DAO.
*
* @param db db helper
*/
public RegistrarDao(Oac2WebDb db) {
this.db = Objects.requireNonNull(db, "db");
}
/**
* Creates a new user.
*
* @param usernameHash SHA-256 hex(username)
* @param passwordEncoded PBKDF2 storage string
* @return created user id
* @throws SQLException on error
*/
public int createUser(String usernameHash, String passwordEncoded) throws SQLException {
String sql = "INSERT INTO users(uid, username, password) VALUES(?,?,?)";
try (Connection c = db.open();
PreparedStatement ps = c.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
ps.setString(1, UUID.randomUUID().toString());
ps.setString(2, usernameHash);
ps.setString(3, passwordEncoded);
ps.executeUpdate();
try (ResultSet rs = ps.getGeneratedKeys()) {
if (rs.next()) return rs.getInt(1);
}
throw new SQLException("No generated key for users");
}
}
/**
* Finds a user by username hash.
*
* @param usernameHash SHA-256 hex(username)
* @return optional user
* @throws SQLException on error
*/
public Optional<UserRow> findUserByUsernameHash(String usernameHash) throws SQLException {
String sql = "SELECT id, uid, username, password FROM users WHERE username = ?";
try (Connection c = db.open();
PreparedStatement ps = c.prepareStatement(sql)) {
ps.setString(1, usernameHash);
try (ResultSet rs = ps.executeQuery()) {
if (!rs.next()) return Optional.empty();
return Optional.of(new UserRow(
rs.getInt("id"),
rs.getString("uid"),
rs.getString("username"),
rs.getString("password")
));
}
}
}
/**
* Resolves TLN id by name.
*
* @param tln TLN name
* @return optional id
* @throws SQLException on error
*/
public Optional<Integer> findTlnId(String tln) throws SQLException {
String sql = "SELECT id FROM tln WHERE name = ?";
try (Connection c = db.open();
PreparedStatement ps = c.prepareStatement(sql)) {
ps.setString(1, tln);
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) return Optional.of(rs.getInt("id"));
return Optional.empty();
}
}
}
/**
* Creates a new infoname owned by a user.
*
* @param tlnId TLN id
* @param info info name (label)
* @param userId owner users.id
* @return infoname id
* @throws SQLException on error
*/
public int createInfoName(int tlnId, String info, int userId) throws SQLException {
String sql = "INSERT INTO infonames(info, tln_id, uid) VALUES(?,?,?)";
try (Connection c = db.open();
PreparedStatement ps = c.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
ps.setString(1, info);
ps.setInt(2, tlnId);
ps.setInt(3, userId);
ps.executeUpdate();
try (ResultSet rs = ps.getGeneratedKeys()) {
if (rs.next()) return rs.getInt(1);
}
throw new SQLException("No generated key for infonames");
}
}
/**
* Checks ownership (used for edit/delete only).
*
* @param infonameId infoname id
* @param userId owner users.id
* @return true if owned by user
* @throws SQLException on error
*/
public boolean isOwnerOfInfoName(int infonameId, int userId) throws SQLException {
String sql = "SELECT 1 FROM infonames WHERE id = ? AND uid = ?";
try (Connection c = db.open();
PreparedStatement ps = c.prepareStatement(sql)) {
ps.setInt(1, infonameId);
ps.setInt(2, userId);
try (ResultSet rs = ps.executeQuery()) {
return rs.next();
}
}
}
/**
* Lists infonames owned by user.
*
* @param userId users.id
* @return result set as lightweight string table
* @throws SQLException on error
*/
public InfoNameRow[] listOwnedInfoNames(int userId) throws SQLException {
String sql = """
SELECT i.id, i.info, t.name AS tln
FROM infonames i
JOIN tln t ON t.id = i.tln_id
WHERE i.uid = ?
ORDER BY t.name ASC, i.info ASC
""";
try (Connection c = db.open();
PreparedStatement ps = c.prepareStatement(sql)) {
ps.setInt(1, userId);
try (ResultSet rs = ps.executeQuery()) {
java.util.ArrayList<InfoNameRow> out = new java.util.ArrayList<>();
while (rs.next()) {
out.add(new InfoNameRow(
rs.getInt("id"),
rs.getString("tln"),
rs.getString("info")
));
}
return out.toArray(new InfoNameRow[0]);
}
}
}
/**
* Ensures a subname row exists (or null when sub is empty).
*
* @param infonameId infoname id
* @param sub sub label
* @return subname id or null
* @throws SQLException on error
*/
public Integer ensureSubname(int infonameId, String sub) throws SQLException {
if (sub == null || sub.isBlank()) return null;
String find = "SELECT id FROM subnames WHERE infoname_id = ? AND name = ?";
try (Connection c = db.open();
PreparedStatement ps = c.prepareStatement(find)) {
ps.setInt(1, infonameId);
ps.setString(2, sub);
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) return rs.getInt("id");
}
}
String ins = "INSERT INTO subnames(name, infoname_id) VALUES(?,?)";
try (Connection c = db.open();
PreparedStatement ps = c.prepareStatement(ins, Statement.RETURN_GENERATED_KEYS)) {
ps.setString(1, sub);
ps.setInt(2, infonameId);
ps.executeUpdate();
try (ResultSet rs = ps.getGeneratedKeys()) {
if (rs.next()) return rs.getInt(1);
}
throw new SQLException("No generated key for subnames");
}
}
/**
* Adds a record.
*
* @param infonameId infoname id
* @param subnameId subname id or null
* @param type record type enum string (A, AAAA, TXT, CNAME, MX, SRV, NS)
* @param value value
* @param ttl ttl
* @param priority priority
* @param port port
* @param weight weight
* @return record id
* @throws SQLException on error
*/
public int addRecord(int infonameId, Integer subnameId, String type, String value, int ttl, Integer priority, Integer port, Integer weight) throws SQLException {
String sql = "INSERT INTO records(infoname_id, subname_id, type, value, ttl, priority, port, weight) VALUES(?,?,?,?,?,?,?,?)";
try (Connection c = db.open();
PreparedStatement ps = c.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
ps.setInt(1, infonameId);
if (subnameId == null) ps.setNull(2, Types.INTEGER);
else ps.setInt(2, subnameId);
ps.setString(3, type);
ps.setString(4, value);
ps.setInt(5, ttl);
if (priority == null) ps.setNull(6, Types.INTEGER); else ps.setInt(6, priority);
if (port == null) ps.setNull(7, Types.INTEGER); else ps.setInt(7, port);
if (weight == null) ps.setNull(8, Types.INTEGER); else ps.setInt(8, weight);
ps.executeUpdate();
try (ResultSet rs = ps.getGeneratedKeys()) {
if (rs.next()) return rs.getInt(1);
}
throw new SQLException("No generated key for records");
}
}
/**
* Deletes an infoname (CASCADE deletes subnames and records by schema).
*
* @param infonameId id
* @throws SQLException on error
*/
public void deleteInfoName(int infonameId) throws SQLException {
String sql = "DELETE FROM infonames WHERE id = ?";
try (Connection c = db.open();
PreparedStatement ps = c.prepareStatement(sql)) {
ps.setInt(1, infonameId);
ps.executeUpdate();
}
}
/**
* Lightweight row for users.
*
* @param id users.id
* @param uid users.uid
* @param usernameHash users.username (hashed)
* @param passwordEncoded users.password
*/
public record UserRow(int id, String uid, String usernameHash, String passwordEncoded) {}
/**
* Lightweight row for owned infonames.
*
* @param id infonames.id
* @param tln tln.name
* @param info infonames.info
*/
public record InfoNameRow(int id, String tln, String info) {}
}

View File

@@ -0,0 +1,29 @@
package org.openautonomousconnection.oac2web.utils;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
/**
* Hash utilities.
*/
public final class Sha256 {
private Sha256() {}
/**
* Hashes text as SHA-256 (hex lowercase).
*
* @param text input
* @return hex sha256
*/
public static String hex(String text) {
if (text == null) text = "";
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] digest = md.digest(text.getBytes(StandardCharsets.UTF_8));
return Hex.lower(digest);
} catch (Exception e) {
throw new IllegalStateException("SHA-256 not available", e);
}
}
}

View File

@@ -1,32 +1,21 @@
package org.openautonomousconnection.oac2web; package org.openautonomousconnection.oac2web;
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.webserver.api.Route;
import org.openautonomousconnection.webserver.api.WebPage; import org.openautonomousconnection.webserver.api.WebPage;
import org.openautonomousconnection.webserver.api.WebPageContext; import org.openautonomousconnection.webserver.api.WebPageContext;
import org.openautonomousconnection.webserver.utils.HttpsProxy;
import java.net.URL; /**
import java.net.URLConnection; * Proxies the HTTPS wiki page through OAC.
import java.nio.charset.StandardCharsets; */
import java.util.HashMap; @Route(path = "wiki.html")
import java.util.Scanner;
public class wiki implements WebPage { public class wiki implements WebPage {
@Override @Override
public WebResponsePacket handle(WebPageContext webPageContext) throws Exception { public WebResponsePacket handle(WebPageContext webPageContext) {
String content = null; if (webPageContext.request.getPath().equalsIgnoreCase("wiki.html"))
URLConnection connection = null; return HttpsProxy.proxyGet(webPageContext, "https://open-autonomous-connection.org/wiki.html");
return null;
try {
connection = new URL("https://open-autonomous-connection.org/wiki.html").openConnection();
Scanner scanner = new Scanner(connection.getInputStream());
scanner.useDelimiter("\\Z");
content = scanner.next();
scanner.close();
}catch ( Exception ex ) {
content = ex.getMessage();
ex.printStackTrace();
}
return new WebResponsePacket(308, "text/html", new HashMap<>(), content.getBytes(StandardCharsets.UTF_8));
} }
} }