From 274bb2b3857cc987cfa21d14775cae9b0dababa5 Mon Sep 17 00:00:00 2001 From: Robert Haas Date: Thu, 3 Nov 2016 09:25:20 -0400 Subject: [PATCH] libpq: Allow connection strings and URIs to specify multiple hosts. 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 | 38 +- src/interfaces/libpq/fe-auth.c | 28 +- src/interfaces/libpq/fe-connect.c | 607 +++++++++++++++++++++--------- src/interfaces/libpq/libpq-int.h | 41 +- 4 files changed, 506 insertions(+), 208 deletions(-) diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml index 4e34f00e44..d04dba7493 100644 --- a/doc/src/sgml/libpq.sgml +++ b/doc/src/sgml/libpq.sgml @@ -756,8 +756,10 @@ PGPing PQping(const char *conninfo); Several libpq functions parse a user-specified string to obtain connection parameters. There are two accepted formats for these strings: plain keyword = value strings - and RFC - 3986 URIs. + and URIs. URIs generally follow + RFC + 3986, except that multi-host connection strings are allowed + as further described below. @@ -792,7 +794,7 @@ host=localhost port=5432 dbname=mydb connect_timeout=10 The general form for a connection URI is: -postgresql://[user[:password]@][netloc][:port][/dbname][?param1=value1&...] +postgresql://[user[:password]@][netloc][:port][,...][/dbname][?param1=value1&...] @@ -809,6 +811,7 @@ postgresql://localhost/mydb postgresql://user@localhost postgresql://user:secret@localhost postgresql://other@localhost/otherdb?connect_timeout=10&application_name=myapp +postgresql://host1:123,host2:456/somedb Components of the hierarchical part of the URI can also be given as parameters. For example: @@ -856,6 +859,15 @@ postgresql:///dbname?host=/var/lib/postgresql postgresql://%2Fvar%2Flib%2Fpostgresql/dbname + + + It is possible to specify multiple host components, each with an optional + port component, in a single URI. A URI of the form + postgresql://host1:port1,host2:port2,host3:port3/ + is equivalent to a connection string of the form + host=host1,host2,host3 port=port1,port2,port3. Each + host will be tried in turn until a connection is successfully established. + @@ -870,12 +882,13 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname host - Name of host to connect to.host name - If this begins with a slash, it specifies Unix-domain + Comma-separated list of host names.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 host 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 host is + not specified is to connect to a Unix-domain socketUnix domain socket in /tmp (or whatever socket directory was specified when 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.port + If the 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. @@ -1394,7 +1410,11 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname The following functions return parameter values established at connection. - These values are fixed for the life of the PGconn object. + These values are fixed for the life of the connection. If a multi-host + connection string is used, the values of PQhost, + PQport, and PQpass can change if a new connection + is established using the same PGconn object. Other values + are fixed for the lifetime of the PGconn object. diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c index 404bc93306..19171fb676 100644 --- a/src/interfaces/libpq/fe-auth.c +++ b/src/interfaces/libpq/fe-auth.c @@ -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) diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index f3a9e5a83f..b4f9ad72e5 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -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; diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 7007692fb1..854ec89924 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -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? */ -- 2.40.0