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  }