mirror of
				https://github.com/postgres/postgres.git
				synced 2025-10-29 22:49:41 +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);
 | |
| }
 |