From 1fcb349688a02c044f675c8c84ae70a107cac76f Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio <jelte.fennema@microsoft.com>
Date: Tue, 2 Jan 2024 11:16:19 +0100
Subject: [PATCH v6 02/10] libpq: Handle NegotiateProtocolVersion message more
 leniently

Currently libpq would always error when the server returned a
NegotiateProtocolVersion message. This was fine because libpq only
supports a single protocol version and did not support any protocol
extensions. But we now need to change that to be able to add support for
future protocol changes, with a working fallback when connecting to an
older server.

This patch modifies the client side checks to allow a range of supported
protocol versions, instead of only allowing the exact version that was
requested. In addition it now allows connecting when the server does not
support some of the requested protocol extensions.

This patch also adds a new PQunsupportedProtocolExtensions API to libpq,
since a user might want to take some action in case a protocol extension
that it had requested is not supported.
---
 doc/src/sgml/libpq.sgml             | 19 ++++++++++++
 src/interfaces/libpq/exports.txt    |  1 +
 src/interfaces/libpq/fe-connect.c   | 46 ++++++++++++++++++++++++++++-
 src/interfaces/libpq/fe-protocol3.c | 46 ++++++++++-------------------
 src/interfaces/libpq/libpq-fe.h     |  1 +
 src/interfaces/libpq/libpq-int.h    |  2 ++
 6 files changed, 83 insertions(+), 32 deletions(-)

diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 21195e0e728..98818970ba8 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -2576,6 +2576,25 @@ int PQprotocolVersion(const PGconn *conn);
      </listitem>
     </varlistentry>
 
+    <varlistentry id="libpq-PQunsupportedProtocolExtensions">
+     <term><function>PQprotocolVersion</function><indexterm><primary>PQprotocolVersion</primary></indexterm></term>
+
+     <listitem>
+      <para>
+       Returns a null-terminated array of protocol extensions that were
+       requested by the client but are not supported by the server.
+<synopsis>
+int PQunsupportedProtocolExtensions(const PGconn *conn);
+</synopsis>
+       Applications might wish to use this function to determine whether certain
+       protocol extensions they intended to use are supported. Even when some
+       extension is not supported the connection can still be used, only the
+       unsupported extensions cannot be used. Returns NULL if the connection is
+       bad.
+      </para>
+     </listitem>
+    </varlistentry>
+
     <varlistentry id="libpq-PQserverVersion">
      <term><function>PQserverVersion</function><indexterm><primary>PQserverVersion</primary></indexterm></term>
 
diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt
index 28b861fd93b..ca9744801a8 100644
--- a/src/interfaces/libpq/exports.txt
+++ b/src/interfaces/libpq/exports.txt
@@ -192,3 +192,4 @@ PQclosePortal             189
 PQsendClosePrepared       190
 PQsendClosePortal         191
 PQchangePassword          192
+PQunsupportedProtocolExtensions 193
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 85da2a4e8dc..fac178817dd 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -382,6 +382,8 @@ static const PQEnvironmentOption EnvironmentOptions[] =
 	}
 };
 
+static const char *no_unsupported_protocol_extensions[1] = {NULL};
+
 /* The connection URI must start with either of the following designators: */
 static const char uri_designator[] = "postgresql://";
 static const char short_uri_designator[] = "postgres://";
@@ -3782,9 +3784,25 @@ keep_going:						/* We will come back to here until there is
 						libpq_append_conn_error(conn, "received invalid protocol negotiation message");
 						goto error_return;
 					}
+
+					if (conn->pversion < PG_PROTOCOL_EARLIEST)
+					{
+						libpq_append_conn_error(conn, "protocol version not supported by server: client supports down to %u.%u, server supports up to %u.%u",
+												PG_PROTOCOL_MAJOR(PG_PROTOCOL_EARLIEST), PG_PROTOCOL_MINOR(PG_PROTOCOL_EARLIEST),
+												PG_PROTOCOL_MAJOR(conn->pversion), PG_PROTOCOL_MINOR(conn->pversion));
+						goto error_return;
+					}
+
+					/* neither -- server shouldn't have sent it */
+					if (!(conn->pversion < PG_PROTOCOL_LATEST) && !conn->unsupported_pextensions)
+					{
+						libpq_append_conn_error(conn, "invalid %s message", "NegotiateProtocolVersion");
+						goto error_return;
+					}
+
 					/* OK, we read the message; mark data consumed */
 					conn->inStart = conn->inCursor;
-					goto error_return;
+					goto keep_going;
 				}
 
 				/* It is an authentication request. */
@@ -4411,6 +4429,20 @@ freePGconn(PGconn *conn)
 	}
 	free(conn->connhost);
 
+	if (conn->unsupported_pextensions)
+	{
+		/* clean up unsupported_pextensions entries */
+		int			i = 0;
+
+		while (conn->unsupported_pextensions[i])
+		{
+			free(conn->unsupported_pextensions[i]);
+			i++;
+		}
+		free(conn->unsupported_pextensions);
+	}
+
+
 	free(conn->client_encoding_initial);
 	free(conn->events);
 	free(conn->pghost);
@@ -7234,6 +7266,18 @@ PQprotocolVersion(const PGconn *conn)
 	return PG_PROTOCOL_MAJOR(conn->pversion);
 }
 
+const char **
+PQunsupportedProtocolExtensions(const PGconn *conn)
+{
+	if (!conn)
+		return NULL;
+	if (conn->status == CONNECTION_BAD)
+		return NULL;
+	if (!conn->unsupported_pextensions)
+		return no_unsupported_protocol_extensions;
+	return (const char **) conn->unsupported_pextensions;
+}
+
 int
 PQserverVersion(const PGconn *conn)
 {
diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c
index 701d58e1087..75a0ee96785 100644
--- a/src/interfaces/libpq/fe-protocol3.c
+++ b/src/interfaces/libpq/fe-protocol3.c
@@ -1410,49 +1410,33 @@ reportErrorPosition(PQExpBuffer msg, const char *query, int loc, int encoding)
 int
 pqGetNegotiateProtocolVersion3(PGconn *conn)
 {
-	int			tmp;
-	ProtocolVersion their_version;
+	int			their_version;
 	int			num;
-	PQExpBufferData buf;
 
-	if (pqGetInt(&tmp, 4, conn) != 0)
+	if (pqGetInt(&their_version, 4, conn) != 0)
 		return EOF;
-	their_version = tmp;
 
 	if (pqGetInt(&num, 4, conn) != 0)
 		return EOF;
 
-	initPQExpBuffer(&buf);
-	for (int i = 0; i < num; i++)
+	conn->pversion = their_version;
+	if (num)
 	{
-		if (pqGets(&conn->workBuffer, conn))
+		conn->unsupported_pextensions = calloc(num + 1, sizeof(char *));
+		for (int i = 0; i < num; i++)
 		{
-			termPQExpBuffer(&buf);
-			return EOF;
+			if (pqGets(&conn->workBuffer, conn))
+			{
+				return EOF;
+			}
+			conn->unsupported_pextensions[i] = strdup(conn->workBuffer.data);
+			if (!conn->unsupported_pextensions[i])
+			{
+				return EOF;
+			}
 		}
-		if (buf.len > 0)
-			appendPQExpBufferChar(&buf, ' ');
-		appendPQExpBufferStr(&buf, conn->workBuffer.data);
 	}
 
-	if (their_version < conn->pversion)
-		libpq_append_conn_error(conn, "protocol version not supported by server: client uses %u.%u, server supports up to %u.%u",
-								PG_PROTOCOL_MAJOR(conn->pversion), PG_PROTOCOL_MINOR(conn->pversion),
-								PG_PROTOCOL_MAJOR(their_version), PG_PROTOCOL_MINOR(their_version));
-	if (num > 0)
-	{
-		appendPQExpBuffer(&conn->errorMessage,
-						  libpq_ngettext("protocol extension not supported by server: %s",
-										 "protocol extensions not supported by server: %s", num),
-						  buf.data);
-		appendPQExpBufferChar(&conn->errorMessage, '\n');
-	}
-
-	/* neither -- server shouldn't have sent it */
-	if (!(their_version < conn->pversion) && !(num > 0))
-		libpq_append_conn_error(conn, "invalid %s message", "NegotiateProtocolVersion");
-
-	termPQExpBuffer(&buf);
 	return 0;
 }
 
diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h
index f0ec660cb69..809204d2eb4 100644
--- a/src/interfaces/libpq/libpq-fe.h
+++ b/src/interfaces/libpq/libpq-fe.h
@@ -347,6 +347,7 @@ extern PGTransactionStatusType PQtransactionStatus(const PGconn *conn);
 extern const char *PQparameterStatus(const PGconn *conn,
 									 const char *paramName);
 extern int	PQprotocolVersion(const PGconn *conn);
+extern const char **PQunsupportedProtocolExtensions(const PGconn *conn);
 extern int	PQserverVersion(const PGconn *conn);
 extern char *PQerrorMessage(const PGconn *conn);
 extern int	PQsocket(const PGconn *conn);
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index f0143726bbc..d302a95ceaa 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -461,6 +461,8 @@ struct pg_conn
 	SockAddr	laddr;			/* Local address */
 	SockAddr	raddr;			/* Remote address */
 	ProtocolVersion pversion;	/* FE/BE protocol version in use */
+	char	  **unsupported_pextensions;	/* Unsupported protocol
+											 * extensions, null-terminated */
 	int			sversion;		/* server version, e.g. 70401 for 7.4.1 */
 	bool		auth_req_received;	/* true if any type of auth req received */
 	bool		password_needed;	/* true if server demanded a password */
-- 
2.34.1

