package org.openautonomousconnection.insserver; 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; import java.io.IOException; import java.security.cert.CertificateException; import java.sql.*; import java.util.*; /** * Database-backed INS server. *

* This implementation resolves records from the SQL schema and performs CNAME recursion (with loop detection + depth limit). * Returned records are deterministically sorted so that callers can safely select index 0 as the "best" record. */ public final class DatabaseINSServer extends ProtocolINSServer { private final String jdbcUrl; private final String jdbcUser; private final String jdbcPassword; /** * Creates a new database-backed INS server. * * @throws IOException If the base server initialization fails. * @throws CertificateException If required certificate files are missing or invalid. */ public DatabaseINSServer(String insInfoSite, String insFrontendSite, String jdbcUrl, String jdbcUser, String jdbcPassword) throws Exception { super(insInfoSite, insFrontendSite); this.jdbcUrl = jdbcUrl; this.jdbcUser = jdbcUser; this.jdbcPassword = jdbcPassword; } private static int safeInt(int v) { return v; } private static String safeString(String s) { return s == null ? "" : s; } private Connection openConnection() throws SQLException { return DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword); } /** * Resolves a request for an INS record based on TLN, name, subname and record type. *

* The implementation: *

* * @param tln The top-level name. * @param name The InfoName. * @param sub Optional subname, may be {@code null}. * @param type The requested record type. * @return Resolved records (possibly empty). */ @Override public List resolve(String tln, String name, String sub, INSRecordType type) { try { List out = resolveInternal(tln, name, sub, type, 0, new HashSet<>()); out.sort(recordComparator(type)); return out; } catch (SQLException ex) { getProtocolBridge().getLogger().exception( "INS resolve failed for " + formatName(tln, name, sub) + " type=" + type, ex ); return new ArrayList<>(); } } /** * Resolves the TLN info site which is used when a client queries {@code info.} without any sub-name. *

* The value is read from {@code tln.info} and must be of the form {@code "host:port"}. * * @param tln The TLN name. * @return The configured info target ("host:port") or {@code null} if not present. */ @Override public String resolveTLNInfoSite(String tln) { String sql = "SELECT info FROM tln WHERE name = ?"; try (Connection conn = openConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, tln); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) return rs.getString("info"); } } catch (SQLException ex) { getProtocolBridge().getLogger().exception("Failed to resolve TLN info site for tln=" + tln, ex); } return null; } private String formatName(String tln, String name, String sub) { if (sub == null || sub.isEmpty()) return name + "." + tln; return sub + "." + name + "." + tln; } /** * Internal recursive resolver with CNAME handling. * * @param depth Current recursion depth. * @param visited Loop detection set of canonical names (sub.name.tln). */ private List resolveInternal( String tln, String name, String sub, INSRecordType requestedType, int depth, Set visited ) throws SQLException { final int MAX_CNAME_DEPTH = 16; String canonical = formatName(tln, name, sub).toLowerCase(Locale.ROOT); if (!visited.add(canonical)) { // loop detected getProtocolBridge().getLogger().warn("CNAME loop detected for " + canonical + " type=" + requestedType); return new ArrayList<>(); } if (depth > MAX_CNAME_DEPTH) { getProtocolBridge().getLogger().warn("CNAME recursion limit exceeded for " + canonical + " type=" + requestedType); return new ArrayList<>(); } try (Connection conn = openConnection()) { 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); // 1) direct records List direct = loadRecords(conn, infoNameId, subNameId, requestedType); direct.sort(recordComparator(requestedType)); // If the requested type is CNAME, do not recurse. if (requestedType == INSRecordType.CNAME) return direct; if (!direct.isEmpty()) return direct; // 2) fallback to CNAME if no direct records exist List cnames = loadRecords(conn, infoNameId, subNameId, INSRecordType.CNAME); cnames.sort(recordComparator(INSRecordType.CNAME)); if (cnames.isEmpty()) return new ArrayList<>(); 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 " + canonical); continue; } // recurse on the target name to fetch the original requested type List resolvedTarget = resolveInternal( target.tln, target.name, target.sub, requestedType, depth + 1, visited ); aggregated.addAll(resolvedTarget); } aggregated.sort(recordComparator(requestedType)); return aggregated; } finally { // important: visited is shared across the recursion chain on purpose // (do not remove canonical here; loop detection should stay for the whole chain) } } /** * Deterministic ordering for returned records. The goal is stable ranking so callers can pick index 0. * *

Rules: *

    *
  • priority ASC (smaller is better)
  • *
  • weight DESC (larger is better)
  • *
  • port ASC (smaller is better)
  • *
  • ttl DESC (larger is better)
  • *
  • value ASC (case-insensitive) as final tie-breaker
  • *
* * @param requestedType The type being requested (kept for future type-specific tuning). * @return Comparator for {@link INSRecord}. */ private Comparator recordComparator(INSRecordType requestedType) { return Comparator .comparingInt((INSRecord r) -> safeInt(r.priority)) .thenComparingInt(r -> -safeInt(r.weight)) .thenComparingInt(r -> safeInt(r.port)) .thenComparingInt(r -> -safeInt(r.ttl)) .thenComparing(r -> safeString(r.value), String.CASE_INSENSITIVE_ORDER); } /** * 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 (e.g. "sub.app.example" or "example.net"). * @return Parsed {@link TargetName} or {@code null} if 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; 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); } private static final class TargetName { final String tln; final String name; final String sub; TargetName(String tln, String name, String sub) { this.tln = tln; this.name = name; this.sub = sub; } } }