493 lines
18 KiB
Diff
493 lines
18 KiB
Diff
From a062fb822382398c7282810029016400feecf644 Mon Sep 17 00:00:00 2001
|
|
From: Tom Lane <tgl@sss.pgh.pa.us>
|
|
Date: Wed, 21 Oct 2020 16:18:41 -0400
|
|
Subject: [PATCH] Fix connection string handling in psql's \connect command.
|
|
|
|
psql's \connect claims to be able to re-use previous connection
|
|
parameters, but in fact it only re-uses the database name, user name,
|
|
host name (and possibly hostaddr, depending on version), and port.
|
|
This is problematic for assorted use cases. Notably, pg_dump[all]
|
|
emits "\connect databasename" commands which we would like to have
|
|
re-use all other parameters. If such a script is loaded in a psql run
|
|
that initially had "-d connstring" with some non-default parameters,
|
|
those other parameters would be lost, potentially causing connection
|
|
failure. (Thus, this is the same kind of bug addressed in commits
|
|
a45bc8a4f and 8e5793ab6, although the details are much different.)
|
|
|
|
To fix, redesign do_connect() so that it pulls out all properties
|
|
of the old PGconn using PQconninfo(), and then replaces individual
|
|
properties in that array. In the case where we don't wish to re-use
|
|
anything, get libpq's default settings using PQconndefaults() and
|
|
replace entries in that, so that we don't need different code paths
|
|
for the two cases.
|
|
|
|
This does result in an additional behavioral change for cases where
|
|
the original connection parameters allowed multiple hosts, say
|
|
"psql -h host1,host2", and the \connect request allows re-use of the
|
|
host setting. Because the previous coding relied on PQhost(), it
|
|
would only permit reconnection to the same host originally selected.
|
|
Although one can think of scenarios where that's a good thing, there
|
|
are others where it is not. Moreover, that behavior doesn't seem to
|
|
meet the principle of least surprise, nor was it documented; nor is
|
|
it even clear it was intended, since that coding long pre-dates the
|
|
addition of multi-host support to libpq. Hence, this patch is content
|
|
to drop it and re-use the host list as given.
|
|
|
|
Per Peter Eisentraut's comments on bug #16604. Back-patch to all
|
|
supported branches.
|
|
|
|
Discussion: https://postgr.es/m/16604-933f4b8791227b15@postgresql.org
|
|
---
|
|
doc/src/sgml/ref/psql-ref.sgml | 44 ++++--
|
|
src/bin/psql/command.c | 272 ++++++++++++++++++++++++---------
|
|
2 files changed, 230 insertions(+), 86 deletions(-)
|
|
|
|
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
|
|
index c8388513b8..7fda949f3b 100644
|
|
--- a/doc/src/sgml/ref/psql-ref.sgml
|
|
+++ b/doc/src/sgml/ref/psql-ref.sgml
|
|
@@ -875,21 +875,17 @@ testdb=>
|
|
<term><literal>\c</literal> or <literal>\connect [ -reuse-previous=<replaceable class="parameter">on|off</replaceable> ] [ <replaceable class="parameter">dbname</replaceable> [ <replaceable class="parameter">username</replaceable> ] [ <replaceable class="parameter">host</replaceable> ] [ <replaceable class="parameter">port</replaceable> ] | <replaceable class="parameter">conninfo</replaceable> ]</literal></term>
|
|
<listitem>
|
|
<para>
|
|
- Establishes a new connection to a <productname>PostgreSQL</>
|
|
+ Establishes a new connection to a <productname>PostgreSQL</productname>
|
|
server. The connection parameters to use can be specified either
|
|
- using a positional syntax, or using <replaceable>conninfo</> connection
|
|
- strings as detailed in <xref linkend="libpq-connstring">.
|
|
+ using a positional syntax (one or more of database name, user,
|
|
+ host, and port), or using a <replaceable>conninfo</replaceable>
|
|
+ connection string as detailed in
|
|
+ <xref linkend="libpq-connstring">. If no arguments are given, a
|
|
+ new connection is made using the same parameters as before.
|
|
</para>
|
|
|
|
<para>
|
|
- Where the command omits database name, user, host, or port, the new
|
|
- connection can reuse values from the previous connection. By default,
|
|
- values from the previous connection are reused except when processing
|
|
- a <replaceable>conninfo</> string. Passing a first argument
|
|
- of <literal>-reuse-previous=on</>
|
|
- or <literal>-reuse-previous=off</literal> overrides that default.
|
|
- When the command neither specifies nor reuses a particular parameter,
|
|
- the <application>libpq</application> default is used. Specifying any
|
|
+ Specifying any
|
|
of <replaceable class="parameter">dbname</replaceable>,
|
|
<replaceable class="parameter">username</replaceable>,
|
|
<replaceable class="parameter">host</replaceable> or
|
|
@@ -897,12 +893,31 @@ testdb=>
|
|
as <literal>-</literal> is equivalent to omitting that parameter.
|
|
</para>
|
|
|
|
+ <para>
|
|
+ The new connection can re-use connection parameters from the previous
|
|
+ connection; not only database name, user, host, and port, but other
|
|
+ settings such as <replaceable>sslmode</replaceable>. By default,
|
|
+ parameters are re-used in the positional syntax, but not when
|
|
+ a <replaceable>conninfo</replaceable> string is given. Passing a
|
|
+ first argument of <literal>-reuse-previous=on</literal>
|
|
+ or <literal>-reuse-previous=off</literal> overrides that default. If
|
|
+ parameters are re-used, then any parameter not explicitly specified as
|
|
+ a positional parameter or in the <replaceable>conninfo</replaceable>
|
|
+ string is taken from the existing connection's parameters. An
|
|
+ exception is that if the <replaceable>host</replaceable> setting
|
|
+ is changed from its previous value using the positional syntax,
|
|
+ any <replaceable>hostaddr</replaceable> setting present in the
|
|
+ existing connection's parameters is dropped.
|
|
+ When the command neither specifies nor reuses a particular parameter,
|
|
+ the <application>libpq</application> default is used.
|
|
+ </para>
|
|
+
|
|
<para>
|
|
If the new connection is successfully made, the previous
|
|
connection is closed.
|
|
- If the connection attempt failed (wrong user name, access
|
|
- denied, etc.), the previous connection will only be kept if
|
|
- <application>psql</application> is in interactive mode. When
|
|
+ If the connection attempt fails (wrong user name, access
|
|
+ denied, etc.), the previous connection will be kept if
|
|
+ <application>psql</application> is in interactive mode. But when
|
|
executing a non-interactive script, processing will
|
|
immediately stop with an error. This distinction was chosen as
|
|
a user convenience against typos on the one hand, and a safety
|
|
@@ -917,6 +932,7 @@ testdb=>
|
|
=> \c mydb myuser host.dom 6432
|
|
=> \c service=foo
|
|
=> \c "host=localhost port=5432 dbname=mydb connect_timeout=10 sslmode=disable"
|
|
+=> \c -reuse-previous=on sslmode=require -- changes only sslmode
|
|
=> \c postgresql://tom@localhost/mydb?application_name=myapp
|
|
</programlisting>
|
|
</listitem>
|
|
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
|
|
index a30a5f8ea0..91f7f0adc3 100644
|
|
--- a/src/bin/psql/command.c
|
|
+++ b/src/bin/psql/command.c
|
|
@@ -2979,12 +2979,15 @@ do_connect(enum trivalue reuse_previous_specification,
|
|
char *dbname, char *user, char *host, char *port)
|
|
{
|
|
PGconn *o_conn = pset.db,
|
|
- *n_conn;
|
|
+ *n_conn = NULL;
|
|
+ PQconninfoOption *cinfo;
|
|
+ int nconnopts = 0;
|
|
+ bool same_host = false;
|
|
char *password = NULL;
|
|
- bool keep_password;
|
|
+ bool success = true;
|
|
+ bool keep_password = true;
|
|
bool has_connection_string;
|
|
bool reuse_previous;
|
|
- PQExpBufferData connstr;
|
|
|
|
if (!o_conn && (!dbname || !user || !host || !port))
|
|
{
|
|
@@ -3012,6 +3015,11 @@ do_connect(enum trivalue reuse_previous_specification,
|
|
reuse_previous = !has_connection_string;
|
|
break;
|
|
}
|
|
+
|
|
+ /* If the old connection does not exist, there is nothing to reuse. */
|
|
+ if (!o_conn)
|
|
+ reuse_previous = false;
|
|
+
|
|
/* Silently ignore arguments subsequent to a connection string. */
|
|
if (has_connection_string)
|
|
{
|
|
@@ -3020,41 +3028,126 @@ do_connect(enum trivalue reuse_previous_specification,
|
|
port = NULL;
|
|
}
|
|
|
|
- /* grab missing values from the old connection */
|
|
- if (!user && reuse_previous)
|
|
- user = PQuser(o_conn);
|
|
- if (!host && reuse_previous)
|
|
- host = PQhost(o_conn);
|
|
- if (!port && reuse_previous)
|
|
- port = PQport(o_conn);
|
|
-
|
|
/*
|
|
- * Any change in the parameters read above makes us discard the password.
|
|
- * We also discard it if we're to use a conninfo rather than the
|
|
- * positional syntax.
|
|
+ * If we intend to re-use connection parameters, collect them out of the
|
|
+ * old connection, then replace individual values as necessary. Otherwise,
|
|
+ * obtain a PQconninfoOption array containing libpq's defaults, and modify
|
|
+ * that. Note this function assumes that PQconninfo, PQconndefaults, and
|
|
+ * PQconninfoParse will all produce arrays containing the same options in
|
|
+ * the same order.
|
|
*/
|
|
- if (has_connection_string)
|
|
- keep_password = false;
|
|
+ if (reuse_previous)
|
|
+ cinfo = PQconninfo(o_conn);
|
|
else
|
|
- keep_password =
|
|
- (user && PQuser(o_conn) && strcmp(user, PQuser(o_conn)) == 0) &&
|
|
- (host && PQhost(o_conn) && strcmp(host, PQhost(o_conn)) == 0) &&
|
|
- (port && PQport(o_conn) && strcmp(port, PQport(o_conn)) == 0);
|
|
+ cinfo = PQconndefaults();
|
|
|
|
- /*
|
|
- * Grab missing dbname from old connection. No password discard if this
|
|
- * changes: passwords aren't (usually) database-specific.
|
|
- */
|
|
- if (!dbname && reuse_previous)
|
|
+ if (cinfo)
|
|
{
|
|
- initPQExpBuffer(&connstr);
|
|
- appendPQExpBuffer(&connstr, "dbname=");
|
|
- appendConnStrVal(&connstr, PQdb(o_conn));
|
|
- dbname = connstr.data;
|
|
- /* has_connection_string=true would be a dead store */
|
|
+ if (has_connection_string)
|
|
+ {
|
|
+ /* Parse the connstring and insert values into cinfo */
|
|
+ PQconninfoOption *replcinfo;
|
|
+ char *errmsg;
|
|
+
|
|
+ replcinfo = PQconninfoParse(dbname, &errmsg);
|
|
+ if (replcinfo)
|
|
+ {
|
|
+ PQconninfoOption *ci;
|
|
+ PQconninfoOption *replci;
|
|
+
|
|
+ for (ci = cinfo, replci = replcinfo;
|
|
+ ci->keyword && replci->keyword;
|
|
+ ci++, replci++)
|
|
+ {
|
|
+ Assert(strcmp(ci->keyword, replci->keyword) == 0);
|
|
+ /* Insert value from connstring if one was provided */
|
|
+ if (replci->val)
|
|
+ {
|
|
+ /*
|
|
+ * We know that both val strings were allocated by
|
|
+ * libpq, so the least messy way to avoid memory leaks
|
|
+ * is to swap them.
|
|
+ */
|
|
+ char *swap = replci->val;
|
|
+
|
|
+ replci->val = ci->val;
|
|
+ ci->val = swap;
|
|
+ }
|
|
+ }
|
|
+ Assert(ci->keyword == NULL && replci->keyword == NULL);
|
|
+
|
|
+ /* While here, determine how many option slots there are */
|
|
+ nconnopts = ci - cinfo;
|
|
+
|
|
+ PQconninfoFree(replcinfo);
|
|
+
|
|
+ /* We never re-use a password with a conninfo string. */
|
|
+ keep_password = false;
|
|
+
|
|
+ /* Don't let code below try to inject dbname into params. */
|
|
+ dbname = NULL;
|
|
+ }
|
|
+ else
|
|
+ {
|
|
+ /* PQconninfoParse failed */
|
|
+ if (errmsg)
|
|
+ {
|
|
+ psql_error("%s", errmsg);
|
|
+ PQfreemem(errmsg);
|
|
+ }
|
|
+ else
|
|
+ psql_error("out of memory\n");
|
|
+ success = false;
|
|
+ }
|
|
+ }
|
|
+ else
|
|
+ {
|
|
+ /*
|
|
+ * If dbname isn't a connection string, then we'll inject it and
|
|
+ * the other parameters into the keyword array below. (We can't
|
|
+ * easily insert them into the cinfo array because of memory
|
|
+ * management issues: PQconninfoFree would misbehave on Windows.)
|
|
+ * However, to avoid dependencies on the order in which parameters
|
|
+ * appear in the array, make a preliminary scan to set
|
|
+ * keep_password and same_host correctly.
|
|
+ *
|
|
+ * While any change in user, host, or port causes us to ignore the
|
|
+ * old connection's password, we don't force that for dbname,
|
|
+ * since passwords aren't database-specific.
|
|
+ */
|
|
+ PQconninfoOption *ci;
|
|
+
|
|
+ for (ci = cinfo; ci->keyword; ci++)
|
|
+ {
|
|
+ if (user && strcmp(ci->keyword, "user") == 0)
|
|
+ {
|
|
+ if (!(ci->val && strcmp(user, ci->val) == 0))
|
|
+ keep_password = false;
|
|
+ }
|
|
+ else if (host && strcmp(ci->keyword, "host") == 0)
|
|
+ {
|
|
+ if (ci->val && strcmp(host, ci->val) == 0)
|
|
+ same_host = true;
|
|
+ else
|
|
+ keep_password = false;
|
|
+ }
|
|
+ else if (port && strcmp(ci->keyword, "port") == 0)
|
|
+ {
|
|
+ if (!(ci->val && strcmp(port, ci->val) == 0))
|
|
+ keep_password = false;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /* While here, determine how many option slots there are */
|
|
+ nconnopts = ci - cinfo;
|
|
+ }
|
|
}
|
|
else
|
|
- connstr.data = NULL;
|
|
+ {
|
|
+ /* We failed to create the cinfo structure */
|
|
+ psql_error("out of memory\n");
|
|
+ success = false;
|
|
+ }
|
|
|
|
/*
|
|
* If the user asked to be prompted for a password, ask for one now. If
|
|
@@ -3066,7 +3159,7 @@ do_connect(enum trivalue reuse_previous_specification,
|
|
* the postmaster's log. But libpq offers no API that would let us obtain
|
|
* a password and then continue with the first connection attempt.
|
|
*/
|
|
- if (pset.getPassword == TRI_YES)
|
|
+ if (pset.getPassword == TRI_YES && success)
|
|
{
|
|
password = prompt_for_password(user);
|
|
}
|
|
@@ -3079,52 +3172,60 @@ do_connect(enum trivalue reuse_previous_specification,
|
|
password = NULL;
|
|
}
|
|
|
|
- while (true)
|
|
+ /* Loop till we have a connection or fail, which we might've already */
|
|
+ while (success)
|
|
{
|
|
-#define PARAMS_ARRAY_SIZE 8
|
|
- const char **keywords = pg_malloc(PARAMS_ARRAY_SIZE * sizeof(*keywords));
|
|
- const char **values = pg_malloc(PARAMS_ARRAY_SIZE * sizeof(*values));
|
|
- int paramnum = -1;
|
|
-
|
|
- keywords[++paramnum] = "host";
|
|
- values[paramnum] = host;
|
|
- keywords[++paramnum] = "port";
|
|
- values[paramnum] = port;
|
|
- keywords[++paramnum] = "user";
|
|
- values[paramnum] = user;
|
|
+ const char **keywords = pg_malloc((nconnopts + 1) * sizeof(*keywords));
|
|
+ const char **values = pg_malloc((nconnopts + 1) * sizeof(*values));
|
|
+ int paramnum = 0;
|
|
+ PQconninfoOption *ci;
|
|
|
|
/*
|
|
- * Position in the array matters when the dbname is a connection
|
|
- * string, because settings in a connection string override earlier
|
|
- * array entries only. Thus, user= in the connection string always
|
|
- * takes effect, but client_encoding= often will not.
|
|
+ * Copy non-default settings into the PQconnectdbParams parameter
|
|
+ * arrays; but override any values specified old-style, as well as the
|
|
+ * password and a couple of fields we want to set forcibly.
|
|
*
|
|
- * If you change this code, also change the initial-connection code in
|
|
+ * If you change this code, see also the initial-connection code in
|
|
* main(). For no good reason, a connection string password= takes
|
|
* precedence in main() but not here.
|
|
*/
|
|
- keywords[++paramnum] = "dbname";
|
|
- values[paramnum] = dbname;
|
|
- keywords[++paramnum] = "password";
|
|
- values[paramnum] = password;
|
|
- keywords[++paramnum] = "fallback_application_name";
|
|
- values[paramnum] = pset.progname;
|
|
- keywords[++paramnum] = "client_encoding";
|
|
- values[paramnum] = (pset.notty || getenv("PGCLIENTENCODING")) ? NULL : "auto";
|
|
-
|
|
+ for (ci = cinfo; ci->keyword; ci++)
|
|
+ {
|
|
+ keywords[paramnum] = ci->keyword;
|
|
+
|
|
+ if (dbname && strcmp(ci->keyword, "dbname") == 0)
|
|
+ values[paramnum++] = dbname;
|
|
+ else if (user && strcmp(ci->keyword, "user") == 0)
|
|
+ values[paramnum++] = user;
|
|
+ else if (host && strcmp(ci->keyword, "host") == 0)
|
|
+ values[paramnum++] = host;
|
|
+ else if (host && !same_host && strcmp(ci->keyword, "hostaddr") == 0)
|
|
+ {
|
|
+ /* If we're changing the host value, drop any old hostaddr */
|
|
+ values[paramnum++] = NULL;
|
|
+ }
|
|
+ else if (port && strcmp(ci->keyword, "port") == 0)
|
|
+ values[paramnum++] = port;
|
|
+ else if (strcmp(ci->keyword, "password") == 0)
|
|
+ values[paramnum++] = password;
|
|
+ else if (strcmp(ci->keyword, "fallback_application_name") == 0)
|
|
+ values[paramnum++] = pset.progname;
|
|
+ else if (strcmp(ci->keyword, "client_encoding") == 0)
|
|
+ values[paramnum++] = (pset.notty || getenv("PGCLIENTENCODING")) ? NULL : "auto";
|
|
+ else if (ci->val)
|
|
+ values[paramnum++] = ci->val;
|
|
+ /* else, don't bother making libpq parse this keyword */
|
|
+ }
|
|
/* add array terminator */
|
|
- keywords[++paramnum] = NULL;
|
|
+ keywords[paramnum] = NULL;
|
|
values[paramnum] = NULL;
|
|
|
|
- n_conn = PQconnectdbParams(keywords, values, true);
|
|
+ /* Note we do not want libpq to re-expand the dbname parameter */
|
|
+ n_conn = PQconnectdbParams(keywords, values, false);
|
|
|
|
pg_free(keywords);
|
|
pg_free(values);
|
|
|
|
- /* We can immediately discard the password -- no longer needed */
|
|
- if (password)
|
|
- pg_free(password);
|
|
-
|
|
if (PQstatus(n_conn) == CONNECTION_OK)
|
|
break;
|
|
|
|
@@ -3134,11 +3235,34 @@ do_connect(enum trivalue reuse_previous_specification,
|
|
*/
|
|
if (!password && PQconnectionNeedsPassword(n_conn) && pset.getPassword != TRI_NO)
|
|
{
|
|
+ /*
|
|
+ * Prompt for password using the username we actually connected
|
|
+ * with --- it might've come out of "dbname" rather than "user".
|
|
+ */
|
|
+ password = prompt_for_password(PQuser(n_conn));
|
|
PQfinish(n_conn);
|
|
- password = prompt_for_password(user);
|
|
+ n_conn = NULL;
|
|
continue;
|
|
}
|
|
|
|
+ /*
|
|
+ * We'll report the error below ... unless n_conn is NULL, indicating
|
|
+ * that libpq didn't have enough memory to make a PGconn.
|
|
+ */
|
|
+ if (n_conn == NULL)
|
|
+ psql_error("out of memory\n");
|
|
+
|
|
+ success = false;
|
|
+ } /* end retry loop */
|
|
+
|
|
+ /* Release locally allocated data, whether we succeeded or not */
|
|
+ if (password)
|
|
+ pg_free(password);
|
|
+ if (cinfo)
|
|
+ PQconninfoFree(cinfo);
|
|
+
|
|
+ if (!success)
|
|
+ {
|
|
/*
|
|
* Failed to connect to the database. In interactive mode, keep the
|
|
* previous connection to the DB; in scripting mode, close our
|
|
@@ -3146,7 +3270,11 @@ do_connect(enum trivalue reuse_previous_specification,
|
|
*/
|
|
if (pset.cur_cmd_interactive)
|
|
{
|
|
- psql_error("%s", PQerrorMessage(n_conn));
|
|
+ if (n_conn)
|
|
+ {
|
|
+ psql_error("%s", PQerrorMessage(n_conn));
|
|
+ PQfinish(n_conn);
|
|
+ }
|
|
|
|
/* pset.db is left unmodified */
|
|
if (o_conn)
|
|
@@ -3154,7 +3282,12 @@ do_connect(enum trivalue reuse_previous_specification,
|
|
}
|
|
else
|
|
{
|
|
- psql_error("\\connect: %s", PQerrorMessage(n_conn));
|
|
+ if (n_conn)
|
|
+ {
|
|
+ psql_error("\\connect: %s", PQerrorMessage(n_conn));
|
|
+ PQfinish(n_conn);
|
|
+ }
|
|
+
|
|
if (o_conn)
|
|
{
|
|
PQfinish(o_conn);
|
|
@@ -3162,13 +3295,8 @@ do_connect(enum trivalue reuse_previous_specification,
|
|
}
|
|
}
|
|
|
|
- PQfinish(n_conn);
|
|
- if (connstr.data)
|
|
- termPQExpBuffer(&connstr);
|
|
return false;
|
|
}
|
|
- if (connstr.data)
|
|
- termPQExpBuffer(&connstr);
|
|
|
|
/*
|
|
* Replace the old connection with the new one, and update
|
|
--
|
|
2.23.0
|
|
|