111 lines
2.6 KiB
Go
111 lines
2.6 KiB
Go
|
// Package pgpassfile is a parser PostgreSQL .pgpass files.
|
||
|
package pgpassfile
|
||
|
|
||
|
import (
|
||
|
"bufio"
|
||
|
"io"
|
||
|
"os"
|
||
|
"regexp"
|
||
|
"strings"
|
||
|
)
|
||
|
|
||
|
// Entry represents a line in a PG passfile.
|
||
|
type Entry struct {
|
||
|
Hostname string
|
||
|
Port string
|
||
|
Database string
|
||
|
Username string
|
||
|
Password string
|
||
|
}
|
||
|
|
||
|
// Passfile is the in memory data structure representing a PG passfile.
|
||
|
type Passfile struct {
|
||
|
Entries []*Entry
|
||
|
}
|
||
|
|
||
|
// ReadPassfile reads the file at path and parses it into a Passfile.
|
||
|
func ReadPassfile(path string) (*Passfile, error) {
|
||
|
f, err := os.Open(path)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
defer f.Close()
|
||
|
|
||
|
return ParsePassfile(f)
|
||
|
}
|
||
|
|
||
|
// ParsePassfile reads r and parses it into a Passfile.
|
||
|
func ParsePassfile(r io.Reader) (*Passfile, error) {
|
||
|
passfile := &Passfile{}
|
||
|
|
||
|
scanner := bufio.NewScanner(r)
|
||
|
for scanner.Scan() {
|
||
|
entry := parseLine(scanner.Text())
|
||
|
if entry != nil {
|
||
|
passfile.Entries = append(passfile.Entries, entry)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return passfile, scanner.Err()
|
||
|
}
|
||
|
|
||
|
// Match (not colons or escaped colon or escaped backslash)+. Essentially gives a split on unescaped
|
||
|
// colon.
|
||
|
var colonSplitterRegexp = regexp.MustCompile("(([^:]|(\\:)))+")
|
||
|
|
||
|
// var colonSplitterRegexp = regexp.MustCompile("((?:[^:]|(?:\\:)|(?:\\\\))+)")
|
||
|
|
||
|
// parseLine parses a line into an *Entry. It returns nil on comment lines or any other unparsable
|
||
|
// line.
|
||
|
func parseLine(line string) *Entry {
|
||
|
const (
|
||
|
tmpBackslash = "\r"
|
||
|
tmpColon = "\n"
|
||
|
)
|
||
|
|
||
|
line = strings.TrimSpace(line)
|
||
|
|
||
|
if strings.HasPrefix(line, "#") {
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
line = strings.Replace(line, `\\`, tmpBackslash, -1)
|
||
|
line = strings.Replace(line, `\:`, tmpColon, -1)
|
||
|
|
||
|
parts := strings.Split(line, ":")
|
||
|
if len(parts) != 5 {
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// Unescape escaped colons and backslashes
|
||
|
for i := range parts {
|
||
|
parts[i] = strings.Replace(parts[i], tmpBackslash, `\`, -1)
|
||
|
parts[i] = strings.Replace(parts[i], tmpColon, `:`, -1)
|
||
|
}
|
||
|
|
||
|
return &Entry{
|
||
|
Hostname: parts[0],
|
||
|
Port: parts[1],
|
||
|
Database: parts[2],
|
||
|
Username: parts[3],
|
||
|
Password: parts[4],
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// FindPassword finds the password for the provided hostname, port, database, and username. For a
|
||
|
// Unix domain socket hostname must be set to "localhost". An empty string will be returned if no
|
||
|
// match is found.
|
||
|
//
|
||
|
// See https://www.postgresql.org/docs/current/libpq-pgpass.html for more password file information.
|
||
|
func (pf *Passfile) FindPassword(hostname, port, database, username string) (password string) {
|
||
|
for _, e := range pf.Entries {
|
||
|
if (e.Hostname == "*" || e.Hostname == hostname) &&
|
||
|
(e.Port == "*" || e.Port == port) &&
|
||
|
(e.Database == "*" || e.Database == database) &&
|
||
|
(e.Username == "*" || e.Username == username) {
|
||
|
return e.Password
|
||
|
}
|
||
|
}
|
||
|
return ""
|
||
|
}
|