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 :
21 2 : 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
71 2 : _PG_init(void)
72 : {
73 2 : 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 2 : 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 :
91 2 : MarkGUCPrefixReserved("basebackup_to_shell");
92 :
93 2 : BaseBackupAddTarget("shell", shell_check_detail, shell_get_sink);
94 2 : }
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 12 : shell_check_detail(char *target, char *target_detail)
103 : {
104 12 : if (shell_required_role[0] != '\0')
105 : {
106 : Oid roleid;
107 :
108 6 : StartTransactionCommand();
109 6 : roleid = get_role_oid(shell_required_role, true);
110 6 : if (!has_privs_of_role(GetUserId(), roleid))
111 2 : ereport(ERROR,
112 : (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
113 : errmsg("permission denied to use basebackup_to_shell")));
114 4 : CommitTransactionCommand();
115 : }
116 :
117 10 : 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 10 : shell_get_sink(bbsink *next_sink, void *detail_arg)
128 : {
129 : bbsink_shell *sink;
130 10 : 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 : */
139 10 : sink = palloc0(sizeof(bbsink_shell));
140 10 : *((const bbsink_ops **) &sink->base.bbs_ops) = &bbsink_shell_ops;
141 10 : sink->base.bbs_next = next_sink;
142 10 : sink->target_detail = detail_arg;
143 10 : sink->shell_command = pstrdup(shell_command);
144 :
145 : /* Reject an empty shell command. */
146 10 : if (sink->shell_command[0] == '\0')
147 2 : 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 824 : for (c = sink->shell_command; *c != '\0'; ++c)
153 : {
154 816 : if (c[0] == '%' && c[1] != '\0')
155 : {
156 12 : if (c[1] == 'd')
157 4 : has_detail_escape = true;
158 12 : ++c;
159 : }
160 : }
161 :
162 : /* There should be a target detail if %d was used, and not otherwise. */
163 8 : if (has_detail_escape && sink->target_detail == NULL)
164 2 : 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 6 : else if (!has_detail_escape && sink->target_detail != NULL)
169 2 : 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 4 : if (sink->target_detail != NULL)
182 : {
183 : char *d;
184 2 : bool scary = false;
185 :
186 8 : for (d = sink->target_detail; *d != '\0'; ++d)
187 : {
188 6 : if (*d >= 'a' && *d <= 'z')
189 6 : continue;
190 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 :
198 2 : if (scary)
199 0 : ereport(ERROR,
200 : errcode(ERRCODE_INVALID_PARAMETER_VALUE),
201 : errmsg("target detail must contain only alphanumeric characters"));
202 : }
203 :
204 4 : 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 *
212 8 : shell_construct_command(const char *base_command, const char *filename,
213 : const char *target_detail)
214 : {
215 8 : 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
223 8 : shell_finish_command(bbsink_shell *sink)
224 : {
225 : int pclose_rc;
226 :
227 : /* There should be a command running. */
228 : Assert(sink->current_command != NULL);
229 : Assert(sink->pipe != NULL);
230 :
231 : /* Close down the pipe we opened. */
232 8 : pclose_rc = ClosePipeStream(sink->pipe);
233 8 : if (pclose_rc == -1)
234 0 : ereport(ERROR,
235 : (errcode_for_file_access(),
236 : errmsg("could not close pipe to external command: %m")));
237 8 : else if (pclose_rc != 0)
238 : {
239 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. */
247 8 : sink->pipe = NULL;
248 8 : pfree(sink->current_command);
249 8 : sink->current_command = NULL;
250 8 : }
251 :
252 : /*
253 : * Start up the shell command, substituting %f in for the current filename.
254 : */
255 : static void
256 8 : shell_run_command(bbsink_shell *sink, const char *filename)
257 : {
258 : /* There should not be anything already running. */
259 : Assert(sink->current_command == NULL);
260 : Assert(sink->pipe == NULL);
261 :
262 : /* Construct a suitable command. */
263 16 : sink->current_command = shell_construct_command(sink->shell_command,
264 : filename,
265 8 : sink->target_detail);
266 :
267 : /* Run it. */
268 8 : sink->pipe = OpenPipeStream(sink->current_command, PG_BINARY_W);
269 8 : if (sink->pipe == NULL)
270 0 : ereport(ERROR,
271 : (errcode_for_file_access(),
272 : errmsg("could not execute command \"%s\": %m",
273 : sink->current_command)));
274 8 : }
275 :
276 : /*
277 : * Send accumulated data to the running shell command.
278 : */
279 : static void
280 10854 : shell_send_data(bbsink_shell *sink, size_t len)
281 : {
282 : /* There should be a command running. */
283 : Assert(sink->current_command != NULL);
284 : Assert(sink->pipe != NULL);
285 :
286 : /* Try to write the data. */
287 21708 : if (fwrite(sink->base.bbs_buffer, len, 1, sink->pipe) != 1 ||
288 10854 : ferror(sink->pipe))
289 : {
290 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 : }
304 10854 : }
305 :
306 : /*
307 : * At start of archive, start up the shell command and forward to next sink.
308 : */
309 : static void
310 4 : bbsink_shell_begin_archive(bbsink *sink, const char *archive_name)
311 : {
312 4 : bbsink_shell *mysink = (bbsink_shell *) sink;
313 :
314 4 : shell_run_command(mysink, archive_name);
315 4 : bbsink_forward_begin_archive(sink, archive_name);
316 4 : }
317 :
318 : /*
319 : * Send archive contents to command's stdin and forward to next sink.
320 : */
321 : static void
322 10834 : bbsink_shell_archive_contents(bbsink *sink, size_t len)
323 : {
324 10834 : bbsink_shell *mysink = (bbsink_shell *) sink;
325 :
326 10834 : shell_send_data(mysink, len);
327 10834 : bbsink_forward_archive_contents(sink, len);
328 10834 : }
329 :
330 : /*
331 : * At end of archive, shut down the shell command and forward to next sink.
332 : */
333 : static void
334 4 : bbsink_shell_end_archive(bbsink *sink)
335 : {
336 4 : bbsink_shell *mysink = (bbsink_shell *) sink;
337 :
338 4 : shell_finish_command(mysink);
339 4 : bbsink_forward_end_archive(sink);
340 4 : }
341 :
342 : /*
343 : * At start of manifest, start up the shell command and forward to next sink.
344 : */
345 : static void
346 4 : bbsink_shell_begin_manifest(bbsink *sink)
347 : {
348 4 : bbsink_shell *mysink = (bbsink_shell *) sink;
349 :
350 4 : shell_run_command(mysink, "backup_manifest");
351 4 : bbsink_forward_begin_manifest(sink);
352 4 : }
353 :
354 : /*
355 : * Send manifest contents to command's stdin and forward to next sink.
356 : */
357 : static void
358 20 : bbsink_shell_manifest_contents(bbsink *sink, size_t len)
359 : {
360 20 : bbsink_shell *mysink = (bbsink_shell *) sink;
361 :
362 20 : shell_send_data(mysink, len);
363 20 : bbsink_forward_manifest_contents(sink, len);
364 20 : }
365 :
366 : /*
367 : * At end of manifest, shut down the shell command and forward to next sink.
368 : */
369 : static void
370 4 : bbsink_shell_end_manifest(bbsink *sink)
371 : {
372 4 : bbsink_shell *mysink = (bbsink_shell *) sink;
373 :
374 4 : shell_finish_command(mysink);
375 4 : bbsink_forward_end_manifest(sink);
376 4 : }
|