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

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

Revision 1.2, Thu Apr 4 19:25:45 2019 UTC (5 years, 2 months ago) by eric
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, OPENBSD_7_2_BASE, OPENBSD_7_2, OPENBSD_7_1_BASE, OPENBSD_7_1, OPENBSD_7_0_BASE, OPENBSD_7_0, OPENBSD_6_9_BASE, OPENBSD_6_9, OPENBSD_6_8_BASE, OPENBSD_6_8, OPENBSD_6_7_BASE, OPENBSD_6_7, OPENBSD_6_6_BASE, OPENBSD_6_6, OPENBSD_6_5_BASE, OPENBSD_6_5, HEAD
Changes since 1.1: +6 -6 lines

accept the NULL string in the proc message formatting api and simplify
code accordingly.

/*	$OpenBSD: engine_lpr.c,v 1.2 2019/04/04 19:25:45 eric 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 <ctype.h>
#include <errno.h>
#include <limits.h>
#include <netgroup.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

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

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

struct lpr_recvfile {
	TAILQ_ENTRY(lpr_recvfile)	 entry;
	char				*dfname;
};

struct lpr_recvjob {
	TAILQ_ENTRY(lpr_recvjob)	 entry;
	struct lp_printer		 lp;
	uint32_t			 connid;
	char				*hostfrom;
	char				*cfname;
	int				 dfcount;
	size_t				 dfsize;
	TAILQ_HEAD(, lpr_recvfile)	 df;
};

static void lpr_allowedhost(uint32_t, const struct sockaddr *);
static void lpr_allowedhost_res(uint32_t, const char *, const char *);
static int lpr_mkstemp(void);
static void lpr_displayq(uint32_t, const char *, int, struct lp_jobfilter *);
static void lpr_displayq_res(uint32_t, int, const char *, const char *);
static void lpr_rmjob(uint32_t, const char *, const char *,
    struct lp_jobfilter *);
static void lpr_rmjob_res(uint32_t, int, const char *, const char *);
static void lpr_recvjob(uint32_t, const char*, const char *);
static void lpr_recvjob_res(uint32_t, int);
static void lpr_recvjob_cf(uint32_t, size_t, const char *);
static void lpr_recvjob_df(uint32_t, size_t, const char *);
static void lpr_recvjob_clear(uint32_t);
static void lpr_recvjob_commit(uint32_t);
static void lpr_recvjob_rollback(uint32_t);
static void lpr_recvjob_free(struct lpr_recvjob *);
static int matchaddr(const char *, const struct sockaddr *, int *);
static int cmpsockaddr(const struct sockaddr *, const struct sockaddr *);

static TAILQ_HEAD(, lpr_recvjob) recvjobs = TAILQ_HEAD_INITIALIZER(recvjobs);

void
lpr_dispatch_frontend(struct imsgproc *proc, struct imsg *imsg)
{
	struct sockaddr_storage ss;
	struct lp_jobfilter jf;
	struct lp_printer lp;
	const char *hostfrom, *prn, *filename, *agent;
	uint32_t connid;
	size_t size;
	int lng, i;

	connid = imsg->hdr.peerid;

	switch (imsg->hdr.type) {
	case IMSG_LPR_ALLOWEDHOST:
		m_get_sockaddr(proc, (struct sockaddr *)&ss);
		m_end(proc);
		lpr_allowedhost(connid, (struct sockaddr *)&ss);
		break;

	case IMSG_LPR_DISPLAYQ:
		memset(&jf, 0, sizeof(jf));
		m_get_int(proc, &lng);
		m_get_string(proc, &jf.hostfrom);
		m_get_string(proc, &prn);
		m_get_int(proc, &jf.njob);
		for (i = 0; i < jf.njob; i++)
			m_get_int(proc, &jf.jobs[i]);
		m_get_int(proc, &jf.nuser);
		for (i = 0; i < jf.nuser; i++)
			m_get_string(proc, &jf.users[i]);
		m_end(proc);
		lpr_displayq(connid, prn, lng, &jf);
		break;

	case IMSG_LPR_PRINTJOB:
		m_get_string(proc, &prn);
		m_end(proc);
		/* Make sure the printer exists. */
		if (lp_getprinter(&lp, prn) == -1)
			break;
		lpr_printjob(lp.lp_name);
		lp_clearprinter(&lp);
		break;

	case IMSG_LPR_RECVJOB:
		m_get_string(proc, &hostfrom);
		m_get_string(proc, &prn);
		m_end(proc);
		lpr_recvjob(connid, hostfrom, prn);
		break;

	case IMSG_LPR_RECVJOB_CLEAR:
		m_end(proc);
		lpr_recvjob_clear(connid);
		break;

	case IMSG_LPR_RECVJOB_CF:
		m_get_size(proc, &size);
		m_get_string(proc, &filename);
		m_end(proc);
		lpr_recvjob_cf(connid, size, filename);
		break;

	case IMSG_LPR_RECVJOB_DF:
		m_get_size(proc, &size);
		m_get_string(proc, &filename);
		m_end(proc);
		lpr_recvjob_df(connid, size, filename);
		break;

	case IMSG_LPR_RECVJOB_COMMIT:
		m_end(proc);
		lpr_recvjob_commit(connid);
		break;

	case IMSG_LPR_RECVJOB_ROLLBACK:
		m_end(proc);
		lpr_recvjob_rollback(connid);
		break;

	case IMSG_LPR_RMJOB:
		memset(&jf, 0, sizeof(jf));
		m_get_string(proc, &jf.hostfrom);
		m_get_string(proc, &prn);
		m_get_string(proc, &agent);
		m_get_int(proc, &jf.njob);
		for (i = 0; i < jf.njob; i++)
			m_get_int(proc, &jf.jobs[i]);
		m_get_int(proc, &jf.nuser);
		for (i = 0; i < jf.nuser; i++)
			m_get_string(proc, &jf.users[i]);
		m_end(proc);
		lpr_rmjob(connid, prn, agent, &jf);
		break;

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

void
lpr_shutdown()
{
	struct lpr_recvjob *j;

	/* Cleanup incoming jobs. */
	while ((j = TAILQ_FIRST(&recvjobs))) {
		lpr_recvjob_clear(j->connid);
		lpr_recvjob_free(j);
	}
}

void
lpr_printjob(const char *prn)
{
	m_create(p_priv, IMSG_LPR_PRINTJOB, 0, 0, -1);
	m_add_string(p_priv, prn);
	m_close(p_priv);
}

static void
lpr_allowedhost(uint32_t connid, const struct sockaddr *sa)
{
	FILE *fp;
	size_t linesz = 0;
	ssize_t linelen;
	char host[NI_MAXHOST], addr[NI_MAXHOST], serv[NI_MAXSERV];
	char dom[NI_MAXHOST], *lp, *ep, *line = NULL;
	int e, rev = 0, ok = 0;

	/* Always accept local connections. */
	if (sa->sa_family == AF_UNIX) {
		lpr_allowedhost_res(connid, lpd_hostname, NULL);
		return;
	}

	host[0] = '\0';

	/* Print address. */
	if ((e = getnameinfo(sa, sa->sa_len, addr, sizeof(addr), serv,
	    sizeof(serv), NI_NUMERICHOST))) {
		log_warnx("%s: could not print addr: %s", __func__,
		    gai_strerror(e));
		lpr_allowedhost_res(connid, host, "Malformed address");
		return;
	}

	/* Get the hostname for the address. */
	if ((e = getnameinfo(sa, sa->sa_len, host, sizeof(host), NULL, 0,
	    NI_NAMEREQD))) {
		if (e != EAI_NONAME)
			log_warnx("%s: could not resolve %s: %s", __func__,
			    addr, gai_strerror(e));
		lpr_allowedhost_res(connid, host,
		    "No hostname found for your address");
		return;
	}

	/* Check for a valid DNS roundtrip. */
	if (!matchaddr(host, sa, &e)) {
		if (e)
			log_warnx("%s: getaddrinfo: %s: %s", __func__,
			    host, gai_strerror(e));
		lpr_allowedhost_res(connid, host, e ?
		    "Cannot resolve your hostname" :
		    "Your hostname and your address do not match");
		return;
	}

	/* Scan the hosts.lpd file. */
	if ((fp = fopen(_PATH_HOSTSLPD, "r")) == NULL) {
		log_warn("%s: %s", __func__, _PATH_HOSTSLPD);
		lpr_allowedhost_res(connid, host,
		    "Cannot access " _PATH_HOSTSLPD);
		return;
	}

	dom[0] = '\0';
	while ((linelen = getline(&line, &linesz, fp)) != -1) {
		/* Drop comment and strip line. */
		for (lp = line; *lp; lp++)
			if (!isspace((unsigned char)*lp))
				break;
		if (*lp == '#' || *lp == '\0')
			continue;
		for (ep = lp + 1; *ep; ep++)
			if (isspace((unsigned char)*ep) || *ep == '#') {
				*ep = '\0';
				break;
			}

		rev = 0;
		switch (lp[0]) {
		case '-':
		case '+':
			switch (lp[1]) {
			case '\0':
				ok = 1;
				break;
			case '@':
				if (dom[0] == '\0')
					getdomainname(dom, sizeof(dom));
				ok = innetgr(lp + 2, host, NULL, dom);
				break;
			default:
				ok = matchaddr(lp + 1, sa, NULL);
				break;
			}
			if (lp[0] == '-')
				ok = -ok;
			break;
		default:
			ok = matchaddr(lp, sa, NULL);
			break;
		}
		if (ok)
			break;
	}

	free(line);
	fclose(fp);

	lpr_allowedhost_res(connid, host,
	    (ok > 0) ? NULL : "Access denied");
}

static void
lpr_allowedhost_res(uint32_t connid, const char *hostname, const char *reject)
{
	m_create(p_frontend, IMSG_LPR_ALLOWEDHOST, connid, 0, -1);
	m_add_string(p_frontend, hostname);
	m_add_string(p_frontend, reject);
	m_close(p_frontend);
}

static int
matchaddr(const char *host, const struct sockaddr *sa, int *gaierrno)
{
	struct addrinfo hints, *res, *r;
	int e, ok = 0;

	memset(&hints, 0, sizeof(hints));
	hints.ai_family = sa->sa_family;
	hints.ai_socktype = SOCK_DGRAM; /*dummy*/
	if ((e = getaddrinfo(host, NULL, &hints, &res))) {
		if (gaierrno)
			*gaierrno = e;
		return 0;
	}
	if (gaierrno)
		*gaierrno = 0;

	for (r = res; r; r = r->ai_next)
		if (cmpsockaddr(sa, r->ai_addr) == 0) {
			ok = 1;
			break;
		}
	freeaddrinfo(res);

	return ok;
}

static int
cmpsockaddr(const struct sockaddr *a, const struct sockaddr *b)
{
	const void *aa, *ab;
	size_t l;

	if (a->sa_family != b->sa_family)
		return (a->sa_family < b->sa_family) ? -1 : 1;

	switch (a->sa_family) {
	case AF_UNIX:
		return 0;

	case AF_INET:
		aa = &(((const struct sockaddr_in*)a)->sin_addr);
		ab = &(((const struct sockaddr_in*)b)->sin_addr);
		l = sizeof(((const struct sockaddr_in*)a)->sin_addr);
		return memcmp(aa, ab, l);

	case AF_INET6:
		aa = &(((const struct sockaddr_in6*)a)->sin6_addr);
		ab = &(((const struct sockaddr_in6*)b)->sin6_addr);
		l = sizeof(((const struct sockaddr_in*)a)->sin_addr);
		return memcmp(aa, ab, l);

	}

	return 0;
}

static int
lpr_mkstemp(void)
{
	char path[PATH_MAX];
	int fd;

	if (strlcpy(path, _PATH_TMP "lpd.XXXXXXXXXX", sizeof(path)) >=
	    sizeof(path)) {
		log_warnx("%s: path too long", __func__);
		return -1;
	}
	if ((fd = mkstemp(path)) == -1) {
		log_warn("%s: mkstemp", __func__);
		return -1;
	}
	(void)unlink(path);
	return fd;

}

static void
lpr_displayq(uint32_t connid, const char *prn, int lng, struct lp_jobfilter *jf)
{
	struct lp_printer lp;
	char cmd[LPR_MAXCMDLEN], buf[16];
	int fd, i;

	if (lp_getprinter(&lp, prn) == -1) {
		lpr_displayq_res(connid, -1, NULL, NULL);
		return;
	}

	fd = lpr_mkstemp();
	if (fd != -1) {
		/* Write formatted queue content into the temporary file. */
		lp_displayq(fd, &lp, lng, jf);
		if (lseek(fd, 0, SEEK_SET) == -1)
			log_warn("%s: lseek", __func__);
	}

	/* Send the result to frontend. */
	if (lp.lp_type == PRN_LPR) {
		snprintf(cmd, sizeof(cmd), "%c%s", lng?'\4':'\3', LP_RP(&lp));
		for (i = 0; i < jf->nuser; i++) {
			strlcat(cmd, " ", sizeof(cmd));
			strlcat(cmd, jf->users[i], sizeof(cmd));
		}
		for (i = 0; i < jf->njob; i++) {
			snprintf(buf, sizeof(buf), " %d", jf->jobs[i]);
			strlcat(cmd, buf, sizeof(cmd));
		}
		lpr_displayq_res(connid, fd, lp.lp_host, cmd);
	}
	else
		lpr_displayq_res(connid, fd, NULL, NULL);

	lp_clearprinter(&lp);
}

static void
lpr_displayq_res(uint32_t connid, int fd, const char *host, const char *cmd)
{
	m_create(p_frontend, IMSG_LPR_DISPLAYQ, connid, 0, fd);
	m_add_string(p_frontend, host);
	m_add_string(p_frontend, cmd);
	m_close(p_frontend);
}

static void
lpr_rmjob(uint32_t connid, const char *prn, const char *agent,
    struct lp_jobfilter *jf)
{
	struct lp_printer lp;
	char cmd[LPR_MAXCMDLEN], buf[16];
	int fd, i, restart = 0;

	if (lp_getprinter(&lp, prn) == -1) {
		lpr_rmjob_res(connid, -1, NULL, NULL);
		return;
	}

	fd = lpr_mkstemp();
	if (fd != -1) {
		/* Write result to the temporary file. */
		restart = lp_rmjob(fd, &lp, agent, jf);
		if (lseek(fd, 0, SEEK_SET) == -1)
			log_warn("%s: lseek", __func__);
	}

	/* Send the result to frontend. */
	if (lp.lp_type == PRN_LPR) {
		snprintf(cmd, sizeof(cmd), "\5%s %s", LP_RP(&lp), agent);
		for (i = 0; i < jf->nuser; i++) {
			strlcat(cmd, " ", sizeof(cmd));
			strlcat(cmd, jf->users[i], sizeof(cmd));
		}
		for (i = 0; i < jf->njob; i++) {
			snprintf(buf, sizeof(buf), " %d", jf->jobs[i]);
			strlcat(cmd, buf, sizeof(cmd));
		}
		lpr_rmjob_res(connid, fd, lp.lp_host, cmd);
	}
	else
		lpr_rmjob_res(connid, fd, NULL, NULL);

	/* If the printer process was stopped, tell parent to re-spawn one. */
	if (restart)
		lpr_printjob(lp.lp_name);

	lp_clearprinter(&lp);
}

static void
lpr_rmjob_res(uint32_t connid, int fd, const char *host, const char *cmd)
{
	m_create(p_frontend, IMSG_LPR_RMJOB, connid, 0, fd);
	m_add_string(p_frontend, host);
	m_add_string(p_frontend, cmd);
	m_close(p_frontend);
}

static void
lpr_recvjob(uint32_t connid, const char *hostfrom, const char *prn)
{
	struct lpr_recvjob *j;
	int qstate;

	if ((j = calloc(1, sizeof(*j))) == NULL) {
		log_warn("%s: calloc", __func__);
		goto fail;
	}
	if (lp_getprinter(&j->lp, prn) == -1)
		goto fail;

	/* Make sure queueing is not disabled. */
	if (lp_getqueuestate(&j->lp, 0, &qstate) == -1) {
		log_warnx("cannot get queue state");
		goto fail;
	}
	if (qstate & LPQ_QUEUE_OFF)
		goto fail;

	if ((j->hostfrom = strdup(hostfrom)) == NULL) {
		log_warn("%s: strdup", __func__);
		goto fail;
	}

	j->connid = connid;
	TAILQ_INIT(&j->df);
	TAILQ_INSERT_TAIL(&recvjobs, j, entry);

	lpr_recvjob_res(connid, LPR_ACK);
	return;

    fail:
	if (j) {
		lp_clearprinter(&j->lp);
		free(j->hostfrom);
	}
	free(j);
	lpr_recvjob_res(connid, LPR_NACK);
}

static void
lpr_recvjob_res(uint32_t connid, int ack)
{
	m_create(p_frontend, IMSG_LPR_RECVJOB, connid, 0, -1);
	m_add_int(p_frontend, ack);
	m_close(p_frontend);
}

static void
lpr_recvjob_cf(uint32_t connid, size_t size, const char *filename)
{
	struct lpr_recvjob *j;
	char fname[PATH_MAX];
	int fd;

	fd = -1;
	TAILQ_FOREACH(j, &recvjobs, entry)
		if (j->connid == connid)
			break;
	if (j == NULL) {
		log_warnx("invalid job id");
		goto done;
	}

	if (j->cfname) {
		log_warnx("duplicate control file");
		goto done;
	}

	if (!lp_validfilename(filename, 1)) {
		log_warnx("invalid control file name %s", filename);
		goto done;
	}

	/* Rewrite file to make sure the hostname is correct. */
	(void)strlcpy(fname, filename, 7);
	if (strlcat(fname, j->hostfrom, sizeof(fname)) >= sizeof(fname)) {
		log_warnx("filename too long");
		goto done;
	}

	if ((j->cfname = strdup(fname)) == NULL) {
		log_warn("%s: stdrup", __func__);
		goto done;
	}

	fd = lp_create(&j->lp, 1, size, j->cfname);
	if (fd == -1) {
		if (errno == EFBIG || errno == ENOSPC)
			log_warn("rejected control file");
		else
			log_warnx("cannot create control file");
		free(j->cfname);
		j->cfname = NULL;
	}

    done:
	m_create(p_frontend, IMSG_LPR_RECVJOB_CF, connid, 0, fd);
	m_add_int(p_frontend, (fd == -1) ? LPR_NACK : LPR_ACK);
	m_add_size(p_frontend, size);
	m_close(p_frontend);
}

static void
lpr_recvjob_df(uint32_t connid, size_t size, const char *filename)
{
	struct lpr_recvfile *f;
	struct lpr_recvjob *j;
	int fd;

	fd = -1;
	TAILQ_FOREACH(j, &recvjobs, entry)
		if (j->connid == connid)
			break;
	if (j == NULL) {
		log_warnx("invalid job id");
		goto done;
	}

	if (!lp_validfilename(filename, 0)) {
		log_warnx("invalid data file name %s", filename);
		goto done;
	}

	if ((f = calloc(1, sizeof(*f))) == NULL) {
		log_warn("%s: calloc", __func__);
		goto done;
	}

	if ((f->dfname = strdup(filename)) == NULL) {
		log_warn("%s: strdup", __func__);
		free(f);
		goto done;
	}

	fd = lp_create(&j->lp, 0, size, f->dfname);
	if (fd == -1) {
		if (errno == EFBIG || errno == ENOSPC)
			log_warn("rejected data file");
		else
			log_warnx("cannot create data file");
		free(f->dfname);
		free(f);
		goto done;
	}

	j->dfcount += 1;
	j->dfsize += size;
	TAILQ_INSERT_TAIL(&j->df, f, entry);

    done:
	m_create(p_frontend, IMSG_LPR_RECVJOB_DF, connid, 0, fd);
	m_add_int(p_frontend, (fd == -1) ? LPR_NACK : LPR_ACK);
	m_add_size(p_frontend, size);
	m_close(p_frontend);
}

static void
lpr_recvjob_clear(uint32_t connid)
{
	struct lpr_recvfile *f;
	struct lpr_recvjob *j;

	TAILQ_FOREACH(j, &recvjobs, entry)
		if (j->connid == connid)
			break;
	if (j == NULL) {
		log_warnx("invalid job id");
		return;
	}

	if (j->cfname) {
		j->cfname[0] = 'c';
		if (lp_unlink(&j->lp, j->cfname) == -1)
			log_warn("cannot unlink %s", j->cfname);
		j->cfname[0] = 't';
		if (lp_unlink(&j->lp, j->cfname) == 1)
			log_warn("cannot unlink %s", j->cfname);
		free(j->cfname);
		j->cfname = NULL;
	}

	while ((f = TAILQ_FIRST(&j->df))) {
		TAILQ_REMOVE(&j->df, f, entry);
		if (lp_unlink(&j->lp, f->dfname) == -1)
			log_warn("cannot unlink %s", f->dfname);
		free(f->dfname);
		free(f);
	}
}

static void
lpr_recvjob_commit(uint32_t connid)
{
	struct lpr_recvjob *j;
	int ack;

	ack = LPR_NACK;
	TAILQ_FOREACH(j, &recvjobs, entry)
		if (j->connid == connid)
			break;
	if (j == NULL) {
		log_warnx("invalid job id");
		return;
	}

	if (!j->cfname) {
		log_warnx("no control file received from %s", j->hostfrom);
		lpr_recvjob_clear(connid);
		lpr_recvjob_free(j);
		return;
	}

	if ((lp_commit(&j->lp, j->cfname) == -1)) {
		log_warn("cannot commit %s", j->cfname);
		lpr_recvjob_clear(connid);
		lpr_recvjob_free(j);
		return;
	}

	log_info("received job %s printer=%s host=%s files=%d size=%zu",
	    j->cfname, j->lp.lp_name, j->hostfrom, j->dfcount, j->dfsize);

	/* Start the printer. */
	lpr_printjob(j->lp.lp_name);
	lpr_recvjob_free(j);
}

static void
lpr_recvjob_rollback(uint32_t connid)
{
	struct lpr_recvjob *j;

	lpr_recvjob_clear(connid);

	TAILQ_FOREACH(j, &recvjobs, entry)
		if (j->connid == connid)
			break;
	if (j == NULL) {
		log_warnx("invalid job id");
		return;
	}
	lpr_recvjob_free(j);
}

static void
lpr_recvjob_free(struct lpr_recvjob *j)
{
	struct lpr_recvfile *f;

	TAILQ_REMOVE(&recvjobs, j, entry);
	lp_clearprinter(&j->lp);
	free(j->hostfrom);
	free(j->cfname);
	while ((f = TAILQ_FIRST(&j->df))) {
		TAILQ_REMOVE(&j->df, f, entry);
		free(f->dfname);
		free(f);
	}
}