/*
	sgopher.c - a very simple client for the internet gopher

	author      : Sean Fuller (fuller@aedc-vax.af.mil)
	              Arnold Engineering and Development Center

	This software is provided "as is" without express or
	implied warranty.  In no event shall the author be liable
	for damages of any kind arising from or in connection
	with the use of this software.

	4/27/1993 - Wrote and released initial version.
	4/28/1993 - Added connection information logging and
	            pulled configuration information out to
	            the make file. Also added help command.
	            Added ability to mail received text.
	4/29/1993 - Added support for gopher indexes.
	            Added ongoing menu download indication.
	            Fixed empty menu retrieval.
	            Moved ask up so not implicitly declared.
	5/3/1993 -  Gopher mail now from GOPHADDR and logged.
	            Added 'Subject:' and 'To:' line in mail sent.
	5/4/1993 -  now porting to VMS
	5/5/1993 -  got it to compile and run on VMS but no items found
	            getting readline errors on server
	5/6/1993 -  Modified for ULTRIX compilation.
	            fixed it so that it doesn't exit on bad connections.
	            fixed index retrieval.
	5/7/1993 -  Added code for doing timeout.
	5/10/1993 - Added = option for retrieval information.
*/

#include "conf.h"

#ifdef VMS
#define read netread
#define write netwrite
#endif

#include <stdio.h>
#include <ctype.h>
#ifdef VMS
#include <stdlib.h>
#include "twg$tcp:[netdist.include.sys]types.h"
#include "twg$tcp:[netdist.include.sys]socket.h"
#include "twg$tcp:[netdist.include.sys]time.h"
#include "twg$tcp:[netdist.include.netinet]in.h"
#include "twg$tcp:[netdist.include.arpa]inet.h"
#include "twg$tcp:[netdist.include]netdb.h"
#else
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#endif
#include <string.h>
#include <time.h>
#ifdef TIMEOUT
#ifdef VMS
#include "twg$tcp:[netdist.include.sys]signal.h"
#else
#include <sys/signal.h>
#endif
#endif

#define BUFSIZE 256
char buf[BUFSIZE];
char title[BUFSIZE];
int fd;
int lines_per_page = 22;

struct gopher_object;
struct gopher_object {
	char *name;
	char *path;
	char *host;
	char *port;
	char viewed;
	struct gopher_object *next;
};

struct stack_item;
struct stack_item {
	struct gopher_object *item;
	int top;
	char title[BUFSIZE];
	struct stack_item *next;
};

struct gopher_object root_object =
{
	"Root Connection", "", HOST, PORT, 0, NULL
};

struct stack_item *gopher_stack = NULL;

#ifdef NEED_STRDUP
char *
#ifdef KRC
strdup(s)
	char *s;
#else
strdup(char *s)
#endif
{
	char *d;
	d = malloc(sizeof(char) * (strlen(s) + 1));
	if (d == NULL)
	{
		printf("Error: out of memory\n");
		exit(0);
	}
	strcpy(d, s);
	return d;
}
#endif

#ifdef TIMEOUT
/*
	handle_sigalrm - received SIGALRM signal, do cleanup and exit.
*/
void
#ifdef KRC
handle_alarm(sig, code, sc)
	int sig, code;
	struct sigcontext *sc;
#else
handle_alarm(int sig, int code, struct sigcontext *sc)
#endif
{
	printf("\r\n");
	printf("Inactive for %d minute%s.  ", TIMEOUT, (TIMEOUT > 1)?"s":"");
	printf("Terminating Gopher client...\r\n");
	exit(0);
}
#endif

/*
	user_input - get string from the user - may use timeout
*/
char *
#ifdef KRC
user_input(buf, bufsize, fp)
	char *buf;
	int bufsize;
	FILE *fp;
#else
user_input(char *buf, int bufsize, FILE *fp)
#endif
{
	char *s;
#ifdef TIMEOUT
	alarm(TIMEOUT * 60); /* set timeout alarm */
#endif
	s = fgets(buf, BUFSIZE, stdin);
#ifdef TIMEOUT
	alarm(0); /* reset timeout alarm */
#endif
#ifdef VMS
	printf("\n");
#endif
	return s;
}

/*
	user_wait - wait for user to press enter
*/
void user_wait()
{
	printf("press <return> to continue: ");
	fflush(stdout);
	user_input(buf, BUFSIZE, stdin);
}

/*
	find_item - return item in menu given number
*/
struct gopher_object *
#ifdef KRC
find_item(g, num)
	struct gopher_object *g;
	char *num;
#else
find_item(struct gopher_object *g, char *num)
#endif
{
	int i;
	i = atoi(num);
	if (i <= 0)
	{
		printf("Item numbers must be greater than 0.\r\n");
		user_wait();
		return NULL;
	}
	while (i > 1 && g)
	{
		i--;
		g = g->next;
	}
	if (g == NULL)
	{
		printf("Item number is too big.\r\n");
		user_wait();
		return NULL;
	}
	return g;
}

#ifdef LOGFILE
/*
	get_client_info - get the ip address and hostname of originator
		and write it out to the log file
*/
void get_client_info()
{
	char ipnum[256];
	char name[256];
	char logtime[256];
        struct sockaddr_in sa;
        int length;
        int rc;
	FILE *fp;
	time_t the_time;
        u_long net_addr;
        struct hostent *hp;
        length = sizeof(sa);
        rc = getpeername(0, &sa, &length);
        if (rc != 0) strcpy(ipnum, "localhost");
        else strcpy(ipnum, inet_ntoa(sa.sin_addr));
	hp = gethostbyaddr(&sa.sin_addr, sizeof(sa.sin_addr), AF_INET);
	the_time = time(0);
	strcpy(logtime, ctime(&the_time));
	if (strlen(logtime) > 0) logtime[strlen(logtime) - 1] = 0;
	if (hp != NULL)
	{
		printf("Connected from %s(%s) at %s\r\n", hp->h_name, ipnum,
			logtime);
		strcpy(name, hp->h_name);
	}
	else
	{
		printf("Connected from %s at %s\r\n", ipnum, logtime);
		strcpy(name, "unknown");
	}
	fp = fopen(LOGFILE, "a");
	if (fp == NULL) return;
	fprintf(fp, "%s %s %s\n", logtime, ipnum, name);
	fclose(fp);
}
#endif

/*
	push_menu - push menu and top item on stack
*/
void
#ifdef KRC
push_menu(g, top)
	struct gopher_object *g;
	int top;
#else
push_menu(struct gopher_object *g, int top)
#endif
{
	struct stack_item *s;
	s = (struct stack_item *)malloc(sizeof(struct stack_item));
	if (s == NULL)
	{
		printf("Error: out of memory\r\n");
		return;
	}
	s->item = g;
	s->top = top;
	s->next = gopher_stack;
	strcpy(s->title, title);
	gopher_stack = s;
}

/*
	pop_menu - pop previous menu and top item from stack
*/
struct gopher_object *
#ifdef KRC
pop_menu(top)
	int *top;
#else
pop_menu(int *top)
#endif
{
	struct gopher_object *g;
	struct stack_item *i;
	if (gopher_stack == NULL) return NULL;
	i = gopher_stack;
	g = i->item;
	gopher_stack = i->next;
	*top = i->top;
	strcpy(title, i->title);
	free(i);
	return g;
}

/*
	readport - read a line in from the socket to the gopher server
*/
int readport()
{
	int i, l;
	char *s = buf;
	l = 0;
	while ((i = read(fd, s, 1)) > 0)
	{
		l += i;
		if (*s == '\n') break;
		if (*s != '\r') s++;
	}
	*s = 0;
	return l;
}

/*
	close_port - close the connection to the gopher server
*/
void close_port()
{
	close(fd);
}

/*
	ask - make the connection to the gopher server and request item
*/
int
#ifdef KRC
ask(g)
	struct gopher_object *g;
#else
ask(struct gopher_object *g)
#endif
{
	struct sockaddr_in sin;
	struct hostent *host;
	int port;
	printf("Connecting...\r\n");
	fflush(stdout);
	bzero((char*)&sin, sizeof(sin));
	port = atoi(g->port);
	sin.sin_port = htons(port);
	host = gethostbyname(g->host);
	if (host == NULL)
	{
		sin.sin_addr.s_addr = inet_addr(g->host);
	}
	else
	{
		memcpy(&sin.sin_addr.s_addr, host->h_addr_list[0],
			host->h_length);
	}
	sin.sin_family = AF_INET;
	fd = socket(AF_INET, SOCK_STREAM, 0);
	if (fd < 0)
	{
		printf("Error: gopher socket unavailable\r\n");
		return 0;
	}
	if (connect(fd, (struct sockaddr *)&sin, sizeof(sin)) < 0)
	{
		printf("Error: gopher server unavailable\r\n");
		return 0;
	}
	if (write(fd, g->path, strlen(g->path)) == -1)
	{
		printf("Error: write error\r\n");
		return 0;
	}
	if (write(fd, "\n", 1) == -1)
	{
		printf("Error: write error\r\n");
		return 0;
	}
	return 1;
}

/*
	mail - Mail gopher item to given email address.
		There is no check to see if it worked.
*/
void
#ifdef KRC
mail(g, email)
	struct gopher_object *g;
	char *email;
#else
mail(struct gopher_object *g, char *email)
#endif
{
	char buf[BUFSIZE];
	struct sockaddr_in sin;
	struct hostent *host;
	char c;
	int mfd;
	FILE *fp;
	/*
		open socket to mailer port on gopher host
	*/
	printf("Sending Mail...\r\n");
	fflush(stdout);
	bzero((char*)&sin, sizeof(sin));
	sin.sin_port = htons(25);
	host = gethostbyname(HOST);
	if (host == NULL)
	{
		sin.sin_addr.s_addr = inet_addr(g->host);
	}
	else
	{
		memcpy(&sin.sin_addr.s_addr, host->h_addr_list[0],
			host->h_length);
	}
	sin.sin_family = AF_INET;
	mfd = socket(AF_INET, SOCK_STREAM, 0);
	if (mfd < 0)
	{
		printf("Error: mail socket unavailable\r\n");
		return;
	}
	if (connect(mfd, (struct sockaddr *)&sin, sizeof(sin)) < 0)
	{
		printf("Error: mail server unavailable\r\n");
		return;
	}
	/*
		send header information
	*/
	sprintf(buf, "HELO %s\n", HOST);
	write(mfd, buf, strlen(buf));
	sprintf(buf, "MAIL From:<%s>\n", GOPHADDR);
	write(mfd, buf, strlen(buf));
	sprintf(buf, "RCPT To:<%s>\n", email);
	write(mfd, buf, strlen(buf));
	sprintf(buf, "DATA\n");
	write(mfd, buf, strlen(buf));
	sprintf(buf, "To: %s\n", email);
	write(mfd, buf, strlen(buf));
	sprintf(buf, "Subject: Gopher: %s\n", g->name + 1);
	write(mfd, buf, strlen(buf));
	if (ask(g))
	{
		while (read(fd, &c, 1) > 0)
		{
			write(mfd, &c, 1);
		}
		close_port();
	}
	sprintf(buf, "\n.\n");
	write(fd, buf, strlen(buf));
	sprintf(buf, "BYE\n");
	write(fd, buf, strlen(buf));
	printf("Message sent.\r\n");
	fflush(stdout);
	close(mfd);
#ifdef LOGFILE
	/*
		Log mail sent.
	*/
	fp = fopen(LOGFILE, "a");
	if (fp == NULL) return;
	fprintf(fp, "Sending %s %s %s to %s\n",
		g->path, g->host, g->port, email);
	fclose(fp);
#endif
}

/*
	parse_menu_item - parse the menu item returned by gopher
*/
struct gopher_object *
#ifdef KRC
parse_menu_item(buf)
	char *buf;
#else
parse_menu_item(char *buf)
#endif
{
	struct gopher_object *g;
	char *s[4];
	int i;
	char *p;
	g = (struct gopher_object *)malloc(sizeof(struct gopher_object));
	if (g == NULL)
	{
		printf("Error: out of memory\r\n");
		return NULL;
	}
	p = buf;
	i = 0;
	while (*p && i < 4)
	{
		s[i] = p;
		while (*p && *p != '\t') p++;
		if (*p)
		{
			*p = 0;
			p++;
			i++;
		}
	}
	g->name = strdup(s[0]);
	g->path = strdup(s[1]);
	g->host = strdup(s[2]);
	g->port = strdup(s[3]);
	g->next = NULL;
	g->viewed = ' ';
	if (strlen(g->name) > 65) g->name[65] = 0;
	return g;
}

/*
	get_text - Get text document and display it.
		The gopher port is kept open during reading.
		Lines are truncated to 79 columns.
*/
void
#ifdef KRC
get_text(g)
	struct gopher_object *g;
#else
get_text(struct gopher_object *g)
#endif
{
	char input[BUFSIZE];
	int i;
	if (ask(g) == 0) return;
	printf("Getting text...\r\n");
	fflush(stdout);
	i = -1;
	while (readport() > 0 && strcmp(buf, "."))
	{
		i++;
		if (i > lines_per_page)
		{
			printf("press <enter> for more or <q> to quit: ");
			fflush(stdout);
			user_input(input, BUFSIZE, stdin);
			if (*input == 'q') break;
			i = 0;
			printf("\r\n");
		}
		buf[79] = 0;
		printf("%s\r\n", buf);
	}
	printf("press <enter> to continue or <m> to mail: ");
	fflush(stdout);
	user_input(input, BUFSIZE, stdin);
	close_port();
	if (*input == 'm')
	{
		printf("enter email address: ");
		fflush(stdout);
		user_input(input, BUFSIZE, stdin);
		for (i=strlen(input) - 1; i>=0; i--)
		{
			if (input[i] == '\r' || input[i] == '\n')
				input[i] = 0;
		}
		mail(g, input);
	}
}

/*
	get_menu - given path get menu
*/
struct gopher_object *
#ifdef KRC
get_menu(g)
	struct gopher_object *g;
#else
get_menu(struct gopher_object *g)
#endif
{
	char group[BUFSIZE];
	int i;
	struct gopher_object *first = NULL;
	struct gopher_object *current = NULL;
	struct gopher_object *new;
	if (ask(g))
	{
		printf("Getting directory...");
		fflush(stdout);
		while (readport() > 0 && strcmp(buf, ".") && strlen(buf) > 0)
		{
			printf(".");
			fflush(stdout);
			new = parse_menu_item(buf);
			if (first)
			{
				current->next = new;
				current = new;
			}
			else
			{
				first = current = new;
			}
		}
		close_port();
		printf("\r\n");
	}
	if (first == NULL)
	{
		first = pop_menu(&i);
		printf("\r\n");
		printf("No menu items available.\r\n");
		user_wait();
	}
	return first;
}

/*
	showmenu - show a menu given pointer to linked list 
*/
void
#ifdef KRC
show_menu(g, top)
	struct gopher_object *g;
	int top;
#else
show_menu(struct gopher_object *g, int top)
#endif
{
	int i = 1;
	char *object_type;
	printf("\r\n");
	printf("%s\r\n", title);
	if (g == NULL)
	{
		printf("No items available\r\n");
	}
	while (g)
	{
		if (g->name[0] == '0') object_type = "text";
		else if (g->name[0] == '1') object_type = "dir";
		else if (g->name[0] == '7') object_type = "indx";
		else if (g->name[0] == '3') object_type = "err";
		else object_type = "?";
		if (i >= top) printf("%3d) %c%-65s [%s]\r\n", i, g->viewed,
			g->name + 1, object_type);
		if (i - top >= lines_per_page - 1) break;
		i++;
		g = g->next;
	}
	printf("Enter #=select p=previous page n=next page u=uplevel ?=help q=quit: ");
}

/*
	next_page - find item number of next page or back to top if last
*/
int
#ifdef KRC
next_page(g, top)
	struct gopher_object *g;
	int top;
#else
next_page(struct gopher_object *g, int top)
#endif
{
	int i;
	i = 1;
	while (g && i < top)
	{
		i++;
		g = g->next;
	}
	if (g)
	{
		if (g->next == NULL)
		{
			return 1;
		}
		for (i=0; i<lines_per_page && g->next; i++)
		{
			top++;
			g = g->next;
		}
	}
	return top;
}

/*
	select_object - select an item by number
*/
struct gopher_object *
#ifdef KRC
select_object(buf, menu, top)
	char *buf;
	struct gopher_object *menu;
	int *top;
#else
select_object(char *buf, struct gopher_object *menu,
		int *top)
#endif
{
	int i;
	char input[BUFSIZE];
	struct gopher_object *g;
	struct gopher_object *go;
	g = menu;
	if ((g = find_item(menu, buf)) == NULL) return menu;
	g->viewed = '*';
	if (g->name[0] == '1')
	{
		push_menu(menu, *top);
		if (strlen(g->name) > 1) strcpy(title, g->name + 1);
		g = get_menu(g);
		if (g)
		{
			*top = 1;
			return g;
		}
		return menu;
	}
	if (g->name[0] == '0') get_text(g);
	else if (g->name[0] == '7')
	{
		printf("\r\n");
		i = strlen(g->name);
		if (i > 1) printf("%s\r\n", g->name + 1);
		printf("Enter argument(s): ");
		fflush(stdout);
		i = strlen(g->path);
		strcpy(input, g->path);
		strcat(input, "\t");
		user_input(input + i + 1, BUFSIZE - i - 2, stdin);
		if (strlen(input) > i + 1)
		{
			go = (struct gopher_object *)malloc(
				sizeof(struct gopher_object));
			if (go == NULL)
			{
				printf("Error: out of memory\r\n");
				return menu;
			}
			go->name = strdup(g->name);
			go->path = strdup(input);
			go->host = strdup(g->host);
			go->port = strdup(g->port);
			push_menu(menu, *top);
			strcpy(title, g->name + 1);
			g = get_menu(go);
			free(go);
			if (g)
			{
				*top = 1;
				return g;
			}
			return menu;
		}
	}
	else
	{
		printf("\r\n");
		printf("The format of the Gopher item that you have selected cannot be retrieved\r\n");
		printf("or displayed by this gopher client.  It is probably a binary file that\r\n");
		printf("contains a picture, music, or program executables.  To retrieve these types\r\n");
		printf("of files you will have to run a different gopher client.  Examples of\r\n");
		printf("other clients that can retrieve these types of data are Xgopher which\r\n");
		printf("runs on UNIX machines and UTGopher which runs on PCs with PC/TCP.\r\n");
		user_wait();
	}
	return menu;
}

/*
	free_directory - free gopher_objects in menu
*/
void
#ifdef KRC
free_directory(menu)
	struct gopher_object *menu;
#else
free_directory(struct gopher_object *menu)
#endif
{
	struct gopher_object *g;
	while (menu)
	{
		g = menu;
		menu = menu->next;
		free(g->name);
		free(g->host);
		free(g->path);
		free(g->port);
		free(g);
	}
}

/*
	help - show help information
*/
void help()
{
	char buf[BUFSIZE];
	printf("\r\n");
	printf("                SIMPLE GOPHER CLIENT HELP\r\n");
	printf("\r\n");
	printf("dir  - a directory of items that you can choose from\r\n");
	printf("text - a text document you can view or email\r\n");
	printf("indx - index type will request input and retrieve a directory\r\n");
	printf("err  - an error that was returned by a server\r\n");
	printf("?    - is displayed for all other types\r\n");
	printf("\r\n");
	printf("command       desc\r\n");
	printf("------------- -----------------------------------------------------------\r\n");
	printf("<n><enter>    type a number then return to fetch an item\r\n");
	printf("n<enter>      to go to the next page of a menu\r\n");
	printf("<enter>       to go to the next page of a menu\r\n");
	printf("p<enter>      to go to the previous page of a menu\r\n");
	printf("?<enter>      to display this information\r\n");
	printf("q<enter>      to exit the gopher client\r\n");
	printf("=<n><enter>   show retrieval information for item\r\n");
	printf("\r\n");
#ifdef GOPHADM
#ifdef GOPHPHN
	printf("If you need more help call %s at %s.\r\n",
		GOPHADM, GOPHPHN);
#endif
#endif
	user_wait();
}

/*
	show_obj_info - show information about gopher object # on menu
*/
void
#ifdef KRC
show_obj_info(g, num)
	struct gopher_object *g;
	char *num;
#else
show_obj_info(struct gopher_object *g, char *num)
#endif
{
	int i;
	if ((g = find_item(g, num)) == NULL) return;
	printf("\r\n");
	printf("name: %s\r\n", g->name);
	printf("path: %s\r\n", g->path);
	printf("host: %s\r\n", g->host);
	printf("port: %s\r\n", g->port);
	printf("\r\n");
	user_wait();
}

/*
	main - parse the command line and call appropriate routines
		repeat until q<enter>
*/
int
#ifdef KRC
main(argc, argv)
	int argc;
	char *argv;
#else
main(int argc, char *argv[])
#endif
{
	char buf[BUFSIZE];
	struct gopher_object *g;
	int top = 1;
	if (argc > 1) root_object.host = argv[1];
	if (argc > 2) root_object.port = argv[2];
	if (argc > 3)
	{
		printf("Format: gopher [host [port]]\r\n");
	}
	strcpy(title, root_object.name);
#ifdef GOVCOMP
	printf("---------------------------------------------------------------------------\r\n");
	printf("WARNING!!  THIS IS A GOVERNMENT OWNED COMPUTER.\r\n");
	printf("---------------------------------------------------------------------------\r\n");
	printf("GOVERNMENT TELECOMMUNICATIONS SYSTEMS AND AUTOMATED INFORMATION SYSTEMS ARE\r\n");
	printf("SUBJECT TO PERIODIC SECURITY TESTING AND MONITORING TO ENSURE PROPER\r\n");
	printf("COMMUNICATIONS SECURITY (COMSEC) PROCEDURES ARE BEING OBSERVED.  USE OF THESE\r\n");
	printf("SYSTEMS CONSTITUTES CONSENT TO SECURITY TESTING AND COMSEC MONITORING.\r\n");
	printf("---------------------------------------------------------------------------\r\n");
	printf("FOR OFFICIAL USE ONLY.\r\n");
	printf("---------------------------------------------------------------------------\r\n");
#endif
#ifdef TIMEOUT
	signal(SIGALRM, handle_alarm);
#endif
	fflush(stdout);
#ifdef LOGFILE
	get_client_info();
#endif
	g = get_menu(&root_object);
	while (1)
	{
		show_menu(g, top);
		fflush(stdout);
		user_input(buf, BUFSIZE, stdin);
		if (*buf == 'q') break;
		switch (*buf)
		{
			case '?':
				help();
				break;
			case 'u':
				free_directory(g);
				g = pop_menu(&top);
				if (g == NULL)
				{
					g = get_menu(&root_object);
					top = 1;
				}
				break;
			case 'p':
				top = (top > lines_per_page)
					? top - lines_per_page : 1;
				break;
			case 'n': case '\n': case '\r': case 0:
				top = next_page(g, top);
				break;
			case '=':
				show_obj_info(g, buf + 1);
				break;
			default:
				g = select_object(buf, g, &top);
		}
	}
	return 0;
}
