From 7a77463ec96db273ac071ec092ab44b9e5e2b25a Mon Sep 17 00:00:00 2001 From: Jack Christensen Date: Sun, 30 Dec 2018 16:53:57 -0600 Subject: [PATCH] Restructure connect process - Moved lots of connection logic to pgconn from pgx - Extracted pgpassfile package --- pgpass.go | 109 +++++++++++++++++++++++++++++++++++++++++++++++++ pgpass_test.go | 52 +++++++++++++++++++++++ 2 files changed, 161 insertions(+) create mode 100644 pgpass.go create mode 100644 pgpass_test.go diff --git a/pgpass.go b/pgpass.go new file mode 100644 index 0000000..cd249bd --- /dev/null +++ b/pgpass.go @@ -0,0 +1,109 @@ +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 "" +} diff --git a/pgpass_test.go b/pgpass_test.go new file mode 100644 index 0000000..adf7f2a --- /dev/null +++ b/pgpass_test.go @@ -0,0 +1,52 @@ +package pgpassfile + +import ( + "bytes" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func unescape(s string) string { + s = strings.Replace(s, `\:`, `:`, -1) + s = strings.Replace(s, `\\`, `\`, -1) + return s +} + +var passfile = [][]string{ + {"test1", "5432", "larrydb", "larry", "whatstheidea"}, + {"test1", "5432", "moedb", "moe", "imbecile"}, + {"test1", "5432", "curlydb", "curly", "nyuknyuknyuk"}, + {"test2", "5432", "*", "shemp", "heymoe"}, + {"test2", "5432", "*", "*", `test\\ing\:`}, + {"localhost", "*", "*", "*", "sesam"}, + {"test3", "*", "", "", "swordfish"}, // user will be filled later +} + +func TestParsePassFile(t *testing.T) { + buf := bytes.NewBufferString(`# A comment + test1:5432:larrydb:larry:whatstheidea + test1:5432:moedb:moe:imbecile + test1:5432:curlydb:curly:nyuknyuknyuk + test2:5432:*:shemp:heymoe + test2:5432:*:*:test\\ing\: + localhost:*:*:*:sesam + `) + + passfile, err := ParsePassfile(buf) + require.Nil(t, err) + + assert.Len(t, passfile.Entries, 6) + + assert.Equal(t, "whatstheidea", passfile.FindPassword("test1", "5432", "larrydb", "larry")) + assert.Equal(t, "imbecile", passfile.FindPassword("test1", "5432", "moedb", "moe")) + assert.Equal(t, `test\ing:`, passfile.FindPassword("test2", "5432", "something", "else")) + assert.Equal(t, "sesam", passfile.FindPassword("localhost", "9999", "foo", "bare")) + + assert.Equal(t, "", passfile.FindPassword("wrong", "5432", "larrydb", "larry")) + assert.Equal(t, "", passfile.FindPassword("test1", "wrong", "larrydb", "larry")) + assert.Equal(t, "", passfile.FindPassword("test1", "5432", "wrong", "larry")) + assert.Equal(t, "", passfile.FindPassword("test1", "5432", "larrydb", "wrong")) +}