From fd58612f74a308aed3f0d3caf79250c0bfce2068 Mon Sep 17 00:00:00 2001 From: Greg Sabino Mullane 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