diff --git a/src/main/java/org/openautonomousconnection/ins/DatabaseINSServer.java b/src/main/java/org/openautonomousconnection/ins/DatabaseINSServer.java index 652bd75..3b79587 100644 --- a/src/main/java/org/openautonomousconnection/ins/DatabaseINSServer.java +++ b/src/main/java/org/openautonomousconnection/ins/DatabaseINSServer.java @@ -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. - *

- * This implementation expects a table schema similar to: - *

- * 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
- * );
- * 
- */ 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. *

- * 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: + *

*/ @Override public List resolve(String tln, String name, String sub, INSRecordType type) { - List 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.} without any sub-name. *

- * 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 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 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 cnames = loadRecords(conn, infoNameId, subNameId, INSRecordType.CNAME); + if (cnames.isEmpty()) return direct; // empty + + List 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 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 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. + *

+ * + * @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 "." + + 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); + } +} \ No newline at end of file diff --git a/src/main/java/org/openautonomousconnection/ins/utils/TargetName.java b/src/main/java/org/openautonomousconnection/ins/utils/TargetName.java new file mode 100644 index 0000000..ceb6fa1 --- /dev/null +++ b/src/main/java/org/openautonomousconnection/ins/utils/TargetName.java @@ -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; + } +}