Updated to new WebServer-Version
This commit is contained in:
53
.idea/misc.xml
generated
53
.idea/misc.xml
generated
@@ -1,9 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DiscordProjectSettings">
|
||||
<option name="show" value="ASK" />
|
||||
<option name="description" value="" />
|
||||
</component>
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="MavenProjectsManager">
|
||||
<option name="originalFiles">
|
||||
@@ -12,6 +8,55 @@
|
||||
</list>
|
||||
</option>
|
||||
</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">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
|
||||
2
pom.xml
2
pom.xml
@@ -104,7 +104,7 @@
|
||||
<dependency>
|
||||
<groupId>org.openautonomousconnection</groupId>
|
||||
<artifactId>WebServer</artifactId>
|
||||
<version>1.0.0-BETA.1.3</version>
|
||||
<version>1.0.0-BETA.1.5</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.openautonomousconnection.oac2web;
|
||||
import com.google.gson.JsonObject;
|
||||
import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.WebResponsePacket;
|
||||
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;
|
||||
|
||||
@@ -18,16 +19,19 @@ import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Route(path = "api")
|
||||
public class api implements WebPage {
|
||||
@Override
|
||||
public WebResponsePacket handle(WebPageContext webPageContext) throws Exception {
|
||||
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");
|
||||
int port = 0;
|
||||
int tcp = 0;
|
||||
int udp = 0;
|
||||
|
||||
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) {
|
||||
return new WebResponsePacket(400, "text/plain", webPageContext.request.getHeaders(),
|
||||
("Bad request:\n" + exception.getMessage()).getBytes());
|
||||
@@ -38,9 +42,10 @@ public class api implements WebPage {
|
||||
|
||||
try {
|
||||
HashMap<String, String> dataMap = new HashMap<>();
|
||||
dataMap.put("insIp", hostIp);
|
||||
dataMap.put("insPort", String.valueOf(port));
|
||||
dataMap.put("insDesc", description);
|
||||
dataMap.put("ip", hostIp);
|
||||
dataMap.put("tcp", String.valueOf(tcp));
|
||||
dataMap.put("udp", String.valueOf(udp));
|
||||
dataMap.put("desc", description);
|
||||
|
||||
URL url = new URL("https://open-autonomous-connection.org/api/add.php");
|
||||
HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
|
||||
|
||||
@@ -1,32 +1,21 @@
|
||||
package org.openautonomousconnection.oac2web;
|
||||
|
||||
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.HttpsProxy;
|
||||
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.Scanner;
|
||||
|
||||
/**
|
||||
* Proxies the HTTPS download page through OAC.
|
||||
*/
|
||||
@Route(path = "download.html")
|
||||
public class download implements WebPage {
|
||||
|
||||
@Override
|
||||
public WebResponsePacket handle(WebPageContext webPageContext) throws Exception {
|
||||
String content = null;
|
||||
URLConnection connection = 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));
|
||||
public WebResponsePacket handle(WebPageContext webPageContext) {
|
||||
if (webPageContext.request.getPath().equalsIgnoreCase("download.html"))
|
||||
return HttpsProxy.proxyGet(webPageContext, "https://open-autonomous-connection.org/download.html");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -1,33 +1,21 @@
|
||||
package org.openautonomousconnection.oac2web;
|
||||
|
||||
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.HttpsProxy;
|
||||
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Scanner;
|
||||
|
||||
/**
|
||||
* Proxies the HTTPS wiki page through OAC.
|
||||
*/
|
||||
@Route(path = "index.html")
|
||||
public class index implements WebPage {
|
||||
|
||||
@Override
|
||||
public WebResponsePacket handle(WebPageContext webPageContext) throws Exception {
|
||||
String content = null;
|
||||
URLConnection connection = 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));
|
||||
public WebResponsePacket handle(WebPageContext webPageContext) {
|
||||
if (webPageContext.request.getPath().equalsIgnoreCase("index.html"))
|
||||
return HttpsProxy.proxyGet(webPageContext, "https://open-autonomous-connection.org/index.html");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +1,21 @@
|
||||
package org.openautonomousconnection.oac2web;
|
||||
|
||||
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.HttpsProxy;
|
||||
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.Scanner;
|
||||
|
||||
/**
|
||||
* Proxies the HTTPS INS page through OAC.
|
||||
*/
|
||||
@Route(path = "ins.php")
|
||||
public class ins implements WebPage {
|
||||
|
||||
@Override
|
||||
public WebResponsePacket handle(WebPageContext webPageContext) throws Exception {
|
||||
String content = null;
|
||||
URLConnection connection = 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));
|
||||
public WebResponsePacket handle(WebPageContext webPageContext) {
|
||||
if (webPageContext.request.getPath().equalsIgnoreCase("ins.php"))
|
||||
return HttpsProxy.proxyGet(webPageContext, "https://open-autonomous-connection.org/ins.php");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +1,21 @@
|
||||
package org.openautonomousconnection.oac2web;
|
||||
|
||||
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.HttpsProxy;
|
||||
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.Scanner;
|
||||
|
||||
/**
|
||||
* Proxies the HTTPS license page through OAC.
|
||||
*/
|
||||
@Route(path = "license.html")
|
||||
public class license implements WebPage {
|
||||
|
||||
@Override
|
||||
public WebResponsePacket handle(WebPageContext webPageContext) throws Exception {
|
||||
String content = null;
|
||||
URLConnection connection = 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));
|
||||
public WebResponsePacket handle(WebPageContext webPageContext) {
|
||||
if (webPageContext.request.getPath().equalsIgnoreCase("license.html"))
|
||||
return HttpsProxy.proxyGet(webPageContext, "https://open-autonomous-connection.org/license.html");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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) {}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,32 +1,21 @@
|
||||
package org.openautonomousconnection.oac2web;
|
||||
|
||||
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.HttpsProxy;
|
||||
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.Scanner;
|
||||
|
||||
/**
|
||||
* Proxies the HTTPS wiki page through OAC.
|
||||
*/
|
||||
@Route(path = "wiki.html")
|
||||
public class wiki implements WebPage {
|
||||
|
||||
@Override
|
||||
public WebResponsePacket handle(WebPageContext webPageContext) throws Exception {
|
||||
String content = null;
|
||||
URLConnection connection = 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));
|
||||
public WebResponsePacket handle(WebPageContext webPageContext) {
|
||||
if (webPageContext.request.getPath().equalsIgnoreCase("wiki.html"))
|
||||
return HttpsProxy.proxyGet(webPageContext, "https://open-autonomous-connection.org/wiki.html");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user