/*
 * Copyright(c) 1997, Space Science and Engineering Center, UW-Madison
 * Refer to "McIDAS Software Acquisition and Distribution Policies"
 * in the file  mcidas/data/license.txt
 */

/**** $Id: mcserv.c,v 1.19 1999/02/23 15:06:44 chadj Tst $ ****/

/*
 * M c I D A S   DDE server interface
 * j.benson  04/92.
 *
 * Completely rewritten.
 * Now fires up local servers, too, and can be started by inetd.
 * Acts as a dummy server (writes the request to a file) when invoked
 * with a base name other than 'mcserv'.
 * No authentication services.
 * David Sanderson 12/93.
 */

/*
 * Optional compression added by j.benson 07/97.
 * Compression is controlled on the client side by the existance
 * of the environment variable.  On the server side, it is
 * triggered by the client having connected to the 'compress'
 * port rather than the usual port.
 *
 * future porting note:
 *  the 'compress' and 'uncompress' commands are found in /bin
 * on AIX, HPUX, and Solaris.  They are found in /usr/ucb on IRIX.
 * if you are porting this to another UNIX which has 'compress'
 * in a different path, add yet another execl to the stack already
 * there.  Be careful to do it for both compress and uncompress.
 *
 * coding style note:
 * you'll notice that return codes are not checked.  stderr has
 * been duped into oblivion, and if you fail to pipe or fork, you
 * can't even send an error message back upstream, because you would be sending
 * it in clear text instead of compressed.  So just dying is
 * about the best you can do.
 */

#include <time.h>
#include <sys/stat.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <fcntl.h>
#include <stdlib.h>
#include <assert.h>
#include <ctype.h>
#include <errno.h>
#include <stdio.h>
#include <pwd.h>

#include "m0select.h"
#include "mcidas.h"
#include "mcidasp.h"
#include "m0adde.h"

/*
 * compression mods added for INM 07.97 jmb
 */
#define COMPRESS_PORT 503
#define COMPRESS_STRING "MCCOMPRESS"

#ifndef INADDR_LOOPBACK
#define INADDR_LOOPBACK (unsigned long)0x7f000001
#endif

/*
 * swrite() - write() a "string variable" (char *)
 */

#define	swrite(fd, s)	(void) write((fd), (s), strlen(s))

/*
 * lwrite() - write() a "string literal" (char [])
 */

#define	lwrite(fd, lit)	(void) write((fd), (lit), sizeof(lit)-1)

/*
 * DDE protocol defines
 */

#define VERHDRLEN	4	/* length of DDE version header, in bytes */
#define PREHDRLEN	12	/* length of DDE pre-request header, in bytes */
#define REQHDRLEN	160	/* length of DDE request header, in bytes */
#define ANSLEN		96	/* length of DDE reply header, in bytes */
#define ANSTEXTLEN	72	/* length of message text in DDE reply header, in bytes */

#define PROTOCOL_VERSION	1	/* protocol version number */

/* this is the composition of the version header */
typedef struct {
Mcuint4      	version;	/* protocol version number */
}		Verhdr;

/* this is the composition of the pre-request header */
typedef struct {
Mcuint4 	servaddr;	/* IP address of server, net byte order */
Mcuint4		servport;	/* port of server, net byte order */
char		servtype[4];	/* data type on server */
}		Prehdr;

/* this is the composition of the request header */
typedef struct {
Mcint4       	word[40];
}		Reqhdr;

/*
 * DDE error numbers and strings
 */

#define EC_TCPIP	(-99)
#define ES_TCPIP	"Cannot initialize OS/2 TCPIP"

#define EC_CONNECT	(-100)
#define ES_CONNECT	"Cannot contact server (connect() failed)"

#define EC_SOCKET	(-101)
#define ES_SOCKET	"Socket call failed (socket() failed)"

#define EC_WRITE	(-102)
#define ES_WRITE	"TCP write failed"

/* NEW ONES */

#define EC_CMDLINE	(-104)
			/* mcserv given bad command line arguments */

#define EC_DEVNULL	(-105)
			/* error opening /dev/null (should not happen) ... */

#define EC_DUPERR	(-106)
#define ES_DUPERR	"error in dup() (should not happen)"

#define EC_ASROOT	(-107)
#define ES_ASROOT	"error in /etc/inetd.conf: server running as root"

#define EC_PASSWD	(-108)
			/* cannot find passwd entry for ... */

#define EC_MALLOC	(-109)
#define ES_MALLOC	"malloc() failed (should not happen)"

#define EC_PUTENV	(-110)
#define ES_PUTENV	"putenv() failed (should not happen)"

#define EC_MCPATH	(-111)
#define ES_MCPATH	"error in prefixing MCPATH"

#define EC_PATH		(-112)
#define ES_PATH		"error in prefixing PATH"

#define EC_CHDIR	(-113)
			/* error in changing directory to ... */

#define EC_NOSERVER	(-114)
			/* cannot find server program ... */

#define EC_BADSERVER	(-115)
			/* cannot run server program ... */

#define EC_PROTOVERS	(-116)
			/* protocol version mismatch: ... */

/*
 * Command-line related variables.
 *
 * If opt_R is set (-R was given), then we know that we may access
 * remote sites we were NOT run from inetd.
 *
 * If opt_R is NOT set, then we have been started from inetd.
 *
 * This makes the installation process more bulletproof, since the
 * option will always be supplied by the library code (and no command
 * line option need be put in /etc/inetd.conf).
 */

static char	cmdopt[] = "R";
static char	cmdusg[] = " [-R]\n";
static int	opt_R = 0;

/*
 * Protocol status variables
 *
 * have_verhdr is true when we have read the version header.
 * have_prehdr is true when we have read the pre-request header.
 * have_reqhdr is true when we have read the request header.
 *
 * client_version is defined only when have_verhdr is true; it gives
 * the protocol version level the client speaks.
 *
 * These are necessary so that die() can tell how much it has to read
 * from the client before it can write the error to the client.
 */

static int	have_verhdr = 0;
static int	have_prehdr = 0;
static int	have_reqhdr = 0;

static Mcuint4      	client_version;

/*
 * Prototypes (roughly showing the call tree, too)
 */

static int	cmdline(int argc, char **argv);
static void		usage(char *cmd);
static char			*mybasename(char *s);

static int	dbserv(char *basename);
static int	mcserv(void);
static void		initenv(void);
static int		prevar(const char *var, char *val, const char *sep);
static int      	getsockfd(Mcuint4 netaddr, unsigned short netport);
static int		cpio(int ifd, int ofd, size_t nbytes);
static void		strdn(char *s);
static void		doproto(int fds[2],unsigned short port);
static void		die(Mcint4 code, const char *text);

/*
 * read and write routines guarded by select()
 *
 * this means that it doesn't matter if the
 * file descriptors are set blocking or non-blocking
 * everything is going to work anyway
 */
static int s_write(int fd, void *buf, int count)
{
    fd_set wset;
    struct timeval timeout;
    int rc;

    timeout.tv_sec=600;
    timeout.tv_usec=0;
    FD_ZERO(&wset);
    FD_SET(fd,&wset);

    switch (select(fd+1,0,&wset,0,&timeout))
    {
    case -1:
        perror("select1");
        exit(1);
    case 0:
        fprintf(stderr,"select timed out\n");
        exit(1);
    default:
        break;
    }

        return write(fd,buf,count);
}

static int s_read(int fd, void *buf, int count)
{
    fd_set rset;
    struct timeval timeout;

    timeout.tv_sec=600;
    timeout.tv_usec=0;
    FD_ZERO(&rset);
    FD_SET(fd,&rset);

    switch (select(fd+1,&rset,0,0,&timeout))
    {
    case -1:
        perror("select2");
        exit(1);
    case 0:
        fprintf(stderr,"select timed out\n");
        exit(1);
    default:
        break;
    }

    return read(fd,buf,count);
}

/*
 * This is where it's at.
 */

int
main(int argc, char **argv)
{
	char *argv0 = mybasename(argv[0]);

	/* behave nicely if someone ran us interactively by accident */

	if(isatty(STDIN) || isatty(STDOUT))
	{
		char *msg;

		msg = stralloc("ERROR: ", argv0
				, " is not a user-level command\n"
				, (char *)0);
		if(msg == (char *)0)
		{
			return 1;
		}
		swrite(STDERR, msg);
		free(msg);

		return 1;
	}

	/* get any command line arguments */

	cmdline(argc, argv);

	/* and do our thing */

	/*
	 * If we have a name other than mcserv, then
	 * behave as a stupid local server that dumps its
	 * request block to a file and exits.
	 *
	 * This is for use in DEBUGGING local servers.
	 */

	if(strcmp(argv0, "mcserv") != 0)
	{
		return dbserv(argv0);
	}

	return mcserv();
}

/*
 * cmdline() - return the index of the first non-option argument
 */

static int
cmdline(int argc, char **argv)
{
	extern char    *optarg; /* points to the option argument */
	extern int      opterr;	/* when 0, getopt is silent on errors */
	extern int      optind;	/* argv[optind] is the next argument */
	extern int      optopt;	/* current option letter */

	int errflag = 0;	/* usage error flag */
	int errcode = 1;	/* usage error exit code */
	int c;

	while((c = getopt(argc, argv, cmdopt)) != -1)
		switch(c)
		{
		case 'R':
			opt_R = 1;
			break;
		case '?':
			errflag = 1;
			if(optopt == '?')
				errcode = 0;
			break;
		}

	if(errflag)
	{
		usage(argv[0]);
		exit(errcode);
	}

	return optind;
}

/*
 * print the command usage to stderr
 */

static void
usage(char *cmd)
{
	static char prefix[] = "usage: ";
	const char *p = cmd ? mybasename(cmd) : "a.out";

	char *msg;

	msg = stralloc(prefix, p, cmdusg, (char *)0);

	/*
	 * If running interactively, print an intelligible message.
	 * Otherwise report it via the DDE protocol mechanism.
	 */
	if(isatty(STDIN) || isatty(STDOUT))
	{
		if(msg)
		{
			swrite(STDERR, msg);
		}
	}
	else
	{
		die(EC_CMDLINE, msg);
		/* NOTREACHED */
	}

	if(msg)
	{
		free(msg);
	}
}

/*
 * mybasename(s) - assumes the string does not end in '/'
 *
 * This used to be
 *
 * #define mybasename(s)	(strrchr((s),'/') ? strrchr((s),'/')+1 : (s))
 *
 * but some systems don't have strrchr() and I didn't want to mess
 * with switching between strrchr() and rindex().
 */

static char *
mybasename(char *s)
{
	char *p;

	if (!s || !*s)
		return s;

	for(p = s; *s; s++)
		if(*s == '/')
			p = s+1;
	return p;
}

/*
 * dbserv() - act as a dummy local server and copy the request
 * block to an output file.
 * Return value is exit status of process.
 */

static int
dbserv(char *argv0)
{
	int	nullfd;
	int	filefd;
	char	*file;

	/*
	 * Open files for server output and input.
	 * The output file name is the program file name
	 * with ".req" appended.
	 *
	 * This has to be hardcoded since the local servers
	 * run with no arguments.
	 */

	file = stralloc(argv0, ".req", (char *)0);
	if(file == (char *)0)
	{
		die(EC_MALLOC, ES_MALLOC);
		/* NOTREACHED */
	}

	if((filefd = open(file, O_WRONLY|O_APPEND|O_CREAT|O_TRUNC|O_NOCTTY, 0666)) == -1)
	{
		/* this isn't the right error code,
		 * but it'll do for now I suppose */
		die(EC_DEVNULL, "error opening output file");
		/* NOTREACHED */
	}

	if((nullfd = open("/dev/null", O_RDONLY, 0)) == -1)
	{
		die(EC_DEVNULL, "error opening /dev/null");
		/* NOTREACHED */
	}

	/*
	 * Now call the code which does the server protocol.
	 * Since we've spoofed the file descriptors, the input
	 * to this process goes into a file.
	 */

	{
		int	dummyfds[2];

		dummyfds[0] = nullfd;
		dummyfds[1] = filefd;

		doproto(dummyfds,0);
	}

	/*
	 * Clean up file descriptors.
	 */

	close(filefd);
	close(nullfd);

	return 0;
}

/*
 * mcserv() - fire up the real server and copy to and fro.
 * Return value is exit status of process.
 */

static int
mcserv(void)
{
	/*
	 * server process access: read from fds[0] to read from the server,
	 * write to fds[1] to write to the server.
	 */

	int		fds[2];

	Mcuint4		netaddr;	/* address, net byte order */
	unsigned short	netport;	/* port, net byte order */

	Verhdr		ver;		/* version record */
	Prehdr		pre;		/* pre-request record */

	assert(sizeof(ver) == VERHDRLEN);	/* paranoia */
	assert(sizeof(pre) == PREHDRLEN);	/* paranoia */

	/* read the transport protocol version header */

	memset(&ver, 0, sizeof(ver));
	if(M0adderead(STDIN, &ver, sizeof(ver)) != sizeof(ver))
	{
		return 0;
	}
	client_version = ntohl(ver.version);
	have_verhdr = 1;

	/*
	 * Check the protocol version
	 */

	if(client_version != PROTOCOL_VERSION)
	{
		char cliver[32];
		char srvver[32];

		char *errmsg;

		sprintf(cliver, "%ld", (long) client_version);
		sprintf(srvver, "%ld", (long) PROTOCOL_VERSION);

		errmsg = stralloc("protocol version mismatch:"
			"client wants ", cliver,
			", server knows ", srvver,
			(char *)0);
		die(EC_PROTOVERS, errmsg);
		/* NOTREACHED */
	}

	/*
	 * Process initializations
	 */
        /*
	 * If started by inetd,
	 * set up environment, current directory, etc.
	 */

	if(!opt_R)
	{
		initenv();
	}

	/* read the pre-request header */

	memset(&pre, 0, sizeof(pre));
	if(M0adderead(STDIN, &pre, sizeof(pre)) != sizeof(pre))
	{
		return 0;
	}
	have_prehdr = 1;

	/*
	 * extract the address and port number of the data server
	 */

	netaddr = pre.servaddr;
	netport = htons(ntohl(pre.servport) & 0xFFFF);

	/*
	 * If we are allowed to contact remote servers,
	 * and the service address is not the loopback address,
	 * then try to connect to the remote server.
	 * Set the fds[] so that we can talk to the remote server.
	 *
	 * Otherwise, exec() a local server.
	 */

	if(opt_R && netaddr != htonl(INADDR_LOOPBACK))
	{
		/*
		 * if compression is enabled,
		 * modify the port number in the pre-header
		 * contacting the server on that new port number
		 * is the signal that you want return data to be compressed
		 */
		if(getenv(COMPRESS_STRING))
		{
			pre.servport=htonl(COMPRESS_PORT);
			netport=htons((unsigned short)COMPRESS_PORT);
		}

		/*
		 * now connect
		 */
		fds[0] = fds[1] = getsockfd(netaddr, netport);
	}
	else
	{
		char *vec[2];	/* server process command vector */
		char  cmd[128];	/* server process command name */
		int pipes[2];   /* pipes for compress coprocess */

	/*
	 * open /dev/null so stderr doesn't get set to it
	 */
	{
		int fd;

#if 0
		if((fd = open("/tmp/johnb", O_WRONLY | O_CREAT | O_APPEND, 0)) == -1)
#else
		if((fd = open("/dev/null", O_RDONLY, 0)) == -1)
#endif
		{
			char *errmsg;
			int err;

			/* save errno before doing any other library calls */
			err = errno;

			errmsg = stralloc("error opening /tmp/johnb"
				" (should not happen): ", strerror(err)
				, (char *)0);

			die(EC_DEVNULL, errmsg);
			/* NOTREACHED */
		}

		close(STDERR);
		if(dup(fd) != STDERR)
		{
			die(EC_DUPERR, ES_DUPERR);
			/* NOTREACHED */
		}
		close(fd);
	}

		/*
		 * exec() local server.
		 *
		 * Here, we have to select which process to run as a coprocess.
		 * Start it up, setting fds[0] and fds[1].
		 */

		/*
		 * construct the command name & vector.
		 * For now we blindly assume that the four characters
		 * in pre.servtype are okay, since that's what the fortran did.
		 */

		strncpy(cmd, pre.servtype, sizeof(pre.servtype));
		cmd[sizeof(pre.servtype)] = '\0';
		strdn(cmd);
		strcat(cmd, "serv");

		vec[0] = cmd;
		vec[1] = (char *)0;

		/*
		 * do compression?
		 * if so we need to start another coprocess: the 'compress'
		 * command
	 	 */
		if( netaddr != htonl(INADDR_LOOPBACK) &&pre.servport==
			htonl(COMPRESS_PORT))
		{
/*
 * on AIX and HPUX, we need yet another coprocess
 * copying the data from pipe to socket
 * because sending the socket directly to compress
 * appeared not to work
 *
 * when reading this section of code, the main point to remember is the
 * child always inherates open file descriptors of the parent
 *
 * note that read may return some arbitrary number of bytes
 * and that it may take more than one write to get them all out
 */
#if 1 /* defined(_AIX) || defined(__hpux) */
			unsigned char b[5120];
			int i,j,k;

			pipe(pipes);
			if(fork()==0)
			{
				/*
				 * child; writing to the socket so close the
				 * write pipe; don't need it.
				 */
				close(pipes[1]);
				while(1)
				{
					i=s_read(pipes[0],b,sizeof(b));
					if(i==0) exit(0); /*normal end of file*/
					if(i<0)
					{
						exit(1); /*error*/
					}
					j=0;
					while(i)
					{
						k=s_write(1,&b[j],i);
						if(k<0)
						{
							exit(1);
						}
						i-=k;
						j+=k;
					} /* end copying out */
				} /* end copying in */
			} /* end child block */

                        /*
                         * parent; redirect output from compress (see below)
			 * into pipe. will be copied to socket by child code
			 * above
                         */
			close(1);
			dup(pipes[1]);
			close(pipes[0]);
			close(pipes[1]);

#endif /* _AIX || __hpux */

                       /*
                        * another set of pipes, connected to compress
                        */
			pipe(pipes);
			if(fork()==0)
			{
				/*
				 * child; you are compress
				 * your stdin will be one end of the pipe
				 * the other end of the pipe is closed
				 * so you will get eof when the other process
				 * quits. eof is not sent until all file
				 * descriptors are closed
				 * everywhere except SGI, the first exec will
				 * succeed and the second will not be used
				 * on SGI, the second will succeed.
				 */
				close(0);
				dup(pipes[0]);
				close(pipes[1]);
				execl("/bin/compress", "compress", "-cf",0);
				execl("/usr/ucb/compress", "compress", "-cf",0);
                                exit(1);
			}
			/*
			 * parent; you are going to be the server
			 * your stdout is re-routed to the pipe, so it will
			 * reach the input end of 'compress'
			 */
			close(1);
			dup(pipes[1]);
			close (pipes[0]);
			close(pipes[1]);
		} /* end of code for compress IF statement */

		/*
		 * Try to exec() the actual server module for the data type.
		 */

		execvp(vec[0], vec);

		/*
		 * If we get to here, we can't run the command.
		 * Give up.
		 */

		{
			char *errmsg;

			errmsg = stralloc("cannot run server '"
					, vec[0], "': ", strerror(errno)
					, (char *)0);
			die(EC_NOSERVER, errmsg);
			/* NOTREACHED */
		}
	}

	/*
	 * If we get to here, we are talking to a remote server,
	 * and we need to pass along the version header and the
	 * pre-request header.
	 */

	/* transmit version header */

	if (M0addewrite(fds[1], (char *) &ver, sizeof(ver)) != sizeof(ver))
	{
		die(EC_WRITE, ES_WRITE);
		/* NOTREACHED */
	}

	/* transmit pre-request header */

	if (M0addewrite(fds[1], (char *) &pre, sizeof(pre)) != sizeof(pre))
	{
		die(EC_WRITE, ES_WRITE);
		/* NOTREACHED */
	}

	/* do the rest of the protocol */

	doproto(fds,netport);

	/*
	 * Clean up file descriptors.
	 */

	close(fds[0]);
	if(fds[0] != fds[1])
	{
		close(fds[1]);
	}

	return 0;
}

/*
 * doproto - copy data between the client and the remote server
 * as the protocol demands, beginning with the request header.
 */

static void doproto(int fds[2],unsigned short port)
{
	size_t          rlen;
	Reqhdr          req;		/* request record */
	int 		pipes[2];

	assert(sizeof(req) == REQHDRLEN);	/* paranoia */

	/* read the request header */
	memset(&req, 0, sizeof(req));
	if(M0adderead(STDIN, &req, sizeof(req)) != sizeof(req))
	{
		return;
	}
	have_reqhdr = 1;

	/* transmit request header */

	if (M0addewrite(fds[1], (char *) &req, sizeof(req)) != sizeof(req))
	{
		die(EC_WRITE, ES_WRITE);
		/* NOTREACHED */
	}

	/* length of rest of request */
	rlen = ntohl(req.word[9]);

	/* copy request from stdin to server */
	if(cpio(STDIN, fds[1], rlen) == -1)
	{
		exit(0);
	}

	/*
	 * uncompress?
	 */
	if((unsigned short)port==(unsigned short)htons(COMPRESS_PORT))
	{
		/*
		 * uncompress
		 * stdin will be the socket
		 * stdout will be the client
		 */
                pipe(pipes);
                if(fork()==0)
                {
			char b[5120];
			int i=1;

        	        close(pipes[0]);
			while (i)
			{
				i=s_read(fds[0],b,sizeof(b));
				if(i<0)
				{
					perror("pipe read");
					exit(1);
				}
				if(i==0) break;
				s_write(pipes[1],b,i);
			}
			exit(0);
                }
		close(0);
		dup(pipes[0]);
		close(pipes[1]);

#if 0
		execl("/usr/sww/bin/zcat","zcat",0);
#endif
		execl("/bin/uncompress", "uncompress","-cf",0);
		execl("/usr/ucb/uncompress", "uncompress","-cf",0);
		execl(M0prefixRootPath ("/usr/contrib/uncompress"), 
                      "uncompress", "-cf", 0);
		exit(1);
	}

	/* copy data from server to stdout */
	do
	{
		Mcint4          xlen;

		/*
		 * Obtain the length of the block (possibly 0)
		 * and also write it to stdout.
		 */
		if(M0adderead(fds[0], &xlen, 4) != 4)
		{
			exit(0);
		}
		if(M0addewrite(STDOUT, &xlen, 4) != 4)
		{
			exit(0);
		}

		/* rlen is the block size */
		rlen = ntohl(xlen);

		if (rlen > 0)
		{
			/* process the block      */
			if(cpio(fds[0], STDOUT, rlen) == -1)
			{
				exit(0);
			}
		}
		else
		{
			/* process the trailer */
			if(cpio(fds[0], STDOUT, ANSLEN-4) == -1)
			{
				exit(0);
			}
		}
	}
	while(rlen > 0);
}

/*
 * initenv - set the environment of the process, its umask,
 * and its current directory appropriately when the process
 * has been run from inetd.
 */

static void
initenv(void)
{
	struct passwd *pw;	/* passwd entry for the current uid */
	char *str;	/* temporary pointer to new env strings */
	char *dir;	/* temporary pointer to name of data dir */

	/* home directory of mcidas account, if any */
	char *mcidas_home = (char *)0;
	uid_t mcidas_uid;

	/* name of the mcidas account */
	static char mcidas_user[] = "mcidas";

	/*
	 * Do NOT allow this process to run as root.
	 */

	if(getuid() == 0 || geteuid() == 0)
	{
		die(EC_ASROOT, ES_ASROOT);
		/* NOTREACHED */
	}

	/*
	 * Set the umask.
	 */

	umask(0022);

	/*
	 * Try to get the home directory of the 'mcidas' account.
	 * Continue even if there isn't one.
	 *
	 * Note that every time you call getpwuid() or getpwnam()
	 * you reset the data the return pointer points to, so
	 * you can't mix them without saving your own copy of
	 * at least some of the data.  We look up the mcidas account
	 * first and save its home directory, then look for the
	 * account of the current uid.
	 */

	mcidas_uid = -1;
	mcidas_home = (char *)0;
	if((pw = getpwnam(mcidas_user)) != (struct passwd *)0)
	{
		mcidas_uid = pw->pw_uid;
		mcidas_home = stralloc(pw->pw_dir, (char *)0);
		if(mcidas_home == (char *)0)
		{
			die(EC_MALLOC, ES_MALLOC);
			/* NOTREACHED */
		}
	}

	/*
	 * If we can't get the passwd info for this uid, then
	 * give up.
	 */

	if((pw = getpwuid(getuid())) == (struct passwd *)0)
	{
		char uid[32];
		char errmsg[128];

		sprintf(uid, "%ld", (long)getuid());

		strcpy(errmsg, "cannot find passwd entry for uid ");
		strcat(errmsg, uid);

		die(EC_PASSWD, errmsg);
		/* NOTREACHED */
	}

	/* set HOME */

	if((str = stralloc("HOME=", pw->pw_dir, (char *)0)) == (char *)0)
	{
		die(EC_MALLOC, ES_MALLOC);
		/* NOTREACHED */
	}
	if(putenv(str) != 0)
	{
		die(EC_PUTENV, ES_PUTENV);
		/* NOTREACHED */
	}

	/* set LOGNAME */

	if((str = stralloc("LOGNAME=", pw->pw_name, (char *)0)) == (char *)0)
	{
		die(EC_MALLOC, ES_MALLOC);
		/* NOTREACHED */
	}
	if(putenv(str) != 0)
	{
		die(EC_PUTENV, ES_PUTENV);
		/* NOTREACHED */
	}

	/* set USER */

	if((str = stralloc("USER=", pw->pw_name, (char *)0)) == (char *)0)
	{
		die(EC_MALLOC, ES_MALLOC);
		/* NOTREACHED */
	}
	if(putenv(str) != 0)
	{
		die(EC_PUTENV, ES_PUTENV);
		/* NOTREACHED */
	}

	/* set MCPATH, if it is not already set */

	/*
	 * If MCPATH is not set, we set it to:
	 *
	 * $HOME/mcidas/data:$HOME/mcidas/help:~mcidas/data:~mcidas/help
	 *
	 * This is what the mcidas.sh script sets it to by default.
	 */

	if(getenv("MCPATH") == (char *)0)
	{
		/* prepend ~mcidas/data:~mcidas/help to any MCPATH */

		if(mcidas_home != (char *)0)
		{
			str = stralloc(mcidas_home, "/", "help", (char *)0);
			if(str == (char *)0)
			{
				die(EC_MALLOC, ES_MALLOC);
				/* NOTREACHED */
			}
			if(prevar("MCPATH", str, ":") == -1)
			{
				die(EC_MCPATH, ES_MCPATH);
				/* NOTREACHED */
			}
			free(str);

			str = stralloc(mcidas_home, "/", "data", (char *)0);
			if(str == (char *)0)
			{
				die(EC_MALLOC, ES_MALLOC);
				/* NOTREACHED */
			}
			if(prevar("MCPATH", str, ":") == -1)
			{
				die(EC_MCPATH, ES_MCPATH);
				/* NOTREACHED */
			}
			free(str);
		}

		/* prepend $HOME/mcidas/data:$HOME/mcidas/help to any MCPATH */

		str = stralloc(pw->pw_dir, "/", "mcidas/help", (char *)0);
		if(str == (char *)0)
		{
			die(EC_MALLOC, ES_MALLOC);
			/* NOTREACHED */
		}
		if(prevar("MCPATH", str, ":") == -1)
		{
			die(EC_MCPATH, ES_MCPATH);
			/* NOTREACHED */
		}
		free(str);

		str = stralloc(pw->pw_dir, "/", "mcidas/data", (char *)0);
		if(str == (char *)0)
		{
			die(EC_MALLOC, ES_MALLOC);
			/* NOTREACHED */
		}
		if(prevar("MCPATH", str, ":") == -1)
		{
			die(EC_MCPATH, ES_MCPATH);
			/* NOTREACHED */
		}
		free(str);
	}

	/* prepend to any PATH */

	/*
	 * We prepend $HOME/bin:$HOME/mcidas/bin:~mcidas/bin to PATH
	 *
	 * Naturally, we do the prepending in reverse order.
	 *
	 * The directories, relative to $HOME, where the executables
	 * live.  This choice of paths is for compatibility with
	 * current McIDAS, more than anything else.  (It could be
	 * completely different...)
	 *
	 * There should be a mechanism for expanding the PATH
	 * setting beyond this, possibly via the command line.
	 *
	 * bindirtail0 will precede bindirtail1 in PATH.
	 */

	/* prepend ~mcidas/bin to any PATH */

	if(mcidas_home != (char *)0)
	{
		str = stralloc(mcidas_home, "/", "bin", (char *)0);
		if(str == (char *)0)
		{
			die(EC_MALLOC, ES_MALLOC);
			/* NOTREACHED */
		}
		if(prevar("PATH", str, ":") == -1)
		{
			die(EC_PATH, ES_PATH);
			/* NOTREACHED */
		}
		free(str);	/* if got this far, str nonnull so safe to call free */
	}

	/* prepend $HOME/mcidas/bin to any PATH */

	str = stralloc(pw->pw_dir, "/", "mcidas/bin", (char *)0);
	if(str == (char *)0)
	{
		die(EC_MALLOC, ES_MALLOC);
		/* NOTREACHED */
	}
	if(prevar("PATH", str, ":") == -1)
	{
		die(EC_PATH, ES_PATH);
		/* NOTREACHED */
	}
	free(str);	/* if got this far, str nonnull so safe to call free */

	/* prepend $HOME/bin to any PATH */

	str = stralloc(pw->pw_dir, "/", "bin", (char *)0);
	if(str == (char *)0)
	{
		die(EC_MALLOC, ES_MALLOC);
		/* NOTREACHED */
	}
	if(prevar("PATH", str, ":") == -1)
	{
		die(EC_PATH, ES_PATH);
		/* NOTREACHED */
	}
	free(str);	/* if got this far, str nonnull so safe to call free */

	/*
	 * construct name of data directory & chdir there
	 *
	 * If our user id is that of the mcidas account, we
	 * use $HOME/data, otherwise we use $HOME/mcidas/data.
	 *
	 * The directory, relative to $HOME, is where the data lives.
	 * We chdir there for compatibility with current McIDAS.
	 *
	 * (Now that MCPATH is in core McIDAS-X, it doesn't really matter
	 * what our current directory is, since files are located via
	 * MCPATH.)
	 */

	dir = stralloc(pw->pw_dir, "/",
			(getuid() == mcidas_uid) ? "data" : "mcidas/data",
			(char *)0);
	if(dir == (char *)0)
	{
		die(EC_MALLOC, ES_MALLOC);
		/* NOTREACHED */
	}
	if(chdir(dir) == -1)
	{
		char *errmsg;
		int	err;

		/* save errno before doing any other library calls */
		err = errno;

		errmsg = stralloc("cannot chdir to "
					, dir, ": ", strerror(err)
					, (char *)0);

		die(EC_CHDIR, errmsg);
		/* NOTREACHED */
	}
	free(dir);

	/* close the passwd file */

	endpwent();

	/* free our copy of the home directory of mcidas, if any */

	if(mcidas_home != (char *)0)
	{
		free(mcidas_home);
	}
}

/*
 * prepend the given value to any existing value for the given environment
 * variable var.
 * separate the new value from any old value with the given
 * separator string.
 *
 * makes local copies of all args as necessary, so the caller
 * is responsible for freeing his own memory.
 *
 * return 0 on success, -1 on failure.
 */

static int
prevar(const char *var, char *val, const char *sep)
{
	char *oldval;
	char *newval;

	if(!var || !val)
	{
		return -1;
	}
	if(!sep)
	{
		sep = "";
	}

	/* get the old value, if any */

	oldval = getenv(var);

	/* construct the new value */

	if(oldval)
	{
		newval = stralloc(var, "=", val, sep, oldval, (char *)0);
	}
	else
	{
		newval = stralloc(var, "=", val, (char *)0);
	}

	/* put it in */
	if(newval == (char *)0)
	{
		die(EC_MALLOC, ES_MALLOC);
		/* NOTREACHED */
	}
	if(putenv(newval) != 0)
	{
		die(EC_PUTENV, ES_PUTENV);
		/* NOTREACHED */
	}

	return 0;
}

/* call to foreign host; returns fd of socket or doesn't return at all. */
static int
getsockfd(Mcuint4 netaddr, unsigned short netport)
{
	int		s;
	int             rc;
	struct sockaddr_in foreign;	/* foreign address   */

	s = socket(AF_INET, SOCK_STREAM, 0);
	if (s < 0)
	{
		die(EC_SOCKET, ES_SOCKET);
		/* NOTREACHED */
	}

	memset(&foreign, 0, sizeof(foreign));
	foreign.sin_family = AF_INET;
	foreign.sin_port = netport;
	foreign.sin_addr.s_addr = netaddr;

	rc = connect(s, (struct sockaddr *)&foreign, sizeof(foreign));
	if (rc < 0)
	{
		close(s);
		die(EC_CONNECT, ES_CONNECT);
		/* NOTREACHED */
	}

	return s;
}

/* cpio() - copy the given number of bytes from ifd to ofd
 *
 * ifd - input file descriptor
 * ofd - output file descriptor
 * numbyte - number of bytes to copy from ifd to ofd
 *
 * returns -1 on error, 0 on success.
 */

static int
cpio(int ifd, int ofd, size_t numbyte)
{
	size_t            byteleft;

	byteleft = numbyte;
	while (byteleft > 0)
	{
		ssize_t         nr;
		size_t          nread;
		ssize_t         rc;
		char		buffer[1024];

		if (byteleft < sizeof(buffer))
		{
			nread = byteleft;
		}
		else
		{
			nread = sizeof(buffer);
		}

		nr = M0adderead(ifd, buffer, nread);
		if (nr == 0 || nr == -1)
		{
			return -1;
		}

		rc = M0addewrite(ofd, buffer, nr);	/* copy bytes to user */
		if (rc != nr)
		{
			return -1;
		}

		byteleft -= nr;
	}
	return 0;
}

/* strdn() - downcase the given C string */

static void
strdn(char *s)
{
	for(; *s; s++)
	{
		if(isascii(*s) && isupper(*s))
		{
			*s = tolower(*s);
		}
	}
}

/*
 * send an error to stdout and exit
 */

static void
die(Mcint4 code, const char *text)
{
	Mcint4          repl[24];
	char		defbuf[80];

	/*
	 * Do something reasonable if called with a null pointer or
	 * empty string.
	 */

	if(text == (char *)0 || text[0] == '\0')
	{
		sprintf(defbuf, "error code %d (no text)", code);
		text = &defbuf[0];
	}

	/*
	 * If we have read the version header AND can deal with
	 * the version, then do any other reads that are necessary
	 * before we can send the error message.
	 *
	 * Otherwise, blindly send the error message and hope for the
	 * best.
	 */

	if(have_verhdr && (client_version == PROTOCOL_VERSION))
	{
		/* read the pre-request header if necessary */

		if(!have_prehdr)
		{
			Prehdr		pre;

			M0adderead(STDIN, &pre, sizeof(pre));
			have_prehdr = 1;
		}

		/* read the request header if necessary */

		if(!have_reqhdr)
		{
			Reqhdr          req;

			M0adderead(STDIN, &req, sizeof(req));
			have_reqhdr = 1;
		}
	}

	/* send the error */

	assert(sizeof(repl) == ANSLEN);

	memset(&repl[0], 0, sizeof(repl));

	/* length of data in response */
	repl[0] = 0;

	/* condition code, in network byte order */
	repl[2] = htonl(code);

	/* message text */
	strtofs((char *)&repl[3], text, ANSTEXTLEN);

	(void) M0addewrite(STDOUT, repl, ANSLEN);

	exit(0);
}