postgresql/contrib/basebackup_to_shell/basebackup_to_shell.c

419 lines
11 KiB
C

/*-------------------------------------------------------------------------
*
* basebackup_to_shell.c
* target base backup files to a shell command
*
* Copyright (c) 2016-2023, 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 "miscadmin.h"
#include "storage/fd.h"
#include "utils/acl.h"
#include "utils/guc.h"
PG_MODULE_MAGIC;
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)
{
StringInfoData buf;
const char *c;
initStringInfo(&buf);
for (c = base_command; *c != '\0'; ++c)
{
/* Anything other than '%' is copied verbatim. */
if (*c != '%')
{
appendStringInfoChar(&buf, *c);
continue;
}
/* Any time we see '%' we eat the following character as well. */
++c;
/*
* The following character determines what we insert here, or may
* cause us to throw an error.
*/
if (*c == '%')
{
/* '%%' is replaced by a single '%' */
appendStringInfoChar(&buf, '%');
}
else if (*c == 'f')
{
/* '%f' is replaced by the filename */
appendStringInfoString(&buf, filename);
}
else if (*c == 'd')
{
/* '%d' is replaced by the target detail */
appendStringInfoString(&buf, target_detail);
}
else if (*c == '\0')
{
/* Incomplete escape sequence, expected a character afterward */
ereport(ERROR,
errcode(ERRCODE_SYNTAX_ERROR),
errmsg("shell command ends unexpectedly after escape character \"%%\""));
}
else
{
/* Unknown escape sequence */
ereport(ERROR,
errcode(ERRCODE_SYNTAX_ERROR),
errmsg("shell command contains unexpected escape sequence \"%c\"",
*c));
}
}
return buf.data;
}
/*
* 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);
}
/*
* 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);
}