2025-12-13 15:53:57 +01:00
|
|
|
package org.openautonomousconnection.insserver;
|
2025-12-11 12:01:09 +01:00
|
|
|
|
|
|
|
|
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.*;
|
2026-01-18 18:34:29 +01:00
|
|
|
import java.util.*;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Database-backed INS server.
|
|
|
|
|
* <p>
|
|
|
|
|
* 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.
|
|
|
|
|
*/
|
2025-12-11 12:01:09 +01:00
|
|
|
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.
|
|
|
|
|
*/
|
2026-02-01 19:06:45 +01:00
|
|
|
public DatabaseINSServer(String insInfoSite, String insFrontendSite, String jdbcUrl, String jdbcUser, String jdbcPassword) throws Exception {
|
|
|
|
|
super(insInfoSite, insFrontendSite);
|
2025-12-11 12:01:09 +01:00
|
|
|
|
2026-01-18 22:01:14 +01:00
|
|
|
this.jdbcUrl = jdbcUrl;
|
|
|
|
|
this.jdbcUser = jdbcUser;
|
|
|
|
|
this.jdbcPassword = jdbcPassword;
|
2025-12-11 12:01:09 +01:00
|
|
|
}
|
|
|
|
|
|
2026-02-11 23:22:20 +01:00
|
|
|
private static int safeInt(int v) {
|
|
|
|
|
return v;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static String safeString(String s) {
|
|
|
|
|
return s == null ? "" : s;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-11 12:01:09 +01:00
|
|
|
private Connection openConnection() throws SQLException {
|
|
|
|
|
return DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2025-12-11 12:36:33 +01:00
|
|
|
* Resolves a request for an INS record based on TLN, name, subname and record type.
|
2025-12-11 12:01:09 +01:00
|
|
|
* <p>
|
2026-01-18 18:34:29 +01:00
|
|
|
* The implementation:
|
2025-12-11 12:36:33 +01:00
|
|
|
* <ul>
|
|
|
|
|
* <li>Locates the corresponding InfoName in the SQL schema</li>
|
|
|
|
|
* <li>Returns all matching {@link INSRecord} entries</li>
|
2026-01-18 18:34:29 +01:00
|
|
|
* <li>Performs CNAME recursion (with loop detection + depth limit) when no direct records for the requested type exist</li>
|
|
|
|
|
* <li>Returns deterministically sorted results (so index 0 can be used as "best record")</li>
|
2025-12-11 12:36:33 +01:00
|
|
|
* </ul>
|
2026-01-18 18:34:29 +01:00
|
|
|
*
|
|
|
|
|
* @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).
|
2025-12-11 12:01:09 +01:00
|
|
|
*/
|
|
|
|
|
@Override
|
|
|
|
|
public List<INSRecord> resolve(String tln, String name, String sub, INSRecordType type) {
|
2025-12-11 12:36:33 +01:00
|
|
|
try {
|
2026-01-18 18:34:29 +01:00
|
|
|
List<INSRecord> out = resolveInternal(tln, name, sub, type, 0, new HashSet<>());
|
|
|
|
|
out.sort(recordComparator(type));
|
|
|
|
|
return out;
|
2025-12-11 12:36:33 +01:00
|
|
|
} catch (SQLException ex) {
|
2026-01-18 18:34:29 +01:00
|
|
|
getProtocolBridge().getLogger().exception(
|
|
|
|
|
"INS resolve failed for " + formatName(tln, name, sub) + " type=" + type,
|
|
|
|
|
ex
|
|
|
|
|
);
|
2025-12-11 12:36:33 +01:00
|
|
|
return new ArrayList<>();
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-11 12:01:09 +01:00
|
|
|
|
2025-12-11 12:36:33 +01:00
|
|
|
/**
|
2026-01-18 18:34:29 +01:00
|
|
|
* Resolves the TLN info site which is used when a client queries {@code info.<tln>} without any sub-name.
|
2025-12-11 12:36:33 +01:00
|
|
|
* <p>
|
2026-01-18 18:34:29 +01:00
|
|
|
* 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.
|
2025-12-11 12:36:33 +01:00
|
|
|
*/
|
|
|
|
|
@Override
|
|
|
|
|
public String resolveTLNInfoSite(String tln) {
|
|
|
|
|
String sql = "SELECT info FROM tln WHERE name = ?";
|
2025-12-11 12:01:09 +01:00
|
|
|
|
|
|
|
|
try (Connection conn = openConnection();
|
|
|
|
|
PreparedStatement ps = conn.prepareStatement(sql)) {
|
|
|
|
|
|
|
|
|
|
ps.setString(1, tln);
|
2025-12-11 12:36:33 +01:00
|
|
|
|
|
|
|
|
try (ResultSet rs = ps.executeQuery()) {
|
2026-01-18 18:34:29 +01:00
|
|
|
if (rs.next()) return rs.getString("info");
|
2025-12-11 12:36:33 +01:00
|
|
|
}
|
|
|
|
|
} 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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-01-18 18:34:29 +01:00
|
|
|
* Internal recursive resolver with CNAME handling.
|
|
|
|
|
*
|
|
|
|
|
* @param depth Current recursion depth.
|
|
|
|
|
* @param visited Loop detection set of canonical names (sub.name.tln).
|
2025-12-11 12:36:33 +01:00
|
|
|
*/
|
2026-01-18 18:34:29 +01:00
|
|
|
private List<INSRecord> resolveInternal(
|
|
|
|
|
String tln,
|
|
|
|
|
String name,
|
|
|
|
|
String sub,
|
|
|
|
|
INSRecordType requestedType,
|
|
|
|
|
int depth,
|
|
|
|
|
Set<String> 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<>();
|
|
|
|
|
}
|
2025-12-11 12:36:33 +01:00
|
|
|
|
|
|
|
|
if (depth > MAX_CNAME_DEPTH) {
|
2026-01-18 18:34:29 +01:00
|
|
|
getProtocolBridge().getLogger().warn("CNAME recursion limit exceeded for " + canonical + " type=" + requestedType);
|
2025-12-11 12:36:33 +01:00
|
|
|
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);
|
|
|
|
|
|
2026-01-18 18:34:29 +01:00
|
|
|
// 1) direct records
|
2025-12-11 12:36:33 +01:00
|
|
|
List<INSRecord> direct = loadRecords(conn, infoNameId, subNameId, requestedType);
|
2026-01-18 18:34:29 +01:00
|
|
|
direct.sort(recordComparator(requestedType));
|
2025-12-11 12:36:33 +01:00
|
|
|
|
2026-01-18 18:34:29 +01:00
|
|
|
// If the requested type is CNAME, do not recurse.
|
2025-12-11 12:36:33 +01:00
|
|
|
if (requestedType == INSRecordType.CNAME) return direct;
|
|
|
|
|
|
|
|
|
|
if (!direct.isEmpty()) return direct;
|
|
|
|
|
|
2026-01-18 18:34:29 +01:00
|
|
|
// 2) fallback to CNAME if no direct records exist
|
2025-12-11 12:36:33 +01:00
|
|
|
List<INSRecord> cnames = loadRecords(conn, infoNameId, subNameId, INSRecordType.CNAME);
|
2026-01-18 18:34:29 +01:00
|
|
|
cnames.sort(recordComparator(INSRecordType.CNAME));
|
|
|
|
|
|
|
|
|
|
if (cnames.isEmpty()) return new ArrayList<>();
|
2025-12-11 12:36:33 +01:00
|
|
|
|
|
|
|
|
List<INSRecord> aggregated = new ArrayList<>();
|
2026-01-18 18:34:29 +01:00
|
|
|
|
2025-12-11 12:36:33 +01:00
|
|
|
for (INSRecord cname : cnames) {
|
|
|
|
|
TargetName target = parseCnameTarget(cname.value);
|
|
|
|
|
if (target == null) {
|
2026-01-18 18:34:29 +01:00
|
|
|
getProtocolBridge().getLogger().warn("Invalid CNAME target '" + cname.value + "' on " + canonical);
|
2025-12-11 12:36:33 +01:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-18 18:34:29 +01:00
|
|
|
// recurse on the target name to fetch the original requested type
|
|
|
|
|
List<INSRecord> resolvedTarget = resolveInternal(
|
2026-01-18 22:01:14 +01:00
|
|
|
target.tln,
|
|
|
|
|
target.name,
|
|
|
|
|
target.sub,
|
2026-01-18 18:34:29 +01:00
|
|
|
requestedType,
|
|
|
|
|
depth + 1,
|
|
|
|
|
visited
|
|
|
|
|
);
|
|
|
|
|
aggregated.addAll(resolvedTarget);
|
2025-12-11 12:36:33 +01:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 18:34:29 +01:00
|
|
|
aggregated.sort(recordComparator(requestedType));
|
2025-12-11 12:36:33 +01:00
|
|
|
return aggregated;
|
2026-01-18 18:34:29 +01:00
|
|
|
} finally {
|
|
|
|
|
// important: visited is shared across the recursion chain on purpose
|
|
|
|
|
// (do not remove canonical here; loop detection should stay for the whole chain)
|
2025-12-11 12:36:33 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-18 18:34:29 +01:00
|
|
|
/**
|
|
|
|
|
* Deterministic ordering for returned records. The goal is stable ranking so callers can pick index 0.
|
|
|
|
|
*
|
|
|
|
|
* <p>Rules:
|
|
|
|
|
* <ul>
|
|
|
|
|
* <li>priority ASC (smaller is better)</li>
|
|
|
|
|
* <li>weight DESC (larger is better)</li>
|
|
|
|
|
* <li>port ASC (smaller is better)</li>
|
|
|
|
|
* <li>ttl DESC (larger is better)</li>
|
|
|
|
|
* <li>value ASC (case-insensitive) as final tie-breaker</li>
|
|
|
|
|
* </ul>
|
|
|
|
|
*
|
|
|
|
|
* @param requestedType The type being requested (kept for future type-specific tuning).
|
|
|
|
|
* @return Comparator for {@link INSRecord}.
|
|
|
|
|
*/
|
|
|
|
|
private Comparator<INSRecord> 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);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-11 12:36:33 +01:00
|
|
|
/**
|
|
|
|
|
* 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 {
|
2026-01-18 18:34:29 +01:00
|
|
|
StringBuilder sql = new StringBuilder(
|
|
|
|
|
"SELECT type, value, ttl, priority, port, weight " +
|
|
|
|
|
"FROM records " +
|
|
|
|
|
"WHERE infoname_id = ? "
|
|
|
|
|
);
|
2025-12-11 12:36:33 +01:00
|
|
|
|
|
|
|
|
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<>();
|
2025-12-11 12:01:09 +01:00
|
|
|
|
|
|
|
|
try (ResultSet rs = ps.executeQuery()) {
|
|
|
|
|
while (rs.next()) {
|
2025-12-11 12:36:33 +01:00
|
|
|
INSRecordType rType = INSRecordType.valueOf(rs.getString("type"));
|
2025-12-11 12:01:09 +01:00
|
|
|
String value = rs.getString("value");
|
2025-12-11 12:36:33 +01:00
|
|
|
int ttl = rs.getInt("ttl");
|
2025-12-11 12:01:09 +01:00
|
|
|
int priority = rs.getInt("priority");
|
|
|
|
|
int port = rs.getInt("port");
|
2025-12-11 12:36:33 +01:00
|
|
|
int weight = rs.getInt("weight");
|
2025-12-11 12:01:09 +01:00
|
|
|
|
2025-12-11 12:36:33 +01:00
|
|
|
result.add(new INSRecord(rType, value, priority, weight, port, ttl));
|
2025-12-11 12:01:09 +01:00
|
|
|
}
|
|
|
|
|
}
|
2025-12-11 12:36:33 +01:00
|
|
|
|
|
|
|
|
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");
|
|
|
|
|
}
|
2025-12-11 12:01:09 +01:00
|
|
|
}
|
2025-12-11 12:36:33 +01:00
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Integer findInfoNameId(Connection conn, int tlnId, String infoName) throws SQLException {
|
|
|
|
|
String sql = "SELECT id FROM infonames WHERE tln_id = ? AND info = ?";
|
2025-12-11 12:01:09 +01:00
|
|
|
|
2025-12-11 12:36:33 +01:00
|
|
|
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;
|
2025-12-11 12:01:09 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2025-12-11 12:36:33 +01:00
|
|
|
* Parses a CNAME target string into TLN, InfoName and optional subname.
|
2025-12-11 12:01:09 +01:00
|
|
|
*
|
2026-01-18 18:34:29 +01:00
|
|
|
* @param value Raw CNAME value (e.g. "sub.app.example" or "example.net").
|
|
|
|
|
* @return Parsed {@link TargetName} or {@code null} if invalid.
|
2025-12-11 12:01:09 +01:00
|
|
|
*/
|
2025-12-11 12:36:33 +01:00
|
|
|
private TargetName parseCnameTarget(String value) {
|
|
|
|
|
if (value == null) return null;
|
|
|
|
|
String trimmed = value.trim();
|
|
|
|
|
if (trimmed.isEmpty()) return null;
|
2025-12-11 12:01:09 +01:00
|
|
|
|
2025-12-11 12:36:33 +01:00
|
|
|
String[] parts = trimmed.split("\\.");
|
2026-01-18 18:34:29 +01:00
|
|
|
if (parts.length < 2) return null;
|
2025-12-11 12:01:09 +01:00
|
|
|
|
2025-12-11 12:36:33 +01:00
|
|
|
String tln = parts[parts.length - 1];
|
|
|
|
|
String name = parts[parts.length - 2];
|
|
|
|
|
String sub = null;
|
2025-12-11 12:01:09 +01:00
|
|
|
|
2025-12-11 12:36:33 +01:00
|
|
|
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]);
|
2025-12-11 12:01:09 +01:00
|
|
|
}
|
2025-12-11 12:36:33 +01:00
|
|
|
sub = sb.toString();
|
2025-12-11 12:01:09 +01:00
|
|
|
}
|
|
|
|
|
|
2025-12-11 12:36:33 +01:00
|
|
|
return new TargetName(tln, name, sub);
|
2025-12-11 12:01:09 +01:00
|
|
|
}
|
2026-01-18 22:01:14 +01:00
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-18 18:34:29 +01:00
|
|
|
}
|