[BACK]Return to frontend_lpr.c CVS log [TXT][DIR] Up to [local] / src / usr.sbin / lpd

File: [local] / src / usr.sbin / lpd / frontend_lpr.c (download)

Revision 1.4, Wed Dec 28 21:30:17 2022 UTC (17 months ago) by jmc
Branch: MAIN
CVS Tags: OPENBSD_7_5_BASE, OPENBSD_7_5, OPENBSD_7_4_BASE, OPENBSD_7_4, OPENBSD_7_3_BASE, OPENBSD_7_3, HEAD
Changes since 1.3: +3 -3 lines

spelling fixes; from paul tagliamonte
any parts of his diff not taken are noted on tech

/*	$OpenBSD: frontend_lpr.c,v 1.4 2022/12/28 21:30:17 jmc Exp $	*/

/*
 * Copyright (c) 2017 Eric Faurot <eric@openbsd.org>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>

#include <ctype.h>
#include <errno.h>
#include <limits.h>
#include <netdb.h>
#include <stdarg.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

#include "lpd.h"
#include "lp.h"

#include "io.h"
#include "log.h"
#include "proc.h"

#define SERVER_TIMEOUT	30000
#define CLIENT_TIMEOUT	 5000

#define	MAXARG	50

#define	F_ZOMBIE	0x1
#define	F_WAITADDRINFO	0x2

#define STATE_READ_COMMAND	0
#define STATE_READ_FILE		1

struct lpr_conn {
	SPLAY_ENTRY(lpr_conn)	 entry;
	uint32_t		 id;
	char			 hostname[NI_MAXHOST];
	struct io		*io;
	int			 state;
	int			 flags;
	int			 recvjob;
	int			 recvcf;
	size_t			 expect;
	FILE			*ofp;	/* output file when receiving data */
	int			 ifd;	/* input file for displayq/rmjob */

	char			*cmd;
	int			 ai_done;
	struct addrinfo		*ai;
	struct io		*iofwd;
};

SPLAY_HEAD(lpr_conn_tree, lpr_conn);

static int lpr_conn_cmp(struct lpr_conn *, struct lpr_conn *);
SPLAY_PROTOTYPE(lpr_conn_tree, lpr_conn, entry, lpr_conn_cmp);

static void lpr_on_allowedhost(struct lpr_conn *, const char *, const char *);
static void lpr_on_recvjob(struct lpr_conn *, int);
static void lpr_on_recvjob_file(struct lpr_conn *, int, size_t, int, int);
static void lpr_on_request(struct lpr_conn *, int, const char *, const char *);
static void lpr_on_getaddrinfo(void *, int, struct addrinfo *);

static void lpr_io_dispatch(struct io *, int, void *);
static int  lpr_readcommand(struct lpr_conn *);
static int  lpr_readfile(struct lpr_conn *);
static int  lpr_parsejobfilter(struct lpr_conn *, struct lp_jobfilter *,
    int, char **);

static void lpr_free(struct lpr_conn *);
static void lpr_close(struct lpr_conn *);
static void lpr_ack(struct lpr_conn *, char);
static void lpr_reply(struct lpr_conn *, const char *);
static void lpr_stream(struct lpr_conn *);
static void lpr_forward(struct lpr_conn *);

static void lpr_iofwd_dispatch(struct io *, int, void *);

static struct lpr_conn_tree conns;

void
lpr_init(void)
{
	SPLAY_INIT(&conns);
}

void
lpr_conn(uint32_t connid, struct listener *l, int sock,
    const struct sockaddr *sa)
{
	struct lpr_conn *conn;

	if ((conn = calloc(1, sizeof(*conn))) == NULL) {
		log_warn("%s: calloc", __func__);
		close(sock);
		frontend_conn_closed(connid);
		return;
	}
	conn->id = connid;
	conn->ifd = -1;
	conn->io = io_new();
	if (conn->io == NULL) {
		log_warn("%s: io_new", __func__);
		free(conn);
		close(sock);
		frontend_conn_closed(connid);
		return;
	}
	SPLAY_INSERT(lpr_conn_tree, &conns, conn);
	io_set_callback(conn->io, lpr_io_dispatch, conn);
	io_set_timeout(conn->io, CLIENT_TIMEOUT);
	io_set_write(conn->io);
	io_attach(conn->io, sock);

	conn->state = STATE_READ_COMMAND;
	m_create(p_engine, IMSG_LPR_ALLOWEDHOST, conn->id, 0, -1);
	m_add_sockaddr(p_engine, sa);
	m_close(p_engine);
}

void
lpr_dispatch_engine(struct imsgproc *proc, struct imsg *imsg)
{
	struct lpr_conn *conn = NULL, key;
	const char *hostname, *reject, *cmd;
	size_t sz;
	int ack, cf = 0;

	key.id = imsg->hdr.peerid;
	if (key.id) {
		conn = SPLAY_FIND(lpr_conn_tree, &conns, &key);
		if (conn == NULL) {
			log_debug("%08x dead-session", key.id);
			if (imsg->fd != -1)
				close(imsg->fd);
			return;
		}
	}

	switch (imsg->hdr.type) {
	case IMSG_LPR_ALLOWEDHOST:
		m_get_string(proc, &hostname);
		m_get_string(proc, &reject);
		m_end(proc);
		lpr_on_allowedhost(conn, hostname, reject);
		break;

	case IMSG_LPR_RECVJOB:
		m_get_int(proc, &ack);
		m_end(proc);
		lpr_on_recvjob(conn, ack);
		break;

	case IMSG_LPR_RECVJOB_CF:
		cf = 1;
	case IMSG_LPR_RECVJOB_DF:
		m_get_int(proc, &ack);
		m_get_size(proc, &sz);
		m_end(proc);
		lpr_on_recvjob_file(conn, ack, sz, cf, imsg->fd);
		break;

	case IMSG_LPR_DISPLAYQ:
	case IMSG_LPR_RMJOB:
		m_get_string(proc, &hostname);
		m_get_string(proc, &cmd);
		m_end(proc);
		lpr_on_request(conn, imsg->fd, hostname, cmd);
		break;

	default:
		fatalx("%s: unexpected imsg %s", __func__,
		    log_fmt_imsgtype(imsg->hdr.type));
	}
}

static void
lpr_on_allowedhost(struct lpr_conn *conn, const char *hostname,
    const char *reject)
{
	strlcpy(conn->hostname, hostname, sizeof(conn->hostname));
	if (reject)
		lpr_reply(conn, reject);
	else
		io_set_read(conn->io);
}

static void
lpr_on_recvjob(struct lpr_conn *conn, int ack)
{
	if (ack == LPR_ACK)
		conn->recvjob = 1;
	else
		log_debug("%08x recvjob failed", conn->id);
	lpr_ack(conn, ack);
}

static void
lpr_on_recvjob_file(struct lpr_conn *conn, int ack, size_t sz, int cf, int fd)
{
	if (ack != LPR_ACK) {
		lpr_ack(conn, ack);
		return;
	}

	if (fd == -1) {
		log_warnx("%s: failed to get fd", __func__);
		lpr_ack(conn, LPR_NACK);
		return;
	}

	conn->ofp = fdopen(fd, "w");
	if (conn->ofp == NULL) {
		log_warn("%s: fdopen", __func__);
		close(fd);
		lpr_ack(conn, LPR_NACK);
		return;
	}

	conn->expect = sz;
	if (cf)
		conn->recvcf = cf;
	conn->state = STATE_READ_FILE;

	lpr_ack(conn, LPR_ACK);
}

static void
lpr_on_request(struct lpr_conn *conn, int fd, const char *hostname,
    const char *cmd)
{
	struct addrinfo hints;

	if (fd == -1) {
		log_warnx("%s: no fd received", __func__);
		lpr_close(conn);
		return;
	}

	log_debug("%08x stream init", conn->id);
	conn->ifd = fd;

	/* Prepare for command forwarding if necessary. */
	if (cmd) {
		log_debug("%08x forwarding to %s: \\%d%s", conn->id, hostname,
		    cmd[0], cmd + 1);
		conn->cmd = strdup(cmd);
		if (conn->cmd == NULL)
			log_warn("%s: strdup", __func__);
		else {
			memset(&hints, 0, sizeof(hints));
			hints.ai_socktype = SOCK_STREAM;
			conn->flags |= F_WAITADDRINFO;
			/*
			 * The callback might run immediately, so conn->ifd
			 * must be set before, to block lpr_forward().
			 */
			resolver_getaddrinfo(hostname, "printer", &hints,
			    lpr_on_getaddrinfo, conn);
		}
	}

	lpr_stream(conn);
}

static void
lpr_on_getaddrinfo(void *arg, int r, struct addrinfo *ai)
{
	struct lpr_conn *conn = arg;

	conn->flags &= ~F_WAITADDRINFO;
	if (conn->flags & F_ZOMBIE) {
		if (ai)
			freeaddrinfo(ai);
		lpr_free(conn);
	}
	else {
		conn->ai_done = 1;
		conn->ai = ai;
		lpr_forward(conn);
	}
}

static void
lpr_io_dispatch(struct io *io, int evt, void *arg)
{
	struct lpr_conn *conn = arg;
	int r;

	switch (evt) {
	case IO_DATAIN:
		switch(conn->state) {
		case STATE_READ_COMMAND:
			r = lpr_readcommand(conn);
			break;
		case STATE_READ_FILE:
			r = lpr_readfile(conn);
			break;
		default:
			fatal("%s: unexpected state %d", __func__, conn->state);
		}

		if (r == 0)
			io_set_write(conn->io);
		return;

	case IO_LOWAT:
		if (conn->recvjob)
			io_set_read(conn->io);
		else if (conn->ifd != -1)
			lpr_stream(conn);
		else if (conn->cmd == NULL)
			lpr_close(conn);
		return;

	case IO_DISCONNECTED:
		log_debug("%08x disconnected", conn->id);
		/*
		 * Some clients don't wait for the last acknowledgment to close
		 * the session.  So just consider it is closed normally.
		 */
	case IO_CLOSED:
		if (conn->recvcf && conn->state == STATE_READ_COMMAND) {
			/*
			 * Commit the transaction if we received a control file
			 * and the last file was received correctly.
			 */
			m_compose(p_engine, IMSG_LPR_RECVJOB_COMMIT, conn->id,
			    0, -1, NULL, 0);
			conn->recvjob = 0;
		}
		break;

	case IO_TIMEOUT:
		log_debug("%08x timeout", conn->id);
		break;

	case IO_ERROR:
		log_debug("%08x io-error", conn->id);
		break;

	default:
		fatalx("%s: unexpected event %d", __func__, evt);
	}

	lpr_close(conn);
}

static int
lpr_readcommand(struct lpr_conn *conn)
{
	struct lp_jobfilter jf;
	size_t count;
	const char *errstr;
	char *argv[MAXARG], *line;
	int i, argc, cmd;

	line = io_getline(conn->io, NULL);
	if (line == NULL) {
		if (io_datalen(conn->io) >= LPR_MAXCMDLEN) {
			lpr_reply(conn, "Request line too long");
			return 0;
		}
		return -1;
	}

	cmd = line[0];
	line++;

	if (cmd == 0) {
		lpr_reply(conn, "No command");
		return 0;
	}

	log_debug("%08x cmd \\%d", conn->id, cmd);

	/* Parse the command. */
	for (argc = 0; argc < MAXARG; ) {
		argv[argc] = strsep(&line, " \t");
		if (argv[argc] == NULL)
			break;
		if (argv[argc][0] != '\0')
			argc++;
	}
	if (argc == MAXARG) {
		lpr_reply(conn, "Argument list too long");
		return 0;
	}

	if (argc == 0) {
		lpr_reply(conn, "No queue specified");
		return 0;
	}

#define CMD(c)    ((int)(c))
#define SUBCMD(c) (0x100 | (int)(c))

	if (conn->recvjob)
		cmd |= 0x100;
	switch (cmd) {
	case CMD('\1'):	/* PRINT <prn> */
		m_create(p_engine, IMSG_LPR_PRINTJOB, 0, 0, -1);
		m_add_string(p_engine, argv[0]);
		m_close(p_engine);
		lpr_ack(conn, LPR_ACK);
		return 0;

	case CMD('\2'):	/* RECEIVE JOB <prn> */
		m_create(p_engine, IMSG_LPR_RECVJOB, conn->id, 0, -1);
		m_add_string(p_engine, conn->hostname);
		m_add_string(p_engine, argv[0]);
		m_close(p_engine);
		return 0;

	case CMD('\3'):	/* QUEUE STATE SHORT <prn> [job#...] [user..] */
	case CMD('\4'):	/* QUEUE STATE LONG  <prn> [job#...] [user..] */
		if (lpr_parsejobfilter(conn, &jf, argc - 1, argv + 1) == -1)
			return 0;

		m_create(p_engine, IMSG_LPR_DISPLAYQ, conn->id, 0, -1);
		m_add_int(p_engine, (cmd == '\3') ? 0 : 1);
		m_add_string(p_engine, conn->hostname);
		m_add_string(p_engine, argv[0]);
		m_add_int(p_engine, jf.njob);
		for (i = 0; i < jf.njob; i++)
			m_add_int(p_engine, jf.jobs[i]);
		m_add_int(p_engine, jf.nuser);
		for (i = 0; i < jf.nuser; i++)
			m_add_string(p_engine, jf.users[i]);
		m_close(p_engine);
		return 0;

	case CMD('\5'):	/* REMOVE JOBS <prn> <agent> [job#...] [user..] */
		if (argc < 2) {
			lpr_reply(conn, "No agent specified");
			return 0;
		}
		if (lpr_parsejobfilter(conn, &jf, argc - 2, argv + 2) == -1)
			return 0;

		m_create(p_engine, IMSG_LPR_RMJOB, conn->id, 0, -1);
		m_add_string(p_engine, conn->hostname);
		m_add_string(p_engine, argv[0]);
		m_add_string(p_engine, argv[1]);
		m_add_int(p_engine, jf.njob);
		for (i = 0; i < jf.njob; i++)
			m_add_int(p_engine, jf.jobs[i]);
		m_add_int(p_engine, jf.nuser);
		for (i = 0; i < jf.nuser; i++)
			m_add_string(p_engine, jf.users[i]);
		m_close(p_engine);
		return 0;

	case SUBCMD('\1'):	/* ABORT */
		m_compose(p_engine, IMSG_LPR_RECVJOB_CLEAR, conn->id, 0, -1,
		    NULL, 0);
		conn->recvcf = 0;
		lpr_ack(conn, LPR_ACK);
		return 0;

	case SUBCMD('\2'):	/* CONTROL FILE <size> <filename> */
	case SUBCMD('\3'):	/* DATA FILE    <size> <filename> */
		if (argc != 2) {
			log_debug("%08x invalid number of argument", conn->id);
			lpr_ack(conn, LPR_NACK);
			return 0;
		}
		errstr = NULL;
		count = strtonum(argv[0], 1, LPR_MAXFILESIZE, &errstr);
		if (errstr) {
			log_debug("%08x invalid file size: %s", conn->id,
			    strerror(errno));
			lpr_ack(conn, LPR_NACK);
			return 0;
		}

		if (cmd == SUBCMD('\2')) {
			if (conn->recvcf) {
				log_debug("%08x cf file already received",
				    conn->id);
				lpr_ack(conn, LPR_NACK);
				return 0;
			}
			m_create(p_engine, IMSG_LPR_RECVJOB_CF, conn->id, 0,
			    -1);
		}
		else
			m_create(p_engine, IMSG_LPR_RECVJOB_DF, conn->id, 0,
			    -1);
		m_add_size(p_engine, count);
		m_add_string(p_engine, argv[1]);
		m_close(p_engine);
		return 0;

	default:
		if (conn->recvjob)
			lpr_reply(conn, "Protocol error");
		else
			lpr_reply(conn, "Illegal service request");
		return 0;
	}
}

static int
lpr_readfile(struct lpr_conn *conn)
{
	size_t len, w;
	char *data;

	if (conn->expect) {
		/* Read file content. */
		data = io_data(conn->io);
		len = io_datalen(conn->io);
		if (len > conn->expect)
			len = conn->expect;

		log_debug("%08x %zu bytes received", conn->id, len);

		w = fwrite(data, 1, len, conn->ofp);
		if (w != len) {
			log_warnx("%s: fwrite", __func__);
			lpr_close(conn);
			return -1;
		}
		io_drop(conn->io, w);
		conn->expect -= w;
		if (conn->expect)
			return -1;

		fclose(conn->ofp);
		conn->ofp = NULL;

		log_debug("%08x file received", conn->id);
	}

	/* Try to read '\0'. */
	len = io_datalen(conn->io);
	if (len == 0)
		return -1;
	data = io_data(conn->io);
	io_drop(conn->io, 1);

	log_debug("%08x eof %d", conn->id, (int)*data);

	if (*data != '\0') {
		lpr_close(conn);
		return -1;
	}

	conn->state = STATE_READ_COMMAND;
	lpr_ack(conn, LPR_ACK);
	return 0;
}

static int
lpr_parsejobfilter(struct lpr_conn *conn, struct lp_jobfilter *jf, int argc,
    char **argv)
{
	const char *errstr;
	char *arg;
	int i, jobnum;

	memset(jf, 0, sizeof(*jf));

	for (i = 0; i < argc; i++) {
		arg = argv[i];
		if (isdigit((unsigned char)arg[0])) {
			if (jf->njob == LP_MAXREQUESTS) {
				lpr_reply(conn, "Too many requests");
				return -1;
			}
			errstr = NULL;
			jobnum = strtonum(arg, 0, INT_MAX, &errstr);
			if (errstr) {
				lpr_reply(conn, "Invalid job number");
				return -1;
			}
			jf->jobs[jf->njob++] = jobnum;
		}
		else {
			if (jf->nuser == LP_MAXUSERS) {
				lpr_reply(conn, "Too many users");
				return -1;
			}
			jf->users[jf->nuser++] = arg;
		}
	}

	return 0;
}

static void
lpr_free(struct lpr_conn *conn)
{
	if ((conn->flags & F_WAITADDRINFO) == 0)
		free(conn);
}

static void
lpr_close(struct lpr_conn *conn)
{
	uint32_t connid = conn->id;

	SPLAY_REMOVE(lpr_conn_tree, &conns, conn);

	if (conn->recvjob)
		m_compose(p_engine, IMSG_LPR_RECVJOB_ROLLBACK, conn->id, 0, -1,
		    NULL, 0);

	io_free(conn->io);
	free(conn->cmd);
	if (conn->ofp)
		fclose(conn->ofp);
	if (conn->ifd != -1)
		close(conn->ifd);
	if (conn->ai)
		freeaddrinfo(conn->ai);
	if (conn->iofwd)
		io_free(conn->iofwd);

	conn->flags |= F_ZOMBIE;
	lpr_free(conn);

	frontend_conn_closed(connid);
}

static void
lpr_ack(struct lpr_conn *conn, char c)
{
	if (c == 0)
		log_debug("%08x ack", conn->id);
	else
		log_debug("%08x nack %d", conn->id, (int)c);

	io_write(conn->io, &c, 1);
}

static void
lpr_reply(struct lpr_conn *conn, const char *s)
{
	log_debug("%08x reply: %s", conn->id, s);

	io_printf(conn->io, "%s\n", s);
}

/*
 * Stream response file to the client.
 */
static void
lpr_stream(struct lpr_conn *conn)
{
	char buf[BUFSIZ];
	ssize_t r;

	for (;;) {
		if (io_queued(conn->io) > 65536)
			return;

		r = read(conn->ifd, buf, sizeof(buf));
		if (r == -1) {
			if (errno == EINTR)
				continue;
			log_warn("%s: read", __func__);
			break;
		}

		if (r == 0) {
			log_debug("%08x stream done", conn->id);
			break;
		}
		log_debug("%08x stream %zu bytes", conn->id, r);

		if (io_write(conn->io, buf, r) == -1) {
			log_warn("%s: io_write", __func__);
			break;
		}
	}

	close(conn->ifd);
	conn->ifd = -1;

	if (conn->cmd)
		lpr_forward(conn);

	else if (io_queued(conn->io) == 0)
		lpr_close(conn);
}

/*
 * Forward request to the remote printer.
 */
static void
lpr_forward(struct lpr_conn *conn)
{
	/*
	 * Do not start forwarding the command if the address is not resolved
	 * or if the local response is still being sent to the client.
	 */
	if (!conn->ai_done || conn->ifd == -1)
		return;

	if (conn->ai == NULL) {
		if (io_queued(conn->io) == 0)
			lpr_close(conn);
		return;
	}

	log_debug("%08x forward start", conn->id);

	conn->iofwd = io_new();
	if (conn->iofwd == NULL) {
		log_warn("%s: io_new", __func__);
		if (io_queued(conn->io) == 0)
			lpr_close(conn);
		return;
	}
	io_set_callback(conn->iofwd, lpr_iofwd_dispatch, conn);
	io_set_timeout(conn->io, SERVER_TIMEOUT);
	io_connect(conn->iofwd, conn->ai);
	conn->ai = NULL;
}

static void
lpr_iofwd_dispatch(struct io *io, int evt, void *arg)
{
	struct lpr_conn *conn = arg;

	switch (evt) {
	case IO_CONNECTED:
		log_debug("%08x forward connected", conn->id);
		/* Send the request. */
		io_print(io, conn->cmd);
		io_print(io, "\n");
		io_set_write(io);
		return;

	case IO_DATAIN:
		/* Relay. */
		io_write(conn->io, io_data(io), io_datalen(io));
		io_drop(io, io_datalen(io));
		return;

	case IO_LOWAT:
		/* Read response. */
		io_set_read(io);
		return;

	case IO_CLOSED:
		break;

	case IO_DISCONNECTED:
		log_debug("%08x forward disconnected", conn->id);
		break;

	case IO_TIMEOUT:
		log_debug("%08x forward timeout", conn->id);
		break;

	case IO_ERROR:
		log_debug("%08x forward io-error", conn->id);
		break;

	default:
		fatalx("%s: unexpected event %d", __func__, evt);
	}

	log_debug("%08x forward done", conn->id);

	io_free(io);
	free(conn->cmd);
	conn->cmd = NULL;
	conn->iofwd = NULL;
	if (io_queued(conn->io) == 0)
		lpr_close(conn);
}

static int
lpr_conn_cmp(struct lpr_conn *a, struct lpr_conn *b)
{
	if (a->id < b->id)
		return -1;
	if (a->id > b->id)
		return 1;
	return 0;
}

SPLAY_GENERATE(lpr_conn_tree, lpr_conn, entry, lpr_conn_cmp);