libpq: Allow connection strings and URIs to specify multiple hosts.
authorRobert Haas <rhaas@postgresql.org>
Thu, 3 Nov 2016 13:25:20 +0000 (09:25 -0400)
committerRobert Haas <rhaas@postgresql.org>
Thu, 3 Nov 2016 13:25:20 +0000 (09:25 -0400)
It's also possible to specify a separate port for each host.

Previously, we'd loop over every address returned by looking up the
host name; now, we'll try every address for every host name.

Patch by me.  Victor Wagner wrote an earlier patch for this feature,
which I read, but I didn't use any of his code.  Review by Mithun Cy.

doc/src/sgml/libpq.sgml
src/interfaces/libpq/fe-auth.c
src/interfaces/libpq/fe-connect.c
src/interfaces/libpq/libpq-int.h

index 4e34f00e44de465f4e9ac97ae3d8bcf82f1c0d14..d04dba7493b2d08c9faa7323e0e90b6a1783de39 100644 (file)
@@ -756,8 +756,10 @@ PGPing PQping(const char *conninfo);
     Several <application>libpq</> functions parse a user-specified string to obtain
     connection parameters.  There are two accepted formats for these strings:
     plain <literal>keyword = value</literal> strings
-    and <ulink url="http://www.ietf.org/rfc/rfc3986.txt">RFC
-    3986</ulink> URIs.
+    and URIs.  URIs generally follow
+    <ulink url="http://www.ietf.org/rfc/rfc3986.txt">RFC
+    3986</ulink>, except that multi-host connection strings are allowed
+    as further described below.
    </para>
 
    <sect3>
@@ -792,7 +794,7 @@ host=localhost port=5432 dbname=mydb connect_timeout=10
    <para>
    The general form for a connection <acronym>URI</acronym> is:
 <synopsis>
-postgresql://[user[:password]@][netloc][:port][/dbname][?param1=value1&amp;...]
+postgresql://[user[:password]@][netloc][:port][,...][/dbname][?param1=value1&amp;...]
 </synopsis>
    </para>
 
@@ -809,6 +811,7 @@ postgresql://localhost/mydb
 postgresql://user@localhost
 postgresql://user:secret@localhost
 postgresql://other@localhost/otherdb?connect_timeout=10&amp;application_name=myapp
+postgresql://host1:123,host2:456/somedb
 </programlisting>
     Components of the hierarchical part of the <acronym>URI</acronym> can also
     be given as parameters.  For example:
@@ -856,6 +859,15 @@ postgresql:///dbname?host=/var/lib/postgresql
 postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
 </programlisting>
    </para>
+
+   <para>
+    It is possible to specify multiple host components, each with an optional
+    port component, in a single URI.  A URI of the form
+    <literal>postgresql://host1:port1,host2:port2,host3:port3/</literal>
+    is equivalent to a connection string of the form
+    <literal>host=host1,host2,host3 port=port1,port2,port3</literal>.  Each
+    host will be tried in turn until a connection is successfully established.
+   </para>
    </sect3>
   </sect2>
 
@@ -870,12 +882,13 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
       <term><literal>host</literal></term>
       <listitem>
        <para>
-        Name of host to connect to.<indexterm><primary>host name</></>
-        If this begins with a slash, it specifies Unix-domain
+        Comma-separated list of host names.<indexterm><primary>host name</></>
+        If a host name begins with a slash, it specifies Unix-domain
         communication rather than TCP/IP communication; the value is the
-        name of the directory in which the socket file is stored.  The
-        default behavior when <literal>host</literal> is not specified
-        is to connect to a Unix-domain
+        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
         socket<indexterm><primary>Unix domain socket</></> in
         <filename>/tmp</filename> (or whatever socket directory was specified
         when <productname>PostgreSQL</> was built). On machines without
@@ -950,6 +963,9 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
         Port number to connect to at the server host, or socket file
         name extension for Unix-domain
         connections.<indexterm><primary>port</></>
+        If the <literal>host</> parameter included multiple, comma-separated
+        hosts, 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.
        </para>
       </listitem>
      </varlistentry>
@@ -1394,7 +1410,11 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
 
   <para>
    The following functions return parameter values established at connection.
-   These values are fixed for the life of the <structname>PGconn</> object.
+   These values are fixed for the life of the connection.  If a multi-host
+   connection string is used, the values of <function>PQhost</>,
+   <function>PQport</>, and <function>PQpass</> can change if a new connection
+   is established using the same <structname>PGconn</> object.  Other values
+   are fixed for the lifetime of the <structname>PGconn</> object.
 
    <variablelist>
     <varlistentry id="libpq-pqdb">
index 404bc93306d00ff2b8c219ce929b7e0051852278..19171fb676af103e5d44d40b743826d07c4be89c 100644 (file)
@@ -683,20 +683,26 @@ pg_fe_sendauth(AuthRequest areq, PGconn *conn)
 
        case AUTH_REQ_MD5:
        case AUTH_REQ_PASSWORD:
-           conn->password_needed = true;
-           if (conn->pgpass == NULL || conn->pgpass[0] == '\0')
            {
-               printfPQExpBuffer(&conn->errorMessage,
-                                 PQnoPasswordSupplied);
-               return STATUS_ERROR;
-           }
-           if (pg_password_sendauth(conn, conn->pgpass, areq) != STATUS_OK)
-           {
-               printfPQExpBuffer(&conn->errorMessage,
+               char   *password = conn->connhost[conn->whichhost].password;
+
+               if (password == NULL)
+                   password = conn->pgpass;
+               conn->password_needed = true;
+               if (password == NULL || password[0] == '\0')
+               {
+                   printfPQExpBuffer(&conn->errorMessage,
+                                     PQnoPasswordSupplied);
+                   return STATUS_ERROR;
+               }
+               if (pg_password_sendauth(conn, password, areq) != STATUS_OK)
+               {
+                   printfPQExpBuffer(&conn->errorMessage,
                     "fe_sendauth: error sending password authentication\n");
-               return STATUS_ERROR;
+                   return STATUS_ERROR;
+               }
+               break;
            }
-           break;
 
        case AUTH_REQ_SCM_CREDS:
            if (pg_local_sendauth(conn) != STATUS_OK)
index f3a9e5a83fec8239b2dcbecd97a3799e0e6fc781..b4f9ad72e598edadc285a76fc7ba7987162ec7fa 100644 (file)
@@ -770,6 +770,148 @@ connectOptions1(PGconn *conn, const char *conninfo)
 static bool
 connectOptions2(PGconn *conn)
 {
+   /*
+    * Allocate memory for details about each host to which we might possibly
+    * try to connect.  If pghostaddr is set, we're only going to try to
+    * connect to that one particular address.  If it's not, we'll use pghost,
+    * which may contain multiple, comma-separated names.
+    */
+   conn->nconnhost = 1;
+   conn->whichhost = 0;
+   if ((conn->pghostaddr == NULL || conn->pghostaddr[0] == '\0')
+       && conn->pghost != NULL)
+   {
+       char   *s;
+
+       for (s = conn->pghost; *s != '\0'; ++s)
+           if (*s == ',')
+               conn->nconnhost++;
+   }
+   conn->connhost = (pg_conn_host *)
+       calloc(conn->nconnhost, sizeof(pg_conn_host));
+   if (conn->connhost == NULL)
+       goto oom_error;
+
+   /*
+    * We now have one pg_conn_host structure per possible host.  Fill in
+    * the host details for each one.
+    */
+   if (conn->pghostaddr != NULL && conn->pghostaddr[0] != '\0')
+   {
+       conn->connhost[0].host = strdup(conn->pghostaddr);
+       if (conn->connhost[0].host == NULL)
+           goto oom_error;
+       conn->connhost[0].type = CHT_HOST_ADDRESS;
+   }
+   else if (conn->pghost != NULL && conn->pghost[0] != '\0')
+   {
+       int     i = 0;
+       char   *s = conn->pghost;
+
+       while (1)
+       {
+           char   *e = s;
+
+           /*
+            * Search for the end of the current hostname; a comma or
+            * end-of-string acts as a terminator.
+            */
+           while (*e != '\0' && *e != ',')
+               ++e;
+
+           /* Copy the hostname whose bounds we just identified. */
+           conn->connhost[i].host =
+               (char *) malloc(sizeof(char) * (e - s + 1));
+           if (conn->connhost[i].host == NULL)
+               goto oom_error;
+           memcpy(conn->connhost[i].host, s, e - s);
+           conn->connhost[i].host[e - s] = '\0';
+
+           /* Identify the type of host. */
+           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
+
+           /* Prepare to find the next host (if any). */
+           if (*e == '\0')
+               break;
+           s = e + 1;
+           i++;
+       }
+   }
+   else
+   {
+#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;
+#endif
+       if (conn->connhost[0].host == NULL)
+           goto oom_error;
+   }
+
+   /*
+    * Next, work out the port number corresponding to each host name.
+    */
+   if (conn->pgport != NULL && conn->pgport[0] != '\0')
+   {
+       int     i = 0;
+       char   *s = conn->pgport;
+       int     nports = 1;
+
+       for (i = 0; i < conn->nconnhost; ++i)
+       {
+           char   *e = s;
+
+           /* Search for the end of the current port number. */
+           while (*e != '\0' && *e != ',')
+               ++e;
+
+           /*
+            * If we found a port number of non-zero length, copy it.
+            * Otherwise, insert the default port number.
+            */
+           if (e > s)
+           {
+               conn->connhost[i].port =
+                   (char *) malloc(sizeof(char) * (e - s + 1));
+               if (conn->connhost[i].port == NULL)
+                   goto oom_error;
+               memcpy(conn->connhost[i].port, s, e - s);
+               conn->connhost[i].port[e - s] = '\0';
+           }
+
+           /*
+            * Move on to the next port number, unless there are no more.
+            * (If only one part number is specified, we reuse it for every
+            * host.)
+            */
+           if (*e != '\0')
+           {
+               s = e + 1;
+               ++nports;
+           }
+       }
+
+       /*
+        * If multiple ports were specified, there must be exactly as many
+        * ports as there were hosts.  Otherwise, we do not know how to match
+        * them up.
+        */
+       if (nports != 1 && nports != conn->nconnhost)
+       {
+           conn->status = CONNECTION_BAD;
+           printfPQExpBuffer(&conn->errorMessage,
+               libpq_gettext("could not match %d port numbers to %d hosts\n"),
+                             nports, conn->nconnhost);
+           return false;
+       }
+   }
+
    /*
     * If user name was not given, fetch it.  (Most likely, the fetch will
     * fail, since the only way we get here is if pg_fe_getauthname() failed
@@ -800,33 +942,27 @@ connectOptions2(PGconn *conn)
    }
 
    /*
-    * Supply default password if none given
+    * Supply default password if none given.  Note that the password might
+    * be different for each host/port pair.
     */
    if (conn->pgpass == NULL || conn->pgpass[0] == '\0')
    {
+       int     i;
+
        if (conn->pgpass)
            free(conn->pgpass);
-       conn->pgpass = PasswordFromFile(conn->pghost, conn->pgport,
-                                       conn->dbName, conn->pguser);
-       if (conn->pgpass == NULL)
+       conn->pgpass = strdup(DefaultPassword);
+       if (!conn->pgpass)
+           goto oom_error;
+       for (i = 0; i < conn->nconnhost; ++i)
        {
-           conn->pgpass = strdup(DefaultPassword);
-           if (!conn->pgpass)
-               goto oom_error;
+           conn->connhost[i].password =
+               PasswordFromFile(conn->connhost[i].host,
+                                conn->connhost[i].port,
+                                conn->dbName, conn->pguser);
+           if (conn->connhost[i].password != NULL)
+               conn->dot_pgpass_used = true;
        }
-       else
-           conn->dot_pgpass_used = true;
-   }
-
-   /*
-    * Allow unix socket specification in the host name
-    */
-   if (conn->pghost && is_absolute_path(conn->pghost))
-   {
-       if (conn->pgunixsocket)
-           free(conn->pgunixsocket);
-       conn->pgunixsocket = conn->pghost;
-       conn->pghost = NULL;
    }
 
    /*
@@ -1142,6 +1278,7 @@ connectFailureMessage(PGconn *conn, int errorno)
    {
        char        host_addr[NI_MAXHOST];
        const char *displayed_host;
+       const char *displayed_port;
        struct sockaddr_storage *addr = &conn->raddr.addr;
 
        /*
@@ -1171,12 +1308,11 @@ connectFailureMessage(PGconn *conn, int errorno)
        else
            strcpy(host_addr, "???");
 
-       if (conn->pghostaddr && conn->pghostaddr[0] != '\0')
-           displayed_host = conn->pghostaddr;
-       else if (conn->pghost && conn->pghost[0] != '\0')
-           displayed_host = conn->pghost;
-       else
-           displayed_host = DefaultHost;
+       /* To which host and port were we actually connecting? */
+       displayed_host = conn->connhost[conn->whichhost].host;
+       displayed_port = conn->connhost[conn->whichhost].port;
+       if (displayed_port == NULL || displayed_port[0] == '\0')
+           displayed_port = DEF_PGPORT_STR;
 
        /*
         * If the user did not supply an IP address using 'hostaddr', and
@@ -1192,7 +1328,7 @@ connectFailureMessage(PGconn *conn, int errorno)
                              SOCK_STRERROR(errorno, sebuf, sizeof(sebuf)),
                              displayed_host,
                              host_addr,
-                             conn->pgport);
+                             displayed_port);
        else
            appendPQExpBuffer(&conn->errorMessage,
                            libpq_gettext("could not connect to server: %s\n"
@@ -1200,7 +1336,7 @@ connectFailureMessage(PGconn *conn, int errorno)
                                       "\tTCP/IP connections on port %s?\n"),
                              SOCK_STRERROR(errorno, sebuf, sizeof(sebuf)),
                              displayed_host,
-                             conn->pgport);
+                             displayed_port);
    }
 }
 
@@ -1390,12 +1526,9 @@ setKeepalivesWin32(PGconn *conn)
 static int
 connectDBStart(PGconn *conn)
 {
-   int         portnum;
    char        portstr[MAXPGPATH];
-   struct addrinfo *addrs = NULL;
-   struct addrinfo hint;
-   const char *node;
    int         ret;
+   int         i;
 
    if (!conn)
        return 0;
@@ -1408,83 +1541,86 @@ connectDBStart(PGconn *conn)
    conn->outCount = 0;
 
    /*
-    * Determine the parameters to pass to pg_getaddrinfo_all.
+    * Look up socket addresses for each possible host using
+    * pg_getaddrinfo_all.
     */
+   for (i = 0; i < conn->nconnhost; ++i)
+   {
+       pg_conn_host *ch = &conn->connhost[i];
+       char       *node = ch->host;
+       struct addrinfo hint;
+       int         thisport;
 
-   /* Initialize hint structure */
-   MemSet(&hint, 0, sizeof(hint));
-   hint.ai_socktype = SOCK_STREAM;
-   hint.ai_family = AF_UNSPEC;
+       /* Initialize hint structure */
+       MemSet(&hint, 0, sizeof(hint));
+       hint.ai_socktype = SOCK_STREAM;
+       hint.ai_family = AF_UNSPEC;
 
-   /* Set up port number as a string */
-   if (conn->pgport != NULL && conn->pgport[0] != '\0')
-   {
-       portnum = atoi(conn->pgport);
-       if (portnum < 1 || portnum > 65535)
+       /* Figure out the port number we're going to use. */
+       if (ch->port == NULL)
+           thisport = DEF_PGPORT;
+       else
        {
-           appendPQExpBuffer(&conn->errorMessage,
+           thisport = atoi(ch->port);
+           if (thisport < 1 || thisport > 65535)
+           {
+               appendPQExpBuffer(&conn->errorMessage,
                              libpq_gettext("invalid port number: \"%s\"\n"),
-                             conn->pgport);
-           conn->options_valid = false;
-           goto connect_errReturn;
+                                 ch->port);
+               conn->options_valid = false;
+               goto connect_errReturn;
+           }
        }
-   }
-   else
-       portnum = DEF_PGPORT;
-   snprintf(portstr, sizeof(portstr), "%d", portnum);
+       snprintf(portstr, sizeof(portstr), "%d", thisport);
 
-   if (conn->pghostaddr != NULL && conn->pghostaddr[0] != '\0')
-   {
-       /* Using pghostaddr avoids a hostname lookup */
-       node = conn->pghostaddr;
-       hint.ai_family = AF_UNSPEC;
-       hint.ai_flags = AI_NUMERICHOST;
-   }
-   else if (conn->pghost != NULL && conn->pghost[0] != '\0')
-   {
-       /* Using pghost, so we have to look-up the hostname */
-       node = conn->pghost;
-       hint.ai_family = AF_UNSPEC;
-   }
-   else
-   {
+       /* Set up for name resolution. */
+       switch (ch->type)
+       {
+           case CHT_HOST_NAME:
+               break;
+           case CHT_HOST_ADDRESS:
+               hint.ai_flags = AI_NUMERICHOST;
+               break;
+           case CHT_UNIX_SOCKET:
 #ifdef HAVE_UNIX_SOCKETS
-       /* pghostaddr and pghost are NULL, so use Unix domain socket */
-       node = NULL;
-       hint.ai_family = AF_UNIX;
-       UNIXSOCK_PATH(portstr, portnum, conn->pgunixsocket);
-       if (strlen(portstr) >= UNIXSOCK_PATH_BUFLEN)
+               node = NULL;
+               hint.ai_family = AF_UNIX;
+               UNIXSOCK_PATH(portstr, thisport, ch->host);
+               if (strlen(portstr) >= UNIXSOCK_PATH_BUFLEN)
+               {
+                   appendPQExpBuffer(&conn->errorMessage,
+                                     libpq_gettext("Unix-domain socket path \"%s\" is too long (maximum %d bytes)\n"),
+                                 portstr,
+                                 (int) (UNIXSOCK_PATH_BUFLEN - 1));
+                   conn->options_valid = false;
+                   goto connect_errReturn;
+               }
+#else
+               Assert(false);
+#endif
+               break;
+       }
+
+       /* Use pg_getaddrinfo_all() to resolve the address */
+       ret = pg_getaddrinfo_all(node, portstr, &hint, &ch->addrlist);
+       if (ret || !ch->addrlist)
        {
-           appendPQExpBuffer(&conn->errorMessage,
-                             libpq_gettext("Unix-domain socket path \"%s\" is too long (maximum %d bytes)\n"),
-                             portstr,
-                             (int) (UNIXSOCK_PATH_BUFLEN - 1));
+           if (node)
+               appendPQExpBuffer(&conn->errorMessage,
+                                 libpq_gettext("could not translate host name \"%s\" to address: %s\n"),
+                                 node, gai_strerror(ret));
+           else
+               appendPQExpBuffer(&conn->errorMessage,
+                                 libpq_gettext("could not translate Unix-domain socket path \"%s\" to address: %s\n"),
+                                 portstr, gai_strerror(ret));
+           if (ch->addrlist)
+           {
+               pg_freeaddrinfo_all(hint.ai_family, ch->addrlist);
+               ch->addrlist = NULL;
+           }
            conn->options_valid = false;
            goto connect_errReturn;
        }
-#else
-       /* Without Unix sockets, default to localhost instead */
-       node = DefaultHost;
-       hint.ai_family = AF_UNSPEC;
-#endif   /* HAVE_UNIX_SOCKETS */
-   }
-
-   /* Use pg_getaddrinfo_all() to resolve the address */
-   ret = pg_getaddrinfo_all(node, portstr, &hint, &addrs);
-   if (ret || !addrs)
-   {
-       if (node)
-           appendPQExpBuffer(&conn->errorMessage,
-                             libpq_gettext("could not translate host name \"%s\" to address: %s\n"),
-                             node, gai_strerror(ret));
-       else
-           appendPQExpBuffer(&conn->errorMessage,
-                             libpq_gettext("could not translate Unix-domain socket path \"%s\" to address: %s\n"),
-                             portstr, gai_strerror(ret));
-       if (addrs)
-           pg_freeaddrinfo_all(hint.ai_family, addrs);
-       conn->options_valid = false;
-       goto connect_errReturn;
    }
 
 #ifdef USE_SSL
@@ -1498,9 +1634,8 @@ connectDBStart(PGconn *conn)
    /*
     * Set up to try to connect, with protocol 3.0 as the first attempt.
     */
-   conn->addrlist = addrs;
-   conn->addr_cur = addrs;
-   conn->addrlist_family = hint.ai_family;
+   conn->whichhost = 0;
+   conn->addr_cur = conn->connhost[0].addrlist;
    conn->pversion = PG_PROTOCOL(3, 0);
    conn->send_appname = true;
    conn->status = CONNECTION_NEEDED;
@@ -1702,11 +1837,27 @@ keep_going:                     /* We will come back to here until there is
                 * returned by pg_getaddrinfo_all().  conn->addr_cur is the
                 * next one to try. We fail when we run out of addresses.
                 */
-               while (conn->addr_cur != NULL)
+               for (;;)
                {
-                   struct addrinfo *addr_cur = conn->addr_cur;
+                   struct addrinfo *addr_cur;
+
+                   /*
+                    * Advance to next possible host, if we've tried all of
+                    * the addresses for the current host.
+                    */
+                   if (conn->addr_cur == NULL)
+                   {
+                       if (++conn->whichhost >= conn->nconnhost)
+                       {
+                           conn->whichhost = 0;
+                           break;
+                       }
+                       conn->addr_cur =
+                           conn->connhost[conn->whichhost].addrlist;
+                   }
 
                    /* Remember current address for possible error msg */
+                   addr_cur = conn->addr_cur;
                    memcpy(&conn->raddr.addr, addr_cur->ai_addr,
                           addr_cur->ai_addrlen);
                    conn->raddr.salen = addr_cur->ai_addrlen;
@@ -1718,7 +1869,8 @@ keep_going:                       /* We will come back to here until there is
                         * ignore socket() failure if we have more addresses
                         * to try
                         */
-                       if (addr_cur->ai_next != NULL)
+                       if (addr_cur->ai_next != NULL ||
+                           conn->whichhost + 1 < conn->nconnhost)
                        {
                            conn->addr_cur = addr_cur->ai_next;
                            continue;
@@ -1944,7 +2096,8 @@ keep_going:                       /* We will come back to here until there is
                     * If more addresses remain, keep trying, just as in the
                     * case where connect() returned failure immediately.
                     */
-                   if (conn->addr_cur->ai_next != NULL)
+                   if (conn->addr_cur->ai_next != NULL ||
+                       conn->whichhost + 1 < conn->nconnhost)
                    {
                        conn->addr_cur = conn->addr_cur->ai_next;
                        conn->status = CONNECTION_NEEDED;
@@ -2599,9 +2752,25 @@ keep_going:                      /* We will come back to here until there is
                    goto error_return;
                }
 
-               /* We can release the address list now. */
-               pg_freeaddrinfo_all(conn->addrlist_family, conn->addrlist);
-               conn->addrlist = NULL;
+               /* We can release the address lists now. */
+               if (conn->connhost != NULL)
+               {
+                   int     i;
+
+                   for (i = 0; i < conn->nconnhost; ++i)
+                   {
+                       int     family = AF_UNSPEC;
+
+#ifdef HAVE_UNIX_SOCKETS
+                       if (conn->connhost[i].type == CHT_UNIX_SOCKET)
+                           family = AF_UNIX;
+#endif
+
+                       pg_freeaddrinfo_all(family,
+                                           conn->connhost[i].addrlist);
+                       conn->connhost[i].addrlist = NULL;
+                   }
+               }
                conn->addr_cur = NULL;
 
                /* Fire up post-connection housekeeping if needed */
@@ -2858,6 +3027,21 @@ freePGconn(PGconn *conn)
        free(conn->events[i].name);
    }
 
+   /* clean up pg_conn_host structures */
+   if (conn->connhost != NULL)
+   {
+       for (i = 0; i < conn->nconnhost; ++i)
+       {
+           if (conn->connhost[i].host != NULL)
+               free(conn->connhost[i].host);
+           if (conn->connhost[i].port != NULL)
+               free(conn->connhost[i].port);
+           if (conn->connhost[i].password != NULL)
+               free(conn->connhost[i].password);
+       }
+       free(conn->connhost);
+   }
+
    if (conn->client_encoding_initial)
        free(conn->client_encoding_initial);
    if (conn->events)
@@ -2868,8 +3052,6 @@ freePGconn(PGconn *conn)
        free(conn->pghostaddr);
    if (conn->pgport)
        free(conn->pgport);
-   if (conn->pgunixsocket)
-       free(conn->pgunixsocket);
    if (conn->pgtty)
        free(conn->pgtty);
    if (conn->connect_timeout)
@@ -2983,8 +3165,24 @@ closePGconn(PGconn *conn)
    conn->asyncStatus = PGASYNC_IDLE;
    pqClearAsyncResult(conn);   /* deallocate result */
    resetPQExpBuffer(&conn->errorMessage);
-   pg_freeaddrinfo_all(conn->addrlist_family, conn->addrlist);
-   conn->addrlist = NULL;
+   if (conn->connhost != NULL)
+   {
+       int     i;
+
+       for (i = 0; i < conn->nconnhost; ++i)
+       {
+           int     family = AF_UNSPEC;
+
+#ifdef HAVE_UNIX_SOCKETS
+           if (conn->connhost[i].type == CHT_UNIX_SOCKET)
+               family = AF_UNIX;
+#endif
+
+           pg_freeaddrinfo_all(family,
+                               conn->connhost[i].addrlist);
+           conn->connhost[i].addrlist = NULL;
+       }
+   }
    conn->addr_cur = NULL;
    notify = conn->notifyHead;
    while (notify != NULL)
@@ -4720,7 +4918,10 @@ conninfo_uri_parse(const char *uri, PQExpBuffer errorMessage,
  * postgresql://[user[:password]@][netloc][:port][/dbname][?param1=value1&...]
  *
  * where "netloc" is a hostname, an IPv4 address, or an IPv6 address surrounded
- * by literal square brackets.
+ * by literal square brackets.  As an extension, we also allow multiple
+ * netloc[:port] specifications, separated by commas:
+ *
+ * postgresql://[user[:password]@][netloc][:port][,...][/dbname][?param1=value1&...]
  *
  * Any of the URI parts might use percent-encoding (%xy).
  */
@@ -4736,6 +4937,17 @@ conninfo_uri_parse_options(PQconninfoOption *options, const char *uri,
    char       *user = NULL;
    char       *host = NULL;
    bool        retval = false;
+   PQExpBufferData hostbuf;
+   PQExpBufferData portbuf;
+
+   initPQExpBuffer(&hostbuf);
+   initPQExpBuffer(&portbuf);
+   if (PQExpBufferDataBroken(hostbuf) || PQExpBufferDataBroken(portbuf))
+   {
+       printfPQExpBuffer(errorMessage,
+                         libpq_gettext("out of memory\n"));
+       return false;
+   }
 
    /* need a modifiable copy of the input URI */
    buf = strdup(uri);
@@ -4810,85 +5022,104 @@ conninfo_uri_parse_options(PQconninfoOption *options, const char *uri,
    }
 
    /*
-    * "p" has been incremented past optional URI credential information at
-    * this point and now points at the "netloc" part of the URI.
-    *
-    * Look for IPv6 address.
+    * There may be multiple netloc[:port] pairs, each separated from the next
+    * by a comma.  When we initially enter this loop, "p" has been
+    * incremented past optional URI credential information at this point and
+    * now points at the "netloc" part of the URI.  On subsequent loop
+    * iterations, "p" has been incremented past the comma separator and now
+    * points at the start of the next "netloc".
     */
-   if (*p == '[')
+   for (;;)
    {
-       host = ++p;
-       while (*p && *p != ']')
-           ++p;
-       if (!*p)
-       {
-           printfPQExpBuffer(errorMessage,
-                             libpq_gettext("end of string reached when looking for matching \"]\" in IPv6 host address in URI: \"%s\"\n"),
-                             uri);
-           goto cleanup;
-       }
-       if (p == host)
-       {
-           printfPQExpBuffer(errorMessage,
-                             libpq_gettext("IPv6 host address may not be empty in URI: \"%s\"\n"),
-                             uri);
-           goto cleanup;
-       }
-
-       /* Cut off the bracket and advance */
-       *(p++) = '\0';
-
        /*
-        * The address may be followed by a port specifier or a slash or a
-        * query.
+        * Look for IPv6 address.
         */
-       if (*p && *p != ':' && *p != '/' && *p != '?')
+       if (*p == '[')
        {
-           printfPQExpBuffer(errorMessage,
-                             libpq_gettext("unexpected character \"%c\" at position %d in URI (expected \":\" or \"/\"): \"%s\"\n"),
-                             *p, (int) (p - buf + 1), uri);
-           goto cleanup;
+           host = ++p;
+           while (*p && *p != ']')
+               ++p;
+           if (!*p)
+           {
+               printfPQExpBuffer(errorMessage,
+                                 libpq_gettext("end of string reached when looking for matching \"]\" in IPv6 host address in URI: \"%s\"\n"),
+                                 uri);
+               goto cleanup;
+           }
+           if (p == host)
+           {
+               printfPQExpBuffer(errorMessage,
+                                 libpq_gettext("IPv6 host address may not be empty in URI: \"%s\"\n"),
+                                 uri);
+               goto cleanup;
+           }
+
+           /* Cut off the bracket and advance */
+           *(p++) = '\0';
+
+           /*
+            * The address may be followed by a port specifier or a slash or a
+            * query or a separator comma.
+            */
+           if (*p && *p != ':' && *p != '/' && *p != '?' && *p != ',')
+           {
+               printfPQExpBuffer(errorMessage,
+                                 libpq_gettext("unexpected character \"%c\" at position %d in URI (expected \":\" or \"/\"): \"%s\"\n"),
+                                 *p, (int) (p - buf + 1), uri);
+               goto cleanup;
+           }
        }
-   }
-   else
-   {
-       /* not an IPv6 address: DNS-named or IPv4 netloc */
-       host = p;
+       else
+       {
+           /* not an IPv6 address: DNS-named or IPv4 netloc */
+           host = p;
 
-       /*
-        * Look for port specifier (colon) or end of host specifier (slash),
-        * or query (question mark).
-        */
-       while (*p && *p != ':' && *p != '/' && *p != '?')
-           ++p;
-   }
+           /*
+            * Look for port specifier (colon) or end of host specifier (slash)
+            * or query (question mark) or host separator (comma).
+            */
+           while (*p && *p != ':' && *p != '/' && *p != '?' && *p != ',')
+               ++p;
+       }
 
-   /* Save the hostname terminator before we null it */
-   prevchar = *p;
-   *p = '\0';
+       /* Save the hostname terminator before we null it */
+       prevchar = *p;
+       *p = '\0';
 
-   if (*host &&
-       !conninfo_storeval(options, "host", host,
-                          errorMessage, false, true))
-       goto cleanup;
+       appendPQExpBufferStr(&hostbuf, host);
 
+       if (prevchar == ':')
+       {
+           const char *port = ++p; /* advance past host terminator */
 
-   if (prevchar == ':')
-   {
-       const char *port = ++p; /* advance past host terminator */
+           while (*p && *p != '/' && *p != '?' && *p != ',')
+               ++p;
 
-       while (*p && *p != '/' && *p != '?')
-           ++p;
+           prevchar = *p;
+           *p = '\0';
 
-       prevchar = *p;
-       *p = '\0';
+           appendPQExpBufferStr(&portbuf, port);
+       }
 
-       if (*port &&
-           !conninfo_storeval(options, "port", port,
-                              errorMessage, false, true))
-           goto cleanup;
+       if (prevchar != ',')
+           break;
+       ++p;                        /* advance past comma separator */
+       appendPQExpBufferStr(&hostbuf, ",");
+       appendPQExpBufferStr(&portbuf, ",");
    }
 
+   /* Save final values for host and port. */
+   if (PQExpBufferDataBroken(hostbuf) || PQExpBufferDataBroken(portbuf))
+       goto cleanup;
+   if (hostbuf.data[0] &&
+       !conninfo_storeval(options, "host", hostbuf.data,
+                          errorMessage, false, true))
+       goto cleanup;
+   if (portbuf.data[0] &&
+       !conninfo_storeval(options, "port", portbuf.data,
+                          errorMessage, false, true))
+       goto cleanup;
+
    if (prevchar && prevchar != '?')
    {
        const char *dbname = ++p;       /* advance past host terminator */
@@ -4923,6 +5154,8 @@ conninfo_uri_parse_options(PQconninfoOption *options, const char *uri,
    retval = true;
 
 cleanup:
+   termPQExpBuffer(&hostbuf);
+   termPQExpBuffer(&portbuf);
    free(buf);
    return retval;
 }
@@ -5342,9 +5575,15 @@ PQuser(const PGconn *conn)
 char *
 PQpass(const PGconn *conn)
 {
+   char   *password = NULL;
+
    if (!conn)
        return NULL;
-   return conn->pgpass;
+   if (conn->connhost != NULL)
+       password = conn->connhost[conn->whichhost].password;
+   if (password == NULL)
+       password = conn->pgpass;
+   return password;
 }
 
 char *
@@ -5352,15 +5591,14 @@ PQhost(const PGconn *conn)
 {
    if (!conn)
        return NULL;
-   if (conn->pghost != NULL && conn->pghost[0] != '\0')
+   if (conn->connhost != NULL)
+       return conn->connhost[conn->whichhost].host;
+   else if (conn->pghost != NULL && conn->pghost[0] != '\0')
        return conn->pghost;
    else
    {
 #ifdef HAVE_UNIX_SOCKETS
-       if (conn->pgunixsocket != NULL && conn->pgunixsocket[0] != '\0')
-           return conn->pgunixsocket;
-       else
-           return DEFAULT_PGSOCKET_DIR;
+       return DEFAULT_PGSOCKET_DIR;
 #else
        return DefaultHost;
 #endif
@@ -5372,6 +5610,8 @@ PQport(const PGconn *conn)
 {
    if (!conn)
        return NULL;
+   if (conn->connhost != NULL)
+       return conn->connhost[conn->whichhost].port;
    return conn->pgport;
 }
 
@@ -5481,10 +5721,13 @@ PQbackendPID(const PGconn *conn)
 int
 PQconnectionNeedsPassword(const PGconn *conn)
 {
+   char   *password;
+
    if (!conn)
        return false;
+   password = PQpass(conn);
    if (conn->password_needed &&
-       (conn->pgpass == NULL || conn->pgpass[0] == '\0'))
+       (password == NULL || password[0] == '\0'))
        return true;
    else
        return false;
index 7007692fb1098f06172990f4f860d0b21817ed5a..854ec89924ba744fff4618847ae3abbcfe81dff2 100644 (file)
@@ -292,6 +292,30 @@ typedef struct pgDataValue
    const char *value;          /* data value, without zero-termination */
 } PGdataValue;
 
+typedef enum pg_conn_host_type
+{
+   CHT_HOST_NAME,
+   CHT_HOST_ADDRESS,
+   CHT_UNIX_SOCKET
+} pg_conn_host_type;
+
+/*
+ * pg_conn_host stores all information about one of possibly several hosts
+ * mentioned in the connection string.  Derived by splitting the pghost
+ * on the comma character and then parsing each segment.
+ */
+typedef struct pg_conn_host
+{
+   char       *host;           /* host name or address, or socket path */
+   pg_conn_host_type type;     /* type of host */
+   char       *port;           /* port number for this host; if not NULL,
+                                * overrrides the PGConn's pgport */
+   char       *password;       /* password for this host, read from the
+                                * password file.  only set if the PGconn's
+                                * pgpass field is NULL. */
+   struct addrinfo *addrlist;  /* list of possible backend addresses */
+} pg_conn_host;
+
 /*
  * PGconn stores all the state data associated with a single connection
  * to a backend.
@@ -299,13 +323,15 @@ typedef struct pgDataValue
 struct pg_conn
 {
    /* Saved values of connection options */
-   char       *pghost;         /* the machine on which the server is running */
+   char       *pghost;         /* the machine on which the server is running,
+                                * or a path to a UNIX-domain socket, or a
+                                * comma-separated list of machines and/or
+                                * paths, optionally with port suffixes; if
+                                * NULL, use DEFAULT_PGSOCKET_DIR */
    char       *pghostaddr;     /* the numeric IP address of the machine on
                                 * which the server is running.  Takes
                                 * precedence over above. */
    char       *pgport;         /* the server's communication port number */
-   char       *pgunixsocket;   /* the directory of the server's Unix-domain
-                                * socket; if NULL, use DEFAULT_PGSOCKET_DIR */
    char       *pgtty;          /* tty on which the backend messages is
                                 * displayed (OBSOLETE, NOT USED) */
    char       *connect_timeout;    /* connection timeout (numeric string) */
@@ -363,6 +389,11 @@ struct pg_conn
    PGnotify   *notifyHead;     /* oldest unreported Notify msg */
    PGnotify   *notifyTail;     /* newest unreported Notify msg */
 
+   /* Support for multiple hosts in connection string */
+   int         nconnhost;      /* # of possible hosts */
+   int         whichhost;      /* host we're currently considering */
+   pg_conn_host *connhost;     /* details about each possible host */
+
    /* Connection data */
    pgsocket    sock;           /* FD for socket, PGINVALID_SOCKET if
                                 * unconnected */
@@ -378,9 +409,7 @@ struct pg_conn
    bool        sigpipe_flag;   /* can we mask SIGPIPE via MSG_NOSIGNAL? */
 
    /* Transient state needed while establishing connection */
-   struct addrinfo *addrlist;  /* list of possible backend addresses */
-   struct addrinfo *addr_cur;  /* the one currently being tried */
-   int         addrlist_family;    /* needed to know how to free addrlist */
+   struct addrinfo *addr_cur;  /* backend address currently being tried */
    PGSetenvStatusType setenv_state;    /* for 2.0 protocol only */
    const PQEnvironmentOption *next_eo;
    bool        send_appname;   /* okay to send application_name? */