diff options
Diffstat (limited to 'src/server.c')
-rw-r--r-- | src/server.c | 7987 |
1 files changed, 7987 insertions, 0 deletions
diff --git a/src/server.c b/src/server.c new file mode 100644 index 0000000..41ece74 --- /dev/null +++ b/src/server.c @@ -0,0 +1,7987 @@ +/* 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 2, 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. */ + +#include "cvs.h" + +/* CVS */ +#include "edit.h" +#include "fileattr.h" +#include "watch.h" + +/* GNULIB */ +#include "buffer.h" +#include "getline.h" +#include "getnline.h" + +int server_active = 0; + +#if defined (SERVER_SUPPORT) || defined (CLIENT_SUPPORT) + +# include "log-buffer.h" +# include "ms-buffer.h" +#endif /* defined(SERVER_SUPPORT) || defined(CLIENT_SUPPORT) */ + +#if defined (HAVE_GSSAPI) && defined (SERVER_SUPPORT) +# include "canon-host.h" +# include "gssapi-client.h" + +/* This stuff isn't included solely with SERVER_SUPPORT since some of these + * functions (encryption & the like) get compiled with or without server + * support. + * + * FIXME - They should be in a different file. + */ +/* We use Kerberos 5 routines to map the GSSAPI credential to a user + name. */ +# include <krb5.h> + +static void gserver_authenticate_connection (void); + +/* Whether we are already wrapping GSSAPI communication. */ +static int cvs_gssapi_wrapping; + +#endif /* defined (HAVE_GSSAPI) && defined (SERVER_SUPPORT) */ + +#ifdef SERVER_SUPPORT + +extern char *server_hostname; + +# if defined (AUTH_SERVER_SUPPORT) || defined (HAVE_KERBEROS) || defined (HAVE_GSSAPI) +# include <sys/socket.h> +# endif + +# ifdef HAVE_SYSLOG_H +# include <syslog.h> +# ifndef LOG_DAEMON /* for ancient syslogs */ +# define LOG_DAEMON 0 +# endif +# endif /* HAVE_SYSLOG_H */ + +# ifdef HAVE_KERBEROS +# include <netinet/in.h> +# include <krb.h> +# ifndef HAVE_KRB_GET_ERR_TEXT +# define krb_get_err_text(status) krb_err_txt[status] +# endif + +/* Information we need if we are going to use Kerberos encryption. */ +static C_Block kblock; +static Key_schedule sched; + +# endif /* HAVE_KERBEROS */ + +/* for select */ +# include "xselect.h" + +# ifndef O_NONBLOCK +# define O_NONBLOCK O_NDELAY +# endif + +/* For initgroups(). */ +# if HAVE_INITGROUPS +# include <grp.h> +# endif /* HAVE_INITGROUPS */ + +# ifdef AUTH_SERVER_SUPPORT + +# ifdef HAVE_GETSPNAM +# include <shadow.h> +# endif + +/* The cvs username sent by the client, which might or might not be + the same as the system username the server eventually switches to + run as. CVS_Username gets set iff password authentication is + successful. */ +char *CVS_Username = NULL; + +/* Used to check that same repos is transmitted in pserver auth and in + later CVS protocol. Exported because root.c also uses. */ +static char *Pserver_Repos = NULL; + +# endif /* AUTH_SERVER_SUPPORT */ + +# ifdef HAVE_PAM +# if defined(HAVE_SECURITY_PAM_APPL_H) +# include <security/pam_appl.h> +# elif defined(HAVE_PAM_PAM_APPL_H) +# include <pam/pam_appl.h> +# endif + +static pam_handle_t *pamh = NULL; + +static char *pam_username; +static char *pam_password; +# endif /* HAVE_PAM */ + + + +/* While processing requests, this buffer accumulates data to be sent to + the client, and then once we are in do_cvs_command, we use it + for all the data to be sent. */ +static struct buffer *buf_to_net; + +/* This buffer is used to read input from the client. */ +static struct buffer *buf_from_net; + + + +# ifdef PROXY_SUPPORT +/* These are the secondary log buffers so that we can disable them after + * creation, when it is determined that they are unneeded, regardless of what + * other filters have been prepended to the buffer chain. + */ +static struct buffer *proxy_log; +static struct buffer *proxy_log_out; + +/* Set while we are reprocessing a log so that we can avoid sending responses + * to some requests twice. + */ +static bool reprocessing; +# endif /* PROXY_SUPPORT */ + + + +/* Arguments storage for `Argument' & `Argumentx' requests. */ +static int argument_count; +static char **argument_vector; +static int argument_vector_size; + +/* + * This is where we stash stuff we are going to use. Format string + * which expects a single directory within it, starting with a slash. + */ +static char *server_temp_dir; + +/* This is the original value of server_temp_dir, before any possible + changes inserted by serve_max_dotdot. */ +static char *orig_server_temp_dir; + +/* Nonzero if we should keep the temp directory around after we exit. */ +static int dont_delete_temp; + +static void server_write_entries (void); + +cvsroot_t *referrer; + + + +/* Populate all of the directories between BASE_DIR and its relative + subdirectory DIR with CVSADM directories. Return 0 for success or + errno value. */ +static int +create_adm_p (char *base_dir, char *dir) +{ + char *dir_where_cvsadm_lives, *dir_to_register, *p, *tmp; + int retval, done; + FILE *f; + + if (strcmp (dir, ".") == 0) + return 0; /* nothing to do */ + + /* Allocate some space for our directory-munging string. */ + p = xmalloc (strlen (dir) + 1); + if (p == NULL) + return ENOMEM; + + dir_where_cvsadm_lives = xmalloc (strlen (base_dir) + strlen (dir) + 100); + if (dir_where_cvsadm_lives == NULL) + { + free (p); + return ENOMEM; + } + + /* Allocate some space for the temporary string in which we will + construct filenames. */ + tmp = xmalloc (strlen (base_dir) + strlen (dir) + 100); + if (tmp == NULL) + { + free (p); + free (dir_where_cvsadm_lives); + return ENOMEM; + } + + + /* We make several passes through this loop. On the first pass, + we simply create the CVSADM directory in the deepest directory. + For each subsequent pass, we try to remove the last path + element from DIR, create the CVSADM directory in the remaining + pathname, and register the subdirectory in the newly created + CVSADM directory. */ + + retval = done = 0; + + strcpy (p, dir); + strcpy (dir_where_cvsadm_lives, base_dir); + strcat (dir_where_cvsadm_lives, "/"); + strcat (dir_where_cvsadm_lives, p); + dir_to_register = NULL; + + while (1) + { + /* Create CVSADM. */ + (void) sprintf (tmp, "%s/%s", dir_where_cvsadm_lives, CVSADM); + if ((CVS_MKDIR (tmp, 0777) < 0) && (errno != EEXIST)) + { + retval = errno; + goto finish; + } + + /* Create CVSADM_REP. */ + (void) sprintf (tmp, "%s/%s", dir_where_cvsadm_lives, CVSADM_REP); + if (! isfile (tmp)) + { + /* Use Emptydir as the placeholder until the client sends + us the real value. This code is similar to checkout.c + (emptydir_name), but the code below returns errors + differently. */ + + char *empty; + empty = xmalloc (strlen (current_parsed_root->directory) + + sizeof (CVSROOTADM) + + sizeof (CVSNULLREPOS) + + 3); + if (! empty) + { + retval = ENOMEM; + goto finish; + } + + /* Create the directory name. */ + (void) sprintf (empty, "%s/%s/%s", current_parsed_root->directory, + CVSROOTADM, CVSNULLREPOS); + + /* Create the directory if it doesn't exist. */ + if (! isfile (empty)) + { + mode_t omask; + omask = umask (cvsumask); + if (CVS_MKDIR (empty, 0777) < 0) + { + retval = errno; + free (empty); + goto finish; + } + (void) umask (omask); + } + + f = CVS_FOPEN (tmp, "w"); + if (f == NULL) + { + retval = errno; + free (empty); + goto finish; + } + /* Write the directory name to CVSADM_REP. */ + if (fprintf (f, "%s\n", empty) < 0) + { + retval = errno; + fclose (f); + free (empty); + goto finish; + } + if (fclose (f) == EOF) + { + retval = errno; + free (empty); + goto finish; + } + + /* Clean up after ourselves. */ + free (empty); + } + + /* Create CVSADM_ENT. We open in append mode because we + don't want to clobber an existing Entries file. */ + (void) sprintf (tmp, "%s/%s", dir_where_cvsadm_lives, CVSADM_ENT); + f = CVS_FOPEN (tmp, "a"); + if (f == NULL) + { + retval = errno; + goto finish; + } + if (fclose (f) == EOF) + { + retval = errno; + goto finish; + } + + if (dir_to_register != NULL) + { + /* FIXME: Yes, this results in duplicate entries in the + Entries.Log file, but it doesn't currently matter. We + might need to change this later on to make sure that we + only write one entry. */ + + Subdir_Register (NULL, dir_where_cvsadm_lives, dir_to_register); + } + + if (done) + break; + + dir_to_register = strrchr (p, '/'); + if (dir_to_register == NULL) + { + dir_to_register = p; + strcpy (dir_where_cvsadm_lives, base_dir); + done = 1; + } + else + { + *dir_to_register = '\0'; + dir_to_register++; + strcpy (dir_where_cvsadm_lives, base_dir); + strcat (dir_where_cvsadm_lives, "/"); + strcat (dir_where_cvsadm_lives, p); + } + } + + finish: + free (tmp); + free (dir_where_cvsadm_lives); + free (p); + return retval; +} + + + +/* + * Make directory DIR, including all intermediate directories if necessary. + * Returns 0 for success or errno code. + */ +static int +mkdir_p (char *dir) +{ + char *p; + char *q = xmalloc (strlen (dir) + 1); + int retval; + + if (q == NULL) + return ENOMEM; + + retval = 0; + + /* + * Skip over leading slash if present. We won't bother to try to + * make '/'. + */ + p = dir + 1; + while (1) + { + while (*p != '/' && *p != '\0') + ++p; + if (*p == '/') + { + strncpy (q, dir, p - dir); + q[p - dir] = '\0'; + if (q[p - dir - 1] != '/' && CVS_MKDIR (q, 0777) < 0) + { + int saved_errno = errno; + + if (saved_errno != EEXIST + && ((saved_errno != EACCES && saved_errno != EROFS) + || !isdir (q))) + { + retval = saved_errno; + goto done; + } + } + ++p; + } + else + { + if (CVS_MKDIR (dir, 0777) < 0) + retval = errno; + goto done; + } + } + done: + free (q); + return retval; +} + + + +/* + * Print the error response for error code STATUS. The caller is + * reponsible for making sure we get back to the command loop without + * any further output occuring. + * Must be called only in contexts where it is OK to send output. + */ +static void +print_error (int status) +{ + char *msg; + char tmpstr[80]; + + buf_output0 (buf_to_net, "error "); + msg = strerror (status); + if (msg == NULL) + { + sprintf (tmpstr, "unknown error %d", status); + msg = tmpstr; + } + buf_output0 (buf_to_net, msg); + buf_append_char (buf_to_net, '\n'); + + buf_flush (buf_to_net, 0); +} + + + +static int pending_error; +/* + * Malloc'd text for pending error. Each line must start with "E ". The + * last line should not end with a newline. + */ +static char *pending_error_text; +static char *pending_warning_text; + +/* If an error is pending, print it and return 1. If not, return 0. + Also prints pending warnings, but this does not affect the return value. + Must be called only in contexts where it is OK to send output. */ +static int +print_pending_error (void) +{ + /* Check this case first since it usually means we are out of memory and + * the buffer output routines might try and allocate memory. + */ + if (!pending_error_text && pending_error) + { + print_error (pending_error); + pending_error = 0; + return 1; + } + + if (pending_warning_text) + { + buf_output0 (buf_to_net, pending_warning_text); + buf_append_char (buf_to_net, '\n'); + buf_flush (buf_to_net, 0); + + free (pending_warning_text); + pending_warning_text = NULL; + } + + if (pending_error_text) + { + buf_output0 (buf_to_net, pending_error_text); + buf_append_char (buf_to_net, '\n'); + if (pending_error) + print_error (pending_error); + else + buf_output0 (buf_to_net, "error \n"); + + buf_flush (buf_to_net, 0); + + pending_error = 0; + free (pending_error_text); + pending_error_text = NULL; + return 1; + } + + return 0; +} + + + +/* Is an error pending? */ +# define error_pending() (pending_error || pending_error_text) +# define warning_pending() (pending_warning_text) + +/* Allocate SIZE bytes for pending_error_text and return nonzero + if we could do it. */ +static inline int +alloc_pending_internal (char **dest, size_t size) +{ + *dest = malloc (size); + if (!*dest) + { + pending_error = ENOMEM; + return 0; + } + return 1; +} + + + +/* Allocate SIZE bytes for pending_error_text and return nonzero + if we could do it. */ +static int +alloc_pending (size_t size) +{ + if (error_pending ()) + /* Probably alloc_pending callers will have already checked for + this case. But we might as well handle it if they don't, I + guess. */ + return 0; + return alloc_pending_internal (&pending_error_text, size); +} + + + +/* Allocate SIZE bytes for pending_error_text and return nonzero + if we could do it. */ +static int +alloc_pending_warning (size_t size) +{ + if (warning_pending ()) + /* Warnings can be lost here. */ + return 0; + return alloc_pending_internal (&pending_warning_text, size); +} + + + +static int +supported_response (char *name) +{ + struct response *rs; + + for (rs = responses; rs->name != NULL; ++rs) + if (strcmp (rs->name, name) == 0) + return rs->status == rs_supported; + error (1, 0, "internal error: testing support for unknown response?"); + /* NOTREACHED */ + return 0; +} + + + +/* + * Return true if we need to relay write requests to a primary server + * and false otherwise. + * + * NOTES + * + * - primarily handles :ext: method as this seems most likely to be used in + * practice. + * + * - :fork: method is handled for testing. + * + * - Could handle pserver too, but would have to store the password + * the client sent us. + * + * + * GLOBALS + * config->PrimaryServer + * The parsed setting from CVSROOT/config, if any, or + * NULL, otherwise. + * current_parsed_root The current repository. + * + * RETURNS + * true If this server is configured as a secondary server. + * false Otherwise. + */ +static inline bool +isProxyServer (void) +{ + assert (current_parsed_root); + + /*** + *** The following is done as a series of if/return combinations an an + *** optimization. + ***/ + + /* If there is no primary server defined in CVSROOT/config, then we can't + * be a secondary. + */ + if (!config || !config->PrimaryServer) return false; + + /* The directory must not match for all methods. */ + if (!isSamePath (config->PrimaryServer->directory, + current_parsed_root->directory)) + return true; + + /* Only the directory is important for fork. */ + if (config->PrimaryServer->method == fork_method) + return false; + + /* Must be :ext: method, then. This is enforced when CVSROOT/config is + * parsed. + */ + assert (config->PrimaryServer->isremote); + + if (isThisHost (config->PrimaryServer->hostname)) + return false; + + return true; +} + + + +static void +serve_valid_responses (char *arg) +{ + char *p = arg; + char *q; + struct response *rs; + +# ifdef PROXY_SUPPORT + /* Process this in the first pass since the data it gathers can be used + * prior to a `Root' request. + */ + if (reprocessing) return; +# endif /* PROXY_SUPPORT */ + + do + { + q = strchr (p, ' '); + if (q != NULL) + *q++ = '\0'; + for (rs = responses; rs->name != NULL; ++rs) + { + if (strcmp (rs->name, p) == 0) + break; + } + if (rs->name == NULL) + /* + * It is a response we have never heard of (and thus never + * will want to use). So don't worry about it. + */ + ; + else + rs->status = rs_supported; + p = q; + } while (q != NULL); + for (rs = responses; rs->name != NULL; ++rs) + { + if (rs->status == rs_essential) + { + buf_output0 (buf_to_net, "E response `"); + buf_output0 (buf_to_net, rs->name); + buf_output0 (buf_to_net, "' not supported by client\nerror \n"); + + /* FIXME: This call to buf_flush could conceivably + cause deadlock, as noted in server_cleanup. */ + buf_flush (buf_to_net, 1); + + exit (EXIT_FAILURE); + } + else if (rs->status == rs_optional) + rs->status = rs_not_supported; + } +} + + + +/* + * Process IDs of the subprocess, or negative if that subprocess + * does not exist. + */ +static pid_t command_pid; + +static void +outbuf_memory_error (struct buffer *buf) +{ + static const char msg[] = "E Fatal server error\n\ +error ENOMEM Virtual memory exhausted.\n"; + if (command_pid > 0) + kill (command_pid, SIGTERM); + + /* + * We have arranged things so that printing this now either will + * be valid, or the "E fatal error" line will get glommed onto the + * end of an existing "E" or "M" response. + */ + + /* If this gives an error, not much we could do. syslog() it? */ + write (STDOUT_FILENO, msg, sizeof (msg) - 1); +# ifdef HAVE_SYSLOG_H + syslog (LOG_DAEMON | LOG_ERR, "virtual memory exhausted"); +# endif /* HAVE_SYSLOG_H */ + exit (EXIT_FAILURE); +} + + + +static void +input_memory_error (struct buffer *buf) +{ + outbuf_memory_error (buf); +} + + + +# ifdef PROXY_SUPPORT +/* This function rewinds the net connection using the write proxy log file. + * + * GLOBALS + * proxy_log The buffer object containing the write proxy log. + * + * RETURNS + * Nothing. + */ +static void +rewind_buf_from_net (void) +{ + struct buffer *log; + + assert (proxy_log); + + /* Free the arguments since we processed some of them in the first pass. + */ + { + /* argument_vector[0] is a dummy argument, we don't mess with + * it. + */ + char **cp; + for (cp = argument_vector + 1; + cp < argument_vector + argument_count; + ++cp) + free (*cp); + + argument_count = 1; + } + + log = log_buffer_rewind (proxy_log); + proxy_log = NULL; + /* Dispose of any read but unused data in the net buffer since it will + * already be in the log. + */ + buf_free_data (buf_from_net); + buf_from_net = ms_buffer_initialize (outbuf_memory_error, log, + buf_from_net); + reprocessing = true; +} +# endif /* PROXY_SUPPORT */ + + + +char *gConfigPath; + + + +/* + * This request cannot be ignored by a potential secondary since it is used to + * determine if we _are_ a secondary. + */ +static void +serve_root (char *arg) +{ + char *path; + + TRACE (TRACE_FUNCTION, "serve_root (%s)", arg ? arg : "(null)"); + + /* Don't process this twice or when errors are pending. */ + if (error_pending() +# ifdef PROXY_SUPPORT + || reprocessing +# endif /* PROXY_SUPPORT */ + ) return; + + if (!ISABSOLUTE (arg)) + { + if (alloc_pending (80 + strlen (arg))) + sprintf (pending_error_text, + "E Root %s must be an absolute pathname", arg); + return; + } + + /* Sending "Root" twice is invalid. + + The other way to handle a duplicate Root requests would be as a + request to clear out all state and start over as if it was a + new connection. Doing this would cause interoperability + headaches, so it should be a different request, if there is + any reason why such a feature is needed. */ + if (current_parsed_root != NULL) + { + if (alloc_pending (80 + strlen (arg))) + sprintf (pending_error_text, + "E Protocol error: Duplicate Root request, for %s", arg); + return; + } + + /* Set original_parsed_root here, not because it can be changed in the + * client Redirect sense, but so we don't have to switch in code that + * runs in both modes to decide which to print. + */ + original_parsed_root = current_parsed_root = local_cvsroot (arg); + +# ifdef AUTH_SERVER_SUPPORT + if (Pserver_Repos != NULL) + { + if (strcmp (Pserver_Repos, current_parsed_root->directory) != 0) + { + if (alloc_pending (80 + strlen (Pserver_Repos) + + strlen (current_parsed_root->directory))) + /* The explicitness is to aid people who are writing clients. + I don't see how this information could help an + attacker. */ + sprintf (pending_error_text, "\ +E Protocol error: Root says \"%s\" but pserver says \"%s\"", + current_parsed_root->directory, Pserver_Repos); + return; + } + } +# endif + + /* For pserver, this will already have happened, and the call will do + nothing. But for rsh, we need to do it now. */ + config = get_root_allow_config (current_parsed_root->directory, + gConfigPath); + +# ifdef PROXY_SUPPORT + /* At this point we have enough information to determine if we are a + * secondary server or not. + */ + if (proxy_log && !isProxyServer ()) + { + /* Else we are not a secondary server. There is no point in + * reprocessing since we handle all the requests we can receive + * before `Root' as we receive them. But close the logs. + */ + log_buffer_closelog (proxy_log); + log_buffer_closelog (proxy_log_out); + proxy_log = NULL; + /* + * Don't need this. We assume it when proxy_log == NULL. + * + * proxy_log_out = NULL; + */ + } +# endif /* PROXY_SUPPORT */ + + /* Now set the TMPDIR environment variable. If it was set in the config + * file, we now know it. + */ + push_env_temp_dir (); + + /* OK, now figure out where we stash our temporary files. */ + { + char *p; + + /* The code which wants to chdir into server_temp_dir is not set + * up to deal with it being a relative path. So give an error + * for that case. + */ + if (!ISABSOLUTE (get_cvs_tmp_dir ())) + { + if (alloc_pending (80 + strlen (get_cvs_tmp_dir ()))) + sprintf (pending_error_text, + "E Value of %s for TMPDIR is not absolute", + get_cvs_tmp_dir ()); + + /* FIXME: we would like this error to be persistent, that + * is, not cleared by print_pending_error. The current client + * will exit as soon as it gets an error, but the protocol spec + * does not require a client to do so. + */ + } + else + { + int status; + int i = 0; + + server_temp_dir = xmalloc (strlen (get_cvs_tmp_dir ()) + 80); + if (!server_temp_dir) + { + /* Strictly speaking, we're not supposed to output anything + * now. But we're about to exit(), give it a try. + */ + printf ("E Fatal server error, aborting.\n\ +error ENOMEM Virtual memory exhausted.\n"); + + exit (EXIT_FAILURE); + } + strcpy (server_temp_dir, get_cvs_tmp_dir ()); + + /* Remove a trailing slash from TMPDIR if present. */ + p = server_temp_dir + strlen (server_temp_dir) - 1; + if (*p == '/') + *p = '\0'; + + /* I wanted to use cvs-serv/PID, but then you have to worry about + * the permissions on the cvs-serv directory being right. So + * use cvs-servPID. + */ + strcat (server_temp_dir, "/cvs-serv"); + + p = server_temp_dir + strlen (server_temp_dir); + sprintf (p, "%ld", (long) getpid ()); + + orig_server_temp_dir = server_temp_dir; + + /* Create the temporary directory, and set the mode to + * 700, to discourage random people from tampering with + * it. + */ + while ((status = mkdir_p (server_temp_dir)) == EEXIST) + { + static const char suffix[] = "abcdefghijklmnopqrstuvwxyz"; + + if (i >= sizeof suffix - 1) break; + if (i == 0) p = server_temp_dir + strlen (server_temp_dir); + p[0] = suffix[i++]; + p[1] = '\0'; + } + if (status) + { + if (alloc_pending (80 + strlen (server_temp_dir))) + sprintf (pending_error_text, + "E can't create temporary directory %s", + server_temp_dir); + pending_error = status; + } +#ifndef CHMOD_BROKEN + else if (chmod (server_temp_dir, S_IRWXU) < 0) + { + int save_errno = errno; + if (alloc_pending (80 + strlen (server_temp_dir))) + sprintf (pending_error_text, +"E cannot change permissions on temporary directory %s", + server_temp_dir); + pending_error = save_errno; + } +#endif + else if (CVS_CHDIR (server_temp_dir) < 0) + { + int save_errno = errno; + if (alloc_pending (80 + strlen (server_temp_dir))) + sprintf (pending_error_text, +"E cannot change to temporary directory %s", + server_temp_dir); + pending_error = save_errno; + } + } + } + + /* Now that we have a config, verify our compression level. Since + * most clients do not send Gzip-stream requests until after the root + * request, wait until the first request following Root to verify that + * compression is being used when level 0 is not allowed. + */ + if (gzip_level) + { + bool forced = false; + + if (gzip_level < config->MinCompressionLevel) + { + gzip_level = config->MinCompressionLevel; + forced = true; + } + + if (gzip_level > config->MaxCompressionLevel) + { + gzip_level = config->MaxCompressionLevel; + forced = true; + } + + if (forced && !quiet + && alloc_pending_warning (120 + strlen (program_name))) + sprintf (pending_warning_text, +"E %s server: Forcing compression level %d (allowed: %d <= z <= %d).", + program_name, gzip_level, config->MinCompressionLevel, + config->MaxCompressionLevel); + } + + path = xmalloc (strlen (current_parsed_root->directory) + + sizeof (CVSROOTADM) + + 2); + if (path == NULL) + { + pending_error = ENOMEM; + return; + } + (void) sprintf (path, "%s/%s", current_parsed_root->directory, CVSROOTADM); + if (!isaccessible (path, R_OK | X_OK)) + { + int save_errno = errno; + if (alloc_pending (80 + strlen (path))) + sprintf (pending_error_text, "E Cannot access %s", path); + pending_error = save_errno; + } + free (path); + + setenv (CVSROOT_ENV, current_parsed_root->directory, 1); +} + + + +static int max_dotdot_limit = 0; + +/* Is this pathname OK to recurse into when we are running as the server? + If not, call error() with a fatal error. */ +void +server_pathname_check (char *path) +{ + TRACE (TRACE_FUNCTION, "server_pathname_check (%s)", + path ? path : "(null)"); + + /* An absolute pathname is almost surely a path on the *client* machine, + and is unlikely to do us any good here. It also is probably capable + of being a security hole in the anonymous readonly case. */ + if (ISABSOLUTE (path)) + /* Giving an error is actually kind of a cop-out, in the sense + that it would be nice for "cvs co -d /foo/bar/baz" to work. + A quick fix in the server would be requiring Max-dotdot of + at least one if pathnames are absolute, and then putting + /abs/foo/bar/baz in the temp dir beside the /d/d/d stuff. + A cleaner fix in the server might be to decouple the + pathnames we pass back to the client from pathnames in our + temp directory (this would also probably remove the need + for Max-dotdot). A fix in the client would have the client + turn it into "cd /foo/bar; cvs co -d baz" (more or less). + This probably has some problems with pathnames which appear + in messages. */ + error ( 1, 0, + "absolute pathnames invalid for server (specified `%s')", + path ); + if (pathname_levels (path) > max_dotdot_limit) + { + /* Similar to the ISABSOLUTE case in security implications. */ + error (0, 0, "protocol error: `%s' contains more leading ..", path); + error (1, 0, "than the %d which Max-dotdot specified", + max_dotdot_limit); + } +} + + + +/* Is file or directory REPOS an absolute pathname within the + current_parsed_root->directory? If yes, return 0. If no, set pending_error + and return 1. */ +static int +outside_root (char *repos) +{ + size_t repos_len = strlen (repos); + size_t root_len = strlen (current_parsed_root->directory); + + /* ISABSOLUTE (repos) should always be true, but + this is a good security precaution regardless. -DRP + */ + if (!ISABSOLUTE (repos)) + { + if (alloc_pending (repos_len + 80)) + sprintf (pending_error_text, "\ +E protocol error: %s is not absolute", repos); + return 1; + } + + if (repos_len < root_len + || strncmp (current_parsed_root->directory, repos, root_len) != 0) + { + not_within: + if (alloc_pending (strlen (current_parsed_root->directory) + + strlen (repos) + + 80)) + sprintf (pending_error_text, "\ +E protocol error: directory '%s' not within root '%s'", + repos, current_parsed_root->directory); + return 1; + } + if (repos_len > root_len) + { + if (repos[root_len] != '/') + goto not_within; + if (pathname_levels (repos + root_len + 1) > 0) + goto not_within; + } + return 0; +} + + + +/* Is file or directory FILE outside the current directory (that is, does + it contain '/')? If no, return 0. If yes, set pending_error + and return 1. */ +static int +outside_dir (char *file) +{ + if (strchr (file, '/') != NULL) + { + if (alloc_pending (strlen (file) + + 80)) + sprintf (pending_error_text, "\ +E protocol error: directory '%s' not within current directory", + file); + return 1; + } + return 0; +} + + + +/* + * Add as many directories to the temp directory as the client tells us it + * will use "..", so we never try to access something outside the temp + * directory via "..". + */ +static void +serve_max_dotdot (char *arg) +{ + int lim = atoi (arg); + int i; + char *p; + +#ifdef PROXY_SUPPORT + if (proxy_log) return; +#endif /* PROXY_SUPPORT */ + + if (lim < 0 || lim > 10000) + return; + p = xmalloc (strlen (server_temp_dir) + 2 * lim + 10); + if (p == NULL) + { + pending_error = ENOMEM; + return; + } + strcpy (p, server_temp_dir); + for (i = 0; i < lim; ++i) + strcat (p, "/d"); + if (server_temp_dir != orig_server_temp_dir) + free (server_temp_dir); + server_temp_dir = p; + max_dotdot_limit = lim; +} + + + +static char *gDirname; +static char *gupdate_dir; + +static void +dirswitch (char *dir, char *repos) +{ + int status; + FILE *f; + size_t dir_len; + + TRACE (TRACE_FUNCTION, "dirswitch (%s, %s)", dir ? dir : "(null)", + repos ? repos : "(null)"); + + server_write_entries (); + + if (error_pending()) return; + + /* Check for bad directory name. + + FIXME: could/should unify these checks with server_pathname_check + except they need to report errors differently. */ + if (ISABSOLUTE (dir)) + { + if (alloc_pending (80 + strlen (dir))) + sprintf ( pending_error_text, + "E absolute pathnames invalid for server (specified `%s')", + dir); + return; + } + if (pathname_levels (dir) > max_dotdot_limit) + { + if (alloc_pending (80 + strlen (dir))) + sprintf (pending_error_text, + "E protocol error: `%s' has too many ..", dir); + return; + } + + dir_len = strlen (dir); + + /* Check for a trailing '/'. This is not ISSLASH because \ in the + protocol is an ordinary character, not a directory separator (of + course, it is perhaps unwise to use it in directory names, but that + is another issue). */ + if (dir_len > 0 + && dir[dir_len - 1] == '/') + { + if (alloc_pending (80 + dir_len)) + sprintf (pending_error_text, + "E protocol error: invalid directory syntax in %s", dir); + return; + } + + if (gDirname != NULL) + free (gDirname); + if (gupdate_dir != NULL) + free (gupdate_dir); + + if (!strcmp (dir, ".")) + gupdate_dir = xstrdup (""); + else + gupdate_dir = xstrdup (dir); + + gDirname = xmalloc (strlen (server_temp_dir) + dir_len + 40); + if (gDirname == NULL) + { + pending_error = ENOMEM; + return; + } + + strcpy (gDirname, server_temp_dir); + strcat (gDirname, "/"); + strcat (gDirname, dir); + + status = mkdir_p (gDirname); + if (status != 0 + && status != EEXIST) + { + if (alloc_pending (80 + strlen (gDirname))) + sprintf (pending_error_text, "E cannot mkdir %s", gDirname); + pending_error = status; + return; + } + + /* We need to create adm directories in all path elements because + we want the server to descend them, even if the client hasn't + sent the appropriate "Argument xxx" command to match the + already-sent "Directory xxx" command. See recurse.c + (start_recursion) for a big discussion of this. */ + + status = create_adm_p (server_temp_dir, dir); + if (status != 0) + { + if (alloc_pending (80 + strlen (gDirname))) + sprintf (pending_error_text, "E cannot create_adm_p %s", gDirname); + pending_error = status; + return; + } + + if ( CVS_CHDIR (gDirname) < 0) + { + int save_errno = errno; + if (alloc_pending (80 + strlen (gDirname))) + sprintf (pending_error_text, "E cannot change to %s", gDirname); + pending_error = save_errno; + return; + } + /* + * This is pretty much like calling Create_Admin, but Create_Admin doesn't + * report errors in the right way for us. + */ + if ((CVS_MKDIR (CVSADM, 0777) < 0) && (errno != EEXIST)) + { + int save_errno = errno; + if (alloc_pending (80 + strlen (gDirname) + strlen (CVSADM))) + sprintf (pending_error_text, + "E cannot mkdir %s/%s", gDirname, CVSADM); + pending_error = save_errno; + return; + } + + /* The following will overwrite the contents of CVSADM_REP. This + is the correct behavior -- mkdir_p may have written a + placeholder value to this file and we need to insert the + correct value. */ + + f = CVS_FOPEN (CVSADM_REP, "w"); + if (f == NULL) + { + int save_errno = errno; + if (alloc_pending (80 + strlen (gDirname) + strlen (CVSADM_REP))) + sprintf (pending_error_text, + "E cannot open %s/%s", gDirname, CVSADM_REP); + pending_error = save_errno; + return; + } + if (fprintf (f, "%s", repos) < 0) + { + int save_errno = errno; + if (alloc_pending (80 + strlen (gDirname) + strlen (CVSADM_REP))) + sprintf (pending_error_text, + "E error writing %s/%s", gDirname, CVSADM_REP); + pending_error = save_errno; + fclose (f); + return; + } + /* Non-remote CVS handles a module representing the entire tree + (e.g., an entry like ``world -a .'') by putting /. at the end + of the Repository file, so we do the same. */ + if (strcmp (dir, ".") == 0 + && current_parsed_root != NULL + && current_parsed_root->directory != NULL + && strcmp (current_parsed_root->directory, repos) == 0) + { + if (fprintf (f, "/.") < 0) + { + int save_errno = errno; + if (alloc_pending (80 + strlen (gDirname) + strlen (CVSADM_REP))) + sprintf (pending_error_text, + "E error writing %s/%s", gDirname, CVSADM_REP); + pending_error = save_errno; + fclose (f); + return; + } + } + if (fprintf (f, "\n") < 0) + { + int save_errno = errno; + if (alloc_pending (80 + strlen (gDirname) + strlen (CVSADM_REP))) + sprintf (pending_error_text, + "E error writing %s/%s", gDirname, CVSADM_REP); + pending_error = save_errno; + fclose (f); + return; + } + if (fclose (f) == EOF) + { + int save_errno = errno; + if (alloc_pending (80 + strlen (gDirname) + strlen (CVSADM_REP))) + sprintf (pending_error_text, + "E error closing %s/%s", gDirname, CVSADM_REP); + pending_error = save_errno; + return; + } + /* We open in append mode because we don't want to clobber an + existing Entries file. */ + f = CVS_FOPEN (CVSADM_ENT, "a"); + if (f == NULL) + { + int save_errno = errno; + if (alloc_pending (80 + strlen (CVSADM_ENT))) + sprintf (pending_error_text, "E cannot open %s", CVSADM_ENT); + pending_error = save_errno; + return; + } + if (fclose (f) == EOF) + { + int save_errno = errno; + if (alloc_pending (80 + strlen (CVSADM_ENT))) + sprintf (pending_error_text, "E cannot close %s", CVSADM_ENT); + pending_error = save_errno; + return; + } +} + + + +static void +serve_repository (char *arg) +{ +# ifdef PROXY_SUPPORT + assert (!proxy_log); +# endif /* PROXY_SUPPORT */ + + if (alloc_pending (80)) + strcpy (pending_error_text, + "E Repository request is obsolete; aborted"); + return; +} + + + +static void +serve_directory (char *arg) +{ + int status; + char *repos; + + TRACE (TRACE_FUNCTION, "serve_directory (%s)", arg ? arg : "(null)"); + + + /* The data needs to be read into the secondary log regardless, but + * processing of anything other than errors is skipped until later. + */ + status = buf_read_line (buf_from_net, &repos, NULL); + if (status == 0) + { + if (!ISABSOLUTE (repos)) + { + /* Make absolute. + * + * FIXME: This is kinda hacky - we should probably only ever store + * and pass SHORT_REPOS (perhaps with the occassional exception + * for optimizations, but many, many functions end up + * deconstructing REPOS to gain SHORT_REPOS anyhow) - the + * CVSROOT portion of REPOS is redundant with + * current_parsed_root->directory - but since this is the way + * things have always been done, changing this will likely involve + * a major overhaul. + */ + char *short_repos; + + short_repos = repos; + repos = Xasprintf ("%s/%s", + current_parsed_root->directory, short_repos); + free (short_repos); + } + else + repos = xstrdup (primary_root_translate (repos)); + + if ( +# ifdef PROXY_SUPPORT + !proxy_log && +# endif /* PROXY_SUPPORT */ + !outside_root (repos)) + dirswitch (arg, repos); + free (repos); + } + else if (status == -2) + { + pending_error = ENOMEM; + } + else if (status != 0) + { + pending_error_text = xmalloc (80 + strlen (arg)); + if (pending_error_text == NULL) + { + pending_error = ENOMEM; + } + else if (status == -1) + { + sprintf (pending_error_text, + "E end of file reading mode for %s", arg); + } + else + { + sprintf (pending_error_text, + "E error reading mode for %s", arg); + pending_error = status; + } + } +} + + + +static void +serve_static_directory (char *arg) +{ + FILE *f; + + if (error_pending () +# ifdef PROXY_SUPPORT + || proxy_log +# endif /* PROXY_SUPPORT */ + ) return; + + f = CVS_FOPEN (CVSADM_ENTSTAT, "w+"); + if (f == NULL) + { + int save_errno = errno; + if (alloc_pending (80 + strlen (CVSADM_ENTSTAT))) + sprintf (pending_error_text, "E cannot open %s", CVSADM_ENTSTAT); + pending_error = save_errno; + return; + } + if (fclose (f) == EOF) + { + int save_errno = errno; + if (alloc_pending (80 + strlen (CVSADM_ENTSTAT))) + sprintf (pending_error_text, "E cannot close %s", CVSADM_ENTSTAT); + pending_error = save_errno; + return; + } +} + + + +static void +serve_sticky (char *arg) +{ + FILE *f; + + if (error_pending () +# ifdef PROXY_SUPPORT + || proxy_log +# endif /* PROXY_SUPPORT */ + ) return; + + f = CVS_FOPEN (CVSADM_TAG, "w+"); + if (f == NULL) + { + int save_errno = errno; + if (alloc_pending (80 + strlen (CVSADM_TAG))) + sprintf (pending_error_text, "E cannot open %s", CVSADM_TAG); + pending_error = save_errno; + return; + } + if (fprintf (f, "%s\n", arg) < 0) + { + int save_errno = errno; + if (alloc_pending (80 + strlen (CVSADM_TAG))) + sprintf (pending_error_text, "E cannot write to %s", CVSADM_TAG); + pending_error = save_errno; + return; + } + if (fclose (f) == EOF) + { + int save_errno = errno; + if (alloc_pending (80 + strlen (CVSADM_TAG))) + sprintf (pending_error_text, "E cannot close %s", CVSADM_TAG); + pending_error = save_errno; + return; + } +} + + + +/* + * Read SIZE bytes from buf_from_net, write them to FILE. + * + * Currently this isn't really used for receiving parts of a file -- + * the file is still sent over in one chunk. But if/when we get + * spiffy in-process gzip support working, perhaps the compressed + * pieces could be sent over as they're ready, if the network is fast + * enough. Or something. + */ +static void +receive_partial_file (size_t size, int file) +{ + while (size > 0) + { + int status; + size_t nread; + char *data; + + status = buf_read_data (buf_from_net, size, &data, &nread); + if (status != 0) + { + if (status == -2) + pending_error = ENOMEM; + else + { + pending_error_text = xmalloc (80); + if (pending_error_text == NULL) + pending_error = ENOMEM; + else if (status == -1) + { + sprintf (pending_error_text, + "E premature end of file from client"); + pending_error = 0; + } + else + { + sprintf (pending_error_text, + "E error reading from client"); + pending_error = status; + } + } + return; + } + + size -= nread; + + while (nread > 0) + { + ssize_t nwrote; + + nwrote = write (file, data, nread); + if (nwrote < 0) + { + int save_errno = errno; + if (alloc_pending (40)) + strcpy (pending_error_text, "E unable to write"); + pending_error = save_errno; + + /* Read and discard the file data. */ + while (size > 0) + { + int status; + size_t nread; + char *data; + + status = buf_read_data (buf_from_net, size, &data, &nread); + if (status != 0) + return; + size -= nread; + } + + return; + } + nread -= nwrote; + data += nwrote; + } + } +} + + + +/* Receive SIZE bytes, write to filename FILE. */ +static void +receive_file (size_t size, char *file, int gzipped) +{ + int fd; + char *arg = file; + + /* Write the file. */ + fd = CVS_OPEN (arg, O_WRONLY | O_CREAT | O_TRUNC, 0600); + if (fd < 0) + { + int save_errno = errno; + if (alloc_pending (40 + strlen (arg))) + sprintf (pending_error_text, "E cannot open %s", arg); + pending_error = save_errno; + return; + } + + if (gzipped) + { + /* Using gunzip_and_write isn't really a high-performance + approach, because it keeps the whole thing in memory + (contiguous memory, worse yet). But it seems easier to + code than the alternative (and less vulnerable to subtle + bugs). Given that this feature is mainly for + compatibility, that is the better tradeoff. */ + + size_t toread = size; + char *filebuf; + char *p; + + filebuf = xmalloc (size); + p = filebuf; + /* If NULL, we still want to read the data and discard it. */ + + while (toread > 0) + { + int status; + size_t nread; + char *data; + + status = buf_read_data (buf_from_net, toread, &data, &nread); + if (status != 0) + { + if (status == -2) + pending_error = ENOMEM; + else + { + pending_error_text = xmalloc (80); + if (pending_error_text == NULL) + pending_error = ENOMEM; + else if (status == -1) + { + sprintf (pending_error_text, + "E premature end of file from client"); + pending_error = 0; + } + else + { + sprintf (pending_error_text, + "E error reading from client"); + pending_error = status; + } + } + return; + } + + toread -= nread; + + if (filebuf != NULL) + { + memcpy (p, data, nread); + p += nread; + } + } + if (filebuf == NULL) + { + pending_error = ENOMEM; + goto out; + } + + if (gunzip_and_write (fd, file, (unsigned char *) filebuf, size)) + { + if (alloc_pending (80)) + sprintf (pending_error_text, + "E aborting due to compression error"); + } + free (filebuf); + } + else + receive_partial_file (size, fd); + + if (pending_error_text) + { + char *p = xrealloc (pending_error_text, + strlen (pending_error_text) + strlen (arg) + 30); + if (p) + { + pending_error_text = p; + sprintf (p + strlen (p), ", file %s", arg); + } + /* else original string is supposed to be unchanged */ + } + + out: + if (close (fd) < 0 && !error_pending ()) + { + int save_errno = errno; + if (alloc_pending (40 + strlen (arg))) + sprintf (pending_error_text, "E cannot close %s", arg); + pending_error = save_errno; + return; + } +} + + + +/* Kopt for the next file sent in Modified or Is-modified. */ +static char *kopt; + +/* Timestamp (Checkin-time) for next file sent in Modified or + Is-modified. */ +static int checkin_time_valid; +static time_t checkin_time; + + + +/* + * Used to keep track of Entry requests. + */ +struct an_entry { + struct an_entry *next; + char *entry; +}; + +static struct an_entry *entries; + +static void +serve_is_modified (char *arg) +{ + struct an_entry *p; + char *name; + char *cp; + char *timefield; + /* Have we found this file in "entries" yet. */ + int found; + + if (error_pending () +# ifdef PROXY_SUPPORT + || proxy_log +# endif /* PROXY_SUPPORT */ + ) return; + + if (outside_dir (arg)) + return; + + /* Rewrite entries file to have `M' in timestamp field. */ + found = 0; + for (p = entries; p != NULL; p = p->next) + { + name = p->entry + 1; + cp = strchr (name, '/'); + if (cp != NULL + && strlen (arg) == cp - name + && strncmp (arg, name, cp - name) == 0) + { + if (!(timefield = strchr (cp + 1, '/')) || *++timefield == '\0') + { + /* We didn't find the record separator or it is followed by + * the end of the string, so just exit. + */ + if (alloc_pending (80)) + sprintf (pending_error_text, + "E Malformed Entry encountered."); + return; + } + /* If the time field is not currently empty, then one of + * serve_modified, serve_is_modified, & serve_unchanged were + * already called for this file. We would like to ignore the + * reinvocation silently or, better yet, exit with an error + * message, but we just avoid the copy-forward and overwrite the + * value from the last invocation instead. See the comment below + * for more. + */ + if (*timefield == '/') + { + /* Copy forward one character. Space was allocated for this + * already in serve_entry(). */ + cp = timefield + strlen (timefield); + cp[1] = '\0'; + while (cp > timefield) + { + *cp = cp[-1]; + --cp; + } + + /* *timefield == '/'; */ + } + /* If *TIMEFIELD wasn't '/' and wasn't '+', we assume that it was + * because of multiple calls to Is-modified & Unchanged by the + * client and just overwrite the value from the last call. + * Technically, we should probably either ignore calls after the + * first or send the client an error, since the client/server + * protocol specification specifies that only one call to either + * Is-Modified or Unchanged is allowed, but broken versions of + * CVSNT (at least 2.0.34 - 2.0.41, reported fixed in 2.0.41a) and + * the WinCVS & TortoiseCVS clients which depend on those broken + * versions of CVSNT (WinCVS 1.3 & at least one TortoiseCVS + * release) rely on this behavior. + */ + if (*timefield != '+') + *timefield = 'M'; + + if (kopt != NULL) + { + if (alloc_pending (strlen (name) + 80)) + sprintf (pending_error_text, + "E protocol error: both Kopt and Entry for %s", + arg); + free (kopt); + kopt = NULL; + return; + } + found = 1; + break; + } + } + if (!found) + { + /* We got Is-modified but no Entry. Add a dummy entry. + The "D" timestamp is what makes it a dummy. */ + p = xmalloc (sizeof (struct an_entry)); + if (p == NULL) + { + pending_error = ENOMEM; + return; + } + p->entry = xmalloc (strlen (arg) + 80); + if (p->entry == NULL) + { + pending_error = ENOMEM; + free (p); + return; + } + strcpy (p->entry, "/"); + strcat (p->entry, arg); + strcat (p->entry, "//D/"); + if (kopt != NULL) + { + strcat (p->entry, kopt); + free (kopt); + kopt = NULL; + } + strcat (p->entry, "/"); + p->next = entries; + entries = p; + } +} + + + +static void +serve_modified (char *arg) +{ + size_t size; + int read_size; + int status; + char *size_text; + char *mode_text; + + int gzipped = 0; + + /* + * This used to return immediately if error_pending () was true. + * However, that fails, because it causes each line of the file to + * be echoed back to the client as an unrecognized command. The + * client isn't reading from the socket, so eventually both + * processes block trying to write to the other. Now, we try to + * read the file if we can. + */ + + status = buf_read_line (buf_from_net, &mode_text, NULL); + if (status != 0) + { + if (status == -2) + pending_error = ENOMEM; + else + { + pending_error_text = xmalloc (80 + strlen (arg)); + if (pending_error_text == NULL) + pending_error = ENOMEM; + else + { + if (status == -1) + sprintf (pending_error_text, + "E end of file reading mode for %s", arg); + else + { + sprintf (pending_error_text, + "E error reading mode for %s", arg); + pending_error = status; + } + } + } + return; + } + + status = buf_read_line (buf_from_net, &size_text, NULL); + if (status != 0) + { + if (status == -2) + pending_error = ENOMEM; + else + { + pending_error_text = xmalloc (80 + strlen (arg)); + if (pending_error_text == NULL) + pending_error = ENOMEM; + else + { + if (status == -1) + sprintf (pending_error_text, + "E end of file reading size for %s", arg); + else + { + sprintf (pending_error_text, + "E error reading size for %s", arg); + pending_error = status; + } + } + } + free (mode_text); + return; + } + if (size_text[0] == 'z') + { + gzipped = 1; + read_size = atoi (size_text + 1); + } + else + read_size = atoi (size_text); + free (size_text); + + if (read_size < 0 && alloc_pending (80)) + { + sprintf (pending_error_text, + "E client sent invalid (negative) file size"); + return; + } + else + size = read_size; + + if (error_pending ()) + { + /* Now that we know the size, read and discard the file data. */ + while (size > 0) + { + int status; + size_t nread; + char *data; + + status = buf_read_data (buf_from_net, size, &data, &nread); + if (status != 0) + return; + size -= nread; + } + free (mode_text); + return; + } + + if ( +# ifdef PROXY_SUPPORT + !proxy_log && +# endif /* PROXY_SUPPORT */ + outside_dir (arg)) + { + free (mode_text); + return; + } + + receive_file (size, +# ifdef PROXY_SUPPORT + proxy_log ? DEVNULL : +# endif /* PROXY_SUPPORT */ + arg, + gzipped); + if (error_pending ()) + { + free (mode_text); + return; + } + +# ifdef PROXY_SUPPORT + /* We've read all the data that needed to be read if we're still logging + * for a secondary. Return. + */ + if (proxy_log) return; +# endif /* PROXY_SUPPORT */ + + if (checkin_time_valid) + { + struct utimbuf t; + + memset (&t, 0, sizeof (t)); + t.modtime = t.actime = checkin_time; + if (utime (arg, &t) < 0) + { + int save_errno = errno; + if (alloc_pending (80 + strlen (arg))) + sprintf (pending_error_text, "E cannot utime %s", arg); + pending_error = save_errno; + free (mode_text); + return; + } + checkin_time_valid = 0; + } + + { + int status = change_mode (arg, mode_text, 0); + free (mode_text); + if (status) + { + if (alloc_pending (40 + strlen (arg))) + sprintf (pending_error_text, + "E cannot change mode for %s", arg); + pending_error = status; + return; + } + } + + /* Make sure that the Entries indicate the right kopt. We probably + could do this even in the non-kopt case and, I think, save a stat() + call in time_stamp_server. But for conservatism I'm leaving the + non-kopt case alone. */ + if (kopt != NULL) + serve_is_modified (arg); +} + + + +static void +serve_enable_unchanged (char *arg) +{ +# ifdef PROXY_SUPPORT + /* Might as well skip this since this function does nothing anyhow. If + * it did do anything and could generate errors, then the line below would + * be necessary since this can be processed before a `Root' request. + * + * if (reprocessing) return; + */ +# endif /* PROXY_SUPPORT */ +} + + + +static void +serve_unchanged (char *arg) +{ + struct an_entry *p; + char *name; + char *cp; + char *timefield; + + if (error_pending () +# ifdef PROXY_SUPPORT + || proxy_log +# endif /* PROXY_SUPPORT */ + ) return; + + if (outside_dir (arg)) + return; + + /* Rewrite entries file to have `=' in timestamp field. */ + for (p = entries; p != NULL; p = p->next) + { + name = p->entry + 1; + cp = strchr (name, '/'); + if (cp != NULL + && strlen (arg) == cp - name + && strncmp (arg, name, cp - name) == 0) + { + if (!(timefield = strchr (cp + 1, '/')) || *++timefield == '\0') + { + /* We didn't find the record separator or it is followed by + * the end of the string, so just exit. + */ + if (alloc_pending (80)) + sprintf (pending_error_text, + "E Malformed Entry encountered."); + return; + } + /* If the time field is not currently empty, then one of + * serve_modified, serve_is_modified, & serve_unchanged were + * already called for this file. We would like to ignore the + * reinvocation silently or, better yet, exit with an error + * message, but we just avoid the copy-forward and overwrite the + * value from the last invocation instead. See the comment below + * for more. + */ + if (*timefield == '/') + { + /* Copy forward one character. Space was allocated for this + * already in serve_entry(). */ + cp = timefield + strlen (timefield); + cp[1] = '\0'; + while (cp > timefield) + { + *cp = cp[-1]; + --cp; + } + + /* *timefield == '/'; */ + } + if (*timefield != '+') + { + /* '+' is a conflict marker and we don't want to mess with it + * until Version_TS catches it. + */ + if (timefield[1] != '/') + { + /* Obliterate anything else in TIMEFIELD. This is again to + * support the broken CVSNT clients mentioned below, in + * conjunction with strict timestamp string boundry + * checking in time_stamp_server() from vers_ts.c & + * file_has_conflict() from subr.c, since the broken + * clients used to send malformed timestamp fields in the + * Entry request that they then depended on the subsequent + * Unchanged request to overwrite. + */ + char *d = timefield + 1; + if ((cp = strchr (d, '/'))) + { + while (*cp) + { + *d++ = *cp++; + } + *d = '\0'; + } + } + /* If *TIMEFIELD wasn't '/', we assume that it was because of + * multiple calls to Is-modified & Unchanged by the client and + * just overwrite the value from the last call. Technically, + * we should probably either ignore calls after the first or + * send the client an error, since the client/server protocol + * specification specifies that only one call to either + * Is-Modified or Unchanged is allowed, but broken versions of + * CVSNT (at least 2.0.34 - 2.0.41, reported fixed in 2.0.41a) + * and the WinCVS & TortoiseCVS clients which depend on those + * broken versions of CVSNT (WinCVS 1.3 & at least one + * TortoiseCVS release) rely on this behavior. + */ + *timefield = '='; + } + break; + } + } +} + + + +static void +serve_entry (char *arg) +{ + struct an_entry *p; + char *cp; + int i = 0; + + if (error_pending() +# ifdef PROXY_SUPPORT + || proxy_log +# endif /* PROXY_SUPPORT */ + ) return; + + /* Verify that the entry is well-formed. This can avoid problems later. + * At the moment we only check that the Entry contains five slashes in + * approximately the correct locations since some of the code makes + * assumptions about this. + */ + cp = arg; + if (*cp == 'D') cp++; + while (i++ < 5) + { + if (!cp || *cp != '/') + { + if (alloc_pending (80)) + sprintf (pending_error_text, + "E protocol error: Malformed Entry"); + return; + } + cp = strchr (cp + 1, '/'); + } + + p = xmalloc (sizeof (struct an_entry)); + if (p == NULL) + { + pending_error = ENOMEM; + return; + } + /* Leave space for serve_unchanged to write '=' if it wants. */ + cp = xmalloc (strlen (arg) + 2); + if (cp == NULL) + { + free (p); + pending_error = ENOMEM; + return; + } + strcpy (cp, arg); + p->next = entries; + p->entry = cp; + entries = p; +} + + + +static void +serve_kopt (char *arg) +{ + if (error_pending () +# ifdef PROXY_SUPPORT + || proxy_log +# endif /* PROXY_SUPPORT */ + ) + return; + + if (kopt != NULL) + { + if (alloc_pending (80 + strlen (arg))) + sprintf (pending_error_text, + "E protocol error: duplicate Kopt request: %s", arg); + return; + } + + /* Do some sanity checks. In particular, that it is not too long. + This lets the rest of the code not worry so much about buffer + overrun attacks. Probably should call RCS_check_kflag here, + but that would mean changing RCS_check_kflag to handle errors + other than via exit(), fprintf(), and such. */ + if (strlen (arg) > 10) + { + if (alloc_pending (80 + strlen (arg))) + sprintf (pending_error_text, + "E protocol error: invalid Kopt request: %s", arg); + return; + } + + kopt = xmalloc (strlen (arg) + 1); + if (kopt == NULL) + { + pending_error = ENOMEM; + return; + } + strcpy (kopt, arg); +} + + + +static void +serve_checkin_time (char *arg) +{ + struct timespec t; + + if (error_pending () +# ifdef PROXY_SUPPORT + || proxy_log +# endif /* PROXY_SUPPORT */ + ) + return; + + if (checkin_time_valid) + { + if (alloc_pending (80 + strlen (arg))) + sprintf (pending_error_text, + "E protocol error: duplicate Checkin-time request: %s", + arg); + return; + } + + if (!get_date (&t, arg, NULL)) + { + if (alloc_pending (80 + strlen (arg))) + sprintf (pending_error_text, "E cannot parse date %s", arg); + return; + } + + /* Truncate any nanoseconds returned by get_date(). */ + checkin_time = t.tv_sec; + checkin_time_valid = 1; +} + + + +static void +server_write_entries (void) +{ + FILE *f; + struct an_entry *p; + struct an_entry *q; + + if (entries == NULL) + return; + + f = NULL; + /* Note that we free all the entries regardless of errors. */ + if (!error_pending ()) + { + /* We open in append mode because we don't want to clobber an + existing Entries file. If we are checking out a module + which explicitly lists more than one file in a particular + directory, then we will wind up calling + server_write_entries for each such file. */ + f = CVS_FOPEN (CVSADM_ENT, "a"); + if (f == NULL) + { + int save_errno = errno; + if (alloc_pending (80 + strlen (CVSADM_ENT))) + sprintf (pending_error_text, "E cannot open %s", CVSADM_ENT); + pending_error = save_errno; + } + } + for (p = entries; p != NULL;) + { + if (!error_pending ()) + { + if (fprintf (f, "%s\n", p->entry) < 0) + { + int save_errno = errno; + if (alloc_pending (80 + strlen(CVSADM_ENT))) + sprintf (pending_error_text, + "E cannot write to %s", CVSADM_ENT); + pending_error = save_errno; + } + } + free (p->entry); + q = p->next; + free (p); + p = q; + } + entries = NULL; + if (f != NULL && fclose (f) == EOF && !error_pending ()) + { + int save_errno = errno; + if (alloc_pending (80 + strlen (CVSADM_ENT))) + sprintf (pending_error_text, "E cannot close %s", CVSADM_ENT); + pending_error = save_errno; + } +} + + + +# ifdef PROXY_SUPPORT +/* + * callback proc to run a script when admin finishes. + */ +static int +prepost_proxy_proc (const char *repository, const char *filter, void *closure) +{ + char *cmdline; + bool *pre = closure; + + /* %c = cvs_cmd_name + * %p = shortrepos + * %r = repository + */ + TRACE (TRACE_FUNCTION, "prepost_proxy_proc (%s, %s, %s)", repository, + filter, *pre ? "pre" : "post"); + + /* + * Cast any NULL arguments as appropriate pointers as this is an + * stdarg function and we need to be certain the caller gets what + * is expected. + */ + cmdline = format_cmdline ( +# ifdef SUPPORT_OLD_INFO_FMT_STRINGS + 0, ".", +# endif /* SUPPORT_OLD_INFO_FMT_STRINGS */ + filter, + "c", "s", cvs_cmd_name, + "R", "s", referrer ? referrer->original : "NONE", + "p", "s", ".", + "r", "s", current_parsed_root->directory, + "P", "s", config->PrimaryServer->original, + (char *) NULL); + + if (!cmdline || !strlen (cmdline)) + { + if (cmdline) free (cmdline); + if (*pre) + error (0, 0, "preadmin proc resolved to the empty string!"); + else + error (0, 0, "postadmin proc resolved to the empty string!"); + return 1; + } + + run_setup (cmdline); + + free (cmdline); + + /* FIXME - read the comment in verifymsg_proc() about why we use abs() + * below() and shouldn't. + */ + return abs (run_exec (RUN_TTY, RUN_TTY, RUN_TTY, + RUN_NORMAL | RUN_SIGIGNORE)); +} + + + +/* Become a secondary write proxy to a master server. + * + * This function opens the connection to the primary, dumps the secondary log + * to the primary, then reads data from any available connection and writes it + * to its partner: + * + * buf_from_net -> buf_to_primary + * buf_from_primary -> buf_to_net + * + * When all "from" connections have sent EOF and all data has been sent to + * "to" connections, this function closes the "to" pipes and returns. + */ +static void +become_proxy (void) +{ + struct buffer *buf_to_primary; + struct buffer *buf_from_primary; + + /* Close the client log and open it for read. */ + struct buffer *buf_clientlog = log_buffer_rewind (proxy_log_out); + int status, to_primary_fd, from_primary_fd, to_net_fd, from_net_fd; + + /* Call presecondary script. */ + bool pre = true; + + char *data; + size_t thispass, got; + int s; + char *newdata; + + Parse_Info (CVSROOTADM_PREPROXY, current_parsed_root->directory, + prepost_proxy_proc, PIOPT_ALL, &pre); + + /* Open connection to primary server. */ + open_connection_to_server (config->PrimaryServer, &buf_to_primary, + &buf_from_primary); + setup_logfiles ("CVS_SECONDARY_LOG", &buf_to_primary, &buf_from_primary); + if ((status = set_nonblock (buf_from_primary))) + error (1, status, "failed to set nonblocking io from primary"); + if ((status = set_nonblock (buf_from_net))) + error (1, status, "failed to set nonblocking io from client"); + if ((status = set_nonblock (buf_to_primary))) + error (1, status, "failed to set nonblocking io to primary"); + if ((status = set_nonblock (buf_to_net))) + error (1, status, "failed to set nonblocking io to client"); + + to_primary_fd = buf_get_fd (buf_to_primary); + from_primary_fd = buf_get_fd (buf_from_primary); + to_net_fd = buf_get_fd (buf_to_net); + assert (to_primary_fd >= 0 && from_primary_fd >= 0 && to_net_fd >= 0); + + /* Close the client log and open it for read. */ + rewind_buf_from_net (); + + while (from_primary_fd >= 0 || to_primary_fd >= 0) + { + fd_set readfds, writefds; + int status, numfds = -1; + struct timeval *timeout_ptr; + struct timeval timeout; + size_t toread; + + FD_ZERO (&readfds); + FD_ZERO (&writefds); + + /* The fd for a multi-source buffer can change with any read. */ + from_net_fd = buf_from_net ? buf_get_fd (buf_from_net) : -1; + + if ((buf_from_net && !buf_empty_p (buf_from_net)) + || (buf_from_primary && !buf_empty_p (buf_from_primary))) + { + /* There is data pending so don't block if we don't find any new + * data on the fds. + */ + timeout.tv_sec = 0; + timeout.tv_usec = 0; + timeout_ptr = &timeout; + } + else + /* block indefinately */ + timeout_ptr = NULL; + + /* Set writefds if data is pending. */ + if (to_net_fd >= 0 && !buf_empty_p (buf_to_net)) + { + FD_SET (to_net_fd, &writefds); + numfds = MAX (numfds, to_net_fd); + } + if (to_primary_fd >= 0 && !buf_empty_p (buf_to_primary)) + { + FD_SET (to_primary_fd, &writefds); + numfds = MAX (numfds, to_primary_fd); + } + + /* Set readfds if descriptors are still open. */ + if (from_net_fd >= 0) + { + FD_SET (from_net_fd, &readfds); + numfds = MAX (numfds, from_net_fd); + } + if (from_primary_fd >= 0) + { + FD_SET (from_primary_fd, &readfds); + numfds = MAX (numfds, from_primary_fd); + } + + /* NUMFDS needs to be the highest descriptor + 1 according to the + * select spec. + */ + numfds++; + + do { + /* This used to select on exceptions too, but as far + as I know there was never any reason to do that and + SCO doesn't let you select on exceptions on pipes. */ + numfds = select (numfds, &readfds, &writefds, + NULL, timeout_ptr); + if (numfds < 0 && errno != EINTR) + { + /* Sending an error to the client, possibly in the middle of a + * separate protocol message, will likely not mean much to the + * client, but it's better than nothing, I guess. + */ + buf_output0 (buf_to_net, "E select failed\n"); + print_error (errno); + exit (EXIT_FAILURE); + } + } while (numfds < 0); + + if (numfds == 0) + { + FD_ZERO (&readfds); + FD_ZERO (&writefds); + } + + if (to_net_fd >= 0 && FD_ISSET (to_net_fd, &writefds)) + { + /* What should we do with errors? syslog() them? */ + buf_send_output (buf_to_net); + buf_flush (buf_to_net, false); + } + + status = 0; + if (from_net_fd >= 0 && (FD_ISSET (from_net_fd, &readfds))) + status = buf_input_data (buf_from_net, NULL); + + if (buf_from_net && !buf_empty_p (buf_from_net)) + { + if (buf_to_primary) + buf_append_buffer (buf_to_primary, buf_from_net); + else + /* (Sys?)log this? */; + + } + + if (status == -1 /* EOF */) + { + SIG_beginCrSect(); + /* Need only to shut this down and set to NULL, really, in + * crit sec, to ensure no double-dispose and to make sure + * network pipes are closed as properly as possible, but I + * don't see much optimization potential in saving values and + * postponing the free. + */ + buf_shutdown (buf_from_net); + buf_free (buf_from_net); + buf_from_net = NULL; + /* So buf_to_primary will be closed at the end of this loop. */ + from_net_fd = -1; + SIG_endCrSect(); + } + else if (status > 0 /* ERRNO */) + { + buf_output0 (buf_to_net, + "E buf_input_data failed reading from client\n"); + print_error (status); + exit (EXIT_FAILURE); + } + + if (to_primary_fd >= 0 && FD_ISSET (to_primary_fd, &writefds)) + { + /* What should we do with errors? syslog() them? */ + buf_send_output (buf_to_primary); + buf_flush (buf_to_primary, false); + } + + status = 0; + if (from_primary_fd >= 0 && FD_ISSET (from_primary_fd, &readfds)) + status = buf_input_data (buf_from_primary, &toread); + + /* Avoid resending data from the server which we already sent to the + * client. Otherwise clients get really confused. + */ + if (buf_clientlog + && buf_from_primary && !buf_empty_p (buf_from_primary)) + { + /* Dispose of data we already sent to the client. */ + while (buf_clientlog && toread > 0) + { + s = buf_read_data (buf_clientlog, toread, &data, &got); + if (s == -2) + error (1, ENOMEM, "Failed to read data."); + if (s == -1) + { + buf_shutdown (buf_clientlog); + buf_clientlog = NULL; + } + else if (s) + error (1, s, "Error reading writeproxy log."); + else + { + thispass = got; + while (thispass > 0) + { + /* No need to check for errors here since we know we + * won't read more than buf_input read into + * BUF_FROM_PRIMARY (see how TOREAD is set above). + */ + buf_read_data (buf_from_primary, thispass, &newdata, + &got); + /* Verify that we are throwing away what we think we + * are. + * + * It is valid to assume that the secondary and primary + * are closely enough in sync that this portion of the + * communication will be in sync beacuse if they were + * not, then the secondary might provide a + * valid-request string to the client which contained a + * request that the primary didn't support. If the + * client later used the request, the primary server + * would exit anyhow. + * + * FIXME? + * An alternative approach might be to make sure that + * the secondary provides the same string as the + * primary regardless, for purposes like pointing a + * secondary at an unwitting primary, in which case it + * might be useful to have some way to override the + * valid-requests string on a secondary, but it seems + * much easier to simply sync the versions, at the + * moment. + */ + if (memcmp (data, newdata, got)) + error (1, 0, "Secondary out of sync with primary!"); + data += got; + thispass -= got; + } + toread -= got; + } + } + } + + if (buf_from_primary && !buf_empty_p (buf_from_primary)) + { + if (buf_to_net) + buf_append_buffer (buf_to_net, buf_from_primary); + else + /* (Sys?)log this? */; + + } + + if (status == -1 /* EOF */) + { + buf_shutdown (buf_from_primary); + buf_from_primary = NULL; + from_primary_fd = -1; + } + else if (status > 0 /* ERRNO */) + { + buf_output0 (buf_to_net, + "E buf_input_data failed reading from primary\n"); + print_error (status); + exit (EXIT_FAILURE); + } + + /* If our "source pipe" is closed and all data has been sent, avoid + * selecting it for writability, but don't actually close the buffer in + * case other routines want to use it later. The buffer will be closed + * in server_cleanup (). + */ + if (from_primary_fd < 0 + && buf_to_net && buf_empty_p (buf_to_net)) + to_net_fd = -1; + + if (buf_to_primary + && (/* Assume that there is no further reason to keep the buffer to + * the primary open if we can no longer read its responses. + */ + (from_primary_fd < 0 && buf_to_primary) + /* Also close buf_to_primary when it becomes impossible to find + * more data to send to it. We don't close buf_from_primary + * yet since there may be data pending or the primary may react + * to the EOF on its input pipe. + */ + || (from_net_fd < 0 && buf_empty_p (buf_to_primary)))) + { + buf_shutdown (buf_to_primary); + buf_free (buf_to_primary); + buf_to_primary = NULL; + + /* Setting the fd < 0 with from_primary_fd already < 0 will cause + * an escape from this while loop. + */ + to_primary_fd = -1; + } + } + + /* Call postsecondary script. */ + pre = false; + Parse_Info (CVSROOTADM_POSTPROXY, current_parsed_root->directory, + prepost_proxy_proc, PIOPT_ALL, &pre); +} +# endif /* PROXY_SUPPORT */ + + + +struct notify_note { + /* Directory in which this notification happens. xmalloc'd*/ + char *dir; + + /* xmalloc'd. */ + char *update_dir; + + /* xmalloc'd. */ + char *filename; + + /* The following three all in one xmalloc'd block, pointed to by TYPE. + Each '\0' terminated. */ + /* "E" or "U". */ + char *type; + /* time+host+dir */ + char *val; + char *watches; + + struct notify_note *next; +}; + +static struct notify_note *notify_list; +/* Used while building list, to point to the last node that already exists. */ +static struct notify_note *last_node; + +static void +serve_notify (char *arg) +{ + struct notify_note *new = NULL; + char *data = NULL; + int status; + + if (error_pending ()) return; + + if (isProxyServer()) + { +# ifdef PROXY_SUPPORT + if (!proxy_log) + { +# endif /* PROXY_SUPPORT */ + if (alloc_pending (160) + strlen (program_name)) + sprintf (pending_error_text, +"E This CVS server does not support disconnected `%s edit'. For now, remove all `%s' files in your workspace and try your command again.", + program_name, CVSADM_NOTIFY); + return; +# ifdef PROXY_SUPPORT + } + else + { + /* This is effectively a write command, so run it on the primary. */ + become_proxy (); + exit (EXIT_SUCCESS); + } +# endif /* PROXY_SUPPORT */ + } + + if (outside_dir (arg)) + return; + + if (gDirname == NULL) + goto error; + + new = xmalloc (sizeof (struct notify_note)); + if (new == NULL) + { + pending_error = ENOMEM; + return; + } + new->dir = xmalloc (strlen (gDirname) + 1); + new->update_dir = xmalloc (strlen (gupdate_dir) + 1); + new->filename = xmalloc (strlen (arg) + 1); + if (new->dir == NULL || new->update_dir == NULL || new->filename == NULL) + { + pending_error = ENOMEM; + if (new->dir != NULL) + free (new->dir); + free (new); + return; + } + strcpy (new->dir, gDirname); + strcpy (new->update_dir, gupdate_dir); + strcpy (new->filename, arg); + + status = buf_read_line (buf_from_net, &data, NULL); + if (status != 0) + { + if (status == -2) + pending_error = ENOMEM; + else + { + pending_error_text = xmalloc (80 + strlen (arg)); + if (pending_error_text == NULL) + pending_error = ENOMEM; + else + { + if (status == -1) + sprintf (pending_error_text, + "E end of file reading notification for %s", arg); + else + { + sprintf (pending_error_text, + "E error reading notification for %s", arg); + pending_error = status; + } + } + } + free (new->filename); + free (new->dir); + free (new); + } + else + { + char *cp; + + if (!data[0]) + goto error; + + if (strchr (data, '+')) + goto error; + + new->type = data; + if (data[1] != '\t') + goto error; + data[1] = '\0'; + cp = data + 2; + new->val = cp; + cp = strchr (cp, '\t'); + if (cp == NULL) + goto error; + *cp++ = '+'; + cp = strchr (cp, '\t'); + if (cp == NULL) + goto error; + *cp++ = '+'; + cp = strchr (cp, '\t'); + if (cp == NULL) + goto error; + *cp++ = '\0'; + new->watches = cp; + /* If there is another tab, ignore everything after it, + for future expansion. */ + cp = strchr (cp, '\t'); + if (cp != NULL) + *cp = '\0'; + + new->next = NULL; + + if (last_node == NULL) + notify_list = new; + else + last_node->next = new; + last_node = new; + } + return; + error: + pending_error = 0; + if (alloc_pending (80)) + strcpy (pending_error_text, + "E Protocol error; misformed Notify request"); + if (data != NULL) + free (data); + if (new != NULL) + { + free (new->filename); + free (new->update_dir); + free (new->dir); + free (new); + } + return; +} + + + +static void +serve_hostname (char *arg) +{ + free (hostname); + hostname = xstrdup (arg); + return; +} + + + +static void +serve_localdir (char *arg) +{ + if (CurDir) free (CurDir); + CurDir = xstrdup (arg); +} + + + +/* Process all the Notify requests that we have stored up. Returns 0 + if successful, if not prints error message (via error()) and + returns negative value. */ +static int +server_notify (void) +{ + struct notify_note *p; + char *repos; + + TRACE (TRACE_FUNCTION, "server_notify()"); + + while (notify_list != NULL) + { + if (CVS_CHDIR (notify_list->dir) < 0) + { + error (0, errno, "cannot change to %s", notify_list->dir); + return -1; + } + repos = Name_Repository (NULL, NULL); + + lock_dir_for_write (repos); + + fileattr_startdir (repos); + + notify_do (*notify_list->type, notify_list->filename, + notify_list->update_dir, getcaller(), notify_list->val, + notify_list->watches, repos); + + buf_output0 (buf_to_net, "Notified "); + { + char *dir = notify_list->dir + strlen (server_temp_dir) + 1; + if (dir[0] == '\0') + buf_append_char (buf_to_net, '.'); + else + buf_output0 (buf_to_net, dir); + buf_append_char (buf_to_net, '/'); + buf_append_char (buf_to_net, '\n'); + } + buf_output0 (buf_to_net, repos); + buf_append_char (buf_to_net, '/'); + buf_output0 (buf_to_net, notify_list->filename); + buf_append_char (buf_to_net, '\n'); + free (repos); + + p = notify_list->next; + free (notify_list->filename); + free (notify_list->dir); + free (notify_list->type); + free (notify_list); + notify_list = p; + + fileattr_write (); + fileattr_free (); + + Lock_Cleanup (); + } + + last_node = NULL; + + /* The code used to call fflush (stdout) here, but that is no + longer necessary. The data is now buffered in buf_to_net, + which will be flushed by the caller, do_cvs_command. */ + + return 0; +} + + + +/* This request is processed in all passes since requests which must + * sometimes be processed before it is known whether we are running as a + * secondary or not, for instance the `expand-modules' request, sometimes use + * the `Arguments'. + */ +static void +serve_argument (char *arg) +{ + char *p; + + if (error_pending()) return; + + if (argument_count >= 10000) + { + if (alloc_pending (80)) + sprintf (pending_error_text, + "E Protocol error: too many arguments"); + return; + } + + if (argument_vector_size <= argument_count) + { + argument_vector_size *= 2; + argument_vector = xnrealloc (argument_vector, + argument_vector_size, sizeof (char *)); + if (argument_vector == NULL) + { + pending_error = ENOMEM; + return; + } + } + p = xmalloc (strlen (arg) + 1); + if (p == NULL) + { + pending_error = ENOMEM; + return; + } + strcpy (p, arg); + argument_vector[argument_count++] = p; +} + + + +/* For secondary servers, this is handled in all passes, as is the `Argument' + * request, and for the same reasons. + */ +static void +serve_argumentx (char *arg) +{ + char *p; + + if (error_pending()) return; + + if (argument_count <= 1) + { + if (alloc_pending (80)) + sprintf (pending_error_text, +"E Protocol error: called argumentx without prior call to argument"); + return; + } + + p = argument_vector[argument_count - 1]; + p = xrealloc (p, strlen (p) + 1 + strlen (arg) + 1); + if (p == NULL) + { + pending_error = ENOMEM; + return; + } + strcat (p, "\n"); + strcat (p, arg); + argument_vector[argument_count - 1] = p; +} + + + +static void +serve_global_option (char *arg) +{ +# ifdef PROXY_SUPPORT + /* This can generate error messages and termination before `Root' requests, + * so it must be dealt with in the first pass. + */ + if (reprocessing) return; +# endif /* PROXY_SUPPORT */ + + if (arg[0] != '-' || arg[1] == '\0' || arg[2] != '\0') + { + error_return: + if (alloc_pending (strlen (arg) + 80)) + sprintf (pending_error_text, + "E Protocol error: bad global option %s", + arg); + return; + } + switch (arg[1]) + { + case 'l': + error(0, 0, "WARNING: global `-l' option ignored."); + break; + case 'n': + noexec = 1; + logoff = 1; + break; + case 'q': + quiet = 1; + break; + case 'r': + cvswrite = 0; + break; + case 'Q': + really_quiet = 1; + break; + case 't': + trace++; + break; + default: + goto error_return; + } +} + + + +/* This needs to be processed before Root requests, so we allow it to be + * be processed before knowing whether we are running as a secondary server + * to allow `noop' and `Root' requests to generate errors as before. + */ +static void +serve_set (char *arg) +{ +# ifdef PROXY_SUPPORT + if (reprocessing) return; +# endif /* PROXY_SUPPORT */ + + /* FIXME: This sends errors immediately (I think); they should be + put into pending_error. */ + variable_set (arg); +} + +# ifdef ENCRYPTION + +# ifdef HAVE_KERBEROS + +static void +serve_kerberos_encrypt( char *arg ) +{ +# ifdef PROXY_SUPPORT + assert (!proxy_log); +# endif /* PROXY_SUPPORT */ + + /* All future communication with the client will be encrypted. */ + + buf_to_net = krb_encrypt_buffer_initialize (buf_to_net, 0, sched, + kblock, + buf_to_net->memory_error); + buf_from_net = krb_encrypt_buffer_initialize (buf_from_net, 1, sched, + kblock, + buf_from_net->memory_error); +} + +# endif /* HAVE_KERBEROS */ + +# ifdef HAVE_GSSAPI + +static void +serve_gssapi_encrypt( char *arg ) +{ +# ifdef PROXY_SUPPORT + assert (!proxy_log); +# endif /* PROXY_SUPPORT */ + + if (cvs_gssapi_wrapping) + { + /* We're already using a gssapi_wrap buffer for stream + authentication. Flush everything we've output so far, and + turn on encryption for future data. On the input side, we + should only have unwrapped as far as the Gssapi-encrypt + command, so future unwrapping will become encrypted. */ + buf_flush (buf_to_net, 1); + cvs_gssapi_encrypt = 1; + return; + } + + /* All future communication with the client will be encrypted. */ + + cvs_gssapi_encrypt = 1; + + buf_to_net = cvs_gssapi_wrap_buffer_initialize (buf_to_net, 0, + gcontext, + buf_to_net->memory_error); + buf_from_net = cvs_gssapi_wrap_buffer_initialize (buf_from_net, 1, + gcontext, + buf_from_net->memory_error); + + cvs_gssapi_wrapping = 1; +} + +# endif /* HAVE_GSSAPI */ + +# endif /* ENCRYPTION */ + +# ifdef HAVE_GSSAPI + +static void +serve_gssapi_authenticate (char *arg) +{ +# ifdef PROXY_SUPPORT + assert (!proxy_log); +# endif /* PROXY_SUPPORT */ + + if (cvs_gssapi_wrapping) + { + /* We're already using a gssapi_wrap buffer for encryption. + That includes authentication, so we don't have to do + anything further. */ + return; + } + + buf_to_net = cvs_gssapi_wrap_buffer_initialize (buf_to_net, 0, + gcontext, + buf_to_net->memory_error); + buf_from_net = cvs_gssapi_wrap_buffer_initialize (buf_from_net, 1, + gcontext, + buf_from_net->memory_error); + + cvs_gssapi_wrapping = 1; +} + +# endif /* HAVE_GSSAPI */ + + + +# ifdef SERVER_FLOWCONTROL +/* The maximum we'll queue to the remote client before blocking. */ +# ifndef SERVER_HI_WATER +# define SERVER_HI_WATER (2 * 1024 * 1024) +# endif /* SERVER_HI_WATER */ +/* When the buffer drops to this, we restart the child */ +# ifndef SERVER_LO_WATER +# define SERVER_LO_WATER (1 * 1024 * 1024) +# endif /* SERVER_LO_WATER */ +# endif /* SERVER_FLOWCONTROL */ + + + +static void +serve_questionable (char *arg) +{ + static int initted; + +# ifdef PROXY_SUPPORT + if (proxy_log) return; +# endif /* PROXY_SUPPORT */ + + if (error_pending ()) return; + + if (!initted) + { + /* Pick up ignores from CVSROOTADM_IGNORE, $HOME/.cvsignore on server, + and CVSIGNORE on server. */ + ign_setup (); + initted = 1; + } + + if (gDirname == NULL) + { + if (alloc_pending (80)) + sprintf (pending_error_text, +"E Protocol error: `Directory' missing"); + return; + } + + if (outside_dir (arg)) + return; + + if (!ign_name (arg)) + { + char *update_dir; + + buf_output (buf_to_net, "M ? ", 4); + update_dir = gDirname + strlen (server_temp_dir) + 1; + if (!(update_dir[0] == '.' && update_dir[1] == '\0')) + { + buf_output0 (buf_to_net, update_dir); + buf_output (buf_to_net, "/", 1); + } + buf_output0 (buf_to_net, arg); + buf_output (buf_to_net, "\n", 1); + } +} + + + +static struct buffer *protocol = NULL; + +/* This is the output which we are saving up to send to the server, in the + child process. We will push it through, via the `protocol' buffer, when + we have a complete line. */ +static struct buffer *saved_output; + +/* Likewise, but stuff which will go to stderr. */ +static struct buffer *saved_outerr; + + + +static void +protocol_memory_error (struct buffer *buf) +{ + error (1, ENOMEM, "Virtual memory exhausted"); +} + + + +/* If command is valid, return 1. + * Else if command is invalid and croak_on_invalid is set, then die. + * Else just return 0 to indicate that command is invalid. + */ +static bool +check_command_valid_p (char *cmd_name) +{ + /* Right now, only pserver notices invalid commands -- namely, + * write attempts by a read-only user. Therefore, if CVS_Username + * is not set, this just returns 1, because CVS_Username unset + * means pserver is not active. + */ +# ifdef AUTH_SERVER_SUPPORT + if (CVS_Username == NULL) + return true; + + if (lookup_command_attribute (cmd_name) & CVS_CMD_MODIFIES_REPOSITORY) + { + /* This command has the potential to modify the repository, so + * we check if the user have permission to do that. + * + * (Only relevant for remote users -- local users can do + * whatever normal Unix file permissions allow them to do.) + * + * The decision method: + * + * If $CVSROOT/CVSADMROOT_READERS exists and user is listed + * in it, then read-only access for user. + * + * Or if $CVSROOT/CVSADMROOT_WRITERS exists and user NOT + * listed in it, then also read-only access for user. + * + * Else read-write access for user. + */ + + char *linebuf = NULL; + int num_red = 0; + size_t linebuf_len = 0; + char *fname; + size_t flen; + FILE *fp; + int found_it = 0; + + /* else */ + flen = strlen (current_parsed_root->directory) + + strlen (CVSROOTADM) + + strlen (CVSROOTADM_READERS) + + 3; + + fname = xmalloc (flen); + (void) sprintf (fname, "%s/%s/%s", current_parsed_root->directory, + CVSROOTADM, CVSROOTADM_READERS); + + fp = fopen (fname, "r"); + + if (fp == NULL) + { + if (!existence_error (errno)) + { + /* Need to deny access, so that attackers can't fool + us with some sort of denial of service attack. */ + error (0, errno, "cannot open %s", fname); + free (fname); + return false; + } + } + else /* successfully opened readers file */ + { + while ((num_red = getline (&linebuf, &linebuf_len, fp)) >= 0) + { + /* Hmmm, is it worth importing my own readline + library into CVS? It takes care of chopping + leading and trailing whitespace, "#" comments, and + newlines automatically when so requested. Would + save some code here... -kff */ + + /* Chop newline by hand, for strcmp()'s sake. */ + if (num_red > 0 && linebuf[num_red - 1] == '\n') + linebuf[num_red - 1] = '\0'; + + if (strcmp (linebuf, CVS_Username) == 0) + goto handle_invalid; + } + if (num_red < 0 && !feof (fp)) + error (0, errno, "cannot read %s", fname); + + /* If not listed specifically as a reader, then this user + has write access by default unless writers are also + specified in a file . */ + if (fclose (fp) < 0) + error (0, errno, "cannot close %s", fname); + } + free (fname); + + /* Now check the writers file. */ + + flen = strlen (current_parsed_root->directory) + + strlen (CVSROOTADM) + + strlen (CVSROOTADM_WRITERS) + + 3; + + fname = xmalloc (flen); + (void) sprintf (fname, "%s/%s/%s", current_parsed_root->directory, + CVSROOTADM, CVSROOTADM_WRITERS); + + fp = fopen (fname, "r"); + + if (fp == NULL) + { + if (linebuf) + free (linebuf); + if (existence_error (errno)) + { + /* Writers file does not exist, so everyone is a writer, + by default. */ + free (fname); + return true; + } + else + { + /* Need to deny access, so that attackers can't fool + us with some sort of denial of service attack. */ + error (0, errno, "cannot read %s", fname); + free (fname); + return false; + } + } + + found_it = 0; + while ((num_red = getline (&linebuf, &linebuf_len, fp)) >= 0) + { + /* Chop newline by hand, for strcmp()'s sake. */ + if (num_red > 0 && linebuf[num_red - 1] == '\n') + linebuf[num_red - 1] = '\0'; + + if (strcmp (linebuf, CVS_Username) == 0) + { + found_it = 1; + break; + } + } + if (num_red < 0 && !feof (fp)) + error (0, errno, "cannot read %s", fname); + + if (found_it) + { + if (fclose (fp) < 0) + error (0, errno, "cannot close %s", fname); + if (linebuf) + free (linebuf); + free (fname); + return true; + } + else /* writers file exists, but this user not listed in it */ + { + handle_invalid: + if (fclose (fp) < 0) + error (0, errno, "cannot close %s", fname); + if (linebuf) + free (linebuf); + free (fname); + return false; + } + } +# endif /* AUTH_SERVER_SUPPORT */ + + /* If ever reach end of this function, command must be valid. */ + return true; +} + + + +/* Execute COMMAND in a subprocess with the approriate funky things done. */ + +static struct fd_set_wrapper { fd_set fds; } command_fds_to_drain; +# ifdef SUNOS_KLUDGE +static int max_command_fd; +# endif + +# ifdef SERVER_FLOWCONTROL +static int flowcontrol_pipe[2]; +# endif /* SERVER_FLOWCONTROL */ + + + +/* + * Set buffer FD to non-blocking I/O. Returns 0 for success or errno + * code. + */ +int +set_nonblock_fd (int fd) +{ +# if defined (F_GETFL) && defined (O_NONBLOCK) && defined (F_SETFL) + int flags; + + flags = fcntl (fd, F_GETFL, 0); + if (flags < 0) + return errno; + if (fcntl (fd, F_SETFL, flags | O_NONBLOCK) < 0) + return errno; +# endif /* F_GETFL && O_NONBLOCK && F_SETFL */ + return 0; +} + + + +static void +do_cvs_command (char *cmd_name, int (*command) (int, char **)) +{ + /* + * The following file descriptors are set to -1 if that file is not + * currently open. + */ + + /* Data on these pipes is a series of '\n'-terminated lines. */ + int stdout_pipe[2]; + int stderr_pipe[2]; + + /* + * Data on this pipe is a series of counted (see buf_send_counted) + * packets. Each packet must be processed atomically (i.e. not + * interleaved with data from stdout_pipe or stderr_pipe). + */ + int protocol_pipe[2]; + + int dev_null_fd = -1; + + int errs; + + TRACE (TRACE_FUNCTION, "do_cvs_command (%s)", cmd_name); + + /* Write proxy logging is always terminated when a command is received. + * Therefore, we wish to avoid reprocessing the command since that would + * cause endless recursion. + */ + if (isProxyServer()) + { +# ifdef PROXY_SUPPORT + if (reprocessing) + /* This must be the second time we've reached this point. + * Done reprocessing. + */ + reprocessing = false; + else + { + if (lookup_command_attribute (cmd_name) + & CVS_CMD_MODIFIES_REPOSITORY) + { + become_proxy (); + exit (EXIT_SUCCESS); + } + else if (/* serve_co may have called this already and missing logs + * should have generated an error in serve_root(). + */ + proxy_log) + { + /* Set up the log for reprocessing. */ + rewind_buf_from_net (); + /* And return to the main loop in server(), where we will now + * find the logged secondary data and reread it. + */ + return; + } + } +# else /* !PROXY_SUPPORT */ + if (lookup_command_attribute (cmd_name) + & CVS_CMD_MODIFIES_REPOSITORY + && alloc_pending (120)) + sprintf (pending_error_text, +"E You need a CVS client that supports the `Redirect' response for write requests to this server."); + return; +# endif /* PROXY_SUPPORT */ + } + + command_pid = -1; + stdout_pipe[0] = -1; + stdout_pipe[1] = -1; + stderr_pipe[0] = -1; + stderr_pipe[1] = -1; + protocol_pipe[0] = -1; + protocol_pipe[1] = -1; + + server_write_entries (); + + if (print_pending_error ()) + goto free_args_and_return; + + /* Global `cvs_cmd_name' is probably "server" right now -- only + serve_export() sets it to anything else. So we will use local + parameter `cmd_name' to determine if this command is valid for + this user. */ + if (!check_command_valid_p (cmd_name)) + { + buf_output0 (buf_to_net, "E "); + buf_output0 (buf_to_net, program_name); + buf_output0 (buf_to_net, " [server aborted]: \""); + buf_output0 (buf_to_net, cmd_name); + buf_output0 (buf_to_net, +"\" requires write access to the repository\n\ +error \n"); + goto free_args_and_return; + } + cvs_cmd_name = cmd_name; + + (void) server_notify (); + + /* + * We use a child process which actually does the operation. This + * is so we can intercept its standard output. Even if all of CVS + * were written to go to some special routine instead of writing + * to stdout or stderr, we would still need to do the same thing + * for the RCS commands. + */ + + if (pipe (stdout_pipe) < 0) + { + buf_output0 (buf_to_net, "E pipe failed\n"); + print_error (errno); + goto error_exit; + } + if (pipe (stderr_pipe) < 0) + { + buf_output0 (buf_to_net, "E pipe failed\n"); + print_error (errno); + goto error_exit; + } + if (pipe (protocol_pipe) < 0) + { + buf_output0 (buf_to_net, "E pipe failed\n"); + print_error (errno); + goto error_exit; + } +# ifdef SERVER_FLOWCONTROL + if (pipe (flowcontrol_pipe) < 0) + { + buf_output0 (buf_to_net, "E pipe failed\n"); + print_error (errno); + goto error_exit; + } + set_nonblock_fd (flowcontrol_pipe[0]); + set_nonblock_fd (flowcontrol_pipe[1]); +# endif /* SERVER_FLOWCONTROL */ + + dev_null_fd = CVS_OPEN (DEVNULL, O_RDONLY); + if (dev_null_fd < 0) + { + buf_output0 (buf_to_net, "E open /dev/null failed\n"); + print_error (errno); + goto error_exit; + } + + /* We shouldn't have any partial lines from cvs_output and + cvs_outerr, but we handle them here in case there is a bug. */ + /* FIXME: appending a newline, rather than using "MT" as we + do in the child process, is probably not really a very good + way to "handle" them. */ + if (! buf_empty_p (saved_output)) + { + buf_append_char (saved_output, '\n'); + buf_copy_lines (buf_to_net, saved_output, 'M'); + } + if (! buf_empty_p (saved_outerr)) + { + buf_append_char (saved_outerr, '\n'); + buf_copy_lines (buf_to_net, saved_outerr, 'E'); + } + + /* Flush out any pending data. */ + buf_flush (buf_to_net, 1); + + /* Don't use vfork; we're not going to exec(). */ + command_pid = fork (); + if (command_pid < 0) + { + buf_output0 (buf_to_net, "E fork failed\n"); + print_error (errno); + goto error_exit; + } + if (command_pid == 0) + { + int exitstatus; + + /* Since we're in the child, and the parent is going to take + care of packaging up our error messages, we can clear this + flag. */ + error_use_protocol = 0; + + protocol = fd_buffer_initialize (protocol_pipe[1], 0, NULL, false, + protocol_memory_error); + + /* At this point we should no longer be using buf_to_net and + buf_from_net. Instead, everything should go through + protocol. */ + if (buf_to_net != NULL) + { + buf_free (buf_to_net); + buf_to_net = NULL; + } + if (buf_from_net != NULL) + { + buf_free (buf_from_net); + buf_from_net = NULL; + } + + /* These were originally set up to use outbuf_memory_error. + Since we're now in the child, we should use the simpler + protocol_memory_error function. */ + saved_output->memory_error = protocol_memory_error; + saved_outerr->memory_error = protocol_memory_error; + + if (dup2 (dev_null_fd, STDIN_FILENO) < 0) + error (1, errno, "can't set up pipes"); + if (dup2 (stdout_pipe[1], STDOUT_FILENO) < 0) + error (1, errno, "can't set up pipes"); + if (dup2 (stderr_pipe[1], STDERR_FILENO) < 0) + error (1, errno, "can't set up pipes"); + close (dev_null_fd); + close (stdout_pipe[0]); + close (stdout_pipe[1]); + close (stderr_pipe[0]); + close (stderr_pipe[1]); + close (protocol_pipe[0]); + close_on_exec (protocol_pipe[1]); +# ifdef SERVER_FLOWCONTROL + close_on_exec (flowcontrol_pipe[0]); + close (flowcontrol_pipe[1]); +# endif /* SERVER_FLOWCONTROL */ + + /* + * Set this in .bashrc if you want to give yourself time to attach + * to the subprocess with a debugger. + */ + if (getenv ("CVS_SERVER_SLEEP")) + { + int secs = atoi (getenv ("CVS_SERVER_SLEEP")); + TRACE (TRACE_DATA, "Sleeping CVS_SERVER_SLEEP (%d) seconds", secs); + sleep (secs); + } + else + TRACE (TRACE_DATA, "CVS_SERVER_SLEEP not set."); + + exitstatus = (*command) (argument_count, argument_vector); + + /* Output any partial lines. If the client doesn't support + "MT", we go ahead and just tack on a newline since the + protocol doesn't support anything better. */ + if (! buf_empty_p (saved_output)) + { + buf_output0 (protocol, supported_response ("MT") ? "MT text " : "M "); + buf_append_buffer (protocol, saved_output); + buf_output (protocol, "\n", 1); + buf_send_counted (protocol); + } + /* For now we just discard partial lines on stderr. I suspect + that CVS can't write such lines unless there is a bug. */ + + buf_free (protocol); + + /* Close the pipes explicitly in order to send an EOF to the parent, + * then wait for the parent to close the flow control pipe. This + * avoids a race condition where a child which dumped more than the + * high water mark into the pipes could complete its job and exit, + * leaving the parent process to attempt to write a stop byte to the + * closed flow control pipe, which earned the parent a SIGPIPE, which + * it normally only expects on the network pipe and that causes it to + * exit with an error message, rather than the SIGCHILD that it knows + * how to handle correctly. + */ + /* Let exit() close STDIN - it's from /dev/null anyhow. */ + fclose (stderr); + fclose (stdout); + close (protocol_pipe[1]); +# ifdef SERVER_FLOWCONTROL + { + char junk; + ssize_t status; + while ((status = read (flowcontrol_pipe[0], &junk, 1)) > 0 + || (status == -1 && errno == EAGAIN)); + } + /* FIXME: No point in printing an error message with error(), + * as STDERR is already closed, but perhaps this could be syslogged? + */ +# endif + + exit (exitstatus); + } + + /* OK, sit around getting all the input from the child. */ + { + struct buffer *stdoutbuf; + struct buffer *stderrbuf; + struct buffer *protocol_inbuf; + /* Number of file descriptors to check in select (). */ + int num_to_check; + int count_needed = 1; +# ifdef SERVER_FLOWCONTROL + int have_flowcontrolled = 0; +# endif /* SERVER_FLOWCONTROL */ + + FD_ZERO (&command_fds_to_drain.fds); + num_to_check = stdout_pipe[0]; + FD_SET (stdout_pipe[0], &command_fds_to_drain.fds); + num_to_check = MAX (num_to_check, stderr_pipe[0]); + FD_SET (stderr_pipe[0], &command_fds_to_drain.fds); + num_to_check = MAX (num_to_check, protocol_pipe[0]); + FD_SET (protocol_pipe[0], &command_fds_to_drain.fds); + num_to_check = MAX (num_to_check, STDOUT_FILENO); +# ifdef SUNOS_KLUDGE + max_command_fd = num_to_check; +# endif + /* + * File descriptors are numbered from 0, so num_to_check needs to + * be one larger than the largest descriptor. + */ + ++num_to_check; + if (num_to_check > FD_SETSIZE) + { + buf_output0 (buf_to_net, + "E internal error: FD_SETSIZE not big enough.\n\ +error \n"); + goto error_exit; + } + + stdoutbuf = fd_buffer_initialize (stdout_pipe[0], 0, NULL, true, + input_memory_error); + + stderrbuf = fd_buffer_initialize (stderr_pipe[0], 0, NULL, true, + input_memory_error); + + protocol_inbuf = fd_buffer_initialize (protocol_pipe[0], 0, NULL, true, + input_memory_error); + + set_nonblock (buf_to_net); + set_nonblock (stdoutbuf); + set_nonblock (stderrbuf); + set_nonblock (protocol_inbuf); + + if (close (stdout_pipe[1]) < 0) + { + buf_output0 (buf_to_net, "E close failed\n"); + print_error (errno); + goto error_exit; + } + stdout_pipe[1] = -1; + + if (close (stderr_pipe[1]) < 0) + { + buf_output0 (buf_to_net, "E close failed\n"); + print_error (errno); + goto error_exit; + } + stderr_pipe[1] = -1; + + if (close (protocol_pipe[1]) < 0) + { + buf_output0 (buf_to_net, "E close failed\n"); + print_error (errno); + goto error_exit; + } + protocol_pipe[1] = -1; + +# ifdef SERVER_FLOWCONTROL + if (close (flowcontrol_pipe[0]) < 0) + { + buf_output0 (buf_to_net, "E close failed\n"); + print_error (errno); + goto error_exit; + } + flowcontrol_pipe[0] = -1; +# endif /* SERVER_FLOWCONTROL */ + + if (close (dev_null_fd) < 0) + { + buf_output0 (buf_to_net, "E close failed\n"); + print_error (errno); + goto error_exit; + } + dev_null_fd = -1; + + while (stdout_pipe[0] >= 0 + || stderr_pipe[0] >= 0 + || protocol_pipe[0] >= 0 + || count_needed <= 0) + { + fd_set readfds; + fd_set writefds; + int numfds; + struct timeval *timeout_ptr; + struct timeval timeout; +# ifdef SERVER_FLOWCONTROL + int bufmemsize; + + /* + * See if we are swamping the remote client and filling our VM. + * Tell child to hold off if we do. + */ + bufmemsize = buf_count_mem (buf_to_net); + if (!have_flowcontrolled && (bufmemsize > SERVER_HI_WATER)) + { + if (write(flowcontrol_pipe[1], "S", 1) == 1) + have_flowcontrolled = 1; + } + else if (have_flowcontrolled && (bufmemsize < SERVER_LO_WATER)) + { + if (write(flowcontrol_pipe[1], "G", 1) == 1) + have_flowcontrolled = 0; + } +# endif /* SERVER_FLOWCONTROL */ + + FD_ZERO (&readfds); + FD_ZERO (&writefds); + + if (count_needed <= 0) + { + /* there is data pending which was read from the protocol pipe + * so don't block if we don't find any data + */ + timeout.tv_sec = 0; + timeout.tv_usec = 0; + timeout_ptr = &timeout; + } + else + { + /* block indefinately */ + timeout_ptr = NULL; + } + + if (! buf_empty_p (buf_to_net)) + FD_SET (STDOUT_FILENO, &writefds); + + if (stdout_pipe[0] >= 0) + { + FD_SET (stdout_pipe[0], &readfds); + } + if (stderr_pipe[0] >= 0) + { + FD_SET (stderr_pipe[0], &readfds); + } + if (protocol_pipe[0] >= 0) + { + FD_SET (protocol_pipe[0], &readfds); + } + + /* This process of selecting on the three pipes means that + we might not get output in the same order in which it + was written, thus producing the well-known + "out-of-order" bug. If the child process uses + cvs_output and cvs_outerr, it will send everything on + the protocol_pipe and avoid this problem, so the + solution is to use cvs_output and cvs_outerr in the + child process. */ + do { + /* This used to select on exceptions too, but as far + as I know there was never any reason to do that and + SCO doesn't let you select on exceptions on pipes. */ + numfds = select (num_to_check, &readfds, &writefds, + NULL, timeout_ptr); + if (numfds < 0 + && errno != EINTR) + { + buf_output0 (buf_to_net, "E select failed\n"); + print_error (errno); + goto error_exit; + } + } while (numfds < 0); + + if (numfds == 0) + { + FD_ZERO (&readfds); + FD_ZERO (&writefds); + } + + if (FD_ISSET (STDOUT_FILENO, &writefds)) + { + /* What should we do with errors? syslog() them? */ + buf_send_output (buf_to_net); + } + + if (protocol_pipe[0] >= 0 + && (FD_ISSET (protocol_pipe[0], &readfds))) + { + int status; + size_t count_read; + + status = buf_input_data (protocol_inbuf, &count_read); + + if (status == -1) + { + close (protocol_pipe[0]); + protocol_pipe[0] = -1; + } + else if (status > 0) + { + buf_output0 (buf_to_net, "E buf_input_data failed\n"); + print_error (status); + goto error_exit; + } + + /* + * We only call buf_copy_counted if we have read + * enough bytes to make it worthwhile. This saves us + * from continually recounting the amount of data we + * have. + */ + count_needed -= count_read; + } + /* this is still part of the protocol pipe procedure, but it is + * outside the above conditional so that unprocessed data can be + * left in the buffer and stderr/stdout can be read when a flush + * signal is received and control can return here without passing + * through the select code and maybe blocking + */ + while (count_needed <= 0) + { + int special = 0; + + count_needed = buf_copy_counted (buf_to_net, + protocol_inbuf, + &special); + + /* What should we do with errors? syslog() them? */ + buf_send_output (buf_to_net); + + /* If SPECIAL got set to <0, it means that the child + * wants us to flush the pipe & maybe stderr or stdout. + * + * After that we break to read stderr & stdout again before + * going back to the protocol pipe + * + * Upon breaking, count_needed = 0, so the next pass will only + * perform a non-blocking select before returning here to finish + * processing data we already read from the protocol buffer + */ + if (special == -1) + { + cvs_flushout(); + break; + } + if (special == -2) + { + /* If the client supports the 'F' command, we send it. */ + if (supported_response ("F")) + { + buf_append_char (buf_to_net, 'F'); + buf_append_char (buf_to_net, '\n'); + } + cvs_flusherr (); + break; + } + } + + if (stdout_pipe[0] >= 0 + && (FD_ISSET (stdout_pipe[0], &readfds))) + { + int status; + + status = buf_input_data (stdoutbuf, NULL); + + buf_copy_lines (buf_to_net, stdoutbuf, 'M'); + + if (status == -1) + { + close (stdout_pipe[0]); + stdout_pipe[0] = -1; + } + else if (status > 0) + { + buf_output0 (buf_to_net, "E buf_input_data failed\n"); + print_error (status); + goto error_exit; + } + + /* What should we do with errors? syslog() them? */ + buf_send_output (buf_to_net); + } + + if (stderr_pipe[0] >= 0 + && (FD_ISSET (stderr_pipe[0], &readfds))) + { + int status; + + status = buf_input_data (stderrbuf, NULL); + + buf_copy_lines (buf_to_net, stderrbuf, 'E'); + + if (status == -1) + { + close (stderr_pipe[0]); + stderr_pipe[0] = -1; + } + else if (status > 0) + { + buf_output0 (buf_to_net, "E buf_input_data failed\n"); + print_error (status); + goto error_exit; + } + + /* What should we do with errors? syslog() them? */ + buf_send_output (buf_to_net); + } + } + + /* + * OK, we've gotten EOF on all the pipes. If there is + * anything left on stdoutbuf or stderrbuf (this could only + * happen if there was no trailing newline), send it over. + */ + if (! buf_empty_p (stdoutbuf)) + { + buf_append_char (stdoutbuf, '\n'); + buf_copy_lines (buf_to_net, stdoutbuf, 'M'); + } + if (! buf_empty_p (stderrbuf)) + { + buf_append_char (stderrbuf, '\n'); + buf_copy_lines (buf_to_net, stderrbuf, 'E'); + } + if (! buf_empty_p (protocol_inbuf)) + buf_output0 (buf_to_net, + "E Protocol error: uncounted data discarded\n"); + +# ifdef SERVER_FLOWCONTROL + close (flowcontrol_pipe[1]); + flowcontrol_pipe[1] = -1; +# endif /* SERVER_FLOWCONTROL */ + + errs = 0; + + while (command_pid > 0) + { + int status; + pid_t waited_pid; + waited_pid = waitpid (command_pid, &status, 0); + if (waited_pid < 0) + { + /* + * Intentionally ignoring EINTR. Other errors + * "can't happen". + */ + continue; + } + + if (WIFEXITED (status)) + errs += WEXITSTATUS (status); + else + { + int sig = WTERMSIG (status); + char buf[50]; + /* + * This is really evil, because signals might be numbered + * differently on the two systems. We should be using + * signal names (either of the "Terminated" or the "SIGTERM" + * variety). But cvs doesn't currently use libiberty...we + * could roll our own.... FIXME. + */ + buf_output0 (buf_to_net, "E Terminated with fatal signal "); + sprintf (buf, "%d\n", sig); + buf_output0 (buf_to_net, buf); + + /* Test for a core dump. */ + if (WCOREDUMP (status)) + { + buf_output0 (buf_to_net, "E Core dumped; preserving "); + buf_output0 (buf_to_net, orig_server_temp_dir); + buf_output0 (buf_to_net, " on server.\n\ +E CVS locks may need cleaning up.\n"); + dont_delete_temp = 1; + } + ++errs; + } + if (waited_pid == command_pid) + command_pid = -1; + } + + /* + * OK, we've waited for the child. By now all CVS locks are free + * and it's OK to block on the network. + */ + set_block (buf_to_net); + buf_flush (buf_to_net, 1); + buf_shutdown (protocol_inbuf); + buf_free (protocol_inbuf); + protocol_inbuf = NULL; + buf_shutdown (stderrbuf); + buf_free (stderrbuf); + stderrbuf = NULL; + buf_shutdown (stdoutbuf); + buf_free (stdoutbuf); + stdoutbuf = NULL; + } + + if (errs) + /* We will have printed an error message already. */ + buf_output0 (buf_to_net, "error \n"); + else + buf_output0 (buf_to_net, "ok\n"); + goto free_args_and_return; + + error_exit: + if (command_pid > 0) + kill (command_pid, SIGTERM); + + while (command_pid > 0) + { + pid_t waited_pid; + waited_pid = waitpid (command_pid, NULL, 0); + if (waited_pid < 0 && errno == EINTR) + continue; + if (waited_pid == command_pid) + command_pid = -1; + } + + close (dev_null_fd); + close (protocol_pipe[0]); + close (protocol_pipe[1]); + close (stderr_pipe[0]); + close (stderr_pipe[1]); + close (stdout_pipe[0]); + close (stdout_pipe[1]); +# ifdef SERVER_FLOWCONTROL + close (flowcontrol_pipe[0]); + close (flowcontrol_pipe[1]); +# endif /* SERVER_FLOWCONTROL */ + + free_args_and_return: + /* Now free the arguments. */ + { + /* argument_vector[0] is a dummy argument, we don't mess with it. */ + char **cp; + for (cp = argument_vector + 1; + cp < argument_vector + argument_count; + ++cp) + free (*cp); + + argument_count = 1; + } + + /* Flush out any data not yet sent. */ + set_block (buf_to_net); + buf_flush (buf_to_net, 1); + + return; +} + + + +# ifdef SERVER_FLOWCONTROL +/* + * Called by the child at convenient points in the server's execution for + * the server child to block.. ie: when it has no locks active. + */ +void +server_pause_check(void) +{ + int paused = 0; + char buf[1]; + + while (read (flowcontrol_pipe[0], buf, 1) == 1) + { + if (*buf == 'S') /* Stop */ + paused = 1; + else if (*buf == 'G') /* Go */ + paused = 0; + else + return; /* ??? */ + } + while (paused) { + int numfds, numtocheck; + fd_set fds; + + FD_ZERO (&fds); + FD_SET (flowcontrol_pipe[0], &fds); + numtocheck = flowcontrol_pipe[0] + 1; + + do { + numfds = select (numtocheck, &fds, NULL, NULL, NULL); + if (numfds < 0 + && errno != EINTR) + { + buf_output0 (buf_to_net, "E select failed\n"); + print_error (errno); + return; + } + } while (numfds < 0); + + if (FD_ISSET (flowcontrol_pipe[0], &fds)) + { + int got; + + while ((got = read (flowcontrol_pipe[0], buf, 1)) == 1) + { + if (*buf == 'S') /* Stop */ + paused = 1; + else if (*buf == 'G') /* Go */ + paused = 0; + else + return; /* ??? */ + } + + /* This assumes that we are using BSD or POSIX nonblocking + I/O. System V nonblocking I/O returns zero if there is + nothing to read. */ + if (got == 0) + error (1, 0, "flow control EOF"); + if (got < 0 && ! blocking_error (errno)) + { + error (1, errno, "flow control read failed"); + } + } + } +} +# endif /* SERVER_FLOWCONTROL */ + + + +/* This variable commented in server.h. */ +char *server_dir = NULL; + + + +static void +output_dir (const char *update_dir, const char *repository) +{ + /* Set up SHORT_REPOS. */ + const char *short_repos = Short_Repository (repository); + + /* Send the update_dir/repos. */ + if (server_dir != NULL) + { + buf_output0 (protocol, server_dir); + buf_output0 (protocol, "/"); + } + if (update_dir[0] == '\0') + buf_output0 (protocol, "."); + else + buf_output0 (protocol, update_dir); + buf_output0 (protocol, "/\n"); + if (short_repos[0] == '\0') + buf_output0 (protocol, "."); + else + buf_output0 (protocol, short_repos); + buf_output0 (protocol, "/"); +} + + + +/* + * Entries line that we are squirreling away to send to the client when + * we are ready. + */ +static char *entries_line; + +/* + * File which has been Scratch_File'd, we are squirreling away that fact + * to inform the client when we are ready. + */ +static char *scratched_file; + +/* + * The scratched_file will need to be removed as well as having its entry + * removed. + */ +static int kill_scratched_file; + + + +void +server_register (const char *name, const char *version, const char *timestamp, + const char *options, const char *tag, const char *date, + const char *conflict) +{ + int len; + + if (options == NULL) + options = ""; + + TRACE (TRACE_FUNCTION, "server_register(%s, %s, %s, %s, %s, %s, %s)", + name, version, timestamp ? timestamp : "", options, + tag ? tag : "", date ? date : "", + conflict ? conflict : ""); + + if (entries_line != NULL) + { + /* + * If CVS decides to Register it more than once (which happens + * on "cvs update foo/foo.c" where foo and foo.c are already + * checked out), use the last of the entries lines Register'd. + */ + free (entries_line); + } + + /* + * I have reports of Scratch_Entry and Register both happening, in + * two different cases. Using the last one which happens is almost + * surely correct; I haven't tracked down why they both happen (or + * even verified that they are for the same file). + */ + if (scratched_file != NULL) + { + free (scratched_file); + scratched_file = NULL; + } + + len = (strlen (name) + strlen (version) + strlen (options) + 80); + if (tag) + len += strlen (tag); + if (date) + len += strlen (date); + + entries_line = xmalloc (len); + sprintf (entries_line, "/%s/%s/", name, version); + if (conflict != NULL) + { + strcat (entries_line, "+="); + } + strcat (entries_line, "/"); + strcat (entries_line, options); + strcat (entries_line, "/"); + if (tag != NULL) + { + strcat (entries_line, "T"); + strcat (entries_line, tag); + } + else if (date != NULL) + { + strcat (entries_line, "D"); + strcat (entries_line, date); + } +} + + + +void +server_scratch (const char *fname) +{ + /* + * I have reports of Scratch_Entry and Register both happening, in + * two different cases. Using the last one which happens is almost + * surely correct; I haven't tracked down why they both happen (or + * even verified that they are for the same file). + * + * Don't know if this is what whoever wrote the above comment was + * talking about, but this can happen in the case where a join + * removes a file - the call to Register puts the '-vers' into the + * Entries file after the file is removed + */ + if (entries_line != NULL) + { + free (entries_line); + entries_line = NULL; + } + + if (scratched_file != NULL) + { + buf_output0 (protocol, + "E CVS server internal error: duplicate Scratch_Entry\n"); + buf_send_counted (protocol); + return; + } + scratched_file = xstrdup (fname); + kill_scratched_file = 1; +} + + + +void +server_scratch_entry_only (void) +{ + kill_scratched_file = 0; +} + + + +/* Print a new entries line, from a previous server_register. */ +static void +new_entries_line (void) +{ + if (entries_line) + { + buf_output0 (protocol, entries_line); + buf_output (protocol, "\n", 1); + } + else + /* Return the error message as the Entries line. */ + buf_output0 (protocol, + "CVS server internal error: Register missing\n"); + free (entries_line); + entries_line = NULL; +} + + + +static void +serve_ci (char *arg) +{ + do_cvs_command ("commit", commit); +} + + + +static void +checked_in_response (const char *file, const char *update_dir, + const char *repository) +{ + if (supported_response ("Mode")) + { + struct stat sb; + char *mode_string; + + if (stat (file, &sb) < 0) + { + /* Not clear to me why the file would fail to exist, but it + was happening somewhere in the testsuite. */ + if (!existence_error (errno)) + error (0, errno, "cannot stat %s", file); + } + else + { + buf_output0 (protocol, "Mode "); + mode_string = mode_to_string (sb.st_mode); + buf_output0 (protocol, mode_string); + buf_output0 (protocol, "\n"); + free (mode_string); + } + } + + buf_output0 (protocol, "Checked-in "); + output_dir (update_dir, repository); + buf_output0 (protocol, file); + buf_output (protocol, "\n", 1); + new_entries_line (); +} + + + +void +server_checked_in (const char *file, const char *update_dir, + const char *repository) +{ + if (noexec) + return; + if (scratched_file != NULL && entries_line == NULL) + { + /* + * This happens if we are now doing a "cvs remove" after a previous + * "cvs add" (without a "cvs ci" in between). + */ + buf_output0 (protocol, "Remove-entry "); + output_dir (update_dir, repository); + buf_output0 (protocol, file); + buf_output (protocol, "\n", 1); + free (scratched_file); + scratched_file = NULL; + } + else + { + checked_in_response (file, update_dir, repository); + } + buf_send_counted (protocol); +} + + + +void +server_update_entries (const char *file, const char *update_dir, + const char *repository, + enum server_updated_arg4 updated) +{ + if (noexec) + return; + if (updated == SERVER_UPDATED) + checked_in_response (file, update_dir, repository); + else + { + if (!supported_response ("New-entry")) + return; + buf_output0 (protocol, "New-entry "); + output_dir (update_dir, repository); + buf_output0 (protocol, file); + buf_output (protocol, "\n", 1); + new_entries_line (); + } + + buf_send_counted (protocol); +} + + + +static void +serve_update (char *arg) +{ + do_cvs_command ("update", update); +} + + + +static void +serve_diff (char *arg) +{ + do_cvs_command ("diff", diff); +} + + + +static void +serve_log (char *arg) +{ + do_cvs_command ("log", cvslog); +} + + + +static void +serve_rlog (char *arg) +{ + do_cvs_command ("rlog", cvslog); +} + + + +static void +serve_ls (char *arg) +{ + do_cvs_command ("ls", ls); +} + + + +static void +serve_rls (char *arg) +{ + do_cvs_command ("rls", ls); +} + + + +static void +serve_add (char *arg) +{ + do_cvs_command ("add", add); +} + + + +static void +serve_remove (char *arg) +{ + do_cvs_command ("remove", cvsremove); +} + + + +static void +serve_status (char *arg) +{ + do_cvs_command ("status", cvsstatus); +} + + + +static void +serve_rdiff (char *arg) +{ + do_cvs_command ("rdiff", patch); +} + + + +static void +serve_tag (char *arg) +{ + do_cvs_command ("tag", cvstag); +} + + + +static void +serve_rtag (char *arg) +{ + do_cvs_command ("rtag", cvstag); +} + + + +static void +serve_import (char *arg) +{ + do_cvs_command ("import", import); +} + + + +static void +serve_admin (char *arg) +{ + do_cvs_command ("admin", admin); +} + + + +static void +serve_history (char *arg) +{ + do_cvs_command ("history", history); +} + + + +static void +serve_release (char *arg) +{ + do_cvs_command ("release", release); +} + + + +static void +serve_watch_on (char *arg) +{ + do_cvs_command ("watch", watch_on); +} + + + +static void +serve_watch_off (char *arg) +{ + do_cvs_command ("watch", watch_off); +} + + + +static void +serve_watch_add (char *arg) +{ + do_cvs_command ("watch", watch_add); +} + + + +static void +serve_watch_remove (char *arg) +{ + do_cvs_command ("watch", watch_remove); +} + + + +static void +serve_watchers (char *arg) +{ + do_cvs_command ("watchers", watchers); +} + + + +static void +serve_editors (char *arg) +{ + do_cvs_command ("editors", editors); +} + + + +static void +serve_edit (char *arg) +{ + do_cvs_command ("edit", edit); +} + + + +# ifdef PROXY_SUPPORT +/* We need to handle some of this before reprocessing since it is defined to + * send a response and print errors before a Root request is received. + */ +# endif /* PROXY_SUPPORT */ +static void +serve_noop (char *arg) +{ + /* Errors could be encountered in the first or second passes, so always + * send them to the client. + */ + bool pe = print_pending_error(); + +# ifdef PROXY_SUPPORT + /* The portions below need not be handled until reprocessing anyhow since + * there should be no entries or notifications prior to that. */ + if (!proxy_log) +# endif /* PROXY_SUPPORT */ + { + server_write_entries (); + if (!pe) + (void) server_notify (); + } + + if (!pe +# ifdef PROXY_SUPPORT + /* "ok" only goes across in the first pass. */ + && !reprocessing +# endif /* PROXY_SUPPORT */ + ) + buf_output0 (buf_to_net, "ok\n"); + buf_flush (buf_to_net, 1); +} + + + +static void +serve_version (char *arg) +{ + do_cvs_command ("version", version); +} + + + +static void +serve_init (char *arg) +{ + cvsroot_t *saved_parsed_root; + + if (!ISABSOLUTE (arg)) + { + if (alloc_pending (80 + strlen (arg))) + sprintf (pending_error_text, + "E init %s must be an absolute pathname", arg); + } +# ifdef AUTH_SERVER_SUPPORT + else if (Pserver_Repos != NULL) + { + if (strcmp (Pserver_Repos, arg) != 0) + { + if (alloc_pending (80 + strlen (Pserver_Repos) + strlen (arg))) + /* The explicitness is to aid people who are writing clients. + I don't see how this information could help an + attacker. */ + sprintf (pending_error_text, "\ +E Protocol error: init says \"%s\" but pserver says \"%s\"", + arg, Pserver_Repos); + } + } +# endif + + if (print_pending_error ()) + return; + + saved_parsed_root = current_parsed_root; + current_parsed_root = local_cvsroot (arg); + + do_cvs_command ("init", init); + + /* Do not free CURRENT_PARSED_ROOT since it is still in the cache. */ + current_parsed_root = saved_parsed_root; +} + + + +static void +serve_annotate (char *arg) +{ + do_cvs_command ("annotate", annotate); +} + + + +static void +serve_rannotate (char *arg) +{ + do_cvs_command ("rannotate", annotate); +} + + + +static void +serve_co (char *arg) +{ + if (print_pending_error ()) + return; + +# ifdef PROXY_SUPPORT + /* If we are not a secondary server, the write proxy log will already have + * been processed. + */ + if (isProxyServer ()) + { + if (reprocessing) + reprocessing = false; + else if (/* The proxy log may be closed if the client sent a + * `Command-prep' request. + */ + proxy_log) + { + /* Set up the log for reprocessing. */ + rewind_buf_from_net (); + /* And return to the main loop in server(), where we will now find + * the logged secondary data and reread it. + */ + return; + } + } +# endif /* PROXY_SUPPORT */ + + /* Compensate for server_export()'s setting of cvs_cmd_name. + * + * [It probably doesn't matter if do_cvs_command() gets "export" + * or "checkout", but we ought to be accurate where possible.] + */ + do_cvs_command (!strcmp (cvs_cmd_name, "export") ? "export" : "checkout", + checkout); +} + + + +static void +serve_export (char *arg) +{ + /* Tell checkout() to behave like export not checkout. */ + cvs_cmd_name = "export"; + serve_co (arg); +} + + + +void +server_copy_file (const char *file, const char *update_dir, + const char *repository, const char *newfile) +{ + /* At least for now, our practice is to have the server enforce + noexec for the repository and the client enforce it for the + working directory. This might want more thought, and/or + documentation in cvsclient.texi (other responses do it + differently). */ + + if (!supported_response ("Copy-file")) + return; + buf_output0 (protocol, "Copy-file "); + output_dir (update_dir, repository); + buf_output0 (protocol, file); + buf_output0 (protocol, "\n"); + buf_output0 (protocol, newfile); + buf_output0 (protocol, "\n"); +} + + + +/* See server.h for description. */ +void +server_modtime (struct file_info *finfo, Vers_TS *vers_ts) +{ + char date[MAXDATELEN]; + char outdate[MAXDATELEN]; + + assert (vers_ts->vn_rcs != NULL); + + if (!supported_response ("Mod-time")) + return; + + if (RCS_getrevtime (finfo->rcs, vers_ts->vn_rcs, date, 0) == (time_t) -1) + /* FIXME? should we be printing some kind of warning? For one + thing I'm not 100% sure whether this happens in non-error + circumstances. */ + return; + date_to_internet (outdate, date); + buf_output0 (protocol, "Mod-time "); + buf_output0 (protocol, outdate); + buf_output0 (protocol, "\n"); +} + + + +/* See server.h for description. */ +void +server_updated ( + struct file_info *finfo, + Vers_TS *vers, + enum server_updated_arg4 updated, + mode_t mode, + unsigned char *checksum, + struct buffer *filebuf) +{ + if (noexec) + { + /* Hmm, maybe if we did the same thing for entries_file, we + could get rid of the kludges in server_register and + server_scratch which refrain from warning if both + Scratch_Entry and Register get called. Maybe. */ + if (scratched_file) + { + free (scratched_file); + scratched_file = NULL; + } + buf_send_counted (protocol); + return; + } + + if (entries_line != NULL && scratched_file == NULL) + { + FILE *f; + struct buffer_data *list, *last; + unsigned long size; + char size_text[80]; + + /* The contents of the file will be in one of filebuf, + list/last, or here. */ + unsigned char *file; + size_t file_allocated; + size_t file_used; + + if (filebuf != NULL) + { + size = buf_length (filebuf); + if (mode == (mode_t) -1) + error (1, 0, "\ +CVS server internal error: no mode in server_updated"); + } + else + { + struct stat sb; + + if (stat (finfo->file, &sb) < 0) + { + if (existence_error (errno)) + { + /* If we have a sticky tag for a branch on which + the file is dead, and cvs update the directory, + it gets a T_CHECKOUT but no file. So in this + case just forget the whole thing. */ + free (entries_line); + entries_line = NULL; + goto done; + } + error (1, errno, "reading %s", finfo->fullname); + } + size = sb.st_size; + if (mode == (mode_t) -1) + { + /* FIXME: When we check out files the umask of the + server (set in .bashrc if rsh is in use) affects + what mode we send, and it shouldn't. */ + mode = sb.st_mode; + } + } + + if (checksum != NULL) + { + static int checksum_supported = -1; + + if (checksum_supported == -1) + { + checksum_supported = supported_response ("Checksum"); + } + + if (checksum_supported) + { + int i; + char buf[3]; + + buf_output0 (protocol, "Checksum "); + for (i = 0; i < 16; i++) + { + sprintf (buf, "%02x", (unsigned int) checksum[i]); + buf_output0 (protocol, buf); + } + buf_append_char (protocol, '\n'); + } + } + + if (updated == SERVER_UPDATED) + { + Node *node; + Entnode *entnode; + + if (!(supported_response ("Created") + && supported_response ("Update-existing"))) + buf_output0 (protocol, "Updated "); + else + { + assert (vers != NULL); + if (vers->ts_user == NULL) + buf_output0 (protocol, "Created "); + else + buf_output0 (protocol, "Update-existing "); + } + + /* Now munge the entries to say that the file is unmodified, + in case we end up processing it again (e.g. modules3-6 + in the testsuite). */ + node = findnode_fn (finfo->entries, finfo->file); + entnode = node->data; + free (entnode->timestamp); + entnode->timestamp = xstrdup ("="); + } + else if (updated == SERVER_MERGED) + buf_output0 (protocol, "Merged "); + else if (updated == SERVER_PATCHED) + buf_output0 (protocol, "Patched "); + else if (updated == SERVER_RCS_DIFF) + buf_output0 (protocol, "Rcs-diff "); + else + abort (); + output_dir (finfo->update_dir, finfo->repository); + buf_output0 (protocol, finfo->file); + buf_output (protocol, "\n", 1); + + new_entries_line (); + + { + char *mode_string; + + mode_string = mode_to_string (mode); + buf_output0 (protocol, mode_string); + buf_output0 (protocol, "\n"); + free (mode_string); + } + + list = last = NULL; + + file = NULL; + file_allocated = 0; + file_used = 0; + + if (size > 0) + { + /* Throughout this section we use binary mode to read the + file we are sending. The client handles any line ending + translation if necessary. */ + + if (file_gzip_level + /* + * For really tiny files, the gzip process startup + * time will outweigh the compression savings. This + * might be computable somehow; using 100 here is just + * a first approximation. + */ + && size > 100) + { + /* Basing this routine on read_and_gzip is not a + high-performance approach. But it seems easier + to code than the alternative (and less + vulnerable to subtle bugs). Given that this feature + is mainly for compatibility, that is the better + tradeoff. */ + + int fd; + + /* Callers must avoid passing us a buffer if + file_gzip_level is set. We could handle this case, + but it's not worth it since this case never arises + with a current client and server. */ + if (filebuf != NULL) + error (1, 0, "\ +CVS server internal error: unhandled case in server_updated"); + + fd = CVS_OPEN (finfo->file, O_RDONLY | OPEN_BINARY, 0); + if (fd < 0) + error (1, errno, "reading %s", finfo->fullname); + if (read_and_gzip (fd, finfo->fullname, &file, + &file_allocated, &file_used, + file_gzip_level)) + error (1, 0, "aborting due to compression error"); + size = file_used; + if (close (fd) < 0) + error (1, errno, "reading %s", finfo->fullname); + /* Prepending length with "z" is flag for using gzip here. */ + buf_output0 (protocol, "z"); + } + else if (filebuf == NULL) + { + long status; + + f = CVS_FOPEN (finfo->file, "rb"); + if (f == NULL) + error (1, errno, "reading %s", finfo->fullname); + status = buf_read_file (f, size, &list, &last); + if (status == -2) + (*protocol->memory_error) (protocol); + else if (status != 0) + error (1, ferror (f) ? errno : 0, "reading %s", + finfo->fullname); + if (fclose (f) == EOF) + error (1, errno, "reading %s", finfo->fullname); + } + } + + sprintf (size_text, "%lu\n", size); + buf_output0 (protocol, size_text); + + if (file != NULL) + { + buf_output (protocol, (char *) file, file_used); + free (file); + file = NULL; + } + else if (filebuf == NULL) + buf_append_data (protocol, list, last); + else + buf_append_buffer (protocol, filebuf); + /* Note we only send a newline here if the file ended with one. */ + + /* + * Avoid using up too much disk space for temporary files. + * A file which does not exist indicates that the file is up-to-date, + * which is now the case. If this is SERVER_MERGED, the file is + * not up-to-date, and we indicate that by leaving the file there. + * I'm thinking of cases like "cvs update foo/foo.c foo". + */ + if ((updated == SERVER_UPDATED + || updated == SERVER_PATCHED + || updated == SERVER_RCS_DIFF) + && filebuf == NULL + /* But if we are joining, we'll need the file when we call + join_file. */ + && !joining ()) + { + if (CVS_UNLINK (finfo->file) < 0) + error (0, errno, "cannot remove temp file for %s", + finfo->fullname); + } + } + else if (scratched_file != NULL && entries_line == NULL) + { + if (strcmp (scratched_file, finfo->file) != 0) + error (1, 0, + "CVS server internal error: `%s' vs. `%s' scratched", + scratched_file, + finfo->file); + free (scratched_file); + scratched_file = NULL; + + if (kill_scratched_file) + buf_output0 (protocol, "Removed "); + else + buf_output0 (protocol, "Remove-entry "); + output_dir (finfo->update_dir, finfo->repository); + buf_output0 (protocol, finfo->file); + buf_output (protocol, "\n", 1); + /* keep the vers structure up to date in case we do a join + * - if there isn't a file, it can't very well have a version number, + * can it? + * + * we do it here on the assumption that since we just told the client + * to remove the file/entry, it will, and we want to remember that. + * If it fails, that's the client's problem, not ours + */ + if (vers && vers->vn_user != NULL) + { + free (vers->vn_user); + vers->vn_user = NULL; + } + if (vers && vers->ts_user != NULL) + { + free (vers->ts_user); + vers->ts_user = NULL; + } + } + else if (scratched_file == NULL && entries_line == NULL) + { + /* + * This can happen with death support if we were processing + * a dead file in a checkout. + */ + } + else + error (1, 0, + "CVS server internal error: Register *and* Scratch_Entry.\n"); + buf_send_counted (protocol); + done:; +} + + + +/* Return whether we should send patches in RCS format. */ +int +server_use_rcs_diff (void) +{ + return supported_response ("Rcs-diff"); +} + + + +void +server_set_entstat (const char *update_dir, const char *repository) +{ + static int set_static_supported = -1; + if (set_static_supported == -1) + set_static_supported = supported_response ("Set-static-directory"); + if (!set_static_supported) return; + + buf_output0 (protocol, "Set-static-directory "); + output_dir (update_dir, repository); + buf_output0 (protocol, "\n"); + buf_send_counted (protocol); +} + + + +void +server_clear_entstat (const char *update_dir, const char *repository) +{ + static int clear_static_supported = -1; + if (clear_static_supported == -1) + clear_static_supported = supported_response ("Clear-static-directory"); + if (!clear_static_supported) return; + + if (noexec) + return; + + buf_output0 (protocol, "Clear-static-directory "); + output_dir (update_dir, repository); + buf_output0 (protocol, "\n"); + buf_send_counted (protocol); +} + + + +void +server_set_sticky (const char *update_dir, const char *repository, + const char *tag, const char *date, int nonbranch) +{ + static int set_sticky_supported = -1; + + assert (update_dir != NULL); + + if (set_sticky_supported == -1) + set_sticky_supported = supported_response ("Set-sticky"); + if (!set_sticky_supported) return; + + if (noexec) + return; + + if (tag == NULL && date == NULL) + { + buf_output0 (protocol, "Clear-sticky "); + output_dir (update_dir, repository); + buf_output0 (protocol, "\n"); + } + else + { + buf_output0 (protocol, "Set-sticky "); + output_dir (update_dir, repository); + buf_output0 (protocol, "\n"); + if (tag != NULL) + { + if (nonbranch) + buf_output0 (protocol, "N"); + else + buf_output0 (protocol, "T"); + buf_output0 (protocol, tag); + } + else + { + buf_output0 (protocol, "D"); + buf_output0 (protocol, date); + } + buf_output0 (protocol, "\n"); + } + buf_send_counted (protocol); +} + + + +void +server_edit_file (struct file_info *finfo) +{ + buf_output (protocol, "Edit-file ", 10); + output_dir (finfo->update_dir, finfo->repository); + buf_output0 (protocol, finfo->file); + buf_output (protocol, "\n", 1); + buf_send_counted (protocol); +} + + + +struct template_proc_data +{ + const char *update_dir; + const char *repository; +}; + +static int +template_proc (const char *repository, const char *template, void *closure) +{ + FILE *fp; + char buf[1024]; + size_t n; + struct stat sb; + struct template_proc_data *data = (struct template_proc_data *)closure; + + if (!supported_response ("Template")) + /* Might want to warn the user that the rcsinfo feature won't work. */ + return 0; + buf_output0 (protocol, "Template "); + output_dir (data->update_dir, data->repository); + buf_output0 (protocol, "\n"); + + fp = CVS_FOPEN (template, "rb"); + if (fp == NULL) + { + error (0, errno, "Couldn't open rcsinfo template file %s", template); + return 1; + } + if (fstat (fileno (fp), &sb) < 0) + { + error (0, errno, "cannot stat rcsinfo template file %s", template); + return 1; + } + sprintf (buf, "%ld\n", (long) sb.st_size); + buf_output0 (protocol, buf); + while (!feof (fp)) + { + n = fread (buf, 1, sizeof buf, fp); + buf_output (protocol, buf, n); + if (ferror (fp)) + { + error (0, errno, "cannot read rcsinfo template file %s", template); + (void) fclose (fp); + return 1; + } + } + buf_send_counted (protocol); + if (fclose (fp) < 0) + error (0, errno, "cannot close rcsinfo template file %s", template); + return 0; +} + + + +void +server_clear_template (const char *update_dir, const char *repository) +{ + assert (update_dir != NULL); + + if (noexec) + return; + + if (!supported_response ("Clear-template") && + !supported_response ("Template")) + /* Might want to warn the user that the rcsinfo feature won't work. */ + return; + + if (supported_response ("Clear-template")) + { + buf_output0 (protocol, "Clear-template "); + output_dir (update_dir, repository); + buf_output0 (protocol, "\n"); + buf_send_counted (protocol); + } + else + { + buf_output0 (protocol, "Template "); + output_dir (update_dir, repository); + buf_output0 (protocol, "\n"); + buf_output0 (protocol, "0\n"); + buf_send_counted (protocol); + } +} + + + +void +server_template (const char *update_dir, const char *repository) +{ + struct template_proc_data data; + data.update_dir = update_dir; + data.repository = repository; + (void) Parse_Info (CVSROOTADM_RCSINFO, repository, template_proc, + PIOPT_ALL, &data); +} + + + +static void +serve_gzip_contents (char *arg) +{ + int level; + bool forced = false; + +# ifdef PROXY_SUPPORT + assert (!proxy_log); +# endif /* PROXY_SUPPORT */ + + level = atoi (arg); + if (level == 0) + level = 6; + + if (config && level < config->MinCompressionLevel) + { + level = config->MinCompressionLevel; + forced = true; + } + if (config && level > config->MaxCompressionLevel) + { + level = config->MaxCompressionLevel; + forced = true; + } + + if (forced && !quiet + && alloc_pending_warning (120 + strlen (program_name))) + sprintf (pending_warning_text, +"E %s server: Forcing compression level %d (allowed: %d <= z <= %d).", + program_name, level, config->MinCompressionLevel, + config->MaxCompressionLevel); + + gzip_level = file_gzip_level = level; +} + + + +static void +serve_gzip_stream (char *arg) +{ + int level; + bool forced = false; + + level = atoi (arg); + + if (config && level < config->MinCompressionLevel) + { + level = config->MinCompressionLevel; + forced = true; + } + if (config && level > config->MaxCompressionLevel) + { + level = config->MaxCompressionLevel; + forced = true; + } + + if (forced && !quiet + && alloc_pending_warning (120 + strlen (program_name))) + sprintf (pending_warning_text, +"E %s server: Forcing compression level %d (allowed: %d <= z <= %d).", + program_name, level, config->MinCompressionLevel, + config->MaxCompressionLevel); + + gzip_level = level; + + /* All further communication with the client will be compressed. + * + * The deflate buffers need to be initialized even for compression level + * 0, or the client will no longer be able to understand us. At + * compression level 0, the correct compression headers will be created and + * sent, but data will thereafter simply be copied to the network buffers. + */ + + /* This needs to be processed in both passes so that we may continue to + * understand client requests on both the socket and from the log. + */ + buf_from_net = compress_buffer_initialize (buf_from_net, 1, + 0 /* Not used. */, + buf_from_net->memory_error); + + /* This needs to be skipped in subsequent passes to avoid compressing data + * to the client twice. + */ +# ifdef PROXY_SUPPORT + if (reprocessing) return; +# endif /* PROXY_SUPPORT */ + buf_to_net = compress_buffer_initialize (buf_to_net, 0, level, + buf_to_net->memory_error); +} + + + +/* Tell the client about RCS options set in CVSROOT/cvswrappers. */ +static void +serve_wrapper_sendme_rcs_options (char *arg) +{ + /* Actually, this is kind of sdrawkcab-ssa: the client wants + * verbatim lines from a cvswrappers file, but the server has + * already parsed the cvswrappers file into the wrap_list struct. + * Therefore, the server loops over wrap_list, unparsing each + * entry before sending it. + */ + char *wrapper_line = NULL; + +# ifdef PROXY_SUPPORT + if (reprocessing) return; +# endif /* PROXY_SUPPORT */ + + wrap_setup (); + + for (wrap_unparse_rcs_options (&wrapper_line, 1); + wrapper_line; + wrap_unparse_rcs_options (&wrapper_line, 0)) + { + buf_output0 (buf_to_net, "Wrapper-rcsOption "); + buf_output0 (buf_to_net, wrapper_line); + buf_output0 (buf_to_net, "\012");; + free (wrapper_line); + } + + buf_output0 (buf_to_net, "ok\012"); + + /* The client is waiting for us, so we better send the data now. */ + buf_flush (buf_to_net, 1); +} + + + +static void +serve_ignore (char *arg) +{ + /* + * Just ignore this command. This is used to support the + * update-patches command, which is not a real command, but a signal + * to the client that update will accept the -u argument. + */ +# ifdef PROXY_SUPPORT + assert (!proxy_log); +# endif /* PROXY_SUPPORT */ +} + + + +static int +expand_proc (int argc, char **argv, char *where, char *mwhere, char *mfile, int shorten, int local_specified, char *omodule, char *msg) +{ + int i; + char *dir = argv[0]; + + /* If mwhere has been specified, the thing we're expanding is a + module -- just return its name so the client will ask for the + right thing later. If it is an alias or a real directory, + mwhere will not be set, so send out the appropriate + expansion. */ + + if (mwhere != NULL) + { + buf_output0 (buf_to_net, "Module-expansion "); + if (server_dir != NULL) + { + buf_output0 (buf_to_net, server_dir); + buf_output0 (buf_to_net, "/"); + } + buf_output0 (buf_to_net, mwhere); + if (mfile != NULL) + { + buf_append_char (buf_to_net, '/'); + buf_output0 (buf_to_net, mfile); + } + buf_append_char (buf_to_net, '\n'); + } + else + { + /* We may not need to do this anymore -- check the definition + of aliases before removing */ + if (argc == 1) + { + buf_output0 (buf_to_net, "Module-expansion "); + if (server_dir != NULL) + { + buf_output0 (buf_to_net, server_dir); + buf_output0 (buf_to_net, "/"); + } + buf_output0 (buf_to_net, dir); + buf_append_char (buf_to_net, '\n'); + } + else + { + for (i = 1; i < argc; ++i) + { + buf_output0 (buf_to_net, "Module-expansion "); + if (server_dir != NULL) + { + buf_output0 (buf_to_net, server_dir); + buf_output0 (buf_to_net, "/"); + } + buf_output0 (buf_to_net, dir); + buf_append_char (buf_to_net, '/'); + buf_output0 (buf_to_net, argv[i]); + buf_append_char (buf_to_net, '\n'); + } + } + } + return 0; +} + + + +static void +serve_expand_modules (char *arg) +{ + int i; + int err = 0; + DBM *db; + +# ifdef PROXY_SUPPORT + /* This needs to be processed in the first pass since the client expects a + * response but we may not yet know if we are a secondary. + * + * On the second pass, we still must make sure to ignore the arguments. + */ + if (!reprocessing) +# endif /* PROXY_SUPPORT */ + { + err = 0; + + db = open_module (); + for (i = 1; i < argument_count; i++) + err += do_module (db, argument_vector[i], + CHECKOUT, "Updating", expand_proc, + NULL, 0, 0, 0, 0, NULL); + close_module (db); + } + + { + /* argument_vector[0] is a dummy argument, we don't mess with it. */ + char **cp; + for (cp = argument_vector + 1; + cp < argument_vector + argument_count; + ++cp) + free (*cp); + + argument_count = 1; + } + +# ifdef PROXY_SUPPORT + if (!reprocessing) +# endif /* PROXY_SUPPORT */ + { + if (err) + /* We will have printed an error message already. */ + buf_output0 (buf_to_net, "error \n"); + else + buf_output0 (buf_to_net, "ok\n"); + + /* The client is waiting for the module expansions, so we must + send the output now. */ + buf_flush (buf_to_net, 1); + } +} + + + +/* Decide if we should redirect the client to another server. + * + * GLOBALS + * config->PrimaryServer The server to redirect write requests to, if + * any. + * + * ASSUMPTIONS + * The `Root' request has already been processed. + * + * RETURNS + * Nothing. + */ +static void +serve_command_prep (char *arg) +{ + bool redirect_supported; +# ifdef PROXY_SUPPORT + bool ditch_log; +# endif /* PROXY_SUPPORT */ + + if (print_pending_error ()) return; + + redirect_supported = supported_response ("Redirect"); + if (redirect_supported + && lookup_command_attribute (arg) & CVS_CMD_MODIFIES_REPOSITORY + /* I call isProxyServer() last because it can probably be the slowest + * call due to the call to gethostbyname(). + */ + && isProxyServer ()) + { + /* Before sending a redirect, send a "Referrer" line to the client, + * if possible, to give admins more control over canonicalizing roots + * sent from the client. + */ + if (supported_response ("Referrer")) + { + /* assume :ext:, since that is all we currently support for + * proxies and redirection. + */ + char *referrer = Xasprintf (":ext:%s@%s%s", getcaller(), + server_hostname, + current_parsed_root->directory); + + buf_output0 (buf_to_net, "Referrer "); + buf_output0 (buf_to_net, referrer); + buf_output0 (buf_to_net, "\n"); + + free (referrer); + } + + /* Send `Redirect' to redirect client requests to the primary. */ + buf_output0 (buf_to_net, "Redirect "); + buf_output0 (buf_to_net, config->PrimaryServer->original); + buf_output0 (buf_to_net, "\n"); + buf_flush (buf_to_net, 1); +# ifdef PROXY_SUPPORT + ditch_log = true; +# endif /* PROXY_SUPPORT */ + } + else + { + /* Send `ok' so the client can proceed. */ + buf_output0 (buf_to_net, "ok\n"); + buf_flush (buf_to_net, 1); +# ifdef PROXY_SUPPORT + if (lookup_command_attribute (arg) & CVS_CMD_MODIFIES_REPOSITORY + && isProxyServer ()) + /* Don't ditch the log for write commands on a proxy server. We + * we got here because the `Redirect' response was not supported. + */ + ditch_log = false; + else + ditch_log = true; +# endif /* PROXY_SUPPORT */ + } +# ifdef PROXY_SUPPORT + if (proxy_log && ditch_log) + { + /* If the client supported the redirect response, then they will always + * be redirected if they are preparing for a write request. It is + * therefore safe to close the proxy logs. + * + * If the client is broken and ignores the redirect, this will be + * detected later, in rewind_buf_from_net(). + * + * Since a `Command-prep' response is only acceptable immediately + * following the `Root' request according to the specification, there + * is no need to rewind the log and reprocess. + */ + log_buffer_closelog (proxy_log); + log_buffer_closelog (proxy_log_out); + proxy_log = NULL; + } +# endif /* PROXY_SUPPORT */ +} + + + +/* Save a referrer, potentially for passing to hook scripts later. + * + * GLOBALS + * referrer Where we save the parsed referrer. + * + * ASSUMPTIONS + * The `Root' request has already been processed. + * There is no need to dispose of REFERRER if it is set. It's memory is + * tracked by parse_root(). + * + * RETURNS + * Nothing. + */ +static void +serve_referrer (char *arg) +{ + if (error_pending ()) return; + + referrer = parse_cvsroot (arg); + + if (!referrer + && alloc_pending (80 + strlen (arg))) + sprintf (pending_error_text, + "E Protocol error: Invalid Referrer: `%s'", + arg); +} + + + +static void serve_valid_requests (char *arg); + +#endif /* SERVER_SUPPORT */ +/* + * Comment to move position of the following #if line which works + * around an apparent bug in Microsoft Visual C++ 6.0 compiler. + */ +#if defined(SERVER_SUPPORT) || defined(CLIENT_SUPPORT) +/* + * Parts of this table are shared with the client code, + * but the client doesn't need to know about the handler + * functions. + */ + +struct request requests[] = +{ +#ifdef SERVER_SUPPORT +#define REQ_LINE(n, f, s) {n, f, s} +#else +#define REQ_LINE(n, f, s) {n, s} +#endif + + REQ_LINE("Root", serve_root, RQ_ESSENTIAL | RQ_ROOTLESS), + REQ_LINE("Valid-responses", serve_valid_responses, + RQ_ESSENTIAL | RQ_ROOTLESS), + REQ_LINE("valid-requests", serve_valid_requests, + RQ_ESSENTIAL | RQ_ROOTLESS), + REQ_LINE("Command-prep", serve_command_prep, 0), + REQ_LINE("Referrer", serve_referrer, 0), + REQ_LINE("Repository", serve_repository, 0), + REQ_LINE("Directory", serve_directory, RQ_ESSENTIAL), + REQ_LINE("Relative-directory", serve_directory, 0), + REQ_LINE("Max-dotdot", serve_max_dotdot, 0), + REQ_LINE("Static-directory", serve_static_directory, 0), + REQ_LINE("Sticky", serve_sticky, 0), + REQ_LINE("Entry", serve_entry, RQ_ESSENTIAL), + REQ_LINE("Kopt", serve_kopt, 0), + REQ_LINE("Checkin-time", serve_checkin_time, 0), + REQ_LINE("Modified", serve_modified, RQ_ESSENTIAL), + REQ_LINE("Is-modified", serve_is_modified, 0), + + /* The client must send this request to interoperate with CVS 1.5 + through 1.9 servers. The server must support it (although it can + be and is a noop) to interoperate with CVS 1.5 to 1.9 clients. */ + REQ_LINE("UseUnchanged", serve_enable_unchanged, RQ_ENABLEME | RQ_ROOTLESS), + + REQ_LINE("Unchanged", serve_unchanged, RQ_ESSENTIAL), + REQ_LINE("Notify", serve_notify, 0), + REQ_LINE("Hostname", serve_hostname, 0), + REQ_LINE("LocalDir", serve_localdir, 0), + REQ_LINE("Questionable", serve_questionable, 0), + REQ_LINE("Argument", serve_argument, RQ_ESSENTIAL), + REQ_LINE("Argumentx", serve_argumentx, RQ_ESSENTIAL), + REQ_LINE("Global_option", serve_global_option, RQ_ROOTLESS), + /* This is rootless, even though the client/server spec does not specify + * such, to allow error messages to be understood by the client when they are + * sent. + */ + REQ_LINE("Gzip-stream", serve_gzip_stream, RQ_ROOTLESS), + REQ_LINE("wrapper-sendme-rcsOptions", + serve_wrapper_sendme_rcs_options, + 0), + REQ_LINE("Set", serve_set, RQ_ROOTLESS), +#ifdef ENCRYPTION + /* These are rootless despite what the client/server spec says for the same + * reasons as Gzip-stream. + */ +# ifdef HAVE_KERBEROS + REQ_LINE("Kerberos-encrypt", serve_kerberos_encrypt, RQ_ROOTLESS), +# endif +# ifdef HAVE_GSSAPI + REQ_LINE("Gssapi-encrypt", serve_gssapi_encrypt, RQ_ROOTLESS), +# endif +#endif +#ifdef HAVE_GSSAPI + REQ_LINE("Gssapi-authenticate", serve_gssapi_authenticate, RQ_ROOTLESS), +#endif + REQ_LINE("expand-modules", serve_expand_modules, 0), + REQ_LINE("ci", serve_ci, RQ_ESSENTIAL), + REQ_LINE("co", serve_co, RQ_ESSENTIAL), + REQ_LINE("update", serve_update, RQ_ESSENTIAL), + REQ_LINE("diff", serve_diff, 0), + REQ_LINE("log", serve_log, 0), + REQ_LINE("rlog", serve_rlog, 0), + REQ_LINE("list", serve_ls, 0), + REQ_LINE("rlist", serve_rls, 0), + /* This allows us to avoid sending `-q' as a command argument to `cvs ls', + * or more accurately, allows us to send `-q' to backwards CVSNT servers. + */ + REQ_LINE("global-list-quiet", serve_noop, RQ_ROOTLESS), + /* Deprecated synonym for rlist, for compatibility with CVSNT. */ + REQ_LINE("ls", serve_rls, 0), + REQ_LINE("add", serve_add, 0), + REQ_LINE("remove", serve_remove, 0), + REQ_LINE("update-patches", serve_ignore, 0), + REQ_LINE("gzip-file-contents", serve_gzip_contents, RQ_ROOTLESS), + REQ_LINE("status", serve_status, 0), + REQ_LINE("rdiff", serve_rdiff, 0), + REQ_LINE("tag", serve_tag, 0), + REQ_LINE("rtag", serve_rtag, 0), + REQ_LINE("import", serve_import, 0), + REQ_LINE("admin", serve_admin, 0), + REQ_LINE("export", serve_export, 0), + REQ_LINE("history", serve_history, 0), + REQ_LINE("release", serve_release, 0), + REQ_LINE("watch-on", serve_watch_on, 0), + REQ_LINE("watch-off", serve_watch_off, 0), + REQ_LINE("watch-add", serve_watch_add, 0), + REQ_LINE("watch-remove", serve_watch_remove, 0), + REQ_LINE("watchers", serve_watchers, 0), + REQ_LINE("editors", serve_editors, 0), + REQ_LINE("edit", serve_edit, 0), + REQ_LINE("init", serve_init, RQ_ROOTLESS), + REQ_LINE("annotate", serve_annotate, 0), + REQ_LINE("rannotate", serve_rannotate, 0), + REQ_LINE("noop", serve_noop, RQ_ROOTLESS), + REQ_LINE("version", serve_version, RQ_ROOTLESS), + REQ_LINE(NULL, NULL, 0) + +#undef REQ_LINE +}; +#endif /* SERVER_SUPPORT or CLIENT_SUPPORT */ + + + +#ifdef SERVER_SUPPORT +/* + * This server request is not ignored by the secondary. + */ +static void +serve_valid_requests (char *arg) +{ + struct request *rq; + + /* Since this is processed in the first pass, don't reprocess it in the + * second. + * + * We still print errors since new errors could have been generated in the + * second pass. + */ + if (print_pending_error () +#ifdef PROXY_SUPPORT + || reprocessing +#endif /* PROXY_SUPPORT */ + ) + return; + + buf_output0 (buf_to_net, "Valid-requests"); + for (rq = requests; rq->name != NULL; rq++) + { + if (rq->func != NULL) + { + buf_append_char (buf_to_net, ' '); + buf_output0 (buf_to_net, rq->name); + } + } + + if (config && config->MinCompressionLevel + && supported_response ("Force-gzip")) + { + buf_output0 (buf_to_net, "\n"); + buf_output0 (buf_to_net, "Force-gzip"); + } + + buf_output0 (buf_to_net, "\nok\n"); + + /* The client is waiting for the list of valid requests, so we + must send the output now. */ + buf_flush (buf_to_net, 1); +} + + + +#ifdef SUNOS_KLUDGE +/* + * Delete temporary files. SIG is the signal making this happen, or + * 0 if not called as a result of a signal. + */ +static int command_pid_is_dead; +static void wait_sig (int sig) +{ + int status; + pid_t r = wait (&status); + if (r == command_pid) + command_pid_is_dead++; +} +#endif /* SUNOS_KLUDGE */ + + + +/* + * This function cleans up after the server. Specifically, it: + * + * <ol> + * <li>Sets BUF_TO_NET to blocking and fluxhes it.</li> + * <li>With SUNOS_KLUDGE enabled: + * <ol> + * <li>Terminates the command process.</li> + * <li>Waits on the command process, draining output as necessary.</li> + * </ol> + * </li> + * <li>Removes the temporary directory.</li> + * <li>Flush and shutdown the buffers.</li> + * <li>Set ERROR_USE_PROTOCOL and SERVER_ACTIVE to false.</li> + * </ol> + * + * NOTES + * This function needs to be reentrant since a call to exit() can cause a + * call to this function, which can then be interrupted by a signal, which + * can cause a second call to this function. + * + * GLOBALS + * buf_from_net The input buffer which brings data from the + * CVS client. + * buf_to_net The output buffer which moves data to the CVS + * client. + * error_use_protocol Set when the server parent process is active. + * Cleared for the server child processes. + * dont_delete_temp Set when a core dump of a child process is + * detected so that the core and related data may + * be preserved. + * noexec Whether we are supposed to change the disk. + * orig_server_temp_dir The temporary directory we created within + * Tmpdir for our duplicate of the client + * workspace. + * + * INPUTS + * None. + * + * ERRORS + * Problems encountered during the cleanup, for instance low memory or + * problems deleting the temp files and directories, can cause the error + * function to be called, which might call exit. If exit gets called in this + * manner. this routine will not complete, but the other exit handlers + * registered via atexit() will still run. + * + * RETURNS + * Nothing. + */ +void +server_cleanup (void) +{ + TRACE (TRACE_FUNCTION, "server_cleanup()"); + + assert (server_active); + + /* FIXME: Do not perform buffered I/O from an interrupt handler like + * this (via error). However, I'm leaving the error-calling code there + * in the hope that on the rare occasion the error call is actually made + * (e.g., a fluky I/O error or permissions problem prevents the deletion + * of a just-created file) reentrancy won't be an issue. + */ + + /* We don't want to be interrupted during calls which set globals to NULL, + * but we know that by the time we reach this function, interrupts have + * already been blocked. + */ + + /* Since we install this function in an atexit() handler before forking, + * reuse the ERROR_USE_PROTOCOL flag, which we know is only set in the + * parent server process, to avoid cleaning up the temp space multiple + * times. Skip the buf_to_net checks too as an optimization since we know + * they will be set to NULL in the child process anyhow. + */ + if (error_use_protocol) + { + if (buf_to_net != NULL) + { + int status; + + /* Since we're done, go ahead and put BUF_TO_NET back into blocking + * mode and send any pending output. In the usual case there won't + * won't be any, but there might be if an error occured. + */ + + set_block (buf_to_net); + buf_flush (buf_to_net, 1); + + /* Next we shut down BUF_FROM_NET. That will pick up the checksum + * generated when the client shuts down its buffer. Then, after we + * have generated any final output, we shut down BUF_TO_NET. + */ + + /* SIG_beginCrSect(); */ + if (buf_from_net) + { + status = buf_shutdown (buf_from_net); + if (status != 0) + error (0, status, "shutting down buffer from client"); + buf_free (buf_from_net); + buf_from_net = NULL; + } + /* SIG_endCrSect(); */ + } + + if (!dont_delete_temp) + { + int save_noexec; + + /* What a bogus kludge. This disgusting code makes all kinds of + assumptions about SunOS, and is only for a bug in that system. + So only enable it on Suns. */ +#ifdef SUNOS_KLUDGE + if (command_pid > 0) + { + /* To avoid crashes on SunOS due to bugs in SunOS tmpfs + * triggered by the use of rename() in RCS, wait for the + * subprocess to die. Unfortunately, this means draining + * output while waiting for it to unblock the signal we sent + * it. Yuck! + */ + int status; + pid_t r; + + signal (SIGCHLD, wait_sig); + /* Perhaps SIGTERM would be more correct. But the child + process will delay the SIGINT delivery until its own + children have exited. */ + kill (command_pid, SIGINT); + /* The caller may also have sent a signal to command_pid, so + * always try waiting. First, though, check and see if it's + * still there.... + */ + do_waitpid: + r = waitpid (command_pid, &status, WNOHANG); + if (r == 0) + ; + else if (r == command_pid) + command_pid_is_dead++; + else if (r == -1) + switch (errno) + { + case ECHILD: + command_pid_is_dead++; + break; + case EINTR: + goto do_waitpid; + } + else + /* waitpid should always return one of the above values */ + abort (); + while (!command_pid_is_dead) + { + struct timeval timeout; + struct fd_set_wrapper readfds; + char buf[100]; + int i; + + /* Use a non-zero timeout to avoid eating up CPU cycles. */ + timeout.tv_sec = 2; + timeout.tv_usec = 0; + readfds = command_fds_to_drain; + switch (select (max_command_fd + 1, &readfds.fds, + NULL, NULL &timeout)) + { + case -1: + if (errno != EINTR) + abort (); + case 0: + /* timeout */ + break; + case 1: + for (i = 0; i <= max_command_fd; i++) + { + if (!FD_ISSET (i, &readfds.fds)) + continue; + /* this fd is non-blocking */ + while (read (i, buf, sizeof (buf)) >= 1) + ; + } + break; + default: + abort (); + } + } + } +#endif /* SUNOS_KLUDGE */ + + /* Make sure our working directory isn't inside the tree we're + going to delete. */ + CVS_CHDIR (get_cvs_tmp_dir ()); + + /* Temporarily clear noexec, so that we clean up our temp directory + regardless of it (this could more cleanly be handled by moving + the noexec check to all the unlink_file_dir callers from + unlink_file_dir itself). */ + save_noexec = noexec; + + /* SIG_beginCrSect(); */ + noexec = 0; + unlink_file_dir (orig_server_temp_dir); + noexec = save_noexec; + /* SIG_endCrSect(); */ + } /* !dont_delete_temp */ + + /* SIG_beginCrSect(); */ + if (buf_to_net != NULL) + { + /* Save BUF_TO_NET and set the global pointer to NULL so that any + * error messages generated during shutdown go to the syslog rather + * than getting lost. + */ + struct buffer *buf_to_net_save = buf_to_net; + buf_to_net = NULL; + + (void) buf_flush (buf_to_net_save, 1); + (void) buf_shutdown (buf_to_net_save); + buf_free (buf_to_net_save); + error_use_protocol = 0; + } + /* SIG_endCrSect(); */ + } + + server_active = 0; +} + + + +#ifdef PROXY_SUPPORT +size_t MaxProxyBufferSize = (size_t)(8 * 1024 * 1024); /* 8 megabytes, + * by default. + */ +#endif /* PROXY_SUPPORT */ + +static const char *const server_usage[] = +{ + "Usage: %s %s [-c config-file]\n", + "\t-c config-file\tPath to an alternative CVS config file.\n", + "Normally invoked by a cvs client on a remote machine.\n", + NULL +}; + + + +void +parseServerOptions (int argc, char **argv) +{ + int c; + + optind = 0; + while ((c = getopt (argc, argv, "+c:")) != -1) + { + switch (c) + { +#ifdef ALLOW_CONFIG_OVERRIDE + case 'c': + if (gConfigPath) free (gConfigPath); + gConfigPath = xstrdup (optarg); + break; +#endif + case '?': + default: + usage (server_usage); + break; + } + } +} + + + +int +server (int argc, char **argv) +{ + char *error_prog_name; /* Used in error messages */ + + if (argc == -1) + usage (server_usage); + + /* Options were pre-parsed in main.c. */ + + /* + * Set this in .bashrc if you want to give yourself time to attach + * to the subprocess with a debugger. + */ + if (getenv ("CVS_PARENT_SERVER_SLEEP")) + { + int secs = atoi (getenv ("CVS_PARENT_SERVER_SLEEP")); + TRACE (TRACE_DATA, "Sleeping CVS_PARENT_SERVER_SLEEP (%d) seconds", + secs); + sleep (secs); + } + else + TRACE (TRACE_DATA, "CVS_PARENT_SERVER_SLEEP not set."); + + /* pserver_authenticate_connection () (called from main ()) can initialize + * these. + */ + if (!buf_to_net) + { + buf_to_net = fd_buffer_initialize (STDOUT_FILENO, 0, NULL, false, + outbuf_memory_error); + buf_from_net = fd_buffer_initialize (STDIN_FILENO, 0, NULL, true, + outbuf_memory_error); + } + + setup_logfiles ("CVS_SERVER_LOG", &buf_to_net, &buf_from_net); + +#ifdef PROXY_SUPPORT + /* We have to set up the recording for all servers. Until we receive the + * `Root' request and load CVSROOT/config, we can't tell if we are a + * secondary or primary. + */ + { + /* Open the secondary log. */ + buf_from_net = log_buffer_initialize (buf_from_net, NULL, +# ifdef PROXY_SUPPORT + true, + config + ? config->MaxProxyBufferSize + : MaxProxyBufferSize, +# endif /* PROXY_SUPPORT */ + true, outbuf_memory_error); + proxy_log = buf_from_net; + + /* And again for the out log. */ + buf_to_net = log_buffer_initialize (buf_to_net, NULL, +# ifdef PROXY_SUPPORT + true, + config + ? config->MaxProxyBufferSize + : MaxProxyBufferSize, +# endif /* PROXY_SUPPORT */ + false, outbuf_memory_error); + proxy_log_out = buf_to_net; + } +#endif /* PROXY_SUPPORT */ + + saved_output = buf_nonio_initialize (outbuf_memory_error); + saved_outerr = buf_nonio_initialize (outbuf_memory_error); + + /* Since we're in the server parent process, error should use the + protocol to report error messages. */ + error_use_protocol = 1; + + /* Now initialize our argument vector (for arguments from the client). */ + + /* Small for testing. */ + argument_vector_size = 1; + argument_vector = xmalloc (argument_vector_size * sizeof (char *)); + argument_count = 1; + /* This gets printed if the client supports an option which the + server doesn't, causing the server to print a usage message. + FIXME: just a nit, I suppose, but the usage message the server + prints isn't literally true--it suggests "cvs server" followed + by options which are for a particular command. Might be nice to + say something like "client apparently supports an option not supported + by this server" or something like that instead of usage message. */ + error_prog_name = xmalloc (strlen (program_name) + 8); + sprintf(error_prog_name, "%s server", program_name); + argument_vector[0] = error_prog_name; + + while (1) + { + char *cmd, *orig_cmd; + struct request *rq; + int status; + + status = buf_read_line (buf_from_net, &cmd, NULL); + if (status == -2) + { + buf_output0 (buf_to_net, "E Fatal server error, aborting.\n\ +error ENOMEM Virtual memory exhausted.\n"); + break; + } + if (status != 0) + break; + + orig_cmd = cmd; + for (rq = requests; rq->name != NULL; ++rq) + if (strncmp (cmd, rq->name, strlen (rq->name)) == 0) + { + int len = strlen (rq->name); + if (cmd[len] == '\0') + cmd += len; + else if (cmd[len] == ' ') + cmd += len + 1; + else + /* + * The first len characters match, but it's a different + * command. e.g. the command is "cooperate" but we matched + * "co". + */ + continue; + + if (!(rq->flags & RQ_ROOTLESS) + && current_parsed_root == NULL) + { + if (alloc_pending (80)) + sprintf (pending_error_text, + "E Protocol error: Root request missing"); + } + else + { + if (config && config->MinCompressionLevel && !gzip_level + && !(rq->flags & RQ_ROOTLESS)) + { + /* This is a rootless request, a minimum compression + * level has been configured, and no compression has + * been requested by the client. + */ + if (alloc_pending (80 + strlen (program_name))) + sprintf (pending_error_text, +"E %s [server aborted]: Compression must be used with this server.", + program_name); + } + (*rq->func) (cmd); + } + break; + } + if (rq->name == NULL) + { + if (!print_pending_error ()) + { + buf_output0 (buf_to_net, "error unrecognized request `"); + buf_output0 (buf_to_net, cmd); + buf_append_char (buf_to_net, '\''); + buf_append_char (buf_to_net, '\n'); + } + } + free (orig_cmd); + } + + free (error_prog_name); + + /* We expect the client is done talking to us at this point. If there is + * any data in the buffer or on the network pipe, then something we didn't + * prepare for is happening. + */ + if (!buf_empty (buf_from_net)) + { + /* Try to send the error message to the client, but also syslog it, in + * case the client isn't listening anymore. + */ +#ifdef HAVE_SYSLOG_H + /* FIXME: Can the IP address of the connecting client be retrieved + * and printed here? + */ + syslog (LOG_DAEMON | LOG_ERR, "Dying gasps received from client."); +#endif /* HAVE_SYSLOG_H */ + error (0, 0, "Dying gasps received from client."); + } + +#ifdef HAVE_PAM + if (pamh) + { + int retval; + + retval = pam_close_session (pamh, 0); +# ifdef HAVE_SYSLOG_H + if (retval != PAM_SUCCESS) + syslog (LOG_DAEMON | LOG_ERR, + "PAM close session error: %s", + pam_strerror (pamh, retval)); +# endif /* HAVE_SYSLOG_H */ + + retval = pam_end (pamh, retval); +# ifdef HAVE_SYSLOG_H + if (retval != PAM_SUCCESS) + syslog (LOG_DAEMON | LOG_ERR, + "PAM failed to release authenticator, error: %s", + pam_strerror (pamh, retval)); +# endif /* HAVE_SYSLOG_H */ + } +#endif /* HAVE_PAM */ + + /* server_cleanup() will be called on a normal exit and close the buffers + * explicitly. + */ + return 0; +} + + + +#if defined (HAVE_KERBEROS) || defined (AUTH_SERVER_SUPPORT) || defined (HAVE_GSSAPI) +static void +switch_to_user (const char *cvs_username, const char *username) +{ + struct passwd *pw; +#ifdef HAVE_PAM + int retval; + char *pam_stage = "open session"; + + if (pamh) + { + retval = pam_open_session (pamh, 0); + if (retval == PAM_SUCCESS) + { + pam_stage = "get pam user"; + retval = pam_get_item (pamh, PAM_USER, (const void **)&username); + } + + if (retval != PAM_SUCCESS) + { + printf("E PAM %s error: %s\n", pam_stage, + pam_strerror (pamh, retval)); + exit (EXIT_FAILURE); + } + } +#endif + + pw = getpwnam (username); + if (pw == NULL) + { + /* check_password contains a similar check, so this usually won't be + reached unless the CVS user is mapped to an invalid system user. */ + + printf ("E Fatal error, aborting.\n\ +error 0 %s: no such system user\n", username); + exit (EXIT_FAILURE); + } + + if (pw->pw_uid == 0) + { +#ifdef HAVE_SYSLOG_H + /* FIXME: Can the IP address of the connecting client be retrieved + * and printed here? + */ + syslog (LOG_DAEMON | LOG_ALERT, + "attempt to root from account: %s", cvs_username + ); +#endif /* HAVE_SYSLOG_H */ + printf("error 0: root not allowed\n"); + exit (EXIT_FAILURE); + } + +#if HAVE_INITGROUPS + if (initgroups (pw->pw_name, pw->pw_gid) < 0 +# ifdef EPERM + /* At least on the system I tried, initgroups() only works as root. + But we do still want to report ENOMEM and whatever other + errors initgroups() might dish up. */ + && errno != EPERM +# endif + ) + { + /* This could be a warning, but I'm not sure I see the point + in doing that instead of an error given that it would happen + on every connection. We could log it somewhere and not tell + the user. But at least for now make it an error. */ + printf ("error 0 initgroups failed: %s\n", strerror (errno)); + exit (EXIT_FAILURE); + } +#endif /* HAVE_INITGROUPS */ + +#ifdef HAVE_PAM + if (pamh) + { + retval = pam_setcred (pamh, PAM_ESTABLISH_CRED); + if (retval != PAM_SUCCESS) + { + printf("E PAM reestablish credentials error: %s\n", + pam_strerror (pamh, retval)); + exit (EXIT_FAILURE); + } + } +#endif + +#ifdef SETXID_SUPPORT + /* honor the setgid bit iff set*/ + if (getgid() != getegid()) + { + if (setgid (getegid ()) < 0) + { + /* See comments at setuid call below for more discussion. */ + printf ("error 0 setgid failed: %s\n", strerror (errno)); + exit (EXIT_FAILURE); + } + } + else +#endif + { + if (setgid (pw->pw_gid) < 0) + { + /* See comments at setuid call below for more discussion. */ + printf ("error 0 setgid failed: %s\n", strerror (errno)); +#ifdef HAVE_SYSLOG_H + syslog (LOG_DAEMON | LOG_ERR, + "setgid to %d failed (%m): real %d/%d, effective %d/%d ", + pw->pw_gid, getuid(), getgid(), geteuid(), getegid()); +#endif /* HAVE_SYSLOG_H */ + exit (EXIT_FAILURE); + } + } + + if (setuid (pw->pw_uid) < 0) + { + /* Note that this means that if run as a non-root user, + CVSROOT/passwd must contain the user we are running as + (e.g. "joe:FsEfVcu:cvs" if run as "cvs" user). This seems + cleaner than ignoring the error like CVS 1.10 and older but + it does mean that some people might need to update their + CVSROOT/passwd file. */ + printf ("error 0 setuid failed: %s\n", strerror (errno)); +#ifdef HAVE_SYSLOG_H + syslog (LOG_DAEMON | LOG_ERR, + "setuid to %d failed (%m): real %d/%d, effective %d/%d ", + pw->pw_uid, getuid(), getgid(), geteuid(), getegid()); +#endif /* HAVE_SYSLOG_H */ + exit (EXIT_FAILURE); + } + + /* We don't want our umask to change file modes. The modes should + be set by the modes used in the repository, and by the umask of + the client. */ + umask (0); + +#ifdef AUTH_SERVER_SUPPORT + /* Make sure our CVS_Username has been set. */ + if (CVS_Username == NULL) + CVS_Username = xstrdup (username); +#endif + + /* Set LOGNAME, USER and CVS_USER in the environment, in case they + are already set to something else. */ + setenv ("LOGNAME", username, 1); + setenv ("USER", username, 1); +# ifdef AUTH_SERVER_SUPPORT + setenv ("CVS_USER", CVS_Username, 1); +# endif +} +#endif + +#ifdef AUTH_SERVER_SUPPORT + +extern char *crypt (const char *, const char *); + + +/* + * 0 means no entry found for this user. + * 1 means entry found and password matches (or found password is empty) + * 2 means entry found, but password does not match. + * + * If 1, host_user_ptr will be set to point at the system + * username (i.e., the "real" identity, which may or may not be the + * CVS username) of this user; caller may free this. Global + * CVS_Username will point at an allocated copy of cvs username (i.e., + * the username argument below). + * kff todo: FIXME: last sentence is not true, it applies to caller. + */ +static int +check_repository_password (char *username, char *password, char *repository, char **host_user_ptr) +{ + int retval = 0; + FILE *fp; + char *filename; + char *linebuf = NULL; + size_t linebuf_len; + int found_it = 0; + int namelen; + + /* We don't use current_parsed_root->directory because it hasn't been + * set yet -- our `repository' argument came from the authentication + * protocol, not the regular CVS protocol. + */ + + filename = xmalloc (strlen (repository) + + 1 + + strlen (CVSROOTADM) + + 1 + + strlen (CVSROOTADM_PASSWD) + + 1); + + (void) sprintf (filename, "%s/%s/%s", repository, + CVSROOTADM, CVSROOTADM_PASSWD); + + fp = CVS_FOPEN (filename, "r"); + if (fp == NULL) + { + if (!existence_error (errno)) + error (0, errno, "cannot open %s", filename); + free (filename); + return 0; + } + + /* Look for a relevant line -- one with this user's name. */ + namelen = strlen (username); + while (getline (&linebuf, &linebuf_len, fp) >= 0) + { + if ((strncmp (linebuf, username, namelen) == 0) + && (linebuf[namelen] == ':')) + { + found_it = 1; + break; + } + } + if (ferror (fp)) + error (0, errno, "cannot read %s", filename); + if (fclose (fp) < 0) + error (0, errno, "cannot close %s", filename); + + /* If found_it, then linebuf contains the information we need. */ + if (found_it) + { + char *found_password, *host_user_tmp; + char *non_cvsuser_portion; + + /* We need to make sure lines such as + * + * "username::sysuser\n" + * "username:\n" + * "username: \n" + * + * all result in a found_password of NULL, but we also need to + * make sure that + * + * "username: :sysuser\n" + * "username: <whatever>:sysuser\n" + * + * continues to result in an impossible password. That way, + * an admin would be on safe ground by going in and tacking a + * space onto the front of a password to disable the account + * (a technique some people use to close accounts + * temporarily). + */ + + /* Make `non_cvsuser_portion' contain everything after the CVS + username, but null out any final newline. */ + non_cvsuser_portion = linebuf + namelen; + strtok (non_cvsuser_portion, "\n"); + + /* If there's a colon now, we just want to inch past it. */ + if (strchr (non_cvsuser_portion, ':') == non_cvsuser_portion) + non_cvsuser_portion++; + + /* Okay, after this conditional chain, found_password and + host_user_tmp will have useful values: */ + + if ((non_cvsuser_portion == NULL) + || (strlen (non_cvsuser_portion) == 0) + || ((strspn (non_cvsuser_portion, " \t")) + == strlen (non_cvsuser_portion))) + { + found_password = NULL; + host_user_tmp = NULL; + } + else if (strncmp (non_cvsuser_portion, ":", 1) == 0) + { + found_password = NULL; + host_user_tmp = non_cvsuser_portion + 1; + if (strlen (host_user_tmp) == 0) + host_user_tmp = NULL; + } + else + { + found_password = strtok (non_cvsuser_portion, ":"); + host_user_tmp = strtok (NULL, ":"); + } + + /* Of course, maybe there was no system user portion... */ + if (host_user_tmp == NULL) + host_user_tmp = username; + + /* Verify blank passwords directly, otherwise use crypt(). */ + if ((found_password == NULL) + || ((strcmp (found_password, crypt (password, found_password)) + == 0))) + { + /* Give host_user_ptr permanent storage. */ + *host_user_ptr = xstrdup (host_user_tmp); + retval = 1; + } + else + { +#ifdef LOG_AUTHPRIV + syslog (LOG_AUTHPRIV | LOG_NOTICE, + "password mismatch for %s in %s: %s vs. %s", username, + repository, crypt(password, found_password), found_password); +#endif + *host_user_ptr = NULL; + retval = 2; + } + } + else /* Didn't find this user, so deny access. */ + { + *host_user_ptr = NULL; + retval = 0; + } + + free (filename); + if (linebuf) + free (linebuf); + + return retval; +} + +#ifdef HAVE_PAM + +static int +cvs_pam_conv (int num_msg, const struct pam_message **msg, + struct pam_response **resp, void *appdata_ptr) +{ + int i; + struct pam_response *response; + + assert (msg && resp); + + response = xnmalloc (num_msg, sizeof (struct pam_response)); + memset (response, 0, num_msg * sizeof (struct pam_response)); + + for (i = 0; i < num_msg; i++) + { + switch (msg[i]->msg_style) + { + /* PAM wants a username */ + case PAM_PROMPT_ECHO_ON: + assert (pam_username != 0); + response[i].resp = xstrdup (pam_username); + break; + /* PAM wants a password */ + case PAM_PROMPT_ECHO_OFF: + assert (pam_password != 0); + response[i].resp = xstrdup (pam_password); + break; + case PAM_ERROR_MSG: + case PAM_TEXT_INFO: + printf ("E %s\n", msg[i]->msg); + break; + /* PAM wants something we don't understand - bail out */ + default: + goto cleanup; + } + } + + *resp = response; + return PAM_SUCCESS; + +cleanup: + for (i = 0; i < num_msg; i++) + { + if (response[i].resp) + { + free (response[i].resp); + response[i].resp = 0; + } + } + free (response); + return PAM_CONV_ERR; +} + +static int +check_pam_password (char **username, char *password) +{ + int retval, err; + struct pam_conv conv = { cvs_pam_conv, 0 }; + char *pam_stage = "start"; + + pam_username = *username; + pam_password = password; + + retval = pam_start (PAM_SERVICE_NAME, *username, &conv, &pamh); + + /* sets a dummy tty name which pam modules can check for */ + if (retval == PAM_SUCCESS) + { + pam_stage = "set dummy tty"; + retval = pam_set_item (pamh, PAM_TTY, PAM_SERVICE_NAME); + } + + if (retval == PAM_SUCCESS) + { + pam_stage = "authenticate"; + retval = pam_authenticate (pamh, 0); + } + + if (retval == PAM_SUCCESS) + { + pam_stage = "account"; + retval = pam_acct_mgmt (pamh, 0); + } + + if (retval == PAM_SUCCESS) + { + pam_stage = "get pam user"; + retval = pam_get_item (pamh, PAM_USER, (const void **)username); + } + + if (retval != PAM_SUCCESS) + printf ("E PAM %s error: %s\n", pam_stage, pam_strerror (pamh, retval)); + + /* clear the pointers to make sure we don't use these references again */ + pam_username = 0; + pam_password = 0; + + return retval == PAM_SUCCESS; /* indicate success */ +} +#endif + +static int +check_system_password (char *username, char *password) +{ + char *found_passwd = NULL; + struct passwd *pw; +#ifdef HAVE_GETSPNAM + { + struct spwd *spw; + + spw = getspnam (username); + if (spw != NULL) + found_passwd = spw->sp_pwdp; + } +#endif + + if (found_passwd == NULL && (pw = getpwnam (username)) != NULL) + found_passwd = pw->pw_passwd; + + if (found_passwd == NULL) + { + printf ("E Fatal error, aborting.\n\ +error 0 %s: no such user\n", username); + + exit (EXIT_FAILURE); + } + + /* Allow for dain bramaged HPUX passwd aging + * - Basically, HPUX adds a comma and some data + * about whether the passwd has expired or not + * on the end of the passwd field. + * - This code replaces the ',' with '\0'. + * + * FIXME - our workaround is brain damaged too. I'm + * guessing that HPUX WANTED other systems to think the + * password was wrong so logins would fail if the + * system didn't handle expired passwds and the passwd + * might be expired. I think the way to go here + * is with PAM. + */ + strtok (found_passwd, ","); + + if (*found_passwd) + { + /* user exists and has a password */ + if (strcmp (found_passwd, crypt (password, found_passwd)) == 0) + return 1; + else + { +#ifdef LOG_AUTHPRIV + syslog (LOG_AUTHPRIV | LOG_NOTICE, + "password mismatch for %s: %s vs. %s", username, + crypt(password, found_passwd), found_passwd); +#endif + return 0; + } + } + +#ifdef LOG_AUTHPRIV + syslog (LOG_AUTHPRIV | LOG_NOTICE, + "user %s authenticated because of blank system password", + username); +#endif + return 1; +} + + + +/* Return a hosting username if password matches, else NULL. */ +static char * +check_password (char *username, char *password, char *repository) +{ + int rc; + char *host_user = NULL; + + /* First we see if this user has a password in the CVS-specific + password file. If so, that's enough to authenticate with. If + not, we'll check /etc/passwd or maybe whatever is configured via PAM. */ + + rc = check_repository_password (username, password, repository, + &host_user); + + if (rc == 2) + return NULL; + + if (rc == 1) + /* host_user already set by reference, so just return. */ + goto handle_return; + + assert (rc == 0); + + if (!config->system_auth) + { + /* Note that the message _does_ distinguish between the case in + which we check for a system password and the case in which + we do not. It is a real pain to track down why it isn't + letting you in if it won't say why, and I am not convinced + that the potential information disclosure to an attacker + outweighs this. */ + printf ("error 0 no such user %s in CVSROOT/passwd\n", username); + + exit (EXIT_FAILURE); + } + + /* No cvs password found, so try /etc/passwd. */ +#ifdef HAVE_PAM + if (check_pam_password (&username, password)) +#else /* !HAVE_PAM */ + if (check_system_password (username, password)) +#endif /* HAVE_PAM */ + host_user = xstrdup (username); + else + host_user = NULL; + +#ifdef LOG_AUTHPRIV + if (!host_user) + syslog (LOG_AUTHPRIV | LOG_NOTICE, + "login refused for %s: user has no password", username); +#endif + +handle_return: + if (host_user) + { + /* Set CVS_Username here, in allocated space. + It might or might not be the same as host_user. */ + CVS_Username = xmalloc (strlen (username) + 1); + strcpy (CVS_Username, username); + } + + return host_user; +} + +#endif /* AUTH_SERVER_SUPPORT */ + +#if defined (AUTH_SERVER_SUPPORT) || defined (HAVE_GSSAPI) + +static void +pserver_read_line (char **tmp, size_t *tmp_len) +{ + int status; + + /* Make sure the protocol starts off on the right foot... */ + status = buf_read_short_line (buf_from_net, tmp, tmp_len, PATH_MAX); + if (status == -1) + { +# ifdef HAVE_SYSLOG_H + syslog (LOG_DAEMON | LOG_NOTICE, + "unexpected EOF encountered during authentication"); +# endif /* HAVE_SYSLOG_H */ + error (1, 0, "unexpected EOF encountered during authentication"); + } + if (status == -2) + status = ENOMEM; + if (status != 0) + { +# ifdef HAVE_SYSLOG_H + syslog (LOG_DAEMON | LOG_NOTICE, + "error reading from net while validating pserver"); +# endif /* HAVE_SYSLOG_H */ + error (1, status, "error reading from net while validating pserver"); + } +} + +/* Read username and password from client (i.e., stdin). + If correct, then switch to run as that user and send an ACK to the + client via stdout, else send NACK and die. */ +void +pserver_authenticate_connection (void) +{ + char *tmp; +#ifdef AUTH_SERVER_SUPPORT + char *repository = NULL; + char *username = NULL; + char *password = NULL; + + char *host_user; + char *descrambled_password; +#endif /* AUTH_SERVER_SUPPORT */ + int verify_and_exit = 0; + + /* The Authentication Protocol. Client sends: + * + * BEGIN AUTH REQUEST\n + * <REPOSITORY>\n + * <USERNAME>\n + * <PASSWORD>\n + * END AUTH REQUEST\n + * + * Server uses above information to authenticate, then sends + * + * I LOVE YOU\n + * + * if it grants access, else + * + * I HATE YOU\n + * + * if it denies access (and it exits if denying). + * + * When the client is "cvs login", the user does not desire actual + * repository access, but would like to confirm the password with + * the server. In this case, the start and stop strings are + * + * BEGIN VERIFICATION REQUEST\n + * + * and + * + * END VERIFICATION REQUEST\n + * + * On a verification request, the server's responses are the same + * (with the obvious semantics), but it exits immediately after + * sending the response in both cases. + * + * Why is the repository sent? Well, note that the actual + * client/server protocol can't start up until authentication is + * successful. But in order to perform authentication, the server + * needs to look up the password in the special CVS passwd file, + * before trying /etc/passwd. So the client transmits the + * repository as part of the "authentication protocol". The + * repository will be redundantly retransmitted later, but that's no + * big deal. + */ + + /* Initialize buffers. */ + buf_to_net = fd_buffer_initialize (STDOUT_FILENO, 0, NULL, false, + outbuf_memory_error); + buf_from_net = fd_buffer_initialize (STDIN_FILENO, 0, NULL, true, + outbuf_memory_error); + +#ifdef SO_KEEPALIVE + /* Set SO_KEEPALIVE on the socket, so that we don't hang forever + if the client dies while we are waiting for input. */ + { + int on = 1; + + if (setsockopt (STDIN_FILENO, SOL_SOCKET, SO_KEEPALIVE, + &on, sizeof on) < 0) + { +# ifdef HAVE_SYSLOG_H + syslog (LOG_DAEMON | LOG_ERR, "error setting KEEPALIVE: %m"); +# endif /* HAVE_SYSLOG_H */ + } + } +#endif + + /* Make sure the protocol starts off on the right foot... */ + pserver_read_line (&tmp, NULL); + + if (strcmp (tmp, "BEGIN VERIFICATION REQUEST") == 0) + verify_and_exit = 1; + else if (strcmp (tmp, "BEGIN AUTH REQUEST") == 0) + ; + else if (strcmp (tmp, "BEGIN GSSAPI REQUEST") == 0) + { +#ifdef HAVE_GSSAPI + free (tmp); + gserver_authenticate_connection (); + return; +#else + error (1, 0, "GSSAPI authentication not supported by this server"); +#endif + } + else + error (1, 0, "bad auth protocol start: %s", tmp); + +#ifndef AUTH_SERVER_SUPPORT + + error (1, 0, "Password authentication not supported by this server"); + +#else /* AUTH_SERVER_SUPPORT */ + + free (tmp); + + /* Get the three important pieces of information in order. */ + /* See above comment about error handling. */ + pserver_read_line (&repository, NULL); + pserver_read_line (&username, NULL); + pserver_read_line (&password, NULL); + + /* ... and make sure the protocol ends on the right foot. */ + /* See above comment about error handling. */ + pserver_read_line (&tmp, NULL); + if (strcmp (tmp, + verify_and_exit ? + "END VERIFICATION REQUEST" : "END AUTH REQUEST") + != 0) + { + error (1, 0, "bad auth protocol end: %s", tmp); + } + free (tmp); + + if (!root_allow_ok (repository)) + { + error (1, 0, "%s: no such repository", repository); +# ifdef HAVE_SYSLOG_H + syslog (LOG_DAEMON | LOG_NOTICE, "login refused for %s", repository); +# endif /* HAVE_SYSLOG_H */ + goto i_hate_you; + } + + /* OK, now parse the config file, so we can use it to control how + to check passwords. If there was an error parsing the config + file, parse_config already printed an error. We keep going. + Why? Because if we didn't, then there would be no way to check + in a new CVSROOT/config file to fix the broken one! */ + config = get_root_allow_config (repository, gConfigPath); + + /* We need the real cleartext before we hash it. */ + descrambled_password = descramble (password); + host_user = check_password (username, descrambled_password, repository); + if (host_user == NULL) + { +# ifdef HAVE_SYSLOG_H + syslog (LOG_DAEMON | LOG_NOTICE, "login failure (for %s)", repository); +# endif /* HAVE_SYSLOG_H */ + memset (descrambled_password, 0, strlen (descrambled_password)); + free (descrambled_password); + i_hate_you: + buf_output0 (buf_to_net, "I HATE YOU\n"); + buf_flush (buf_to_net, true); + + /* Don't worry about server_cleanup, server_active isn't set + yet. */ + exit (EXIT_FAILURE); + } + memset (descrambled_password, 0, strlen (descrambled_password)); + free (descrambled_password); + + /* Don't go any farther if we're just responding to "cvs login". */ + if (verify_and_exit) + { + buf_output0 (buf_to_net, "I LOVE YOU\n"); + buf_flush (buf_to_net, true); + exit (EXIT_SUCCESS); + } + + /* Set Pserver_Repos so that we can check later that the same + repository is sent in later client/server protocol. */ + Pserver_Repos = xmalloc (strlen (repository) + 1); + strcpy (Pserver_Repos, repository); + + /* Switch to run as this user. */ + switch_to_user (username, host_user); + free (host_user); + free (repository); + free (username); + free (password); + + buf_output0 (buf_to_net, "I LOVE YOU\n"); + buf_flush (buf_to_net, true); +#endif /* AUTH_SERVER_SUPPORT */ +} + +#endif /* AUTH_SERVER_SUPPORT || HAVE_GSSAPI */ + + +#ifdef HAVE_KERBEROS +void +kserver_authenticate_connection( void ) +{ + int status; + char instance[INST_SZ]; + struct sockaddr_in peer; + struct sockaddr_in laddr; + int len; + KTEXT_ST ticket; + AUTH_DAT auth; + char version[KRB_SENDAUTH_VLEN]; + char user[ANAME_SZ]; + + strcpy (instance, "*"); + len = sizeof peer; + if (getpeername (STDIN_FILENO, (struct sockaddr *) &peer, &len) < 0 + || getsockname (STDIN_FILENO, (struct sockaddr *) &laddr, + &len) < 0) + { + printf ("E Fatal error, aborting.\n\ +error %s getpeername or getsockname failed\n", strerror (errno)); + + exit (EXIT_FAILURE); + } + +#ifdef SO_KEEPALIVE + /* Set SO_KEEPALIVE on the socket, so that we don't hang forever + if the client dies while we are waiting for input. */ + { + int on = 1; + + if (setsockopt (STDIN_FILENO, SOL_SOCKET, SO_KEEPALIVE, + (char *) &on, sizeof on) < 0) + { +# ifdef HAVE_SYSLOG_H + syslog (LOG_DAEMON | LOG_ERR, "error setting KEEPALIVE: %m"); +# endif /* HAVE_SYSLOG_H */ + } + } +#endif + + status = krb_recvauth (KOPT_DO_MUTUAL, STDIN_FILENO, &ticket, "rcmd", + instance, &peer, &laddr, &auth, "", sched, + version); + if (status != KSUCCESS) + { + printf ("E Fatal error, aborting.\n\ +error 0 kerberos: %s\n", krb_get_err_text(status)); + + exit (EXIT_FAILURE); + } + + memcpy (kblock, auth.session, sizeof (C_Block)); + + /* Get the local name. */ + status = krb_kntoln (&auth, user); + if (status != KSUCCESS) + { + printf ("E Fatal error, aborting.\n" + "error 0 kerberos: can't get local name: %s\n", + krb_get_err_text(status)); + + exit (EXIT_FAILURE); + } + + /* Switch to run as this user. */ + switch_to_user ("Kerberos 4", user); +} +#endif /* HAVE_KERBEROS */ + + + +# ifdef HAVE_GSSAPI /* && SERVER_SUPPORT */ +/* Authenticate a GSSAPI connection. This is called from + * pserver_authenticate_connection, and it handles success and failure + * the same way. + * + * GLOBALS + * server_hostname The name of this host, as set via a call to + * xgethostname() in main(). + */ +static void +gserver_authenticate_connection (void) +{ + char *hn; + gss_buffer_desc tok_in, tok_out; + char buf[1024]; + char *credbuf; + size_t credbuflen; + OM_uint32 stat_min, ret; + gss_name_t server_name, client_name; + gss_cred_id_t server_creds; + int nbytes; + gss_OID mechid; + + hn = canon_host (server_hostname); + if (!hn) + error (1, 0, "can't get canonical hostname for `%s': %s", + server_hostname, ch_strerror ()); + + sprintf (buf, "cvs@%s", hn); + free (hn); + tok_in.value = buf; + tok_in.length = strlen (buf); + + if (gss_import_name (&stat_min, &tok_in, GSS_C_NT_HOSTBASED_SERVICE, + &server_name) != GSS_S_COMPLETE) + error (1, 0, "could not import GSSAPI service name %s", buf); + + /* Acquire the server credential to verify the client's + authentication. */ + if (gss_acquire_cred (&stat_min, server_name, 0, GSS_C_NULL_OID_SET, + GSS_C_ACCEPT, &server_creds, + NULL, NULL) != GSS_S_COMPLETE) + error (1, 0, "could not acquire GSSAPI server credentials"); + + gss_release_name (&stat_min, &server_name); + + /* The client will send us a two byte length followed by that many + bytes. */ + if (fread (buf, 1, 2, stdin) != 2) + error (1, errno, "read of length failed"); + + nbytes = ((buf[0] & 0xff) << 8) | (buf[1] & 0xff); + if (nbytes <= sizeof buf) + { + credbuf = buf; + credbuflen = sizeof buf; + } + else + { + credbuflen = nbytes; + credbuf = xmalloc (credbuflen); + } + + if (fread (credbuf, 1, nbytes, stdin) != nbytes) + error (1, errno, "read of data failed"); + + gcontext = GSS_C_NO_CONTEXT; + tok_in.length = nbytes; + tok_in.value = credbuf; + + if (gss_accept_sec_context (&stat_min, + &gcontext, /* context_handle */ + server_creds, /* verifier_cred_handle */ + &tok_in, /* input_token */ + NULL, /* channel bindings */ + &client_name, /* src_name */ + &mechid, /* mech_type */ + &tok_out, /* output_token */ + &ret, + NULL, /* ignore time_rec */ + NULL) /* ignore del_cred_handle */ + != GSS_S_COMPLETE) + { + error (1, 0, "could not verify credentials"); + } + + /* FIXME: Use Kerberos v5 specific code to authenticate to a user. + We could instead use an authentication to access mapping. */ + { + krb5_context kc; + krb5_principal p; + gss_buffer_desc desc; + + krb5_init_context (&kc); + if (gss_display_name (&stat_min, client_name, &desc, + &mechid) != GSS_S_COMPLETE + || krb5_parse_name (kc, ((gss_buffer_t) &desc)->value, &p) != 0 + || krb5_aname_to_localname (kc, p, sizeof buf, buf) != 0 + || krb5_kuserok (kc, p, buf) != TRUE) + { + error (1, 0, "access denied"); + } + krb5_free_principal (kc, p); + krb5_free_context (kc); + } + + if (tok_out.length != 0) + { + char cbuf[2]; + + cbuf[0] = (tok_out.length >> 8) & 0xff; + cbuf[1] = tok_out.length & 0xff; + if (fwrite (cbuf, 1, 2, stdout) != 2 + || (fwrite (tok_out.value, 1, tok_out.length, stdout) + != tok_out.length)) + error (1, errno, "fwrite failed"); + } + + switch_to_user ("GSSAPI", buf); + + if (credbuf != buf) + free (credbuf); + + printf ("I LOVE YOU\n"); + fflush (stdout); +} + +# endif /* HAVE_GSSAPI */ + +#endif /* SERVER_SUPPORT */ + +#if defined (CLIENT_SUPPORT) || defined (SERVER_SUPPORT) + +/* This global variable is non-zero if the user requests encryption on + the command line. */ +int cvsencrypt; + +/* This global variable is non-zero if the users requests stream + authentication on the command line. */ +int cvsauthenticate; + +#ifdef ENCRYPTION + +#ifdef HAVE_KERBEROS + +/* An encryption interface using Kerberos. This is built on top of a + packetizing buffer. */ + +/* This structure is the closure field of the Kerberos translation + routines. */ +struct krb_encrypt_data +{ + /* The Kerberos key schedule. */ + Key_schedule sched; + /* The Kerberos DES block. */ + C_Block block; +}; + + + +/* Decrypt Kerberos data. */ +static int +krb_encrypt_input( void *fnclosure, const char *input, char *output, int size ) +{ + struct krb_encrypt_data *kd = (struct krb_encrypt_data *) fnclosure; + int tcount; + + des_cbc_encrypt ((C_Block *) input, (C_Block *) output, + size, kd->sched, &kd->block, 0); + + /* SIZE is the size of the buffer, which is set by the encryption + routine. The packetizing buffer will arrange for the first two + bytes in the decrypted buffer to be the real (unaligned) + length. As a safety check, make sure that the length in the + buffer corresponds to SIZE. Note that the length in the buffer + is just the length of the data. We must add 2 to account for + the buffer count itself. */ + tcount = ((output[0] & 0xff) << 8) + (output[1] & 0xff); + if (((tcount + 2 + 7) & ~7) != size) + error (1, 0, "Decryption failure"); + + return 0; +} + + + +/* Encrypt Kerberos data. */ +static int +krb_encrypt_output( void *fnclosure, const char *input, char *output, + int size, int *translated ) +{ + struct krb_encrypt_data *kd = (struct krb_encrypt_data *) fnclosure; + int aligned; + + /* For security against a known plaintext attack, we should + initialize any padding bytes to random values. Instead, we + just pick up whatever is on the stack, which is at least better + than using zero. */ + + /* Align SIZE to an 8 byte boundary. Note that SIZE includes the + two byte buffer count at the start of INPUT which was added by + the packetizing buffer. */ + aligned = (size + 7) & ~7; + + /* We use des_cbc_encrypt rather than krb_mk_priv because the + latter sticks a timestamp in the block, and krb_rd_priv expects + that timestamp to be within five minutes of the current time. + Given the way the CVS server buffers up data, that can easily + fail over a long network connection. We trust krb_recvauth to + guard against a replay attack. */ + + des_cbc_encrypt ((C_Block *) input, (C_Block *) output, aligned, + kd->sched, &kd->block, 1); + + *translated = aligned; + + return 0; +} + + + +/* Create a Kerberos encryption buffer. We use a packetizing buffer + with Kerberos encryption translation routines. */ +struct buffer * +krb_encrypt_buffer_initialize( struct buffer *buf, int input, + Key_schedule sched, C_Block block, + void *memory( struct buffer * ) ) +{ + struct krb_encrypt_data *kd; + + kd = (struct krb_encrypt_data *) xmalloc (sizeof *kd); + memcpy (kd->sched, sched, sizeof (Key_schedule)); + memcpy (kd->block, block, sizeof (C_Block)); + + return packetizing_buffer_initialize (buf, + input ? krb_encrypt_input : NULL, + input ? NULL : krb_encrypt_output, + kd, + memory); +} + +#endif /* HAVE_KERBEROS */ +#endif /* ENCRYPTION */ +#endif /* defined (CLIENT_SUPPORT) || defined (SERVER_SUPPORT) */ + + + +/* Output LEN bytes at STR. If LEN is zero, then output up to (not including) + the first '\0' byte. */ +void +cvs_output (const char *str, size_t len) +{ + if (len == 0) + len = strlen (str); +#ifdef SERVER_SUPPORT + if (error_use_protocol) + { + if (buf_to_net) + { + buf_output (saved_output, str, len); + buf_copy_lines (buf_to_net, saved_output, 'M'); + } +# if HAVE_SYSLOG_H + else + syslog (LOG_DAEMON | LOG_ERR, + "Attempt to write message after close of network buffer. " + "Message was: %s", + str); +# endif /* HAVE_SYSLOG_H */ + } + else if (server_active) + { + if (protocol) + { + buf_output (saved_output, str, len); + buf_copy_lines (protocol, saved_output, 'M'); + buf_send_counted (protocol); + } +# if HAVE_SYSLOG_H + else + syslog (LOG_DAEMON | LOG_ERR, + "Attempt to write message before initialization of " + "protocol buffer. Message was: %s", + str); +# endif /* HAVE_SYSLOG_H */ + } + else +#endif + { + size_t written; + size_t to_write = len; + const char *p = str; + + /* Local users that do 'cvs status 2>&1' on a local repository + may see the informational messages out-of-order with the + status messages unless we use the fflush (stderr) here. */ + fflush (stderr); + + while (to_write > 0) + { + written = fwrite (p, 1, to_write, stdout); + if (written == 0) + break; + p += written; + to_write -= written; + } + } +} + +/* Output LEN bytes at STR in binary mode. If LEN is zero, then + output zero bytes. */ + +void +cvs_output_binary (char *str, size_t len) +{ +#ifdef SERVER_SUPPORT + if (error_use_protocol || server_active) + { + struct buffer *buf; + char size_text[40]; + + if (error_use_protocol) + buf = buf_to_net; + else + buf = protocol; + + assert (buf); + + if (!supported_response ("Mbinary")) + { + error (0, 0, "\ +this client does not support writing binary files to stdout"); + return; + } + + buf_output0 (buf, "Mbinary\012"); + sprintf (size_text, "%lu\012", (unsigned long) len); + buf_output0 (buf, size_text); + + /* Not sure what would be involved in using buf_append_data here + without stepping on the toes of our caller (which is responsible + for the memory allocation of STR). */ + buf_output (buf, str, len); + + if (!error_use_protocol) + buf_send_counted (protocol); + } + else +#endif + { + size_t written; + size_t to_write = len; + const char *p = str; +#ifdef USE_SETMODE_STDOUT + int oldmode; +#endif + + /* Local users that do 'cvs status 2>&1' on a local repository + may see the informational messages out-of-order with the + status messages unless we use the fflush (stderr) here. */ + fflush (stderr); + +#ifdef USE_SETMODE_STDOUT + /* It is possible that this should be the same ifdef as + USE_SETMODE_BINARY but at least for the moment we keep them + separate. Mostly this is just laziness and/or a question + of what has been tested where. Also there might be an + issue of setmode vs. _setmode. */ + /* The Windows doc says to call setmode only right after startup. + I assume that what they are talking about can also be helped + by flushing the stream before changing the mode. */ + fflush (stdout); + oldmode = _setmode (_fileno (stdout), OPEN_BINARY); + if (oldmode < 0) + error (0, errno, "failed to setmode on stdout"); +#endif + + while (to_write > 0) + { + written = fwrite (p, 1, to_write, stdout); + if (written == 0) + break; + p += written; + to_write -= written; + } +#ifdef USE_SETMODE_STDOUT + fflush (stdout); + if (_setmode (_fileno (stdout), oldmode) != OPEN_BINARY) + error (0, errno, "failed to setmode on stdout"); +#endif + } +} + + + +/* Like CVS_OUTPUT but output is for stderr not stdout. */ +void +cvs_outerr (const char *str, size_t len) +{ + if (len == 0) + len = strlen (str); +#ifdef SERVER_SUPPORT + if (error_use_protocol) + { + if (buf_to_net) + { + buf_output (saved_outerr, str, len); + buf_copy_lines (buf_to_net, saved_outerr, 'E'); + } +# if HAVE_SYSLOG_H + else + syslog (LOG_DAEMON | LOG_ERR, + "Attempt to write error message after close of network " + "buffer. Message was: `%s'", + str); +# endif /* HAVE_SYSLOG_H */ + } + else if (server_active) + { + if (protocol) + { + buf_output (saved_outerr, str, len); + buf_copy_lines (protocol, saved_outerr, 'E'); + buf_send_counted (protocol); + } +# if HAVE_SYSLOG_H + else + syslog (LOG_DAEMON | LOG_ERR, + "Attempt to write error message before initialization of " + "protocol buffer. Message was: `%s'", + str); +# endif /* HAVE_SYSLOG_H */ + } + else +#endif + { + size_t written; + size_t to_write = len; + const char *p = str; + + /* Make sure that output appears in order if stdout and stderr + point to the same place. For the server case this is taken + care of by the fact that saved_outerr always holds less + than a line. */ + fflush (stdout); + + while (to_write > 0) + { + written = fwrite (p, 1, to_write, stderr); + if (written == 0) + break; + p += written; + to_write -= written; + } + } +} + + + +/* Flush stderr. stderr is normally flushed automatically, of course, + but this function is used to flush information from the server back + to the client. */ +void +cvs_flusherr (void) +{ +#ifdef SERVER_SUPPORT + if (error_use_protocol) + { + /* skip the actual stderr flush in this case since the parent process + * on the server should only be writing to stdout anyhow + */ + /* Flush what we can to the network, but don't block. */ + buf_flush (buf_to_net, 0); + } + else if (server_active) + { + /* make sure stderr is flushed before we send the flush count on the + * protocol pipe + */ + fflush (stderr); + /* Send a special count to tell the parent to flush. */ + buf_send_special_count (protocol, -2); + } + else +#endif + fflush (stderr); +} + + + +/* Make it possible for the user to see what has been written to + stdout (it is up to the implementation to decide exactly how far it + should go to ensure this). */ +void +cvs_flushout (void) +{ +#ifdef SERVER_SUPPORT + if (error_use_protocol) + { + /* Flush what we can to the network, but don't block. */ + buf_flush (buf_to_net, 0); + } + else if (server_active) + { + /* Just do nothing. This is because the code which + cvs_flushout replaces, setting stdout to line buffering in + main.c, didn't get called in the server child process. But + in the future it is quite plausible that we'll want to make + this case work analogously to cvs_flusherr. + + FIXME - DRP - I tried to implement this and triggered the following + error: "Protocol error: uncounted data discarded". I don't need + this feature right now, so I'm not going to bother with it yet. + */ + buf_send_special_count (protocol, -1); + } + else +#endif + fflush (stdout); +} + + + +/* Output TEXT, tagging it according to TAG. There are lots more + details about what TAG means in cvsclient.texi but for the simple + case (e.g. non-client/server), TAG is just "newline" to output a + newline (in which case TEXT must be NULL), and any other tag to + output normal text. + + Note that there is no way to output either \0 or \n as part of TEXT. */ + +void +cvs_output_tagged (const char *tag, const char *text) +{ + if (text != NULL && strchr (text, '\n') != NULL) + /* Uh oh. The protocol has no way to cope with this. For now + we dump core, although that really isn't such a nice + response given that this probably can be caused by newlines + in filenames and other causes other than bugs in CVS. Note + that we don't want to turn this into "MT newline" because + this case is a newline within a tagged item, not a newline + as extraneous sugar for the user. */ + assert (0); + + /* Start and end tags don't take any text, per cvsclient.texi. */ + if (tag[0] == '+' || tag[0] == '-') + assert (text == NULL); + +#ifdef SERVER_SUPPORT + if (server_active && supported_response ("MT")) + { + struct buffer *buf; + + if (error_use_protocol) + buf = buf_to_net; + else + buf = protocol; + + buf_output0 (buf, "MT "); + buf_output0 (buf, tag); + if (text != NULL) + { + buf_output (buf, " ", 1); + buf_output0 (buf, text); + } + buf_output (buf, "\n", 1); + + if (!error_use_protocol) + buf_send_counted (protocol); + } + else +#endif /* SERVER_SUPPORT */ + { + /* No MT support or we are using a local repository. */ + if (strcmp (tag, "newline") == 0) + cvs_output ("\n", 1); + else if (strcmp (tag, "date") == 0) + { +#ifdef SERVER_SUPPORT + if (server_active) + /* Output UTC when running as a server without MT support in + * the client since it is likely to be more meaningful than + * localtime. + */ + cvs_output (text, 0); + else +#endif /* SERVER_SUPPORT */ + { + char *date_in = xstrdup (text); + char *date = format_date_alloc (date_in); + cvs_output (date, 0); + free (date); + free (date_in); + } + } + else if (text != NULL) + cvs_output (text, 0); + } +} + + + +/* + * void cvs_trace(int level, const char *fmt, ...) + * + * Print tracing information to stderr on request. Levels are implemented + * as with CVSNT. + */ +void +cvs_trace (int level, const char *fmt, ...) +{ + if (trace >= level) + { + va_list va; + + va_start (va, fmt); +#ifdef SERVER_SUPPORT + fprintf (stderr,"%c -> ",server_active?(isProxyServer()?'P':'S'):' '); +#else /* ! SERVER_SUPPORT */ + fprintf (stderr," -> "); +#endif + vfprintf (stderr, fmt, va); + fprintf (stderr,"\n"); + va_end (va); + } +} |