root/usr.sbin/lpd/printer.c
/*      $OpenBSD: printer.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/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 privileges.
         */
        if (!debug) {
                fd = open(LP_LF(lp), O_WRONLY|O_APPEND);
                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 privileges. */
        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 < 0) {
                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 descriptor, 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;
        }
}