src/usr.sbin/rpki-client/rsync.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);
}