From d5072d11e5d52e10b9e82301fa7f16b49fd42d08 Mon Sep 17 00:00:00 2001 From: Dimitri Fontaine Date: Tue, 29 Aug 2017 03:16:35 +0200 Subject: [PATCH] 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. --- pgloader.1 | 3 ++ pgloader.1.md | 9 ++++ pgloader.asd | 1 + src/parsers/command-db-uri.lisp | 40 +++++++++----- src/parsers/parse-pgpass.lisp | 92 +++++++++++++++++++++++++++++++++ 5 files changed, 131 insertions(+), 14 deletions(-) create mode 100644 src/parsers/parse-pgpass.lisp 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))))