1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
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()
}
|