From fd58612f74a308aed3f0d3caf79250c0bfce2068 Mon Sep 17 00:00:00 2001
From: Greg Sabino Mullane <greg@turnstep.com>
Date: Tue, 27 May 2025 07:30:07 -0400
Subject: [PATCH] Allow specific information to be output directly by Postgres.

---
 src/backend/postmaster/postmaster.c           | 196 +++++++++++++++++-
 src/backend/utils/misc/guc_tables.c           |  27 +++
 src/backend/utils/misc/postgresql.conf.sample |   8 +
 src/include/postmaster/postmaster.h           |   4 +
 src/test/postmaster/t/004_exposed.pl          | 105 ++++++++++
 5 files changed, 339 insertions(+), 1 deletion(-)
 create mode 100644 src/test/postmaster/t/004_exposed.pl

diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index 490f7ce..5818f63 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -92,6 +92,7 @@
 #include "access/xlog.h"
 #include "access/xlog_internal.h"
 #include "access/xlogrecovery.h"
+#include "access/xlogutils.h"
 #include "common/file_perm.h"
 #include "common/pg_prng.h"
 #include "lib/ilist.h"
@@ -197,6 +198,9 @@ btmask_contains(BackendTypeMask mask, BackendType t)
 }
 
 
+int			total_expose = 0;
+
+
 BackgroundWorker *MyBgworkerEntry = NULL;
 
 /* The socket number we are listening for connections on */
@@ -247,6 +251,10 @@ char	   *bonjour_name;
 bool		restart_after_crash = true;
 bool		remove_temp_files_after_crash = true;
 
+bool		expose_recovery = false;
+bool		expose_sysid = false;
+bool		expose_version = false;
+
 /*
  * When terminating child processes after fatal errors, like a crash of a
  * child process, we normally send SIGQUIT -- and most other comments in this
@@ -453,6 +461,8 @@ static void StartSysLogger(void);
 static void StartAutovacuumWorker(void);
 static bool StartBackgroundWorker(RegisteredBgWorker *rw);
 static void InitPostmasterDeathWatchHandle(void);
+static bool ExposeInformation(int fd);
+
 
 #ifdef WIN32
 #define WNOHANG 0				/* ignored, so any integer value will do */
@@ -1699,7 +1709,13 @@ ServerLoop(void)
 				ClientSocket s;
 
 				if (AcceptConnection(events[i].fd, &s) == STATUS_OK)
-					BackendStartup(&s);
+				{
+					if ((expose_recovery || expose_sysid || expose_version)
+						&& ExposeInformation(s.sock))
+						total_expose++;
+					else
+						BackendStartup(&s);
+				}
 
 				/* We no longer need the open socket in this process */
 				if (s.sock != PGINVALID_SOCKET)
@@ -4616,3 +4632,181 @@ InitPostmasterDeathWatchHandle(void)
 								 GetLastError())));
 #endif							/* WIN32 */
 }
+
+
+static
+bool
+ExposeInformation(int fd)
+{
+
+/*
+ * ExposeInformation
+ *
+ *
+ * Handle early socket probe before full backend startup.
+ * Responds to small set of predefined endpoints (e.g. GET /info)
+ *
+ * Requires at least one "expose_" GUC to be true.
+ *
+ * Returns true if endpoint is recognized.
+ * Caller is responsible for closing the socket.
+ */
+
+#define EXPOSE_MIN_QUERY 9		/* Shortest possible line: "Get /info" */
+#define EXPOSE_MAX_QUERY 16		/* Longest possible GET line */
+
+/* What information is being returned */
+	typedef enum
+	{
+		EXPOSE_NOTHING,
+		EXPOSE_HEAD_REPLICA,
+		EXPOSE_GET_ALL,
+		EXPOSE_GET_REPLICA,
+		EXPOSE_GET_SYSID,
+		EXPOSE_GET_VERSION,
+	}			ReturnType;
+
+	typedef struct
+	{
+		const char *endpoint;
+		const bool *require;
+		ReturnType	type;
+	}			endpoint_action;
+
+	static endpoint_action endpoint_actions[] =
+	{
+		{
+			"HEAD /replica", &expose_recovery, EXPOSE_HEAD_REPLICA
+		},
+		{
+			"GET /replica", &expose_recovery, EXPOSE_GET_REPLICA
+		},
+		{
+			"GET /sysid", &expose_sysid, EXPOSE_GET_SYSID
+		},
+		{
+			"GET /version", &expose_version, EXPOSE_GET_VERSION
+		},
+		{
+			"GET /info", NULL, EXPOSE_GET_ALL
+		}
+	};
+
+	ssize_t		n;
+	char		buf[EXPOSE_MAX_QUERY + 1];
+	int			type;
+
+	Assert(expose_recovery || expose_sysid || expose_version);
+
+	do
+	{
+		n = recv(fd, buf, EXPOSE_MAX_QUERY, MSG_PEEK);
+	} while (n < 0 && errno == EINTR);
+
+	/*
+	 * Leave as soon as possible if not chance we are interested. We also
+	 * simply return false for n == -1
+	 */
+	if (n < EXPOSE_MIN_QUERY)
+		return false;
+
+	buf[n] = '\0';
+
+	type = EXPOSE_NOTHING;
+	for (int i = 0; i < lengthof(endpoint_actions); i++)
+	{
+		if (
+			strncmp(buf, endpoint_actions[i].endpoint, strlen(endpoint_actions[i].endpoint)) == 0
+			&&
+			(endpoint_actions[i].require == NULL
+			 ||
+			 *(endpoint_actions[i].require)
+			 ))
+		{
+			type = endpoint_actions[i].type;
+			break;
+		}
+	}
+
+	if (type == EXPOSE_NOTHING)
+		return false;
+
+	{
+		static const char http_version[] = "HTTP/1.1";
+		static const char http_type[] = "Content-Type: text/plain";
+		static const char *http_conn = "Connection: close";
+		static const char http_len[] = "Content-Length";
+
+		StringInfoData msg;
+
+		if (type == EXPOSE_HEAD_REPLICA)
+		{
+			/*
+			 * Caller only cares about the HTTP response code, so no content
+			 * needed
+			 */
+
+			initStringInfoExt(&msg, 64);
+
+			appendStringInfo(&msg,
+							 "%s %s\r\n"
+							 "%s\r\n"
+							 "%s\r\n\r\n",
+							 http_version,
+							 (RecoveryInProgress() ? "200 OK" : "503 Service Unavailable"),
+							 http_type,
+							 http_conn
+				);
+		}
+		else
+		{
+			StringInfoData content;
+
+			initStringInfoExt(&content, 64);
+
+			if (expose_recovery && (type == EXPOSE_GET_ALL || type == EXPOSE_GET_REPLICA))
+				appendStringInfo(&content, "%s%d\r\n",
+								 type == EXPOSE_GET_ALL ? "RECOVERY: " : "",
+								 RecoveryInProgress() ? 1 : 0);
+			if (expose_sysid && (type == EXPOSE_GET_ALL || type == EXPOSE_GET_SYSID))
+				appendStringInfo(&content, "%s%lu\r\n",
+								 type == EXPOSE_GET_ALL ? "SYSID: " : "",
+								 GetSystemIdentifier());
+			if (expose_version && (type == EXPOSE_GET_ALL || type == EXPOSE_GET_VERSION))
+				appendStringInfo(&content, "%s%d\r\n",
+								 type == EXPOSE_GET_ALL ? "VERSION: " : "",
+								 PG_VERSION_NUM);
+
+			initStringInfoExt(&msg, 256);
+
+			appendStringInfo(&msg,
+							 "%s 200 OK\r\n"
+							 "%s\r\n"
+							 "%s: %d\r\n"
+							 "%s\r\n\r\n"
+							 "%s",
+							 http_version,
+							 http_type,
+							 http_len, content.len,
+							 http_conn,
+							 content.data
+				);
+
+			pfree(content.data);
+		}
+
+		do
+		{
+			n = send(fd, msg.data, msg.len, 0);
+		} while (n < 0 && errno == EINTR);
+
+		pfree(msg.data);
+
+		if (n < 0)
+			elog(DEBUG1, "could not send to client: %m");
+
+		return true;
+
+	}
+
+}
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 2f8cbd8..38bea71 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -1701,6 +1701,33 @@ struct config_bool ConfigureNamesBool[] =
 		true,
 		NULL, NULL, NULL
 	},
+	{
+		{"expose_recovery", PGC_SIGHUP, CLIENT_CONN_STATEMENT,
+			gettext_noop("Exposes if the server is in recovery without a login."),
+			NULL
+		},
+		&expose_recovery,
+		false,
+		NULL, NULL, NULL
+	},
+	{
+		{"expose_sysid", PGC_SIGHUP, CLIENT_CONN_STATEMENT,
+			gettext_noop("Exposes the system identifier without a login."),
+			NULL
+		},
+		&expose_sysid,
+		false,
+		NULL, NULL, NULL
+	},
+	{
+		{"expose_version", PGC_SIGHUP, CLIENT_CONN_STATEMENT,
+			gettext_noop("Exposes the version without a login."),
+			NULL
+		},
+		&expose_version,
+		false,
+		NULL, NULL, NULL
+	},
 	{
 		{"array_nulls", PGC_USERSET, COMPAT_OPTIONS_PREVIOUS,
 			gettext_noop("Enables input of NULL elements in arrays."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 87ce76b..a16d372 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -91,6 +91,14 @@
 					# disconnection while running queries;
 					# 0 for never
 
+
+# - Expose information -
+
+#expose_recovery = off
+#expose_sysid = off
+#expose_version = off
+
+
 # - Authentication -
 
 #authentication_timeout = 1min		# 1s-600s
diff --git a/src/include/postmaster/postmaster.h b/src/include/postmaster/postmaster.h
index 92497cd..15662ce 100644
--- a/src/include/postmaster/postmaster.h
+++ b/src/include/postmaster/postmaster.h
@@ -70,6 +70,10 @@ extern PGDLLIMPORT bool restart_after_crash;
 extern PGDLLIMPORT bool remove_temp_files_after_crash;
 extern PGDLLIMPORT bool send_abort_for_crash;
 extern PGDLLIMPORT bool send_abort_for_kill;
+extern PGDLLIMPORT bool expose_recovery;
+extern PGDLLIMPORT bool expose_sysid;
+extern PGDLLIMPORT bool expose_version;
+
 
 #ifdef WIN32
 extern PGDLLIMPORT HANDLE PostmasterHandle;
diff --git a/src/test/postmaster/t/004_exposed.pl b/src/test/postmaster/t/004_exposed.pl
new file mode 100644
index 0000000..97cc6b1
--- /dev/null
+++ b/src/test/postmaster/t/004_exposed.pl
@@ -0,0 +1,105 @@
+
+# Copyright (c) 2025, PostgreSQL Global Development Group
+
+# Test pre-fork HTTP endpoint information
+
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use Test::More;
+
+my $node = PostgreSQL::Test::Cluster->new('primary');
+$node->init();
+$node->start;
+
+SKIP:
+{
+	skip "this test requires working raw_connect()"
+	  unless $node->raw_connect_works();
+
+	my ($endpoint, $sock, $reply);
+
+	$endpoint = 'GET /info';
+	$sock = $node->raw_connect();
+	$sock->send($endpoint);
+	$sock->recv($reply, 200);
+	is($reply, '', "nothing is returned by $endpoint");
+	$sock->close();
+
+	$endpoint = 'HEAD /replica';
+	$sock = $node->raw_connect();
+	$sock->send($endpoint);
+	$sock->recv($reply, 200);
+	is($reply, '', "nothing is returned by $endpoint");
+
+	$endpoint = 'GET /replica';
+	$sock = $node->raw_connect();
+	$sock->send($endpoint);
+	$sock->recv($reply, 200);
+	is($reply, '', "nothing is returned by $endpoint");
+	$sock->close();
+
+	$node->append_conf('postgresql.conf', "expose_recovery = on");
+	$node->reload();
+
+	$sock = $node->raw_connect();
+	$sock->send($endpoint);
+	$sock->recv($reply, 200);
+	like($reply, qr[\r\n0\b], "recovery information is returned by $endpoint");
+	$sock->close();
+
+	$endpoint = 'GET /sysid';
+	$sock = $node->raw_connect();
+	$sock->send($endpoint);
+	$sock->recv($reply, 200);
+	is($reply, '', "nothing is returned by $endpoint");
+	$sock->close();
+
+	$node->append_conf('postgresql.conf', "expose_sysid = on");
+	$node->reload();
+
+	$sock = $node->raw_connect();
+	$sock->send($endpoint);
+	$sock->recv($reply, 200);
+	like($reply, qr[\r\n\d{10}], "system identifier is returned by $endpoint");
+	$sock->close();
+
+	$endpoint = 'GET /version';
+	$sock = $node->raw_connect();
+	$sock->send($endpoint);
+	$sock->recv($reply, 200);
+	is($reply, '', "nothing is returned by $endpoint");
+	$sock->close();
+
+	$node->append_conf('postgresql.conf', "expose_version = on");
+	$node->reload();
+
+	$sock = $node->raw_connect();
+	$sock->send($endpoint);
+	$sock->recv($reply, 200);
+	like($reply, qr[\r\n\d{6}\b], "version number is returned by $endpoint");
+	$sock->close();
+
+
+	$endpoint = 'GET /info';
+	$sock = $node->raw_connect();
+	$sock->send($endpoint);
+	$sock->recv($reply, 200);
+	like($reply, qr[RECOVERY: 0\b], "recovery is returned by $endpoint");
+	like($reply, qr[SYSID: \d{10}], "sysid is returned by $endpoint");
+	like($reply, qr[VERSION: \d{6}\b], "version number is returned by $endpoint");
+	$sock->close();
+
+	$endpoint = 'HEAD /replica';
+	$sock = $node->raw_connect();
+	$sock->send($endpoint);
+	$sock->recv($reply, 200);
+	like($reply, qr[503 Service Unavailable], "503 returned by $endpoint");
+	$sock->close();
+
+	## Not working yet:
+	## $node->backup('testbackup1');
+
+}
+
+done_testing();
-- 
2.30.2

