[BACK]Return to receiver.c CVS log [TXT][DIR] Up to [local] / src / usr.bin / rsync

File: [local] / src / usr.bin / rsync / receiver.c (download)

Revision 1.15, Sat Feb 16 10:49:37 2019 UTC (5 years, 3 months ago) by florian
Branch: MAIN
Changes since 1.14: +2 -1 lines

I did some work here.

/*	$Id: receiver.c,v 1.15 2019/02/16 10:49:37 florian Exp $ */

/*
 * Copyright (c) 2019 Kristaps Dzonsons <kristaps@bsd.lv>
 * Copyright (c) 2019 Florian Obser <florian@openbsd.org>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */
#include <sys/mman.h>
#include <sys/stat.h>

#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <math.h>
#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>

#include "extern.h"

enum	pfdt {
	PFD_SENDER_IN = 0, /* input from the sender */
	PFD_UPLOADER_IN, /* uploader input from a local file */
	PFD_DOWNLOADER_IN, /* downloader input from a local file */
	PFD_SENDER_OUT, /* output to the sender */
	PFD__MAX
};

int
rsync_set_metadata(struct sess *sess, int newfile,
	int fd, const struct flist *f, const char *path)
{
	uid_t		 uid = (uid_t)-1;
	gid_t		 gid = (gid_t)-1;
	struct timespec	 tv[2];

	/*
	 * Conditionally adjust identifiers.
	 * If we have an EPERM, report it but continue on: this just
	 * means that we're mapping into an unknown (or disallowed)
	 * group identifier.
	 */

	if (getuid() == 0 && sess->opts->preserve_uids)
		uid = f->st.uid;
	if (sess->opts->preserve_gids)
		gid = f->st.gid;

	if (uid != (uid_t)-1 || gid != (gid_t)-1) {
		if (fchown(fd, uid, gid) == -1) {
			if (errno != EPERM) {
				ERR(sess, "%s: fchown", path);
				return 0;
			}
			WARNX(sess, "%s: identity unknown or not available "
				"to user.group: %u.%u", f->path, uid, gid);
		} else
			LOG4(sess, "%s: updated uid and/or gid", f->path);
	}

	/* Conditionally adjust file modification time. */

	if (sess->opts->preserve_times) {
		struct timeval now;

		gettimeofday(&now, NULL);
		TIMEVAL_TO_TIMESPEC(&now, &tv[0]);
		tv[1].tv_sec = f->st.mtime;
		tv[1].tv_nsec = 0;
		if (futimens(fd, tv) == -1) {
			ERR(sess, "%s: futimens", path);
			return 0;
		}
		LOG4(sess, "%s: updated date", f->path);
	}

	/* Conditionally adjust file permissions. */

	if (newfile || sess->opts->preserve_perms) {
		if (fchmod(fd, f->st.mode) == -1) {
			ERR(sess, "%s: fchmod", path);
			return 0;
		}
		LOG4(sess, "%s: updated permissions", f->path);
	}

	return 1;
}

int
rsync_set_metadata_at(struct sess *sess, int newfile, int rootfd,
	const struct flist *f, const char *path)
{
	uid_t		 uid = (uid_t)-1;
	gid_t		 gid = (gid_t)-1;
	struct timespec	 tv[2];

	/*
	 * Conditionally adjust identifiers.
	 * If we have an EPERM, report it but continue on: this just
	 * means that we're mapping into an unknown (or disallowed)
	 * group identifier.
	 */

	if (getuid() == 0 && sess->opts->preserve_uids)
		uid = f->st.uid;
	if (sess->opts->preserve_gids)
		gid = f->st.gid;

	if (uid != (uid_t)-1 || gid != (gid_t)-1) {
		if (fchownat(rootfd, path, uid, gid, AT_SYMLINK_NOFOLLOW) ==
		    -1) {
			if (errno != EPERM) {
				ERR(sess, "%s: fchownat", path);
				return 0;
			}
			WARNX(sess, "%s: identity unknown or not available "
				"to user.group: %u.%u", f->path, uid, gid);
		} else
			LOG4(sess, "%s: updated uid and/or gid", f->path);
	}

	/* Conditionally adjust file modification time. */

	if (sess->opts->preserve_times) {
		struct timeval now;

		gettimeofday(&now, NULL);
		TIMEVAL_TO_TIMESPEC(&now, &tv[0]);
		tv[1].tv_sec = f->st.mtime;
		tv[1].tv_nsec = 0;
		if (utimensat(rootfd, path, tv, AT_SYMLINK_NOFOLLOW) == -1) {
			ERR(sess, "%s: utimensat", path);
			return 0;
		}
		LOG4(sess, "%s: updated date", f->path);
	}

	/* Conditionally adjust file permissions. */

	if (newfile || sess->opts->preserve_perms) {
		if (fchmodat(rootfd, path, f->st.mode, AT_SYMLINK_NOFOLLOW) ==
		    -1) {
			ERR(sess, "%s: fchmodat", path);
			return 0;
		}
		LOG4(sess, "%s: updated permissions", f->path);
	}

	return 1;
}

/*
 * Pledges: unveil, unix, rpath, cpath, wpath, stdio, fattr, chown.
 * Pledges (dry-run): -unix, -cpath, -wpath, -fattr, -chown.
 */
int
rsync_receiver(struct sess *sess, int fdin, int fdout, const char *root)
{
	struct flist	*fl = NULL, *dfl = NULL;
	size_t		 i, flsz = 0, dflsz = 0, excl;
	char		*tofree;
	int		 rc = 0, dfd = -1, phase = 0, c;
	int32_t		 ioerror;
	struct pollfd	 pfd[PFD__MAX];
	struct download	*dl = NULL;
	struct upload	*ul = NULL;
	mode_t		 oumask;

	if (pledge("stdio unix rpath wpath cpath dpath fattr chown getpw unveil", NULL) == -1) {
		ERR(sess, "pledge");
		goto out;
	}

	/* Client sends zero-length exclusions. */

	if (!sess->opts->server &&
	     !io_write_int(sess, fdout, 0)) {
		ERRX1(sess, "io_write_int");
		goto out;
	}

	if (sess->opts->server && sess->opts->del) {
		if (!io_read_size(sess, fdin, &excl)) {
			ERRX1(sess, "io_read_size");
			goto out;
		} else if (0 != excl) {
			ERRX(sess, "exclusion list is non-empty");
			goto out;
		}
	}

	/*
	 * Start by receiving the file list and our mystery number.
	 * These we're going to be touching on our local system.
	 */

	if (!flist_recv(sess, fdin, &fl, &flsz)) {
		ERRX1(sess, "flist_recv");
		goto out;
	}

	/* The IO error is sent after the file list. */

	if (!io_read_int(sess, fdin, &ioerror)) {
		ERRX1(sess, "io_read_int");
		goto out;
	} else if (0 != ioerror) {
		ERRX1(sess, "io_error is non-zero");
		goto out;
	}

	if (flsz == 0 && !sess->opts->server) {
		WARNX(sess, "receiver has empty file list: exiting");
		rc = 1;
		goto out;
	} else if (!sess->opts->server)
		LOG1(sess, "Transfer starting: %zu files", flsz);

	LOG2(sess, "%s: receiver destination", root);

	/*
	 * Create the path for our destination directory, if we're not
	 * in dry-run mode (which would otherwise crash w/the pledge).
	 * This uses our current umask: we might set the permissions on
	 * this directory in post_dir().
	 */

	if (!sess->opts->dry_run) {
		if ((tofree = strdup(root)) == NULL) {
			ERR(sess, "strdup");
			goto out;
		} else if (mkpath(sess, tofree) < 0) {
			ERRX1(sess, "%s: mkpath", root);
			free(tofree);
			goto out;
		}
		free(tofree);
	}

	/*
	 * Disable umask() so we can set permissions fully.
	 * Then open the directory iff we're not in dry_run.
	 */

	oumask = umask(0);

	if (!sess->opts->dry_run) {
		dfd = open(root, O_RDONLY | O_DIRECTORY, 0);
		if (dfd == -1) {
			ERR(sess, "%s: open", root);
			goto out;
		}
	}

	/*
	 * Begin by conditionally getting all files we have currently
	 * available in our destination.
	 */

	if (sess->opts->del &&
	    sess->opts->recursive &&
	    !flist_gen_dels(sess, root, &dfl, &dflsz, fl, flsz)) {
		ERRX1(sess, "flist_gen_local");
		goto out;
	}

	/*
	 * Make our entire view of the file-system be limited to what's
	 * in the root directory.
	 * This prevents us from accidentally (or "under the influence")
	 * writing into other parts of the file-system.
	 */

	if (unveil(root, "rwc") == -1) {
		ERR(sess, "%s: unveil", root);
		goto out;
	} else if (unveil(NULL, NULL) == -1) {
		ERR(sess, "%s: unveil", root);
		goto out;
	}

	/* If we have a local set, go for the deletion. */

	if (!flist_del(sess, dfd, dfl, dflsz)) {
		ERRX1(sess, "flist_del");
		goto out;
	}

	/* Initialise poll events to listen from the sender. */

	pfd[PFD_SENDER_IN].fd = fdin;
	pfd[PFD_UPLOADER_IN].fd = -1;
	pfd[PFD_DOWNLOADER_IN].fd = -1;
	pfd[PFD_SENDER_OUT].fd = fdout;

	pfd[PFD_SENDER_IN].events = POLLIN;
	pfd[PFD_UPLOADER_IN].events = POLLIN;
	pfd[PFD_DOWNLOADER_IN].events = POLLIN;
	pfd[PFD_SENDER_OUT].events = POLLOUT;

	ul = upload_alloc(sess, root, dfd, fdout,
		CSUM_LENGTH_PHASE1, fl, flsz, oumask);
	if (ul == NULL) {
		ERRX1(sess, "upload_alloc");
		goto out;
	}

	dl = download_alloc(sess, fdin, fl, flsz, dfd);
	if (dl == NULL) {
		ERRX1(sess, "download_alloc");
		goto out;
	}

	LOG2(sess, "%s: ready for phase 1 data", root);

	for (;;) {
		if ((c = poll(pfd, PFD__MAX, INFTIM)) == -1) {
			ERR(sess, "poll");
			goto out;
		}

		for (i = 0; i < PFD__MAX; i++)
			if (pfd[i].revents & (POLLERR|POLLNVAL)) {
				ERRX(sess, "poll: bad fd");
				goto out;
			} else if (pfd[i].revents & POLLHUP) {
				ERRX(sess, "poll: hangup");
				goto out;
			}

		/*
		 * If we have a read event and we're multiplexing, we
		 * might just have error messages in the pipe.
		 * It's important to flush these out so that we don't
		 * clog the pipe.
		 * Unset our polling status if there's nothing that
		 * remains in the pipe.
		 */

		if (sess->mplex_reads &&
		    (POLLIN & pfd[PFD_SENDER_IN].revents)) {
			if (!io_read_flush(sess, fdin)) {
				ERRX1(sess, "io_read_flush");
				goto out;
			} else if (sess->mplex_read_remain == 0)
				pfd[PFD_SENDER_IN].revents &= ~POLLIN;
		}


		/*
		 * We run the uploader if we have files left to examine
		 * (i < flsz) or if we have a file that we've opened and
		 * is read to mmap.
		 */

		if ((POLLIN & pfd[PFD_UPLOADER_IN].revents) ||
		    (POLLOUT & pfd[PFD_SENDER_OUT].revents)) {
			c = rsync_uploader(ul,
				&pfd[PFD_UPLOADER_IN].fd,
				sess, &pfd[PFD_SENDER_OUT].fd);
			if (c < 0) {
				ERRX1(sess, "rsync_uploader");
				goto out;
			}
		}

		/*
		 * We need to run the downloader when we either have
		 * read events from the sender or an asynchronous local
		 * open is ready.
		 * XXX: we don't disable PFD_SENDER_IN like with the
		 * uploader because we might stop getting error
		 * messages, which will otherwise clog up the pipes.
		 */

		if ((POLLIN & pfd[PFD_SENDER_IN].revents) ||
		    (POLLIN & pfd[PFD_DOWNLOADER_IN].revents)) {
			c = rsync_downloader(dl, sess,
				&pfd[PFD_DOWNLOADER_IN].fd);
			if (c < 0) {
				ERRX1(sess, "rsync_downloader");
				goto out;
			} else if (c == 0) {
				assert(phase == 0);
				phase++;
				LOG2(sess, "%s: receiver ready "
					"for phase 2 data", root);
				break;
			}

			/*
			 * FIXME: if we have any errors during the
			 * download, most notably files getting out of
			 * sync between the send and the receiver, then
			 * here we should bump our checksum length and
			 * go into the second phase.
			 */
		}
	}

	/* Properly close us out by progressing through the phases. */

	if (phase == 1) {
		if (!io_write_int(sess, fdout, -1)) {
			ERRX1(sess, "io_write_int");
			goto out;
		} else if (!io_read_int(sess, fdin, &ioerror)) {
			ERRX1(sess, "io_read_int");
			goto out;
		} else if (ioerror != -1) {
			ERRX(sess, "expected phase ack");
			goto out;
		}
	}

	/*
	 * Now all of our transfers are complete, so we can fix up our
	 * directory permissions.
	 */

	if (!rsync_uploader_tail(ul, sess)) {
		ERRX1(sess, "rsync_uploader_tail");
		goto out;
	}

	/* Process server statistics and say good-bye. */

	if (!sess_stats_recv(sess, fdin)) {
		ERRX1(sess, "sess_stats_recv");
		goto out;
	} else if (!io_write_int(sess, fdout, -1)) {
		ERRX1(sess, "io_write_int");
		goto out;
	}

	LOG2(sess, "receiver finished updating");
	rc = 1;
out:
	if (dfd != -1)
		close(dfd);
	upload_free(ul);
	download_free(dl);
	flist_free(fl, flsz);
	flist_free(dfl, dflsz);
	return rc;
}