1// Package pgpassfile is a parser PostgreSQL .pgpass files.
2package pgpassfile
3
4import (
5	"bufio"
6	"io"
7	"os"
8	"regexp"
9	"strings"
10)
11
12// Entry represents a line in a PG passfile.
13type Entry struct {
14	Hostname string
15	Port     string
16	Database string
17	Username string
18	Password string
19}
20
21// Passfile is the in memory data structure representing a PG passfile.
22type Passfile struct {
23	Entries []*Entry
24}
25
26// ReadPassfile reads the file at path and parses it into a Passfile.
27func ReadPassfile(path string) (*Passfile, error) {
28	f, err := os.Open(path)
29	if err != nil {
30		return nil, err
31	}
32	defer f.Close()
33
34	return ParsePassfile(f)
35}
36
37// ParsePassfile reads r and parses it into a Passfile.
38func ParsePassfile(r io.Reader) (*Passfile, error) {
39	passfile := &Passfile{}
40
41	scanner := bufio.NewScanner(r)
42	for scanner.Scan() {
43		entry := parseLine(scanner.Text())
44		if entry != nil {
45			passfile.Entries = append(passfile.Entries, entry)
46		}
47	}
48
49	return passfile, scanner.Err()
50}
51
52// Match (not colons or escaped colon or escaped backslash)+. Essentially gives a split on unescaped
53// colon.
54var colonSplitterRegexp = regexp.MustCompile("(([^:]|(\\:)))+")
55
56// var colonSplitterRegexp = regexp.MustCompile("((?:[^:]|(?:\\:)|(?:\\\\))+)")
57
58// parseLine parses a line into an *Entry. It returns nil on comment lines or any other unparsable
59// line.
60func parseLine(line string) *Entry {
61	const (
62		tmpBackslash = "\r"
63		tmpColon     = "\n"
64	)
65
66	line = strings.TrimSpace(line)
67
68	if strings.HasPrefix(line, "#") {
69		return nil
70	}
71
72	line = strings.Replace(line, `\\`, tmpBackslash, -1)
73	line = strings.Replace(line, `\:`, tmpColon, -1)
74
75	parts := strings.Split(line, ":")
76	if len(parts) != 5 {
77		return nil
78	}
79
80	// Unescape escaped colons and backslashes
81	for i := range parts {
82		parts[i] = strings.Replace(parts[i], tmpBackslash, `\`, -1)
83		parts[i] = strings.Replace(parts[i], tmpColon, `:`, -1)
84	}
85
86	return &Entry{
87		Hostname: parts[0],
88		Port:     parts[1],
89		Database: parts[2],
90		Username: parts[3],
91		Password: parts[4],
92	}
93}
94
95// FindPassword finds the password for the provided hostname, port, database, and username. For a
96// Unix domain socket hostname must be set to "localhost". An empty string will be returned if no
97// match is found.
98//
99// See https://www.postgresql.org/docs/current/libpq-pgpass.html for more password file information.
100func (pf *Passfile) FindPassword(hostname, port, database, username string) (password string) {
101	for _, e := range pf.Entries {
102		if (e.Hostname == "*" || e.Hostname == hostname) &&
103			(e.Port == "*" || e.Port == port) &&
104			(e.Database == "*" || e.Database == database) &&
105			(e.Username == "*" || e.Username == username) {
106			return e.Password
107		}
108	}
109	return ""
110}
111