/* mod_cvs.c - Module for Apache which automatically updates files that are checked out from a CVS repository. Authors: Martin Insulander (martin@insulander.com) Contributors: Frank Patz (fp@contact.de) Noa Resare (noa@resare.com) Copyright (C) 1999 Martin Insulander. Licensed under the Academic Free License version 1.2 The license works similiarly to the Apache license. For detailed information, please see the file LICENSE.AFL $Id: mod_cvs.c,v 1.3 2003/12/25 22:48:21 noa 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 "apr_general.h" #include "apr_strings.h" #include #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 AP_MODULE_DECLARE_DATA 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, 0, 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 = apr_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, 0, r->server, "Lock aquired: %s", lockfile); ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_DEBUG, 0, r->server, "%s", cmd); result = system(cmd); if (unlink(lockfile) == 0) { ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_DEBUG, 0, r->server, "Lock released: %s", lockfile); } else { ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_ERR, 0, 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, 0, 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, 0, r->server, "Timeout waiting for lockfile: %s (timeout=%d)", lockfile, timeout); } } } else { ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_DEBUG, 0, 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, 0, 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, 0, r->server, "CVS log denied: %s", r->uri); return HTTP_FORBIDDEN; } /* pstr = command line string */ ps = apr_pstrcat(r->pool, file, " > '", TMP_PREPEND, file, LOG_APPEND, "'",NULL); pstr = apr_psprintf(r->pool, cfg->cvs_logcmdline, ps); /* Run CVS */ chdir(dir); ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_NOTICE, 0, 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 HTTP_NOT_FOUND */ return HTTP_NOT_FOUND; } /* Check if the file is there and size > 0 (the user could have entered the wrong filename) */ pstr=apr_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, 0, r->server, "CVS log gave me nothing in %s", pstr); return HTTP_NOT_FOUND; } pstr = apr_pstrdup(r->pool, r->uri); c = strrchr(pstr, '/')+1; if (c != NULL) *c = '\0'; pstr = apr_pstrcat(r->pool, pstr, TMP_PREPEND, file, LOG_APPEND, NULL); r->args = NULL; apr_stat(&(r->finfo), r->filename, APR_FINFO_NORM, r->pool); ap_internal_redirect(pstr, r); pstr = apr_pstrcat(r->pool, TMP_PREPEND, file, LOG_APPEND, NULL); if (unlink(pstr) != 0) ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_ERR, 0, 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 = apr_pstrcat(r->pool, dir, CVS_REPFILE, NULL); file = fopen(pstr, "r"); if (!file) { ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_DEBUG, 0, 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 = apr_pstrcat(r->pool, dir, CVS_ROOTFILE, NULL); file = fopen(pstr, "r"); if (!file) { ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_DEBUG, 0, r->server, "Root file not found: %s", pstr); return NULL; } fgets(root, BUFSIZE, file); root[strlen(root)-1] = '\0'; fclose(file); pstr = apr_pstrcat(r->pool, root, "/", rep, NULL); return pstr; } else { pstr = apr_pstrdup(r->pool, rep); return pstr; } } /* Fixup function - this is what actually gets done */ static int cvs_fixup(request_rec *r) { char *reploc, *pstr, *ps, *ps2, *c, *dir, *file, *date; struct stat st; int retval = DECLINED; 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, 0, 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 = HTTP_FORBIDDEN; goto QUIT; } /* If we're the first request (not internally redirected), set up a timeout in case mod_cvs screws up */ /* XXX some source indicate that it always is ok to remove ap_soft_timeout, however I'm not sure -- noa@resare.com if (r->prev == NULL) { ap_soft_timeout("mod_cvs timeout", r); } */ /* Some string handling... */ dir = apr_pstrdup(r->pool, r->filename); c = strrchr(dir,'/'); if (c != NULL) *c = '\0'; file = apr_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.filetype == APR_DIR) { /* A ?DATE= arg is specified with a directory, we'll decline this and handle index files later instead */ retval = DECLINED; goto QUIT; } pstr = apr_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 = apr_pstrdup(r->pool, c); pstr = apr_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 = apr_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, 0, r->server, "Date checkout denied: %s", r->uri); retval = HTTP_FORBIDDEN; goto QUIT; } c++; date = apr_pstrdup(r->pool, c); pstr = apr_pstrdup(r->pool, r->path_info); c = strrchr(pstr,'/'); if (c != NULL) { *c = '\0'; dir = apr_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 = apr_pstrcat(r->pool, r->path_info, "?DATE=", date, NULL); ap_internal_redirect(pstr, r); retval = DONE; } else { retval = HTTP_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 = apr_pstrcat(r->pool, r->uri, "/", NULL); apr_table_add(r->headers_out, "Location", pstr); retval = HTTP_TEMPORARY_REDIRECT; goto QUIT; } /* pstr = command line string */ ps = apr_pstrcat(r->pool, "\"", date, "\"", NULL); ps2 = apr_pstrcat(r->pool, file, " > '", TMP_PREPEND, date, file, "'", NULL); pstr = apr_psprintf(r->pool, cfg->cvs_datecmdline, ps, ps2); /* Run CVS */ ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_NOTICE, 0, 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 HTTP_NOT_FOUND */ retval = HTTP_NOT_FOUND; goto QUIT; } /* Check if the file is there and size > 0 (the user could have entered the wrong filename) */ pstr=apr_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, 0, r->server, "Date checkout gave me nothing in %s", pstr); retval = HTTP_NOT_FOUND; goto QUIT; } pstr = apr_pstrdup(r->pool, r->path_info); c = strrchr(pstr, '/')+1; if (c != NULL) *c = '\0'; pstr = apr_pstrcat(r->pool, pstr, TMP_PREPEND, ap_escape_path_segment(r->pool, date), file, NULL); apr_stat(&(r->finfo), r->filename, APR_FINFO_NORM, r->pool); ap_internal_redirect(pstr, r); pstr = apr_pstrcat(r->pool, TMP_PREPEND, date, file, NULL); if (unlink(pstr) != 0) ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_ERR, 0, 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, 0, 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 = apr_pstrcat(r->pool, reploc, "/", file, ",v", NULL); if (stat(pstr,&st) != 0) { /* ,v file not found, is it a directory? */ pstr = apr_pstrcat(r->pool, reploc, "/", file, NULL); if (stat(pstr,&st) != 0) { /* no, the file might be in the attic (deleted) */ pstr = apr_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, 0, r->server, "No %s/(Attic/)%s(,v)", reploc, file); retval = DECLINED; goto QUIT; } } } if ((r->finfo.filetype == APR_NOFILE) || ((st.st_mtime - MTIME_DIFFER) > (r->finfo.mtime / 1000000))) { /* The file has changed, we're updating */ chdir(dir); /* weird CVS behaviour makes this better */ /* pstr = command line string */ pstr = apr_psprintf(r->pool, cfg->cvs_cmdline, file); /* Run CVS */ ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_NOTICE, 0, r->server, "CVS Update on %s", r->filename); if (call_cvs(r, pstr, file) != 0) { ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_ERR, 0, r->server, "CVS update failed."); retval = HTTP_INTERNAL_SERVER_ERROR; goto QUIT; } ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_DEBUG, 0, 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, 0, r->server, "Couldn't reset atime/mtime for: %s", file); retval = HTTP_INTERNAL_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 = apr_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 (apr_stat(&(r->finfo), r->filename, APR_FINFO_NORM, r->pool) != 0) { ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_ERR, 0, r->server, "File not found: %s", r->filename); retval = HTTP_NOT_FOUND; goto QUIT; } } else { ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_DEBUG, 0, r->server, "No update needed on: %s", r->filename); } } } QUIT: /* No freeing needed since we use resource pool provided by server */ /* weather this can be safely removed needs further investigation XXX noa@resare.com ap_kill_timeout(r); */ return retval; } /* Module initialization - called when server starts */ static int cvs_init(apr_pool_t *p, apr_pool_t *plog, apr_pool_t *ptemp, server_rec *s) { ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_INFO, 0, s, "mod_cvs $Id: mod_cvs.c,v 1.3 2003/12/25 22:48:21 noa Exp $"); return OK; } /* Configuration creation - creates config structure */ static void *create_cvs_config(apr_pool_t *p, char *dummy) { cvs_config *new = (cvs_config*) apr_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[] = { AP_INIT_FLAG("CVSCheck", ap_set_flag_slot, (void*)APR_OFFSETOF(cvs_config, check_cvs), OR_FILEINFO, "On or Off"), AP_INIT_FLAG("CVSAllowDateCheckout", ap_set_flag_slot, (void*)APR_OFFSETOF(cvs_config, allow_date), OR_FILEINFO, "On or Off"), AP_INIT_FLAG("CVSAllowLog", ap_set_flag_slot, (void*)APR_OFFSETOF(cvs_config, allow_log), OR_FILEINFO, "On or Off"), AP_INIT_FLAG("CVSAllowCVSFiles", ap_set_flag_slot, (void*)APR_OFFSETOF(cvs_config, allow_cvsfiles), OR_FILEINFO, "On or Off"), AP_INIT_FLAG("CVSUseLocking", ap_set_flag_slot, (void*)APR_OFFSETOF(cvs_config, use_locking), OR_FILEINFO, "On or Off"), AP_INIT_FLAG("CVSWaitForLock", ap_set_flag_slot, (void*)APR_OFFSETOF(cvs_config, wait_for_lock), OR_FILEINFO, "On or Off"), AP_INIT_TAKE1("CVSWaitTimeout", ap_set_string_slot, (void*)APR_OFFSETOF(cvs_config, cvs_waittimeout), OR_FILEINFO, "Number of seconds to wait for lockfile to disappear."), AP_INIT_TAKE1("CVSCmdline", ap_set_string_slot, (void*)APR_OFFSETOF(cvs_config, cvs_cmdline), OR_FILEINFO, "The commandline invoked (filename is appended) when " "updating."), AP_INIT_TAKE1("CVSDateCmdline", ap_set_string_slot, (void*)APR_OFFSETOF(cvs_config, cvs_datecmdline), OR_FILEINFO, "The commandline invoked (date and filename appended) " "at date checkout."), AP_INIT_TAKE1("CVSDateCmdline", ap_set_string_slot, (void*)APR_OFFSETOF(cvs_config, cvs_datecmdline), OR_FILEINFO, "The commandline invoked (date and filename appended) " "at date checkout."), AP_INIT_TAKE1("CVSLogCmdline", ap_set_string_slot, (void*)APR_OFFSETOF(cvs_config, cvs_logcmdline), OR_FILEINFO, "The commandline invoked (filename appended) " "for log output."), AP_INIT_TAKE1("CVSLockPath", ap_set_string_slot, (void*)APR_OFFSETOF(cvs_config, cvs_lockpath), OR_FILEINFO, "The path (relative to the requested file) where " "mod_cvs puts its lockfiles."), { NULL } }; static void mod_cvs_register_hooks (apr_pool_t *p) { /* the hook needs to be last, as it needs to happen after for example DirectoryIndex handling */ ap_hook_fixups(cvs_fixup, NULL, NULL, APR_HOOK_LAST); ap_hook_post_config(cvs_init, NULL, NULL, APR_HOOK_MIDDLE); } /* Module record, list of functions for server to call */ module AP_MODULE_DECLARE_DATA cvs_module = { STANDARD20_MODULE_STUFF, create_cvs_config, NULL, NULL, NULL, cvs_cmds, mod_cvs_register_hooks };