/* Copyright (C) , Loic Dachary , 2001 Copyright (C) 2005, 2006 Sergey Poznyakoff This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ /* sv_sync_www.c - Allow unprivileged users to do a web update on www.gnu.org.ua In CVSROOT/loginfo file ALL /bin/sv_sync_www_schedule ${USER} ${CVSROOT} %s In crontab (remove the backslash before /): 0 *\/1 * * * /bin/sv_sync_www -s /var/spool/savane Or, to synchronize right after commit, in CVSROOT/loginfo: ALL (date; cat; (sleep 2; /bin/sv_sync_www %{s} ) &\ ) \ >> /var/log/sv_sync_www 2>&1 For test purpose: ./sv_sync_www -n ' bla bla' ./sv_sync_www -n 'foo/bar bla bla' ./sv_sync_www -n 'f'\''oo bla bla' ./sv_sync_www -n 'f'\''oo - New directory' ./sv_sync_www -n 'bla - Imported sources' The idea is that it runs a cvs update -l (to prevent recursion) in the directory where the commit was done. Since the command will be called once for each directory where a commit did some action there is no need for recursion. In the case of an import command this does not hold and a recursion must always be done since there is only one call to the script for a whole imported tree (this happens when the argument contains the Imported source string). After running update, the program looks for file ".symlinks" and executes it, when present. File format is: empty lines and lines started with ';' or '#' are ignored. Rest of lines are split in two words: first word is the name of an existing file, second one is the link name. Both names should be in the current directory. Care is taken to prevent linking to upper-level directories. The %{s} argument in loginfo invocation is a single argument that lists the directory and all the files involved. As a special case if the directory was added the file list is replaced by '- New directory'. This is lame since adding the files -, New and directory will produce the same effect, but it's unlikely. The same applies when a whole source tree is imported using cvs import in which case the file list is replaced by '- Imported sources'. There are three cases to take in account (topdir is the absolute path of the directory in which the CVS tree was extracted, subdirectory is the directory given in argument): - commit that modify the top level directory files cd topdir ; cvs update -l - commit that adds a new directory or that import a whole source tree cd topdir ; cvs update 'subdirectory' - commit that modify files in a subdirectory cd topdir/subdirectory ; cvs update -l In order to prevent security compromise the directory name is quoted. Originaly by Gordon Matzigkeit , 2000-11-28 Update CVS_COMMANDS to reduce noise Loic Dachary , 2001-02-26 Modify to allow generic call from loginfo file in an efficient way Loic Dachary , 2001-03-10 Introduce command line options, make program configurable and implement .symlinks functionality Sergey Poznyakoff , 2005-07-31 Implement 'scan directory' mode Sergey Poznyakoff , 2005-02-09 */ #ifdef HAVE_CONFIG_H # include #endif #ifndef _GNU_SOURCE # define _GNU_SOURCE #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "gsc.h" #define obstack_chunk_alloc malloc #define obstack_chunk_free free #include char *rsh_program = "/usr/bin/ssh"; char *hostname; char *default_repository = "/webcvs"; char *default_spooldir = "/var/spool/savane"; char *access_method = NULL; char *www_directory = "/home/puszcza/software"; char *cvs_command = "CVS_RSH=ssh cvs -q -z3"; uid_t savane_uid; gid_t savane_gid; uid_t min_uid = 100; gid_t min_gid = 100; int dry_run = 0; int debug = 0; int verbose_level = 0; /* Utility functions */ /* * Return a malloc'ed copy of the string argument with all ' * substituted by '\'' and a trailing and leading '. * For instance : foo -> 'foo' * fib'ou -> 'fib'\''ou' */ char * quote (const char *string) { int i; int length; int count = 0; char *out; if (!string) return NULL; length = strlen (string); for (i = 0; i < length; i++) if (string[i] == '\'') count++; /* +1 for null at end, (count * 3) for '\'', +2 for leading ' and trailing ' */ out = (char *) malloc (length + 1 + count * 3 + 2); if (count > 0) { const char *from_p = string; char *to_p = out; *to_p++ = '\''; while (*from_p) { if (*from_p == '\'') { strcpy (to_p, "'\\''"); to_p += 4; from_p++; } else *to_p++ = *from_p++; } *to_p++ = '\''; *to_p = '\0'; } else sprintf (out, "'%s'", string); return out; } void usage () { printf ("sv_sync_www [OPTIONS] DIRECTORY\n\n"); printf ("OPTIONS are:\n"); printf ("\nMode selection:\n"); printf (" -h, --help Display this help summary\n"); printf (" -s, --cron Cron job mode: process each file in DIRECTORY\n"); printf (" The argument is optional, it defaults to %s\n", default_spooldir); printf ("\nMode modification:\n"); printf (" -n, --dry-run Dry run: do nothing, only print what would have been done\n"); printf (" -x, --debug Enable debugging output\n"); printf ("\nConfiguration:\n"); printf (" -c, --cvs-command=COMMAND\n"); printf (" Set cvs command to use (default %s)\n", cvs_command); printf (" -H, --hostname=HOSTNAME\n"); printf (" Set hostname or IP address of the host where WWW directory\n"); printf (" resides (default: none, WWW and CVS reside on the same host)\n"); printf (" -m, --method=METHOD\n"); printf (" Set default access method. METHOD will be prepended to\n"); printf (" REPOSITORY (see -R option below) before accessing the\n"); printf (" repository\n"); printf (" -r, --rsh-program=RSH_PROGRAM\n"); printf (" Set full name of RSH program\n"); printf (" (default: %s)\n", rsh_program); printf (" -R, --repository=REPOSITORY\n"); printf (" Set full name of the CVS repository\n"); printf (" (default: %s)\n", default_repository); printf (" -w, --destination-directory, --www-directory=DIRECTORY\n"); printf (" Set full name of the WWW directory\n"); printf (" (default: %s)\n", www_directory); printf (" -v, --verbose Increase verbosity level\n"); printf (" -U, --min-uid=UID Set minimum allowable UID value\n"); printf (" -G, --min-gid=GID Set minimum allowable GID value\n"); } void switch_to_privs (uid_t uid, gid_t gid) { if (gsc_userprivs (uid, &gid, 1)) error (1, 0, "%s", gsc_userprivs_errstring ()); } /* Core functions (single job support) */ static int run_cmd (char *cmd) { pid_t pid; char *xargv[6]; int xargc = 0; char *new_envp[] = { NULL }; char *p; char *program; pid = fork (); if (pid < 0) error (1, errno, "cannot fork"); if (pid > 0) /* Master process */ { int status; waitpid (pid, &status, 0); if (WIFEXITED (status)) { status = WEXITSTATUS (status); if (status) error (0, 0, "subprocess returned exit code %d", status); return status; } else if (WIFSIGNALED (status)) { error (0, 0, "subprocess terminated on signal %d", WTERMSIG (status)); return -1; } else { error (0, 0, "subprocess terminated"); return -1; } } if (hostname) { program = dry_run ? "/bin/echo" : rsh_program; xargv[xargc++] = rsh_program; xargv[xargc++] = hostname; } else { if (dry_run) { program = "/bin/echo"; xargv[xargc++] = "echo"; } else { program = "/bin/sh"; xargv[xargc++] = "-sh"; xargv[xargc++] = "-c"; if (debug) xargv[xargc++] = "-x"; } } xargv[xargc++] = cmd; xargv[xargc++] = NULL; /* Pick out the name of the program. */ p = strrchr (xargv[0], '/'); if (p) xargv[0] = p; execve (program, xargv, new_envp); error (1, errno, "Cannot exec `%s'", program); } int has_file_list (const char *arg) { /* * If this is a new directory creation or an import command, * there are no files to collect in the argument list. * Otherwise, there are files to collect. */ return (strcmp (arg, "- New directory") && strcmp (arg, "- Imported sources")); } static int split_arg (const char *arg, char **pdir, char **pfiles) { char *tmp; /* * First comes the relative directory name or empty string if current. * directory = 0 if no subdirectory * directory = relative path if subdirectory */ if ((tmp = strchr (arg, ' '))) { size_t len = tmp - arg; if (len > 0) { *pdir = malloc (len + 1); if (!*pdir) return -1; memcpy (*pdir, arg, len); (*pdir)[len] = 0; } else *pdir = NULL; *pfiles = strdup (tmp + 1); if (!*pfiles) { free (*pdir); return -1; } return 0; } return 1; } int sync_www (const char *repository, const char *unquoted_directory, int files) { int rc; char *directory = quote (unquoted_directory); char *cmd; /* Check that the local directory exists. */ if (!dry_run && files && unquoted_directory) { struct stat stbuf; char *localdir; size_t len = strlen (repository); localdir = malloc (len + 1 + strlen (unquoted_directory) + 1); if (!localdir) { error (0, 0, "not enough memory"); return 1; } strcpy (localdir, repository); if (localdir[len - 1] != '/') localdir[len++] = '/'; strcpy (localdir + len, unquoted_directory); /* Check to see that the local unquoted_directory actually exists. */ if (stat (localdir, &stbuf) || (!S_ISDIR (stbuf.st_mode) && (errno = ENOTDIR))) { error (0, errno, "Cannot find `%s'", localdir); return 1; } } /* Update from CVS */ if (directory && !files) asprintf (&cmd, "cd %s && ( %s -d %s%s checkout %s )", www_directory, cvs_command, access_method ? access_method : "", repository, directory); else if (!directory && files) asprintf (&cmd, "cd %s && ( %s update -l )", www_directory, cvs_command); else if (directory && files) asprintf (&cmd, "cd %s/%s && ( %s update -l )", www_directory, directory, cvs_command); else { error (0, 0, "Unexpected null directory and files"); return 1; } if (run_cmd (cmd)) return 1; free (cmd); /* Restore symlinks */ asprintf (&cmd, "cd %s/%s && if test -r .symlinks; then " "grep -v '^[ \t]*[;#]' .symlinks | " "sed 's,^/,_,;s,\\.\\./,__/,g' |" "while read S T; do ln -sf $S $T; done; fi", www_directory, directory); rc = run_cmd (cmd); free (cmd); return rc; } static int need_cleanup = 0; static RETSIGTYPE sig_child (int sig) { need_cleanup = 1; signal (sig, sig_child); } void setup_signals () { signal(SIGCHLD, sig_child); } void reset_signals () { signal(SIGCHLD, SIG_DFL); } /* Job control */ struct sync_entry { char *repo; char *dir; int files; }; int do_sync (struct sync_entry *array, size_t count); struct sync_job { const char *file; /* Control file */ pid_t pid; /* PID if the job is running 0 if the job is free */ }; struct sync_job *jobtab; size_t job_max = 128; size_t total_files; size_t processed_files; void setup_jobs () { jobtab = calloc (job_max, sizeof (jobtab[0])); if (!jobtab) error (1, errno, "cannot initialize job table"); } struct sync_job * find_job_pid (pid_t pid) { struct sync_job *sp; for (sp = jobtab; sp < jobtab + job_max; sp++) if (sp->pid == pid) return sp; return NULL; } size_t job_count () { struct sync_job *sp; size_t count = 0; for (sp = jobtab; sp < jobtab + job_max; sp++) if (sp->pid) count++; return count; } int waitjob () { pid_t pid; int status; int rc = 0; while ((pid = waitpid (-1, &status, WNOHANG)) > 0) { struct sync_job *jp = find_job_pid (pid); if (!jp) { error (0, 0, "Unknown job terminated"); continue; } jp->pid = 0; rc++; total_files++; if (WIFEXITED (status)) { if (WEXITSTATUS (status)) error (0, 0, "Child %lu exited with status %d", (unsigned long) pid, WEXITSTATUS (status)); else { if (!dry_run && unlink (jp->file)) error (0, errno, "Cannot unlink file `%s'", jp->file); else processed_files++; } } else if (WIFSIGNALED (status)) error (0, 0, "Child %lu terminated on signal %d", (unsigned long) pid, WTERMSIG (status)); else error (0, 0, "Child %lu terminated, reason unknown", (unsigned long) pid); } return rc; } int schedule_job (uid_t uid, gid_t gid, const char *file, struct sync_entry *array, size_t count) { struct sync_job *jp; while (!waitjob () && job_count () == job_max) ; jp = find_job_pid (0); jp->pid = fork (); switch (jp->pid) { case -1: error (0, errno, "Cannot start process"); return -1; case 0: reset_signals (); switch_to_privs (savane_uid, savane_gid); exit (do_sync (array, count)); default: jp->file = file; } return 0; } /* Multi-job support */ int do_sync (struct sync_entry *array, size_t count) { int rc = 0; struct sync_entry *sp; for (sp = array; sp < array + count; sp++) rc |= sync_www (sp->repo, sp->dir, sp->files); if (debug) fprintf (stderr, "do_sync: returning %d\n",rc); return rc; } int sync_from_file (uid_t uid, const char *spooldir, const char *file) { char *buf = NULL; size_t size = 0; size_t line = 0; int err = 0; struct obstack stk; struct sync_entry *sp; struct sync_entry ent; struct sync_entry *sync_array; FILE *fp = fopen (file, "r"); if (!fp) { error (0, errno, "Cannot open file `%s/%s'", spooldir, file); return 1; } obstack_init (&stk); while (getline (&buf, &size, fp) > 0) { char *p; struct passwd *pw; p = buf + strlen (buf) - 1; if (*p == '\n') *p = 0; line++; p = strtok (buf, " \t"); if (!p) { error (0, 0, "%s/%s:%lu: Invalid line", spooldir, file, (unsigned long) line); err = 1; break; } pw = getpwnam (p); if (!pw) { error (0, 0, "%s/%s:%lu: cannot get password entry for user %s", spooldir, file, (unsigned long) line, p); err = 1; break; } if (uid != pw->pw_uid) { error (0, 0, "%s/%s:%lu: invalid uid", spooldir, file, (unsigned long) line); err = 1; break; } p = strtok (NULL, " \t"); if (!p) { error (0, 0, "%s/%s:%lu: Invalid line, second field is missing", spooldir, file, (unsigned long) line); err = 1; break; } ent.repo = strdup (p); if (!ent.repo) { error (0, ENOMEM, "%s/%s:%lu", spooldir, file, (unsigned long) line); err = 1; break; } p = strtok (NULL, ""); if (!p) { error (0, 0, "%s/%s:%lu: Invalid line, only two fields", spooldir, file, (unsigned long) line); err = 1; break; } err = split_arg (p, &ent.dir, &p); if (err) { error (0, err < 0 ? errno : 0, "%s/%s:%lu: Cannot split argument", spooldir, file, (unsigned long) line); break; } ent.files = has_file_list (p); free (p); obstack_grow (&stk, &ent, sizeof (ent)); } fclose (fp); sync_array = obstack_finish (&stk); if (err == 0) { if (getuid ()) uid = savane_uid; err = schedule_job (uid, savane_gid, file, sync_array, line); } for (sp = sync_array; sp < sync_array + line; sp++) { free (sp->repo); free (sp->dir); } obstack_free (&stk, NULL); return err; } int scan_spool (const char *spooldir) { DIR *dir; struct dirent *ent; if (chdir (spooldir)) { error (0, errno, "Cannot change to `%s'", spooldir); return 1; } dir = opendir ("."); if (!dir) { error (0, errno, "Cannot open directory `%s'", spooldir); return 1; } while ((ent = readdir (dir))) { struct stat st; waitjob (); if (ent->d_name[0] == '.') continue; if (stat (ent->d_name, &st)) { error (0, errno, "Cannot stat file `%s/%s'", spooldir, ent->d_name); continue; } if (S_ISREG (st.st_mode)) { if (st.st_uid < min_uid || st.st_gid < min_gid) { error (0, 0, "Ignoring file `%s/%s': UID or GID out of range", spooldir, ent->d_name); continue; } sync_from_file (st.st_uid, spooldir, ent->d_name); } } while (job_count () > 0) waitjob (); if ((verbose_level == 1 && total_files) || verbose_level > 1) printf ("Total files: %lu; Jobs processed: %lu\n", (unsigned long) total_files, (unsigned long) processed_files); closedir (dir); return 0; } struct option options[] = { { "cvs-command", required_argument, NULL, 'c' }, { "help", no_argument, NULL, 'h' }, { "hostname", required_argument, NULL, 'H'}, { "min-gid", required_argument, NULL, 'G' }, { "method", required_argument, NULL, 'm' }, { "dry-run", no_argument, NULL, 'n' }, { "rsh-program", required_argument, NULL, 'r' }, { "repository", required_argument, NULL, 'R' }, { "cron", no_argument, NULL, 's' }, { "user", required_argument, NULL, 'u' }, { "min-uid", required_argument, NULL, 'U' }, { "www-directory", required_argument, NULL, 'w' }, { "destination-directory", required_argument, NULL, 'w' }, { "debug", no_argument, NULL, 'x' }, { "verbose", no_argument, NULL, 'v' }, { NULL } }; int main (int argc, char **argv) { int i; char *p; int spooldir_mode = 0; char *user = NULL; char *repository = default_repository; while ((i = getopt_long (argc, argv, "c:hH:G:m:nr:R:su:U:vw:x", options, NULL)) != EOF) switch (i) { case 'c': cvs_command = optarg; break; case 'H': hostname = optarg; break; case 'h': usage (); exit (0); case 'G': min_gid = strtoul (optarg, &p, 0); if (p) error (1, 0, "invalid numeric value (near %s)", p); break; case 'm': access_method = optarg; break; case 'n': dry_run = 1; break; case 'r': rsh_program = optarg; break; case 'R': repository = optarg; break; case 's': spooldir_mode = 1; break; case 'U': min_uid = strtoul (optarg, &p, 0); if (p) error (1, 0, "invalid numeric value (near %s)", p); break; case 'u': user = optarg; break; case 'v': verbose_level++; break; case 'w': www_directory = optarg; break; case 'x': debug = 1; break; default: exit (1); } if (debug) { fprintf (stderr, "Invocation:\n"); for (i = 0; i < argc; i++) fprintf (stderr, "%s\n", argv[i]); fprintf (stderr, "\n"); } argc -= optind; argv += optind; if (user) { struct passwd *pw = getpwnam (user); if (!pw) error (1, errno, "cannot get password entry for user %s", user); savane_uid = pw->pw_uid; savane_gid = pw->pw_gid; } else { savane_uid = geteuid (); savane_gid = getegid (); } if (spooldir_mode) { char *spooldir = NULL; if (argc > 1) error (1, 0, "Extra arguments in scan mode"); else if (argc == 1) spooldir = argv[0]; else spooldir = default_spooldir; setsid (); setup_signals (); setup_jobs (); return scan_spool (spooldir); } else { char *unquoted_directory = NULL; char *tmp; switch_to_privs (savane_uid, savane_gid); if (argc == 1) { int rc = split_arg (argv[0], &unquoted_directory, &tmp); if (rc < 0) error (1, errno, "Cannot split argument"); else if (rc > 0) error (1, 0, "Failed to find directory in first argument: `%s'", argv[0]); } else if (argc == 2) { unquoted_directory = argv[0]; tmp = argv[1]; } else error (1, 0, "You must specify one or two arguments"); return sync_www (repository, unquoted_directory, has_file_list (tmp)); } /*NOTREACHED*/ return 0; }