/* * Copyright (c) 2001 Quartet Network Storage, Inc. * Author: Alfred Perlstein , * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * * $QNS: cvswrap.c,v 1.7 2002/01/10 22:40:59 bright Exp $ */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "config.h" /* uncomment the following line get debug tracing */ /* #define CVSWRAP_DEBUG */ #ifndef PATH_REALCVS #define PATH_REALCVS "/usr/bin/ncvs" #endif #ifndef PATH_CONFFILE #define PATH_CONFFILE "/etc/cvswrap.conf" #endif /* max amount of data to buffer to parse the cvsroot in server mode */ #define CLIENT_BUFSIZE (64 * 1024) /* number of seconds to wait for client to pass us cvsroot in server mode */ #define CLIENT_WAIT 10 #ifdef CVSWRAP_DEBUG #define bug(fmt, args...) \ do { \ fprintf(stderr, "\n%s %05d : ", \ FUNCDEF, __LINE__); \ fprintf(stderr, fmt "\n", ## args); \ } while (0) #else #define bug(fmt, args...) #endif #define SECS_TO_MILLISECS(secs) ((secs) * 1000) #define MICROSECS_TO_SECS(micro) ((micro) / 1000) static char * cvsprog_default_get(void); static char * cvsprog_get(const char *cvsroot); static void cvsprog_fork(const char *cvsprog, char **argv, pid_t *childpidp, int *childfdp); static char * cvsprog_parseconf(const char *cvsroot); static void cvsprog_tee(int wrfd, const char *buf, size_t buflen); static int cvsprog_wait(pid_t pid); static int fd_read_wait(int fd, int period, struct timeval *tp); static int getkeyval(char **strp, char **keyp, char **valp); static unsigned char * getcvsrootfromdir(void); static unsigned char * getcvsrootfromdir_helper(FILE *); static char * parse_line(const char *cvsroot, char *line); static void servermode_parseandbuffer(char **buf, size_t *buflen, char **cvsroot); static int servermode_parseroot(const char *buf, size_t *posp, char **cvsrootp); static void slashfix(char *); static int strings_have_line(char * const *strings, const char *line); static char * strndup(const char *str, size_t len); static int timeval_diff(const struct timeval *tv1, const struct timeval *tv2); static int writebuf(int fd, const char *buf, size_t len); static u_int32_t getoptiosiz(const int fd1, const int fd2); int main(argc, argv) int argc; char **argv; { size_t buflen; int ch, childfd; char *cvsroot, *cvsprog, *buf; pid_t childpid; buf = cvsroot = NULL; /* suppress messages from getopt for opts we don't care about */ opterr = 0; /* search for the -d /cvsroot directive */ while ((ch = getopt(argc, argv, "d:")) != -1) { if (ch == 'd' && optarg != NULL) { cvsroot = optarg; break; } } if (cvsroot == NULL) { /* * parse the cvs protocol if we are invoked with 'server' * as an arg, otherwise try to get CVSROOT from ./CVS/Root * the the environment. */ bug("eh?"); if (strings_have_line(argv, "server")) { servermode_parseandbuffer(&buf, &buflen, &cvsroot); } else { cvsroot = getcvsrootfromdir(); if (cvsroot == NULL) cvsroot = getenv("CVSROOT"); } } bug("cvsroot = %s\n", cvsroot); /* * ok, now clean up the cvsroot: * remove the leading remote/pserver * remove any extranious slashes in the path */ if (cvsroot != NULL) { char *p; p = rindex(cvsroot, ':'); p = (p == NULL) ? cvsroot : p + 1; cvsroot = strdup(p); if (cvsroot == NULL) err(1, "strdup"); slashfix(cvsroot); } /* parse the config file setting uid/gid and running programs */ cvsprog = cvsprog_get(cvsroot); bug("cvsprog = %s\n", cvsprog); /* if we didn't need to read stdin, then just exec. */ if (buf == NULL && execv(cvsprog, argv)) err(1, "exec"); /* * otherwise we need to pipe our buffered data to a forked copy. */ /* fork, give back child pid and its stdin fd. */ cvsprog_fork(cvsprog, argv, &childpid, &childfd); /* pass buffered data and stdin to the forked child. */ cvsprog_tee(childfd, buf, buflen); /* close our pipe to the child so it will exit */ (void)close(childfd); /* return the child's exit code or error */ return (cvsprog_wait(childpid)); } /* * return: true if the NULL terminated string list contains "line" */ static int strings_have_line(char * const *strings, const char *line) { while (*strings != NULL && strcmp(*strings, line) != 0) strings++; return (*strings == NULL ? 0 : 1); } /* * return the difference between the two timevals in milliseconds */ static int timeval_diff(const struct timeval *tv1, const struct timeval *tv2) { struct timeval tv3; timersub(tv1, tv2, &tv3); return (SECS_TO_MILLISECS(tv3.tv_sec) + MICROSECS_TO_SECS(tv3.tv_sec)); } /* * Considering start time "*tm_startp" wait on filedescriptor "fd" * for up to "period" seconds for readable data. */ static int fd_read_wait(int fd, int period, struct timeval *tm_startp) { struct pollfd pfd; struct timeval tm_now; int timeleft; bug("start"); pfd.fd = fd; pfd.events = POLLRDNORM; pfd.revents = 0; if (gettimeofday(&tm_now, NULL) != 0) err(1, "gettimeofday"); timeleft = SECS_TO_MILLISECS(period) - timeval_diff(tm_startp, &tm_now); if (timeleft <= 0) { bug("timeleft"); return (-1); } if (poll(&pfd, 1, timeleft) != 1) { bug("poll"); return (-1); } if ((pfd.revents & pfd.events) == 0) { bug("events"); return (-1); } return (0); } /* * read from stdin until we get our line "Root /cvs/foo/\n" * return the buffered data, its length and the "/cvs/foo" portion */ static void servermode_parseandbuffer(char **bufp, size_t *buflenp, char **cvsrootp) { int fd; ssize_t error, nread; size_t pos; char *rbuf; struct timeval tm_start; bug("start"); *bufp = rbuf = malloc(CLIENT_BUFSIZE + 1); if (rbuf == NULL) err(1, "malloc"); fd = fileno(stdin); nread = 0; pos = 0; if (gettimeofday(&tm_start, NULL) != 0) err(1, "gettimeofday"); while (nread < CLIENT_BUFSIZE) { if (fd_read_wait(fd, CLIENT_WAIT, &tm_start) == -1) break; error = read(fd, rbuf + nread, CLIENT_BUFSIZE - nread); if (error == -1 || error == 0) break; nread += error; *(rbuf + nread) = '\0'; bug("read: %s\n<<<<<", rbuf); if (servermode_parseroot(rbuf, &pos, cvsrootp)) break; } bug("cvsroot = %s", *cvsrootp == NULL ? "NULL" : *cvsrootp); *buflenp = nread; } static int servermode_parseroot(const char *buf, size_t *posp, char **cvsrootp) { const char *root = "Root"; const int rootlen = sizeof("Root") - 1; const char *p, *mayberoot; p = buf + *posp; again: while (*p != '\0' && !isalpha(*p)) p++; if (strncmp(p, root, rootlen) == 0) { if (*(p + rootlen) == '\0') return (0); p += rootlen; /* It's not 'Root' but 'RootFoo' or something too long */ if (!isblank(*p)) { /* skip to next line or end of buffer */ p += strcspn(p, "\n\r"); while (isspace(*p)) p++; /* if there's more to parse then start over */ if (*p != '\0') { *posp = p - buf; goto again; } /* otherwise we'll redo this line next time around */ return (0); } /* Skip any whitespace to get at the CVSROOT */ while (isblank(*p)) p++; /* Nothing? Well that's not good return an error. */ if (isspace(*p)) return (-1); /* Ok, we found a possible root or we're at end of buffer */ mayberoot = p; /* Skip over the root if it's there */ while (*p != '\0' && !isspace(*p)) p++; /* Did we hit the end of buffer before newline? try again */ if (*p == '\0') return (0); /* we got it, try to make a copy */ *cvsrootp = strndup(mayberoot, p - mayberoot); if (*cvsrootp == NULL) err(1, "strndup"); return (1); } return (0); } /* * return: a copy of "len" bytes from byte string "str" and NULL terminate. */ static char * strndup(const char *str, size_t len) { char *ret; ret = malloc(len + 1); if (ret == NULL) return (NULL); if (memccpy(ret, str, '\0', len) == NULL) *(ret + len) = '\0'; return (ret); } /* * Wait for child cvs process to exit. * return: if child exited normally return exit status of child * otherwise return EXIT_FAILURE */ static int cvsprog_wait(pid_t pid) { int status; while (waitpid(pid, &status, 0) == -1) { if (errno != EINTR) err(1, "watipid"); } return (WIFEXITED(status) ? WEXITSTATUS(status) : EXIT_FAILURE); } /* * Spawn child cvs program * return: its pid and file descriptor for stdin */ static void cvsprog_fork(cvsprog, argv, childpidp, childfdp) const char *cvsprog; char **argv; pid_t *childpidp; int *childfdp; { int fildes[2]; int rdfd, wrfd; pid_t pid; if (pipe(fildes) == -1) err(1, "pipe"); rdfd = fildes[0]; wrfd = fildes[1]; pid = fork(); if (pid == -1) err(1, "fork"); if (pid == 0) { if (close(wrfd) == -1) err(1, "child: close"); if (dup2(rdfd, STDIN_FILENO) == -1) err(1, "child: dup2"); if (execv(cvsprog, argv) == -1) err(1, "child: exec"); } (void)close(rdfd); *childpidp = pid; *childfdp = wrfd; } /* * pipe the buffer and the rest of stdin to the child's fd */ static void cvsprog_tee(int wrfd, const char *buf, size_t buflen) { int stdinfd; u_int32_t optiosiz; char *optiobuf; if (writebuf(wrfd, buf, buflen) == -1) { if (errno == EPIPE) return; errx(1, "parent: write: buf(%p), buflen(%d) %d - %s", buf, buflen, errno, strerror(errno)); } /* * determine the best IO size to use */ stdinfd = fileno(stdin); optiosiz = getoptiosiz(stdinfd, wrfd); optiobuf = malloc(optiosiz); if (optiobuf == NULL) err(1, "optio"); for ( ;; ) { ssize_t bytesread; bytesread = read(stdinfd, optiobuf, optiosiz); if (bytesread == -1) errx(1, "parent: read"); if (bytesread == 0) break; if (writebuf(wrfd, optiobuf, bytesread) == -1) { if (errno != EPIPE) errx(1, "parent: write"); break; } } free(optiobuf); } /* * write the buffer to the filedescriptor * return: -1 if write fails for any reason, 0 if successful */ static int writebuf(int fd, const char *buf, size_t len) { ssize_t written; size_t i; for (i = 0; i < len; i += written) { written = write(fd, buf + i, len - i); if (written == -1) return (-1); } return (0); } /* * return: the struct stat st_blksize field (optimal IO size) * for the passed descriptor. */ static u_int32_t get_st_blksize(const int fd) { struct stat sb; return (fstat(fd, &sb) == -1 ? 0 : sb.st_blksize); } /* * return: the optimal IO size or PIPE_BUF of the two passed descriptors */ static u_int32_t getoptiosiz(const int fd1, const int fd2) { u_int32_t opt1, opt2; u_int32_t ret; opt1 = get_st_blksize(fd1); opt2 = get_st_blksize(fd2); ret = opt1 > opt2 ? opt1 : opt2; return (ret < PIPE_BUF ? PIPE_BUF : ret); } /* * get the values to setuid/setgid to and do it. */ static char * cvsprog_get(cvsroot) const char *cvsroot; { char *ret; ret = cvsprog_parseconf(cvsroot); if (ret != NULL) return (ret); return (cvsprog_default_get()); } static char *cvsprog_default = NULL; static char * cvsprog_default_get(void) { return (cvsprog_default == NULL ? PATH_REALCVS : cvsprog_default); } static void cvsprog_default_set(const char *cvsprog) { if (cvsprog_default != NULL) free(cvsprog_default); if (cvsprog == NULL) { cvsprog_default = NULL; return; } cvsprog_default = strdup(cvsprog); if (cvsprog_default == NULL) err(1, "strdup"); } static char * cvsprog_parseconf(cvsroot) const char *cvsroot; { FILE *fp; char *line, *freeme = NULL, *ret; size_t len, malloclen; bug("start"); malloclen = 0; if (cvsroot == NULL) return (NULL); fp = fopen(PATH_CONFFILE, "r"); bug("fopen(%s) %s\n", PATH_CONFFILE, fp == NULL ? "bad" : "ok"); if (fp == NULL) return (NULL); while ((line = fgetln(fp, &len)) != NULL) { if (freeme != NULL) { free(freeme); freeme = NULL; } /* NULL terminate the line or make a copy if we need to. */ if (line[len - 1] == '\n') { bug("newline"); line[len - 1] = '\0'; } else { bug("allocline"); /* * if we have a buffer and it's not big enough * then drop it */ if (freeme != NULL && len - 1 >= malloclen) { free(freeme); freeme = NULL; } if (freeme == NULL) { freeme = malloc(len + 1); if (freeme == NULL) err(1, "malloc"); malloclen = len + 1; } memcpy(freeme, line, len); line[len] = '\0'; } bug("line: %s", line); ret = parse_line(cvsroot, line); if (ret != NULL) break; } if (freeme != NULL) free(freeme); fclose(fp); return (ret); } /* * remove all redundant and trailing occurances of '/' */ static void slashfix(string) char *string; { char *p, *str; p = str = string; while (*str) { *p++ = *str; while (*str == '/' && *(str + 1) == '/') str++; str++; } str--; while (str >= string && *str == '/') *str-- = '\0'; } /* * parse the line provided. * return: cvs program to execute based on cvsroot or NULL */ static char * parse_line(cvsroot, line) const char *cvsroot; char *line; { int setglobal; char *dir, *p, *key, *val, *ret; setglobal = 0; ret = NULL; /* remove comments */ while ((p = rindex(line, '#')) != NULL) *p = '\0'; /* trim leading whitespace */ while (isspace(*line)) line++; /* if the line is empty stop here */ if (*line == '\0') goto out; dir = p = line; /* get to the end of the first string and NULL terminate it */ while (*p && !isspace(*p)) p++; /* if we've hit the end of the line then it's bogus. complain? */ if (*p == NULL) goto out; *p++ = '\0'; slashfix(dir); /* is this the line with our cvsroot? */ if (*dir == '*') setglobal = 1; else if (strcmp(dir, cvsroot)) goto out; while (getkeyval(&p, &key, &val)) { bug("key = %s, val = %s", key, val); if (strcmp(key, "prog") == 0 && val != NULL) { if (setglobal) { cvsprog_default_set(val); } else { ret = strdup(val); if (ret == NULL) err(1, "strdup"); } goto out; } } out: bug("ret: %s", ret == NULL ? "NULL" : ret); return (ret); } static int getkeyval(strp, keyp, valp) char **strp, **keyp, **valp; { char *p; if (strp == NULL || *strp == NULL) goto bad; p = *strp; /* skip leading spaces */ while (isspace(*p)) p++; /* EOL? error */ if (*p == '\0') goto bad; /* we've got the key */ *keyp = p; /* skip over the key */ while (isalnum(*p)) p++; /* skip over and NULL out spaces after key */ while (isspace(*p)) *p++ = '\0'; /* * there should be an equal sign, * skip over it or error if it's not there */ if (*p != '=') goto bad; *p++ = '\0'; while (isspace(*p)) *p++ = '\0'; /* EOL means no value for our key, error */ if (*p == '\0') goto bad; *valp = p; /* skip over the value now that we have it */ while (*p && !isspace(*p) && *p != ',') p++; /* * if we're at the EOL then NULL out strp, * otherwise set it to the character after the one we're going to * NULL out */ *strp = (*p == '\0') ? NULL : p + 1; *p = '\0'; return (1); bad: *strp = *keyp = *valp = NULL; return (0); } static unsigned char * getcvsrootfromdir(void) { FILE *fp; unsigned char *ret; fp = fopen("CVS/Root", "r"); bug("fopen(CVS/Root) %s\n", fp == NULL ? "bad" : "ok"); if (fp == NULL) return (NULL); ret = getcvsrootfromdir_helper(fp); bug("ret = %s\n", ret); fclose(fp); return (ret); } static unsigned char * getcvsrootfromdir_helper(fp) FILE *fp; { size_t len; unsigned char *line, *ret; if ((line = fgetln(fp, &len)) == NULL) return (NULL); if ((ret = malloc(len + 1)) == NULL) return (NULL); memcpy(ret, line, len); ret[len] = '\0'; len--; while (isspace(ret[len]) && len > 0) ret[len--] = '\0'; return (ret); }