diff options
Diffstat (limited to 'contrib/basebackup_to_shell/basebackup_to_shell.c')
-rw-r--r-- | contrib/basebackup_to_shell/basebackup_to_shell.c | 425 |
1 files changed, 425 insertions, 0 deletions
diff --git a/contrib/basebackup_to_shell/basebackup_to_shell.c b/contrib/basebackup_to_shell/basebackup_to_shell.c new file mode 100644 index 0000000..5a26bc6 --- /dev/null +++ b/contrib/basebackup_to_shell/basebackup_to_shell.c @@ -0,0 +1,425 @@ +/*------------------------------------------------------------------------- + * + * basebackup_to_shell.c + * target base backup files to a shell command + * + * Copyright (c) 2016-2022, 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; + +void _PG_init(void); + +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(char *base_command, const char *filename, + char *target_detail) +{ + StringInfoData buf; + 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); + 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); +} |