summaryrefslogtreecommitdiffstats
path: root/contrib/basebackup_to_shell/basebackup_to_shell.c
diff options
context:
space:
mode:
Diffstat (limited to 'contrib/basebackup_to_shell/basebackup_to_shell.c')
-rw-r--r--contrib/basebackup_to_shell/basebackup_to_shell.c425
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);
+}