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 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186
|
// Copyright 2016 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//
// This source code is an adaptation of:
// https://go.dev/src/os/user/lookup_unix.go
// to provide user lookup functionality against an arbitrary password file.
package passwdfile
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"os"
"os/user"
"reflect"
"strconv"
"strings"
)
var colon = []byte(":")
// lineFunc returns a value, an error, or (nil, nil) to skip the row.
type lineFunc func(line []byte) (v any, err error)
// readColonFile parses r as an /etc/group or /etc/passwd style file, running
// fn for each row. readColonFile returns a value, an error, or (nil, nil) if
// the end of the file is reached without a match.
//
// readCols is the minimum number of colon-separated fields that will be passed
// to fn; in a long line additional fields may be silently discarded.
func readColonFile(r io.Reader, fn lineFunc, readCols int) (v any, err error) {
rd := bufio.NewReader(r)
// Read the file line-by-line.
for {
var isPrefix bool
var wholeLine []byte
// Read the next line. We do so in chunks (as much as reader's
// buffer is able to keep), check if we read enough columns
// already on each step and store final result in wholeLine.
for {
var line []byte
line, isPrefix, err = rd.ReadLine()
if err != nil {
// We should return (nil, nil) if EOF is reached
// without a match.
if err == io.EOF {
err = nil
}
return nil, err
}
// Simple common case: line is short enough to fit in a
// single reader's buffer.
if !isPrefix && len(wholeLine) == 0 {
wholeLine = line
break
}
wholeLine = append(wholeLine, line...)
// Check if we read the whole line (or enough columns)
// already.
if !isPrefix || bytes.Count(wholeLine, []byte{':'}) >= readCols {
break
}
}
// There's no spec for /etc/passwd or /etc/group, but we try to follow
// the same rules as the glibc parser, which allows comments and blank
// space at the beginning of a line.
wholeLine = bytes.TrimSpace(wholeLine)
if len(wholeLine) == 0 || wholeLine[0] == '#' {
continue
}
v, err = fn(wholeLine)
if v != nil || err != nil {
return v, err
}
// If necessary, skip the rest of the line
for ; isPrefix; _, isPrefix, err = rd.ReadLine() {
if err != nil {
// We should return (nil, nil) if EOF is reached without a match.
if err == io.EOF {
err = nil
}
return nil, err
}
}
}
}
// returns a *User for a row if that row's has the given value at the
// given index.
func matchUserIndexValue(value string, idx int) lineFunc {
var leadColon string
if idx > 0 {
leadColon = ":"
}
substr := []byte(leadColon + value + ":")
return func(line []byte) (v any, err error) {
if !bytes.Contains(line, substr) || bytes.Count(line, colon) < 6 {
return
}
// kevin:x:1005:1006::/home/kevin:/usr/bin/zsh
parts := strings.SplitN(string(line), ":", 7)
if len(parts) < 6 || parts[idx] != value || parts[0] == "" ||
parts[0][0] == '+' || parts[0][0] == '-' {
return
}
if _, err := strconv.Atoi(parts[2]); err != nil {
return nil, nil
}
if _, err := strconv.Atoi(parts[3]); err != nil {
return nil, nil
}
u := &user.User{
Username: parts[0],
Uid: parts[2],
Gid: parts[3],
Name: parts[4],
HomeDir: parts[5],
}
// The pw_gecos field isn't quite standardized. Some docs
// say: "It is expected to be a comma separated list of
// personal data where the first item is the full name of the
// user."
u.Name, _, _ = strings.Cut(u.Name, ",")
return u, nil
}
}
func findUserID(uid string, r io.Reader) (*user.User, error) {
i, e := strconv.Atoi(uid)
if e != nil {
return nil, errors.New("user: invalid userid " + uid)
}
if v, err := readColonFile(r, matchUserIndexValue(uid, 2), 6); err != nil {
return nil, err
} else if v != nil {
u, ok := v.(*user.User)
if !ok {
return nil, fmt.Errorf("expected user info, but found %v", reflect.TypeOf(v))
}
return u, nil
}
return nil, user.UnknownUserIdError(i)
}
func findUsername(name string, r io.Reader) (*user.User, error) {
if v, err := readColonFile(r, matchUserIndexValue(name, 0), 6); err != nil {
return nil, err
} else if v != nil {
u, ok := v.(*user.User)
if !ok {
return nil, fmt.Errorf("expected user info, but found %v", reflect.TypeOf(v))
}
return u, nil
}
return nil, user.UnknownUserError(name)
}
func LookupUserInFile(userFile, username string) (*user.User, error) {
f, err := os.Open(userFile)
if err != nil {
return nil, err
}
defer f.Close()
return findUsername(username, f)
}
func LookupUserIDInFile(userFile, uid string) (*user.User, error) {
f, err := os.Open(userFile)
if err != nil {
return nil, err
}
defer f.Close()
return findUserID(uid, f)
}
|