/*
** Copyright 2001 Double Precision, Inc.  See COPYING for
** distribution information.
*/

static const char rcsid[]="$Id: mimegpgfork.c,v 1.3 2002/02/19 13:00:20 mrsam Exp $";

#include "config.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <ctype.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
#include "gpg.h"

#include <sys/types.h>
#if	HAVE_SYS_TIME_H
#include	<sys/time.h>
#endif
#if HAVE_SYS_WAIT_H
#include	<sys/wait.h>
#endif
#ifndef WEXITSTATUS
#define WEXITSTATUS(stat_val) ((unsigned)(stat_val) >> 8)
#endif
#ifndef WIFEXITED
#define WIFEXITED(stat_val) (((stat_val) & 255) == 0)
#endif

#include	"mimegpgfork.h"

extern char *passphrase_fd;

static int gpgmime_fork(const char *gpgdir,
		 int xargc, char **xargv, int argc, char **argv,
		 struct gpgmime_forkinfo *fi)
{
	int pipes[3][2];
	int n;
	pid_t p;

	for (n=0; n<3; n++)
	{
		if (pipe(pipes[n]) < 0)
		{
			while (n)
			{
				--n;
				close(pipes[n][0]);
				close(pipes[n][1]);
				return (-1);
			}
		}
	}

	if (fcntl(pipes[0][1], F_SETFL, O_NONBLOCK) ||
	    fcntl(pipes[1][0], F_SETFL, O_NONBLOCK) ||
	    fcntl(pipes[2][0], F_SETFL, O_NONBLOCK))
	{
		for (n=0; n<3; n++)
		{
			close(pipes[n][0]);
			close(pipes[n][1]);
		}
		return (-1);
	}

	signal(SIGCHLD, SIG_DFL);

	p=fork();
	if (p < 0)
	{
		for (n=0; n<3; n++)
		{
			close(pipes[n][0]);
			close(pipes[n][1]);
		}
		return (-1);
	}

	if (p == 0)
	{
		char **newargv;
		int i;
		const char *gpg;

		close(0);
		dup(pipes[0][0]);
		close(1);
		dup(pipes[1][1]);
		close(2);
		dup(pipes[2][1]);

		for (n=0; n<3; n++)
		{
			close(pipes[n][0]);
			close(pipes[n][1]);
		}

		newargv=malloc( (xargc + argc + 4) * sizeof(char *));
		if (!newargv)
		{
			perror("malloc");
			_exit(1);
		}

		i=0;
		newargv[i++]="gpg";
		if (passphrase_fd)
		{
			int n=atoi(passphrase_fd);

			if (lseek(n, 0L, SEEK_SET) < 0)
			{
				perror("passphrase-fd: seek");
				_exit(1);
			}

			newargv[i++]="--passphrase-fd";
			newargv[i++]=passphrase_fd;
		}

		for (n=0; n<xargc; n++)
			newargv[i++]=xargv[n];
		for (n=0; n<argc; n++)
			newargv[i++]=argv[n];

		newargv[i]=0;

		if (gpgdir)
		{
			char *s;

			s=malloc(sizeof("GNUPGHOME=")+strlen(gpgdir));
			if (!s)
			{
				perror("malloc");
				exit(1);
			}
			strcat(strcpy(s, "GNUPGHOME="), gpgdir);
			if (putenv(s) < 0)
			{
				perror("putenv");
				exit(1);
			}
		}

		gpg=getenv("GPG");
		if (!gpg || !*gpg)
			gpg=GPG;

		execvp(gpg, newargv);
		perror(gpg);
		_exit(1);
	}

	fi->gpg_errflag=0;
	fi->togpg_fd=pipes[0][1];
	close(pipes[0][0]);
	fi->fromgpg_fd=pipes[1][0];
	close(pipes[1][1]);
	fi->fromgpg_errfd=pipes[2][0];
	close(pipes[2][1]);

	fi->gpg_writecnt=0;
	fi->gpg_errcnt=0;
	fi->gpg_errbuf[0]=0;
	fi->gpg_pid=p;
	signal(SIGPIPE, SIG_IGN);

	fi->gpg_readhandler=0;
	fi->gpg_voidarg=0;
	return (0);
}

static void gpgmime_writeflush(struct gpgmime_forkinfo *);

void gpgmime_write(struct gpgmime_forkinfo *fi, const char *p, size_t n)
{
	while (n)
	{
		size_t i;

		if (fi->gpg_writecnt == sizeof(fi->gpg_writebuf))
			gpgmime_writeflush(fi);

		i=sizeof(fi->gpg_writebuf) - fi->gpg_writecnt;

		if ((size_t)i > n)
			i=n;

		memcpy(fi->gpg_writebuf + fi->gpg_writecnt, p, i);

		fi->gpg_writecnt += i;

		p += i;
		n -= i;
	}
}

static void gpgmime_read(struct gpgmime_forkinfo *fi, fd_set *fdr)
{
	char readbuf[BUFSIZ];
	int i;

	if (fi->fromgpg_fd >= 0 && FD_ISSET(fi->fromgpg_fd, fdr))
	{
		i=read(fi->fromgpg_fd, readbuf, sizeof(readbuf));

		if (i <= 0)
		{
			close(fi->fromgpg_fd);
			fi->fromgpg_fd= -1;
		}
		else
		{
			if (fi->gpg_readhandler)
				fi->gpg_errflag=
					(*fi->gpg_readhandler)(readbuf, i,
							       fi->gpg_voidarg
							       );
		}
	}

	if (fi->fromgpg_errfd >= 0 && FD_ISSET(fi->fromgpg_errfd, fdr))
	{
		i=read(fi->fromgpg_errfd, readbuf, sizeof(readbuf));

		if (i <= 0)
		{
			close(fi->fromgpg_errfd);
			fi->fromgpg_errfd= -1;
		}
		else
		{
			if (i >= sizeof(fi->gpg_errbuf)-1-fi->gpg_errcnt)
				i=sizeof(fi->gpg_errbuf)-1-fi->gpg_errcnt;
			if (i)
			{
				memcpy(fi->gpg_errbuf+fi->gpg_errcnt,
				       readbuf, i);
				fi->gpg_errbuf[fi->gpg_errcnt += i]=0;
			}
		}
	}
}

static void gpgmime_writeflush(struct gpgmime_forkinfo *fi)
{
	const char *p=fi->gpg_writebuf;
	unsigned n=fi->gpg_writecnt;

	while (n && !fi->gpg_errflag)
	{
		fd_set fdr, fdw;
		int maxfd=fi->togpg_fd, i;

		FD_ZERO(&fdr);
		FD_ZERO(&fdw);

		FD_SET(fi->togpg_fd, &fdw);

		if (fi->fromgpg_fd >= 0)
		{
			FD_SET(fi->fromgpg_fd, &fdr);
			if (fi->fromgpg_fd > maxfd)
				maxfd=fi->fromgpg_fd;
		}

		if (fi->fromgpg_errfd >= 0)
		{
			FD_SET(fi->fromgpg_errfd, &fdr);
			if (fi->fromgpg_errfd > maxfd)
				maxfd=fi->fromgpg_errfd;
		}

		if (select(maxfd+1, &fdr, &fdw, NULL, NULL) <= 0)
			continue;

		gpgmime_read(fi, &fdr);

		if (!FD_ISSET(fi->togpg_fd, &fdw))
			continue;

		i=write(fi->togpg_fd, p, n);

		if (i <= 0)
			fi->gpg_errflag=1;
		else
		{
			p += i;
			n -= i;
		}
	}
	fi->gpg_writecnt=0;
}

int gpgmime_finish(struct gpgmime_forkinfo *fi)
{
	pid_t p2;
	int waitstat;

	gpgmime_writeflush(fi);
	close(fi->togpg_fd);
	signal(SIGPIPE, SIG_DFL);

	while (!fi->gpg_errflag &&
	       (fi->fromgpg_fd >= 0 || fi->fromgpg_errfd >= 0))
	{
		fd_set fdr;
		int maxfd=0;

		FD_ZERO(&fdr);

		if (fi->fromgpg_fd >= 0)
		{
			FD_SET(fi->fromgpg_fd, &fdr);
			if (fi->fromgpg_fd > maxfd)
				maxfd=fi->fromgpg_fd;
		}

		if (fi->fromgpg_errfd >= 0)
		{
			FD_SET(fi->fromgpg_errfd, &fdr);
			if (fi->fromgpg_errfd > maxfd)
				maxfd=fi->fromgpg_errfd;
		}

		if (select(maxfd+1, &fdr, NULL, NULL, NULL) <= 0)
			continue;

		gpgmime_read(fi, &fdr);
	}

	while ((p2=wait(&waitstat)) != fi->gpg_pid)
	{
		if (p2 < 0 && errno == ECHILD)
			break;
	}

	if (fi->gpg_errflag == 0)
	{
		if (!WIFEXITED(waitstat))
			fi->gpg_errflag=1;
		else
			fi->gpg_errflag=WEXITSTATUS(waitstat);
	}
	return (fi->gpg_errflag);
}

int gpgmime_fork_signencrypt(const char *gpgdir,
			     int flags,
			     int argc, char **argv,
			     int (*output_func)(const char *, size_t, void *),
			     void *output_voidarg,
			     struct gpgmime_forkinfo *gpgfi)
{
	char *xargvec[5];
	int xargc=0;
	int rc;

	if (flags & GPG_SE_SIGN)
	{
		xargvec[xargc++]="-s";
		if (! (flags & GPG_SE_ENCRYPT))
			xargvec[xargc++]="-b";
	}

	if (flags & GPG_SE_ENCRYPT)
		xargvec[xargc++]="-e";

	xargvec[xargc++]="-a";

	rc=gpgmime_fork(gpgdir, xargc, xargvec, argc, argv, gpgfi);

	gpgfi->gpg_readhandler= output_func;
	gpgfi->gpg_voidarg=output_voidarg;
	return (rc);
}

int gpgmime_forkchecksign(const char *gpgdir,
			  const char *content_filename,
			  const char *signature_filename,
			  int argc, char **argv,
			  struct gpgmime_forkinfo *gpgfi)
{
	char *xargvec[3];
	char **newargv;
	int i;
	int rc;

	xargvec[0]="--verify";
	xargvec[1]="--charset";
	xargvec[2]=GPG_CHARSET;

	newargv=(char **)malloc((argc+2)*sizeof(char *));
	if (!newargv)
	{
		perror("malloc");
		exit(1);
	}

	for (i=0; i<argc; i++)
		newargv[i]=argv[i];
	newargv[i++]=(char *)signature_filename;
	newargv[i++]=(char *)content_filename;

	rc=gpgmime_fork(NULL, 3, xargvec, i, newargv, gpgfi);
	free(newargv);
	return (rc);
}

int gpgmime_forkdecrypt(const char *gpgdir,
			int argc, char **argv,
			int (*output_func)(const char *, size_t, void *),
			void *output_voidarg,
			struct gpgmime_forkinfo *gpgfi)
{
	char *xargv[3];
	int rc;

	xargv[0]="--decrypt";
	xargv[1]="--charset";
	xargv[2]=GPG_CHARSET;

	rc=gpgmime_fork(NULL, 3, xargv, argc, argv, gpgfi);

	gpgfi->gpg_readhandler= output_func;
	gpgfi->gpg_voidarg= output_voidarg;
	return (rc);
}

const char *gpgmime_getoutput(struct gpgmime_forkinfo *gpgfi)
{
	return (gpgfi->gpg_errbuf);
}

const char *gpgmime_getcharset(struct gpgmime_forkinfo *gpgfi)
{
	return (GPG_CHARSET);
}
