1;;;
2;;; PostgreSQL pgpass parser, see
3;;;  https://www.postgresql.org/docs/current/static/libpq-pgpass.html
4;;;
5
6(in-package :pgloader.parser)
7
8(defstruct pgpass
9  hostname port database username password)
10
11(defun pgpass-char-p (char)
12  (not (member char '(#\: #\\) :test #'char=)))
13
14(defrule pgpass-escaped-char (and #\\ (or #\\ #\:))
15  (:lambda (c) (second c)))
16
17(defrule pgpass-ipv6-hostname (and #\[
18                                   (+ (or (digit-char-p character) ":"))
19                                   #\])
20  (:lambda (ipv6) (text (second ipv6))))
21
22(defrule pgpass-entry (or "*"
23                          (+ (or pgpass-ipv6-hostname
24                                 pgpass-escaped-char
25                                 (pgpass-char-p character))))
26  (:lambda (e) (text e)))
27
28(defrule pgpass-line (and (? pgpass-entry) #\: pgpass-entry #\:
29                          pgpass-entry #\: pgpass-entry #\:
30                          (? pgpass-entry))
31  (:lambda (pl)
32    (make-pgpass :hostname (or (first pl) "localhost")
33                 :port (third pl)
34                 :database (fifth pl)
35                 :username (seventh pl)
36                 :password (ninth pl))))
37
38(defun get-pgpass-filename ()
39  "Return where to find .pgpass file"
40  (or (uiop:getenv "PGPASSFILE")
41      #-windows (uiop:merge-pathnames* (uiop:make-pathname* :name ".pgpass")
42                                       (user-homedir-pathname))
43      #+windows (let ((pgpass-dir (format nil "~a/~a/"
44                                          (uiop:getenv " %APPDATA%")
45                                          "postgresql")))
46                  (uiop:make-pathname* :directory pgpass-dir
47                                       :name "pgpass"
48                                       :type "conf"))))
49
50(defun parse-pgpass-file (&optional pgpass-filename)
51  (let ((pgpass-filename (or pgpass-filename (get-pgpass-filename))))
52    (when (and pgpass-filename (probe-file pgpass-filename))
53      (with-open-file (s pgpass-filename
54                         :direction :input
55                         :if-does-not-exist nil
56                         :element-type 'character)
57        (when s
58          (loop :for line := (read-line s nil nil)
59             :while line
60             :when (and line
61                        (< 0 (length line))
62                        (char/= #\# (aref line 0)))
63             :collect (parse 'pgpass-line line)))))))
64
65(defun match-hostname (pgpass hostname)
66  "A host name of localhost matches both TCP (host name localhost) and Unix
67  domain socket (pghost empty or the default socket directory) connections
68  coming from the local machine."
69  (cond ((and (string= "localhost" (pgpass-hostname pgpass))
70              (or (eq :unix hostname)
71                  (and (stringp hostname)
72                       (string= "localhost" hostname)))))
73        ((string= "*" (pgpass-hostname pgpass))
74         t)
75        (t
76         (and (stringp hostname)
77              (string= (pgpass-hostname pgpass) hostname)))))
78
79(defun match-pgpass (pgpass hostname port database username)
80  (flet ((same-p (entry param)
81           (or (string= "*" entry)
82               (string= entry param))))
83    (when (and (match-hostname pgpass hostname)
84               (same-p (pgpass-port pgpass)     port)
85               (same-p (pgpass-database pgpass) database)
86               (same-p (pgpass-username pgpass) username))
87      (pgpass-password pgpass))))
88
89(defun match-pgpass-entries (pgpass-lines hostname port database username)
90  "Return matched password from ~/.pgpass or PGPASSFILE, or nil."
91  (loop :for pgpass :in pgpass-lines
92     :thereis (match-pgpass pgpass hostname port database username)))
93
94(defun match-pgpass-file (hostname port database username)
95  "Return matched password from ~/.pgpass or PGPASSFILE, or nil."
96  (handler-case
97      (let ((pgpass-entries (parse-pgpass-file)))
98        (when pgpass-entries
99          (match-pgpass-entries pgpass-entries hostname port database username)))
100    (condition (e)
101      ;; if we had any problem (parsing error in pgpass or otherwise), just
102      ;; return a NIL password
103      (log-message :warning "Error reading pgass file: ~a" e)
104      nil)))
105