summaryrefslogtreecommitdiffstats
path: root/contrib/cvs/src/server.c
diff options
context:
space:
mode:
Diffstat (limited to 'contrib/cvs/src/server.c')
-rw-r--r--contrib/cvs/src/server.c6760
1 files changed, 6760 insertions, 0 deletions
diff --git a/contrib/cvs/src/server.c b/contrib/cvs/src/server.c
new file mode 100644
index 0000000..cace89d
--- /dev/null
+++ b/contrib/cvs/src/server.c
@@ -0,0 +1,6760 @@
+/* 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. */
+
+/*
+ * $FreeBSD$
+ */
+
+#include <assert.h>
+#include "cvs.h"
+#include "watch.h"
+#include "edit.h"
+#include "fileattr.h"
+#include "getline.h"
+#include "buffer.h"
+
+int server_active = 0;
+
+#if defined(SERVER_SUPPORT) || defined(CLIENT_SUPPORT)
+# ifdef HAVE_GSSAPI
+/* 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.
+ */
+# include <netdb.h>
+# include "xgssapi.h"
+/* We use Kerberos 5 routines to map the GSSAPI credential to a user
+ name. */
+# include <krb5.h>
+
+/* We need this to wrap data. */
+static gss_ctx_id_t gcontext;
+
+static void gserver_authenticate_connection PROTO((void));
+
+/* Whether we are already wrapping GSSAPI communication. */
+static int cvs_gssapi_wrapping;
+
+# ifdef ENCRYPTION
+/* Whether to encrypt GSSAPI communication. We use a global variable
+ like this because we use the same buffer type (gssapi_wrap) to
+ handle both authentication and encryption, and we don't want
+ multiple instances of that buffer in the communication stream. */
+int cvs_gssapi_encrypt;
+# endif
+# endif /* HAVE_GSSAPI */
+#endif /* defined(SERVER_SUPPORT) || defined(CLIENT_SUPPORT) */
+
+#ifdef SERVER_SUPPORT
+
+#ifdef HAVE_WINSOCK_H
+#include <winsock.h>
+#endif
+
+#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
+
+#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
+
+/* for select */
+#include "xselect.h"
+
+#ifndef O_NONBLOCK
+#define O_NONBLOCK O_NDELAY
+#endif
+
+/* EWOULDBLOCK is not defined by POSIX, but some BSD systems will
+ return it, rather than EAGAIN, for nonblocking writes. */
+#ifdef EWOULDBLOCK
+#define blocking_error(err) ((err) == EWOULDBLOCK || (err) == EAGAIN)
+#else
+#define blocking_error(err) ((err) == EAGAIN)
+#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;
+
+/* Should we check for system usernames/passwords? Can be changed by
+ CVSROOT/config. */
+int system_auth = 1;
+
+# endif /* AUTH_SERVER_SUPPORT */
+
+
+/* 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;
+
+/*
+ * 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 PROTO((void));
+
+/* All server communication goes through buffer structures. Most of
+ the buffers are built on top of a file descriptor. This structure
+ is used as the closure field in a buffer. */
+
+struct fd_buffer
+{
+ /* The file descriptor. */
+ int fd;
+ /* Nonzero if the file descriptor is in blocking mode. */
+ int blocking;
+};
+
+static struct buffer *fd_buffer_initialize
+ PROTO ((int, int, void (*) (struct buffer *)));
+static int fd_buffer_input PROTO((void *, char *, int, int, int *));
+static int fd_buffer_output PROTO((void *, const char *, int, int *));
+static int fd_buffer_flush PROTO((void *));
+static int fd_buffer_block PROTO((void *, int));
+static int fd_buffer_shutdown PROTO((struct buffer *));
+
+/* Initialize a buffer built on a file descriptor. FD is the file
+ descriptor. INPUT is nonzero if this is for input, zero if this is
+ for output. MEMORY is the function to call when a memory error
+ occurs. */
+
+static struct buffer *
+fd_buffer_initialize (fd, input, memory)
+ int fd;
+ int input;
+ void (*memory) PROTO((struct buffer *));
+{
+ struct fd_buffer *n;
+
+ n = (struct fd_buffer *) xmalloc (sizeof *n);
+ n->fd = fd;
+ n->blocking = 1;
+ return buf_initialize (input ? fd_buffer_input : NULL,
+ input ? NULL : fd_buffer_output,
+ input ? NULL : fd_buffer_flush,
+ fd_buffer_block,
+ fd_buffer_shutdown,
+ memory,
+ n);
+}
+
+/* The buffer input function for a buffer built on a file descriptor. */
+
+static int
+fd_buffer_input (closure, data, need, size, got)
+ void *closure;
+ char *data;
+ int need;
+ int size;
+ int *got;
+{
+ struct fd_buffer *fd = (struct fd_buffer *) closure;
+ int nbytes;
+
+ if (! fd->blocking)
+ nbytes = read (fd->fd, data, size);
+ else
+ {
+ /* This case is not efficient. Fortunately, I don't think it
+ ever actually happens. */
+ nbytes = read (fd->fd, data, need == 0 ? 1 : need);
+ }
+
+ if (nbytes > 0)
+ {
+ *got = nbytes;
+ return 0;
+ }
+
+ *got = 0;
+
+ if (nbytes == 0)
+ {
+ /* End of file. This assumes that we are using POSIX or BSD
+ style nonblocking I/O. On System V we will get a zero
+ return if there is no data, even when not at EOF. */
+ return -1;
+ }
+
+ /* Some error occurred. */
+
+ if (blocking_error (errno))
+ {
+ /* Everything's fine, we just didn't get any data. */
+ return 0;
+ }
+
+ return errno;
+}
+
+/* The buffer output function for a buffer built on a file descriptor. */
+
+static int
+fd_buffer_output (closure, data, have, wrote)
+ void *closure;
+ const char *data;
+ int have;
+ int *wrote;
+{
+ struct fd_buffer *fd = (struct fd_buffer *) closure;
+
+ *wrote = 0;
+
+ while (have > 0)
+ {
+ int nbytes;
+
+ nbytes = write (fd->fd, data, have);
+
+ if (nbytes <= 0)
+ {
+ if (! fd->blocking
+ && (nbytes == 0 || blocking_error (errno)))
+ {
+ /* A nonblocking write failed to write any data. Just
+ return. */
+ return 0;
+ }
+
+ /* Some sort of error occurred. */
+
+ if (nbytes == 0)
+ return EIO;
+
+ return errno;
+ }
+
+ *wrote += nbytes;
+ data += nbytes;
+ have -= nbytes;
+ }
+
+ return 0;
+}
+
+/* The buffer flush function for a buffer built on a file descriptor. */
+
+/*ARGSUSED*/
+static int
+fd_buffer_flush (closure)
+ void *closure;
+{
+ /* Nothing to do. File descriptors are always flushed. */
+ return 0;
+}
+
+/* The buffer block function for a buffer built on a file descriptor. */
+
+static int
+fd_buffer_block (closure, block)
+ void *closure;
+ int block;
+{
+ struct fd_buffer *fd = (struct fd_buffer *) closure;
+ int flags;
+
+ flags = fcntl (fd->fd, F_GETFL, 0);
+ if (flags < 0)
+ return errno;
+
+ if (block)
+ flags &= ~O_NONBLOCK;
+ else
+ flags |= O_NONBLOCK;
+
+ if (fcntl (fd->fd, F_SETFL, flags) < 0)
+ return errno;
+
+ fd->blocking = block;
+
+ return 0;
+}
+
+/* The buffer shutdown function for a buffer built on a file descriptor. */
+
+static int
+fd_buffer_shutdown (buf)
+ struct buffer *buf;
+{
+ free (buf->closure);
+ buf->closure = NULL;
+ return 0;
+}
+
+/* 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 PROTO((char *, char *));
+
+static int
+create_adm_p (base_dir, dir)
+ 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 ((List *) 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 PROTO((char *));
+
+static int
+mkdir_p (dir)
+ 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 (status)
+ 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;
+
+/* If an error is pending, print it and return 1. If not, return 0.
+ Must be called only in contexts where it is OK to send output. */
+static int
+print_pending_error ()
+{
+ 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;
+ }
+ else if (pending_error)
+ {
+ print_error (pending_error);
+ pending_error = 0;
+ return 1;
+ }
+ else
+ return 0;
+}
+
+/* Is an error pending? */
+#define error_pending() (pending_error || pending_error_text)
+
+static int alloc_pending PROTO ((size_t size));
+
+/* Allocate SIZE bytes for pending_error_text and return nonzero
+ if we could do it. */
+static int
+alloc_pending (size)
+ 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;
+ pending_error_text = xmalloc (size);
+ if (pending_error_text == NULL)
+ {
+ pending_error = ENOMEM;
+ return 0;
+ }
+ return 1;
+}
+
+static void serve_is_modified PROTO ((char *));
+
+static int supported_response PROTO ((char *));
+
+static int
+supported_response (name)
+ 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;
+}
+
+static void
+serve_valid_responses (arg)
+ char *arg;
+{
+ char *p = arg;
+ char *q;
+ struct response *rs;
+ 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);
+
+ error_exit ();
+ }
+ else if (rs->status == rs_optional)
+ rs->status = rs_not_supported;
+ }
+}
+
+static void
+serve_root (arg)
+ char *arg;
+{
+ char *env;
+ char *path;
+
+ if (error_pending()) 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 illegal.
+
+ 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;
+ }
+
+ /* We need to check :ext: server here, :pserver: checks happen below. */
+ if (root_allow_used() && !root_allow_ok (arg)
+# ifdef AUTH_SERVER_SUPPORT
+ && Pserver_Repos == NULL
+# endif
+ )
+ {
+ if (alloc_pending (80 + strlen (arg)))
+ sprintf (pending_error_text,
+ "E Bad root %s", arg);
+ return;
+ }
+
+#ifdef AUTH_SERVER_SUPPORT
+ 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: Root says \"%s\" but pserver says \"%s\"",
+ arg, Pserver_Repos);
+ return;
+ }
+ }
+#endif
+
+ current_parsed_root = local_cvsroot (arg);
+
+ /* For pserver, this will already have happened, and the call will do
+ nothing. But for rsh, we need to do it now. */
+ parse_config (current_parsed_root->directory);
+
+ /* Now is a good time to read CVSROOT/options too. */
+ parseopts(current_parsed_root->directory);
+
+ 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);
+
+#ifdef HAVE_PUTENV
+ env = xmalloc (strlen (CVSROOT_ENV) + strlen (current_parsed_root->directory) + 2);
+ if (env == NULL)
+ {
+ pending_error = ENOMEM;
+ return;
+ }
+ (void) sprintf (env, "%s=%s", CVSROOT_ENV, current_parsed_root->directory);
+ (void) putenv (env);
+ /* do not free env, as putenv has control of it */
+#endif
+}
+
+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 (path)
+ char *path;
+{
+ /* 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 pathname `%s' illegal for server", 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);
+ }
+}
+
+static int outside_root PROTO ((char *));
+
+/* 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 (repos)
+ 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;
+}
+
+static int outside_dir PROTO ((char *));
+
+/* 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 (file)
+ 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 (arg)
+ char *arg;
+{
+ int lim = atoi (arg);
+ int i;
+ char *p;
+
+ 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 *dir_name;
+
+static void
+dirswitch (dir, repos)
+ char *dir;
+ char *repos;
+{
+ int status;
+ FILE *f;
+ size_t dir_len;
+
+ 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 pathname `%s' illegal for server", 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 ISDIRSEP 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 (dir_name != NULL)
+ free (dir_name);
+
+ dir_name = xmalloc (strlen (server_temp_dir) + dir_len + 40);
+ if (dir_name == NULL)
+ {
+ pending_error = ENOMEM;
+ return;
+ }
+
+ strcpy (dir_name, server_temp_dir);
+ strcat (dir_name, "/");
+ strcat (dir_name, dir);
+
+ status = mkdir_p (dir_name);
+ if (status != 0
+ && status != EEXIST)
+ {
+ if (alloc_pending (80 + strlen (dir_name)))
+ sprintf (pending_error_text, "E cannot mkdir %s", dir_name);
+ 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 (dir_name)))
+ sprintf (pending_error_text, "E cannot create_adm_p %s", dir_name);
+ pending_error = status;
+ return;
+ }
+
+ if ( CVS_CHDIR (dir_name) < 0)
+ {
+ int save_errno = errno;
+ if (alloc_pending (80 + strlen (dir_name)))
+ sprintf (pending_error_text, "E cannot change to %s", dir_name);
+ 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 (dir_name) + strlen (CVSADM)))
+ sprintf (pending_error_text,
+ "E cannot mkdir %s/%s", dir_name, 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 (dir_name) + strlen (CVSADM_REP)))
+ sprintf (pending_error_text,
+ "E cannot open %s/%s", dir_name, CVSADM_REP);
+ pending_error = save_errno;
+ return;
+ }
+ if (fprintf (f, "%s", repos) < 0)
+ {
+ int save_errno = errno;
+ if (alloc_pending (80 + strlen (dir_name) + strlen (CVSADM_REP)))
+ sprintf (pending_error_text,
+ "E error writing %s/%s", dir_name, 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 (dir_name) + strlen (CVSADM_REP)))
+ sprintf (pending_error_text,
+ "E error writing %s/%s", dir_name, CVSADM_REP);
+ pending_error = save_errno;
+ fclose (f);
+ return;
+ }
+ }
+ if (fprintf (f, "\n") < 0)
+ {
+ int save_errno = errno;
+ if (alloc_pending (80 + strlen (dir_name) + strlen (CVSADM_REP)))
+ sprintf (pending_error_text,
+ "E error writing %s/%s", dir_name, CVSADM_REP);
+ pending_error = save_errno;
+ fclose (f);
+ return;
+ }
+ if (fclose (f) == EOF)
+ {
+ int save_errno = errno;
+ if (alloc_pending (80 + strlen (dir_name) + strlen (CVSADM_REP)))
+ sprintf (pending_error_text,
+ "E error closing %s/%s", dir_name, 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 (arg)
+ char *arg;
+{
+ if (alloc_pending (80))
+ strcpy (pending_error_text,
+ "E Repository request is obsolete; aborted");
+ return;
+}
+
+static void
+serve_directory (arg)
+ char *arg;
+{
+ int status;
+ char *repos;
+
+ status = buf_read_line (buf_from_net, &repos, (int *) NULL);
+ if (status == 0)
+ {
+ if (!outside_root (repos))
+ dirswitch (arg, repos);
+ free (repos);
+ }
+ else 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;
+ }
+ }
+}
+
+static void
+serve_static_directory (arg)
+ char *arg;
+{
+ FILE *f;
+
+ if (error_pending ()) 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 (arg)
+ char *arg;
+{
+ FILE *f;
+
+ if (error_pending ()) 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;
+ (void) fclose (f);
+ 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, file)
+ int size;
+ int file;
+{
+ while (size > 0)
+ {
+ int status, 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)
+ {
+ int 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, 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, file, gzipped)
+ int 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. */
+
+ int 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, 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;
+
+static void serve_modified PROTO ((char *));
+
+static void
+serve_modified (arg)
+ char *arg;
+{
+ int size, 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, (int *) 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, (int *) 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;
+ size = atoi (size_text + 1);
+ }
+ else
+ size = atoi (size_text);
+ free (size_text);
+
+ if (error_pending ())
+ {
+ /* Now that we know the size, read and discard the file data. */
+ while (size > 0)
+ {
+ int status, nread;
+ char *data;
+
+ status = buf_read_data (buf_from_net, size, &data, &nread);
+ if (status != 0)
+ return;
+ size -= nread;
+ }
+ free (mode_text);
+ return;
+ }
+
+ if (outside_dir (arg))
+ {
+ free (mode_text);
+ return;
+ }
+
+ if (size >= 0)
+ {
+ receive_file (size, arg, gzipped);
+ if (error_pending ())
+ {
+ free (mode_text);
+ return;
+ }
+ }
+
+ 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 (arg)
+ char *arg;
+{
+}
+
+struct an_entry {
+ struct an_entry *next;
+ char *entry;
+};
+
+static struct an_entry *entries;
+
+static void serve_unchanged PROTO ((char *));
+
+static void
+serve_unchanged (arg)
+ char *arg;
+{
+ struct an_entry *p;
+ char *name;
+ char *cp;
+ char *timefield;
+
+ if (error_pending ()) 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;
+ }
+ }
+ /* 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 WinCVS & TortoiseCVS rely on
+ * this behavior.
+ */
+ if (*timefield != '+')
+ /* Skip this for entries with conflict markers. */
+ *timefield = '=';
+ break;
+ }
+ }
+}
+
+static void
+serve_is_modified (arg)
+ 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 ()) 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;
+ }
+ }
+ /* 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 WinCVS & TortoiseCVS rely on
+ * this behavior.
+ */
+ if (*timefield != '+')
+ /* Skip this for entries with conflict markers. */
+ *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 = (struct an_entry *) 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_entry PROTO ((char *));
+
+static void
+serve_entry (arg)
+ char *arg;
+{
+ struct an_entry *p;
+ char *cp;
+ int i = 0;
+ if (error_pending()) 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 PROTO ((char *));
+
+static void
+serve_kopt (arg)
+ char *arg;
+{
+ if (error_pending ())
+ 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 PROTO ((char *));
+
+static void
+serve_checkin_time (arg)
+ char *arg;
+{
+ if (error_pending ())
+ 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;
+ }
+
+ checkin_time = get_date (arg, NULL);
+ if (checkin_time == (time_t)-1)
+ {
+ if (alloc_pending (80 + strlen (arg)))
+ sprintf (pending_error_text, "E cannot parse date %s", arg);
+ return;
+ }
+ checkin_time_valid = 1;
+}
+
+static void
+server_write_entries ()
+{
+ 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;
+ }
+}
+
+struct notify_note {
+ /* Directory in which this notification happens. xmalloc'd*/
+ char *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 PROTO ((char *));
+
+static void
+serve_notify (arg)
+ char *arg;
+{
+ struct notify_note *new = NULL;
+ char *data = NULL;
+ int status;
+
+ if (error_pending ()) return;
+
+ if (outside_dir (arg))
+ return;
+
+ if (dir_name == NULL)
+ goto error;
+
+ new = (struct notify_note *) xmalloc (sizeof (struct notify_note));
+ if (new == NULL)
+ {
+ pending_error = ENOMEM;
+ return;
+ }
+ new->dir = xmalloc (strlen (dir_name) + 1);
+ new->filename = xmalloc (strlen (arg) + 1);
+ if (new->dir == NULL || new->filename == NULL)
+ {
+ pending_error = ENOMEM;
+ if (new->dir != NULL)
+ free (new->dir);
+ free (new);
+ return;
+ }
+ strcpy (new->dir, dir_name);
+ strcpy (new->filename, arg);
+
+ status = buf_read_line (buf_from_net, &data, (int *) 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->dir);
+ free (new);
+ }
+ return;
+}
+
+/* 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 ()
+{
+ struct notify_note *p;
+ char *repos;
+
+ 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, 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;
+}
+
+static int argument_count;
+static char **argument_vector;
+static int argument_vector_size;
+
+static void
+serve_argument (arg)
+ 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 =
+ (char **) xrealloc ((char *)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;
+}
+
+static void
+serve_argumentx (arg)
+ 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 (arg)
+ char *arg;
+{
+ 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 = 1;
+ break;
+ default:
+ goto error_return;
+ }
+}
+
+static void
+serve_set (arg)
+ char *arg;
+{
+ /* 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 (arg)
+ char *arg;
+{
+ /* 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 (arg)
+ char *arg;
+{
+ 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 (arg)
+ char *arg;
+{
+ 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 PROTO((char *));
+
+static void
+serve_questionable (arg)
+ char *arg;
+{
+ static int initted;
+
+ if (!initted)
+ {
+ /* Pick up ignores from CVSROOTADM_IGNORE, $HOME/.cvsignore on server,
+ and CVSIGNORE on server. */
+ ign_setup ();
+ initted = 1;
+ }
+
+ if (dir_name == NULL)
+ {
+ buf_output0 (buf_to_net, "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 = dir_name + 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;
+
+/* 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 (buf)
+ struct buffer *buf;
+{
+ error (1, ENOMEM, "Virtual memory exhausted");
+}
+
+/*
+ * Process IDs of the subprocess, or negative if that subprocess
+ * does not exist.
+ */
+static pid_t command_pid;
+
+static void
+outbuf_memory_error (buf)
+ 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 legal, 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
+ error_exit ();
+}
+
+static void
+input_memory_error (buf)
+ struct buffer *buf;
+{
+ outbuf_memory_error (buf);
+}
+
+
+
+/* If command is legal, return 1.
+ * Else if command is illegal and croak_on_illegal is set, then die.
+ * Else just return 0 to indicate that command is illegal.
+ */
+static int
+check_command_legal_p (cmd_name)
+ char *cmd_name;
+{
+ /* Right now, only pserver notices illegal 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 1;
+
+ 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 0;
+ }
+ }
+ 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_illegal;
+ }
+ 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 1;
+ }
+ 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 0;
+ }
+ }
+
+ 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 1;
+ }
+ else /* writers file exists, but this user not listed in it */
+ {
+ handle_illegal:
+ if (fclose (fp) < 0)
+ error (0, errno, "cannot close %s", fname);
+ if (linebuf)
+ free (linebuf);
+ free (fname);
+ return 0;
+ }
+ }
+#endif /* AUTH_SERVER_SUPPORT */
+
+ /* If ever reach end of this function, command must be legal. */
+ return 1;
+}
+
+
+
+/* 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 (fd)
+ int fd;
+{
+ int flags;
+
+ flags = fcntl (fd, F_GETFL, 0);
+ if (flags < 0)
+ return errno;
+ if (fcntl (fd, F_SETFL, flags | O_NONBLOCK) < 0)
+ return errno;
+ return 0;
+}
+
+
+
+/*
+ * Set buffer FD to blocking I/O. Returns 0 for success or errno code.
+ */
+int
+set_block_fd (fd)
+ int fd;
+{
+ int flags;
+
+ flags = fcntl (fd, F_GETFL, 0);
+ if (flags < 0)
+ return errno;
+ if (fcntl (fd, F_SETFL, flags & ~O_NONBLOCK) < 0)
+ return errno;
+ return 0;
+}
+
+
+
+static void
+do_cvs_command (cmd_name, command)
+ char *cmd_name;
+ int (*command) PROTO((int argc, char **argv));
+{
+ /*
+ * 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 = 0;
+
+ 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 legal for
+ this user. */
+ if (!check_command_legal_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,
+ 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"));
+ sleep (secs);
+ }
+
+ 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;
+ set_block_fd (flowcontrol_pipe[0]);
+ while (read (flowcontrol_pipe[0], &junk, 1) > 0);
+ }
+ /* FIXME: No point in printing an error message with error(),
+ * as STDERR is already closed, but perhaps this could be syslogged?
+ */
+#endif
+
+ rcs_cleanup ();
+ Lock_Cleanup ();
+ /* Don't call server_cleanup - the parent will handle that. */
+#ifdef SYSTEM_CLEANUP
+ /* Hook for OS-specific behavior, for example socket subsystems on
+ NT and OS2 or dealing with windows and arguments on Mac. */
+ SYSTEM_CLEANUP ();
+#endif
+ exit (exitstatus);
+ }
+
+ /* OK, sit around getting all the input from the child. */
+ {
+ struct buffer *stdoutbuf = NULL;
+ struct buffer *stderrbuf = NULL;
+ struct buffer *protocol_inbuf = NULL;
+ int err_exit = 0;
+ /* 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);
+ if (stderr_pipe[0] > num_to_check)
+ num_to_check = stderr_pipe[0];
+ FD_SET (stderr_pipe[0], &command_fds_to_drain.fds);
+ if (protocol_pipe[0] > num_to_check)
+ num_to_check = protocol_pipe[0];
+ FD_SET (protocol_pipe[0], &command_fds_to_drain.fds);
+ if (STDOUT_FILENO > num_to_check)
+ 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], 1,
+ input_memory_error);
+
+ stderrbuf = fd_buffer_initialize (stderr_pipe[0], 1,
+ input_memory_error);
+
+ protocol_inbuf = fd_buffer_initialize (protocol_pipe[0], 1,
+ 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);
+ err_exit = 1;
+ goto child_finish;
+ }
+ stdout_pipe[1] = -1;
+
+ if (close (stderr_pipe[1]) < 0)
+ {
+ buf_output0 (buf_to_net, "E close failed\n");
+ print_error (errno);
+ err_exit = 1;
+ goto child_finish;
+ }
+ stderr_pipe[1] = -1;
+
+ if (close (protocol_pipe[1]) < 0)
+ {
+ buf_output0 (buf_to_net, "E close failed\n");
+ print_error (errno);
+ err_exit = 1;
+ goto child_finish;
+ }
+ 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);
+ err_exit = 1;
+ goto child_finish;
+ }
+ 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);
+ dev_null_fd = -1; /* Do not try to close it again. */
+ err_exit = 1;
+ goto child_finish;
+ }
+ 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,
+ (fd_set *)0, timeout_ptr);
+ if (numfds < 0
+ && errno != EINTR)
+ {
+ buf_output0 (buf_to_net, "E select failed\n");
+ print_error (errno);
+ err_exit = 1;
+ goto child_finish;
+ }
+ } 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;
+ int 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);
+ err_exit = 1;
+ goto child_finish;
+ }
+
+ /*
+ * 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, (int *) 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);
+ err_exit = 1;
+ goto child_finish;
+ }
+
+ /* 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, (int *) 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);
+ err_exit = 1;
+ goto child_finish;
+ }
+
+ /* 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;
+ }
+
+ child_finish:
+ /*
+ * 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);
+ if (protocol_inbuf)
+ {
+ buf_shutdown (protocol_inbuf);
+ buf_free (protocol_inbuf);
+ protocol_inbuf = NULL;
+ }
+ if (stderrbuf)
+ {
+ buf_shutdown (stderrbuf);
+ buf_free (stderrbuf);
+ stderrbuf = NULL;
+ }
+ if (stdoutbuf)
+ {
+ buf_shutdown (stdoutbuf);
+ buf_free (stdoutbuf);
+ stdoutbuf = NULL;
+ }
+ if (err_exit)
+ goto error_exit;
+ }
+
+ 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, (int *) 0, 0);
+ if (waited_pid < 0 && errno == EINTR)
+ continue;
+ if (waited_pid == command_pid)
+ command_pid = -1;
+ }
+
+ if (dev_null_fd >= 0)
+ 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()
+{
+ 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, (fd_set *)0,
+ (fd_set *)0, (struct timeval *)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 PROTO((const char *, const char *));
+
+static void
+output_dir (update_dir, repository)
+ const char *update_dir;
+ const char *repository;
+{
+ 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");
+ buf_output0 (protocol, repository);
+ 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 (name, version, timestamp, options, tag, date, conflict)
+ 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 = "";
+
+ if (trace)
+ {
+ (void) fprintf (stderr,
+ "%s-> server_register(%s, %s, %s, %s, %s, %s, %s)\n",
+ CLIENT_SERVER_STR,
+ 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 (fname)
+ 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 ()
+{
+ kill_scratched_file = 0;
+}
+
+/* Print a new entries line, from a previous server_register. */
+static void
+new_entries_line ()
+{
+ 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 (arg)
+ char *arg;
+{
+ do_cvs_command ("commit", commit);
+}
+
+static void
+checked_in_response (file, update_dir, repository)
+ char *file;
+ char *update_dir;
+ char *repository;
+{
+ if (supported_response ("Mode"))
+ {
+ struct stat sb;
+ char *mode_string;
+
+ if ( CVS_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 (file, update_dir, repository)
+ const char *file;
+ const char *update_dir;
+ const char *repository;
+{
+ assert (file);
+ assert (update_dir);
+ assert (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 (file, update_dir, repository, updated)
+ 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 (arg)
+ char *arg;
+{
+ do_cvs_command ("update", update);
+}
+
+static void
+serve_diff (arg)
+ char *arg;
+{
+ do_cvs_command ("diff", diff);
+}
+
+static void
+serve_log (arg)
+ char *arg;
+{
+ do_cvs_command ("log", cvslog);
+}
+
+static void
+serve_rlog (arg)
+ char *arg;
+{
+ do_cvs_command ("rlog", cvslog);
+}
+
+static void
+serve_add (arg)
+ char *arg;
+{
+ do_cvs_command ("add", add);
+}
+
+static void
+serve_remove (arg)
+ char *arg;
+{
+ do_cvs_command ("remove", cvsremove);
+}
+
+static void
+serve_status (arg)
+ char *arg;
+{
+ do_cvs_command ("status", cvsstatus);
+}
+
+static void
+serve_rdiff (arg)
+ char *arg;
+{
+ do_cvs_command ("rdiff", patch);
+}
+
+static void
+serve_tag (arg)
+ char *arg;
+{
+ do_cvs_command ("tag", cvstag);
+}
+
+static void
+serve_rtag (arg)
+ char *arg;
+{
+ do_cvs_command ("rtag", cvstag);
+}
+
+static void
+serve_import (arg)
+ char *arg;
+{
+ do_cvs_command ("import", import);
+}
+
+static void
+serve_admin (arg)
+ char *arg;
+{
+ do_cvs_command ("admin", admin);
+}
+
+static void
+serve_history (arg)
+ char *arg;
+{
+ do_cvs_command ("history", history);
+}
+
+static void
+serve_release (arg)
+ char *arg;
+{
+ do_cvs_command ("release", release);
+}
+
+static void serve_watch_on PROTO ((char *));
+
+static void
+serve_watch_on (arg)
+ char *arg;
+{
+ do_cvs_command ("watch", watch_on);
+}
+
+static void serve_watch_off PROTO ((char *));
+
+static void
+serve_watch_off (arg)
+ char *arg;
+{
+ do_cvs_command ("watch", watch_off);
+}
+
+static void serve_watch_add PROTO ((char *));
+
+static void
+serve_watch_add (arg)
+ char *arg;
+{
+ do_cvs_command ("watch", watch_add);
+}
+
+static void serve_watch_remove PROTO ((char *));
+
+static void
+serve_watch_remove (arg)
+ char *arg;
+{
+ do_cvs_command ("watch", watch_remove);
+}
+
+static void serve_watchers PROTO ((char *));
+
+static void
+serve_watchers (arg)
+ char *arg;
+{
+ do_cvs_command ("watchers", watchers);
+}
+
+static void serve_editors PROTO ((char *));
+
+static void
+serve_editors (arg)
+ char *arg;
+{
+ do_cvs_command ("editors", editors);
+}
+
+static void serve_noop PROTO ((char *));
+
+static void
+serve_noop (arg)
+ char *arg;
+{
+
+ server_write_entries ();
+ if (!print_pending_error ())
+ {
+ (void) server_notify ();
+ buf_output0 (buf_to_net, "ok\n");
+ }
+ buf_flush (buf_to_net, 1);
+}
+
+static void serve_version PROTO ((char *));
+
+static void
+serve_version (arg)
+ char *arg;
+{
+ do_cvs_command ("version", version);
+}
+
+static void serve_init PROTO ((char *));
+
+static void
+serve_init (arg)
+ char *arg;
+{
+ if (alloc_pending (80 + strlen (arg)))
+ sprintf (pending_error_text, "E init may not be run remotely");
+
+ if (print_pending_error ())
+ return;
+}
+
+static void serve_annotate PROTO ((char *));
+
+static void
+serve_annotate (arg)
+ char *arg;
+{
+ do_cvs_command ("annotate", annotate);
+}
+
+static void serve_rannotate PROTO ((char *));
+
+static void
+serve_rannotate (arg)
+ char *arg;
+{
+ do_cvs_command ("rannotate", annotate);
+}
+
+static void
+serve_co (arg)
+ char *arg;
+{
+ char *tempdir;
+ int status;
+
+ if (print_pending_error ())
+ return;
+
+ if (!isdir (CVSADM))
+ {
+ /*
+ * The client has not sent a "Repository" line. Check out
+ * into a pristine directory.
+ */
+ tempdir = xmalloc (strlen (server_temp_dir) + 80);
+ if (tempdir == NULL)
+ {
+ buf_output0 (buf_to_net, "E Out of memory\n");
+ return;
+ }
+ strcpy (tempdir, server_temp_dir);
+ strcat (tempdir, "/checkout-dir");
+ status = mkdir_p (tempdir);
+ if (status != 0 && status != EEXIST)
+ {
+ buf_output0 (buf_to_net, "E Cannot create ");
+ buf_output0 (buf_to_net, tempdir);
+ buf_append_char (buf_to_net, '\n');
+ print_error (errno);
+ free (tempdir);
+ return;
+ }
+
+ if ( CVS_CHDIR (tempdir) < 0)
+ {
+ buf_output0 (buf_to_net, "E Cannot change to directory ");
+ buf_output0 (buf_to_net, tempdir);
+ buf_append_char (buf_to_net, '\n');
+ print_error (errno);
+ free (tempdir);
+ return;
+ }
+ free (tempdir);
+ }
+
+ /* 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") == 0) ?
+ "export" : "checkout",
+ checkout);
+}
+
+static void
+serve_export (arg)
+ char *arg;
+{
+ /* Tell checkout() to behave like export not checkout. */
+ cvs_cmd_name = "export";
+ serve_co (arg);
+}
+
+
+
+void
+server_copy_file (file, update_dir, repository, newfile)
+ 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 (finfo, vers_ts)
+ 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. */
+
+#if defined (USE_PROTOTYPES) ? USE_PROTOTYPES : defined (__STDC__)
+/* Need to prototype because mode_t might be smaller than int. */
+void
+server_updated (
+ struct file_info *finfo,
+ Vers_TS *vers,
+ enum server_updated_arg4 updated,
+ mode_t mode,
+ unsigned char *checksum,
+ struct buffer *filebuf)
+#else
+void
+server_updated (finfo, vers, updated, mode, checksum, filebuf)
+ struct file_info *finfo;
+ Vers_TS *vers;
+ enum server_updated_arg4 updated;
+ mode_t mode;
+ unsigned char *checksum;
+ struct buffer *filebuf;
+#endif
+{
+ 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 ( CVS_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;
+
+ 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);
+ assert (node != NULL);
+ if (node != NULL)
+ {
+ Entnode *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 ()
+{
+ return supported_response ("Rcs-diff");
+}
+
+
+
+void
+server_set_entstat (update_dir, repository)
+ 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 (update_dir, repository)
+ 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 (update_dir, repository, tag, date, nonbranch)
+ 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);
+}
+
+struct template_proc_data
+{
+ const char *update_dir;
+ const char *repository;
+};
+
+/* Here as a static until we get around to fixing Parse_Info to pass along
+ a void * for it. */
+static struct template_proc_data *tpd;
+
+static int
+template_proc PROTO((const char *repository, const char *template));
+
+static int
+template_proc (repository, template)
+ const char *repository;
+ const char *template;
+{
+ FILE *fp;
+ char buf[1024];
+ size_t n;
+ struct stat sb;
+ struct template_proc_data *data = tpd;
+
+ 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_template (update_dir, repository)
+ const char *update_dir;
+ const char *repository;
+{
+ struct template_proc_data data;
+ data.update_dir = update_dir;
+ data.repository = repository;
+ tpd = &data;
+ (void) Parse_Info (CVSROOTADM_RCSINFO, repository, template_proc, 1);
+}
+
+
+
+static void
+serve_gzip_contents (arg)
+ char *arg;
+{
+ int level;
+ level = atoi (arg);
+ if (level == 0)
+ level = 6;
+ file_gzip_level = level;
+}
+
+static void
+serve_gzip_stream (arg)
+ char *arg;
+{
+ int level;
+ level = atoi (arg);
+ if (level == 0)
+ level = 6;
+
+ /* All further communication with the client will be compressed. */
+
+ buf_to_net = compress_buffer_initialize (buf_to_net, 0, level,
+ buf_to_net->memory_error);
+ buf_from_net = compress_buffer_initialize (buf_from_net, 1, level,
+ buf_from_net->memory_error);
+}
+
+/* Tell the client about RCS options set in CVSROOT/cvswrappers. */
+static void
+serve_wrapper_sendme_rcs_options (arg)
+ 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;
+
+ 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 (arg)
+ 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.
+ */
+}
+
+static int
+expand_proc (argc, argv, where, mwhere, mfile, shorten,
+ local_specified, omodule, msg)
+ 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 (arg)
+ char *arg;
+{
+ int i;
+ int err;
+ DBM *db;
+ 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,
+ (char *) 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;
+ }
+ 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);
+}
+
+
+
+static void serve_valid_requests PROTO((char *arg));
+
+#endif /* SERVER_SUPPORT */
+#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("Repository", serve_repository, 0),
+ REQ_LINE("Directory", serve_directory, RQ_ESSENTIAL),
+ 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),
+ REQ_LINE("Empty-conflicts", serve_noop, 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("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),
+ REQ_LINE("Gzip-stream", serve_gzip_stream, 0),
+ REQ_LINE("wrapper-sendme-rcsOptions",
+ serve_wrapper_sendme_rcs_options,
+ 0),
+ REQ_LINE("Set", serve_set, RQ_ROOTLESS),
+#ifdef ENCRYPTION
+# ifdef HAVE_KERBEROS
+ REQ_LINE("Kerberos-encrypt", serve_kerberos_encrypt, 0),
+# endif
+# ifdef HAVE_GSSAPI
+ REQ_LINE("Gssapi-encrypt", serve_gssapi_encrypt, 0),
+# endif
+#endif
+#ifdef HAVE_GSSAPI
+ REQ_LINE("Gssapi-authenticate", serve_gssapi_authenticate, 0),
+#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("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, 0),
+ 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("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
+
+static void
+serve_valid_requests (arg)
+ char *arg;
+{
+ struct request *rq;
+ if (print_pending_error ())
+ 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);
+ }
+ }
+ 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 (sig)
+ int sig;
+{
+ int status;
+ pid_t r = wait (&status);
+ if (r == command_pid)
+ command_pid_is_dead++;
+}
+#endif /* SUNOS_KLUDGE */
+
+void
+server_cleanup (sig)
+ int sig;
+{
+ /* Do "rm -rf" on the temp directory. */
+ int status;
+ int save_noexec;
+
+ if (buf_to_net != NULL)
+ {
+ /* 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.
+ */
+
+ if (buf_from_net != NULL)
+ {
+ 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;
+ }
+
+ if (dont_delete_temp)
+ {
+ (void) buf_flush (buf_to_net, 1);
+ (void) buf_shutdown (buf_to_net);
+ buf_free (buf_to_net);
+ buf_to_net = NULL;
+ error_use_protocol = 0;
+ return;
+ }
+ }
+ else if (dont_delete_temp)
+ return;
+
+ /* 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);
+ if (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,
+ (fd_set *)0, (fd_set *)0,
+ &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 */
+
+ CVS_CHDIR (Tmpdir);
+ /* 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;
+ noexec = 0;
+ /* FIXME? Would be nice to not ignore errors. But what should we do?
+ We could try to do this before we shut down the network connection,
+ and try to notify the client (but the client might not be waiting
+ for responses). We could try something like syslog() or our own
+ log file. */
+ unlink_file_dir (orig_server_temp_dir);
+ noexec = save_noexec;
+
+ if (buf_to_net != NULL)
+ {
+ (void) buf_flush (buf_to_net, 1);
+ (void) buf_shutdown (buf_to_net);
+ buf_free (buf_to_net);
+ buf_to_net = NULL;
+ error_use_protocol = 0;
+ }
+}
+
+int
+server (argc, argv)
+ int argc;
+ char **argv;
+{
+ char *error_prog_name; /* Used in error messages */
+
+ if (argc == -1)
+ {
+ static const char *const msg[] =
+ {
+ "Usage: %s %s\n",
+ " Normally invoked by a cvs client on a remote machine.\n",
+ NULL
+ };
+ usage (msg);
+ }
+ /* Ignore argc and argv. They might be from .cvsrc. */
+
+ buf_to_net = fd_buffer_initialize (STDOUT_FILENO, 0,
+ outbuf_memory_error);
+ buf_from_net = stdio_buffer_initialize (stdin, 0, 1, outbuf_memory_error);
+
+ 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;
+
+ /* 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 (Tmpdir))
+ {
+ if (alloc_pending (80 + strlen (Tmpdir)))
+ sprintf (pending_error_text,
+ "E Value of %s for TMPDIR is not absolute", Tmpdir);
+
+ /* 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 (Tmpdir) + 80);
+ if (server_temp_dir == NULL)
+ {
+ /*
+ * 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");
+
+ error_exit ();
+ }
+ strcpy (server_temp_dir, Tmpdir);
+
+ /* 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 != 0)
+ {
+ 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 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)
+ {
+ /* For commands which change the way in which data
+ is sent and received, for example Gzip-stream,
+ this does the wrong thing. Since the client
+ assumes that everything is being compressed,
+ unconditionally, there is no way to give this
+ error to the client without turning on
+ compression. The obvious fix would be to make
+ Gzip-stream RQ_ROOTLESS (with the corresponding
+ change to the spec), and that might be a good
+ idea but then again I can see some settings in
+ CVSROOT about what compression level to allow.
+ I suppose a more baroque answer would be to
+ turn on compression (say, at level 1), just
+ enough to give the "Root request missing"
+ error. For now we just lose. */
+ if (alloc_pending (80))
+ sprintf (pending_error_text,
+ "E Protocol error: Root request missing");
+ }
+ else
+ (*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
+ error (0, 0, "Dying gasps received from client.");
+ }
+
+ /* This command will actually close the network buffers. */
+ server_cleanup (0);
+ return 0;
+}
+
+
+
+#if defined (HAVE_KERBEROS) || defined (AUTH_SERVER_SUPPORT) || defined (HAVE_GSSAPI)
+static void switch_to_user PROTO((const char *, const char *));
+
+static void
+switch_to_user (cvs_username, username)
+ const char *cvs_username; /* Only used for error messages. */
+ const char *username;
+{
+ struct passwd *pw;
+
+ 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);
+ /* Don't worry about server_cleanup; server_active isn't set yet. */
+ error_exit ();
+ }
+
+ 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
+ printf("error 0: root not allowed\n");
+ error_exit ();
+ }
+
+#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));
+ /* Don't worry about server_cleanup; server_active isn't set yet. */
+ error_exit ();
+ }
+#endif /* HAVE_INITGROUPS */
+
+#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));
+ /* Don't worry about server_cleanup;
+ server_active isn't set yet. */
+ error_exit ();
+ }
+ }
+ 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
+ /* Don't worry about server_cleanup;
+ server_active isn't set yet. */
+ error_exit ();
+ }
+ }
+
+ 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
+ /* Don't worry about server_cleanup; server_active isn't set yet. */
+ error_exit ();
+ }
+
+ /* 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
+
+#if HAVE_PUTENV
+ /* Set LOGNAME, USER and CVS_USER in the environment, in case they
+ are already set to something else. */
+ {
+ char *env;
+
+ env = xmalloc (sizeof "LOGNAME=" + strlen (username));
+ (void) sprintf (env, "LOGNAME=%s", username);
+ (void) putenv (env);
+
+ env = xmalloc (sizeof "USER=" + strlen (username));
+ (void) sprintf (env, "USER=%s", username);
+ (void) putenv (env);
+
+#ifdef AUTH_SERVER_SUPPORT
+ env = xmalloc (sizeof "CVS_USER=" + strlen (CVS_Username));
+ (void) sprintf (env, "CVS_USER=%s", CVS_Username);
+ (void) putenv (env);
+#endif
+ }
+#endif /* HAVE_PUTENV */
+}
+#endif
+
+#ifdef AUTH_SERVER_SUPPORT
+
+extern char *crypt PROTO((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 (username, password, repository, host_user_ptr)
+ char *username, *password, *repository, **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;
+}
+
+
+
+/* Return a hosting username if password matches, else NULL. */
+static char *
+check_password (username, password, repository)
+ char *username, *password, *repository;
+{
+ int rc;
+ char *host_user = NULL;
+ char *found_passwd = NULL;
+ struct passwd *pw;
+
+ /* 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. */
+
+ if (require_real_user)
+ rc = 0; /* "not found" */
+ else
+ 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 (!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);
+
+ error_exit ();
+ }
+
+ /* No cvs password found, so try /etc/passwd. */
+
+#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);
+
+ error_exit ();
+ }
+
+ /* 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)
+ {
+ host_user = xstrdup (username);
+ }
+ else
+ {
+ host_user = NULL;
+#ifdef LOG_AUTHPRIV
+ syslog (LOG_AUTHPRIV | LOG_NOTICE,
+ "password mismatch for %s: %s vs. %s", username,
+ crypt(password, found_passwd), found_passwd);
+#endif
+ }
+ goto handle_return;
+ }
+
+ if (password && *password)
+ {
+ /* user exists and has no system password, but we got
+ one as parameter */
+ host_user = xstrdup (username);
+ goto handle_return;
+ }
+
+ /* user exists but has no password at all */
+ host_user = NULL;
+#ifdef LOG_AUTHPRIV
+ 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)
+
+/* 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 ()
+{
+ char *tmp = NULL;
+ size_t tmp_allocated = 0;
+#ifdef AUTH_SERVER_SUPPORT
+ char *repository = NULL;
+ size_t repository_allocated = 0;
+ char *username = NULL;
+ size_t username_allocated = 0;
+ char *password = NULL;
+ size_t password_allocated = 0;
+
+ 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.
+ */
+
+#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
+ }
+ }
+#endif
+
+ /* Make sure the protocol starts off on the right foot... */
+ if (getline_safe (&tmp, &tmp_allocated, stdin, PATH_MAX) < 0)
+ {
+#ifdef HAVE_SYSLOG_H
+ syslog (LOG_DAEMON | LOG_NOTICE, "bad auth protocol start: EOF");
+#endif
+ error (1, 0, "bad auth protocol start: EOF");
+ }
+
+ if (strcmp (tmp, "BEGIN VERIFICATION REQUEST\n") == 0)
+ verify_and_exit = 1;
+ else if (strcmp (tmp, "BEGIN AUTH REQUEST\n") == 0)
+ ;
+ else if (strcmp (tmp, "BEGIN GSSAPI REQUEST\n") == 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 */
+
+ /* Get the three important pieces of information in order. */
+ /* See above comment about error handling. */
+ getline_safe (&repository, &repository_allocated, stdin, PATH_MAX);
+ getline_safe (&username, &username_allocated, stdin, PATH_MAX);
+ getline_safe (&password, &password_allocated, stdin, PATH_MAX);
+
+ /* Make them pure.
+ *
+ * We check that none of the lines were truncated by getnline in order
+ * to be sure that we don't accidentally allow a blind DOS attack to
+ * authenticate, however slim the odds of that might be.
+ */
+ if (!strip_trailing_newlines (repository)
+ || !strip_trailing_newlines (username)
+ || !strip_trailing_newlines (password))
+ error (1, 0, "Maximum line length exceeded during authentication.");
+
+ /* ... and make sure the protocol ends on the right foot. */
+ /* See above comment about error handling. */
+ getline_safe (&tmp, &tmp_allocated, stdin, PATH_MAX);
+ if (strcmp (tmp,
+ verify_and_exit ?
+ "END VERIFICATION REQUEST\n" : "END AUTH REQUEST\n")
+ != 0)
+ {
+ error (1, 0, "bad auth protocol end: %s", tmp);
+ }
+ if (!root_allow_ok (repository))
+ {
+ printf ("error 0 %s: no such repository\n", repository);
+#ifdef HAVE_SYSLOG_H
+ syslog (LOG_DAEMON | LOG_NOTICE, "login refused for %s", repository);
+#endif
+ 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! */
+ parse_config (repository);
+
+ /* 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
+ memset (descrambled_password, 0, strlen (descrambled_password));
+ free (descrambled_password);
+ i_hate_you:
+ printf ("I HATE YOU\n");
+ fflush (stdout);
+
+ /* Don't worry about server_cleanup, server_active isn't set
+ yet. */
+ error_exit ();
+ }
+ 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)
+ {
+ printf ("I LOVE YOU\n");
+ fflush (stdout);
+
+ /* It's okay to skip rcs_cleanup() and Lock_Cleanup() here. */
+
+#ifdef SYSTEM_CLEANUP
+ /* Hook for OS-specific behavior, for example socket subsystems on
+ NT and OS2 or dealing with windows and arguments on Mac. */
+ SYSTEM_CLEANUP ();
+#endif
+
+ exit (0);
+ }
+
+ /* 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 (tmp);
+ free (repository);
+ free (username);
+ free (password);
+
+ printf ("I LOVE YOU\n");
+ fflush (stdout);
+#endif /* AUTH_SERVER_SUPPORT */
+}
+
+#endif /* AUTH_SERVER_SUPPORT || HAVE_GSSAPI */
+
+
+#ifdef HAVE_KERBEROS
+void
+kserver_authenticate_connection ()
+{
+ 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));
+
+ error_exit ();
+ }
+
+#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
+ }
+ }
+#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));
+
+ error_exit ();
+ }
+
+ 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));
+
+ error_exit ();
+ }
+
+ /* Switch to run as this user. */
+ switch_to_user ("Kerberos 4", user);
+}
+#endif /* HAVE_KERBEROS */
+
+#ifdef HAVE_GSSAPI
+
+#ifndef MAXHOSTNAMELEN
+#define MAXHOSTNAMELEN (256)
+#endif
+
+/* Authenticate a GSSAPI connection. This is called from
+ pserver_authenticate_connection, and it handles success and failure
+ the same way. */
+
+static void
+gserver_authenticate_connection ()
+{
+ char hostname[MAXHOSTNAMELEN];
+ struct hostent *hp;
+ 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;
+
+ gethostname (hostname, sizeof hostname);
+ hp = gethostbyname (hostname);
+ if (hp == NULL)
+ error (1, 0, "can't get canonical hostname");
+
+ sprintf (buf, "cvs@%s", hp->h_name);
+ 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 HAVE_GSSAPI
+
+/* An buffer interface using GSSAPI. This is built on top of a
+ packetizing buffer. */
+
+/* This structure is the closure field of the GSSAPI translation
+ routines. */
+
+struct cvs_gssapi_wrap_data
+{
+ /* The GSSAPI context. */
+ gss_ctx_id_t gcontext;
+};
+
+static int cvs_gssapi_wrap_input PROTO((void *, const char *, char *, int));
+static int cvs_gssapi_wrap_output PROTO((void *, const char *, char *, int,
+ int *));
+
+/* Create a GSSAPI wrapping buffer. We use a packetizing buffer with
+ GSSAPI wrapping routines. */
+
+struct buffer *
+cvs_gssapi_wrap_buffer_initialize (buf, input, gcontext, memory)
+ struct buffer *buf;
+ int input;
+ gss_ctx_id_t gcontext;
+ void (*memory) PROTO((struct buffer *));
+{
+ struct cvs_gssapi_wrap_data *gd;
+
+ gd = (struct cvs_gssapi_wrap_data *) xmalloc (sizeof *gd);
+ gd->gcontext = gcontext;
+
+ return (packetizing_buffer_initialize
+ (buf,
+ input ? cvs_gssapi_wrap_input : NULL,
+ input ? NULL : cvs_gssapi_wrap_output,
+ gd,
+ memory));
+}
+
+/* Unwrap data using GSSAPI. */
+
+static int
+cvs_gssapi_wrap_input (fnclosure, input, output, size)
+ void *fnclosure;
+ const char *input;
+ char *output;
+ int size;
+{
+ struct cvs_gssapi_wrap_data *gd =
+ (struct cvs_gssapi_wrap_data *) fnclosure;
+ gss_buffer_desc inbuf, outbuf;
+ OM_uint32 stat_min;
+ int conf;
+
+ inbuf.value = (void *) input;
+ inbuf.length = size;
+
+ if (gss_unwrap (&stat_min, gd->gcontext, &inbuf, &outbuf, &conf, NULL)
+ != GSS_S_COMPLETE)
+ {
+ error (1, 0, "gss_unwrap failed");
+ }
+
+ if (outbuf.length > size)
+ abort ();
+
+ memcpy (output, outbuf.value, outbuf.length);
+
+ /* The real packet size is stored in the data, so we don't need to
+ remember outbuf.length. */
+
+ gss_release_buffer (&stat_min, &outbuf);
+
+ return 0;
+}
+
+/* Wrap data using GSSAPI. */
+
+static int
+cvs_gssapi_wrap_output (fnclosure, input, output, size, translated)
+ void *fnclosure;
+ const char *input;
+ char *output;
+ int size;
+ int *translated;
+{
+ struct cvs_gssapi_wrap_data *gd =
+ (struct cvs_gssapi_wrap_data *) fnclosure;
+ gss_buffer_desc inbuf, outbuf;
+ OM_uint32 stat_min;
+ int conf_req, conf;
+
+ inbuf.value = (void *) input;
+ inbuf.length = size;
+
+#ifdef ENCRYPTION
+ conf_req = cvs_gssapi_encrypt;
+#else
+ conf_req = 0;
+#endif
+
+ if (gss_wrap (&stat_min, gd->gcontext, conf_req, GSS_C_QOP_DEFAULT,
+ &inbuf, &conf, &outbuf) != GSS_S_COMPLETE)
+ error (1, 0, "gss_wrap failed");
+
+ /* The packetizing buffer only permits us to add 100 bytes.
+ FIXME: I don't know what, if anything, is guaranteed by GSSAPI.
+ This may need to be increased for a different GSSAPI
+ implementation, or we may need a different algorithm. */
+ if (outbuf.length > size + 100)
+ abort ();
+
+ memcpy (output, outbuf.value, outbuf.length);
+
+ *translated = outbuf.length;
+
+ gss_release_buffer (&stat_min, &outbuf);
+
+ return 0;
+}
+
+#endif /* HAVE_GSSAPI */
+
+#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;
+};
+
+static int krb_encrypt_input PROTO((void *, const char *, char *, int));
+static int krb_encrypt_output PROTO((void *, const char *, char *, int,
+ int *));
+
+/* Create a Kerberos encryption buffer. We use a packetizing buffer
+ with Kerberos encryption translation routines. */
+
+struct buffer *
+krb_encrypt_buffer_initialize (buf, input, sched, block, memory)
+ struct buffer *buf;
+ int input;
+ Key_schedule sched;
+ C_Block block;
+ void (*memory) PROTO((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);
+}
+
+/* Decrypt Kerberos data. */
+
+static int
+krb_encrypt_input (fnclosure, input, output, size)
+ 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 (fnclosure, input, output, size, translated)
+ 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;
+}
+
+#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 (str, len)
+ const char *str;
+ size_t len;
+{
+ if (len == 0)
+ len = strlen (str);
+#ifdef SERVER_SUPPORT
+ if (error_use_protocol && buf_to_net != NULL)
+ {
+ buf_output (saved_output, str, len);
+ buf_copy_lines (buf_to_net, saved_output, 'M');
+ }
+ else if (server_active && protocol != NULL)
+ {
+ buf_output (saved_output, str, len);
+ buf_copy_lines (protocol, saved_output, 'M');
+ buf_send_counted (protocol);
+ }
+ 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 (str, len)
+ 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;
+
+ 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 (str, len)
+ const char *str;
+ size_t len;
+{
+ if (len == 0)
+ len = strlen (str);
+#ifdef SERVER_SUPPORT
+ if (error_use_protocol)
+ {
+ buf_output (saved_outerr, str, len);
+ buf_copy_lines (buf_to_net, saved_outerr, 'E');
+ }
+ else if (server_active)
+ {
+ buf_output (saved_outerr, str, len);
+ buf_copy_lines (protocol, saved_outerr, 'E');
+ buf_send_counted (protocol);
+ }
+ 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 ()
+{
+#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 ()
+{
+#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 (tag, text)
+ 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
+ {
+ if (strcmp (tag, "newline") == 0)
+ cvs_output ("\n", 1);
+ else if (text != NULL)
+ cvs_output (text, 0);
+ }
+}
OpenPOWER on IntegriCloud