Age Owner Branch data 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-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 : :
761 rhaas@postgresql.org 21 :CBC 11 : 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 : 11 : _PG_init(void)
69 : : {
70 : 11 : 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 : 11 : 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 : :
713 michael@paquier.xyz 88 : 11 : MarkGUCPrefixReserved("basebackup_to_shell");
89 : :
761 rhaas@postgresql.org 90 : 11 : BaseBackupAddTarget("shell", shell_check_detail, shell_get_sink);
91 : 11 : }
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 : 6 : shell_check_detail(char *target, char *target_detail)
100 : : {
101 [ + + ]: 6 : if (shell_required_role[0] != '\0')
102 : : {
103 : : Oid roleid;
104 : :
105 : 3 : StartTransactionCommand();
106 : 3 : roleid = get_role_oid(shell_required_role, true);
743 mail@joeconway.com 107 [ + + ]: 3 : if (!has_privs_of_role(GetUserId(), roleid))
761 rhaas@postgresql.org 108 [ + - ]: 1 : ereport(ERROR,
109 : : (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
110 : : errmsg("permission denied to use basebackup_to_shell")));
111 : 2 : CommitTransactionCommand();
112 : : }
113 : :
114 : 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 : : */
123 : : static bbsink *
124 : 5 : shell_get_sink(bbsink *next_sink, void *detail_arg)
125 : : {
126 : : bbsink_shell *sink;
703 tgl@sss.pgh.pa.us 127 : 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.
135 : : */
761 rhaas@postgresql.org 136 : 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;
140 : 5 : sink->shell_command = pstrdup(shell_command);
141 : :
142 : : /* Reject an empty shell command. */
143 [ + + ]: 5 : if (sink->shell_command[0] == '\0')
144 [ + - ]: 1 : 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 [ + + ]: 544 : for (c = sink->shell_command; *c != '\0'; ++c)
150 : : {
151 [ + + + - ]: 540 : if (c[0] == '%' && c[1] != '\0')
152 : : {
153 [ + + ]: 6 : if (c[1] == 'd')
154 : 2 : has_detail_escape = true;
155 : 6 : ++c;
156 : : }
157 : : }
158 : :
159 : : /* There should be a target detail if %d was used, and not otherwise. */
160 [ + + + + ]: 4 : if (has_detail_escape && sink->target_detail == NULL)
161 [ + - ]: 1 : 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 [ + + + + ]: 3 : else if (!has_detail_escape && sink->target_detail != NULL)
166 [ + - ]: 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.
177 : : */
178 [ + + ]: 2 : if (sink->target_detail != NULL)
179 : : {
180 : : char *d;
703 tgl@sss.pgh.pa.us 181 : 1 : bool scary = false;
182 : :
761 rhaas@postgresql.org 183 [ + + ]: 4 : for (d = sink->target_detail; *d != '\0'; ++d)
184 : : {
185 [ + - + - ]: 3 : if (*d >= 'a' && *d <= 'z')
186 : 3 : continue;
761 rhaas@postgresql.org 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;
192 : 0 : break;
193 : : }
194 : :
761 rhaas@postgresql.org 195 [ - + ]:CBC 1 : if (scary)
761 rhaas@postgresql.org 196 [ # # ]:UBC 0 : ereport(ERROR,
197 : : errcode(ERRCODE_INVALID_PARAMETER_VALUE),
198 : : errmsg("target detail must contain only alphanumeric characters"));
199 : : }
200 : :
761 rhaas@postgresql.org 201 :CBC 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 : : */
208 : : static char *
488 peter@eisentraut.org 209 : 4 : shell_construct_command(const char *base_command, const char *filename,
210 : : const char *target_detail)
211 : : {
459 212 : 4 : 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
761 rhaas@postgresql.org 220 : 4 : shell_finish_command(bbsink_shell *sink)
221 : : {
222 : : int pclose_rc;
223 : :
224 : : /* There should be a command running. */
225 [ - + ]: 4 : Assert(sink->current_command != NULL);
226 [ - + ]: 4 : Assert(sink->pipe != NULL);
227 : :
228 : : /* Close down the pipe we opened. */
229 : 4 : pclose_rc = ClosePipeStream(sink->pipe);
230 [ - + ]: 4 : if (pclose_rc == -1)
761 rhaas@postgresql.org 231 [ # # ]:UBC 0 : ereport(ERROR,
232 : : (errcode_for_file_access(),
233 : : errmsg("could not close pipe to external command: %m")));
761 rhaas@postgresql.org 234 [ - + ]:CBC 4 : else if (pclose_rc != 0)
235 : : {
761 rhaas@postgresql.org 236 [ # # ]:UBC 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. */
761 rhaas@postgresql.org 244 :CBC 4 : sink->pipe = NULL;
245 : 4 : pfree(sink->current_command);
246 : 4 : sink->current_command = NULL;
247 : 4 : }
248 : :
249 : : /*
250 : : * Start up the shell command, substituting %f in for the current filename.
251 : : */
252 : : static void
253 : 4 : shell_run_command(bbsink_shell *sink, const char *filename)
254 : : {
255 : : /* There should not be anything already running. */
256 [ - + ]: 4 : Assert(sink->current_command == NULL);
257 [ - + ]: 4 : Assert(sink->pipe == NULL);
258 : :
259 : : /* Construct a suitable command. */
260 : 8 : sink->current_command = shell_construct_command(sink->shell_command,
261 : : filename,
262 : 4 : sink->target_detail);
263 : :
264 : : /* Run it. */
265 : 4 : sink->pipe = OpenPipeStream(sink->current_command, PG_BINARY_W);
368 266 [ - + ]: 4 : if (sink->pipe == NULL)
368 rhaas@postgresql.org 267 [ # # ]:UBC 0 : ereport(ERROR,
268 : : (errcode_for_file_access(),
269 : : errmsg("could not execute command \"%s\": %m",
270 : : sink->current_command)));
761 rhaas@postgresql.org 271 :CBC 4 : }
272 : :
273 : : /*
274 : : * Send accumulated data to the running shell command.
275 : : */
276 : : static void
277 : 5406 : shell_send_data(bbsink_shell *sink, size_t len)
278 : : {
279 : : /* There should be a command running. */
280 [ - + ]: 5406 : Assert(sink->current_command != NULL);
281 [ - + ]: 5406 : Assert(sink->pipe != NULL);
282 : :
283 : : /* Try to write the data. */
284 [ + - - + ]: 10812 : if (fwrite(sink->base.bbs_buffer, len, 1, sink->pipe) != 1 ||
285 : 5406 : ferror(sink->pipe))
286 : : {
761 rhaas@postgresql.org 287 [ # # ]:UBC 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 : : }
761 rhaas@postgresql.org 301 :CBC 5406 : }
302 : :
303 : : /*
304 : : * At start of archive, start up the shell command and forward to next sink.
305 : : */
306 : : static void
307 : 2 : bbsink_shell_begin_archive(bbsink *sink, const char *archive_name)
308 : : {
309 : 2 : bbsink_shell *mysink = (bbsink_shell *) sink;
310 : :
311 : 2 : shell_run_command(mysink, archive_name);
312 : 2 : bbsink_forward_begin_archive(sink, archive_name);
313 : 2 : }
314 : :
315 : : /*
316 : : * Send archive contents to command's stdin and forward to next sink.
317 : : */
318 : : static void
319 : 5396 : bbsink_shell_archive_contents(bbsink *sink, size_t len)
320 : : {
321 : 5396 : bbsink_shell *mysink = (bbsink_shell *) sink;
322 : :
323 : 5396 : shell_send_data(mysink, len);
324 : 5396 : bbsink_forward_archive_contents(sink, len);
325 : 5396 : }
326 : :
327 : : /*
328 : : * At end of archive, shut down the shell command and forward to next sink.
329 : : */
330 : : static void
331 : 2 : bbsink_shell_end_archive(bbsink *sink)
332 : : {
333 : 2 : bbsink_shell *mysink = (bbsink_shell *) sink;
334 : :
335 : 2 : shell_finish_command(mysink);
336 : 2 : bbsink_forward_end_archive(sink);
337 : 2 : }
338 : :
339 : : /*
340 : : * At start of manifest, start up the shell command and forward to next sink.
341 : : */
342 : : static void
343 : 2 : bbsink_shell_begin_manifest(bbsink *sink)
344 : : {
345 : 2 : bbsink_shell *mysink = (bbsink_shell *) sink;
346 : :
347 : 2 : shell_run_command(mysink, "backup_manifest");
348 : 2 : bbsink_forward_begin_manifest(sink);
349 : 2 : }
350 : :
351 : : /*
352 : : * Send manifest contents to command's stdin and forward to next sink.
353 : : */
354 : : static void
355 : 10 : bbsink_shell_manifest_contents(bbsink *sink, size_t len)
356 : : {
357 : 10 : bbsink_shell *mysink = (bbsink_shell *) sink;
358 : :
359 : 10 : shell_send_data(mysink, len);
360 : 10 : bbsink_forward_manifest_contents(sink, len);
361 : 10 : }
362 : :
363 : : /*
364 : : * At end of manifest, shut down the shell command and forward to next sink.
365 : : */
366 : : static void
367 : 2 : bbsink_shell_end_manifest(bbsink *sink)
368 : : {
369 : 2 : bbsink_shell *mysink = (bbsink_shell *) sink;
370 : :
371 : 2 : shell_finish_command(mysink);
372 : 2 : bbsink_forward_end_manifest(sink);
373 : 2 : }
|