Age Owner TLA 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-2023, 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 :
390 rhaas 21 GIC 1 : PG_MODULE_MAGIC;
390 rhaas 22 ECB :
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
390 rhaas 68 GIC 1 : _PG_init(void)
390 rhaas 69 ECB : {
390 rhaas 70 GIC 1 : 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);
390 rhaas 78 ECB :
390 rhaas 79 GIC 1 : 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);
390 rhaas 87 ECB :
342 michael 88 GIC 1 : MarkGUCPrefixReserved("basebackup_to_shell");
342 michael 89 ECB :
390 rhaas 90 CBC 1 : BaseBackupAddTarget("shell", shell_check_detail, shell_get_sink);
390 rhaas 91 GIC 1 : }
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 : */
390 rhaas 98 ECB : static void *
390 rhaas 99 GIC 6 : shell_check_detail(char *target, char *target_detail)
390 rhaas 100 ECB : {
390 rhaas 101 GIC 6 : if (shell_required_role[0] != '\0')
102 : {
103 : Oid roleid;
390 rhaas 104 ECB :
390 rhaas 105 CBC 3 : StartTransactionCommand();
106 3 : roleid = get_role_oid(shell_required_role, true);
372 mail 107 3 : if (!has_privs_of_role(GetUserId(), roleid))
390 rhaas 108 GIC 1 : ereport(ERROR,
109 : (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
390 rhaas 110 ECB : errmsg("permission denied to use basebackup_to_shell")));
390 rhaas 111 GIC 2 : CommitTransactionCommand();
112 : }
390 rhaas 113 ECB :
390 rhaas 114 GIC 5 : 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 : */
390 rhaas 123 ECB : static bbsink *
390 rhaas 124 GIC 5 : shell_get_sink(bbsink *next_sink, void *detail_arg)
125 : {
390 rhaas 126 ECB : bbsink_shell *sink;
332 tgl 127 GIC 5 : 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.
390 rhaas 135 ECB : */
390 rhaas 136 CBC 5 : sink = palloc0(sizeof(bbsink_shell));
137 5 : *((const bbsink_ops **) &sink->base.bbs_ops) = &bbsink_shell_ops;
138 5 : sink->base.bbs_next = next_sink;
139 5 : sink->target_detail = detail_arg;
390 rhaas 140 GIC 5 : sink->shell_command = pstrdup(shell_command);
141 :
390 rhaas 142 ECB : /* Reject an empty shell command. */
390 rhaas 143 CBC 5 : if (sink->shell_command[0] == '\0')
390 rhaas 144 GIC 1 : ereport(ERROR,
145 : errcode(ERRCODE_INVALID_PARAMETER_VALUE),
146 : errmsg("shell command for backup is not configured"));
147 :
390 rhaas 148 ECB : /* Determine whether the shell command we're using contains %d. */
390 rhaas 149 GIC 396 : for (c = sink->shell_command; *c != '\0'; ++c)
390 rhaas 150 ECB : {
390 rhaas 151 GIC 392 : if (c[0] == '%' && c[1] != '\0')
390 rhaas 152 ECB : {
390 rhaas 153 CBC 6 : if (c[1] == 'd')
154 2 : has_detail_escape = true;
390 rhaas 155 GIC 6 : ++c;
156 : }
157 : }
158 :
390 rhaas 159 ECB : /* There should be a target detail if %d was used, and not otherwise. */
390 rhaas 160 CBC 4 : if (has_detail_escape && sink->target_detail == NULL)
390 rhaas 161 GIC 1 : ereport(ERROR,
162 : (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
163 : errmsg("a target detail is required because the configured command includes %%d"),
390 rhaas 164 ECB : errhint("Try \"pg_basebackup --target shell:DETAIL ...\"")));
390 rhaas 165 CBC 3 : else if (!has_detail_escape && sink->target_detail != NULL)
390 rhaas 166 GIC 1 : 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.
390 rhaas 177 ECB : */
390 rhaas 178 GIC 2 : if (sink->target_detail != NULL)
179 : {
332 tgl 180 ECB : char *d;
332 tgl 181 GIC 1 : bool scary = false;
390 rhaas 182 ECB :
390 rhaas 183 GIC 4 : for (d = sink->target_detail; *d != '\0'; ++d)
390 rhaas 184 ECB : {
390 rhaas 185 CBC 3 : if (*d >= 'a' && *d <= 'z')
390 rhaas 186 GBC 3 : continue;
390 rhaas 187 UBC 0 : if (*d >= 'A' && *d <= 'Z')
188 0 : continue;
189 0 : if (*d >= '0' && *d <= '9')
190 0 : continue;
191 0 : scary = true;
390 rhaas 192 UIC 0 : break;
193 : }
390 rhaas 194 ECB :
390 rhaas 195 GBC 1 : if (scary)
390 rhaas 196 UIC 0 : ereport(ERROR,
197 : errcode(ERRCODE_INVALID_PARAMETER_VALUE),
198 : errmsg("target detail must contain only alphanumeric characters"));
199 : }
390 rhaas 200 ECB :
390 rhaas 201 GIC 2 : 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 : */
390 rhaas 208 ECB : static char *
117 peter 209 GNC 4 : shell_construct_command(const char *base_command, const char *filename,
210 : const char *target_detail)
390 rhaas 211 ECB : {
88 peter 212 GNC 4 : return replace_percent_placeholders(base_command, "basebackup_to_shell.command",
213 : "df", target_detail, filename);
390 rhaas 214 ECB : }
215 :
216 : /*
217 : * Finish executing the shell command once all data has been written.
218 : */
219 : static void
390 rhaas 220 CBC 4 : shell_finish_command(bbsink_shell *sink)
221 : {
222 : int pclose_rc;
390 rhaas 223 ECB :
224 : /* There should be a command running. */
390 rhaas 225 GIC 4 : Assert(sink->current_command != NULL);
226 4 : Assert(sink->pipe != NULL);
390 rhaas 227 ECB :
228 : /* Close down the pipe we opened. */
390 rhaas 229 GIC 4 : pclose_rc = ClosePipeStream(sink->pipe);
390 rhaas 230 GBC 4 : if (pclose_rc == -1)
390 rhaas 231 UIC 0 : ereport(ERROR,
232 : (errcode_for_file_access(),
233 : errmsg("could not close pipe to external command: %m")));
390 rhaas 234 GIC 4 : else if (pclose_rc != 0)
235 : {
390 rhaas 236 UIC 0 : ereport(ERROR,
390 rhaas 237 EUB : (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. */
390 rhaas 244 CBC 4 : sink->pipe = NULL;
390 rhaas 245 GIC 4 : pfree(sink->current_command);
246 4 : sink->current_command = NULL;
247 4 : }
248 :
249 : /*
390 rhaas 250 ECB : * Start up the shell command, substituting %f in for the current filename.
251 : */
252 : static void
390 rhaas 253 GIC 4 : shell_run_command(bbsink_shell *sink, const char *filename)
390 rhaas 254 ECB : {
255 : /* There should not be anything already running. */
390 rhaas 256 CBC 4 : Assert(sink->current_command == NULL);
390 rhaas 257 GIC 4 : Assert(sink->pipe == NULL);
258 :
259 : /* Construct a suitable command. */
260 8 : sink->current_command = shell_construct_command(sink->shell_command,
261 : filename,
390 rhaas 262 CBC 4 : sink->target_detail);
263 :
390 rhaas 264 ECB : /* Run it. */
390 rhaas 265 GIC 4 : sink->pipe = OpenPipeStream(sink->current_command, PG_BINARY_W);
390 rhaas 266 CBC 4 : }
390 rhaas 267 ECB :
268 : /*
269 : * Send accumulated data to the running shell command.
270 : */
271 : static void
390 rhaas 272 GIC 5416 : shell_send_data(bbsink_shell *sink, size_t len)
273 : {
390 rhaas 274 ECB : /* There should be a command running. */
390 rhaas 275 GIC 5416 : Assert(sink->current_command != NULL);
390 rhaas 276 CBC 5416 : Assert(sink->pipe != NULL);
277 :
390 rhaas 278 ECB : /* Try to write the data. */
390 rhaas 279 CBC 10832 : if (fwrite(sink->base.bbs_buffer, len, 1, sink->pipe) != 1 ||
280 5416 : ferror(sink->pipe))
281 : {
390 rhaas 282 UIC 0 : if (errno == EPIPE)
283 : {
284 : /*
285 : * The error we're about to throw would shut down the command
332 tgl 286 ECB : * anyway, but we may get a more meaningful error message by doing
287 : * this. If not, we'll fall through to the generic error below.
390 rhaas 288 : */
390 rhaas 289 UIC 0 : shell_finish_command(sink);
390 rhaas 290 LBC 0 : errno = EPIPE;
390 rhaas 291 ECB : }
390 rhaas 292 LBC 0 : ereport(ERROR,
293 : (errcode_for_file_access(),
294 : errmsg("could not write to shell backup program: %m")));
295 : }
390 rhaas 296 GIC 5416 : }
297 :
390 rhaas 298 ECB : /*
299 : * At start of archive, start up the shell command and forward to next sink.
300 : */
301 : static void
390 rhaas 302 CBC 2 : bbsink_shell_begin_archive(bbsink *sink, const char *archive_name)
390 rhaas 303 ECB : {
390 rhaas 304 CBC 2 : bbsink_shell *mysink = (bbsink_shell *) sink;
305 :
390 rhaas 306 GIC 2 : shell_run_command(mysink, archive_name);
307 2 : bbsink_forward_begin_archive(sink, archive_name);
308 2 : }
309 :
390 rhaas 310 ECB : /*
311 : * Send archive contents to command's stdin and forward to next sink.
312 : */
313 : static void
390 rhaas 314 CBC 5406 : bbsink_shell_archive_contents(bbsink *sink, size_t len)
390 rhaas 315 ECB : {
390 rhaas 316 CBC 5406 : bbsink_shell *mysink = (bbsink_shell *) sink;
317 :
390 rhaas 318 GIC 5406 : shell_send_data(mysink, len);
319 5406 : bbsink_forward_archive_contents(sink, len);
320 5406 : }
321 :
322 : /*
323 : * At end of archive, shut down the shell command and forward to next sink.
324 : */
325 : static void
326 2 : bbsink_shell_end_archive(bbsink *sink)
327 : {
328 2 : bbsink_shell *mysink = (bbsink_shell *) sink;
329 :
330 2 : shell_finish_command(mysink);
331 2 : bbsink_forward_end_archive(sink);
332 2 : }
333 :
334 : /*
335 : * At start of manifest, start up the shell command and forward to next sink.
336 : */
337 : static void
338 2 : bbsink_shell_begin_manifest(bbsink *sink)
339 : {
340 2 : bbsink_shell *mysink = (bbsink_shell *) sink;
341 :
342 2 : shell_run_command(mysink, "backup_manifest");
343 2 : bbsink_forward_begin_manifest(sink);
344 2 : }
345 :
346 : /*
347 : * Send manifest contents to command's stdin and forward to next sink.
348 : */
349 : static void
350 10 : bbsink_shell_manifest_contents(bbsink *sink, size_t len)
351 : {
352 10 : bbsink_shell *mysink = (bbsink_shell *) sink;
353 :
354 10 : shell_send_data(mysink, len);
355 10 : bbsink_forward_manifest_contents(sink, len);
356 10 : }
357 :
358 : /*
359 : * At end of manifest, shut down the shell command and forward to next sink.
360 : */
361 : static void
362 2 : bbsink_shell_end_manifest(bbsink *sink)
363 : {
364 2 : bbsink_shell *mysink = (bbsink_shell *) sink;
365 :
366 2 : shell_finish_command(mysink);
367 2 : bbsink_forward_end_manifest(sink);
368 2 : }
|