nss.mx raw

   1  // Copyright 2015 The Go Authors. All rights reserved.
   2  // Use of this source code is governed by a BSD-style
   3  // license that can be found in the LICENSE file.
   4  
   5  package net
   6  
   7  import (
   8  	"errors"
   9  	"internal/bytealg"
  10  	"os"
  11  	"sync"
  12  	"time"
  13  )
  14  
  15  const (
  16  	nssConfigPath = "/etc/nsswitch.conf"
  17  )
  18  
  19  var nssConfig nsswitchConfig
  20  
  21  type nsswitchConfig struct {
  22  	initOnce sync.Once // guards init of nsswitchConfig
  23  
  24  	// ch is used as a semaphore that only allows one lookup at a
  25  	// time to recheck nsswitch.conf
  26  	ch          chan struct{} // guards lastChecked and modTime
  27  	lastChecked time.Time     // last time nsswitch.conf was checked
  28  
  29  	mu      sync.Mutex // protects nssConf
  30  	nssConf *nssConf
  31  }
  32  
  33  func getSystemNSS() *nssConf {
  34  	nssConfig.tryUpdate()
  35  	nssConfig.mu.Lock()
  36  	conf := nssConfig.nssConf
  37  	nssConfig.mu.Unlock()
  38  	return conf
  39  }
  40  
  41  // init initializes conf and is only called via conf.initOnce.
  42  func (conf *nsswitchConfig) init() {
  43  	conf.nssConf = parseNSSConfFile("/etc/nsswitch.conf")
  44  	conf.lastChecked = time.Now()
  45  	conf.ch = chan struct{}{1}
  46  }
  47  
  48  // tryUpdate tries to update conf.
  49  func (conf *nsswitchConfig) tryUpdate() {
  50  	conf.initOnce.Do(conf.init)
  51  
  52  	// Ensure only one update at a time checks nsswitch.conf
  53  	if !conf.tryAcquireSema() {
  54  		return
  55  	}
  56  	defer conf.releaseSema()
  57  
  58  	now := time.Now()
  59  	if conf.lastChecked.After(now.Add(-5 * time.Second)) {
  60  		return
  61  	}
  62  	conf.lastChecked = now
  63  
  64  	var mtime time.Time
  65  	if fi, err := os.Stat(nssConfigPath); err == nil {
  66  		mtime = fi.ModTime()
  67  	}
  68  	if mtime.Equal(conf.nssConf.mtime) {
  69  		return
  70  	}
  71  
  72  	nssConf := parseNSSConfFile(nssConfigPath)
  73  	conf.mu.Lock()
  74  	conf.nssConf = nssConf
  75  	conf.mu.Unlock()
  76  }
  77  
  78  func (conf *nsswitchConfig) acquireSema() {
  79  	conf.ch <- struct{}{}
  80  }
  81  
  82  func (conf *nsswitchConfig) tryAcquireSema() bool {
  83  	select {
  84  	case conf.ch <- struct{}{}:
  85  		return true
  86  	default:
  87  		return false
  88  	}
  89  }
  90  
  91  func (conf *nsswitchConfig) releaseSema() {
  92  	<-conf.ch
  93  }
  94  
  95  // nssConf represents the state of the machine's /etc/nsswitch.conf file.
  96  type nssConf struct {
  97  	mtime   time.Time              // time of nsswitch.conf modification
  98  	err     error                  // any error encountered opening or parsing the file
  99  	sources map[string][]nssSource // keyed by database (e.g. "hosts")
 100  }
 101  
 102  type nssSource struct {
 103  	source   string // e.g. "compat", "files", "mdns4_minimal"
 104  	criteria []nssCriterion
 105  }
 106  
 107  // standardCriteria reports all specified criteria have the default
 108  // status actions.
 109  func (s nssSource) standardCriteria() bool {
 110  	for i, crit := range s.criteria {
 111  		if !crit.standardStatusAction(i == len(s.criteria)-1) {
 112  			return false
 113  		}
 114  	}
 115  	return true
 116  }
 117  
 118  // nssCriterion is the parsed structure of one of the criteria in brackets
 119  // after an NSS source name.
 120  type nssCriterion struct {
 121  	negate bool   // if "!" was present
 122  	status string // e.g. "success", "unavail" (lowercase)
 123  	action string // e.g. "return", "continue" (lowercase)
 124  }
 125  
 126  // standardStatusAction reports whether c is equivalent to not
 127  // specifying the criterion at all. last is whether this criteria is the
 128  // last in the list.
 129  func (c nssCriterion) standardStatusAction(last bool) bool {
 130  	if c.negate {
 131  		return false
 132  	}
 133  	var def string
 134  	switch c.status {
 135  	case "success":
 136  		def = "return"
 137  	case "notfound", "unavail", "tryagain":
 138  		def = "continue"
 139  	default:
 140  		// Unknown status
 141  		return false
 142  	}
 143  	if last && c.action == "return" {
 144  		return true
 145  	}
 146  	return c.action == def
 147  }
 148  
 149  func parseNSSConfFile(file string) *nssConf {
 150  	f, err := open(file)
 151  	if err != nil {
 152  		return &nssConf{err: err}
 153  	}
 154  	defer f.close()
 155  	mtime, _, err := f.stat()
 156  	if err != nil {
 157  		return &nssConf{err: err}
 158  	}
 159  
 160  	conf := parseNSSConf(f)
 161  	conf.mtime = mtime
 162  	return conf
 163  }
 164  
 165  func parseNSSConf(f *file) *nssConf {
 166  	conf := &nssConf{}
 167  	for line, ok := f.readLine(); ok; line, ok = f.readLine() {
 168  		line = trimSpace(removeComment(line))
 169  		if len(line) == 0 {
 170  			continue
 171  		}
 172  		colon := bytealg.IndexByteString(line, ':')
 173  		if colon == -1 {
 174  			conf.err = errors.New("no colon on line")
 175  			return conf
 176  		}
 177  		db := trimSpace(line[:colon])
 178  		srcs := line[colon+1:]
 179  		for {
 180  			srcs = trimSpace(srcs)
 181  			if len(srcs) == 0 {
 182  				break
 183  			}
 184  			sp := bytealg.IndexByteString(srcs, ' ')
 185  			var src string
 186  			if sp == -1 {
 187  				src = srcs
 188  				srcs = "" // done
 189  			} else {
 190  				src = srcs[:sp]
 191  				srcs = trimSpace(srcs[sp+1:])
 192  			}
 193  			var criteria []nssCriterion
 194  			// See if there's a criteria block in brackets.
 195  			if len(srcs) > 0 && srcs[0] == '[' {
 196  				bclose := bytealg.IndexByteString(srcs, ']')
 197  				if bclose == -1 {
 198  					conf.err = errors.New("unclosed criterion bracket")
 199  					return conf
 200  				}
 201  				var err error
 202  				criteria, err = parseCriteria(srcs[1:bclose])
 203  				if err != nil {
 204  					conf.err = errors.New("invalid criteria: " + srcs[1:bclose])
 205  					return conf
 206  				}
 207  				srcs = srcs[bclose+1:]
 208  			}
 209  			if conf.sources == nil {
 210  				conf.sources = map[string][]nssSource{}
 211  			}
 212  			conf.sources[db] = append(conf.sources[db], nssSource{
 213  				source:   src,
 214  				criteria: criteria,
 215  			})
 216  		}
 217  	}
 218  	return conf
 219  }
 220  
 221  // parses "foo=bar !foo=bar"
 222  func parseCriteria(x string) (c []nssCriterion, err error) {
 223  	err = foreachField(x, func(f string) error {
 224  		not := false
 225  		if len(f) > 0 && f[0] == '!' {
 226  			not = true
 227  			f = f[1:]
 228  		}
 229  		if len(f) < 3 {
 230  			return errors.New("criterion too short")
 231  		}
 232  		eq := bytealg.IndexByteString(f, '=')
 233  		if eq == -1 {
 234  			return errors.New("criterion lacks equal sign")
 235  		}
 236  		if hasUpperCase(f) {
 237  			lower := []byte(f)
 238  			lowerASCIIBytes(lower)
 239  			f = string(lower)
 240  		}
 241  		c = append(c, nssCriterion{
 242  			negate: not,
 243  			status: f[:eq],
 244  			action: f[eq+1:],
 245  		})
 246  		return nil
 247  	})
 248  	return
 249  }
 250