diff options
author | Eric Faurot <eric@cvs.openbsd.org> | 2018-04-27 16:14:38 +0000 |
---|---|---|
committer | Eric Faurot <eric@cvs.openbsd.org> | 2018-04-27 16:14:38 +0000 |
commit | 396409d3f48b2b81d5bdeeec18d62fc6f8b48238 (patch) | |
tree | 74790a98b23741a16fd61a15f1c88ddb1b1f2e0b /usr.sbin/lpd/printer.c | |
parent | eb452b015e8c2a6e5ce80d6872e2adec6c181392 (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.c | 1401 |
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; + } +} |