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