pgpass.go (2709B)
1 // Package pgpassfile is a parser PostgreSQL .pgpass files. 2 package pgpassfile 3 4 import ( 5 "bufio" 6 "io" 7 "os" 8 "regexp" 9 "strings" 10 ) 11 12 // Entry represents a line in a PG passfile. 13 type 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. 22 type Passfile struct { 23 Entries []*Entry 24 } 25 26 // ReadPassfile reads the file at path and parses it into a Passfile. 27 func 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. 38 func 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. 54 var 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. 60 func 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. 100 func (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 }