LCOV - code coverage report
Current view: top level - contrib/basebackup_to_shell - basebackup_to_shell.c (source / functions) Hit Total Coverage
Test: PostgreSQL 17devel Lines: 90 104 86.5 %
Date: 2024-02-28 22:11:00 Functions: 14 14 100.0 %
Legend: Lines: hit not hit

          Line data    Source code
       1             : /*-------------------------------------------------------------------------
       2             :  *
       3             :  * basebackup_to_shell.c
       4             :  *    target base backup files to a shell command
       5             :  *
       6             :  * Copyright (c) 2016-2024, PostgreSQL Global Development Group
       7             :  *
       8             :  *    contrib/basebackup_to_shell/basebackup_to_shell.c
       9             :  *-------------------------------------------------------------------------
      10             :  */
      11             : #include "postgres.h"
      12             : 
      13             : #include "access/xact.h"
      14             : #include "backup/basebackup_target.h"
      15             : #include "common/percentrepl.h"
      16             : #include "miscadmin.h"
      17             : #include "storage/fd.h"
      18             : #include "utils/acl.h"
      19             : #include "utils/guc.h"
      20             : 
      21           2 : PG_MODULE_MAGIC;
      22             : 
      23             : typedef struct bbsink_shell
      24             : {
      25             :     /* Common information for all types of sink. */
      26             :     bbsink      base;
      27             : 
      28             :     /* User-supplied target detail string. */
      29             :     char       *target_detail;
      30             : 
      31             :     /* Shell command pattern being used for this backup. */
      32             :     char       *shell_command;
      33             : 
      34             :     /* The command that is currently running. */
      35             :     char       *current_command;
      36             : 
      37             :     /* Pipe to the running command. */
      38             :     FILE       *pipe;
      39             : } bbsink_shell;
      40             : 
      41             : static void *shell_check_detail(char *target, char *target_detail);
      42             : static bbsink *shell_get_sink(bbsink *next_sink, void *detail_arg);
      43             : 
      44             : static void bbsink_shell_begin_archive(bbsink *sink,
      45             :                                        const char *archive_name);
      46             : static void bbsink_shell_archive_contents(bbsink *sink, size_t len);
      47             : static void bbsink_shell_end_archive(bbsink *sink);
      48             : static void bbsink_shell_begin_manifest(bbsink *sink);
      49             : static void bbsink_shell_manifest_contents(bbsink *sink, size_t len);
      50             : static void bbsink_shell_end_manifest(bbsink *sink);
      51             : 
      52             : static const bbsink_ops bbsink_shell_ops = {
      53             :     .begin_backup = bbsink_forward_begin_backup,
      54             :     .begin_archive = bbsink_shell_begin_archive,
      55             :     .archive_contents = bbsink_shell_archive_contents,
      56             :     .end_archive = bbsink_shell_end_archive,
      57             :     .begin_manifest = bbsink_shell_begin_manifest,
      58             :     .manifest_contents = bbsink_shell_manifest_contents,
      59             :     .end_manifest = bbsink_shell_end_manifest,
      60             :     .end_backup = bbsink_forward_end_backup,
      61             :     .cleanup = bbsink_forward_cleanup
      62             : };
      63             : 
      64             : static char *shell_command = "";
      65             : static char *shell_required_role = "";
      66             : 
      67             : void
      68           2 : _PG_init(void)
      69             : {
      70           2 :     DefineCustomStringVariable("basebackup_to_shell.command",
      71             :                                "Shell command to be executed for each backup file.",
      72             :                                NULL,
      73             :                                &shell_command,
      74             :                                "",
      75             :                                PGC_SIGHUP,
      76             :                                0,
      77             :                                NULL, NULL, NULL);
      78             : 
      79           2 :     DefineCustomStringVariable("basebackup_to_shell.required_role",
      80             :                                "Backup user must be a member of this role to use shell backup target.",
      81             :                                NULL,
      82             :                                &shell_required_role,
      83             :                                "",
      84             :                                PGC_SIGHUP,
      85             :                                0,
      86             :                                NULL, NULL, NULL);
      87             : 
      88           2 :     MarkGUCPrefixReserved("basebackup_to_shell");
      89             : 
      90           2 :     BaseBackupAddTarget("shell", shell_check_detail, shell_get_sink);
      91           2 : }
      92             : 
      93             : /*
      94             :  * We choose to defer sanity checking until shell_get_sink(), and so
      95             :  * just pass the target detail through without doing anything. However, we do
      96             :  * permissions checks here, before any real work has been done.
      97             :  */
      98             : static void *
      99          12 : shell_check_detail(char *target, char *target_detail)
     100             : {
     101          12 :     if (shell_required_role[0] != '\0')
     102             :     {
     103             :         Oid         roleid;
     104             : 
     105           6 :         StartTransactionCommand();
     106           6 :         roleid = get_role_oid(shell_required_role, true);
     107           6 :         if (!has_privs_of_role(GetUserId(), roleid))
     108           2 :             ereport(ERROR,
     109             :                     (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
     110             :                      errmsg("permission denied to use basebackup_to_shell")));
     111           4 :         CommitTransactionCommand();
     112             :     }
     113             : 
     114          10 :     return target_detail;
     115             : }
     116             : 
     117             : /*
     118             :  * Set up a bbsink to implement this base backup target.
     119             :  *
     120             :  * This is also a convenient place to sanity check that a target detail was
     121             :  * given if and only if %d is present.
     122             :  */
     123             : static bbsink *
     124          10 : shell_get_sink(bbsink *next_sink, void *detail_arg)
     125             : {
     126             :     bbsink_shell *sink;
     127          10 :     bool        has_detail_escape = false;
     128             :     char       *c;
     129             : 
     130             :     /*
     131             :      * Set up the bbsink.
     132             :      *
     133             :      * We remember the current value of basebackup_to_shell.shell_command to
     134             :      * be certain that it can't change under us during the backup.
     135             :      */
     136          10 :     sink = palloc0(sizeof(bbsink_shell));
     137          10 :     *((const bbsink_ops **) &sink->base.bbs_ops) = &bbsink_shell_ops;
     138          10 :     sink->base.bbs_next = next_sink;
     139          10 :     sink->target_detail = detail_arg;
     140          10 :     sink->shell_command = pstrdup(shell_command);
     141             : 
     142             :     /* Reject an empty shell command. */
     143          10 :     if (sink->shell_command[0] == '\0')
     144           2 :         ereport(ERROR,
     145             :                 errcode(ERRCODE_INVALID_PARAMETER_VALUE),
     146             :                 errmsg("shell command for backup is not configured"));
     147             : 
     148             :     /* Determine whether the shell command we're using contains %d. */
     149         824 :     for (c = sink->shell_command; *c != '\0'; ++c)
     150             :     {
     151         816 :         if (c[0] == '%' && c[1] != '\0')
     152             :         {
     153          12 :             if (c[1] == 'd')
     154           4 :                 has_detail_escape = true;
     155          12 :             ++c;
     156             :         }
     157             :     }
     158             : 
     159             :     /* There should be a target detail if %d was used, and not otherwise. */
     160           8 :     if (has_detail_escape && sink->target_detail == NULL)
     161           2 :         ereport(ERROR,
     162             :                 (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
     163             :                  errmsg("a target detail is required because the configured command includes %%d"),
     164             :                  errhint("Try \"pg_basebackup --target shell:DETAIL ...\"")));
     165           6 :     else if (!has_detail_escape && sink->target_detail != NULL)
     166           2 :         ereport(ERROR,
     167             :                 (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
     168             :                  errmsg("a target detail is not permitted because the configured command does not include %%d")));
     169             : 
     170             :     /*
     171             :      * Since we're passing the string provided by the user to popen(), it will
     172             :      * be interpreted by the shell, which is a potential security
     173             :      * vulnerability, since the user invoking this module is not necessarily a
     174             :      * superuser. To stay out of trouble, we must disallow any shell
     175             :      * metacharacters here; to be conservative and keep things simple, we
     176             :      * allow only alphanumerics.
     177             :      */
     178           4 :     if (sink->target_detail != NULL)
     179             :     {
     180             :         char       *d;
     181           2 :         bool        scary = false;
     182             : 
     183           8 :         for (d = sink->target_detail; *d != '\0'; ++d)
     184             :         {
     185           6 :             if (*d >= 'a' && *d <= 'z')
     186           6 :                 continue;
     187           0 :             if (*d >= 'A' && *d <= 'Z')
     188           0 :                 continue;
     189           0 :             if (*d >= '0' && *d <= '9')
     190           0 :                 continue;
     191           0 :             scary = true;
     192           0 :             break;
     193             :         }
     194             : 
     195           2 :         if (scary)
     196           0 :             ereport(ERROR,
     197             :                     errcode(ERRCODE_INVALID_PARAMETER_VALUE),
     198             :                     errmsg("target detail must contain only alphanumeric characters"));
     199             :     }
     200             : 
     201           4 :     return &sink->base;
     202             : }
     203             : 
     204             : /*
     205             :  * Construct the exact shell command that we're actually going to run,
     206             :  * making substitutions as appropriate for escape sequences.
     207             :  */
     208             : static char *
     209           8 : shell_construct_command(const char *base_command, const char *filename,
     210             :                         const char *target_detail)
     211             : {
     212           8 :     return replace_percent_placeholders(base_command, "basebackup_to_shell.command",
     213             :                                         "df", target_detail, filename);
     214             : }
     215             : 
     216             : /*
     217             :  * Finish executing the shell command once all data has been written.
     218             :  */
     219             : static void
     220           8 : shell_finish_command(bbsink_shell *sink)
     221             : {
     222             :     int         pclose_rc;
     223             : 
     224             :     /* There should be a command running. */
     225             :     Assert(sink->current_command != NULL);
     226             :     Assert(sink->pipe != NULL);
     227             : 
     228             :     /* Close down the pipe we opened. */
     229           8 :     pclose_rc = ClosePipeStream(sink->pipe);
     230           8 :     if (pclose_rc == -1)
     231           0 :         ereport(ERROR,
     232             :                 (errcode_for_file_access(),
     233             :                  errmsg("could not close pipe to external command: %m")));
     234           8 :     else if (pclose_rc != 0)
     235             :     {
     236           0 :         ereport(ERROR,
     237             :                 (errcode(ERRCODE_EXTERNAL_ROUTINE_EXCEPTION),
     238             :                  errmsg("shell command \"%s\" failed",
     239             :                         sink->current_command),
     240             :                  errdetail_internal("%s", wait_result_to_str(pclose_rc))));
     241             :     }
     242             : 
     243             :     /* Clean up. */
     244           8 :     sink->pipe = NULL;
     245           8 :     pfree(sink->current_command);
     246           8 :     sink->current_command = NULL;
     247           8 : }
     248             : 
     249             : /*
     250             :  * Start up the shell command, substituting %f in for the current filename.
     251             :  */
     252             : static void
     253           8 : shell_run_command(bbsink_shell *sink, const char *filename)
     254             : {
     255             :     /* There should not be anything already running. */
     256             :     Assert(sink->current_command == NULL);
     257             :     Assert(sink->pipe == NULL);
     258             : 
     259             :     /* Construct a suitable command. */
     260          16 :     sink->current_command = shell_construct_command(sink->shell_command,
     261             :                                                     filename,
     262           8 :                                                     sink->target_detail);
     263             : 
     264             :     /* Run it. */
     265           8 :     sink->pipe = OpenPipeStream(sink->current_command, PG_BINARY_W);
     266           8 :     if (sink->pipe == NULL)
     267           0 :         ereport(ERROR,
     268             :                 (errcode_for_file_access(),
     269             :                  errmsg("could not execute command \"%s\": %m",
     270             :                         sink->current_command)));
     271           8 : }
     272             : 
     273             : /*
     274             :  * Send accumulated data to the running shell command.
     275             :  */
     276             : static void
     277       10764 : shell_send_data(bbsink_shell *sink, size_t len)
     278             : {
     279             :     /* There should be a command running. */
     280             :     Assert(sink->current_command != NULL);
     281             :     Assert(sink->pipe != NULL);
     282             : 
     283             :     /* Try to write the data. */
     284       21528 :     if (fwrite(sink->base.bbs_buffer, len, 1, sink->pipe) != 1 ||
     285       10764 :         ferror(sink->pipe))
     286             :     {
     287           0 :         if (errno == EPIPE)
     288             :         {
     289             :             /*
     290             :              * The error we're about to throw would shut down the command
     291             :              * anyway, but we may get a more meaningful error message by doing
     292             :              * this. If not, we'll fall through to the generic error below.
     293             :              */
     294           0 :             shell_finish_command(sink);
     295           0 :             errno = EPIPE;
     296             :         }
     297           0 :         ereport(ERROR,
     298             :                 (errcode_for_file_access(),
     299             :                  errmsg("could not write to shell backup program: %m")));
     300             :     }
     301       10764 : }
     302             : 
     303             : /*
     304             :  * At start of archive, start up the shell command and forward to next sink.
     305             :  */
     306             : static void
     307           4 : bbsink_shell_begin_archive(bbsink *sink, const char *archive_name)
     308             : {
     309           4 :     bbsink_shell *mysink = (bbsink_shell *) sink;
     310             : 
     311           4 :     shell_run_command(mysink, archive_name);
     312           4 :     bbsink_forward_begin_archive(sink, archive_name);
     313           4 : }
     314             : 
     315             : /*
     316             :  * Send archive contents to command's stdin and forward to next sink.
     317             :  */
     318             : static void
     319       10744 : bbsink_shell_archive_contents(bbsink *sink, size_t len)
     320             : {
     321       10744 :     bbsink_shell *mysink = (bbsink_shell *) sink;
     322             : 
     323       10744 :     shell_send_data(mysink, len);
     324       10744 :     bbsink_forward_archive_contents(sink, len);
     325       10744 : }
     326             : 
     327             : /*
     328             :  * At end of archive, shut down the shell command and forward to next sink.
     329             :  */
     330             : static void
     331           4 : bbsink_shell_end_archive(bbsink *sink)
     332             : {
     333           4 :     bbsink_shell *mysink = (bbsink_shell *) sink;
     334             : 
     335           4 :     shell_finish_command(mysink);
     336           4 :     bbsink_forward_end_archive(sink);
     337           4 : }
     338             : 
     339             : /*
     340             :  * At start of manifest, start up the shell command and forward to next sink.
     341             :  */
     342             : static void
     343           4 : bbsink_shell_begin_manifest(bbsink *sink)
     344             : {
     345           4 :     bbsink_shell *mysink = (bbsink_shell *) sink;
     346             : 
     347           4 :     shell_run_command(mysink, "backup_manifest");
     348           4 :     bbsink_forward_begin_manifest(sink);
     349           4 : }
     350             : 
     351             : /*
     352             :  * Send manifest contents to command's stdin and forward to next sink.
     353             :  */
     354             : static void
     355          20 : bbsink_shell_manifest_contents(bbsink *sink, size_t len)
     356             : {
     357          20 :     bbsink_shell *mysink = (bbsink_shell *) sink;
     358             : 
     359          20 :     shell_send_data(mysink, len);
     360          20 :     bbsink_forward_manifest_contents(sink, len);
     361          20 : }
     362             : 
     363             : /*
     364             :  * At end of manifest, shut down the shell command and forward to next sink.
     365             :  */
     366             : static void
     367           4 : bbsink_shell_end_manifest(bbsink *sink)
     368             : {
     369           4 :     bbsink_shell *mysink = (bbsink_shell *) sink;
     370             : 
     371           4 :     shell_finish_command(mysink);
     372           4 :     bbsink_forward_end_manifest(sink);
     373           4 : }

Generated by: LCOV version 1.14