/* $OpenBSD: engine_lpr.c,v 1.2 2019/04/04 19:25:45 eric Exp $ */ /* * Copyright (c) 2017 Eric Faurot * * 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 #include #include #include #include #include #include #include #include "lpd.h" #include "lp.h" #include "log.h" #include "proc.h" struct lpr_recvfile { TAILQ_ENTRY(lpr_recvfile) entry; char *dfname; }; struct lpr_recvjob { TAILQ_ENTRY(lpr_recvjob) entry; struct lp_printer lp; uint32_t connid; char *hostfrom; char *cfname; int dfcount; size_t dfsize; TAILQ_HEAD(, lpr_recvfile) df; }; static void lpr_allowedhost(uint32_t, const struct sockaddr *); static void lpr_allowedhost_res(uint32_t, const char *, const char *); static int lpr_mkstemp(void); static void lpr_displayq(uint32_t, const char *, int, struct lp_jobfilter *); static void lpr_displayq_res(uint32_t, int, const char *, const char *); static void lpr_rmjob(uint32_t, const char *, const char *, struct lp_jobfilter *); static void lpr_rmjob_res(uint32_t, int, const char *, const char *); static void lpr_recvjob(uint32_t, const char*, const char *); static void lpr_recvjob_res(uint32_t, int); static void lpr_recvjob_cf(uint32_t, size_t, const char *); static void lpr_recvjob_df(uint32_t, size_t, const char *); static void lpr_recvjob_clear(uint32_t); static void lpr_recvjob_commit(uint32_t); static void lpr_recvjob_rollback(uint32_t); static void lpr_recvjob_free(struct lpr_recvjob *); static int matchaddr(const char *, const struct sockaddr *, int *); static int cmpsockaddr(const struct sockaddr *, const struct sockaddr *); static TAILQ_HEAD(, lpr_recvjob) recvjobs = TAILQ_HEAD_INITIALIZER(recvjobs); void lpr_dispatch_frontend(struct imsgproc *proc, struct imsg *imsg) { struct sockaddr_storage ss; struct lp_jobfilter jf; struct lp_printer lp; const char *hostfrom, *prn, *filename, *agent; uint32_t connid; size_t size; int lng, i; connid = imsg->hdr.peerid; switch (imsg->hdr.type) { case IMSG_LPR_ALLOWEDHOST: m_get_sockaddr(proc, (struct sockaddr *)&ss); m_end(proc); lpr_allowedhost(connid, (struct sockaddr *)&ss); break; case IMSG_LPR_DISPLAYQ: memset(&jf, 0, sizeof(jf)); m_get_int(proc, &lng); m_get_string(proc, &jf.hostfrom); m_get_string(proc, &prn); m_get_int(proc, &jf.njob); for (i = 0; i < jf.njob; i++) m_get_int(proc, &jf.jobs[i]); m_get_int(proc, &jf.nuser); for (i = 0; i < jf.nuser; i++) m_get_string(proc, &jf.users[i]); m_end(proc); lpr_displayq(connid, prn, lng, &jf); break; case IMSG_LPR_PRINTJOB: m_get_string(proc, &prn); m_end(proc); /* Make sure the printer exists. */ if (lp_getprinter(&lp, prn) == -1) break; lpr_printjob(lp.lp_name); lp_clearprinter(&lp); break; case IMSG_LPR_RECVJOB: m_get_string(proc, &hostfrom); m_get_string(proc, &prn); m_end(proc); lpr_recvjob(connid, hostfrom, prn); break; case IMSG_LPR_RECVJOB_CLEAR: m_end(proc); lpr_recvjob_clear(connid); break; case IMSG_LPR_RECVJOB_CF: m_get_size(proc, &size); m_get_string(proc, &filename); m_end(proc); lpr_recvjob_cf(connid, size, filename); break; case IMSG_LPR_RECVJOB_DF: m_get_size(proc, &size); m_get_string(proc, &filename); m_end(proc); lpr_recvjob_df(connid, size, filename); break; case IMSG_LPR_RECVJOB_COMMIT: m_end(proc); lpr_recvjob_commit(connid); break; case IMSG_LPR_RECVJOB_ROLLBACK: m_end(proc); lpr_recvjob_rollback(connid); break; case IMSG_LPR_RMJOB: memset(&jf, 0, sizeof(jf)); m_get_string(proc, &jf.hostfrom); m_get_string(proc, &prn); m_get_string(proc, &agent); m_get_int(proc, &jf.njob); for (i = 0; i < jf.njob; i++) m_get_int(proc, &jf.jobs[i]); m_get_int(proc, &jf.nuser); for (i = 0; i < jf.nuser; i++) m_get_string(proc, &jf.users[i]); m_end(proc); lpr_rmjob(connid, prn, agent, &jf); break; default: fatalx("%s: unexpected imsg %s", __func__, log_fmt_imsgtype(imsg->hdr.type)); } } void lpr_shutdown() { struct lpr_recvjob *j; /* Cleanup incoming jobs. */ while ((j = TAILQ_FIRST(&recvjobs))) { lpr_recvjob_clear(j->connid); lpr_recvjob_free(j); } } void lpr_printjob(const char *prn) { m_create(p_priv, IMSG_LPR_PRINTJOB, 0, 0, -1); m_add_string(p_priv, prn); m_close(p_priv); } static void lpr_allowedhost(uint32_t connid, const struct sockaddr *sa) { FILE *fp; size_t linesz = 0; ssize_t linelen; char host[NI_MAXHOST], addr[NI_MAXHOST], serv[NI_MAXSERV]; char dom[NI_MAXHOST], *lp, *ep, *line = NULL; int e, rev = 0, ok = 0; /* Always accept local connections. */ if (sa->sa_family == AF_UNIX) { lpr_allowedhost_res(connid, lpd_hostname, NULL); return; } host[0] = '\0'; /* Print address. */ if ((e = getnameinfo(sa, sa->sa_len, addr, sizeof(addr), serv, sizeof(serv), NI_NUMERICHOST))) { log_warnx("%s: could not print addr: %s", __func__, gai_strerror(e)); lpr_allowedhost_res(connid, host, "Malformed address"); return; } /* Get the hostname for the address. */ if ((e = getnameinfo(sa, sa->sa_len, host, sizeof(host), NULL, 0, NI_NAMEREQD))) { if (e != EAI_NONAME) log_warnx("%s: could not resolve %s: %s", __func__, addr, gai_strerror(e)); lpr_allowedhost_res(connid, host, "No hostname found for your address"); return; } /* Check for a valid DNS roundtrip. */ if (!matchaddr(host, sa, &e)) { if (e) log_warnx("%s: getaddrinfo: %s: %s", __func__, host, gai_strerror(e)); lpr_allowedhost_res(connid, host, e ? "Cannot resolve your hostname" : "Your hostname and your address do not match"); return; } /* Scan the hosts.lpd file. */ if ((fp = fopen(_PATH_HOSTSLPD, "r")) == NULL) { log_warn("%s: %s", __func__, _PATH_HOSTSLPD); lpr_allowedhost_res(connid, host, "Cannot access " _PATH_HOSTSLPD); return; } dom[0] = '\0'; while ((linelen = getline(&line, &linesz, fp)) != -1) { /* Drop comment and strip line. */ for (lp = line; *lp; lp++) if (!isspace((unsigned char)*lp)) break; if (*lp == '#' || *lp == '\0') continue; for (ep = lp + 1; *ep; ep++) if (isspace((unsigned char)*ep) || *ep == '#') { *ep = '\0'; break; } rev = 0; switch (lp[0]) { case '-': case '+': switch (lp[1]) { case '\0': ok = 1; break; case '@': if (dom[0] == '\0') getdomainname(dom, sizeof(dom)); ok = innetgr(lp + 2, host, NULL, dom); break; default: ok = matchaddr(lp + 1, sa, NULL); break; } if (lp[0] == '-') ok = -ok; break; default: ok = matchaddr(lp, sa, NULL); break; } if (ok) break; } free(line); fclose(fp); lpr_allowedhost_res(connid, host, (ok > 0) ? NULL : "Access denied"); } static void lpr_allowedhost_res(uint32_t connid, const char *hostname, const char *reject) { m_create(p_frontend, IMSG_LPR_ALLOWEDHOST, connid, 0, -1); m_add_string(p_frontend, hostname); m_add_string(p_frontend, reject); m_close(p_frontend); } static int matchaddr(const char *host, const struct sockaddr *sa, int *gaierrno) { struct addrinfo hints, *res, *r; int e, ok = 0; memset(&hints, 0, sizeof(hints)); hints.ai_family = sa->sa_family; hints.ai_socktype = SOCK_DGRAM; /*dummy*/ if ((e = getaddrinfo(host, NULL, &hints, &res))) { if (gaierrno) *gaierrno = e; return 0; } if (gaierrno) *gaierrno = 0; for (r = res; r; r = r->ai_next) if (cmpsockaddr(sa, r->ai_addr) == 0) { ok = 1; break; } freeaddrinfo(res); return ok; } static int cmpsockaddr(const struct sockaddr *a, const struct sockaddr *b) { const void *aa, *ab; size_t l; if (a->sa_family != b->sa_family) return (a->sa_family < b->sa_family) ? -1 : 1; switch (a->sa_family) { case AF_UNIX: return 0; case AF_INET: aa = &(((const struct sockaddr_in*)a)->sin_addr); ab = &(((const struct sockaddr_in*)b)->sin_addr); l = sizeof(((const struct sockaddr_in*)a)->sin_addr); return memcmp(aa, ab, l); case AF_INET6: aa = &(((const struct sockaddr_in6*)a)->sin6_addr); ab = &(((const struct sockaddr_in6*)b)->sin6_addr); l = sizeof(((const struct sockaddr_in*)a)->sin_addr); return memcmp(aa, ab, l); } return 0; } static int lpr_mkstemp(void) { char path[PATH_MAX]; int fd; if (strlcpy(path, _PATH_TMP "lpd.XXXXXXXXXX", sizeof(path)) >= sizeof(path)) { log_warnx("%s: path too long", __func__); return -1; } if ((fd = mkstemp(path)) == -1) { log_warn("%s: mkstemp", __func__); return -1; } (void)unlink(path); return fd; } static void lpr_displayq(uint32_t connid, const char *prn, int lng, struct lp_jobfilter *jf) { struct lp_printer lp; char cmd[LPR_MAXCMDLEN], buf[16]; int fd, i; if (lp_getprinter(&lp, prn) == -1) { lpr_displayq_res(connid, -1, NULL, NULL); return; } fd = lpr_mkstemp(); if (fd != -1) { /* Write formatted queue content into the temporary file. */ lp_displayq(fd, &lp, lng, jf); if (lseek(fd, 0, SEEK_SET) == -1) log_warn("%s: lseek", __func__); } /* Send the result to frontend. */ if (lp.lp_type == PRN_LPR) { snprintf(cmd, sizeof(cmd), "%c%s", lng?'\4':'\3', LP_RP(&lp)); for (i = 0; i < jf->nuser; i++) { strlcat(cmd, " ", sizeof(cmd)); strlcat(cmd, jf->users[i], sizeof(cmd)); } for (i = 0; i < jf->njob; i++) { snprintf(buf, sizeof(buf), " %d", jf->jobs[i]); strlcat(cmd, buf, sizeof(cmd)); } lpr_displayq_res(connid, fd, lp.lp_host, cmd); } else lpr_displayq_res(connid, fd, NULL, NULL); lp_clearprinter(&lp); } static void lpr_displayq_res(uint32_t connid, int fd, const char *host, const char *cmd) { m_create(p_frontend, IMSG_LPR_DISPLAYQ, connid, 0, fd); m_add_string(p_frontend, host); m_add_string(p_frontend, cmd); m_close(p_frontend); } static void lpr_rmjob(uint32_t connid, const char *prn, const char *agent, struct lp_jobfilter *jf) { struct lp_printer lp; char cmd[LPR_MAXCMDLEN], buf[16]; int fd, i, restart = 0; if (lp_getprinter(&lp, prn) == -1) { lpr_rmjob_res(connid, -1, NULL, NULL); return; } fd = lpr_mkstemp(); if (fd != -1) { /* Write result to the temporary file. */ restart = lp_rmjob(fd, &lp, agent, jf); if (lseek(fd, 0, SEEK_SET) == -1) log_warn("%s: lseek", __func__); } /* Send the result to frontend. */ if (lp.lp_type == PRN_LPR) { snprintf(cmd, sizeof(cmd), "\5%s %s", LP_RP(&lp), agent); for (i = 0; i < jf->nuser; i++) { strlcat(cmd, " ", sizeof(cmd)); strlcat(cmd, jf->users[i], sizeof(cmd)); } for (i = 0; i < jf->njob; i++) { snprintf(buf, sizeof(buf), " %d", jf->jobs[i]); strlcat(cmd, buf, sizeof(cmd)); } lpr_rmjob_res(connid, fd, lp.lp_host, cmd); } else lpr_rmjob_res(connid, fd, NULL, NULL); /* If the printer process was stopped, tell parent to re-spawn one. */ if (restart) lpr_printjob(lp.lp_name); lp_clearprinter(&lp); } static void lpr_rmjob_res(uint32_t connid, int fd, const char *host, const char *cmd) { m_create(p_frontend, IMSG_LPR_RMJOB, connid, 0, fd); m_add_string(p_frontend, host); m_add_string(p_frontend, cmd); m_close(p_frontend); } static void lpr_recvjob(uint32_t connid, const char *hostfrom, const char *prn) { struct lpr_recvjob *j; int qstate; if ((j = calloc(1, sizeof(*j))) == NULL) { log_warn("%s: calloc", __func__); goto fail; } if (lp_getprinter(&j->lp, prn) == -1) goto fail; /* Make sure queueing is not disabled. */ if (lp_getqueuestate(&j->lp, 0, &qstate) == -1) { log_warnx("cannot get queue state"); goto fail; } if (qstate & LPQ_QUEUE_OFF) goto fail; if ((j->hostfrom = strdup(hostfrom)) == NULL) { log_warn("%s: strdup", __func__); goto fail; } j->connid = connid; TAILQ_INIT(&j->df); TAILQ_INSERT_TAIL(&recvjobs, j, entry); lpr_recvjob_res(connid, LPR_ACK); return; fail: if (j) { lp_clearprinter(&j->lp); free(j->hostfrom); } free(j); lpr_recvjob_res(connid, LPR_NACK); } static void lpr_recvjob_res(uint32_t connid, int ack) { m_create(p_frontend, IMSG_LPR_RECVJOB, connid, 0, -1); m_add_int(p_frontend, ack); m_close(p_frontend); } static void lpr_recvjob_cf(uint32_t connid, size_t size, const char *filename) { struct lpr_recvjob *j; char fname[PATH_MAX]; int fd; fd = -1; TAILQ_FOREACH(j, &recvjobs, entry) if (j->connid == connid) break; if (j == NULL) { log_warnx("invalid job id"); goto done; } if (j->cfname) { log_warnx("duplicate control file"); goto done; } if (!lp_validfilename(filename, 1)) { log_warnx("invalid control file name %s", filename); goto done; } /* Rewrite file to make sure the hostname is correct. */ (void)strlcpy(fname, filename, 7); if (strlcat(fname, j->hostfrom, sizeof(fname)) >= sizeof(fname)) { log_warnx("filename too long"); goto done; } if ((j->cfname = strdup(fname)) == NULL) { log_warn("%s: stdrup", __func__); goto done; } fd = lp_create(&j->lp, 1, size, j->cfname); if (fd == -1) { if (errno == EFBIG || errno == ENOSPC) log_warn("rejected control file"); else log_warnx("cannot create control file"); free(j->cfname); j->cfname = NULL; } done: m_create(p_frontend, IMSG_LPR_RECVJOB_CF, connid, 0, fd); m_add_int(p_frontend, (fd == -1) ? LPR_NACK : LPR_ACK); m_add_size(p_frontend, size); m_close(p_frontend); } static void lpr_recvjob_df(uint32_t connid, size_t size, const char *filename) { struct lpr_recvfile *f; struct lpr_recvjob *j; int fd; fd = -1; TAILQ_FOREACH(j, &recvjobs, entry) if (j->connid == connid) break; if (j == NULL) { log_warnx("invalid job id"); goto done; } if (!lp_validfilename(filename, 0)) { log_warnx("invalid data file name %s", filename); goto done; } if ((f = calloc(1, sizeof(*f))) == NULL) { log_warn("%s: calloc", __func__); goto done; } if ((f->dfname = strdup(filename)) == NULL) { log_warn("%s: strdup", __func__); free(f); goto done; } fd = lp_create(&j->lp, 0, size, f->dfname); if (fd == -1) { if (errno == EFBIG || errno == ENOSPC) log_warn("rejected data file"); else log_warnx("cannot create data file"); free(f->dfname); free(f); goto done; } j->dfcount += 1; j->dfsize += size; TAILQ_INSERT_TAIL(&j->df, f, entry); done: m_create(p_frontend, IMSG_LPR_RECVJOB_DF, connid, 0, fd); m_add_int(p_frontend, (fd == -1) ? LPR_NACK : LPR_ACK); m_add_size(p_frontend, size); m_close(p_frontend); } static void lpr_recvjob_clear(uint32_t connid) { struct lpr_recvfile *f; struct lpr_recvjob *j; TAILQ_FOREACH(j, &recvjobs, entry) if (j->connid == connid) break; if (j == NULL) { log_warnx("invalid job id"); return; } if (j->cfname) { j->cfname[0] = 'c'; if (lp_unlink(&j->lp, j->cfname) == -1) log_warn("cannot unlink %s", j->cfname); j->cfname[0] = 't'; if (lp_unlink(&j->lp, j->cfname) == 1) log_warn("cannot unlink %s", j->cfname); free(j->cfname); j->cfname = NULL; } while ((f = TAILQ_FIRST(&j->df))) { TAILQ_REMOVE(&j->df, f, entry); if (lp_unlink(&j->lp, f->dfname) == -1) log_warn("cannot unlink %s", f->dfname); free(f->dfname); free(f); } } static void lpr_recvjob_commit(uint32_t connid) { struct lpr_recvjob *j; int ack; ack = LPR_NACK; TAILQ_FOREACH(j, &recvjobs, entry) if (j->connid == connid) break; if (j == NULL) { log_warnx("invalid job id"); return; } if (!j->cfname) { log_warnx("no control file received from %s", j->hostfrom); lpr_recvjob_clear(connid); lpr_recvjob_free(j); return; } if ((lp_commit(&j->lp, j->cfname) == -1)) { log_warn("cannot commit %s", j->cfname); lpr_recvjob_clear(connid); lpr_recvjob_free(j); return; } log_info("received job %s printer=%s host=%s files=%d size=%zu", j->cfname, j->lp.lp_name, j->hostfrom, j->dfcount, j->dfsize); /* Start the printer. */ lpr_printjob(j->lp.lp_name); lpr_recvjob_free(j); } static void lpr_recvjob_rollback(uint32_t connid) { struct lpr_recvjob *j; lpr_recvjob_clear(connid); TAILQ_FOREACH(j, &recvjobs, entry) if (j->connid == connid) break; if (j == NULL) { log_warnx("invalid job id"); return; } lpr_recvjob_free(j); } static void lpr_recvjob_free(struct lpr_recvjob *j) { struct lpr_recvfile *f; TAILQ_REMOVE(&recvjobs, j, entry); lp_clearprinter(&j->lp); free(j->hostfrom); free(j->cfname); while ((f = TAILQ_FIRST(&j->df))) { TAILQ_REMOVE(&j->df, f, entry); free(f->dfname); free(f); } }