package netrc import ( "bufio" "bytes" "fmt" "io" "io/ioutil" "os" "strings" "sync" "unicode" "unicode/utf8" ) type tkType int const ( tkMachine tkType = iota tkDefault tkLogin tkPassword tkAccount tkMacdef tkComment tkWhitespace ) var keywords = map[string]tkType{ "machine": tkMachine, "default": tkDefault, "login": tkLogin, "password": tkPassword, "account": tkAccount, "macdef": tkMacdef, "#": tkComment, } type Netrc struct { tokens []*token machines []*Machine macros Macros updateLock sync.Mutex } // FindMachine returns the Machine in n named by name. If a machine named by // name exists, it is returned. If no Machine with name name is found and there // is a ``default'' machine, the ``default'' machine is returned. Otherwise, nil // is returned. func (n *Netrc) FindMachine(name string) (m *Machine) { // TODO(bgentry): not safe for concurrency var def *Machine for _, m = range n.machines { if m.Name == name { return m } if m.IsDefault() { def = m } } if def == nil { return nil } return def } // MarshalText implements the encoding.TextMarshaler interface to encode a // Netrc into text format. func (n *Netrc) MarshalText() (text []byte, err error) { // TODO(bgentry): not safe for concurrency for i := range n.tokens { switch n.tokens[i].kind { case tkComment, tkDefault, tkWhitespace: // always append these types text = append(text, n.tokens[i].rawkind...) default: if n.tokens[i].value != "" { // skip empty-value tokens text = append(text, n.tokens[i].rawkind...) } } if n.tokens[i].kind == tkMacdef { text = append(text, ' ') text = append(text, n.tokens[i].macroName...) } text = append(text, n.tokens[i].rawvalue...) } return } func (n *Netrc) NewMachine(name, login, password, account string) *Machine { n.updateLock.Lock() defer n.updateLock.Unlock() prefix := "\n" if len(n.tokens) == 0 { prefix = "" } m := &Machine{ Name: name, Login: login, Password: password, Account: account, nametoken: &token{ kind: tkMachine, rawkind: []byte(prefix + "machine"), value: name, rawvalue: []byte(" " + name), }, logintoken: &token{ kind: tkLogin, rawkind: []byte("\n\tlogin"), value: login, rawvalue: []byte(" " + login), }, passtoken: &token{ kind: tkPassword, rawkind: []byte("\n\tpassword"), value: password, rawvalue: []byte(" " + password), }, accounttoken: &token{ kind: tkAccount, rawkind: []byte("\n\taccount"), value: account, rawvalue: []byte(" " + account), }, } n.insertMachineTokensBeforeDefault(m) for i := range n.machines { if n.machines[i].IsDefault() { n.machines = append(append(n.machines[:i], m), n.machines[i:]...) return m } } n.machines = append(n.machines, m) return m } func (n *Netrc) insertMachineTokensBeforeDefault(m *Machine) { newtokens := []*token{m.nametoken} if m.logintoken.value != "" { newtokens = append(newtokens, m.logintoken) } if m.passtoken.value != "" { newtokens = append(newtokens, m.passtoken) } if m.accounttoken.value != "" { newtokens = append(newtokens, m.accounttoken) } for i := range n.tokens { if n.tokens[i].kind == tkDefault { // found the default, now insert tokens before it n.tokens = append(n.tokens[:i], append(newtokens, n.tokens[i:]...)...) return } } // didn't find a default, just add the newtokens to the end n.tokens = append(n.tokens, newtokens...) return } func (n *Netrc) RemoveMachine(name string) { n.updateLock.Lock() defer n.updateLock.Unlock() for i := range n.machines { if n.machines[i] != nil && n.machines[i].Name == name { m := n.machines[i] for _, t := range []*token{ m.nametoken, m.logintoken, m.passtoken, m.accounttoken, } { n.removeToken(t) } n.machines = append(n.machines[:i], n.machines[i+1:]...) return } } } func (n *Netrc) removeToken(t *token) { if t != nil { for i := range n.tokens { if n.tokens[i] == t { n.tokens = append(n.tokens[:i], n.tokens[i+1:]...) return } } } } // Machine contains information about a remote machine. type Machine struct { Name string Login string Password string Account string nametoken *token logintoken *token passtoken *token accounttoken *token } // IsDefault returns true if the machine is a "default" token, denoted by an // empty name. func (m *Machine) IsDefault() bool { return m.Name == "" } // UpdatePassword sets the password for the Machine m. func (m *Machine) UpdatePassword(newpass string) { m.Password = newpass updateTokenValue(m.passtoken, newpass) } // UpdateLogin sets the login for the Machine m. func (m *Machine) UpdateLogin(newlogin string) { m.Login = newlogin updateTokenValue(m.logintoken, newlogin) } // UpdateAccount sets the login for the Machine m. func (m *Machine) UpdateAccount(newaccount string) { m.Account = newaccount updateTokenValue(m.accounttoken, newaccount) } func updateTokenValue(t *token, value string) { oldvalue := t.value t.value = value newraw := make([]byte, len(t.rawvalue)) copy(newraw, t.rawvalue) t.rawvalue = append( bytes.TrimSuffix(newraw, []byte(oldvalue)), []byte(value)..., ) } // Macros contains all the macro definitions in a netrc file. type Macros map[string]string type token struct { kind tkType macroName string value string rawkind []byte rawvalue []byte } // Error represents a netrc file parse error. type Error struct { LineNum int // Line number Msg string // Error message } // Error returns a string representation of error e. func (e *Error) Error() string { return fmt.Sprintf("line %d: %s", e.LineNum, e.Msg) } func (e *Error) BadDefaultOrder() bool { return e.Msg == errBadDefaultOrder } const errBadDefaultOrder = "default token must appear after all machine tokens" // scanLinesKeepPrefix is a split function for a Scanner that returns each line // of text. The returned token may include newlines if they are before the // first non-space character. The returned line may be empty. The end-of-line // marker is one optional carriage return followed by one mandatory newline. In // regular expression notation, it is `\r?\n`. The last non-empty line of // input will be returned even if it has no newline. func scanLinesKeepPrefix(data []byte, atEOF bool) (advance int, token []byte, err error) { if atEOF && len(data) == 0 { return 0, nil, nil } // Skip leading spaces. start := 0 for width := 0; start < len(data); start += width { var r rune r, width = utf8.DecodeRune(data[start:]) if !unicode.IsSpace(r) { break } } if i := bytes.IndexByte(data[start:], '\n'); i >= 0 { // We have a full newline-terminated line. return start + i, data[0 : start+i], nil } // If we're at EOF, we have a final, non-terminated line. Return it. if atEOF { return len(data), data, nil } // Request more data. return 0, nil, nil } // scanWordsKeepPrefix is a split function for a Scanner that returns each // space-separated word of text, with prefixing spaces included. It will never // return an empty string. The definition of space is set by unicode.IsSpace. // // Adapted from bufio.ScanWords(). func scanTokensKeepPrefix(data []byte, atEOF bool) (advance int, token []byte, err error) { // Skip leading spaces. start := 0 for width := 0; start < len(data); start += width { var r rune r, width = utf8.DecodeRune(data[start:]) if !unicode.IsSpace(r) { break } } if atEOF && len(data) == 0 || start == len(data) { return len(data), data, nil } if len(data) > start && data[start] == '#' { return scanLinesKeepPrefix(data, atEOF) } // Scan until space, marking end of word. for width, i := 0, start; i < len(data); i += width { var r rune r, width = utf8.DecodeRune(data[i:]) if unicode.IsSpace(r) { return i, data[:i], nil } } // If we're at EOF, we have a final, non-empty, non-terminated word. Return it. if atEOF && len(data) > start { return len(data), data, nil } // Request more data. return 0, nil, nil } func newToken(rawb []byte) (*token, error) { _, tkind, err := bufio.ScanWords(rawb, true) if err != nil { return nil, err } var ok bool t := token{rawkind: rawb} t.kind, ok = keywords[string(tkind)] if !ok { trimmed := strings.TrimSpace(string(tkind)) if trimmed == "" { t.kind = tkWhitespace // whitespace-only, should happen only at EOF return &t, nil } if strings.HasPrefix(trimmed, "#") { t.kind = tkComment // this is a comment return &t, nil } return &t, fmt.Errorf("keyword expected; got " + string(tkind)) } return &t, nil } func scanValue(scanner *bufio.Scanner, pos int) ([]byte, string, int, error) { if scanner.Scan() { raw := scanner.Bytes() pos += bytes.Count(raw, []byte{'\n'}) return raw, strings.TrimSpace(string(raw)), pos, nil } if err := scanner.Err(); err != nil { return nil, "", pos, &Error{pos, err.Error()} } return nil, "", pos, nil } func parse(r io.Reader, pos int) (*Netrc, error) { b, err := ioutil.ReadAll(r) if err != nil { return nil, err } nrc := Netrc{machines: make([]*Machine, 0, 20), macros: make(Macros, 10)} defaultSeen := false var currentMacro *token var m *Machine var t *token scanner := bufio.NewScanner(bytes.NewReader(b)) scanner.Split(scanTokensKeepPrefix) for scanner.Scan() { rawb := scanner.Bytes() if len(rawb) == 0 { break } pos += bytes.Count(rawb, []byte{'\n'}) t, err = newToken(rawb) if err != nil { if currentMacro == nil { return nil, &Error{pos, err.Error()} } currentMacro.rawvalue = append(currentMacro.rawvalue, rawb...) continue } if currentMacro != nil && bytes.Contains(rawb, []byte{'\n', '\n'}) { // if macro rawvalue + rawb would contain \n\n, then macro def is over currentMacro.value = strings.TrimLeft(string(currentMacro.rawvalue), "\r\n") nrc.macros[currentMacro.macroName] = currentMacro.value currentMacro = nil } switch t.kind { case tkMacdef: if _, t.macroName, pos, err = scanValue(scanner, pos); err != nil { return nil, &Error{pos, err.Error()} } currentMacro = t case tkDefault: if defaultSeen { return nil, &Error{pos, "multiple default token"} } if m != nil { nrc.machines, m = append(nrc.machines, m), nil } m = new(Machine) m.Name = "" defaultSeen = true case tkMachine: if defaultSeen { return nil, &Error{pos, errBadDefaultOrder} } if m != nil { nrc.machines, m = append(nrc.machines, m), nil } m = new(Machine) if t.rawvalue, m.Name, pos, err = scanValue(scanner, pos); err != nil { return nil, &Error{pos, err.Error()} } t.value = m.Name m.nametoken = t case tkLogin: if m == nil || m.Login != "" { return nil, &Error{pos, "unexpected token login "} } if t.rawvalue, m.Login, pos, err = scanValue(scanner, pos); err != nil { return nil, &Error{pos, err.Error()} } t.value = m.Login m.logintoken = t case tkPassword: if m == nil || m.Password != "" { return nil, &Error{pos, "unexpected token password"} } if t.rawvalue, m.Password, pos, err = scanValue(scanner, pos); err != nil { return nil, &Error{pos, err.Error()} } t.value = m.Password m.passtoken = t case tkAccount: if m == nil || m.Account != "" { return nil, &Error{pos, "unexpected token account"} } if t.rawvalue, m.Account, pos, err = scanValue(scanner, pos); err != nil { return nil, &Error{pos, err.Error()} } t.value = m.Account m.accounttoken = t } nrc.tokens = append(nrc.tokens, t) } if err := scanner.Err(); err != nil { return nil, err } if m != nil { nrc.machines, m = append(nrc.machines, m), nil } return &nrc, nil } // ParseFile opens the file at filename and then passes its io.Reader to // Parse(). func ParseFile(filename string) (*Netrc, error) { fd, err := os.Open(filename) if err != nil { return nil, err } defer fd.Close() return Parse(fd) } // Parse parses from the the Reader r as a netrc file and returns the set of // machine information and macros defined in it. The ``default'' machine, // which is intended to be used when no machine name matches, is identified // by an empty machine name. There can be only one ``default'' machine. // // If there is a parsing error, an Error is returned. func Parse(r io.Reader) (*Netrc, error) { return parse(r, 1) } // FindMachine parses the netrc file identified by filename and returns the // Machine named by name. If a problem occurs parsing the file at filename, an // error is returned. If a machine named by name exists, it is returned. If no // Machine with name name is found and there is a ``default'' machine, the // ``default'' machine is returned. Otherwise, nil is returned. func FindMachine(filename, name string) (m *Machine, err error) { n, err := ParseFile(filename) if err != nil { return nil, err } return n.FindMachine(name), nil }