mirror of
				https://github.com/postgres/postgres.git
				synced 2025-11-03 09:13:20 +03:00 
			
		
		
		
	It seems potentially useful to label our shared libraries with version information, now that a facility exists for retrieving that. This patch labels them with the PG_VERSION string. There was some discussion about using semantic versioning conventions, but that doesn't seem terribly helpful for modules with no SQL-level presence; and for those that do have SQL objects, we typically expect them to support multiple revisions of the SQL definitions, so it'd still not be very helpful. I did not label any of src/test/modules/. It seems unnecessary since we don't install those, and besides there ought to be someplace that still provides test coverage for the original PG_MODULE_MAGIC macro. Author: Tom Lane <tgl@sss.pgh.pa.us> Discussion: https://postgr.es/m/dd4d1b59-d0fe-49d5-b28f-1e463b68fa32@gmail.com
		
			
				
	
	
		
			377 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
			
		
		
	
	
			377 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
/*-------------------------------------------------------------------------
 | 
						|
 *
 | 
						|
 * basebackup_to_shell.c
 | 
						|
 *	  target base backup files to a shell command
 | 
						|
 *
 | 
						|
 * Copyright (c) 2016-2025, PostgreSQL Global Development Group
 | 
						|
 *
 | 
						|
 *	  contrib/basebackup_to_shell/basebackup_to_shell.c
 | 
						|
 *-------------------------------------------------------------------------
 | 
						|
 */
 | 
						|
#include "postgres.h"
 | 
						|
 | 
						|
#include "access/xact.h"
 | 
						|
#include "backup/basebackup_target.h"
 | 
						|
#include "common/percentrepl.h"
 | 
						|
#include "miscadmin.h"
 | 
						|
#include "storage/fd.h"
 | 
						|
#include "utils/acl.h"
 | 
						|
#include "utils/guc.h"
 | 
						|
 | 
						|
PG_MODULE_MAGIC_EXT(
 | 
						|
					.name = "basebackup_to_shell",
 | 
						|
					.version = PG_VERSION
 | 
						|
);
 | 
						|
 | 
						|
typedef struct bbsink_shell
 | 
						|
{
 | 
						|
	/* Common information for all types of sink. */
 | 
						|
	bbsink		base;
 | 
						|
 | 
						|
	/* User-supplied target detail string. */
 | 
						|
	char	   *target_detail;
 | 
						|
 | 
						|
	/* Shell command pattern being used for this backup. */
 | 
						|
	char	   *shell_command;
 | 
						|
 | 
						|
	/* The command that is currently running. */
 | 
						|
	char	   *current_command;
 | 
						|
 | 
						|
	/* Pipe to the running command. */
 | 
						|
	FILE	   *pipe;
 | 
						|
} bbsink_shell;
 | 
						|
 | 
						|
static void *shell_check_detail(char *target, char *target_detail);
 | 
						|
static bbsink *shell_get_sink(bbsink *next_sink, void *detail_arg);
 | 
						|
 | 
						|
static void bbsink_shell_begin_archive(bbsink *sink,
 | 
						|
									   const char *archive_name);
 | 
						|
static void bbsink_shell_archive_contents(bbsink *sink, size_t len);
 | 
						|
static void bbsink_shell_end_archive(bbsink *sink);
 | 
						|
static void bbsink_shell_begin_manifest(bbsink *sink);
 | 
						|
static void bbsink_shell_manifest_contents(bbsink *sink, size_t len);
 | 
						|
static void bbsink_shell_end_manifest(bbsink *sink);
 | 
						|
 | 
						|
static const bbsink_ops bbsink_shell_ops = {
 | 
						|
	.begin_backup = bbsink_forward_begin_backup,
 | 
						|
	.begin_archive = bbsink_shell_begin_archive,
 | 
						|
	.archive_contents = bbsink_shell_archive_contents,
 | 
						|
	.end_archive = bbsink_shell_end_archive,
 | 
						|
	.begin_manifest = bbsink_shell_begin_manifest,
 | 
						|
	.manifest_contents = bbsink_shell_manifest_contents,
 | 
						|
	.end_manifest = bbsink_shell_end_manifest,
 | 
						|
	.end_backup = bbsink_forward_end_backup,
 | 
						|
	.cleanup = bbsink_forward_cleanup
 | 
						|
};
 | 
						|
 | 
						|
static char *shell_command = "";
 | 
						|
static char *shell_required_role = "";
 | 
						|
 | 
						|
void
 | 
						|
_PG_init(void)
 | 
						|
{
 | 
						|
	DefineCustomStringVariable("basebackup_to_shell.command",
 | 
						|
							   "Shell command to be executed for each backup file.",
 | 
						|
							   NULL,
 | 
						|
							   &shell_command,
 | 
						|
							   "",
 | 
						|
							   PGC_SIGHUP,
 | 
						|
							   0,
 | 
						|
							   NULL, NULL, NULL);
 | 
						|
 | 
						|
	DefineCustomStringVariable("basebackup_to_shell.required_role",
 | 
						|
							   "Backup user must be a member of this role to use shell backup target.",
 | 
						|
							   NULL,
 | 
						|
							   &shell_required_role,
 | 
						|
							   "",
 | 
						|
							   PGC_SIGHUP,
 | 
						|
							   0,
 | 
						|
							   NULL, NULL, NULL);
 | 
						|
 | 
						|
	MarkGUCPrefixReserved("basebackup_to_shell");
 | 
						|
 | 
						|
	BaseBackupAddTarget("shell", shell_check_detail, shell_get_sink);
 | 
						|
}
 | 
						|
 | 
						|
/*
 | 
						|
 * We choose to defer sanity checking until shell_get_sink(), and so
 | 
						|
 * just pass the target detail through without doing anything. However, we do
 | 
						|
 * permissions checks here, before any real work has been done.
 | 
						|
 */
 | 
						|
static void *
 | 
						|
shell_check_detail(char *target, char *target_detail)
 | 
						|
{
 | 
						|
	if (shell_required_role[0] != '\0')
 | 
						|
	{
 | 
						|
		Oid			roleid;
 | 
						|
 | 
						|
		StartTransactionCommand();
 | 
						|
		roleid = get_role_oid(shell_required_role, true);
 | 
						|
		if (!has_privs_of_role(GetUserId(), roleid))
 | 
						|
			ereport(ERROR,
 | 
						|
					(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
 | 
						|
					 errmsg("permission denied to use basebackup_to_shell")));
 | 
						|
		CommitTransactionCommand();
 | 
						|
	}
 | 
						|
 | 
						|
	return target_detail;
 | 
						|
}
 | 
						|
 | 
						|
/*
 | 
						|
 * Set up a bbsink to implement this base backup target.
 | 
						|
 *
 | 
						|
 * This is also a convenient place to sanity check that a target detail was
 | 
						|
 * given if and only if %d is present.
 | 
						|
 */
 | 
						|
static bbsink *
 | 
						|
shell_get_sink(bbsink *next_sink, void *detail_arg)
 | 
						|
{
 | 
						|
	bbsink_shell *sink;
 | 
						|
	bool		has_detail_escape = false;
 | 
						|
	char	   *c;
 | 
						|
 | 
						|
	/*
 | 
						|
	 * Set up the bbsink.
 | 
						|
	 *
 | 
						|
	 * We remember the current value of basebackup_to_shell.shell_command to
 | 
						|
	 * be certain that it can't change under us during the backup.
 | 
						|
	 */
 | 
						|
	sink = palloc0(sizeof(bbsink_shell));
 | 
						|
	*((const bbsink_ops **) &sink->base.bbs_ops) = &bbsink_shell_ops;
 | 
						|
	sink->base.bbs_next = next_sink;
 | 
						|
	sink->target_detail = detail_arg;
 | 
						|
	sink->shell_command = pstrdup(shell_command);
 | 
						|
 | 
						|
	/* Reject an empty shell command. */
 | 
						|
	if (sink->shell_command[0] == '\0')
 | 
						|
		ereport(ERROR,
 | 
						|
				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 | 
						|
				errmsg("shell command for backup is not configured"));
 | 
						|
 | 
						|
	/* Determine whether the shell command we're using contains %d. */
 | 
						|
	for (c = sink->shell_command; *c != '\0'; ++c)
 | 
						|
	{
 | 
						|
		if (c[0] == '%' && c[1] != '\0')
 | 
						|
		{
 | 
						|
			if (c[1] == 'd')
 | 
						|
				has_detail_escape = true;
 | 
						|
			++c;
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	/* There should be a target detail if %d was used, and not otherwise. */
 | 
						|
	if (has_detail_escape && sink->target_detail == NULL)
 | 
						|
		ereport(ERROR,
 | 
						|
				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 | 
						|
				 errmsg("a target detail is required because the configured command includes %%d"),
 | 
						|
				 errhint("Try \"pg_basebackup --target shell:DETAIL ...\"")));
 | 
						|
	else if (!has_detail_escape && sink->target_detail != NULL)
 | 
						|
		ereport(ERROR,
 | 
						|
				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 | 
						|
				 errmsg("a target detail is not permitted because the configured command does not include %%d")));
 | 
						|
 | 
						|
	/*
 | 
						|
	 * Since we're passing the string provided by the user to popen(), it will
 | 
						|
	 * be interpreted by the shell, which is a potential security
 | 
						|
	 * vulnerability, since the user invoking this module is not necessarily a
 | 
						|
	 * superuser. To stay out of trouble, we must disallow any shell
 | 
						|
	 * metacharacters here; to be conservative and keep things simple, we
 | 
						|
	 * allow only alphanumerics.
 | 
						|
	 */
 | 
						|
	if (sink->target_detail != NULL)
 | 
						|
	{
 | 
						|
		char	   *d;
 | 
						|
		bool		scary = false;
 | 
						|
 | 
						|
		for (d = sink->target_detail; *d != '\0'; ++d)
 | 
						|
		{
 | 
						|
			if (*d >= 'a' && *d <= 'z')
 | 
						|
				continue;
 | 
						|
			if (*d >= 'A' && *d <= 'Z')
 | 
						|
				continue;
 | 
						|
			if (*d >= '0' && *d <= '9')
 | 
						|
				continue;
 | 
						|
			scary = true;
 | 
						|
			break;
 | 
						|
		}
 | 
						|
 | 
						|
		if (scary)
 | 
						|
			ereport(ERROR,
 | 
						|
					errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 | 
						|
					errmsg("target detail must contain only alphanumeric characters"));
 | 
						|
	}
 | 
						|
 | 
						|
	return &sink->base;
 | 
						|
}
 | 
						|
 | 
						|
/*
 | 
						|
 * Construct the exact shell command that we're actually going to run,
 | 
						|
 * making substitutions as appropriate for escape sequences.
 | 
						|
 */
 | 
						|
static char *
 | 
						|
shell_construct_command(const char *base_command, const char *filename,
 | 
						|
						const char *target_detail)
 | 
						|
{
 | 
						|
	return replace_percent_placeholders(base_command, "basebackup_to_shell.command",
 | 
						|
										"df", target_detail, filename);
 | 
						|
}
 | 
						|
 | 
						|
/*
 | 
						|
 * Finish executing the shell command once all data has been written.
 | 
						|
 */
 | 
						|
static void
 | 
						|
shell_finish_command(bbsink_shell *sink)
 | 
						|
{
 | 
						|
	int			pclose_rc;
 | 
						|
 | 
						|
	/* There should be a command running. */
 | 
						|
	Assert(sink->current_command != NULL);
 | 
						|
	Assert(sink->pipe != NULL);
 | 
						|
 | 
						|
	/* Close down the pipe we opened. */
 | 
						|
	pclose_rc = ClosePipeStream(sink->pipe);
 | 
						|
	if (pclose_rc == -1)
 | 
						|
		ereport(ERROR,
 | 
						|
				(errcode_for_file_access(),
 | 
						|
				 errmsg("could not close pipe to external command: %m")));
 | 
						|
	else if (pclose_rc != 0)
 | 
						|
	{
 | 
						|
		ereport(ERROR,
 | 
						|
				(errcode(ERRCODE_EXTERNAL_ROUTINE_EXCEPTION),
 | 
						|
				 errmsg("shell command \"%s\" failed",
 | 
						|
						sink->current_command),
 | 
						|
				 errdetail_internal("%s", wait_result_to_str(pclose_rc))));
 | 
						|
	}
 | 
						|
 | 
						|
	/* Clean up. */
 | 
						|
	sink->pipe = NULL;
 | 
						|
	pfree(sink->current_command);
 | 
						|
	sink->current_command = NULL;
 | 
						|
}
 | 
						|
 | 
						|
/*
 | 
						|
 * Start up the shell command, substituting %f in for the current filename.
 | 
						|
 */
 | 
						|
static void
 | 
						|
shell_run_command(bbsink_shell *sink, const char *filename)
 | 
						|
{
 | 
						|
	/* There should not be anything already running. */
 | 
						|
	Assert(sink->current_command == NULL);
 | 
						|
	Assert(sink->pipe == NULL);
 | 
						|
 | 
						|
	/* Construct a suitable command. */
 | 
						|
	sink->current_command = shell_construct_command(sink->shell_command,
 | 
						|
													filename,
 | 
						|
													sink->target_detail);
 | 
						|
 | 
						|
	/* Run it. */
 | 
						|
	sink->pipe = OpenPipeStream(sink->current_command, PG_BINARY_W);
 | 
						|
	if (sink->pipe == NULL)
 | 
						|
		ereport(ERROR,
 | 
						|
				(errcode_for_file_access(),
 | 
						|
				 errmsg("could not execute command \"%s\": %m",
 | 
						|
						sink->current_command)));
 | 
						|
}
 | 
						|
 | 
						|
/*
 | 
						|
 * Send accumulated data to the running shell command.
 | 
						|
 */
 | 
						|
static void
 | 
						|
shell_send_data(bbsink_shell *sink, size_t len)
 | 
						|
{
 | 
						|
	/* There should be a command running. */
 | 
						|
	Assert(sink->current_command != NULL);
 | 
						|
	Assert(sink->pipe != NULL);
 | 
						|
 | 
						|
	/* Try to write the data. */
 | 
						|
	if (fwrite(sink->base.bbs_buffer, len, 1, sink->pipe) != 1 ||
 | 
						|
		ferror(sink->pipe))
 | 
						|
	{
 | 
						|
		if (errno == EPIPE)
 | 
						|
		{
 | 
						|
			/*
 | 
						|
			 * The error we're about to throw would shut down the command
 | 
						|
			 * anyway, but we may get a more meaningful error message by doing
 | 
						|
			 * this. If not, we'll fall through to the generic error below.
 | 
						|
			 */
 | 
						|
			shell_finish_command(sink);
 | 
						|
			errno = EPIPE;
 | 
						|
		}
 | 
						|
		ereport(ERROR,
 | 
						|
				(errcode_for_file_access(),
 | 
						|
				 errmsg("could not write to shell backup program: %m")));
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
/*
 | 
						|
 * At start of archive, start up the shell command and forward to next sink.
 | 
						|
 */
 | 
						|
static void
 | 
						|
bbsink_shell_begin_archive(bbsink *sink, const char *archive_name)
 | 
						|
{
 | 
						|
	bbsink_shell *mysink = (bbsink_shell *) sink;
 | 
						|
 | 
						|
	shell_run_command(mysink, archive_name);
 | 
						|
	bbsink_forward_begin_archive(sink, archive_name);
 | 
						|
}
 | 
						|
 | 
						|
/*
 | 
						|
 * Send archive contents to command's stdin and forward to next sink.
 | 
						|
 */
 | 
						|
static void
 | 
						|
bbsink_shell_archive_contents(bbsink *sink, size_t len)
 | 
						|
{
 | 
						|
	bbsink_shell *mysink = (bbsink_shell *) sink;
 | 
						|
 | 
						|
	shell_send_data(mysink, len);
 | 
						|
	bbsink_forward_archive_contents(sink, len);
 | 
						|
}
 | 
						|
 | 
						|
/*
 | 
						|
 * At end of archive, shut down the shell command and forward to next sink.
 | 
						|
 */
 | 
						|
static void
 | 
						|
bbsink_shell_end_archive(bbsink *sink)
 | 
						|
{
 | 
						|
	bbsink_shell *mysink = (bbsink_shell *) sink;
 | 
						|
 | 
						|
	shell_finish_command(mysink);
 | 
						|
	bbsink_forward_end_archive(sink);
 | 
						|
}
 | 
						|
 | 
						|
/*
 | 
						|
 * At start of manifest, start up the shell command and forward to next sink.
 | 
						|
 */
 | 
						|
static void
 | 
						|
bbsink_shell_begin_manifest(bbsink *sink)
 | 
						|
{
 | 
						|
	bbsink_shell *mysink = (bbsink_shell *) sink;
 | 
						|
 | 
						|
	shell_run_command(mysink, "backup_manifest");
 | 
						|
	bbsink_forward_begin_manifest(sink);
 | 
						|
}
 | 
						|
 | 
						|
/*
 | 
						|
 * Send manifest contents to command's stdin and forward to next sink.
 | 
						|
 */
 | 
						|
static void
 | 
						|
bbsink_shell_manifest_contents(bbsink *sink, size_t len)
 | 
						|
{
 | 
						|
	bbsink_shell *mysink = (bbsink_shell *) sink;
 | 
						|
 | 
						|
	shell_send_data(mysink, len);
 | 
						|
	bbsink_forward_manifest_contents(sink, len);
 | 
						|
}
 | 
						|
 | 
						|
/*
 | 
						|
 * At end of manifest, shut down the shell command and forward to next sink.
 | 
						|
 */
 | 
						|
static void
 | 
						|
bbsink_shell_end_manifest(bbsink *sink)
 | 
						|
{
 | 
						|
	bbsink_shell *mysink = (bbsink_shell *) sink;
 | 
						|
 | 
						|
	shell_finish_command(mysink);
 | 
						|
	bbsink_forward_end_manifest(sink);
 | 
						|
}
 |