summaryrefslogtreecommitdiff
path: root/usr.sbin/lpd/printer.c
diff options
context:
space:
mode:
authorEric Faurot <eric@cvs.openbsd.org>2018-04-27 16:14:38 +0000
committerEric Faurot <eric@cvs.openbsd.org>2018-04-27 16:14:38 +0000
commit396409d3f48b2b81d5bdeeec18d62fc6f8b48238 (patch)
tree74790a98b23741a16fd61a15f1c88ddb1b1f2e0b /usr.sbin/lpd/printer.c
parenteb452b015e8c2a6e5ce80d6872e2adec6c181392 (diff)
Import lpd, a re-implementation of the lpr daemon following the latest
OpenBSD coding practices (fork+exec/privsep/pledge/...). It is only intended to replace the lpd(8) daemon for the moment, not the lpr(1), lprm(1), lpq(1) and lpc(8) commands. This is a work in progress. The server part should be fairly functionnal, but the printer part is not complete: remote printers should work, for local printers it depends on the setup. Anyway, at this point it's better in the tree than rotting on my disk. ok deraadt@
Diffstat (limited to 'usr.sbin/lpd/printer.c')
-rw-r--r--usr.sbin/lpd/printer.c1401
1 files changed, 1401 insertions, 0 deletions
diff --git a/usr.sbin/lpd/printer.c b/usr.sbin/lpd/printer.c
new file mode 100644
index 00000000000..ff4b0cf3d1d
--- /dev/null
+++ b/usr.sbin/lpd/printer.c
@@ -0,0 +1,1401 @@
+/* $OpenBSD: printer.c,v 1.1.1.1 2018/04/27 16:14:37 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 <sys/stat.h>
+#include <sys/tree.h>
+#include <sys/wait.h>
+
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <pwd.h>
+#include <signal.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <syslog.h>
+#include <unistd.h>
+#include <vis.h>
+
+#include "lpd.h"
+#include "lp.h"
+#include "log.h"
+
+#define RETRY_MAX 5
+
+#define JOB_OK 0
+#define JOB_AGAIN 1
+#define JOB_IGNORE 2
+#define JOB_ERROR 3
+
+enum {
+ OK = 0,
+ ERR_TRANSIENT, /* transient error */
+ ERR_ACCOUNT, /* account required on the local machine */
+ ERR_ACCESS, /* cannot read file */
+ ERR_INODE, /* inode changed */
+ ERR_NOIMPL, /* unimplemented feature */
+ ERR_REJECTED, /* remote server rejected a job */
+ ERR_ERROR, /* filter report an error */
+ ERR_FILTER, /* filter return invalid status */
+};
+
+struct job {
+ char *class;
+ char *host;
+ char *literal;
+ char *mail;
+ char *name;
+ char *person;
+ char *statinfo;
+ char *title;
+ int indent;
+ int pagewidth;
+};
+
+struct prnstate {
+ int pfd; /* printer fd */
+ int ofilter; /* use output filter when printing */
+ int ofd; /* output filter fd */
+ pid_t opid; /* output filter process */
+ int tof; /* true if at top of form */
+ int count; /* number of printed files */
+ char efile[64]; /* filename for filter stderr */
+};
+
+static void sighandler(int);
+static char *xstrdup(const char *);
+
+static int openfile(const char *, const char *, struct stat *, FILE **);
+static int printjob(const char *, int);
+static void printbanner(struct job *);
+static int printfile(struct job *, int, const char *, const char *);
+static int sendjob(const char *, int);
+static int sendcmd(const char *, ...);
+static int sendfile(int, const char *, const char *);
+static int recvack(void);
+static void mailreport(struct job *, int);
+
+static void prn_open(void);
+static int prn_connect(void);
+static void prn_close(void);
+static int prn_fstart(void);
+static void prn_fsuspend(void);
+static void prn_fresume(void);
+static void prn_fclose(void);
+static int prn_formfeed(void);
+static int prn_write(const char *, size_t);
+static int prn_writefile(FILE *);
+static int prn_puts(const char *);
+static ssize_t prn_read(char *, size_t);
+
+static struct lp_printer *lp;
+static struct prnstate *prn;
+
+void
+printer(int debug, int verbose, const char *name)
+{
+ struct sigaction sa;
+ struct passwd *pw;
+ struct lp_queue q;
+ int fd, jobidx, qstate, r, reload, retry;
+ char buf[64], curr[1024];
+
+ /* Early initialisation. */
+ log_init(debug, LOG_LPR);
+ log_setverbose(verbose);
+ snprintf(buf, sizeof(buf), "printer:%s", name);
+ log_procinit(buf);
+ setproctitle("%s", buf);
+
+ if ((lpd_hostname = malloc(HOST_NAME_MAX+1)) == NULL)
+ fatal("%s: malloc", __func__);
+ gethostname(lpd_hostname, HOST_NAME_MAX+1);
+
+ /* Detach from lpd session if not in debug mode. */
+ if (!debug)
+ if (setsid() == -1)
+ fatal("%s: setsid", __func__);
+
+ /* Read printer config. */
+ if ((lp = calloc(1, sizeof(*lp))) == NULL)
+ fatal("%s: calloc", __func__);
+ if (lp_getprinter(lp, name) == -1)
+ exit(1);
+
+ /*
+ * Redirect stderr if not in debug mode.
+ * This must be done before dropping priviledges.
+ */
+ if (!debug) {
+ fd = open(LP_LF(lp), O_WRONLY|O_APPEND, 0664);
+ if (fd == -1)
+ fatal("%s: open: %s", __func__, LP_LF(lp));
+ if (fd != STDERR_FILENO) {
+ if (dup2(fd, STDERR_FILENO) == -1)
+ fatalx("%s: dup2", __func__);
+ (void)close(fd);
+ }
+ }
+
+ /* Drop priviledges. */
+ if ((pw = getpwnam(LPD_USER)) == NULL)
+ fatalx("unknown user " LPD_USER);
+
+ if (setgroups(1, &pw->pw_gid) ||
+ setresgid(pw->pw_gid, pw->pw_gid, pw->pw_gid) ||
+ setresuid(pw->pw_uid, pw->pw_uid, pw->pw_uid))
+ fatal("cannot drop privileges");
+
+ /* Initialize the printer state. */
+ if ((prn = calloc(1, sizeof(*prn))) == NULL)
+ fatal("%s: calloc", __func__);
+ prn->pfd = -1;
+ prn->ofd = -1;
+
+ /* Setup signals */
+ memset(&sa, 0, sizeof(sa));
+ sa.sa_handler = sighandler;
+ sa.sa_flags = SA_RESTART;
+ sigemptyset(&sa.sa_mask);
+ sigaddset(&sa.sa_mask, SIGINT); /* for kill() in sighandler */
+ sigaction(SIGHUP, &sa, NULL);
+ sigaction(SIGINT, &sa, NULL);
+ sigaction(SIGQUIT, &sa, NULL);
+ sigaction(SIGTERM, &sa, NULL);
+
+ /* Grab lock file. */
+ if (lp_lock(lp) == -1) {
+ if (errno == EWOULDBLOCK) {
+ log_debug("already locked");
+ exit(0);
+ }
+ fatalx("cannot open lock file");
+ }
+
+ /* Pledge. */
+ switch (lp->lp_type) {
+ case PRN_LOCAL:
+ pledge("stdio rpath wpath cpath flock getpw tty proc exec",
+ NULL);
+ break;
+
+ case PRN_NET:
+ pledge("stdio rpath wpath cpath inet flock dns getpw proc exec",
+ NULL);
+ break;
+
+ case PRN_LPR:
+ pledge("stdio rpath wpath cpath inet flock dns getpw", NULL);
+ break;
+ }
+
+ /* Start processing the queue. */
+ memset(&q, 0, sizeof(q));
+ jobidx = 0;
+ reload = 1;
+ retry = 0;
+ curr[0] = '\0';
+
+ for (;;) {
+
+ /* Check the queue state. */
+ if (lp_getqueuestate(lp, 1, &qstate) == -1)
+ fatalx("cannot get queue state");
+ if (qstate & LPQ_PRINTER_DOWN) {
+ log_debug("printing disabled");
+ break;
+ }
+ if (qstate & LPQ_QUEUE_UPDATED) {
+ log_debug("queue updated");
+ if (reload == 0)
+ lp_clearqueue(&q);
+ reload = 1;
+ }
+
+ /* Read the queue if needed. */
+ if (reload || q.count == 0) {
+ if (lp_readqueue(lp, &q) == -1)
+ fatalx("cannot read queue");
+ jobidx = 0;
+ reload = 0;
+ }
+
+ /* If the queue is empty, all done */
+ if (q.count <= jobidx) {
+ log_debug("queue empty");
+ break;
+ }
+
+ /* Open the printer if needed. */
+ if (prn->pfd == -1) {
+ prn_open();
+ /*
+ * Opening the printer might take some time.
+ * Re-read the queue in case its state has changed.
+ */
+ lp_clearqueue(&q);
+ reload = 1;
+ continue;
+ }
+
+ if (strcmp(curr, q.cfname[jobidx]))
+ retry = 0;
+ else
+ strlcpy(curr, q.cfname[jobidx], sizeof(curr));
+
+ lp_setcurrtask(lp, q.cfname[jobidx]);
+ if (lp->lp_type == PRN_LPR)
+ r = sendjob(q.cfname[jobidx], retry);
+ else
+ r = printjob(q.cfname[jobidx], retry);
+ lp_setcurrtask(lp, NULL);
+
+ switch (r) {
+ case JOB_OK:
+ log_info("job %s %s successfully", q.cfname[jobidx],
+ (lp->lp_type == PRN_LPR) ? "relayed" : "printed");
+ break;
+ case JOB_AGAIN:
+ retry++;
+ continue;
+ case JOB_IGNORE:
+ break;
+ case JOB_ERROR:
+ log_warnx("job %s could not be printed",
+ q.cfname[jobidx]);
+ break;
+ }
+ curr[0] = '\0';
+ jobidx++;
+ retry = 0;
+ }
+
+ if (prn->pfd != -1) {
+ if (prn->count) {
+ prn_formfeed();
+ if (lp->lp_tr)
+ prn_puts(lp->lp_tr);
+ }
+ prn_close();
+ }
+
+ exit(0);
+}
+
+static void
+sighandler(int code)
+{
+ log_info("got signal %d", code);
+
+ exit(0);
+}
+
+static char *
+xstrdup(const char *s)
+{
+ char *r;
+
+ if ((r = strdup(s)) == NULL)
+ fatal("strdup");
+
+ return r;
+}
+
+/*
+ * Open control/data file, and check that the inode information is valid.
+ * On success, fill the "st" structure and set "fpp" and return 0 (OK).
+ * Return an error code on error.
+ */
+static int
+openfile(const char *fname, const char *inodeinfo, struct stat *st, FILE **fpp)
+{
+ FILE *fp;
+ char buf[64];
+
+ if (inodeinfo) {
+ log_warnx("cannot open %s: symlink not implemented", fname);
+ return ERR_NOIMPL;
+ }
+ else {
+ if ((fp = lp_fopen(lp, fname)) == NULL) {
+ log_warn("cannot open %s", fname);
+ return ERR_ACCESS;
+ }
+ }
+
+ if (fstat(fileno(fp), st) == -1) {
+ log_warn("%s: fstat: %s", __func__, fname);
+ fclose(fp);
+ return ERR_ACCESS;
+ }
+
+ if (inodeinfo) {
+ snprintf(buf, sizeof(buf), "%d %llu", st->st_dev, st->st_ino);
+ if (strcmp(inodeinfo, buf)) {
+ log_warnx("inode changed for %s", fname);
+ fclose(fp);
+ return ERR_INODE;
+ }
+ }
+
+ *fpp = fp;
+
+ return OK;
+}
+
+/*
+ * Print the job described by the control file.
+ */
+static int
+printjob(const char *cfname, int retry)
+{
+ struct job job;
+ FILE *fp;
+ ssize_t len;
+ size_t linesz = 0;
+ char *line = NULL;
+ const char *errstr;
+ long long num;
+ int r, ret = JOB_OK;
+
+ log_debug("printing job %s...", cfname);
+
+ prn->efile[0] = '\0';
+ memset(&job, 0, sizeof(job));
+ job.pagewidth = lp->lp_pw;
+
+ if ((fp = lp_fopen(lp, cfname)) == NULL) {
+ if (errno == ENOENT) {
+ log_info("missing control file %s", cfname);
+ return JOB_IGNORE;
+ }
+ /* XXX no fatal? */
+ fatal("cannot open %s", cfname);
+ }
+
+ /* First pass: setup the job structure, print banner and print data. */
+ while ((len = getline(&line, &linesz, fp)) != -1) {
+ if (line[len-1] == '\n')
+ line[len-1] = '\0';
+
+ switch (line[0]) {
+ case 'C': /* Classification */
+ if (line[1]) {
+ free(job.class);
+ job.class = xstrdup(line + 1);
+ }
+ else if (job.class == NULL)
+ job.class = xstrdup(lpd_hostname);
+ break;
+
+ case 'H': /* Host name */
+ free(job.host);
+ job.host = xstrdup(line + 1);
+ if (job.class == NULL)
+ job.class = xstrdup(line + 1);
+ break;
+
+ case 'I': /* Indent */
+ errstr = NULL;
+ num = strtonum(line + 1, 0, INT_MAX, &errstr);
+ if (errstr == NULL)
+ job.indent = num;
+ else
+ log_warnx("strtonum: %s", errstr);
+ break;
+
+ case 'J': /* Job Name */
+ free(job.name);
+ if (line[1])
+ job.name = strdup(line + 1);
+ else
+ job.name = strdup(" ");
+ break;
+
+ case 'L': /* Literal */
+ free(job.literal);
+ job.literal = xstrdup(line + 1);
+ if (!lp->lp_sh && !lp->lp_hl)
+ printbanner(&job);
+ break;
+
+ case 'M': /* Send mail to the specified user */
+ free(job.mail);
+ job.mail = xstrdup(line + 1);
+ break;
+
+ case 'N': /* Filename */
+ break;
+
+ case 'P': /* Person */
+ free(job.person);
+ job.person = xstrdup(line + 1);
+ if (lp->lp_rs && getpwnam(job.person) == NULL) {
+ mailreport(&job, ERR_ACCOUNT);
+ ret = JOB_ERROR;
+ goto remove;
+ }
+ break;
+
+ case 'S': /* Stat info for symlink protection */
+ job.statinfo = xstrdup(line + 1);
+ break;
+
+ case 'T': /* Title for pr */
+ job.title = xstrdup(line + 1);
+ break;
+
+ case 'U': /* Unlink */
+ break;
+
+ case 'W': /* Width */
+ errstr = NULL;
+ num = strtonum(line + 1, 0, INT_MAX, &errstr);
+ if (errstr == NULL)
+ job.pagewidth = num;
+ else
+ log_warnx("strtonum: %s", errstr);
+ break;
+
+ case '1': /* troff fonts */
+ case '2':
+ case '3':
+ case '4':
+ /* XXX not implemented */
+ break;
+
+ default:
+ if (line[0] < 'a' || line[0] > 'z')
+ break;
+
+ r = printfile(&job, line[0], line+1, job.statinfo);
+ free(job.statinfo);
+ job.statinfo = NULL;
+ free(job.title);
+ job.title = NULL;
+ if (r) {
+ if (r == ERR_TRANSIENT && retry < RETRY_MAX) {
+ ret = JOB_AGAIN;
+ goto done;
+ }
+ mailreport(&job, r);
+ ret = JOB_ERROR;
+ goto remove;
+ }
+ }
+ }
+
+ remove:
+ if (lp_unlink(lp, cfname) == -1)
+ log_warn("cannot unlink %s", cfname);
+
+ /* Second pass: print trailing banner, mail report, and remove files. */
+ rewind(fp);
+ while ((len = getline(&line, &linesz, fp)) != -1) {
+ if (line[len-1] == '\n')
+ line[len-1] = '\0';
+
+ switch (line[0]) {
+ case 'L': /* Literal */
+ if (ret != JOB_OK)
+ break;
+ if (!lp->lp_sh && lp->lp_hl)
+ printbanner(&job);
+ break;
+
+ case 'M': /* Send mail to the specified user */
+ if (ret == JOB_OK)
+ mailreport(&job, ret);
+ break;
+
+ case 'U': /* Unlink */
+ if (lp_unlink(lp, line + 1) == -1)
+ log_warn("cannot unlink %s", line + 1);
+ break;
+ }
+ }
+
+ done:
+ if (prn->efile[0])
+ unlink(prn->efile);
+ (void)fclose(fp);
+ free(job.class);
+ free(job.host);
+ free(job.literal);
+ free(job.mail);
+ free(job.name);
+ free(job.person);
+ free(job.statinfo);
+ free(job.title);
+ return ret;
+}
+
+static void
+printbanner(struct job *job)
+{
+ time_t t;
+
+ time(&t);
+
+ prn_formfeed();
+
+ if (lp->lp_sb) {
+ if (job->class) {
+ prn_puts(job->class);
+ prn_puts(":");
+ }
+ prn_puts(job->literal);
+ prn_puts(" Job: ");
+ prn_puts(job->name);
+ prn_puts(" Date: ");
+ prn_puts(ctime(&t));
+ prn_puts("\n");
+ } else {
+ prn_puts("\n\n\n");
+ lp_banner(prn->pfd, job->literal, lp->lp_pw);
+ prn_puts("\n\n");
+ lp_banner(prn->pfd, job->name, lp->lp_pw);
+ if (job->class) {
+ prn_puts("\n\n\n");
+ lp_banner(prn->pfd, job->class, lp->lp_pw);
+ }
+ prn_puts("\n\n\n\n\t\t\t\t\tJob: ");
+ prn_puts(job->name);
+ prn_puts("\n\t\t\t\t\tDate: ");
+ prn_puts(ctime(&t));
+ prn_puts("\n");
+ }
+
+ prn_formfeed();
+}
+
+static int
+printfile(struct job *job, int fmt, const char *fname, const char *inodeinfo)
+{
+ pid_t pid;
+ struct stat st;
+ FILE *fp;
+ size_t n;
+ int ret, argc, efd, status;
+ char *argv[16], *prog, width[16], length[16], indent[16], tmp[512];
+
+ log_debug("printing file %s...", fname);
+
+ switch (fmt) {
+ case 'f': /* print file as-is */
+ case 'o': /* print postscript file */
+ case 'l': /* print file as-is but pass control chars */
+ break;
+
+ case 'p': /* print using pr(1) */
+ case 'r': /* print fortran text file */
+ case 't': /* print troff output */
+ case 'n': /* print ditroff output */
+ case 'd': /* print tex output */
+ case 'c': /* print cifplot output */
+ case 'g': /* print plot output */
+ case 'v': /* print raster output */
+ default:
+ log_warn("unrecognized output format '%c'", fmt);
+ return ERR_NOIMPL;
+ }
+
+ if ((ret = openfile(fname, inodeinfo, &st, &fp)) != OK)
+ return ret;
+
+ prn_formfeed();
+
+ /*
+ * No input filter, just write the raw file.
+ */
+ if (!lp->lp_if) {
+ if (prn_writefile(fp) == -1)
+ ret = ERR_TRANSIENT;
+ else
+ ret = OK;
+ (void)fclose(fp);
+ return ret;
+ }
+
+ /*
+ * Otherwise, run the input filter with proper plumbing.
+ */
+
+ /* Prepare filter arguments. */
+ snprintf(width, sizeof(width), "-w%d", job->pagewidth);
+ snprintf(length, sizeof(length), "-l%ld", lp->lp_pl);
+ snprintf(indent, sizeof(indent), "-i%d", job->indent);
+ prog = strrchr(lp->lp_if, '/');
+
+ argc = 0;
+ argv[argc++] = prog ? (prog + 1) : lp->lp_if;
+ if (fmt == 'l')
+ argv[argc++] = "-c";
+ argv[argc++] = width;
+ argv[argc++] = length;
+ argv[argc++] = indent;
+ argv[argc++] = "-n";
+ argv[argc++] = job->person;
+ if (job->name) {
+ argv[argc++] = "-j";
+ argv[argc++]= job->name;
+ }
+ argv[argc++] = "-h";
+ argv[argc++] = job->host;
+ argv[argc++] = lp->lp_af;
+ argv[argc++] = NULL;
+
+ /* Open the stderr file. */
+ strlcpy(prn->efile, "/tmp/prn.XXXXXXXX", sizeof(prn->efile));
+ if ((efd = mkstemp(prn->efile)) == -1) {
+ log_warn("%s: mkstemp", __func__);
+ (void)fclose(fp);
+ return ERR_TRANSIENT;
+ }
+
+ /* Disable output filter. */
+ prn_fsuspend();
+
+ /* Run input filter */
+ switch ((pid = fork())) {
+ case -1:
+ log_warn("%s: fork", __func__);
+ close(efd);
+ prn_fresume();
+ return ERR_TRANSIENT;
+
+ case 0:
+ if (dup2(fileno(fp), STDIN_FILENO) == -1)
+ fatal("%s:, dup2", __func__);
+ if (dup2(prn->pfd, STDOUT_FILENO) == -1)
+ fatal("%s:, dup2", __func__);
+ if (dup2(efd, STDERR_FILENO) == -1)
+ fatal("%s:, dup2", __func__);
+ if (closefrom(3) == -1)
+ fatal("%s:, closefrom", __func__);
+ execv(lp->lp_if, argv);
+ log_warn("%s:, execv", __func__);
+ exit(2);
+
+ default:
+ break;
+ }
+
+ log_debug("waiting for ifilter...");
+
+ /* Wait for input filter to finish. */
+ while (waitpid(pid, &status, 0) == -1)
+ log_warn("%s: waitpid", __func__);
+
+ log_debug("ifilter done, status %d", status);
+
+ /* Resume output filter */
+ prn_fresume();
+ prn->tof = 0;
+
+ /* Copy efd to stderr */
+ if (lseek(efd, 0, SEEK_SET) == -1)
+ log_warn("%s: lseek", __func__);
+ while ((n = read(efd, tmp, sizeof(tmp))) > 0)
+ (void)write(STDERR_FILENO, tmp, n);
+ close(efd);
+
+ if (!WIFEXITED(status)) {
+ log_warn("filter terminated (termsig=%d)", WTERMSIG(status));
+ return ERR_FILTER;
+ }
+
+ switch (WEXITSTATUS(status)) {
+ case 0:
+ prn->tof = 1;
+ return OK;
+
+ case 1:
+ return ERR_TRANSIENT;
+
+ case 2:
+ return ERR_ERROR;
+
+ default:
+ log_warn("filter exited (exitstatus=%d)", WEXITSTATUS(status));
+ return ERR_FILTER;
+ }
+}
+
+static int
+sendjob(const char *cfname, int retry)
+{
+ struct job job;
+ FILE *fp;
+ ssize_t len;
+ size_t linesz = 0;
+ char *line = NULL;
+ int ret = JOB_OK, r;
+
+ log_debug("sending job %s...", cfname);
+
+ memset(&job, 0, sizeof(job));
+
+ if ((fp = lp_fopen(lp, cfname)) == NULL) {
+ if (errno == ENOENT) {
+ log_info("missing control file %s", cfname);
+ return JOB_IGNORE;
+ }
+ /* XXX no fatal? */
+ fatal("cannot open %s", cfname);
+ }
+
+ /* First pass: setup the job structure, and forward data files. */
+ while ((len = getline(&line, &linesz, fp)) != -1) {
+ if (line[len-1] == '\n')
+ line[len-1] = '\0';
+
+ switch (line[0]) {
+ case 'P':
+ free(job.person);
+ job.person = xstrdup(line + 1);
+ break;
+
+ case 'S':
+ free(job.statinfo);
+ job.statinfo = xstrdup(line + 1);
+ break;
+
+ default:
+ if (line[0] < 'a' || line[0] > 'z')
+ break;
+
+ r = sendfile('\3', line+1, job.statinfo);
+ free(job.statinfo);
+ job.statinfo = NULL;
+ if (r) {
+ if (r == ERR_TRANSIENT && retry < RETRY_MAX) {
+ ret = JOB_AGAIN;
+ goto done;
+ }
+ mailreport(&job, r);
+ ret = JOB_ERROR;
+ goto remove;
+ }
+ }
+ }
+
+ /* Send the control file. */
+ if ((r = sendfile('\2', cfname, ""))) {
+ if (r == ERR_TRANSIENT && retry < RETRY_MAX) {
+ ret = JOB_AGAIN;
+ goto done;
+ }
+ mailreport(&job, r);
+ ret = JOB_ERROR;
+ }
+
+ remove:
+ if (lp_unlink(lp, cfname) == -1)
+ log_warn("cannot unlink %s", cfname);
+
+ /* Second pass: remove files. */
+ rewind(fp);
+ while ((len = getline(&line, &linesz, fp)) != -1) {
+ if (line[len-1] == '\n')
+ line[len-1] = '\0';
+
+ switch (line[0]) {
+ case 'U':
+ if (lp_unlink(lp, line + 1) == -1)
+ log_warn("cannot unlink %s", line + 1);
+ break;
+ }
+ }
+
+ done:
+ (void)fclose(fp);
+ free(line);
+ free(job.person);
+ free(job.statinfo);
+ return ret;
+}
+
+/*
+ * Send a LPR command to the remote lpd server and return the ack.
+ * Return 0 for ack, 1 or nack, -1 and set errno on error.
+ */
+static int
+sendcmd(const char *fmt, ...)
+{
+ va_list ap;
+ unsigned char line[1024];
+ int len;
+
+ va_start(ap, fmt);
+ len = vsnprintf(line, sizeof(line), fmt, ap);
+ va_end(ap);
+
+ if (len == -1) {
+ log_warn("%s: vsnprintf", __func__);
+ return -1;
+ }
+
+ if (prn_puts(line) == -1)
+ return -1;
+
+ return recvack();
+}
+
+static int
+sendfile(int type, const char *fname, const char *inodeinfo)
+{
+ struct stat st;
+ FILE *fp = NULL;
+ int ret;
+
+ log_debug("sending file %s...", fname);
+
+ if ((ret = openfile(fname, inodeinfo, &st, &fp)) != OK)
+ return ret;
+
+ ret = ERR_TRANSIENT;
+ if (sendcmd("%c%lld %s\n", type, (long long)st.st_size, fname)) {
+ if (errno == 0)
+ ret = ERR_REJECTED;
+ goto fail;
+ }
+
+ lp_setstatus(lp, "sending %s to %s", fname, lp->lp_rm);
+ if (prn_writefile(fp) == -1 || prn_write("\0", 1) == -1)
+ goto fail;
+ if (recvack()) {
+ if (errno == 0)
+ ret = ERR_REJECTED;
+ goto fail;
+ }
+
+ ret = OK;
+
+ fail:
+ (void)fclose(fp);
+
+ if (ret == ERR_REJECTED)
+ log_warnx("%s rejected by remote host", fname);
+
+ return ret;
+}
+
+/*
+ * Read a ack response from the server.
+ * Return 0 for ack, 1 or nack, -1 and set errno on error.
+ */
+static int
+recvack(void)
+{
+ char visbuf[256 * 4 + 1];
+ unsigned char line[1024];
+ ssize_t n;
+
+ if ((n = prn_read(line, sizeof(line))) == -1)
+ return -1;
+
+ if (n == 1) {
+ errno = 0;
+ if (line[0])
+ log_warnx("%s: \\%d", lp->lp_host, line[0]);
+ return line[0] ? 1 : 0;
+ }
+
+ if (n > 256)
+ n = 256;
+ line[n] = '\0';
+ if (line[n-1] == '\n')
+ line[--n] = '\0';
+
+ strvisx(visbuf, line, n, VIS_NL | VIS_CSTYLE);
+ log_warnx("%s: %s", lp->lp_host, visbuf);
+
+ errno = 0;
+ return -1;
+}
+
+static void
+mailreport(struct job *job, int result)
+{
+ struct stat st;
+ FILE *fp = NULL, *efp;
+ const char *user;
+ char *cp;
+ int p[2], c;
+
+ if (job->mail)
+ user = job->mail;
+ else
+ user = job->person;
+ if (user == NULL) {
+ log_warnx("no user to send report to");
+ return;
+ }
+
+ if (pipe(p) == -1) {
+ log_warn("pipe");
+ return;
+ }
+
+ switch (fork()) {
+ case -1:
+ (void)close(p[0]);
+ (void)close(p[1]);
+ log_warn("fork");
+ return;
+
+ case 0:
+ if (dup2(p[0], 0) == -1)
+ fatal("%s: dup2", __func__);
+ (void)closefrom(3);
+ if ((cp = strrchr(_PATH_SENDMAIL, '/')))
+ cp++;
+ else
+ cp = _PATH_SENDMAIL;
+ execl(_PATH_SENDMAIL, cp, "-t", (char *)NULL);
+ fatal("%s: execl: %s", __func__, _PATH_SENDMAIL);
+
+ default:
+ (void)close(p[0]);
+ if ((fp = fdopen(p[1], "w")) == NULL) {
+ (void)close(p[1]);
+ log_warn("fdopen");
+ return;
+ }
+ }
+
+ fprintf(fp, "Auto-Submitted: auto-generated\n");
+ fprintf(fp, "To: %s@%s\n", user, job->host);
+ fprintf(fp, "Subject: %s printer job \"%s\"\n", lp->lp_name,
+ job->name ? job->name : "<unknown>");
+ fprintf(fp, "Reply-To: root@%s\n\n", lpd_hostname);
+ fprintf(fp, "Your printer job ");
+ if (job->name)
+ fprintf(fp, " (%s) ", job->name);
+
+ fprintf(fp, "\n");
+
+ switch (result) {
+ case OK:
+ fprintf(fp, "completed successfully");
+ break;
+
+ case ERR_ACCOUNT:
+ fprintf(fp, "could not be printed without an account on %s",
+ lpd_hostname);
+ break;
+
+ case ERR_ACCESS:
+ fprintf(fp, "could not be printed because the file could "
+ " not be read");
+ break;
+
+ case ERR_INODE:
+ fprintf(fp, "was not printed because it was not linked to"
+ " the original file");
+ break;
+
+ case ERR_NOIMPL:
+ fprintf(fp, "was not printed because some feature is missing");
+ break;
+
+ case ERR_FILTER:
+ efp = fopen(prn->efile, "r");
+ if (efp && fstat(fileno(efp), &st) == 0 && st.st_size) {
+ fprintf(fp,
+ "had the following errors and may not have printed:\n");
+ while ((c = getc(efp)) != EOF)
+ putc(c, fp);
+ }
+ else
+ fprintf(fp,
+ "had some errors and may not have printed\n");
+
+ if (efp)
+ fclose(efp);
+ break;
+
+ default:
+ printf("could not be printed");
+ break;
+ }
+
+ fprintf(fp, "\n");
+ fclose(fp);
+
+ wait(NULL);
+}
+
+static void
+prn_open(void)
+{
+ const char *status, *oldstatus;
+ int i;
+
+ switch (lp->lp_type) {
+ case PRN_LOCAL:
+ lp_setstatus(lp, "opening %s", LP_LP(lp));
+ break;
+
+ case PRN_NET:
+ case PRN_LPR:
+ lp_setstatus(lp, "connecting to %s:%s", lp->lp_host,
+ lp->lp_port ? lp->lp_port : "printer");
+ break;
+ }
+
+ status = oldstatus = NULL;
+ for (i = 0; prn->pfd == -1; i += (i < 6) ? 1 : 0) {
+
+ if (status != oldstatus) {
+ lp_setstatus(lp, "%s", status);
+ oldstatus = status;
+ }
+
+ if (i)
+ sleep(1 << i);
+
+ if ((prn->pfd = prn_connect()) == -1) {
+ status = "waiting for printer to come up";
+ continue;
+ }
+
+ if (lp->lp_type == PRN_LPR) {
+ /* Send a recvjob request. */
+ if (sendcmd("\2%s\n", LP_RP(lp))) {
+ if (errno == 0)
+ log_warnx("remote queue is disabled");
+ (void)close(prn->pfd);
+ prn->pfd = -1;
+ status = "waiting for queue to be enabled";
+ }
+ }
+ }
+
+ switch (lp->lp_type) {
+ case PRN_LOCAL:
+ lp_setstatus(lp, "printing to %s", LP_LP(lp));
+ break;
+
+ case PRN_NET:
+ lp_setstatus(lp, "printing to %s:%s", lp->lp_host, lp->lp_port);
+ break;
+
+ case PRN_LPR:
+ lp_setstatus(lp, "sending to %s", lp->lp_host);
+ break;
+ }
+
+ prn->tof = lp->lp_fo ? 0 : 1;
+ prn->count = 0;
+
+ prn_fstart();
+}
+
+/*
+ * Open the printer device, or connect to the remote host.
+ * Return the printer file desciptor, or -1 on error.
+ */
+static int
+prn_connect(void)
+{
+ struct addrinfo hints, *res, *res0;
+ int save_errno;
+ int fd, e, mode;
+ const char *cause = NULL, *host, *port;
+
+ if (lp->lp_type == PRN_LOCAL) {
+ mode = lp->lp_rw ? O_RDWR : O_WRONLY;
+ if ((fd = open(LP_LP(lp), mode)) == -1) {
+ log_warn("failed to open %s", LP_LP(lp));
+ return -1;
+ }
+
+ if (isatty(fd)) {
+ lp_stty(lp, fd);
+ return -1;
+ }
+
+ return fd;
+ }
+
+ host = lp->lp_host;
+ port = lp->lp_port ? lp->lp_port : "printer";
+
+ memset(&hints, 0, sizeof(hints));
+ hints.ai_family = AF_UNSPEC;
+ hints.ai_socktype = SOCK_STREAM;
+ if ((e = getaddrinfo(host, port, &hints, &res0))) {
+ log_warnx("%s:%s: %s", host, port, gai_strerror(e));
+ return -1;
+ }
+
+ fd = -1;
+ for (res = res0; res && fd == -1; res = res->ai_next) {
+ fd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
+ if (fd == -1)
+ cause = "socket";
+ else if (connect(fd, res->ai_addr, res->ai_addrlen) == -1) {
+ cause = "connect";
+ save_errno = errno;
+ (void)close(fd);
+ errno = save_errno;
+ fd = -1;
+ }
+ }
+
+ if (fd == -1)
+ log_warn("%s", cause);
+ else
+ log_debug("connected to %s:%s", host, port);
+
+ freeaddrinfo(res0);
+ return fd;
+}
+
+static void
+prn_close(void)
+{
+ prn_fclose();
+
+ (void)close(prn->pfd);
+ prn->pfd = -1;
+}
+
+/*
+ * Fork the output filter process if needed.
+ */
+static int
+prn_fstart(void)
+{
+ char width[32], length[32], *cp;
+ int fildes[2], i;
+
+ if (lp->lp_type == PRN_LPR || (!lp->lp_of))
+ return 0;
+
+ pipe(fildes);
+
+ for (i = 0; i < 20; i++) {
+ if (i)
+ sleep(i);
+ if ((prn->opid = fork()) != -1)
+ break;
+ log_warn("%s: fork", __func__);
+ }
+
+ if (prn->opid == -1) {
+ log_warnx("cannot fork output filter");
+ return -1;
+ }
+
+ if (prn->opid == 0) {
+ /* child */
+ dup2(fildes[0], 0);
+ dup2(prn->pfd, 1);
+ (void)closefrom(3);
+ cp = strrchr(lp->lp_of, '/');
+ if (cp)
+ cp += 1;
+ else
+ cp = lp->lp_of;
+ snprintf(width, sizeof(width), "-w%ld", lp->lp_pw);
+ snprintf(length, sizeof(length), "-l%ld", lp->lp_pl);
+ execl(lp->lp_of, cp, width, length, (char *)NULL);
+ log_warn("%s: execl", __func__);
+ exit(1);
+ }
+
+ close(fildes[0]);
+ prn->ofd = fildes[1];
+ prn->ofilter = 1;
+
+ return 0;
+}
+
+/*
+ * Suspend the output filter process.
+ */
+static void
+prn_fsuspend(void)
+{
+ pid_t pid;
+ int status;
+
+ if (prn->opid == 0)
+ return;
+
+ prn_puts("\031\1");
+ while ((pid = waitpid(WAIT_ANY, &status, WUNTRACED)) && pid != prn->opid)
+ ;
+
+ prn->ofilter = 0;
+ if (!WIFSTOPPED(status)) {
+ log_warn("output filter died (exitstatus=%d termsig=%d)",
+ WEXITSTATUS(status), WTERMSIG(status));
+ prn->opid = 0;
+ prn_fclose();
+ }
+}
+
+/*
+ * Resume the output filter process.
+ */
+static void
+prn_fresume(void)
+{
+ if (prn->opid == 0)
+ return;
+
+ if (kill(prn->opid, SIGCONT) == -1)
+ fatal("cannot restart output filter");
+ prn->ofilter = 1;
+}
+
+/*
+ * Close the output filter socket and wait for the process to terminate
+ * if currently running.
+ */
+static void
+prn_fclose(void)
+{
+ pid_t pid;
+
+ close(prn->ofd);
+ prn->ofd = -1;
+
+ while (prn->opid) {
+ pid = wait(NULL);
+ if (pid == -1)
+ log_warn("%s: wait", __func__);
+ else if (pid == prn->opid)
+ prn->opid = 0;
+ }
+}
+
+/*
+ * Write a form-feed if the printer cap requires it, and if not currently
+ * at top of form. Return 0 on success, or -1 on error and set errno.
+ */
+static int
+prn_formfeed(void)
+{
+ if (!lp->lp_sf && !prn->tof)
+ if (prn_puts(LP_FF(lp)) == -1)
+ return -1;
+ prn->tof = 1;
+ return 0;
+}
+
+/*
+ * Write data to the printer (or output filter process).
+ * Return 0 on success, or -1 and set errno.
+ */
+static int
+prn_write(const char *buf, size_t len)
+{
+ ssize_t n;
+ int fd;
+
+ fd = prn->ofilter ? prn->ofd : prn->pfd;
+
+ log_debug("prn_write(fd=%d len=%zu, of=%d pfd=%d ofd=%d)", fd, len,
+ prn->ofilter, prn->pfd, prn->ofd);
+
+ if (fd == -1) {
+ log_warnx("printer socket not opened");
+ errno = EPIPE;
+ return -1;
+ }
+
+ while (len) {
+ if ((n = write(fd, buf, len)) == -1) {
+ if (errno == EINTR)
+ continue;
+ log_warn("%s: write", __func__);
+ /* XXX close the printer */
+ return -1;
+ }
+ len -= n;
+ buf += n;
+ prn->tof = 0;
+ }
+
+ return 0;
+}
+
+/*
+ * Write a string to the printer (or output filter process).
+ * Return 0 on success, or -1 and set errno.
+ */
+static int
+prn_puts(const char *buf)
+{
+ return prn_write(buf, strlen(buf));
+}
+
+/*
+ * Write the FILE content to the printer (or output filter process).
+ * Return 0 on success, or -1 and set errno.
+ */
+static int
+prn_writefile(FILE *fp)
+{
+ char buf[BUFSIZ];
+ size_t r;
+
+ while (!feof(fp)) {
+ r = fread(buf, 1, sizeof(buf), fp);
+ if (ferror(fp)) {
+ log_warn("%s: fread", __func__);
+ return -1;
+ }
+ if (r && (prn_write(buf, r) == -1))
+ return -1;
+ }
+
+ return 0;
+}
+
+/*
+ * Read data from the printer socket into the given buffer.
+ * Return 0 on success, or -1 and set errno.
+ */
+static ssize_t
+prn_read(char *buf, size_t sz)
+{
+ ssize_t n;
+
+ for (;;) {
+ if ((n = read(prn->pfd, buf, sz)) == 0) {
+ errno = ECONNRESET;
+ n = -1;
+ }
+ if (n == -1) {
+ if (errno == EINTR)
+ continue;
+ /* XXX close printer? */
+ log_warn("%s: read", __func__);
+ return -1;
+ }
+ return n;
+ }
+}