239 lines
4.7 KiB
Go
239 lines
4.7 KiB
Go
|
package netrc
|
||
|
|
||
|
import (
|
||
|
"bufio"
|
||
|
"bytes"
|
||
|
"errors"
|
||
|
"io"
|
||
|
"io/ioutil"
|
||
|
"os"
|
||
|
"os/exec"
|
||
|
"path/filepath"
|
||
|
"regexp"
|
||
|
"unicode"
|
||
|
)
|
||
|
|
||
|
// ErrInvalidNetrc means there was an error parsing the netrc file
|
||
|
var ErrInvalidNetrc = errors.New("Invalid netrc")
|
||
|
|
||
|
// Netrc file
|
||
|
type Netrc struct {
|
||
|
Path string
|
||
|
machines []*Machine
|
||
|
tokens []string
|
||
|
}
|
||
|
|
||
|
// Machine from the netrc file
|
||
|
type Machine struct {
|
||
|
Name string
|
||
|
IsDefault bool
|
||
|
tokens []string
|
||
|
}
|
||
|
|
||
|
// Parse the netrc file at the given path
|
||
|
// It returns a Netrc instance
|
||
|
func Parse(path string) (*Netrc, error) {
|
||
|
file, err := read(path)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
netrc, err := parse(lex(file))
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
netrc.Path = path
|
||
|
return netrc, nil
|
||
|
}
|
||
|
|
||
|
// Machine gets a machine by name
|
||
|
func (n *Netrc) Machine(name string) *Machine {
|
||
|
for _, m := range n.machines {
|
||
|
if m.Name == name {
|
||
|
return m
|
||
|
}
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// AddMachine adds a machine
|
||
|
func (n *Netrc) AddMachine(name, login, password string) {
|
||
|
machine := n.Machine(name)
|
||
|
if machine == nil {
|
||
|
machine = &Machine{}
|
||
|
n.machines = append(n.machines, machine)
|
||
|
}
|
||
|
machine.Name = name
|
||
|
machine.tokens = []string{"machine ", name, "\n"}
|
||
|
machine.Set("login", login)
|
||
|
machine.Set("password", password)
|
||
|
}
|
||
|
|
||
|
// RemoveMachine remove a machine
|
||
|
func (n *Netrc) RemoveMachine(name string) {
|
||
|
for i, machine := range n.machines {
|
||
|
if machine.Name == name {
|
||
|
n.machines = append(n.machines[:i], n.machines[i+1:]...)
|
||
|
// continue removing but start over since the indexes changed
|
||
|
n.RemoveMachine(name)
|
||
|
return
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Render out the netrc file to a string
|
||
|
func (n *Netrc) Render() string {
|
||
|
var b bytes.Buffer
|
||
|
for _, token := range n.tokens {
|
||
|
b.WriteString(token)
|
||
|
}
|
||
|
for _, machine := range n.machines {
|
||
|
for _, token := range machine.tokens {
|
||
|
b.WriteString(token)
|
||
|
}
|
||
|
}
|
||
|
return b.String()
|
||
|
}
|
||
|
|
||
|
// Save the file to disk
|
||
|
func (n *Netrc) Save() error {
|
||
|
body := []byte(n.Render())
|
||
|
if filepath.Ext(n.Path) == ".gpg" {
|
||
|
cmd := exec.Command("gpg", "-a", "--batch", "--default-recipient-self", "-e")
|
||
|
stdin, err := cmd.StdinPipe()
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
stdin.Write(body)
|
||
|
stdin.Close()
|
||
|
cmd.Stderr = os.Stderr
|
||
|
body, err = cmd.Output()
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
}
|
||
|
return ioutil.WriteFile(n.Path, body, 0600)
|
||
|
}
|
||
|
|
||
|
func read(path string) (io.Reader, error) {
|
||
|
if filepath.Ext(path) == ".gpg" {
|
||
|
cmd := exec.Command("gpg", "--batch", "--quiet", "--decrypt", path)
|
||
|
cmd.Stderr = os.Stderr
|
||
|
stdout, err := cmd.StdoutPipe()
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
err = cmd.Start()
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
return stdout, nil
|
||
|
}
|
||
|
return os.Open(path)
|
||
|
}
|
||
|
|
||
|
func lex(file io.Reader) []string {
|
||
|
commentRe := regexp.MustCompile("\\s*#")
|
||
|
scanner := bufio.NewScanner(file)
|
||
|
scanner.Split(func(data []byte, eof bool) (int, []byte, error) {
|
||
|
if eof && len(data) == 0 {
|
||
|
return 0, nil, nil
|
||
|
}
|
||
|
inWhitespace := unicode.IsSpace(rune(data[0]))
|
||
|
for i, c := range data {
|
||
|
if c == '#' {
|
||
|
// line has a comment
|
||
|
i = commentRe.FindIndex(data)[0]
|
||
|
if i == 0 {
|
||
|
// currently in a comment
|
||
|
i = bytes.IndexByte(data, '\n')
|
||
|
if i == -1 {
|
||
|
// no newline at end
|
||
|
if !eof {
|
||
|
return 0, nil, nil
|
||
|
}
|
||
|
i = len(data)
|
||
|
}
|
||
|
for i < len(data) {
|
||
|
if !unicode.IsSpace(rune(data[i])) {
|
||
|
break
|
||
|
}
|
||
|
i++
|
||
|
}
|
||
|
}
|
||
|
return i, data[0:i], nil
|
||
|
}
|
||
|
if unicode.IsSpace(rune(c)) != inWhitespace {
|
||
|
return i, data[0:i], nil
|
||
|
}
|
||
|
}
|
||
|
if eof {
|
||
|
return len(data), data, nil
|
||
|
}
|
||
|
return 0, nil, nil
|
||
|
})
|
||
|
tokens := make([]string, 0, 100)
|
||
|
for scanner.Scan() {
|
||
|
tokens = append(tokens, scanner.Text())
|
||
|
}
|
||
|
return tokens
|
||
|
}
|
||
|
|
||
|
func parse(tokens []string) (*Netrc, error) {
|
||
|
n := &Netrc{}
|
||
|
n.machines = make([]*Machine, 0, 20)
|
||
|
var machine *Machine
|
||
|
for i, token := range tokens {
|
||
|
// group tokens into machines
|
||
|
if token == "machine" || token == "default" {
|
||
|
// start new group
|
||
|
machine = &Machine{}
|
||
|
n.machines = append(n.machines, machine)
|
||
|
if token == "default" {
|
||
|
machine.IsDefault = true
|
||
|
machine.Name = "default"
|
||
|
} else {
|
||
|
machine.Name = tokens[i+2]
|
||
|
}
|
||
|
}
|
||
|
if machine == nil {
|
||
|
n.tokens = append(n.tokens, token)
|
||
|
} else {
|
||
|
machine.tokens = append(machine.tokens, token)
|
||
|
}
|
||
|
}
|
||
|
return n, nil
|
||
|
}
|
||
|
|
||
|
// Get a property from a machine
|
||
|
func (m *Machine) Get(name string) string {
|
||
|
i := 4
|
||
|
if m.IsDefault {
|
||
|
i = 2
|
||
|
}
|
||
|
for {
|
||
|
if i+2 >= len(m.tokens) {
|
||
|
return ""
|
||
|
}
|
||
|
if m.tokens[i] == name {
|
||
|
return m.tokens[i+2]
|
||
|
}
|
||
|
i = i + 4
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Set a property on the machine
|
||
|
func (m *Machine) Set(name, value string) {
|
||
|
i := 4
|
||
|
if m.IsDefault {
|
||
|
i = 2
|
||
|
}
|
||
|
for i+2 < len(m.tokens) {
|
||
|
if m.tokens[i] == name {
|
||
|
m.tokens[i+2] = value
|
||
|
return
|
||
|
}
|
||
|
i = i + 4
|
||
|
}
|
||
|
m.tokens = append(m.tokens, " ", name, " ", value, "\n")
|
||
|
}
|