Fix libpq's code for searching .pgpass; rationalize empty-list-item cases.

Before v10, we always searched ~/.pgpass using the host parameter,
and nothing else, to match to the "hostname" field of ~/.pgpass.
(However, null host or host matching DEFAULT_PGSOCKET_DIR was replaced by
"localhost".)  In v10, this got broken by commit 274bb2b38, repaired by
commit bdac9836d, and broken again by commit 7b02ba62e; in the code
actually shipped, we'd search with hostaddr if both that and host were
specified --- though oddly, *not* if only hostaddr were specified.
Since this is directly contrary to the documentation, and not
backwards-compatible, it's clearly a bug.

However, the change wasn't totally without justification, even though it
wasn't done quite right, because the pre-v10 behavior has arguably been
buggy since we added hostaddr.  If hostaddr is specified and host isn't,
the pre-v10 code will search ~/.pgpass for "localhost", and ship that
password off to a server that most likely isn't local at all.  That's
unhelpful at best, and could be a security breach at worst.

Therefore, rather than just revert to that old behavior, let's define
the behavior as "search with host if provided, else with hostaddr if
provided, else search for localhost".  (As before, a host name matching
DEFAULT_PGSOCKET_DIR is replaced by localhost.)  This matches the
behavior of the actual connection code, so that we don't pick up an
inappropriate password; and it allows useful searches to happen when
only hostaddr is given.

While we're messing around here, ensure that empty elements within a
host or hostaddr list select the same behavior as a totally-empty
field would; for instance "host=a,,b" is equivalent to "host=a,/tmp,b"
if DEFAULT_PGSOCKET_DIR is /tmp.  Things worked that way in some cases
already, but not consistently so, which contributed to the confusion
about what key ~/.pgpass would get searched with.

Update documentation accordingly, and also clarify some nearby text.

Back-patch to v10 where the host/hostaddr list functionality was
introduced.

Discussion: https://postgr.es/m/30805.1532749137@sss.pgh.pa.us
This commit is contained in:
Tom Lane 2018-08-01 12:30:36 -04:00
parent e80f2b335e
commit e3f99e03e2
2 changed files with 85 additions and 57 deletions

View File

@ -938,8 +938,8 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
<para>
If a password file is used, you can have different passwords for
different hosts. All the other connection options are the same for every
host, it is not possible to e.g. specify a different username for
different hosts.
host in the list; it is not possible to e.g. specify different
usernames for different hosts.
</para>
</sect3>
</sect2>
@ -961,7 +961,7 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
name of the directory in which the socket file is stored. If
multiple host names are specified, each will be tried in turn in
the order given. The default behavior when <literal>host</literal> is
not specified is to connect to a Unix-domain
not specified, or is empty, is to connect to a Unix-domain
socket<indexterm><primary>Unix domain socket</primary></indexterm> in
<filename>/tmp</filename> (or whatever socket directory was specified
when <productname>PostgreSQL</productname> was built). On machines without
@ -969,7 +969,8 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
</para>
<para>
A comma-separated list of host names is also accepted, in which case
each host name in the list is tried in order. See
each host name in the list is tried in order; an empty item in the
list selects the default behavior as explained above. See
<xref linkend="libpq-multiple-hosts"/> for details.
</para>
</listitem>
@ -1020,14 +1021,17 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
</itemizedlist>
Note that authentication is likely to fail if <literal>host</literal>
is not the name of the server at network address <literal>hostaddr</literal>.
Also, note that <literal>host</literal> rather than <literal>hostaddr</literal>
Also, when both <literal>host</literal> and <literal>hostaddr</literal>
are specified, <literal>host</literal>
is used to identify the connection in a password file (see
<xref linkend="libpq-pgpass"/>).
</para>
<para>
A comma-separated list of <literal>hostaddr</literal> values is also
accepted, in which case each host in the list is tried in order. See
accepted, in which case each host in the list is tried in order.
An empty item in the list causes the corresponding host name to be
used, or the default host name if that is empty as well. See
<xref linkend="libpq-multiple-hosts"/> for details.
</para>
<para>
@ -1047,9 +1051,12 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
name extension for Unix-domain
connections.<indexterm><primary>port</primary></indexterm>
If multiple hosts were given in the <literal>host</literal> or
<literal>hostaddr</literal> parameters, this parameter may specify a list
of ports of equal length, or it may specify a single port number to
be used for all hosts.
<literal>hostaddr</literal> parameters, this parameter may specify a
comma-separated list of ports of the same length as the host list, or
it may specify a single port number to be used for all hosts.
An empty string, or an empty item in a comma-separated list,
specifies the default port number established
when <productname>PostgreSQL</productname> was built.
</para>
</listitem>
</varlistentry>
@ -1683,6 +1690,17 @@ char *PQuser(const PGconn *conn);
char *PQpass(const PGconn *conn);
</synopsis>
</para>
<para>
<function>PQpass</function> will return either the password specified
in the connection parameters, or if there was none and the password
was obtained from the <link linkend="libpq-pgpass">password
file</link>, it will return that. In the latter case,
if multiple hosts were specified in the connection parameters, it is
not possible to rely on the result of <function>PQpass</function> until
the connection is established. The status of the connection can be
checked using the function <function>PQstatus</function>.
</para>
</listitem>
</varlistentry>
@ -7521,13 +7539,18 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough)
used. (Therefore, put more-specific entries first when you are using
wildcards.) If an entry needs to contain <literal>:</literal> or
<literal>\</literal>, escape this character with <literal>\</literal>.
A host name of <literal>localhost</literal> matches both TCP (host name
<literal>localhost</literal>) and Unix domain socket (<literal>pghost</literal> empty
or the default socket directory) connections coming from the local
machine. In a standby server, a database name of <literal>replication</literal>
The host name field is matched to the <literal>host</literal> connection
parameter if that is specified, otherwise to
the <literal>hostaddr</literal> parameter if that is specified; if neither
are given then the host name <literal>localhost</literal> is searched for.
The host name <literal>localhost</literal> is also searched for when
the connection is a Unix-domain socket connection and
the <literal>host</literal> parameter
matches <application>libpq</application>'s default socket directory path.
In a standby server, a database field of <literal>replication</literal>
matches streaming replication connections made to the master server.
The <literal>database</literal> field is of limited usefulness because
users have the same password for all databases in the same cluster.
The database field is of limited usefulness otherwise, because users have
the same password for all databases in the same cluster.
</para>
<para>

View File

@ -901,6 +901,8 @@ parse_comma_separated_list(char **startptr, bool *more)
static bool
connectOptions2(PGconn *conn)
{
int i;
/*
* Allocate memory for details about each host to which we might possibly
* try to connect. For that, count the number of elements in the hostaddr
@ -920,11 +922,10 @@ connectOptions2(PGconn *conn)
/*
* We now have one pg_conn_host structure per possible host. Fill in the
* host details for each one.
* host and hostaddr fields for each, by splitting the parameter strings.
*/
if (conn->pghostaddr != NULL && conn->pghostaddr[0] != '\0')
{
int i;
char *s = conn->pghostaddr;
bool more = true;
@ -933,8 +934,6 @@ connectOptions2(PGconn *conn)
conn->connhost[i].hostaddr = parse_comma_separated_list(&s, &more);
if (conn->connhost[i].hostaddr == NULL)
goto oom_error;
conn->connhost[i].type = CHT_HOST_ADDRESS;
}
/*
@ -948,7 +947,6 @@ connectOptions2(PGconn *conn)
if (conn->pghost != NULL && conn->pghost[0] != '\0')
{
int i;
char *s = conn->pghost;
bool more = true;
@ -957,17 +955,9 @@ connectOptions2(PGconn *conn)
conn->connhost[i].host = parse_comma_separated_list(&s, &more);
if (conn->connhost[i].host == NULL)
goto oom_error;
/* Identify the type of host. */
if (conn->pghostaddr == NULL || conn->pghostaddr[0] == '\0')
{
conn->connhost[i].type = CHT_HOST_NAME;
#ifdef HAVE_UNIX_SOCKETS
if (is_absolute_path(conn->connhost[i].host))
conn->connhost[i].type = CHT_UNIX_SOCKET;
#endif
}
}
/* Check for wrong number of host items. */
if (more || i != conn->nconnhost)
{
conn->status = CONNECTION_BAD;
@ -979,29 +969,48 @@ connectOptions2(PGconn *conn)
}
/*
* If neither host or hostaddr options was given, connect to default host.
* Now, for each host slot, identify the type of address spec, and fill in
* the default address if nothing was given.
*/
if ((conn->pghostaddr == NULL || conn->pghostaddr[0] == '\0') &&
(conn->pghost == NULL || conn->pghost[0] == '\0'))
for (i = 0; i < conn->nconnhost; i++)
{
Assert(conn->nconnhost == 1);
pg_conn_host *ch = &conn->connhost[i];
if (ch->hostaddr != NULL && ch->hostaddr[0] != '\0')
ch->type = CHT_HOST_ADDRESS;
else if (ch->host != NULL && ch->host[0] != '\0')
{
ch->type = CHT_HOST_NAME;
#ifdef HAVE_UNIX_SOCKETS
conn->connhost[0].host = strdup(DEFAULT_PGSOCKET_DIR);
conn->connhost[0].type = CHT_UNIX_SOCKET;
#else
conn->connhost[0].host = strdup(DefaultHost);
conn->connhost[0].type = CHT_HOST_NAME;
if (is_absolute_path(ch->host))
ch->type = CHT_UNIX_SOCKET;
#endif
if (conn->connhost[0].host == NULL)
goto oom_error;
}
else
{
if (ch->host)
free(ch->host);
#ifdef HAVE_UNIX_SOCKETS
ch->host = strdup(DEFAULT_PGSOCKET_DIR);
ch->type = CHT_UNIX_SOCKET;
#else
ch->host = strdup(DefaultHost);
ch->type = CHT_HOST_NAME;
#endif
if (ch->host == NULL)
goto oom_error;
}
}
/*
* Next, work out the port number corresponding to each host name.
*
* Note: unlike the above for host names, this could leave the port fields
* as null or empty strings. We will substitute DEF_PGPORT whenever we
* read such a port field.
*/
if (conn->pgport != NULL && conn->pgport[0] != '\0')
{
int i;
char *s = conn->pgport;
bool more = true;
@ -1065,8 +1074,8 @@ connectOptions2(PGconn *conn)
}
/*
* Supply default password if none given. Note that the password might be
* different for each host/port pair.
* If password was not given, try to look it up in password file. Note
* that the result might be different for each host/port pair.
*/
if (conn->pgpass == NULL || conn->pgpass[0] == '\0')
{
@ -1089,20 +1098,16 @@ connectOptions2(PGconn *conn)
if (conn->pgpassfile != NULL && conn->pgpassfile[0] != '\0')
{
int i;
for (i = 0; i < conn->nconnhost; i++)
{
/*
* Try to get a password for this host from pgpassfile. We use
* host name rather than host address in the same manner as
* PQhost().
* Try to get a password for this host from file. We use host
* for the hostname search key if given, else hostaddr (at
* least one of them is guaranteed nonempty by now).
*/
char *pwhost = conn->connhost[i].host;
const char *pwhost = conn->connhost[i].host;
if (conn->connhost[i].type == CHT_HOST_ADDRESS &&
conn->connhost[i].host != NULL &&
conn->connhost[i].host[0] != '\0')
if (pwhost == NULL || pwhost[0] == '\0')
pwhost = conn->connhost[i].hostaddr;
conn->connhost[i].password =
@ -6385,14 +6390,14 @@ passwordFromFile(const char *hostname, const char *port, const char *dbname,
#define LINELEN NAMEDATALEN*5
char buf[LINELEN];
if (dbname == NULL || strlen(dbname) == 0)
if (dbname == NULL || dbname[0] == '\0')
return NULL;
if (username == NULL || strlen(username) == 0)
if (username == NULL || username[0] == '\0')
return NULL;
/* 'localhost' matches pghost of '' or the default socket directory */
if (hostname == NULL)
if (hostname == NULL || hostname[0] == '\0')
hostname = DefaultHost;
else if (is_absolute_path(hostname))
@ -6403,7 +6408,7 @@ passwordFromFile(const char *hostname, const char *port, const char *dbname,
if (strcmp(hostname, DEFAULT_PGSOCKET_DIR) == 0)
hostname = DefaultHost;
if (port == NULL)
if (port == NULL || port[0] == '\0')
port = DEF_PGPORT_STR;
/* If password file cannot be opened, ignore it. */