/*

PPRD

Line printer daemon using Berkeley LPD protocol on top of WATTCP for up
to 3 parallel printer ports.

This daemon is written as a state machine because it has to support
several connections on a single threaded system, DOS.

When started up, it checks to see how many printers are known to the
BIOS. These printers are served as LPT[123].  It listens on the
standard LPD port 515, but this can be changed from the command line.

Examples of options:

-p1515	listen on port 1515 instead
-j9200	use direct protocol instead at port 9100 (defaults to 9100 if unspec)
-23	disable printers 2 and 3
-n2	two printers, no matter what BIOS claims
-b12	on 1 and 2 bypass BIOS and send directly to port (parallel ports only)
-t	don't indicate available printers with tones
-i	reinitialise printer via hardware line on job abort
-s	disable subnet match (server and client must be on same subnet)
-alist	comma separated list of up to 20 domain names allowed connection
-dlist	comma separated list of up to 20 domain names denied connection
	last two are mutually exclusive and independent of subnet check
-lhost	log diagnostics to host of that domain name

Jobs can be aborted by C-F1 through C-F3 for that printer.

The LPD protocol is summarised below where S = server and C = client.
Ack = '\0' and nak = '\001'.

C: \002printer\n
S: ack/nak
C: \003size datafilename\n	(size is bytes as decimal string)
S: ack/nak
C: size bytes of data + one '\0' byte
S: ack/nak
C: \002size cntlfilename\n
S: ack/nak
C: size bytes of control + one '\0' byte
S: ack/nak

The data and control files can occur in either order.  In this
implementation the filenames are ignored.

This program was compiled under Borland C++ 3.1 in C mode for the small
model. You will require the WATTCP libraries. A copy is included in the
distribution. For the sources of WATTCP, ask archie where the archives
are.

Please send bug fixes, ports and enhancements to the author for
incorporation in newer versions.

Copyright (C) 1995  Ken Yap (ken@syd.dit.csiro.au)

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 of the License, 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.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.

*/

#include	<stdio.h>
#include	<stdlib.h>
#include	<string.h>
#include	<memory.h>
#include	<sys/types.h>
#include	<time.h>

#include	<tcp.h>

#include	<dos.h>
#include	<bios.h>
#include	<conio.h>

#include	"pprd.h"

#define		PROGRAM		"PPRD"
#define		VERSION		"Version 1.00 15 Jul 1995"
#define		AUTHOR		"Copyright (C) 1995 Ken Yap (ken@syd.dit.csiro.au)"

#define		MAXLPT		3	/* supported under DOS */
#ifdef		unix			/* test implementation */
#define		MAXCON		1
#else
/* for lpr, 1 for lpq, and direct */
#define		MAXCON		(MAXLPT+1)
#endif
/* try this many times to see if can accept next byte right away */
#define		POLL_MAX	10

unsigned int		lpdport = LPDPORT;
unsigned int		jdport = JDPORT;
int			jdprotocol = 0;	/* use direct protocol if != 0 */
int			nlpt = 0;
struct lpt_info		lpt[MAXLPT];
int			notone = 0;
int			reinit = 0;	/* reinitialise on abort */
unsigned int		lpt_tone[MAXLPT] = { 523, 587, 659 };	/* CDE */
char			lpt_names[MAXLPT][16] = { "lpt1", "lpt2", "lpt3" };
struct conn_info	conn[MAXCON];	/* one extra for lpq/lprm requests */
struct conn_info	log;		/* for connection to syslogd */
char			*loghost = 0;
int			check_subnet = 1;
int			nallow = 0, ndeny = 0;
longword		allow[20], deny[20];
void            	(*normal_init)(char *name, char *value);

#define		MAXALLOW	(sizeof(allow)/sizeof(allow[0]))
#define		MAXDENY		(sizeof(deny)/sizeof(deny[0]))

extern longword		my_ip_addr;
extern longword		sin_mask;

#define	ack_cmd(c)	(void)sock_fastwrite(&c->sock, (byte *)"", 1)
#define	nak_cmd(c)	(void)sock_fastwrite(&c->sock, (byte *)"\001", 1)

char *ptime(void)
{
	time_t	t;
	char	*p;

	t = time(0);
	p = ctime(&t);
	p[24] = '\0';
	return (p);
}

void init_queues(void)
{
	struct conn_info	*c;
	int			i, ret;
	struct lpt_info		*p;

	if (nlpt == 0)				/* not overridden by -n? */
	{
		ret = biosequip();
		nlpt = (ret >> 14) & 0x3;
	}
	for (i = 0; i < nlpt; ++i)
	{
		p = &lpt[i];
		if (p->avail == DISABLED)	/* unavail from cmd line? */
			continue;
		(void)bios_printer_init(i);	/* initialise printer */
		sleep(2);			/* let printer settle */
		p->status = bios_printer_status(i);	/* get status */
		if (p->status & (P_TIMEOUT|P_IOERROR))
		{
			p->avail = NONESUCH;	/* printer not available */
			(void)printf("%s: Error initialising %s\n",
				ptime(), lpt_names[i]);
		}
		else
		{
			p->avail = FREE;	/* printer available */
			(void)printf("%s: %s initialised",
				ptime(), lpt_names[i]);
			if (p->hwaddr && (ret = peek(0, 0x408 + i * 2)) != 0)
				(void)printf(", direct hardware access at %#3X",
					p->hwaddr = ret);
			(void)printf("\n");
		}
	}
	for (i = 0; i < MAXLPT; ++i)
	{
		if (notone || lpt[i].avail != FREE)
			continue;
		sound(lpt_tone[i]);
		sleep(1);
		nosound();
	}
	for (i = 0; i < MAXCON; ++i)
	{
		c = &conn[i];
		c->state = INIT;		/* initial connection state */
		c->controlfirst = 0;
		c->printer = -1;
	}
}

void init_log(void)
{
	longword		logip;

	if (loghost == 0 ||
		(logip = resolve(loghost)) == (longword)0 ||
		udp_open(&log.sock, 0, logip, LOGPORT, NULL) == 0)
	{
		loghost = 0;
		return;
	}
	(void)sprintf(log.buffer, LOG_TAG PROGRAM " " VERSION
		", %d printer(s)\n", nlpt);
	(void)sock_fastwrite(&log.sock, log.buffer, strlen(log.buffer));
}

void report_change(char *msg, int printer)
{
	(void)printf("%s: %s %s\n", ptime(), lpt_names[printer], msg);
	if (loghost != 0)		/* log to syslogd */
	{
		(void)sprintf(log.buffer, LOG_TAG "%s %s\n", lpt_names[printer], msg);
		(void)sock_fastwrite(&log.sock, log.buffer, strlen(log.buffer));
	}
}

void lpt_status_change(int i, int delta, int status)
{
	if (status & P_TIMEOUT)
		report_change("time out", i);
	if (status & P_IOERROR)
		report_change("I/O error", i);
	if (delta & P_SELECTED)
	{
		if (status & P_SELECTED)
			report_change("online", i);
		else
			report_change("offline", i);
	}
	if (status & P_NOPAPER)
		report_change("paper out", i);
}

int same_subnet(longword client)
{
	/* assumes that bit operations can be done on longword */
	return (((client ^ my_ip_addr) & sin_mask) == 0);
}

int in_list(longword ip, longword table[], int entries, int maxtable)
{
	int		i;

	for (i = 0; i < entries && i < maxtable; ++i)
		if (ip == table[i])
			return (1);
	return (0);
}

int check_access(longword ip)
{
	if (check_subnet && !same_subnet(ip))
		return (0);
	if (nallow > 0)
		return (in_list(ip, allow, nallow, MAXALLOW) ? 1 : 0);
	if (ndeny > 0)
		return (in_list(ip, deny, ndeny, MAXDENY) ? 0 : 1);
	return (1);
}

/*
 *	Translate from name to printer number
 */
int printernumber(char *printername)
{
	int		i;

	for (i = 0; i < MAXLPT; ++i)
		if (stricmp(lpt_names[i], printername) == 0)
			return (i);
	return (-1);
}

void queuename(struct conn_info *c)
{
	int		i, current, delta;
	char		printername[sizeof(lpt_names[0])];

	*c->bufip = '\0';
	if (*c->buffer != '\002' && *c->buffer != '\003')
	{
		c->state = NAKANDCLOSE;
		return;
	}
	if (sscanf(c->buffer+1, "%15s", printername) != 1 ||
		(i = printernumber(printername)) < 0)
	{
		(void)printf("%s: Printer name error: %s", ptime(), c->buffer+1);
		c->state = NAKANDCLOSE;
		return;
	}
	switch (*c->buffer)
	{
	case '\002':
		if (lpt[i].avail != FREE)
		{
			(void)printf("%s: Printer not available: %s",
				ptime(), c->buffer+1);
			c->state = NAKANDCLOSE;
			return;
		}
		c->printer = i;
		lpt[i].avail = BUSY;
		(void)printf("%s: Job for %s started\n",
			ptime(), lpt_names[i]);
		c->state = RECVJOB;
		ack_cmd(c);
		break;
	case '\003':
	case '\004':
		(void)sprintf(c->buffer, "%s is ", lpt_names[i]);
		switch (lpt[i].avail)
		{
		case BUSY:
			(void)strcat(c->buffer, "busy\n");
			break;
		case FREE:
			(void)strcat(c->buffer, "available");
			/* get status of printer and save it */
			lpt[i].status = current = bios_printer_status(i);
			(void)strcat(c->buffer, current & P_SELECTED ?
				" online" : " offline");
			if (current & P_NOPAPER)
				(void)strcat(c->buffer, " no paper");
			(void)strcat(c->buffer, "\n");
			break;
		default:
			(void)strcat(c->buffer, "not available\n");
			break;
		}
		(void)sock_fastwrite(&c->sock, c->buffer, strlen(c->buffer));
		c->state = CLOSING;
		break;
	}
}

void get_jobinfo(struct conn_info *c)
{
	char			*p;

	p = c->bufip;
	if (buffer_room(c) == 0)
		--p;
	*p = '\0';
	for (p = strtok(c->buffer, "\n"); p != 0; p = strtok(0, "\n"))
	{
		switch (*p)
		{
		case 'N':
			(void)strncat(c->jobname, p+1, sizeof(c->jobname)-1);
			break;
		case 'P':
			(void)strncat(c->username, p+1, sizeof(c->username)-1);
			break;
		case 'H':
			(void)strncat(c->hostname, p+1, sizeof(c->hostname)-1);
			break;
		}
	}
}

void read_bytes(struct conn_info *c)
{
	int		count;
	char		*p;

	if ((count = sock_rbused(&c->sock)) > 0)
	{
		if (count > buffer_room(c))
			count = buffer_room(c);
		(void)sock_fastread(&c->sock, (byte *)c->bufip, count);
		c->bufip += count;
	}
	if (!data_unseen(c))
		return;
nextstate:
	switch (c->state)
	{
	case QUEUENAME:
		queuename(c);
		reset_ptrs(c);
		break;
	case RECVJOB:
		switch (*c->buffer)
		{
		case '\001':
			ack_cmd(c);
			c->state = CLOSING;
			break;
		case '\002':
			c->state = CONTROLINFO;
			c->controlfirst = 1;
			goto nextstate;
		case '\003':
			c->state = DATAINFO;
			goto nextstate;
		}
	case CONTROLINFO:
	case DATAINFO:
		p = c->bufip;
		if (buffer_room(c) == 0)
			--p;
		*p = '\0';
		if (sscanf(c->buffer+1, "%ld", &c->bytelen) != 1)
		{
			(void)printf("%s: %s, error parsing %s",
				ptime(), lpt_names[c->printer], c->buffer+1);
			c->state = NAKANDCLOSE;
			return;
		}
		++c->bytelen;		/* for EOF byte */
		ack_cmd(c);
		c->jobname[0] = '\0';
		c->username[0] = '\0';
		c->hostname[0] = '\0';
		c->state = c->state == CONTROLINFO ? CONTROL : DATA;
		if (c->state == DATA)
			c->starttime = time(0);
		reset_ptrs(c);
		break;
	case CONTROL:
		/* copy out interesting information from buffer */
		get_jobinfo(c);
		c->bytelen -= count;
		if (c->bytelen <= 0)
		{
			(void)printf("%s: Job ", ptime());
			if (c->jobname[0] != '\0')
				(void)printf("%s ", c->jobname);
			if (c->username[0] != '\0' && c->hostname[0] != '\0')
				(void)printf("for %s@%s ", c->username,
					c->hostname);
			(void)printf("on %s\n", lpt_names[c->printer]);
			ack_cmd(c);
			c->state = c->controlfirst ? DATAINFO : CLOSING;
		}
		reset_ptrs(c);
		break;
	case DATA:
		c->bytelen -= count;
		if (c->bytelen <= 0 && count > 0)	/* skip EOF byte */
			--c->bufip;
		c->state = PRINTING;
		break;
	}
}

/*
 *	Direct hardware write to printer port
 */
int printer_outbuf(int port, char *buffer, int count)
{
	int		i, printed, status;

	printed = 0;
	inportb(port+1);		/* read status */
	status = (inportb(port+1) & 0xf8) ^ 0x48;
	while (count > 0 && (status & P_READY) == P_READY)
	{
		outportb(port, *buffer++);	/* write character */
		outportb(port+2, 0x0d);		/* raise strobe */
		outportb(port+2, 0x0c);		/* lower strobe */
		++printed;
		--count;
		/* sample the busy line for a few tries */
		for (i = 0; i < POLL_MAX; ++i)
		{
			status = (inportb(port+1) & 0xf8) ^ 0x48;
			if ((status & P_READY) == P_READY)
				break;
		}
	}
	return (printed);
}

void print_data(struct conn_info *c)
{
	int		printer, current, delta, printed;
	struct lpt_info	*p;

	p = &lpt[printer = c->printer];
	current = bios_printer_status(printer);
	/* report changes from last time */
	delta = (current ^ p->status) & P_CHANGES;
	if (delta)
		lpt_status_change(printer, delta, current);
	p->status = current;
	/* if printer is busy, try to collect more data */
	if ((current & P_READY) != P_READY && buffer_room(c) > 0)
	{
		c->state = DATA;
		return;
	}
	if (p->hwaddr != 0)
	{
		printed = printer_outbuf(p->hwaddr, c->bufop,
			c->bufip - c->bufop);
		c->bufop += printed;
	}
	else
	{
		printed = 0;
		/* loop, printing as much as possible */
		/* eat your heart out, Pascal */
		while (c->bufop < c->bufip &&
			((current & P_READY) == P_READY ||
			(bios_printer_status(printer) & P_READY) == P_READY))
		{
			current = bios_printer_outch(printer, *c->bufop++);
			++printed;
		}
	}
	if (!data_remaining(c))
	{
		reset_ptrs(c);
		/* in jdprotocol bytelen will not be 0 before EOF
		   well, not before maxlong bytes anyway */
		if (c->bytelen > 0)
			c->state = DATA;
		else
		{
			ack_cmd(c);
			c->state = c->controlfirst ? CLOSING : CONTROLINFO;
		}
	}
	c->joblen += printed;
}

void show_stats(struct conn_info *c)
{
	time_t		elapsed;

	elapsed = time(0) - c->starttime;
	(void)printf("%s: %s: %ld bytes %ld seconds", ptime(),
		lpt_names[c->printer], c->joblen, elapsed);
	if (elapsed > 0)
		(void)printf(" %ld bytes/second", c->joblen / elapsed);
	(void)printf("\n");
}

void loop(int port, struct conn_info *c)
{
	unsigned int	cport;
	struct sockaddr	client;
	int		i;
	char		*name;

	switch (c->state) {
	case INIT:
		if (jdprotocol)
		{
			if (port >= MAXLPT || lpt[port].avail != FREE)
				break;		/* don't offer connection */
			cport = jdport + port;
			c->printer = port;
		}
		else
			cport = lpdport;
		tcp_listen(&c->sock, cport, 0L, 0, NULL, 0);
		(void)printf("%s: Connection %d listening on TCP port %u\n",
			ptime(), port, cport);
		c->state = WAITING;
		break;
	case WAITING:
		tcp_tick(NULL);
		if (!sock_established(&c->sock))
			break;
		i = sizeof(client);
		client.s_ip = 0;
		name = getpeername(&c->sock, &client, &i) == 0 ?
			inet_ntoa(c->buffer, client.s_ip) : "?";
		if (check_access(client.s_ip))
		{
			(void)printf("%s: Connection %d from %s\n",
				ptime(), port, name);
			c->state = jdprotocol ? DATA : QUEUENAME;
			c->bytelen = 0x7ffffff;	/* maxlong */
			c->starttime = time(0);
			c->joblen = 0L;
		}
		else
		{
			(void)printf("%s: Connection %d from %s refused\n",
				ptime(), port, name);
			c->state = CLOSING;
		}
		reset_ptrs(c);
		break;
	case QUEUENAME:
	case RECVJOB:
	case CONTROLINFO:
	case CONTROL:
	case DATAINFO:
	case DATA:
		if (tcp_tick(&c->sock))
			read_bytes(c);
		/* don't shutdown until all printed */
		else if (data_remaining(c))
			c->state = PRINTING;
		else
			c->state = CLOSING;
		break;
	case PRINTING:
		if (tcp_tick(&c->sock) || data_remaining(c))
			print_data(c);
		else
			c->state = CLOSING;
		break;
	case NAKANDCLOSE:
		nak_cmd(c);
	case CLOSING:
		show_stats(c);
		sock_flush(&c->sock);
		sock_close(&c->sock);
reinit:
		/* free up printer */
		if (0 <= c->printer && c->printer < MAXLPT
			&& lpt[c->printer].avail == BUSY)
			lpt[c->printer].avail = FREE;
		c->state = INIT;
		c->controlfirst = 0;
		c->printer = -1;
		break;
	case ABORT:
		sock_abort(&c->sock);
		goto reinit;
	}
}

void check_key(void)
{
	int		key, i;

	if (((key = bioskey(0)) & 0xff) != 0)
		return;			/* ASCII key */
	key = (key >> 8) & 0xff;
	if (key < CF1 || key > CF3)
		return;
	key -= CF1;		/* which printer? */
	for (i = 0; i < MAXCON; ++i)
	{
		if (conn[i].printer == key && conn[i].state != INIT)
		{
			if (reinit)
			{
				(void)printf("%s: Aborting job and reinitialising %s\n",
					ptime(), lpt_names[key]);
				(void)biosprint(1, 0, key);
				sleep(2);		/* let printer settle */
			}
			else
			{
				(void)printf("%s: Aborting job on %s\n",
					ptime(), lpt_names[key]);
			}
			conn[i].state = ABORT;		/* abort job */
			return;
		}
	}
}

void make_list(char *adjective, char *s,
	longword table[], int *nentries, int maxtable)
{
	char		*p;
	longword	ip;
	int		i;
	char		buffer[64];

	for ( ; *s != '\0'; s = p + 1)
	{
		if ((p = strchr(s, ',')) == 0)
			p = s + strlen(s) - 1;
		else
			*p = '\0';		/* mark end */
		if ((ip = resolve(s)) == (longword)0)
			continue;
		if (*nentries >= maxtable)
			continue;		/* should print warning */
		table[(*nentries)++] = ip;
	}
	for (i = 0; i < *nentries; ++i)
		(void)printf("%s ", inet_ntoa(buffer, table[i]));
	if (*nentries > 0)
		(void)printf("%s access\n", adjective);
}

void my_init(char *name, char *value)
{
	if (strcmp(name, "PRINTER1NAME") == 0)
	{
		strncpy(lpt_names[0], value, sizeof(lpt_names[0]));
		lpt_names[0][sizeof(lpt_names[0])-1] = '\0';
	}
	else if (strcmp(name, "PRINTER2NAME") == 0)
	{
		strncpy(lpt_names[1], value, sizeof(lpt_names[1]));
		lpt_names[1][sizeof(lpt_names[1])-1] = '\0';
	}
	else if (strcmp(name, "PRINTER3NAME") == 0)
	{
		strncpy(lpt_names[2], value, sizeof(lpt_names[2]));
		lpt_names[2][sizeof(lpt_names[2])-1] = '\0';
	}
	else if (normal_init)
		(*normal_init)(name, value);
}

void options(int argc, char **argv)
{
	char		*s;
	int		i;

	for (i = 0; i < MAXLPT; ++i)
		lpt[i].hwaddr = 0;
	for (--argc, ++argv; argc > 0 && *argv[0] == '-'; --argc, ++argv)
	{
		s = argv[0];
		switch (*++s)
		{
		case '1': case '2': case '3':
			for ( ; '1' <= *s && *s <= '3'; ++s)
				lpt[*s - '1'].avail  = DISABLED;
			break;
		case 'a':
			if (ndeny <= 0)
				make_list("allowed", ++s, allow, &nallow,
					MAXALLOW);
			break;
		case 'b':
			++s;
			for ( ; '1' <= *s && *s <= '3'; ++s)
				lpt[*s - '1'].hwaddr = 1;
			break;
		case 'd':
			if (nallow <= 0)
				make_list("denied", ++s, deny, &ndeny,
					MAXDENY);
			break;
		case 'i':
			reinit = 1;
			break;
		case 'j':
			jdprotocol = 1;
			if ((jdport = atoi(++s)) <= 0)
				jdport = JDPORT;
			break;
		case 'l':
			loghost = ++s;
			break;
		case 'n':
			if ((nlpt = atoi(++s)) > MAXLPT || nlpt < 0)
				nlpt = 0;
			break;
		case 'p':		/* alternate TCP port */
			if ((lpdport = atoi(++s)) <= 0)
				lpdport = LPDPORT;
			break;
		case 's':
			check_subnet = 0;
			break;
		case 't':
			notone = 1;
			break;
		}
	}
}

int main(int argc, char **argv)
{
	int		i;

	(void)printf(PROGRAM " " VERSION " " AUTHOR "\n");
	(void)printf(PROGRAM " comes with ABSOLUTELY NO WARRANTY; for details read the file COPYING\n");
	(void)printf("This is free software, and you are welcome to redistribute it\n");
	(void)printf(" under certain conditions; see the file COPYING for details.\n");
	(void)printf("C-F1 through C-F3 to abort printer jobs\n");
	(void)memset(conn, 0, sizeof(conn));
	(void)memset(lpt, 0, sizeof(lpt));
	tzset();
	options(argc, argv);
	/* hook onto init procedure to get NAME=VALUE pairs */
	normal_init = usr_init;
	usr_init = my_init;
	dbuginit();
	sock_init();
	init_queues();
	if (nlpt == 0)
	{
		(void)printf("%s: No printers connected\n", ptime());
		return (1);
	}
	init_log();
	for (;;)
	{
		for (i = 0; i < MAXCON; ++i)
			loop(i, &conn[i]);
		if (kbhit())
			check_key();
	}
	/*NOTREACHED*/
	return (0);
}
