/* * Print server daemon. */ #include "apue.h" #include "print.h" #include "ipp.h" #include #include #include #include #include #include #include #include /* * These are for the HTTP response from the printer. */ #define HTTP_INFO(x) ((x) >= 100 && (x) <= 199) #define HTTP_SUCCESS(x) ((x) >= 200 && (x) <= 299) /* * Describes a print job. */ struct job { struct job *next; /* next in list */ struct job *prev; /* previous in list */ long jobid; /* job ID */ struct printreq req; /* copy of print request */ }; /* * Describes a thread processing a client request. */ struct worker_thread { struct worker_thread *next; /* next in list */ struct worker_thread *prev; /* previous in list */ pthread_t tid; /* thread ID */ int sockfd; /* socket */ }; /* * Needed for logging. */ int log_to_stderr = 0; /* * Printer-related stuff. */ struct addrinfo *printer; char *printer_name; pthread_mutex_t configlock = PTHREAD_MUTEX_INITIALIZER; int reread; /* * Thread-related stuff. */ struct worker_thread *workers; pthread_mutex_t workerlock = PTHREAD_MUTEX_INITIALIZER; sigset_t mask; /* * Job-related stuff. */ struct job *jobhead, *jobtail; int jobfd; long nextjob; pthread_mutex_t joblock = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t jobwait = PTHREAD_COND_INITIALIZER; /* * Function prototypes. */ void init_request(void); void init_printer(void); void update_jobno(void); long get_newjobno(void); void add_job(struct printreq *, long); void replace_job(struct job *); void remove_job(struct job *); void build_qonstart(void); void *client_thread(void *); void *printer_thread(void *); void *signal_thread(void *); ssize_t readmore(int, char **, int, int *); int printer_status(int, struct job *); void add_worker(pthread_t, int); void kill_workers(void); void client_cleanup(void *); /* * Main print server thread. Accepts connect requests from * clients and spawns additional threads to service requests. * * LOCKING: none. */ int main(int argc, char *argv[]) { pthread_t tid; struct addrinfo *ailist, *aip; int sockfd, err, i, n, maxfd; char *host; fd_set rendezvous, rset; struct sigaction sa; struct passwd *pwdp; if (argc != 1) err_quit("usage: printd"); daemonize("printd"); sigemptyset(&sa.sa_mask); sa.sa_flags = 0; sa.sa_handler = SIG_IGN; if (sigaction(SIGPIPE, &sa, NULL) < 0) log_sys("sigaction failed"); sigemptyset(&mask); sigaddset(&mask, SIGHUP); sigaddset(&mask, SIGTERM); if ((err = pthread_sigmask(SIG_BLOCK, &mask, NULL)) != 0) log_sys("pthread_sigmask failed"); init_request(); init_printer(); #ifdef _SC_HOST_NAME_MAX n = sysconf(_SC_HOST_NAME_MAX); if (n < 0) /* best guess */ #endif n = HOST_NAME_MAX; if ((host = malloc(n)) == NULL) log_sys("malloc error"); if (gethostname(host, n) < 0) log_sys("gethostname error"); if ((err = getaddrlist(host, "print", &ailist)) != 0) { log_quit("getaddrinfo error: %s", gai_strerror(err)); exit(1); } FD_ZERO(&rendezvous); maxfd = -1; for (aip = ailist; aip != NULL; aip = aip->ai_next) { if ((sockfd = initserver(SOCK_STREAM, aip->ai_addr, aip->ai_addrlen, QLEN)) >= 0) { FD_SET(sockfd, &rendezvous); if (sockfd > maxfd) maxfd = sockfd; } } if (maxfd == -1) log_quit("service not enabled"); pwdp = getpwnam("lp"); if (pwdp == NULL) log_sys("can't find user lp"); if (pwdp->pw_uid == 0) log_quit("user lp is privileged"); if (setuid(pwdp->pw_uid) < 0) log_sys("can't change IDs to user lp"); pthread_create(&tid, NULL, printer_thread, NULL); pthread_create(&tid, NULL, signal_thread, NULL); build_qonstart(); log_msg("daemon initialized"); for (;;) { rset = rendezvous; if (select(maxfd+1, &rset, NULL, NULL, NULL) < 0) log_sys("select failed"); for (i = 0; i <= maxfd; i++) { if (FD_ISSET(i, &rset)) { /* * Accept the connection and handle * the request. */ sockfd = accept(i, NULL, NULL); if (sockfd < 0) log_ret("accept failed"); pthread_create(&tid, NULL, client_thread, (void *)sockfd); } } } exit(1); } /* * Initialize the job ID file. Use a record lock to prevent * more than one printer daemon from running at a time. * * LOCKING: none, except for record-lock on job ID file. */ void init_request(void) { int n; char name[FILENMSZ]; sprintf(name, "%s/%s", SPOOLDIR, JOBFILE); jobfd = open(name, O_CREAT|O_RDWR, S_IRUSR|S_IWUSR); if (write_lock(jobfd, 0, SEEK_SET, 0) < 0) log_quit("daemon already running"); /* * Reuse the name buffer for the job counter. */ if ((n = read(jobfd, name, FILENMSZ)) < 0) log_sys("can't read job file"); if (n == 0) nextjob = 1; else nextjob = atol(name); } /* * Initialize printer information. * * LOCKING: none. */ void init_printer(void) { printer = get_printaddr(); if (printer == NULL) { log_msg("no printer device registered"); exit(1); } printer_name = printer->ai_canonname; if (printer_name == NULL) printer_name = "printer"; log_msg("printer is %s", printer_name); } /* * Update the job ID file with the next job number. * * LOCKING: none. */ void update_jobno(void) { char buf[32]; lseek(jobfd, 0, SEEK_SET); sprintf(buf, "%ld", nextjob); if (write(jobfd, buf, strlen(buf)) < 0) log_sys("can't update job file"); } /* * Get the next job number. * * LOCKING: acquires and releases joblock. */ long get_newjobno(void) { long jobid; pthread_mutex_lock(&joblock); jobid = nextjob++; if (nextjob <= 0) nextjob = 1; pthread_mutex_unlock(&joblock); return(jobid); } /* * Add a new job to the list of pending jobs. Then signal * the printer thread that a job is pending. * * LOCKING: acquires and releases joblock. */ void add_job(struct printreq *reqp, long jobid) { struct job *jp; if ((jp = malloc(sizeof(struct job))) == NULL) log_sys("malloc failed"); memcpy(&jp->req, reqp, sizeof(struct printreq)); jp->jobid = jobid; jp->next = NULL; pthread_mutex_lock(&joblock); jp->prev = jobtail; if (jobtail == NULL) jobhead = jp; else jobtail->next = jp; jobtail = jp; pthread_mutex_unlock(&joblock); pthread_cond_signal(&jobwait); } /* * Replace a job back on the head of the list. * * LOCKING: acquires and releases joblock. */ void replace_job(struct job *jp) { pthread_mutex_lock(&joblock); jp->prev = NULL; jp->next = jobhead; if (jobhead == NULL) jobtail = jp; else jobhead->prev = jp; jobhead = jp; pthread_mutex_unlock(&joblock); } /* * Remove a job from the list of pending jobs. * * LOCKING: caller must hold joblock. */ void remove_job(struct job *target) { if (target->next != NULL) target->next->prev = target->prev; else jobtail = target->prev; if (target->prev != NULL) target->prev->next = target->next; else jobhead = target->next; } /* * Check the spool directory for pending jobs on start-up. * * LOCKING: none. */ void build_qonstart(void) { int fd, err, nr; long jobid; DIR *dirp; struct dirent *entp; struct printreq req; char dname[FILENMSZ], fname[FILENMSZ]; sprintf(dname, "%s/%s", SPOOLDIR, REQDIR); if ((dirp = opendir(dname)) == NULL) return; while ((entp = readdir(dirp)) != NULL) { /* * Skip "." and ".." */ if (strcmp(entp->d_name, ".") == 0 || strcmp(entp->d_name, "..") == 0) continue; /* * Read the request structure. */ sprintf(fname, "%s/%s/%s", SPOOLDIR, REQDIR, entp->d_name); if ((fd = open(fname, O_RDONLY)) < 0) continue; nr = read(fd, &req, sizeof(struct printreq)); if (nr != sizeof(struct printreq)) { if (nr < 0) err = errno; else err = EIO; close(fd); log_msg("build_qonstart: can't read %s: %s", fname, strerror(err)); unlink(fname); sprintf(fname, "%s/%s/%s", SPOOLDIR, DATADIR, entp->d_name); unlink(fname); continue; } jobid = atol(entp->d_name); log_msg("adding job %ld to queue", jobid); add_job(&req, jobid); } closedir(dirp); } /* * Accept a print job from a client. * * LOCKING: none. */ void * client_thread(void *arg) { int n, fd, sockfd, nr, nw, first; long jobid; pthread_t tid; struct printreq req; struct printresp res; char name[FILENMSZ]; char buf[IOBUFSZ]; tid = pthread_self(); pthread_cleanup_push(client_cleanup, (void *)tid); sockfd = (int)arg; add_worker(tid, sockfd); /* * Read the request header. */ if ((n = treadn(sockfd, &req, sizeof(struct printreq), 10)) != sizeof(struct printreq)) { res.jobid = 0; if (n < 0) res.retcode = htonl(errno); else res.retcode = htonl(EIO); strncpy(res.msg, strerror(res.retcode), MSGLEN_MAX); writen(sockfd, &res, sizeof(struct printresp)); pthread_exit((void *)1); } req.size = ntohl(req.size); req.flags = ntohl(req.flags); /* * Create the data file. */ jobid = get_newjobno(); sprintf(name, "%s/%s/%ld", SPOOLDIR, DATADIR, jobid); if ((fd = creat(name, FILEPERM)) < 0) { res.jobid = 0; if (n < 0) res.retcode = htonl(errno); else res.retcode = htonl(EIO); log_msg("client_thread: can't create %s: %s", name, strerror(res.retcode)); strncpy(res.msg, strerror(res.retcode), MSGLEN_MAX); writen(sockfd, &res, sizeof(struct printresp)); pthread_exit((void *)1); } /* * Read the file and store it in the spool directory. */ first = 1; while ((nr = tread(sockfd, buf, IOBUFSZ, 20)) > 0) { if (first) { first = 0; if (strncmp(buf, "%!PS", 4) != 0) req.flags |= PR_TEXT; } nw = write(fd, buf, nr); if (nw != nr) { if (nw < 0) res.retcode = htonl(errno); else res.retcode = htonl(EIO); log_msg("client_thread: can't write %s: %s", name, strerror(res.retcode)); close(fd); strncpy(res.msg, strerror(res.retcode), MSGLEN_MAX); writen(sockfd, &res, sizeof(struct printresp)); unlink(name); pthread_exit((void *)1); } } close(fd); /* * Create the control file. */ sprintf(name, "%s/%s/%ld", SPOOLDIR, REQDIR, jobid); fd = creat(name, FILEPERM); if (fd < 0) { res.jobid = 0; if (n < 0) res.retcode = htonl(errno); else res.retcode = htonl(EIO); log_msg("client_thread: can't create %s: %s", name, strerror(res.retcode)); strncpy(res.msg, strerror(res.retcode), MSGLEN_MAX); writen(sockfd, &res, sizeof(struct printresp)); sprintf(name, "%s/%s/%ld", SPOOLDIR, DATADIR, jobid); unlink(name); pthread_exit((void *)1); } nw = write(fd, &req, sizeof(struct printreq)); if (nw != sizeof(struct printreq)) { res.jobid = 0; if (nw < 0) res.retcode = htonl(errno); else res.retcode = htonl(EIO); log_msg("client_thread: can't write %s: %s", name, strerror(res.retcode)); close(fd); strncpy(res.msg, strerror(res.retcode), MSGLEN_MAX); writen(sockfd, &res, sizeof(struct printresp)); unlink(name); sprintf(name, "%s/%s/%ld", SPOOLDIR, DATADIR, jobid); unlink(name); pthread_exit((void *)1); } close(fd); /* * Send response to client. */ res.retcode = 0; res.jobid = htonl(jobid); sprintf(res.msg, "request ID %ld", jobid); writen(sockfd, &res, sizeof(struct printresp)); /* * Notify the printer thread, clean up, and exit. */ log_msg("adding job %ld to queue", jobid); add_job(&req, jobid); pthread_cleanup_pop(1); return((void *)0); } /* * Add a worker to the list of worker threads. * * LOCKING: acquires and releases workerlock. */ void add_worker(pthread_t tid, int sockfd) { struct worker_thread *wtp; if ((wtp = malloc(sizeof(struct worker_thread))) == NULL) { log_ret("add_worker: can't malloc"); pthread_exit((void *)1); } wtp->tid = tid; wtp->sockfd = sockfd; pthread_mutex_lock(&workerlock); wtp->prev = NULL; wtp->next = workers; if (workers == NULL) workers = wtp; else workers->prev = wtp; pthread_mutex_unlock(&workerlock); } /* * Cancel (kill) all outstanding workers. * * LOCKING: acquires and releases workerlock. */ void kill_workers(void) { struct worker_thread *wtp; pthread_mutex_lock(&workerlock); for (wtp = workers; wtp != NULL; wtp = wtp->next) pthread_cancel(wtp->tid); pthread_mutex_unlock(&workerlock); } /* * Cancellation routine for the worker thread. * * LOCKING: acquires and releases workerlock. */ void client_cleanup(void *arg) { struct worker_thread *wtp; pthread_t tid; tid = (pthread_t)arg; pthread_mutex_lock(&workerlock); for (wtp = workers; wtp != NULL; wtp = wtp->next) { if (wtp->tid == tid) { if (wtp->next != NULL) wtp->next->prev = wtp->prev; if (wtp->prev != NULL) wtp->prev->next = wtp->next; else workers = wtp->next; break; } } pthread_mutex_unlock(&workerlock); if (wtp != NULL) { close(wtp->sockfd); free(wtp); } } /* * Deal with signals. * * LOCKING: acquires and releases configlock. */ void * signal_thread(void *arg) { int err, signo; for (;;) { err = sigwait(&mask, &signo); if (err != 0) log_quit("sigwait failed: %s", strerror(err)); switch (signo) { case SIGHUP: /* * Schedule to re-read the configuration file. */ pthread_mutex_lock(&configlock); reread = 1; pthread_mutex_unlock(&configlock); break; case SIGTERM: kill_workers(); log_msg("terminate with signal %s", strsignal(signo)); exit(0); default: kill_workers(); log_quit("unexpected signal %d", signo); } } } /* * Add an option to the IPP header. * * LOCKING: none. */ char * add_option(char *cp, int tag, char *optname, char *optval) { int n; union { int16_t s; char c[2]; } u; *cp++ = tag; n = strlen(optname); u.s = htons(n); *cp++ = u.c[0]; *cp++ = u.c[1]; strcpy(cp, optname); cp += n; n = strlen(optval); u.s = htons(n); *cp++ = u.c[0]; *cp++ = u.c[1]; strcpy(cp, optval); return(cp + n); } /* * Single thread to communicate with the printer. * * LOCKING: acquires and releases joblock and configlock. */ void * printer_thread(void *arg) { struct job *jp; int hlen, ilen, sockfd, fd, nr, nw; char *icp, *hcp; struct ipp_hdr *hp; struct stat sbuf; struct iovec iov[2]; char name[FILENMSZ]; char hbuf[HBUFSZ]; char ibuf[IBUFSZ]; char buf[IOBUFSZ]; char str[64]; for (;;) { /* * Get a job to print. */ pthread_mutex_lock(&joblock); while (jobhead == NULL) { log_msg("printer_thread: waiting..."); pthread_cond_wait(&jobwait, &joblock); } remove_job(jp = jobhead); log_msg("printer_thread: picked up job %ld", jp->jobid); pthread_mutex_unlock(&joblock); update_jobno(); /* * Check for a change in the config file. */ pthread_mutex_lock(&configlock); if (reread) { freeaddrinfo(printer); printer = NULL; printer_name = NULL; reread = 0; pthread_mutex_unlock(&configlock); init_printer(); } else { pthread_mutex_unlock(&configlock); } /* * Send job to printer. */ sprintf(name, "%s/%s/%ld", SPOOLDIR, DATADIR, jp->jobid); if ((fd = open(name, O_RDONLY)) < 0) { log_msg("job %ld canceled - can't open %s: %s", jp->jobid, name, strerror(errno)); free(jp); continue; } if (fstat(fd, &sbuf) < 0) { log_msg("job %ld canceled - can't fstat %s: %s", jp->jobid, name, strerror(errno)); free(jp); close(fd); continue; } if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { log_msg("job %ld deferred - can't create socket: %s", jp->jobid, strerror(errno)); goto defer; } if (connect_retry(sockfd, printer->ai_addr, printer->ai_addrlen) < 0) { log_msg("job %ld deferred - can't contact printer: %s", jp->jobid, strerror(errno)); goto defer; } /* * Set up the IPP header. */ icp = ibuf; hp = (struct ipp_hdr *)icp; hp->major_version = 1; hp->minor_version = 1; hp->operation = htons(OP_PRINT_JOB); hp->request_id = htonl(jp->jobid); icp += offsetof(struct ipp_hdr, attr_group); *icp++ = TAG_OPERATION_ATTR; icp = add_option(icp, TAG_CHARSET, "attributes-charset", "utf-8"); icp = add_option(icp, TAG_NATULANG, "attributes-natural-language", "en-us"); sprintf(str, "http://%s:%d", printer_name, IPP_PORT); icp = add_option(icp, TAG_URI, "printer-uri", str); icp = add_option(icp, TAG_NAMEWOLANG, "requesting-user-name", jp->req.usernm); icp = add_option(icp, TAG_NAMEWOLANG, "job-name", jp->req.jobnm); if (jp->req.flags & PR_TEXT) { icp = add_option(icp, TAG_MIMETYPE, "document-format", "text/plain"); } else { icp = add_option(icp, TAG_MIMETYPE, "document-format", "application/postscript"); } *icp++ = TAG_END_OF_ATTR; ilen = icp - ibuf; /* * Set up the HTTP header. */ hcp = hbuf; sprintf(hcp, "POST /%s/ipp HTTP/1.1\r\n", printer_name); hcp += strlen(hcp); sprintf(hcp, "Content-Length: %ld\r\n", (long)sbuf.st_size + ilen); hcp += strlen(hcp); strcpy(hcp, "Content-Type: application/ipp\r\n"); hcp += strlen(hcp); sprintf(hcp, "Host: %s:%d\r\n", printer_name, IPP_PORT); hcp += strlen(hcp); *hcp++ = '\r'; *hcp++ = '\n'; hlen = hcp - hbuf; /* * Write the headers first. Then send the file. */ iov[0].iov_base = hbuf; iov[0].iov_len = hlen; iov[1].iov_base = ibuf; iov[1].iov_len = ilen; if ((nw = writev(sockfd, iov, 2)) != hlen + ilen) { log_ret("can't write to printer"); goto defer; } while ((nr = read(fd, buf, IOBUFSZ)) > 0) { if ((nw = write(sockfd, buf, nr)) != nr) { if (nw < 0) log_ret("can't write to printer"); else log_msg("short write (%d/%d) to printer", nw, nr); goto defer; } } if (nr < 0) { log_ret("can't read %s", name); goto defer; } /* * Read the response from the printer. */ if (printer_status(sockfd, jp)) { unlink(name); sprintf(name, "%s/%s/%ld", SPOOLDIR, REQDIR, jp->jobid); unlink(name); free(jp); jp = NULL; } defer: close(fd); if (sockfd >= 0) close(sockfd); if (jp != NULL) { replace_job(jp); sleep(60); } } } /* * Read data from the printer, possibly increasing the buffer. * Returns offset of end of data in buffer or -1 on failure. * * LOCKING: none. */ ssize_t readmore(int sockfd, char **bpp, int off, int *bszp) { ssize_t nr; char *bp = *bpp; int bsz = *bszp; if (off >= bsz) { bsz += IOBUFSZ; if ((bp = realloc(*bpp, bsz)) == NULL) log_sys("readmore: can't allocate bigger read buffer"); *bszp = bsz; *bpp = bp; } if ((nr = tread(sockfd, &bp[off], bsz-off, 1)) > 0) return(off+nr); else return(-1); } /* * Read and parse the response from the printer. Return 1 * if the request was successful, and 0 otherwise. * * LOCKING: none. */ int printer_status(int sockfd, struct job *jp) { int i, success, code, len, found, bufsz; long jobid; ssize_t nr; char *statcode, *reason, *cp, *contentlen; struct ipp_hdr *hp; char *bp; /* * Read the HTTP header followed by the IPP response header. * They can be returned in multiple read attempts. Use the * Content-Length specifier to determine how much to read. */ success = 0; bufsz = IOBUFSZ; if ((bp = malloc(IOBUFSZ)) == NULL) log_sys("printer_status: can't allocate read buffer"); while ((nr = tread(sockfd, bp, IOBUFSZ, 5)) > 0) { /* * Find the status. Response starts with "HTTP/x.y" * so we can skip the first 8 characters. */ cp = bp + 8; while (isspace((int)*cp)) cp++; statcode = cp; while (isdigit((int)*cp)) cp++; if (cp == statcode) { /* Bad format; log it and move on */ log_msg(bp); } else { *cp++ = '\0'; reason = cp; while (*cp != '\r' && *cp != '\n') cp++; *cp = '\0'; code = atoi(statcode); if (HTTP_INFO(code)) continue; if (!HTTP_SUCCESS(code)) { /* probable error: log it */ bp[nr] = '\0'; log_msg("error: %s", reason); break; } /* * The HTTP request was okay, but we still * need to check the IPP status. First * search for the Content-Length specifier. */ i = cp - bp; for (;;) { while (*cp != 'C' && *cp != 'c' && i < nr) { cp++; i++; } if (i >= nr && /* get more header */ ((nr = readmore(sockfd, &bp, i, &bufsz)) < 0)) goto out; cp = &bp[i]; if (strncasecmp(cp, "Content-Length:", 15) == 0) { cp += 15; while (isspace((int)*cp)) cp++; contentlen = cp; while (isdigit((int)*cp)) cp++; *cp++ = '\0'; i = cp - bp; len = atoi(contentlen); break; } else { cp++; i++; } } if (i >= nr && /* get more header */ ((nr = readmore(sockfd, &bp, i, &bufsz)) < 0)) goto out; cp = &bp[i]; found = 0; while (!found) { /* look for end of HTTP header */ while (i < nr - 2) { if (*cp == '\n' && *(cp + 1) == '\r' && *(cp + 2) == '\n') { found = 1; cp += 3; i += 3; break; } cp++; i++; } if (i >= nr && /* get more header */ ((nr = readmore(sockfd, &bp, i, &bufsz)) < 0)) goto out; cp = &bp[i]; } if (nr - i < len && /* get more header */ ((nr = readmore(sockfd, &bp, i, &bufsz)) < 0)) goto out; cp = &bp[i]; hp = (struct ipp_hdr *)cp; i = ntohs(hp->status); jobid = ntohl(hp->request_id); if (jobid != jp->jobid) { /* * Different jobs. Ignore it. */ log_msg("jobid %ld status code %d", jobid, i); break; } if (STATCLASS_OK(i)) success = 1; break; } } out: free(bp); if (nr < 0) { log_msg("jobid %ld: error reading printer response: %s", jobid, strerror(errno)); } return(success); }