diff options
Diffstat (limited to 'database/dns.go')
| -rw-r--r-- | database/dns.go | 157 |
1 files changed, 157 insertions, 0 deletions
diff --git a/database/dns.go b/database/dns.go new file mode 100644 index 0000000..8372936 --- /dev/null +++ b/database/dns.go @@ -0,0 +1,157 @@ +package database + +import ( + "database/sql" + "strings" + "time" + + _ "github.com/mattn/go-sqlite3" +) + +// DNSRecord is a user-owned record in the LAN zone. Names are stored fully +// qualified with a trailing dot (e.g. "tux.penguins.lan.") to match the form +// the resolver queries with. +type DNSRecord struct { + ID string + UserID string + Name string + Type string + Content string + TTL int + CreatedAt time.Time +} + +const dnsColumns = "id, user_id, name, type, content, ttl, created_at" + +func scanDNSRecord(s interface{ Scan(...any) error }) (*DNSRecord, error) { + var r DNSRecord + if err := s.Scan(&r.ID, &r.UserID, &r.Name, &r.Type, &r.Content, &r.TTL, &r.CreatedAt); err != nil { + return nil, err + } + return &r, nil +} + +func CountUserDNSRecords(db *sql.DB, userID string) (int, error) { + var count int + err := db.QueryRow("SELECT COUNT(*) FROM dns_records WHERE user_id = ?", userID).Scan(&count) + return count, err +} + +func GetUserDNSRecords(db *sql.DB, userID string) ([]DNSRecord, error) { + rows, err := db.Query("SELECT "+dnsColumns+" FROM dns_records WHERE user_id = ? ORDER BY name, type", userID) + if err != nil { + return nil, err + } + defer rows.Close() + + records := []DNSRecord{} + for rows.Next() { + r, err := scanDNSRecord(rows) + if err != nil { + return nil, err + } + records = append(records, *r) + } + return records, rows.Err() +} + +// SaveDNSRecord upserts a record. The unique (name, type, content) index means +// re-saving the same record is a no-op rather than a duplicate. +func SaveDNSRecord(db *sql.DB, record *DNSRecord) (*DNSRecord, error) { + if record.CreatedAt.IsZero() { + record.CreatedAt = time.Now() + } + _, err := db.Exec( + "INSERT OR REPLACE INTO dns_records ("+dnsColumns+") VALUES (?, ?, ?, ?, ?, ?, ?)", + record.ID, record.UserID, record.Name, record.Type, record.Content, record.TTL, record.CreatedAt, + ) + if err != nil { + return nil, err + } + return record, nil +} + +func GetDNSRecord(db *sql.DB, recordID string) (*DNSRecord, error) { + r, err := scanDNSRecord(db.QueryRow("SELECT "+dnsColumns+" FROM dns_records WHERE id = ?", recordID)) + if err == sql.ErrNoRows { + return nil, ErrNotFound + } + return r, err +} + +func DeleteDNSRecord(db *sql.DB, recordID string) error { + _, err := db.Exec("DELETE FROM dns_records WHERE id = ?", recordID) + return err +} + +// DeleteUserRecordsByNameType removes a user's records at a name of a given +// type. Used to keep a single CNAME per name (so re-running the wizard or +// repointing the primary replaces rather than duplicates). +func DeleteUserRecordsByNameType(db *sql.DB, userID, name, qtype string) error { + _, err := db.Exec("DELETE FROM dns_records WHERE user_id = ? AND name = ? AND type = ?", userID, name, qtype) + return err +} + +// RenameUserDNSDomain moves a user's records from oldDomain to newDomain (both +// of the form "<username>.<zone>", no trailing dot) when their handle changes, +// rewriting the suffix in both record names and contents so their setup keeps +// working under the new name. Records that don't reference the domain are left +// alone. +func RenameUserDNSDomain(db *sql.DB, userID, oldDomain, newDomain string) error { + records, err := GetUserDNSRecords(db, userID) + if err != nil { + return err + } + for _, rec := range records { + newName := rewriteDomainSuffix(rec.Name, oldDomain, newDomain) + newContent := rewriteDomainSuffix(rec.Content, oldDomain, newDomain) + if newName == rec.Name && newContent == rec.Content { + continue + } + rec.Name, rec.Content = newName, newContent + if _, err := SaveDNSRecord(db, &rec); err != nil { + return err + } + } + return nil +} + +// rewriteDomainSuffix replaces a trailing oldDomain (the whole string or a +// dot-separated suffix) with newDomain, preserving any trailing dot. A value +// that merely contains oldDomain mid-string is left untouched. +func rewriteDomainSuffix(s, oldDomain, newDomain string) string { + hadDot := strings.HasSuffix(s, ".") + bare := strings.TrimSuffix(s, ".") + switch { + case bare == oldDomain: + bare = newDomain + case strings.HasSuffix(bare, "."+oldDomain): + bare = strings.TrimSuffix(bare, "."+oldDomain) + "." + newDomain + default: + return s + } + if hadDot { + return bare + "." + } + return bare +} + +// FindDNSRecords returns every record matching a fully-qualified name and type. +// This is the resolver's lookup path. +func FindDNSRecords(db *sql.DB, name, qtype string) ([]DNSRecord, error) { + rows, err := db.Query("SELECT "+dnsColumns+" FROM dns_records WHERE name = ? AND type = ?", name, qtype) + if err != nil { + return nil, err + } + defer rows.Close() + + records := []DNSRecord{} + for rows.Next() { + r, err := scanDNSRecord(rows) + if err != nil { + return nil, err + } + records = append(records, *r) + } + return records, rows.Err() +} |
