xref: /openbsd/usr.bin/rsync/main.c (revision fecc32f1)
1 /*	$OpenBSD: main.c,v 1.58 2021/08/30 20:25:01 job Exp $ */
2 /*
3  * Copyright (c) 2019 Kristaps Dzonsons <kristaps@bsd.lv>
4  *
5  * Permission to use, copy, modify, and distribute this software for any
6  * purpose with or without fee is hereby granted, provided that the above
7  * copyright notice and this permission notice appear in all copies.
8  *
9  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
16  */
17 #include <sys/stat.h>
18 #include <sys/socket.h>
19 #include <sys/wait.h>
20 
21 #include <assert.h>
22 #include <err.h>
23 #include <getopt.h>
24 #include <stdint.h>
25 #include <stdio.h>
26 #include <stdlib.h>
27 #include <string.h>
28 #include <unistd.h>
29 
30 #include "extern.h"
31 
32 int verbose;
33 int poll_timeout;
34 
35 /*
36  * A remote host is has a colon before the first path separator.
37  * This works for rsh remote hosts (host:/foo/bar), implicit rsync
38  * remote hosts (host::/foo/bar), and explicit (rsync://host/foo).
39  * Return zero if local, non-zero if remote.
40  */
41 static int
42 fargs_is_remote(const char *v)
43 {
44 	size_t	 pos;
45 
46 	pos = strcspn(v, ":/");
47 	return v[pos] == ':';
48 }
49 
50 /*
51  * Test whether a remote host is specifically an rsync daemon.
52  * Return zero if not, non-zero if so.
53  */
54 static int
55 fargs_is_daemon(const char *v)
56 {
57 	size_t	 pos;
58 
59 	if (strncasecmp(v, "rsync://", 8) == 0)
60 		return 1;
61 
62 	pos = strcspn(v, ":/");
63 	return v[pos] == ':' && v[pos + 1] == ':';
64 }
65 
66 /*
67  * Take the command-line filenames (e.g., rsync foo/ bar/ baz/) and
68  * determine our operating mode.
69  * For example, if the first argument is a remote file, this means that
70  * we're going to transfer from the remote to the local.
71  * We also make sure that the arguments are consistent, that is, if
72  * we're going to transfer from the local to the remote, that no
73  * filenames for the local transfer indicate remote hosts.
74  * Always returns the parsed and sanitised options.
75  */
76 static struct fargs *
77 fargs_parse(size_t argc, char *argv[], struct opts *opts)
78 {
79 	struct fargs	*f = NULL;
80 	char		*cp, *ccp;
81 	size_t		 i, j, len = 0;
82 
83 	/* Allocations. */
84 
85 	if ((f = calloc(1, sizeof(struct fargs))) == NULL)
86 		err(ERR_NOMEM, NULL);
87 
88 	f->sourcesz = argc - 1;
89 	if ((f->sources = calloc(f->sourcesz, sizeof(char *))) == NULL)
90 		err(ERR_NOMEM, NULL);
91 
92 	for (i = 0; i < argc - 1; i++)
93 		if ((f->sources[i] = strdup(argv[i])) == NULL)
94 			err(ERR_NOMEM, NULL);
95 
96 	if ((f->sink = strdup(argv[i])) == NULL)
97 		err(ERR_NOMEM, NULL);
98 
99 	/*
100 	 * Test files for its locality.
101 	 * If the last is a remote host, then we're sending from the
102 	 * local to the remote host ("sender" mode).
103 	 * If the first, remote to local ("receiver" mode).
104 	 * If neither, a local transfer in sender style.
105 	 */
106 
107 	f->mode = FARGS_SENDER;
108 
109 	if (fargs_is_remote(f->sink)) {
110 		f->mode = FARGS_SENDER;
111 		if ((f->host = strdup(f->sink)) == NULL)
112 			err(ERR_NOMEM, NULL);
113 	}
114 
115 	if (fargs_is_remote(f->sources[0])) {
116 		if (f->host != NULL)
117 			errx(ERR_SYNTAX, "both source and destination "
118 			    "cannot be remote files");
119 		f->mode = FARGS_RECEIVER;
120 		if ((f->host = strdup(f->sources[0])) == NULL)
121 			err(ERR_NOMEM, NULL);
122 	}
123 
124 	if (f->host != NULL) {
125 		if (strncasecmp(f->host, "rsync://", 8) == 0) {
126 			/* rsync://host[:port]/module[/path] */
127 			f->remote = 1;
128 			len = strlen(f->host) - 8 + 1;
129 			memmove(f->host, f->host + 8, len);
130 			if ((cp = strchr(f->host, '/')) == NULL)
131 				errx(ERR_SYNTAX,
132 				    "rsync protocol requires a module name");
133 			*cp++ = '\0';
134 			f->module = cp;
135 			if ((cp = strchr(f->module, '/')) != NULL)
136 				*cp = '\0';
137 			if ((cp = strchr(f->host, ':')) != NULL) {
138 				/* host:port --> extract port */
139 				*cp++ = '\0';
140 				opts->port = cp;
141 			}
142 		} else {
143 			/* host:[/path] */
144 			cp = strchr(f->host, ':');
145 			assert(cp != NULL);
146 			*cp++ = '\0';
147 			if (*cp == ':') {
148 				/* host::module[/path] */
149 				f->remote = 1;
150 				f->module = ++cp;
151 				cp = strchr(f->module, '/');
152 				if (cp != NULL)
153 					*cp = '\0';
154 			}
155 		}
156 		if ((len = strlen(f->host)) == 0)
157 			errx(ERR_SYNTAX, "empty remote host");
158 		if (f->remote && strlen(f->module) == 0)
159 			errx(ERR_SYNTAX, "empty remote module");
160 	}
161 
162 	/* Make sure we have the same "hostspec" for all files. */
163 
164 	if (!f->remote) {
165 		if (f->mode == FARGS_SENDER)
166 			for (i = 0; i < f->sourcesz; i++) {
167 				if (!fargs_is_remote(f->sources[i]))
168 					continue;
169 				errx(ERR_SYNTAX,
170 				    "remote file in list of local sources: %s",
171 				    f->sources[i]);
172 			}
173 		if (f->mode == FARGS_RECEIVER)
174 			for (i = 0; i < f->sourcesz; i++) {
175 				if (fargs_is_remote(f->sources[i]) &&
176 				    !fargs_is_daemon(f->sources[i]))
177 					continue;
178 				if (fargs_is_daemon(f->sources[i]))
179 					errx(ERR_SYNTAX,
180 					    "remote daemon in list of remote "
181 					    "sources: %s", f->sources[i]);
182 				errx(ERR_SYNTAX, "local file in list of "
183 				    "remote sources: %s", f->sources[i]);
184 			}
185 	} else {
186 		if (f->mode != FARGS_RECEIVER)
187 			errx(ERR_SYNTAX, "sender mode for remote "
188 				"daemon receivers not yet supported");
189 		for (i = 0; i < f->sourcesz; i++) {
190 			if (fargs_is_daemon(f->sources[i]))
191 				continue;
192 			errx(ERR_SYNTAX, "non-remote daemon file "
193 				"in list of remote daemon sources: "
194 				"%s", f->sources[i]);
195 		}
196 	}
197 
198 	/*
199 	 * If we're not remote and a sender, strip our hostname.
200 	 * Then exit if we're a sender or a local connection.
201 	 */
202 
203 	if (!f->remote) {
204 		if (f->host == NULL)
205 			return f;
206 		if (f->mode == FARGS_SENDER) {
207 			assert(f->host != NULL);
208 			assert(len > 0);
209 			j = strlen(f->sink);
210 			memmove(f->sink, f->sink + len + 1, j - len);
211 			return f;
212 		} else if (f->mode != FARGS_RECEIVER)
213 			return f;
214 	}
215 
216 	/*
217 	 * Now strip the hostnames from the remote host.
218 	 *   rsync://host/module/path -> module/path
219 	 *   host::module/path -> module/path
220 	 *   host:path -> path
221 	 * Also make sure that the remote hosts are the same.
222 	 */
223 
224 	assert(f->host != NULL);
225 	assert(len > 0);
226 
227 	for (i = 0; i < f->sourcesz; i++) {
228 		cp = f->sources[i];
229 		j = strlen(cp);
230 		if (f->remote &&
231 		    strncasecmp(cp, "rsync://", 8) == 0) {
232 			/* rsync://path */
233 			cp += 8;
234 			if ((ccp = strchr(cp, ':')))	/* skip :port */
235 				*ccp = '\0';
236 			if (strncmp(cp, f->host, len) ||
237 			    (cp[len] != '/' && cp[len] != '\0'))
238 				errx(ERR_SYNTAX, "different remote host: %s",
239 				    f->sources[i]);
240 			memmove(f->sources[i],
241 				f->sources[i] + len + 8 + 1,
242 				j - len - 8);
243 		} else if (f->remote && strncmp(cp, "::", 2) == 0) {
244 			/* ::path */
245 			memmove(f->sources[i],
246 				f->sources[i] + 2, j - 1);
247 		} else if (f->remote) {
248 			/* host::path */
249 			if (strncmp(cp, f->host, len) ||
250 			    (cp[len] != ':' && cp[len] != '\0'))
251 				errx(ERR_SYNTAX, "different remote host: %s",
252 				    f->sources[i]);
253 			memmove(f->sources[i], f->sources[i] + len + 2,
254 			    j - len - 1);
255 		} else if (cp[0] == ':') {
256 			/* :path */
257 			memmove(f->sources[i], f->sources[i] + 1, j);
258 		} else {
259 			/* host:path */
260 			if (strncmp(cp, f->host, len) ||
261 			    (cp[len] != ':' && cp[len] != '\0'))
262 				errx(ERR_SYNTAX, "different remote host: %s",
263 				    f->sources[i]);
264 			memmove(f->sources[i],
265 				f->sources[i] + len + 1, j - len);
266 		}
267 	}
268 
269 	return f;
270 }
271 
272 static struct opts	 opts;
273 
274 #define OP_ADDRESS	1000
275 #define OP_PORT		1001
276 #define OP_RSYNCPATH	1002
277 #define OP_TIMEOUT	1003
278 #define OP_VERSION	1004
279 #define OP_EXCLUDE	1005
280 #define OP_INCLUDE	1006
281 #define OP_EXCLUDE_FROM	1007
282 #define OP_INCLUDE_FROM	1008
283 
284 const struct option	 lopts[] = {
285     { "address",	required_argument, NULL,		OP_ADDRESS },
286     { "archive",	no_argument,	NULL,			'a' },
287     { "compress",	no_argument,	NULL,			'z' },
288     { "del",		no_argument,	&opts.del,		1 },
289     { "delete",		no_argument,	&opts.del,		1 },
290     { "devices",	no_argument,	&opts.devices,		1 },
291     { "no-devices",	no_argument,	&opts.devices,		0 },
292     { "dry-run",	no_argument,	&opts.dry_run,		1 },
293     { "exclude",	required_argument, NULL, 		OP_EXCLUDE },
294     { "exclude-from",	required_argument, NULL,		OP_EXCLUDE_FROM },
295     { "from0",		no_argument,	NULL,			'0' },
296     { "no-from0",	no_argument,	&opts.from0,		0 },
297     { "group",		no_argument,	&opts.preserve_gids,	1 },
298     { "no-group",	no_argument,	&opts.preserve_gids,	0 },
299     { "help",		no_argument,	NULL,			'h' },
300     { "include",	required_argument, NULL, 		OP_INCLUDE },
301     { "include-from",	required_argument, NULL,		OP_INCLUDE_FROM },
302     { "links",		no_argument,	&opts.preserve_links,	1 },
303     { "no-links",	no_argument,	&opts.preserve_links,	0 },
304     { "no-motd",	no_argument,	&opts.no_motd,		1 },
305     { "numeric-ids",	no_argument,	&opts.numeric_ids,	1 },
306     { "owner",		no_argument,	&opts.preserve_uids,	1 },
307     { "no-owner",	no_argument,	&opts.preserve_uids,	0 },
308     { "perms",		no_argument,	&opts.preserve_perms,	1 },
309     { "no-perms",	no_argument,	&opts.preserve_perms,	0 },
310     { "port",		required_argument, NULL,		OP_PORT },
311     { "recursive",	no_argument,	&opts.recursive,	1 },
312     { "no-recursive",	no_argument,	&opts.recursive,	0 },
313     { "rsh",		required_argument, NULL,		'e' },
314     { "rsync-path",	required_argument, NULL,		OP_RSYNCPATH },
315     { "sender",		no_argument,	&opts.sender,		1 },
316     { "server",		no_argument,	&opts.server,		1 },
317     { "specials",	no_argument,	&opts.specials,		1 },
318     { "no-specials",	no_argument,	&opts.specials,		0 },
319     { "timeout",	required_argument, NULL,		OP_TIMEOUT },
320     { "times",		no_argument,	&opts.preserve_times,	1 },
321     { "no-times",	no_argument,	&opts.preserve_times,	0 },
322     { "verbose",	no_argument,	&verbose,		1 },
323     { "no-verbose",	no_argument,	&verbose,		0 },
324     { "version",	no_argument,	NULL,			OP_VERSION },
325     { NULL,		0,		NULL,			0 }
326 };
327 
328 int
329 main(int argc, char *argv[])
330 {
331 	pid_t		 child;
332 	int		 fds[2], sd = -1, rc, c, st, i;
333 	struct sess	  sess;
334 	struct fargs	*fargs;
335 	char		**args;
336 	const char 	*errstr;
337 
338 	/* Global pledge. */
339 
340 	if (pledge("stdio unix rpath wpath cpath dpath inet fattr chown dns getpw proc exec unveil",
341 	    NULL) == -1)
342 		err(ERR_IPC, "pledge");
343 
344 	while ((c = getopt_long(argc, argv, "Dae:ghlnoprtvxz", lopts, NULL))
345 	    != -1) {
346 		switch (c) {
347 		case '0':
348 			opts.from0 = 1;
349 			break;
350 		case 'D':
351 			opts.devices = 1;
352 			opts.specials = 1;
353 			break;
354 		case 'a':
355 			opts.recursive = 1;
356 			opts.preserve_links = 1;
357 			opts.preserve_perms = 1;
358 			opts.preserve_times = 1;
359 			opts.preserve_gids = 1;
360 			opts.preserve_uids = 1;
361 			opts.devices = 1;
362 			opts.specials = 1;
363 			break;
364 		case 'e':
365 			opts.ssh_prog = optarg;
366 			break;
367 		case 'g':
368 			opts.preserve_gids = 1;
369 			break;
370 		case 'l':
371 			opts.preserve_links = 1;
372 			break;
373 		case 'n':
374 			opts.dry_run = 1;
375 			break;
376 		case 'o':
377 			opts.preserve_uids = 1;
378 			break;
379 		case 'p':
380 			opts.preserve_perms = 1;
381 			break;
382 		case 'r':
383 			opts.recursive = 1;
384 			break;
385 		case 't':
386 			opts.preserve_times = 1;
387 			break;
388 		case 'v':
389 			verbose++;
390 			break;
391 		case 'x':
392 			opts.one_file_system++;
393 			break;
394 		case 'z':
395 			fprintf(stderr, "%s: -z not supported yet\n", getprogname());
396 			break;
397 		case 0:
398 			/* Non-NULL flag values (e.g., --sender). */
399 			break;
400 		case OP_ADDRESS:
401 			opts.address = optarg;
402 			break;
403 		case OP_PORT:
404 			opts.port = optarg;
405 			break;
406 		case OP_RSYNCPATH:
407 			opts.rsync_path = optarg;
408 			break;
409 		case OP_TIMEOUT:
410 			poll_timeout = strtonum(optarg, 0, 60*60, &errstr);
411 			if (errstr != NULL)
412 				errx(ERR_SYNTAX, "timeout is %s: %s",
413 				    errstr, optarg);
414 			break;
415 		case OP_EXCLUDE:
416 			if (parse_rule(optarg, RULE_EXCLUDE) == -1)
417 				errx(ERR_SYNTAX, "syntax error in exclude: %s",
418 				    optarg);
419 			break;
420 		case OP_INCLUDE:
421 			if (parse_rule(optarg, RULE_INCLUDE) == -1)
422 				errx(ERR_SYNTAX, "syntax error in include: %s",
423 				    optarg);
424 			break;
425 		case OP_EXCLUDE_FROM:
426 			parse_file(optarg, RULE_EXCLUDE,
427 			    opts.from0 ? '\0' : '\n' );
428 			break;
429 		case OP_INCLUDE_FROM:
430 			parse_file(optarg, RULE_INCLUDE,
431 			    opts.from0 ? '\0' : '\n' );
432 			break;
433 		case OP_VERSION:
434 			fprintf(stderr, "openrsync: protocol version %u\n",
435 			    RSYNC_PROTOCOL);
436 			exit(0);
437 		case 'h':
438 		default:
439 			goto usage;
440 		}
441 	}
442 
443 	argc -= optind;
444 	argv += optind;
445 
446 	/* FIXME: reference implementation rsync accepts this. */
447 
448 	if (argc < 2)
449 		goto usage;
450 
451 	if (opts.port == NULL)
452 		opts.port = "rsync";
453 
454 	/* by default and for --timeout=0 disable poll_timeout */
455 	if (poll_timeout == 0)
456 		poll_timeout = -1;
457 	else
458 		poll_timeout *= 1000;
459 
460 	/*
461 	 * This is what happens when we're started with the "hidden"
462 	 * --server option, which is invoked for the rsync on the remote
463 	 * host by the parent.
464 	 */
465 
466 	if (opts.server)
467 		exit(rsync_server(&opts, (size_t)argc, argv));
468 
469 	/*
470 	 * Now we know that we're the client on the local machine
471 	 * invoking rsync(1).
472 	 * At this point, we need to start the client and server
473 	 * initiation logic.
474 	 * The client is what we continue running on this host; the
475 	 * server is what we'll use to connect to the remote and
476 	 * invoke rsync with the --server option.
477 	 */
478 
479 	fargs = fargs_parse(argc, argv, &opts);
480 	assert(fargs != NULL);
481 
482 	/*
483 	 * If we're contacting an rsync:// daemon, then we don't need to
484 	 * fork, because we won't start a server ourselves.
485 	 * Route directly into the socket code, unless a remote shell
486 	 * has explicitly been specified.
487 	 */
488 
489 	if (fargs->remote && opts.ssh_prog == NULL) {
490 		assert(fargs->mode == FARGS_RECEIVER);
491 		if ((rc = rsync_connect(&opts, &sd, fargs)) == 0) {
492 			rc = rsync_socket(&opts, sd, fargs);
493 			close(sd);
494 		}
495 		exit(rc);
496 	}
497 
498 	/* Drop the dns/inet possibility. */
499 
500 	if (pledge("stdio unix rpath wpath cpath dpath fattr chown getpw proc exec unveil",
501 	    NULL) == -1)
502 		err(ERR_IPC, "pledge");
503 
504 	/* Create a bidirectional socket and start our child. */
505 
506 	if (socketpair(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK, 0, fds) == -1)
507 		err(ERR_IPC, "socketpair");
508 
509 	switch ((child = fork())) {
510 	case -1:
511 		err(ERR_IPC, "fork");
512 	case 0:
513 		close(fds[0]);
514 		if (pledge("stdio exec", NULL) == -1)
515 			err(ERR_IPC, "pledge");
516 
517 		memset(&sess, 0, sizeof(struct sess));
518 		sess.opts = &opts;
519 
520 		args = fargs_cmdline(&sess, fargs, NULL);
521 
522 		for (i = 0; args[i] != NULL; i++)
523 			LOG2("exec[%d] = %s", i, args[i]);
524 
525 		/* Make sure the child's stdin is from the sender. */
526 		if (dup2(fds[1], STDIN_FILENO) == -1)
527 			err(ERR_IPC, "dup2");
528 		if (dup2(fds[1], STDOUT_FILENO) == -1)
529 			err(ERR_IPC, "dup2");
530 		execvp(args[0], args);
531 		_exit(ERR_IPC);
532 		/* NOTREACHED */
533 	default:
534 		close(fds[1]);
535 		if (!fargs->remote)
536 			rc = rsync_client(&opts, fds[0], fargs);
537 		else
538 			rc = rsync_socket(&opts, fds[0], fargs);
539 		break;
540 	}
541 
542 	close(fds[0]);
543 
544 	if (waitpid(child, &st, 0) == -1)
545 		err(ERR_WAITPID, "waitpid");
546 
547 	/*
548 	 * If we don't already have an error (rc == 0), then inherit the
549 	 * error code of rsync_server() if it has exited.
550 	 * If it hasn't exited, it overrides our return value.
551 	 */
552 
553 	if (rc == 0) {
554 		if (WIFEXITED(st))
555 			rc = WEXITSTATUS(st);
556 		else if (WIFSIGNALED(st))
557 			rc = ERR_TERMIMATED;
558 		else
559 			rc = ERR_WAITPID;
560 	}
561 
562 	exit(rc);
563 usage:
564 	fprintf(stderr, "usage: %s"
565 	    " [-aDglnoprtvx] [-e program] [--address=sourceaddr] [--del]\n"
566 	    "\t[--exclude] [--exclude-from=file] [--include] "
567 	    "[--include-from=file]\n"
568 	    "\t[--no-motd] [--numeric-ids] [--port=portnumber] "
569 	    "[--rsync-path=program]\n\t[--timeout=seconds] [--version] "
570             "source ... directory\n",
571 	    getprogname());
572 	exit(ERR_SYNTAX);
573 }
574