From dab6ea7fd21aafbf5f87b163903e54f129f45e0b Mon Sep 17 00:00:00 2001 From: Greg Sabino Mullane Date: Sat, 31 May 2025 00:38:26 -0400 Subject: [PATCH] Allow specific information to be output directly by Postgres. --- doc/src/sgml/config.sgml | 78 +++++++ src/backend/tcop/backend_startup.c | 191 ++++++++++++++++++ src/backend/utils/misc/guc_tables.c | 27 +++ src/backend/utils/misc/postgresql.conf.sample | 8 + src/include/postmaster/postmaster.h | 4 + 5 files changed, 308 insertions(+) diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index f4a0191c55b..5774d78740f 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -1077,6 +1077,84 @@ include_dir 'conf.d' + + Expose Settings + + + + + expose_recovery (boolean) + + expose_recovery configuration parameter + + + + + Enables reporting if the server is in recovery mode without requiring + an authenticated login. Clients can send the string GET /replica + and will receive a 1 or 0. This is equivalent to logging in and running + SELECT pg_is_in_recovery(). A client can also send the + string HEAD /replica which will solely return an HTTP literal: + 200 if the server is in recovery, 503 if not. + (This allows a drop-in replacement to the same Patroni functionality) + Finally, a client can issue GET /info and receive the string + RECOVERY: followed by a 1 or 0. + + + + + + expose_sysid (boolean) + + expose_sysid configuration parameter + + + + + Enables reporting the system identifier of the cluster without requiring + an authenticated login. Clients can send the string GET /sysid + and will receive the numeric system identifier. This is a unique number generated + by each cluster when initdb is run. + + + A client can issue GET /info and receive the string + SYSID: followed by the numeric system identifier. + + + This feature is useful for determining if the server is the same server as previously + encountered. Note than primary and replica servers will share the same system + identifier. + + + + + + expose_version (boolean) + + expose_version configuration parameter + + + + + Enables reporting the numeric version of the Postgres cluster without requiring + an authenticated login. Clients can send the string GET /version + and will receive an integer representing the version. + + + A client can issue GET /info and receive the string + VERSION: followed by the numeric version. + + + This is particularly useful for non-Postgres systems (esp. security scanners) that + need a way to easily determine the version of Postgres in use without requiring + a Postgres client - or without needing any knowledge of the Postgres protocol at all. + + + + + + + Authentication diff --git a/src/backend/tcop/backend_startup.c b/src/backend/tcop/backend_startup.c index a7d1fec981f..ce8a5087ec6 100644 --- a/src/backend/tcop/backend_startup.c +++ b/src/backend/tcop/backend_startup.c @@ -46,6 +46,10 @@ bool Trace_connection_negotiation = false; uint32 log_connections = 0; char *log_connections_string = NULL; +bool expose_recovery = false; +bool expose_sysid = false; +bool expose_version = false; + /* Other globals */ @@ -65,6 +69,7 @@ static void SendNegotiateProtocolVersion(List *unrecognized_protocol_options); static void process_startup_packet_die(SIGNAL_ARGS); static void StartupPacketTimeoutHandler(void); static bool validate_log_connections_options(List *elemlist, uint32 *flags); +static bool ExposeInformation(pgsocket fd); /* * Entry point for a new backend process. @@ -148,6 +153,15 @@ BackendInitialize(ClientSocket *client_sock, CAC_state cac) StringInfoData ps_data; MemoryContext oldcontext; + /* + * Possibly scan for a simple GET / HEAD request. If this is detected and + * handled, we are done and can immediately exit + */ + if ((expose_recovery || expose_sysid || expose_version) + && ExposeInformation(client_sock->sock)) + proc_exit(0); + /* Should we do exit(0) here, despite the warnings in ipc.c? */ + /* Tell fd.c about the long-lived FD associated with the client_sock */ ReserveExternalFD(); @@ -1113,3 +1127,180 @@ assign_log_connections(const char *newval, void *extra) { log_connections = *((int *) extra); } + + +static +bool +ExposeInformation(pgsocket 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. + */ + +#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 2f8cbd86759..38bea719011 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 87ce76b18f4..a16d3726824 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 92497cd6a0f..15662ce0059 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; -- 2.30.2