410 lines
9.3 KiB
C
410 lines
9.3 KiB
C
/* $OpenBSD: rsync.c,v 1.51 2024/08/20 13:31:49 claudio Exp $ */
|
|
/*
|
|
* Copyright (c) 2019 Kristaps Dzonsons <kristaps@bsd.lv>
|
|
*
|
|
* 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/queue.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/wait.h>
|
|
#include <netinet/in.h>
|
|
#include <err.h>
|
|
#include <errno.h>
|
|
#include <poll.h>
|
|
#include <resolv.h>
|
|
#include <signal.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
#include <imsg.h>
|
|
|
|
#include "extern.h"
|
|
|
|
#define __STRINGIFY(x) #x
|
|
#define STRINGIFY(x) __STRINGIFY(x)
|
|
|
|
/*
|
|
* A running rsync process.
|
|
* We can have multiple of these simultaneously and need to keep track
|
|
* of which process maps to which request.
|
|
*/
|
|
struct rsync {
|
|
TAILQ_ENTRY(rsync) entry;
|
|
char *uri; /* uri of this rsync proc */
|
|
char *dst; /* destination directory */
|
|
char *compdst; /* compare against directory */
|
|
unsigned int id; /* identity of request */
|
|
pid_t pid; /* pid of process or 0 if unassociated */
|
|
};
|
|
|
|
static TAILQ_HEAD(, rsync) states = TAILQ_HEAD_INITIALIZER(states);
|
|
|
|
/*
|
|
* Return the base of a rsync URI (rsync://hostname/module). The
|
|
* caRepository provided by the RIR CAs point deeper than they should
|
|
* which would result in many rsync calls for almost every subdirectory.
|
|
* This is inefficient so instead crop the URI to a common base.
|
|
* The returned string needs to be freed by the caller.
|
|
*/
|
|
char *
|
|
rsync_base_uri(const char *uri)
|
|
{
|
|
const char *host, *module, *rest;
|
|
char *base_uri;
|
|
|
|
/* Case-insensitive rsync URI. */
|
|
if (strncasecmp(uri, RSYNC_PROTO, RSYNC_PROTO_LEN) != 0) {
|
|
warnx("%s: not using rsync schema", uri);
|
|
return NULL;
|
|
}
|
|
|
|
/* Parse the non-zero-length hostname. */
|
|
host = uri + 8;
|
|
|
|
if ((module = strchr(host, '/')) == NULL) {
|
|
warnx("%s: missing rsync module", uri);
|
|
return NULL;
|
|
} else if (module == host) {
|
|
warnx("%s: zero-length rsync host", uri);
|
|
return NULL;
|
|
}
|
|
|
|
/* The non-zero-length module follows the hostname. */
|
|
module++;
|
|
if (*module == '\0') {
|
|
warnx("%s: zero-length rsync module", uri);
|
|
return NULL;
|
|
}
|
|
|
|
/* The path component is optional. */
|
|
if ((rest = strchr(module, '/')) == NULL) {
|
|
if ((base_uri = strdup(uri)) == NULL)
|
|
err(1, NULL);
|
|
return base_uri;
|
|
} else if (rest == module) {
|
|
warnx("%s: zero-length module", uri);
|
|
return NULL;
|
|
}
|
|
|
|
if ((base_uri = strndup(uri, rest - uri)) == NULL)
|
|
err(1, NULL);
|
|
return base_uri;
|
|
}
|
|
|
|
/*
|
|
* The directory passed as --compare-dest needs to be relative to
|
|
* the destination directory. This function takes care of that.
|
|
*/
|
|
static char *
|
|
rsync_fixup_dest(char *destdir, char *compdir)
|
|
{
|
|
const char *dotdot = "../../../../../../"; /* should be enough */
|
|
int dirs = 1;
|
|
char *fn;
|
|
char c;
|
|
|
|
while ((c = *destdir++) != '\0')
|
|
if (c == '/')
|
|
dirs++;
|
|
|
|
if (dirs > 6)
|
|
/* too deep for us */
|
|
return NULL;
|
|
|
|
if ((asprintf(&fn, "%.*s%s", dirs * 3, dotdot, compdir)) == -1)
|
|
err(1, NULL);
|
|
return fn;
|
|
}
|
|
|
|
static pid_t
|
|
exec_rsync(const char *prog, const char *bind_addr, char *uri, char *dst,
|
|
char *compdst)
|
|
{
|
|
pid_t pid;
|
|
char *args[32];
|
|
char *reldst;
|
|
int i;
|
|
|
|
if ((pid = fork()) == -1)
|
|
err(1, "fork");
|
|
|
|
if (pid == 0) {
|
|
if (pledge("stdio exec", NULL) == -1)
|
|
err(1, "pledge");
|
|
i = 0;
|
|
args[i++] = (char *)prog;
|
|
args[i++] = "-rtO";
|
|
args[i++] = "--no-motd";
|
|
args[i++] = "--min-size=" STRINGIFY(MIN_FILE_SIZE);
|
|
args[i++] = "--max-size=" STRINGIFY(MAX_FILE_SIZE);
|
|
args[i++] = "--contimeout=" STRINGIFY(MAX_CONN_TIMEOUT);
|
|
args[i++] = "--timeout=" STRINGIFY(MAX_IO_TIMEOUT);
|
|
args[i++] = "--include=*/";
|
|
args[i++] = "--include=*.cer";
|
|
args[i++] = "--include=*.crl";
|
|
args[i++] = "--include=*.gbr";
|
|
args[i++] = "--include=*.mft";
|
|
args[i++] = "--include=*.roa";
|
|
args[i++] = "--include=*.asa";
|
|
args[i++] = "--include=*.tak";
|
|
args[i++] = "--include=*.spl";
|
|
args[i++] = "--exclude=*";
|
|
if (bind_addr != NULL) {
|
|
args[i++] = "--address";
|
|
args[i++] = (char *)bind_addr;
|
|
}
|
|
if (compdst != NULL &&
|
|
(reldst = rsync_fixup_dest(dst, compdst)) != NULL) {
|
|
args[i++] = "--compare-dest";
|
|
args[i++] = reldst;
|
|
}
|
|
args[i++] = uri;
|
|
args[i++] = dst;
|
|
args[i] = NULL;
|
|
/* XXX args overflow not prevented */
|
|
execvp(args[0], args);
|
|
err(1, "%s: execvp", prog);
|
|
}
|
|
|
|
return pid;
|
|
}
|
|
|
|
static void
|
|
rsync_new(unsigned int id, char *uri, char *dst, char *compdst)
|
|
{
|
|
struct rsync *s;
|
|
|
|
if ((s = calloc(1, sizeof(*s))) == NULL)
|
|
err(1, NULL);
|
|
|
|
s->id = id;
|
|
s->uri = uri;
|
|
s->dst = dst;
|
|
s->compdst = compdst;
|
|
|
|
TAILQ_INSERT_TAIL(&states, s, entry);
|
|
}
|
|
|
|
static void
|
|
rsync_free(struct rsync *s)
|
|
{
|
|
TAILQ_REMOVE(&states, s, entry);
|
|
free(s->uri);
|
|
free(s->dst);
|
|
free(s->compdst);
|
|
free(s);
|
|
}
|
|
|
|
static void
|
|
proc_child(int signal)
|
|
{
|
|
|
|
/* Nothing: just discard. */
|
|
}
|
|
|
|
/*
|
|
* Process used for synchronising repositories.
|
|
* This simply waits to be told which repository to synchronise, then
|
|
* does so.
|
|
* It then responds with the identifier of the repo that it updated.
|
|
* It only exits cleanly when fd is closed.
|
|
*/
|
|
void
|
|
proc_rsync(char *prog, char *bind_addr, int fd)
|
|
{
|
|
int nprocs = 0, npending = 0, rc = 0;
|
|
struct pollfd pfd;
|
|
struct msgbuf msgq;
|
|
struct ibuf *b, *inbuf = NULL;
|
|
sigset_t mask, oldmask;
|
|
struct rsync *s, *ns;
|
|
|
|
if (pledge("stdio rpath proc exec unveil", NULL) == -1)
|
|
err(1, "pledge");
|
|
|
|
pfd.fd = fd;
|
|
msgbuf_init(&msgq);
|
|
msgq.fd = fd;
|
|
|
|
/*
|
|
* Unveil the command we want to run.
|
|
* If this has a pathname component in it, interpret as a file
|
|
* and unveil the file directly.
|
|
* Otherwise, look up the command in our PATH.
|
|
*/
|
|
|
|
if (strchr(prog, '/') == NULL) {
|
|
const char *pp;
|
|
char *save, *cmd, *path;
|
|
struct stat stt;
|
|
|
|
if (getenv("PATH") == NULL)
|
|
errx(1, "PATH is unset");
|
|
if ((path = strdup(getenv("PATH"))) == NULL)
|
|
err(1, NULL);
|
|
save = path;
|
|
while ((pp = strsep(&path, ":")) != NULL) {
|
|
if (*pp == '\0')
|
|
continue;
|
|
if (asprintf(&cmd, "%s/%s", pp, prog) == -1)
|
|
err(1, NULL);
|
|
if (lstat(cmd, &stt) == -1) {
|
|
free(cmd);
|
|
continue;
|
|
} else if (unveil(cmd, "x") == -1)
|
|
err(1, "%s: unveil", cmd);
|
|
free(cmd);
|
|
break;
|
|
}
|
|
free(save);
|
|
} else if (unveil(prog, "x") == -1)
|
|
err(1, "%s: unveil", prog);
|
|
|
|
if (pledge("stdio proc exec", NULL) == -1)
|
|
err(1, "pledge");
|
|
|
|
/* Initialise retriever for children exiting. */
|
|
|
|
if (sigemptyset(&mask) == -1)
|
|
err(1, NULL);
|
|
if (signal(SIGCHLD, proc_child) == SIG_ERR)
|
|
err(1, NULL);
|
|
if (sigaddset(&mask, SIGCHLD) == -1)
|
|
err(1, NULL);
|
|
if (sigprocmask(SIG_BLOCK, &mask, &oldmask) == -1)
|
|
err(1, NULL);
|
|
|
|
for (;;) {
|
|
char *uri, *dst, *compdst;
|
|
unsigned int id;
|
|
pid_t pid;
|
|
int st;
|
|
|
|
pfd.events = 0;
|
|
pfd.events |= POLLIN;
|
|
if (msgbuf_queuelen(&msgq) > 0)
|
|
pfd.events |= POLLOUT;
|
|
|
|
if (npending > 0 && nprocs < MAX_RSYNC_REQUESTS) {
|
|
TAILQ_FOREACH(s, &states, entry) {
|
|
if (s->pid == 0) {
|
|
s->pid = exec_rsync(prog, bind_addr,
|
|
s->uri, s->dst, s->compdst);
|
|
if (++nprocs >= MAX_RSYNC_REQUESTS)
|
|
break;
|
|
if (--npending == 0)
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (ppoll(&pfd, 1, NULL, &oldmask) == -1) {
|
|
if (errno != EINTR)
|
|
err(1, "ppoll");
|
|
|
|
/*
|
|
* If we've received an EINTR, it means that one
|
|
* of our children has exited and we can reap it
|
|
* and look up its identifier.
|
|
* Then we respond to the parent.
|
|
*/
|
|
|
|
while ((pid = waitpid(WAIT_ANY, &st, WNOHANG)) > 0) {
|
|
int ok = 1;
|
|
|
|
TAILQ_FOREACH(s, &states, entry)
|
|
if (s->pid == pid)
|
|
break;
|
|
if (s == NULL)
|
|
errx(1, "waitpid: %d unexpected", pid);
|
|
|
|
if (!WIFEXITED(st)) {
|
|
warnx("rsync %s terminated abnormally",
|
|
s->uri);
|
|
rc = 1;
|
|
ok = 0;
|
|
} else if (WEXITSTATUS(st) != 0) {
|
|
warnx("rsync %s failed", s->uri);
|
|
ok = 0;
|
|
}
|
|
|
|
b = io_new_buffer();
|
|
io_simple_buffer(b, &s->id, sizeof(s->id));
|
|
io_simple_buffer(b, &ok, sizeof(ok));
|
|
io_close_buffer(&msgq, b);
|
|
|
|
rsync_free(s);
|
|
nprocs--;
|
|
}
|
|
if (pid == -1 && errno != ECHILD)
|
|
err(1, "waitpid");
|
|
|
|
continue;
|
|
}
|
|
|
|
if (pfd.revents & POLLOUT) {
|
|
switch (msgbuf_write(&msgq)) {
|
|
case 0:
|
|
errx(1, "write: connection closed");
|
|
case -1:
|
|
err(1, "write");
|
|
}
|
|
}
|
|
|
|
/* connection closed */
|
|
if (pfd.revents & POLLHUP)
|
|
break;
|
|
|
|
if (!(pfd.revents & POLLIN))
|
|
continue;
|
|
|
|
b = io_buf_read(fd, &inbuf);
|
|
if (b == NULL)
|
|
continue;
|
|
|
|
/* Read host and module. */
|
|
io_read_buf(b, &id, sizeof(id));
|
|
io_read_str(b, &dst);
|
|
io_read_str(b, &compdst);
|
|
io_read_str(b, &uri);
|
|
|
|
ibuf_free(b);
|
|
|
|
if (dst != NULL) {
|
|
rsync_new(id, uri, dst, compdst);
|
|
npending++;
|
|
} else {
|
|
TAILQ_FOREACH(s, &states, entry)
|
|
if (s->id == id)
|
|
break;
|
|
if (s != NULL) {
|
|
if (s->pid != 0)
|
|
kill(s->pid, SIGTERM);
|
|
else
|
|
rsync_free(s);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* No need for these to be hanging around. */
|
|
TAILQ_FOREACH_SAFE(s, &states, entry, ns) {
|
|
if (s->pid != 0)
|
|
kill(s->pid, SIGTERM);
|
|
rsync_free(s);
|
|
}
|
|
|
|
msgbuf_clear(&msgq);
|
|
exit(rc);
|
|
}
|