Finished INS

This commit is contained in:
Finn
2025-12-11 12:36:33 +01:00
parent 6f5d355f79
commit 25d6f1de21
2 changed files with 211 additions and 82 deletions

View File

@@ -1,7 +1,7 @@
package org.openautonomousconnection.ins;
import dev.unlegitdqrk.unlegitlibrary.file.ConfigurationManager;
import org.openautonomousconnection.protocol.ProtocolBridge;
import org.openautonomousconnection.ins.utils.TargetName;
import org.openautonomousconnection.protocol.side.ins.ProtocolINSServer;
import org.openautonomousconnection.protocol.versions.v1_0_0.beta.INSRecord;
import org.openautonomousconnection.protocol.versions.v1_0_0.beta.INSRecordType;
@@ -13,31 +13,6 @@ import java.sql.*;
import java.util.ArrayList;
import java.util.List;
/**
* Concrete INS server implementation backed by a SQL database.
* <p>
* This implementation expects a table schema similar to:
* <pre>
* CREATE TABLE ins_records (
* id BIGINT PRIMARY KEY AUTO_INCREMENT,
* tln VARCHAR(64) NOT NULL,
* name VARCHAR(255) NOT NULL,
* sub VARCHAR(255) NULL,
* type VARCHAR(16) NOT NULL, -- matches {@link INSRecordType#name()}
* value VARCHAR(1024) NOT NULL,
* priority INT NOT NULL,
* weight INT NOT NULL,
* port INT NOT NULL,
* ttl INT NOT NULL
* );
*
* CREATE TABLE ins_tln_info (
* tln VARCHAR(64) PRIMARY KEY,
* host VARCHAR(255) NOT NULL,
* port INT NOT NULL
* );
* </pre>
*/
public final class DatabaseINSServer extends ProtocolINSServer {
private final String jdbcUrl;
@@ -59,7 +34,10 @@ public final class DatabaseINSServer extends ProtocolINSServer {
configurationManager.loadProperties();
if (!configurationManager.isSet("db.url")) {
configurationManager.set("db.url", "jdbc:mysql://localhost:3306/DB_NAME?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC\n");
configurationManager.set(
"db.url",
"jdbc:mysql://localhost:3306/ins?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC"
);
configurationManager.saveProperties();
}
@@ -83,70 +61,36 @@ public final class DatabaseINSServer extends ProtocolINSServer {
}
/**
* Resolves records for the given INS name from the database.
* Resolves a request for an INS record based on TLN, name, subname and record type.
* <p>
* This method does a single lookup and returns exactly the records that
* match the given query (no CNAME recursion here that is handled on a
* higher level, e.g. via {@code INSRecordTools.resolveWithCNAME(...)}).
*
* @param tln Top-level-name.
* @param name InfoName.
* @param sub Optional sub-name. May be {@code null}.
* @param type Desired record type.
*
* @return List of matching {@link INSRecord}s, or an empty list if none exist.
* This implementation:
* <ul>
* <li>Locates the corresponding InfoName in the SQL schema</li>
* <li>Returns all matching {@link INSRecord} entries</li>
* <li>Performs CNAME recursion (with a depth limit) when no direct
* records for the requested type exist</li>
* </ul>
*/
@Override
public List<INSRecord> resolve(String tln, String name, String sub, INSRecordType type) {
List<INSRecord> result = new ArrayList<>();
String sql =
"SELECT type, value, priority, weight, port, ttl " +
"FROM ins_records " +
"WHERE tln = ? AND name = ? AND type = ? " +
"AND ( (sub IS NULL AND ? IS NULL) OR sub = ? )";
try (Connection conn = openConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, tln);
ps.setString(2, name);
ps.setString(3, type.name());
ps.setString(4, sub);
ps.setString(5, sub);
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
INSRecordType dbType = INSRecordType.valueOf(rs.getString("type"));
String value = rs.getString("value");
int priority = rs.getInt("priority");
int weight = rs.getInt("weight");
int port = rs.getInt("port");
int ttl = rs.getInt("ttl");
result.add(new INSRecord(dbType, value, priority, weight, port, ttl));
}
}
try {
return resolveInternal(tln, name, sub, type, 0);
} catch (SQLException ex) {
getProtocolBridge().getLogger().exception("Failed to resolve INS record from database", ex);
getProtocolBridge().getLogger().exception("INS resolve failed for " + formatName(tln, name, sub) + " type=" + type, ex);
return new ArrayList<>();
}
return result;
}
/**
* Resolves the TLN info site,
* which is used when a client queries {@code info.<tln>} without any sub-name.
* <p>
* The result must be returned as {@code "host:port"}.
*
* @param tln The TLN the client is asking about.
*
* @return A {@code "host:port"} string or {@code null} if no TLN info site is configured.
* The value is read from {@code tln.info} and must be of the form
* {@code "host:port"}.
*/
@Override
public String resolveTLNInfoSite(String tln) {
String sql = "SELECT host, port FROM ins_tln_info WHERE tln = ?";
String sql = "SELECT info FROM tln WHERE name = ?";
try (Connection conn = openConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
@@ -154,11 +98,7 @@ public final class DatabaseINSServer extends ProtocolINSServer {
ps.setString(1, tln);
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
String host = rs.getString("host");
int port = rs.getInt("port");
return host + ":" + port;
}
if (rs.next()) return rs.getString("info"); // expected "host:port"
}
} catch (SQLException ex) {
getProtocolBridge().getLogger().exception("Failed to resolve TLN info site for tln=" + tln, ex);
@@ -166,4 +106,177 @@ public final class DatabaseINSServer extends ProtocolINSServer {
return null;
}
}
private String formatName(String tln, String name, String sub) {
if (sub == null || sub.isEmpty()) return name + "." + tln;
return sub + "." + name + "." + tln;
}
/**
* Recursive resolver with CNAME handling and depth limit.
*/
private List<INSRecord> resolveInternal(String tln, String name, String sub, INSRecordType requestedType, int depth) throws SQLException {
final int MAX_CNAME_DEPTH = 8;
if (depth > MAX_CNAME_DEPTH) {
getProtocolBridge().getLogger().warn("CNAME recursion limit exceeded for " + formatName(tln, name, sub) + " type=" + requestedType);
return new ArrayList<>();
}
try (Connection conn = openConnection()) {
// Get IDs
Integer tlnId = findTLNId(conn, tln);
if (tlnId == null) return new ArrayList<>();
Integer infoNameId = findInfoNameId(conn, tlnId, name);
if (infoNameId == null) return new ArrayList<>();
Integer subNameId = findSubNameId(conn, infoNameId, sub);
// Get records
List<INSRecord> direct = loadRecords(conn, infoNameId, subNameId, requestedType);
// No more recursion when CNAME received
if (requestedType == INSRecordType.CNAME) return direct;
if (!direct.isEmpty()) return direct;
// CNAME lookup
List<INSRecord> cnames = loadRecords(conn, infoNameId, subNameId, INSRecordType.CNAME);
if (cnames.isEmpty()) return direct; // empty
List<INSRecord> aggregated = new ArrayList<>();
for (INSRecord cname : cnames) {
TargetName target = parseCnameTarget(cname.value);
if (target == null) {
getProtocolBridge().getLogger().warn("Invalid CNAME target '" + cname.value + "' on " + formatName(tln, name, sub));
continue;
}
// Rekursiv den Zielnamen auflösen
aggregated.addAll(resolveInternal(target.tln, target.name, target.sub, requestedType, depth + 1));
}
return aggregated;
}
}
/**
* Loads all records of a given type for (infoname_id, subname_id).
*
* @param type May be {@code null} to load all types.
*/
private List<INSRecord> loadRecords(Connection conn, int infonameId, Integer subnameId, INSRecordType type) throws SQLException {
StringBuilder sql = new StringBuilder("SELECT type, value, ttl, priority, port, weight FROM records WHERE infoname_id = ? ");
if (subnameId == null) sql.append("AND subname_id IS NULL ");
else sql.append("AND subname_id = ? ");
if (type != null) sql.append("AND type = ? ");
try (PreparedStatement ps = conn.prepareStatement(sql.toString())) {
int idx = 1;
ps.setInt(idx++, infonameId);
if (subnameId != null) ps.setInt(idx++, subnameId);
if (type != null) ps.setString(idx, type.name());
List<INSRecord> result = new ArrayList<>();
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
INSRecordType rType = INSRecordType.valueOf(rs.getString("type"));
String value = rs.getString("value");
int ttl = rs.getInt("ttl");
int priority = rs.getInt("priority");
int port = rs.getInt("port");
int weight = rs.getInt("weight");
result.add(new INSRecord(rType, value, priority, weight, port, ttl));
}
}
return result;
}
}
private Integer findTLNId(Connection conn, String tln) throws SQLException {
String sql = "SELECT id FROM tln WHERE name = ?";
try (PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, tln);
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) return rs.getInt("id");
}
}
return null;
}
private Integer findInfoNameId(Connection conn, int tlnId, String infoName) throws SQLException {
String sql = "SELECT id FROM infonames WHERE tln_id = ? AND info = ?";
try (PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setInt(1, tlnId);
ps.setString(2, infoName);
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) return rs.getInt("id");
}
}
return null;
}
private Integer findSubNameId(Connection conn, int infoNameId, String sub) throws SQLException {
if (sub == null || sub.isEmpty()) return null;
String sql = "SELECT id FROM subnames WHERE infoname_id = ? AND name = ?";
try (PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setInt(1, infoNameId);
ps.setString(2, sub);
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) return rs.getInt("id");
}
}
return null;
}
/**
* Parses a CNAME target string into TLN, InfoName and optional subname.
* <p>
*
* @param value Raw CNAME value from the {@code records} table.
* @return Parsed {@link TargetName} or {@code null} if the value is invalid.
*/
private TargetName parseCnameTarget(String value) {
if (value == null) return null;
String trimmed = value.trim();
if (trimmed.isEmpty()) return null;
String[] parts = trimmed.split("\\.");
if (parts.length < 2) return null; // at least "<name>.<tln>"
String tln = parts[parts.length - 1];
String name = parts[parts.length - 2];
String sub = null;
if (parts.length > 2) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < parts.length - 2; i++) {
if (i > 0) sb.append('.');
sb.append(parts[i]);
}
sub = sb.toString();
}
return new TargetName(tln, name, sub);
}
}

View File

@@ -0,0 +1,16 @@
package org.openautonomousconnection.ins.utils;
/**
* Represents a parsed CNAME target in (tln, name, sub) form.
*/
public final class TargetName {
public final String tln;
public final String name;
public final String sub;
public TargetName(String tln, String name, String sub) {
this.tln = tln;
this.name = name;
this.sub = sub;
}
}