/* mod_cvs.c - Module for Apache which automatically updates files that are checked out from a CVS repository. Authors: Martin Insulander (main@isk.kth.se) Contributors: Frank Patz (fp@contact.de) Copyright (C) 1999 Martin Insulander. For license agreement, please see the file README included in the mod_cvs distribution. $Id: mod_cvs.c,v 1.25 1999/05/12 12:44:38 main Exp $ */ #include "httpd.h" #include "http_config.h" #include "http_protocol.h" #include "http_log.h" #include "util_script.h" #include "http_main.h" #include "http_request.h" #include #include #include #include #include #define BUFSIZE 256 #define CVS_ROOTFILE "/CVS/Root" #define CVS_REPFILE "/CVS/Repository" #define DEFAULT_CVS_CMDLINE "cvs -q update -dP %s" #define DEFAULT_CVS_DATECMDLINE "cvs -q update -fp -D %s %s" #define DEFAULT_CVS_LOGCMDLINE "cvs log %s"; #define DEFAULT_CVS_LOCKPATH "./CVS" #define DEFAULT_CVS_WAITTIMEOUT "30" #define TMP_PREPEND "mod_cvs" #define LOG_APPEND ".cvslog" #define LOCKFILE "mod_cvs_lock" #define MTIME_DIFFER 10 /* st_mtime can differ between the repository file and the checked out copy, allow that to be this many seconds */ module MODULE_VAR_EXPORT cvs_module; /* Structure of configuration variables*/ typedef struct { int check_cvs; int allow_date; int allow_log; int allow_cvsfiles; int use_locking; int wait_for_lock; char *cvs_waittimeout; char *cvs_cmdline; char *cvs_datecmdline; char *cvs_logcmdline; char *cvs_lockpath; } cvs_config; /* Call cvs with locking, to avoid running multiple cvs's on the same requested file. */ static int call_cvs(request_rec *r, const char *cmd, const char *file) { struct stat st; int result, timeout, count; char *lockfile; cvs_config *cfg; /* Get config information */ cfg = (cvs_config*) ap_get_module_config(r->per_dir_config, &cvs_module); /* Check wether we're supposed to use locking */ if (!cfg->use_locking) { ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_DEBUG, r->server, "%s", cmd); return system(cmd); } /* Execute CVS with locking. If a lock file is found, another mod_cvs is already running, and then we'll return the old version. */ lockfile = ap_pstrcat(r->pool, cfg->cvs_lockpath, "/", LOCKFILE, NULL); if (open(lockfile,O_CREAT|O_EXCL) != -1) { /* We've created a lockfile and are now updating the file */ ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_DEBUG, r->server, "Lock aquired: %s", lockfile); ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_DEBUG, r->server, "%s", cmd); result = system(cmd); if (unlink(lockfile) == 0) { ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_DEBUG, r->server, "Lock released: %s", lockfile); } else { ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_ERR, r->server, "Couldn't release lock: %s", lockfile); } } else if (errno == EEXIST) { /* Found a lock file. We'll either wait or show old revision. */ if (cfg->wait_for_lock) { timeout = atoi(cfg->cvs_waittimeout); ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_DEBUG, r->server, "Lockfile found: %s, waiting (timeout=%d).", lockfile, timeout); count = 0; while(stat(lockfile,&st) == 0) { sleep(1); count++; if ( (timeout != 0) && (count >= timeout) ) { /* Wait for lock timed out - we'll delete the lockfile */ unlink(lockfile); ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_ERR, r->server, "Timeout waiting for lockfile: %s (timeout=%d)", lockfile, timeout); } } } else { ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_DEBUG, r->server, "Lockfile found: %s, showing old revision", lockfile); } result = 0; } else { /* There was an error while creating the lockfile */ ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_ERR, r->server, "Couldn't create lockfile: %s", lockfile); result = 1; } return result; } /* cvs_log - print output of "cvs log" of the requested file */ static int cvs_log(request_rec *r, char *dir, char *file) { char *pstr, *ps, *c; struct stat st; cvs_config *cfg; /* Get config information */ cfg = (cvs_config*) ap_get_module_config(r->per_dir_config, &cvs_module); if ((!cfg->allow_log) && (r->prev == NULL)) { ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_NOTICE, r->server, "CVS log denied: %s", r->uri); return FORBIDDEN; } /* pstr = command line string */ ps = ap_pstrcat(r->pool, file, " > '", TMP_PREPEND, file, LOG_APPEND, "'",NULL); pstr = ap_psprintf(r->pool, cfg->cvs_logcmdline, ps); /* Run CVS */ chdir(dir); ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_NOTICE, r->server, "CVS Log on %s", r->filename); if (call_cvs(r, pstr, file) != 0) { /* CVS returned non-zero which means the user probably specified an erraneous date format, we'll return NOT_FOUND */ return NOT_FOUND; } /* Check if the file is there and size > 0 (the user could have entered the wrong filename) */ pstr=ap_pstrcat(r->pool, TMP_PREPEND, file, LOG_APPEND, NULL); if ((stat(pstr, &st) != 0) || (st.st_size == 0)) { ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_DEBUG, r->server, "CVS log gave me nothing in %s", pstr); return NOT_FOUND; } pstr = ap_pstrdup(r->pool, r->uri); c = strrchr(pstr, '/')+1; if (c != NULL) *c = '\0'; pstr = ap_pstrcat(r->pool, pstr, TMP_PREPEND, file, LOG_APPEND, NULL); r->args = NULL; stat(r->filename,&(r->finfo)); ap_internal_redirect(pstr, r); pstr = ap_pstrcat(r->pool, TMP_PREPEND, file, LOG_APPEND, NULL); if (unlink(pstr) != 0) ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_ERR, r->server, "Couldn't unlink temporary file: %s", pstr); return DONE; } static char* get_repository_location(request_rec *r, char *dir) { FILE *file; char root[BUFSIZE], rep[BUFSIZE]; char *pstr; pstr = ap_pstrcat(r->pool, dir, CVS_REPFILE, NULL); file = fopen(pstr, "r"); if (!file) { ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_DEBUG, r->server, "Repository file not found: %s", pstr); return NULL; } fgets(rep,BUFSIZE,file); rep[strlen(rep)-1] = '\0'; fclose(file); if (rep[0] != '/') { /* Repository is a relative path to Root, so we need to read that */ pstr = ap_pstrcat(r->pool, dir, CVS_ROOTFILE, NULL); file = fopen(pstr, "r"); if (!file) { ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_DEBUG, r->server, "Root file not found: %s", pstr); return NULL; } fgets(root, BUFSIZE, file); root[strlen(root)-1] = '\0'; fclose(file); pstr = ap_pstrcat(r->pool, root, "/", rep, NULL); return pstr; } else { pstr = ap_pstrdup(r->pool, rep); return pstr; } } /* Fixup function - this is what actually gets done */ static int cvs_fixup(request_rec *r) { char s[BUFSIZE]; char *reploc, *pstr, *ps, *ps2, *c, *dir, *file, *date; DIR *cvsdir; FILE *repfile; struct stat st; int retval = DECLINED, i, j, found; char **cvsargv; cvs_config *cfg; struct utimbuf newutime; /* Check request method, only GET allowed */ if (r->method_number != M_GET) { goto QUIT; } /* Get config information */ cfg = (cvs_config*) ap_get_module_config(r->per_dir_config, &cvs_module); /* Check wether we're supposed to do a cvs update (if needed) or not */ if (! cfg->check_cvs) { ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_DEBUG, r->server, "CVSCheck is off here: %s", r->uri); goto QUIT; } /* Check if the requested file is within the CVS/ dir, and deny req if we're supposed to. */ if ( (!cfg->allow_cvsfiles) && (strstr(r->uri,"/CVS/") != NULL) ) { retval = FORBIDDEN; goto QUIT; } /* If we're the first request (not internally redirected), set up a timeout in case mod_cvs screws up */ if (r->prev == NULL) { ap_soft_timeout("mod_cvs timeout", r); } /* Some string handling... */ dir = ap_pstrdup(r->pool, r->filename); c = strrchr(dir,'/'); if (c != NULL) *c = '\0'; file = ap_pstrdup(r->pool, (char*) rindex(r->filename,'/')+1); /* ---- DATE/LOG ARG ---- */ /* Check if there was a check-out date specified in the query args */ if (r->args != NULL) { if (r->finfo.st_mode & S_IFDIR) { /* A ?DATE= arg is specified with a directory, we'll decline this and handle index files later instead */ retval = DECLINED; goto QUIT; } pstr = ap_pstrdup(r->pool, r->args); c = strrchr(pstr, '='); if (c != NULL) *c = '\0'; if (strcmp(pstr,"DATE") == 0) { /* There was a ?DATE= arg, we need to redirect it back to the /DATE=/ format */ c++; date = ap_pstrdup(r->pool, c); pstr = ap_pstrcat(r->pool, "/DATE=", date, r->uri, NULL); ap_internal_redirect(pstr, r); retval = DONE; goto QUIT; } else if (strcmp(pstr,"LOG") == 0) { /* LOG parameter passed */ retval = cvs_log(r,dir,file); goto QUIT; } } /* ---- DATE PATH ---- */ /* Check if there was a check-out date specified in the request */ pstr = ap_pstrdup(r->pool, file); c = strrchr(pstr,'='); if (c != NULL) *c = '\0'; if (strcmp(pstr,"DATE") == 0) { /* DATE specified in URI, we're checking out an old version... */ if ((!cfg->allow_date) && (r->prev == NULL)) { ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_NOTICE, r->server, "Date checkout denied: %s", r->uri); retval = FORBIDDEN; goto QUIT; } c++; date = ap_pstrdup(r->pool, c); pstr = ap_pstrdup(r->pool, r->path_info); c = strrchr(pstr,'/'); if (c != NULL) { *c = '\0'; dir = ap_pstrcat(r->pool, dir, pstr, NULL); file = c+1; } else { file = ""; } chdir(dir); if (strcmp(file, "") == 0) { if ((stat(dir,&st) == 0) && (st.st_mode & S_IFDIR)) { /* The request was for a directory, we need to redirect this with an arg (?DATE=) instead */ pstr = ap_pstrcat(r->pool, r->path_info, "?DATE=", date, NULL); ap_internal_redirect(pstr, r); retval = DONE; } else { retval = NOT_FOUND; } goto QUIT; } if ((stat(file, &st) != 0) && (st.st_mode & S_IFDIR)) { /* Directory specified without trailing /. We have to do an external redirect. */ pstr = ap_pstrcat(r->pool, r->uri, "/", NULL); ap_table_add(r->headers_out, "Location", pstr); retval = REDIRECT; goto QUIT; } /* pstr = command line string */ ps = ap_pstrcat(r->pool, "\"", date, "\"", NULL); ps2 = ap_pstrcat(r->pool, file, " > '", TMP_PREPEND, date, file, "'", NULL); pstr = ap_psprintf(r->pool, cfg->cvs_datecmdline, ps, ps2); /* Run CVS */ ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_NOTICE, r->server, "CVS Date Checkout on %s", r->filename); if (call_cvs(r, pstr, file) != 0) { /* CVS returned non-zero which means the user probably specified an erraneous date format, we'll return NOT_FOUND */ retval = NOT_FOUND; goto QUIT; } /* Check if the file is there and size > 0 (the user could have entered the wrong filename) */ pstr=ap_pstrcat(r->pool, TMP_PREPEND, date, file, NULL); if ((stat(pstr, &st) != 0) || (st.st_size == 0)) { ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_DEBUG, r->server, "Date checkout gave me nothing in %s", pstr); retval = NOT_FOUND; goto QUIT; } pstr = ap_pstrdup(r->pool, r->path_info); c = strrchr(pstr, '/')+1; if (c != NULL) *c = '\0'; pstr = ap_pstrcat(r->pool, pstr, TMP_PREPEND, ap_escape_path_segment(r->pool, date), file, NULL); stat(r->filename,&(r->finfo)); ap_internal_redirect(pstr, r); pstr = ap_pstrcat(r->pool, TMP_PREPEND, date, file, NULL); if (unlink(pstr) != 0) ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_ERR, r->server, "Couldn't unlink temporary file: %s", pstr); retval = DONE; goto QUIT; } /* ---- NO DATE/LOG REQUEST ---- */ if (! (reploc = get_repository_location(r, dir)) ) { ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_INFO, r->server, "CVSCheck specified in non-CVSed directory: %s", dir); } else { /* If it's not one of our own temporary files, check if we're supposed to update and if so, run cvs and do lots of magic */ if (strncmp(TMP_PREPEND, file, strlen(TMP_PREPEND)) != 0) { /* Let's stat the file(s) in the repository */ pstr = ap_pstrcat(r->pool, reploc, "/", file, ",v", NULL); if (stat(pstr,&st) != 0) { /* ,v file not found, is it a directory? */ pstr = ap_pstrcat(r->pool, reploc, "/", file, NULL); if (stat(pstr,&st) != 0) { /* no, the file might be in the attic (deleted) */ pstr = ap_pstrcat(r->pool, reploc, "/Attic/", file, ",v", NULL); if (stat(pstr, &st) != 0) { /* nope, but let's just log a debug message */ ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_DEBUG, r->server, "No %s/(Attic/)%s(,v)", reploc, file); retval = DECLINED; goto QUIT; } } } if ((r->finfo.st_mode == 0) || ((st.st_mtime - MTIME_DIFFER) > r->finfo.st_mtime)) { /* The file has changed, we're updating */ chdir(dir); /* weird CVS behaviour makes this better */ /* pstr = command line string */ pstr = ap_psprintf(r->pool, cfg->cvs_cmdline, file); /* Run CVS */ ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_NOTICE, r->server, "CVS Update on %s", r->filename); if (call_cvs(r, pstr, file) != 0) { ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_ERR, r->server, "CVS update failed."); retval = SERVER_ERROR; goto QUIT; } ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_DEBUG, r->server, "Update done."); if (st.st_mode & S_IFDIR) { /* We know the dir is updated - let's set the mtime, just to be sure (KLUDGE correct of some weird bug.. cause of strange CVS behaviour? */ newutime.actime = st.st_atime; newutime.modtime = st.st_mtime; if (utime(file, &newutime) != 0) { ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_ERR,r->server, "Couldn't reset atime/mtime for: %s", file); retval = SERVER_ERROR; goto QUIT; } /* The user requested a directory - we need to redirect, so that we can find the indexfile and stuff */ r->path_info = ""; ap_internal_redirect(r->uri, r); return DONE; } /* Pass on info to the next handlers */ /* When Apache doesn't find the file it sets r->filename to be the closest directory found, and r->path_info to be the part that was not found. Now, we'll append path_info to filename, because now r->filename + "/" + r->path_info does exist (hopefully). */ if ((r->path_info != NULL) && (strcmp(r->path_info,"") != 0)) { r->filename = ap_pstrcat(r->pool, r->filename, r->path_info, NULL); r->path_info = ""; } /* We also have to re-stat the file for the next handlers to get the picture. */ if (stat(r->filename,&(r->finfo)) != 0) { ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_ERR, r->server, "File not found: %s", r->filename); retval = NOT_FOUND; goto QUIT; } } else { ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_DEBUG, r->server, "No update needed on: %s", r->filename); } } } QUIT: /* No freeing needed since we use resource pool provided by server */ ap_kill_timeout(r); return retval; } /* Module initialization - called when server starts */ void cvs_init(server_rec *s, pool *p) { ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_INFO, s, "mod_cvs $Id: mod_cvs.c,v 1.25 1999/05/12 12:44:38 main Exp $"); } /* Configuration creation - creates config structure */ void *create_cvs_config(pool *p, char *dummy) { cvs_config *new = (cvs_config*) ap_palloc(p, sizeof(cvs_config)); new->check_cvs = 0; new->allow_date = 0; new->allow_date = 0; new->allow_cvsfiles = 0; new->use_locking = 1; new->wait_for_lock = 1; new->cvs_cmdline = DEFAULT_CVS_CMDLINE; new->cvs_datecmdline = DEFAULT_CVS_DATECMDLINE; new->cvs_logcmdline = DEFAULT_CVS_LOGCMDLINE; new->cvs_lockpath = DEFAULT_CVS_LOCKPATH; new->cvs_waittimeout = DEFAULT_CVS_WAITTIMEOUT; return new; } /* Command record - list of commands to corresponding directives */ command_rec cvs_cmds[] = { { "CVSCheck", ap_set_flag_slot, (void*) XtOffsetOf(cvs_config, check_cvs), OR_FILEINFO, FLAG, "On or Off" }, { "CVSAllowDateCheckout", ap_set_flag_slot, (void*) XtOffsetOf(cvs_config, allow_date), OR_FILEINFO, FLAG, "On or Off" }, { "CVSAllowLog", ap_set_flag_slot, (void*) XtOffsetOf(cvs_config, allow_log), OR_FILEINFO, FLAG, "On or Off" }, { "CVSAllowCVSFiles", ap_set_flag_slot, (void*) XtOffsetOf(cvs_config, allow_cvsfiles), OR_FILEINFO, FLAG, "On or Off" }, { "CVSUseLocking", ap_set_flag_slot, (void*) XtOffsetOf(cvs_config, use_locking), OR_FILEINFO, FLAG, "On or Off" }, { "CVSWaitForLock", ap_set_flag_slot, (void*) XtOffsetOf(cvs_config, wait_for_lock), OR_FILEINFO, FLAG, "On or Off" }, { "CVSWaitTimeout", ap_set_string_slot, (void*) XtOffsetOf(cvs_config, cvs_waittimeout), OR_FILEINFO, TAKE1, "Number of seconds to wait for lockfile to disappear." }, { "CVSCmdline", ap_set_string_slot, (void*) XtOffsetOf(cvs_config, cvs_cmdline), OR_FILEINFO, TAKE1, "The commandline invoked (filename is appended) when updating." }, { "CVSDateCmdline", ap_set_string_slot, (void*) XtOffsetOf(cvs_config, cvs_datecmdline), OR_FILEINFO, TAKE1, "The commandline invoked (date and filename appended) at date checkout." }, { "CVSLogCmdline", ap_set_string_slot, (void*) XtOffsetOf(cvs_config, cvs_logcmdline), OR_FILEINFO, TAKE1, "The commandline invoked (filename appended) for log output." }, { "CVSLockPath", ap_set_string_slot, (void*) XtOffsetOf(cvs_config, cvs_lockpath), OR_FILEINFO, TAKE1, "The path (relative to the requested file) where mod_cvs puts its lockfiles." }, { NULL } }; /* Module record, list of functions for server to call */ module MODULE_VAR_EXPORT cvs_module = { STANDARD_MODULE_STUFF, cvs_init, /* initializer */ create_cvs_config, /* create per-directory config structure */ NULL, /* merge per-directory config structures */ NULL, /* create per-server config structure */ NULL, /* merge per-server config structures */ cvs_cmds, /* command table */ NULL, /* handlers */ NULL, /* translate_handler */ NULL, /* check_user_id */ NULL, /* check auth */ NULL, /* check access */ NULL, /* type_checker */ cvs_fixup, /* pre-run fixups */ NULL, /* logger */ NULL, /* header parser */ NULL, /* child_init */ NULL, /* child_exit */ NULL /* post read-request */ };