github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/kbfs/libpages/dns.go (about) 1 // Copyright 2017 Keybase Inc. All rights reserved. 2 // Use of this source code is governed by a BSD 3 // license that can be found in the LICENSE file. 4 5 package libpages 6 7 import ( 8 "net" 9 "strings" 10 "sync" 11 "time" 12 13 "go.uber.org/zap" 14 "go.uber.org/zap/zapcore" 15 ) 16 17 // RootLoader is the interface for loading a site root. 18 type RootLoader interface { 19 LoadRoot(domain string) (root Root, err error) 20 } 21 22 // cachedRootValidDuration specifies the duration that a Root can be cached 23 // for. Ideally we'd cache it properly according to the TTL we get from the 24 // DNS. But unfortunately Go doesn't expose that through the `net` package. So 25 // just cache for a fixed duration of 10 seconds for now. 26 const cachedRootValidDuration = 10 * time.Second 27 28 type cachedRoot struct { 29 root *Root 30 expire time.Time 31 } 32 33 type dnsRootLoader struct { 34 log *zap.Logger 35 36 lock sync.RWMutex 37 rootCache map[string]cachedRoot 38 } 39 40 // NewDNSRootLoader makes a new RootLoader backed by DNS TXT record. It caches 41 // the root for a short period time. This is the RootLoader that should be 42 // used in all non-test scenarios. 43 // 44 // When loading from DNS, it does so with following steps: 45 // 1. Construct a domain name by prefixing the `domain` parameter with 46 // "_keybase_pages." or "_keybasepages". So for example, 47 // "static.keybase.io" turns into "_keybase_pages.static.keybase.io" or 48 // "_keybasepages.static.keybase.io". 49 // 2. Load TXT record(s) from the domain constructed in step 1, and look for 50 // one starting with "kbp=". If exactly one exists, parse it into a `Root` 51 // and return it. 52 // 53 // There must be exactly one "kbp=" TXT record configured for domain. If more 54 // than one exists, an ErrKeybasePagesRecordTooMany{} is returned. If none is 55 // found, an ErrKeybasePagesRecordNotFound{} is returned. In case the user has 56 // some configuration that requires other records that we can't foresee for 57 // now, other records (TXT or not) can co-exist with the "kbp=" record (as long 58 // as no CNAME record exists on the "_keybase_pages." or "_keybasepages." 59 // prefixed domain of course). 60 // 61 // If the given domain is invalid, the domain name constructed in this step 62 // will be invalid too, which causes Go's DNS resolver to return a net.DNSError 63 // typed "no such host" error. 64 // 65 // Examples for "static.keybase.io", "meatball.gao.io", "song.gao.io", 66 // "blah.strib.io", and "kbp.jzila.com" respectively: 67 // 68 // _keybase_pages.static.keybase.io TXT "kbp=/keybase/team/keybase.bots/static.keybase.io" 69 // _keybase_pages.meatball.gao.io TXT "kbp=/keybase/public/songgao/meatball/" 70 // _keybase_pages.song.gao.io TXT "kbp=/keybase/private/songgao,kb_bot/blah" 71 // _keybase_pages.blah.strib.io TXT "kbp=/keybase/private/strib#kb_bot/blahblahb" "lah/blah/" 72 // _keybase_pages.kbp.jzila.com TXT "kbp=git@keybase:private/jzila,kb_bot/kbp.git" 73 func NewDNSRootLoader(log *zap.Logger) RootLoader { 74 return &dnsRootLoader{ 75 log: log, 76 rootCache: make(map[string]cachedRoot), 77 } 78 } 79 80 const ( 81 keybasePagesPrefix = "kbp=" 82 ) 83 84 // ErrKeybasePagesRecordNotFound is returned when a domain requested doesn't 85 // have a kbp= record configured. 86 type ErrKeybasePagesRecordNotFound struct{} 87 88 // Error implements the error interface. 89 func (ErrKeybasePagesRecordNotFound) Error() string { 90 return "no TXT record is found for " + keybasePagesPrefix 91 } 92 93 // ErrKeybasePagesRecordTooMany is returned when a domain requested has more 94 // than one kbp= record configured. 95 type ErrKeybasePagesRecordTooMany struct{} 96 97 // Error implements the error interface. 98 func (ErrKeybasePagesRecordTooMany) Error() string { 99 return "more than 1 TXT record are found for " + keybasePagesPrefix 100 } 101 102 // kbpRecordPrefixes specifies the TXT record prefixes that we look at to 103 // locate the root of these Keybase pages. We have 2 records since some 104 // registrars don't support underscores in the middle of a domain. This order 105 // must remain fixed since it reflects the order the strings are evaluated in. 106 var kbpRecordPrefixes = []string{"_keybase_pages.", "_keybasepages."} 107 108 func (l *dnsRootLoader) loadRoot(domain string) (root *Root, err error) { 109 var rootPath string 110 111 defer func() { 112 zapFields := []zapcore.Field{ 113 zap.String("domain", domain), 114 zap.String("kbp_record", rootPath), 115 } 116 if err == nil { 117 l.log.Info("LoadRootFromDNS", zapFields...) 118 } else { 119 l.log.Warn("LoadRootFromDNS", append(zapFields, zap.Error(err))...) 120 } 121 }() 122 123 // Check all possible kbp record prefixes. 124 var txtRecords []string 125 for _, kbpRecordPrefix := range kbpRecordPrefixes { 126 txtRecords, err = net.LookupTXT(kbpRecordPrefix + domain) 127 if err == nil { 128 break 129 } 130 } 131 if err != nil { 132 return nil, err 133 } 134 135 for _, r := range txtRecords { 136 r = strings.TrimSpace(r) 137 138 if strings.HasPrefix(r, keybasePagesPrefix) { 139 if len(rootPath) != 0 { 140 return nil, ErrKeybasePagesRecordTooMany{} 141 } 142 rootPath = r[len(keybasePagesPrefix):] 143 } 144 } 145 146 if len(rootPath) == 0 { 147 return nil, ErrKeybasePagesRecordNotFound{} 148 } 149 150 return ParseRoot(rootPath) 151 } 152 153 // LoadRoot implements the RootLoader interface. 154 func (l *dnsRootLoader) LoadRoot(domain string) (Root, error) { 155 l.lock.RLock() 156 cached, ok := l.rootCache[domain] 157 l.lock.RUnlock() 158 159 if ok && time.Now().Before(cached.expire) { 160 return *cached.root, nil 161 } 162 163 root, err := l.loadRoot(domain) 164 if err != nil { 165 return Root{}, err 166 } 167 168 l.lock.Lock() 169 defer l.lock.Unlock() 170 l.rootCache[domain] = cachedRoot{ 171 root: root, 172 expire: time.Now().Add(cachedRootValidDuration), 173 } 174 175 return *root, nil 176 }