Home | History | Annotate | Download | only in user
      1 // Copyright 2016 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 // +build darwin dragonfly freebsd !android,linux nacl netbsd openbsd solaris
      6 // +build !cgo
      7 
      8 package user
      9 
     10 import (
     11 	"bufio"
     12 	"bytes"
     13 	"errors"
     14 	"io"
     15 	"os"
     16 	"strconv"
     17 	"strings"
     18 )
     19 
     20 const groupFile = "/etc/group"
     21 const userFile = "/etc/passwd"
     22 
     23 var colon = []byte{':'}
     24 
     25 func init() {
     26 	groupImplemented = false
     27 }
     28 
     29 // lineFunc returns a value, an error, or (nil, nil) to skip the row.
     30 type lineFunc func(line []byte) (v interface{}, err error)
     31 
     32 // readColonFile parses r as an /etc/group or /etc/passwd style file, running
     33 // fn for each row. readColonFile returns a value, an error, or (nil, nil) if
     34 // the end of the file is reached without a match.
     35 func readColonFile(r io.Reader, fn lineFunc) (v interface{}, err error) {
     36 	bs := bufio.NewScanner(r)
     37 	for bs.Scan() {
     38 		line := bs.Bytes()
     39 		// There's no spec for /etc/passwd or /etc/group, but we try to follow
     40 		// the same rules as the glibc parser, which allows comments and blank
     41 		// space at the beginning of a line.
     42 		line = bytes.TrimSpace(line)
     43 		if len(line) == 0 || line[0] == '#' {
     44 			continue
     45 		}
     46 		v, err = fn(line)
     47 		if v != nil || err != nil {
     48 			return
     49 		}
     50 	}
     51 	return nil, bs.Err()
     52 }
     53 
     54 func matchGroupIndexValue(value string, idx int) lineFunc {
     55 	var leadColon string
     56 	if idx > 0 {
     57 		leadColon = ":"
     58 	}
     59 	substr := []byte(leadColon + value + ":")
     60 	return func(line []byte) (v interface{}, err error) {
     61 		if !bytes.Contains(line, substr) || bytes.Count(line, colon) < 3 {
     62 			return
     63 		}
     64 		// wheel:*:0:root
     65 		parts := strings.SplitN(string(line), ":", 4)
     66 		if len(parts) < 4 || parts[0] == "" || parts[idx] != value ||
     67 			// If the file contains +foo and you search for "foo", glibc
     68 			// returns an "invalid argument" error. Similarly, if you search
     69 			// for a gid for a row where the group name starts with "+" or "-",
     70 			// glibc fails to find the record.
     71 			parts[0][0] == '+' || parts[0][0] == '-' {
     72 			return
     73 		}
     74 		if _, err := strconv.Atoi(parts[2]); err != nil {
     75 			return nil, nil
     76 		}
     77 		return &Group{Name: parts[0], Gid: parts[2]}, nil
     78 	}
     79 }
     80 
     81 func findGroupId(id string, r io.Reader) (*Group, error) {
     82 	if v, err := readColonFile(r, matchGroupIndexValue(id, 2)); err != nil {
     83 		return nil, err
     84 	} else if v != nil {
     85 		return v.(*Group), nil
     86 	}
     87 	return nil, UnknownGroupIdError(id)
     88 }
     89 
     90 func findGroupName(name string, r io.Reader) (*Group, error) {
     91 	if v, err := readColonFile(r, matchGroupIndexValue(name, 0)); err != nil {
     92 		return nil, err
     93 	} else if v != nil {
     94 		return v.(*Group), nil
     95 	}
     96 	return nil, UnknownGroupError(name)
     97 }
     98 
     99 // returns a *User for a row if that row's has the given value at the
    100 // given index.
    101 func matchUserIndexValue(value string, idx int) lineFunc {
    102 	var leadColon string
    103 	if idx > 0 {
    104 		leadColon = ":"
    105 	}
    106 	substr := []byte(leadColon + value + ":")
    107 	return func(line []byte) (v interface{}, err error) {
    108 		if !bytes.Contains(line, substr) || bytes.Count(line, colon) < 6 {
    109 			return
    110 		}
    111 		// kevin:x:1005:1006::/home/kevin:/usr/bin/zsh
    112 		parts := strings.SplitN(string(line), ":", 7)
    113 		if len(parts) < 6 || parts[idx] != value || parts[0] == "" ||
    114 			parts[0][0] == '+' || parts[0][0] == '-' {
    115 			return
    116 		}
    117 		if _, err := strconv.Atoi(parts[2]); err != nil {
    118 			return nil, nil
    119 		}
    120 		if _, err := strconv.Atoi(parts[3]); err != nil {
    121 			return nil, nil
    122 		}
    123 		u := &User{
    124 			Username: parts[0],
    125 			Uid:      parts[2],
    126 			Gid:      parts[3],
    127 			Name:     parts[4],
    128 			HomeDir:  parts[5],
    129 		}
    130 		// The pw_gecos field isn't quite standardized. Some docs
    131 		// say: "It is expected to be a comma separated list of
    132 		// personal data where the first item is the full name of the
    133 		// user."
    134 		if i := strings.Index(u.Name, ","); i >= 0 {
    135 			u.Name = u.Name[:i]
    136 		}
    137 		return u, nil
    138 	}
    139 }
    140 
    141 func findUserId(uid string, r io.Reader) (*User, error) {
    142 	i, e := strconv.Atoi(uid)
    143 	if e != nil {
    144 		return nil, errors.New("user: invalid userid " + uid)
    145 	}
    146 	if v, err := readColonFile(r, matchUserIndexValue(uid, 2)); err != nil {
    147 		return nil, err
    148 	} else if v != nil {
    149 		return v.(*User), nil
    150 	}
    151 	return nil, UnknownUserIdError(i)
    152 }
    153 
    154 func findUsername(name string, r io.Reader) (*User, error) {
    155 	if v, err := readColonFile(r, matchUserIndexValue(name, 0)); err != nil {
    156 		return nil, err
    157 	} else if v != nil {
    158 		return v.(*User), nil
    159 	}
    160 	return nil, UnknownUserError(name)
    161 }
    162 
    163 func lookupGroup(groupname string) (*Group, error) {
    164 	f, err := os.Open(groupFile)
    165 	if err != nil {
    166 		return nil, err
    167 	}
    168 	defer f.Close()
    169 	return findGroupName(groupname, f)
    170 }
    171 
    172 func lookupGroupId(id string) (*Group, error) {
    173 	f, err := os.Open(groupFile)
    174 	if err != nil {
    175 		return nil, err
    176 	}
    177 	defer f.Close()
    178 	return findGroupId(id, f)
    179 }
    180 
    181 func lookupUser(username string) (*User, error) {
    182 	f, err := os.Open(userFile)
    183 	if err != nil {
    184 		return nil, err
    185 	}
    186 	defer f.Close()
    187 	return findUsername(username, f)
    188 }
    189 
    190 func lookupUserId(uid string) (*User, error) {
    191 	f, err := os.Open(userFile)
    192 	if err != nil {
    193 		return nil, err
    194 	}
    195 	defer f.Close()
    196 	return findUserId(uid, f)
    197 }
    198