Implement support for a pgpass file.

The implementation follows PostgreSQL specifications as closely as possible,
with the escaping rules and the matching rules. The default path where to
find the .pgpass (or pgpass.conf on windows) are as documented in PostgreSQL
too. Only missing are the file permissions check.

Fix #460.
This commit is contained in:
Dimitri Fontaine 2017-08-29 03:16:35 +02:00
parent bcc934d7aa
commit d5072d11e5
5 changed files with 131 additions and 14 deletions

View File

@ -806,6 +806,9 @@ Can contain any character, including the at sign (\fB@\fR) which must then be do
.IP
When omitted, the \fIpassword\fR defaults to the value of the \fBPGPASSWORD\fR environment variable if it is set, otherwise the password is left unset\.
.
.IP
When no \fIpassword\fR is found either in the connection URI nor in the environment, then pgloader looks for a \fB\.pgpass\fR file as documented at https://www\.postgresql\.org/docs/current/static/libpq\-pgpass\.html\. The implementation is not that of \fBlibpq\fR though\. As with \fBlibpq\fR you can set the environment variable \fBPGPASSFILE\fR to point to a \fB\.pgpass\fR file, and pgloader defaults to \fB~/\.pgpass\fR on unix like systems and \fB%APPDATA%\epostgresql\epgpass\.conf\fR on windows\. Matching rules and syntax are the same as with \fBlibpq\fR, refer to its documentation\.
.
.IP "\(bu" 4
\fInetloc\fR
.

View File

@ -677,6 +677,15 @@ Where:
When omitted, the *password* defaults to the value of the `PGPASSWORD`
environment variable if it is set, otherwise the password is left
unset.
When no *password* is found either in the connection URI nor in the
environment, then pgloader looks for a `.pgpass` file as documented at
https://www.postgresql.org/docs/current/static/libpq-pgpass.html. The
implementation is not that of `libpq` though. As with `libpq` you can
set the environment variable `PGPASSFILE` to point to a `.pgpass` file,
and pgloader defaults to `~/.pgpass` on unix like systems and
`%APPDATA%\postgresql\pgpass.conf` on windows. Matching rules and syntax
are the same as with `libpq`, refer to its documentation.
- *netloc*

View File

@ -117,6 +117,7 @@
(:file "command-utils")
(:file "command-keywords")
(:file "command-regexp")
(:file "parse-pgpass")
(:file "command-db-uri")
(:file "command-source")
(:file "command-options")

View File

@ -194,21 +194,33 @@
;; Default to environment variables as described in
;; http://www.postgresql.org/docs/9.3/static/app-psql.html
(declare (ignore type))
(make-instance 'pgsql-connection
:user (or user
(getenv-default "PGUSER"
#+unix (getenv-default "USER")
#-unix (getenv-default "UserName")))
:pass (or password (getenv-default "PGPASSWORD"))
:host (or host (getenv-default "PGHOST"
#+unix :unix
#-unix "localhost"))
:port (or port (parse-integer
(getenv-default "PGPORT" "5432")))
:name (or dbname (getenv-default "PGDATABASE" user))
(let ((pgconn
(make-instance 'pgsql-connection
:user (or user
(getenv-default "PGUSER"
#+unix
(getenv-default "USER")
#-unix
(getenv-default "UserName")))
:host (or host (getenv-default "PGHOST"
#+unix :unix
#-unix "localhost"))
:port (or port (parse-integer
(getenv-default "PGPORT" "5432")))
:name (or dbname (getenv-default "PGDATABASE" user))
:use-ssl use-ssl
:table-name table-name))))
:use-ssl use-ssl
:table-name table-name)))
;; Now set the password, maybe from ~/.pgpass
(setf (db-pass pgconn)
(or password
(getenv-default "PGPASSWORD")
(match-pgpass-file (db-host pgconn)
(princ-to-string (db-port pgconn))
(db-name pgconn)
(db-user pgconn))))
;; And return our pgconn instance
pgconn))))
(defrule target (and kw-into pgsql-uri)
(:destructure (into target)

View File

@ -0,0 +1,92 @@
;;;
;;; PostgreSQL pgpass parser, see
;;; https://www.postgresql.org/docs/current/static/libpq-pgpass.html
;;;
(in-package :pgloader.parser)
(defstruct pgpass
hostname port database username password)
(defun pgpass-char-p (char)
(not (member char '(#\: #\\) :test #'char=)))
(defrule pgpass-escaped-char (and #\\ (or #\\ #\:))
(:lambda (c) (second c)))
(defrule pgpass-entry (or "*"
(+ (or pgpass-escaped-char
(pgpass-char-p character))))
(:lambda (e) (text e)))
(defrule pgpass-line (and pgpass-entry #\: pgpass-entry #\:
pgpass-entry #\: pgpass-entry #\:
pgpass-entry)
(:lambda (pl)
(make-pgpass :hostname (first pl)
:port (third pl)
:database (fifth pl)
:username (seventh pl)
:password (ninth pl))))
(defun get-pgpass-filename ()
"Return where to find .pgpass file"
(or (uiop:getenv "PGPASSFILE")
#-windows (uiop:merge-pathnames* (uiop:make-pathname* :name ".pgpass")
(user-homedir-pathname))
#+windows (let ((pgpass-dir (format nil "~a/~a/"
(uiop:getenv " %APPDATA%")
"postgresql")))
(uiop:make-pathname* :directory pgpass-dir
:name "pgpass"
:type "conf"))))
(defun parse-pgpass-file (&optional pgpass-filename)
(let ((pgpass-filename (or pgpass-filename (get-pgpass-filename))))
(when (and pgpass-filename (probe-file pgpass-filename))
(with-open-file (s pgpass-filename
:direction :input
:if-does-not-exist nil
:element-type 'character)
(when s
(loop :for line := (read-line s nil nil)
:while line
:when (and line
(< 0 (length line))
(char/= #\# (aref line 0)))
:collect (parse 'pgpass-line line)))))))
(defun match-hostname (pgpass hostname)
"A host name of localhost matches both TCP (host name localhost) and Unix
domain socket (pghost empty or the default socket directory) connections
coming from the local machine."
(cond ((and (string= "localhost" (pgpass-hostname pgpass))
(or (eq :unix hostname)
(and (stringp hostname)
(string= "localhost" hostname)))))
((string= "*" (pgpass-hostname pgpass))
t)
(t
(and (stringp hostname)
(string= (pgpass-hostname pgpass) hostname)))))
(defun match-pgpass (pgpass hostname port database username)
(flet ((same-p (entry param)
(or (string= "*" entry)
(string= entry param))))
(when (and (match-hostname pgpass hostname)
(same-p (pgpass-port pgpass) port)
(same-p (pgpass-database pgpass) database)
(same-p (pgpass-username pgpass) username))
(pgpass-password pgpass))))
(defun match-pgpass-entries (pgpass-lines hostname port database username)
"Return matched password from ~/.pgpass or PGPASSFILE, or nil."
(loop :for pgpass :in pgpass-lines
:thereis (match-pgpass pgpass hostname port database username)))
(defun match-pgpass-file (hostname port database username)
"Return matched password from ~/.pgpass or PGPASSFILE, or nil."
(let ((pgpass-entries (parse-pgpass-file)))
(when pgpass-entries
(match-pgpass-entries pgpass-entries hostname port database username))))