From 964fcb2ded5b54ce95a2a52b54d006a69d41e118 Mon Sep 17 00:00:00 2001
From: Greg Stark <stark@mit.edu>
Date: Thu, 16 Mar 2023 11:55:16 -0400
Subject: [PATCH v4 2/4] Direct SSL connections client support

---
 src/interfaces/libpq/fe-connect.c | 91 +++++++++++++++++++++++++++++--
 src/interfaces/libpq/libpq-fe.h   |  1 +
 src/interfaces/libpq/libpq-int.h  |  3 +
 3 files changed, 89 insertions(+), 6 deletions(-)

diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index b9f899c552..eb4cfdee17 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -271,6 +271,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
 		"SSL-Mode", "", 12,		/* sizeof("verify-full") == 12 */
 	offsetof(struct pg_conn, sslmode)},
 
+	{"sslnegotiation", "PGSSLNEGOTIATION", "postgres", NULL,
+		"SSL-Negotiation", "", 14,		/* strlen("requiredirect") == 14 */
+	offsetof(struct pg_conn, sslnegotiation)},
+
 	{"sslcompression", "PGSSLCOMPRESSION", "0", NULL,
 		"SSL-Compression", "", 1,
 	offsetof(struct pg_conn, sslcompression)},
@@ -1463,11 +1467,36 @@ connectOptions2(PGconn *conn)
 		}
 #endif
 	}
+
+	/*
+	 * validate sslnegotiation option, default is "postgres" for the postgres
+	 * style negotiated connection with an extra round trip but more options.
+	 */
+	if (conn->sslnegotiation)
+	{
+		if (strcmp(conn->sslnegotiation, "postgres") != 0
+			&& strcmp(conn->sslnegotiation, "direct") != 0
+			&& strcmp(conn->sslnegotiation, "requiredirect") != 0)
+		{
+			conn->status = CONNECTION_BAD;
+			libpq_append_conn_error(conn, "invalid %s value: \"%s\"",
+									"sslnegotiation", conn->sslnegotiation);
+			return false;
+		}
+
+#ifndef USE_SSL
+		if (conn->sslnegotiation[0] != 'p') {
+			conn->status = CONNECTION_BAD;
+			libpq_append_conn_error(conn, "sslnegotiation value \"%s\" invalid when SSL support is not compiled in",
+									conn->sslnegotiation);
+			return false;
+		}
+#endif
+	}
 	else
 	{
-		conn->sslmode = strdup(DefaultSSLMode);
-		if (!conn->sslmode)
-			goto oom_error;
+		libpq_append_conn_error(conn, "sslnegotiation missing?");
+		return false;
 	}
 
 	/*
@@ -1527,6 +1556,18 @@ connectOptions2(PGconn *conn)
 									conn->gssencmode);
 			return false;
 		}
+#endif
+#ifdef USE_SSL
+		/* GSS is incompatible with direct SSL connections so it requires the
+		 * default postgres style connection ssl negotiation  */
+		if (strcmp(conn->gssencmode, "require") == 0 &&
+			strcmp(conn->sslnegotiation, "postgres") != 0)
+		{
+			conn->status = CONNECTION_BAD;
+			libpq_append_conn_error(conn, "gssencmode value \"%s\" invalid when Direct SSL negotiation is enabled",
+									conn->gssencmode);
+			return false;
+		}
 #endif
 	}
 	else
@@ -2583,11 +2624,12 @@ keep_going:						/* We will come back to here until there is
 		/* initialize these values based on SSL mode */
 		conn->allow_ssl_try = (conn->sslmode[0] != 'd');	/* "disable" */
 		conn->wait_ssl_try = (conn->sslmode[0] == 'a'); /* "allow" */
+		/* direct ssl is incompatible with "allow" or "disabled" ssl */
+		conn->allow_direct_ssl_try = conn->allow_ssl_try && !conn->wait_ssl_try && (conn->sslnegotiation[0] != 'p');
 #endif
 #ifdef ENABLE_GSS
 		conn->try_gss = (conn->gssencmode[0] != 'd');	/* "disable" */
 #endif
-
 		reset_connection_state_machine = false;
 		need_new_connection = true;
 	}
@@ -3046,6 +3088,28 @@ keep_going:						/* We will come back to here until there is
 				if (pqsecure_initialize(conn, false, true) < 0)
 					goto error_return;
 
+				/* If SSL is enabled and direct SSL connections are enabled
+				 * and we haven't already established an SSL connection (or
+				 * already tried a direct connection and failed or succeeded)
+				 * then try just enabling SSL directly.
+				 *
+				 * If we fail then we'll either fail the connection (if
+				 * sslnegotiation is set to requiredirect or turn
+				 * allow_direct_ssl_try to false
+				 */
+				if (conn->allow_ssl_try
+					&& !conn->wait_ssl_try
+					&& conn->allow_direct_ssl_try
+					&& !conn->ssl_in_use
+#ifdef ENABLE_GSS
+					&& !conn->gssenc
+#endif
+					)
+				{
+					conn->status = CONNECTION_SSL_STARTUP;
+					return PGRES_POLLING_WRITING;
+				}
+
 				/*
 				 * If SSL is enabled and we haven't already got encryption of
 				 * some sort running, request SSL instead of sending the
@@ -3122,9 +3186,11 @@ keep_going:						/* We will come back to here until there is
 
 				/*
 				 * On first time through, get the postmaster's response to our
-				 * SSL negotiation packet.
+				 * SSL negotiation packet. If we are trying a direct ssl
+				 * connection skip reading the negotiation packet and go
+				 * straight to initiating an ssl connection.
 				 */
-				if (!conn->ssl_in_use)
+				if (!conn->ssl_in_use && !conn->allow_direct_ssl_try)
 				{
 					/*
 					 * We use pqReadData here since it has the logic to
@@ -3230,6 +3296,18 @@ keep_going:						/* We will come back to here until there is
 				}
 				if (pollres == PGRES_POLLING_FAILED)
 				{
+					/* Failed direct ssl connection, possibly try a new connection with postgres negotiation */
+					if (conn->allow_direct_ssl_try)
+					{
+						/* if it's requiredirect then it's a hard failure */
+						if (conn->sslnegotiation[0] == 'r')
+							goto error_return;
+						/* otherwise only retry using postgres connection */
+						conn->allow_direct_ssl_try = false;
+						need_new_connection = true;
+						goto keep_going;
+					}
+
 					/*
 					 * Failed ... if sslmode is "prefer" then do a non-SSL
 					 * retry
@@ -4231,6 +4309,7 @@ freePGconn(PGconn *conn)
 	free(conn->keepalives_interval);
 	free(conn->keepalives_count);
 	free(conn->sslmode);
+	free(conn->sslnegotiation);
 	free(conn->sslcert);
 	free(conn->sslkey);
 	if (conn->sslpassword)
diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h
index f3d9220496..05821b8473 100644
--- a/src/interfaces/libpq/libpq-fe.h
+++ b/src/interfaces/libpq/libpq-fe.h
@@ -72,6 +72,7 @@ typedef enum
 	CONNECTION_AUTH_OK,			/* Received authentication; waiting for
 								 * backend startup. */
 	CONNECTION_SETENV,			/* This state is no longer used. */
+	CONNECTION_DIRECT_SSL_STARTUP, /* Starting SSL without PG Negotiation. */
 	CONNECTION_SSL_STARTUP,		/* Negotiating SSL. */
 	CONNECTION_NEEDED,			/* Internal state: connect() needed */
 	CONNECTION_CHECK_WRITABLE,	/* Checking if session is read-write. */
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index 1dc264fe54..8d8964d835 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -380,6 +380,7 @@ struct pg_conn
 	char	   *keepalives_count;	/* maximum number of TCP keepalive
 									 * retransmits */
 	char	   *sslmode;		/* SSL mode (require,prefer,allow,disable) */
+	char	   *sslnegotiation;	/* SSL initiation style (postgres,direct,requiredirect) */
 	char	   *sslcompression; /* SSL compression (0 or 1) */
 	char	   *sslkey;			/* client key filename */
 	char	   *sslcert;		/* client certificate filename */
@@ -529,6 +530,8 @@ struct pg_conn
 	bool		ssl_in_use;
 
 #ifdef USE_SSL
+	bool		allow_direct_ssl_try; /* Try to make a direct SSL connection
+									   * without an "SSL negotiation packet" */
 	bool		allow_ssl_try;	/* Allowed to try SSL negotiation */
 	bool		wait_ssl_try;	/* Delay SSL negotiation until after
 								 * attempting normal connection */
-- 
2.39.2

