6761 lines
165 KiB
C
6761 lines
165 KiB
C
/* 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);
|
||
}
|
||
}
|