diff --git a/pgloader.1 b/pgloader.1 index 154bc0e..9b17dcc 100644 --- a/pgloader.1 +++ b/pgloader.1 @@ -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 . diff --git a/pgloader.1.md b/pgloader.1.md index d64c35a..e1bc0bf 100644 --- a/pgloader.1.md +++ b/pgloader.1.md @@ -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* diff --git a/pgloader.asd b/pgloader.asd index 12e7036..434a024 100644 --- a/pgloader.asd +++ b/pgloader.asd @@ -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") diff --git a/src/parsers/command-db-uri.lisp b/src/parsers/command-db-uri.lisp index caf1655..1bcc494 100644 --- a/src/parsers/command-db-uri.lisp +++ b/src/parsers/command-db-uri.lisp @@ -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) diff --git a/src/parsers/parse-pgpass.lisp b/src/parsers/parse-pgpass.lisp new file mode 100644 index 0000000..50f4d0b --- /dev/null +++ b/src/parsers/parse-pgpass.lisp @@ -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))))