1/*	$OpenBSD: frontend_lpr.c,v 1.4 2022/12/28 21:30:17 jmc Exp $	*/
2
3/*
4 * Copyright (c) 2017 Eric Faurot <eric@openbsd.org>
5 *
6 * Permission to use, copy, modify, and distribute this software for any
7 * purpose with or without fee is hereby granted, provided that the above
8 * copyright notice and this permission notice appear in all copies.
9 *
10 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17 */
18
19#include <sys/types.h>
20#include <sys/socket.h>
21#include <netinet/in.h>
22
23#include <ctype.h>
24#include <errno.h>
25#include <limits.h>
26#include <netdb.h>
27#include <stdarg.h>
28#include <stdlib.h>
29#include <stdio.h>
30#include <string.h>
31#include <unistd.h>
32
33#include "lpd.h"
34#include "lp.h"
35
36#include "io.h"
37#include "log.h"
38#include "proc.h"
39
40#define SERVER_TIMEOUT	30000
41#define CLIENT_TIMEOUT	 5000
42
43#define	MAXARG	50
44
45#define	F_ZOMBIE	0x1
46#define	F_WAITADDRINFO	0x2
47
48#define STATE_READ_COMMAND	0
49#define STATE_READ_FILE		1
50
51struct lpr_conn {
52	SPLAY_ENTRY(lpr_conn)	 entry;
53	uint32_t		 id;
54	char			 hostname[NI_MAXHOST];
55	struct io		*io;
56	int			 state;
57	int			 flags;
58	int			 recvjob;
59	int			 recvcf;
60	size_t			 expect;
61	FILE			*ofp;	/* output file when receiving data */
62	int			 ifd;	/* input file for displayq/rmjob */
63
64	char			*cmd;
65	int			 ai_done;
66	struct addrinfo		*ai;
67	struct io		*iofwd;
68};
69
70SPLAY_HEAD(lpr_conn_tree, lpr_conn);
71
72static int lpr_conn_cmp(struct lpr_conn *, struct lpr_conn *);
73SPLAY_PROTOTYPE(lpr_conn_tree, lpr_conn, entry, lpr_conn_cmp);
74
75static void lpr_on_allowedhost(struct lpr_conn *, const char *, const char *);
76static void lpr_on_recvjob(struct lpr_conn *, int);
77static void lpr_on_recvjob_file(struct lpr_conn *, int, size_t, int, int);
78static void lpr_on_request(struct lpr_conn *, int, const char *, const char *);
79static void lpr_on_getaddrinfo(void *, int, struct addrinfo *);
80
81static void lpr_io_dispatch(struct io *, int, void *);
82static int  lpr_readcommand(struct lpr_conn *);
83static int  lpr_readfile(struct lpr_conn *);
84static int  lpr_parsejobfilter(struct lpr_conn *, struct lp_jobfilter *,
85    int, char **);
86
87static void lpr_free(struct lpr_conn *);
88static void lpr_close(struct lpr_conn *);
89static void lpr_ack(struct lpr_conn *, char);
90static void lpr_reply(struct lpr_conn *, const char *);
91static void lpr_stream(struct lpr_conn *);
92static void lpr_forward(struct lpr_conn *);
93
94static void lpr_iofwd_dispatch(struct io *, int, void *);
95
96static struct lpr_conn_tree conns;
97
98void
99lpr_init(void)
100{
101	SPLAY_INIT(&conns);
102}
103
104void
105lpr_conn(uint32_t connid, struct listener *l, int sock,
106    const struct sockaddr *sa)
107{
108	struct lpr_conn *conn;
109
110	if ((conn = calloc(1, sizeof(*conn))) == NULL) {
111		log_warn("%s: calloc", __func__);
112		close(sock);
113		frontend_conn_closed(connid);
114		return;
115	}
116	conn->id = connid;
117	conn->ifd = -1;
118	conn->io = io_new();
119	if (conn->io == NULL) {
120		log_warn("%s: io_new", __func__);
121		free(conn);
122		close(sock);
123		frontend_conn_closed(connid);
124		return;
125	}
126	SPLAY_INSERT(lpr_conn_tree, &conns, conn);
127	io_set_callback(conn->io, lpr_io_dispatch, conn);
128	io_set_timeout(conn->io, CLIENT_TIMEOUT);
129	io_set_write(conn->io);
130	io_attach(conn->io, sock);
131
132	conn->state = STATE_READ_COMMAND;
133	m_create(p_engine, IMSG_LPR_ALLOWEDHOST, conn->id, 0, -1);
134	m_add_sockaddr(p_engine, sa);
135	m_close(p_engine);
136}
137
138void
139lpr_dispatch_engine(struct imsgproc *proc, struct imsg *imsg)
140{
141	struct lpr_conn *conn = NULL, key;
142	const char *hostname, *reject, *cmd;
143	size_t sz;
144	int ack, cf = 0;
145
146	key.id = imsg->hdr.peerid;
147	if (key.id) {
148		conn = SPLAY_FIND(lpr_conn_tree, &conns, &key);
149		if (conn == NULL) {
150			log_debug("%08x dead-session", key.id);
151			if (imsg->fd != -1)
152				close(imsg->fd);
153			return;
154		}
155	}
156
157	switch (imsg->hdr.type) {
158	case IMSG_LPR_ALLOWEDHOST:
159		m_get_string(proc, &hostname);
160		m_get_string(proc, &reject);
161		m_end(proc);
162		lpr_on_allowedhost(conn, hostname, reject);
163		break;
164
165	case IMSG_LPR_RECVJOB:
166		m_get_int(proc, &ack);
167		m_end(proc);
168		lpr_on_recvjob(conn, ack);
169		break;
170
171	case IMSG_LPR_RECVJOB_CF:
172		cf = 1;
173	case IMSG_LPR_RECVJOB_DF:
174		m_get_int(proc, &ack);
175		m_get_size(proc, &sz);
176		m_end(proc);
177		lpr_on_recvjob_file(conn, ack, sz, cf, imsg->fd);
178		break;
179
180	case IMSG_LPR_DISPLAYQ:
181	case IMSG_LPR_RMJOB:
182		m_get_string(proc, &hostname);
183		m_get_string(proc, &cmd);
184		m_end(proc);
185		lpr_on_request(conn, imsg->fd, hostname, cmd);
186		break;
187
188	default:
189		fatalx("%s: unexpected imsg %s", __func__,
190		    log_fmt_imsgtype(imsg->hdr.type));
191	}
192}
193
194static void
195lpr_on_allowedhost(struct lpr_conn *conn, const char *hostname,
196    const char *reject)
197{
198	strlcpy(conn->hostname, hostname, sizeof(conn->hostname));
199	if (reject)
200		lpr_reply(conn, reject);
201	else
202		io_set_read(conn->io);
203}
204
205static void
206lpr_on_recvjob(struct lpr_conn *conn, int ack)
207{
208	if (ack == LPR_ACK)
209		conn->recvjob = 1;
210	else
211		log_debug("%08x recvjob failed", conn->id);
212	lpr_ack(conn, ack);
213}
214
215static void
216lpr_on_recvjob_file(struct lpr_conn *conn, int ack, size_t sz, int cf, int fd)
217{
218	if (ack != LPR_ACK) {
219		lpr_ack(conn, ack);
220		return;
221	}
222
223	if (fd == -1) {
224		log_warnx("%s: failed to get fd", __func__);
225		lpr_ack(conn, LPR_NACK);
226		return;
227	}
228
229	conn->ofp = fdopen(fd, "w");
230	if (conn->ofp == NULL) {
231		log_warn("%s: fdopen", __func__);
232		close(fd);
233		lpr_ack(conn, LPR_NACK);
234		return;
235	}
236
237	conn->expect = sz;
238	if (cf)
239		conn->recvcf = cf;
240	conn->state = STATE_READ_FILE;
241
242	lpr_ack(conn, LPR_ACK);
243}
244
245static void
246lpr_on_request(struct lpr_conn *conn, int fd, const char *hostname,
247    const char *cmd)
248{
249	struct addrinfo hints;
250
251	if (fd == -1) {
252		log_warnx("%s: no fd received", __func__);
253		lpr_close(conn);
254		return;
255	}
256
257	log_debug("%08x stream init", conn->id);
258	conn->ifd = fd;
259
260	/* Prepare for command forwarding if necessary. */
261	if (cmd) {
262		log_debug("%08x forwarding to %s: \\%d%s", conn->id, hostname,
263		    cmd[0], cmd + 1);
264		conn->cmd = strdup(cmd);
265		if (conn->cmd == NULL)
266			log_warn("%s: strdup", __func__);
267		else {
268			memset(&hints, 0, sizeof(hints));
269			hints.ai_socktype = SOCK_STREAM;
270			conn->flags |= F_WAITADDRINFO;
271			/*
272			 * The callback might run immediately, so conn->ifd
273			 * must be set before, to block lpr_forward().
274			 */
275			resolver_getaddrinfo(hostname, "printer", &hints,
276			    lpr_on_getaddrinfo, conn);
277		}
278	}
279
280	lpr_stream(conn);
281}
282
283static void
284lpr_on_getaddrinfo(void *arg, int r, struct addrinfo *ai)
285{
286	struct lpr_conn *conn = arg;
287
288	conn->flags &= ~F_WAITADDRINFO;
289	if (conn->flags & F_ZOMBIE) {
290		if (ai)
291			freeaddrinfo(ai);
292		lpr_free(conn);
293	}
294	else {
295		conn->ai_done = 1;
296		conn->ai = ai;
297		lpr_forward(conn);
298	}
299}
300
301static void
302lpr_io_dispatch(struct io *io, int evt, void *arg)
303{
304	struct lpr_conn *conn = arg;
305	int r;
306
307	switch (evt) {
308	case IO_DATAIN:
309		switch(conn->state) {
310		case STATE_READ_COMMAND:
311			r = lpr_readcommand(conn);
312			break;
313		case STATE_READ_FILE:
314			r = lpr_readfile(conn);
315			break;
316		default:
317			fatal("%s: unexpected state %d", __func__, conn->state);
318		}
319
320		if (r == 0)
321			io_set_write(conn->io);
322		return;
323
324	case IO_LOWAT:
325		if (conn->recvjob)
326			io_set_read(conn->io);
327		else if (conn->ifd != -1)
328			lpr_stream(conn);
329		else if (conn->cmd == NULL)
330			lpr_close(conn);
331		return;
332
333	case IO_DISCONNECTED:
334		log_debug("%08x disconnected", conn->id);
335		/*
336		 * Some clients don't wait for the last acknowledgment to close
337		 * the session.  So just consider it is closed normally.
338		 */
339	case IO_CLOSED:
340		if (conn->recvcf && conn->state == STATE_READ_COMMAND) {
341			/*
342			 * Commit the transaction if we received a control file
343			 * and the last file was received correctly.
344			 */
345			m_compose(p_engine, IMSG_LPR_RECVJOB_COMMIT, conn->id,
346			    0, -1, NULL, 0);
347			conn->recvjob = 0;
348		}
349		break;
350
351	case IO_TIMEOUT:
352		log_debug("%08x timeout", conn->id);
353		break;
354
355	case IO_ERROR:
356		log_debug("%08x io-error", conn->id);
357		break;
358
359	default:
360		fatalx("%s: unexpected event %d", __func__, evt);
361	}
362
363	lpr_close(conn);
364}
365
366static int
367lpr_readcommand(struct lpr_conn *conn)
368{
369	struct lp_jobfilter jf;
370	size_t count;
371	const char *errstr;
372	char *argv[MAXARG], *line;
373	int i, argc, cmd;
374
375	line = io_getline(conn->io, NULL);
376	if (line == NULL) {
377		if (io_datalen(conn->io) >= LPR_MAXCMDLEN) {
378			lpr_reply(conn, "Request line too long");
379			return 0;
380		}
381		return -1;
382	}
383
384	cmd = line[0];
385	line++;
386
387	if (cmd == 0) {
388		lpr_reply(conn, "No command");
389		return 0;
390	}
391
392	log_debug("%08x cmd \\%d", conn->id, cmd);
393
394	/* Parse the command. */
395	for (argc = 0; argc < MAXARG; ) {
396		argv[argc] = strsep(&line, " \t");
397		if (argv[argc] == NULL)
398			break;
399		if (argv[argc][0] != '\0')
400			argc++;
401	}
402	if (argc == MAXARG) {
403		lpr_reply(conn, "Argument list too long");
404		return 0;
405	}
406
407	if (argc == 0) {
408		lpr_reply(conn, "No queue specified");
409		return 0;
410	}
411
412#define CMD(c)    ((int)(c))
413#define SUBCMD(c) (0x100 | (int)(c))
414
415	if (conn->recvjob)
416		cmd |= 0x100;
417	switch (cmd) {
418	case CMD('\1'):	/* PRINT <prn> */
419		m_create(p_engine, IMSG_LPR_PRINTJOB, 0, 0, -1);
420		m_add_string(p_engine, argv[0]);
421		m_close(p_engine);
422		lpr_ack(conn, LPR_ACK);
423		return 0;
424
425	case CMD('\2'):	/* RECEIVE JOB <prn> */
426		m_create(p_engine, IMSG_LPR_RECVJOB, conn->id, 0, -1);
427		m_add_string(p_engine, conn->hostname);
428		m_add_string(p_engine, argv[0]);
429		m_close(p_engine);
430		return 0;
431
432	case CMD('\3'):	/* QUEUE STATE SHORT <prn> [job#...] [user..] */
433	case CMD('\4'):	/* QUEUE STATE LONG  <prn> [job#...] [user..] */
434		if (lpr_parsejobfilter(conn, &jf, argc - 1, argv + 1) == -1)
435			return 0;
436
437		m_create(p_engine, IMSG_LPR_DISPLAYQ, conn->id, 0, -1);
438		m_add_int(p_engine, (cmd == '\3') ? 0 : 1);
439		m_add_string(p_engine, conn->hostname);
440		m_add_string(p_engine, argv[0]);
441		m_add_int(p_engine, jf.njob);
442		for (i = 0; i < jf.njob; i++)
443			m_add_int(p_engine, jf.jobs[i]);
444		m_add_int(p_engine, jf.nuser);
445		for (i = 0; i < jf.nuser; i++)
446			m_add_string(p_engine, jf.users[i]);
447		m_close(p_engine);
448		return 0;
449
450	case CMD('\5'):	/* REMOVE JOBS <prn> <agent> [job#...] [user..] */
451		if (argc < 2) {
452			lpr_reply(conn, "No agent specified");
453			return 0;
454		}
455		if (lpr_parsejobfilter(conn, &jf, argc - 2, argv + 2) == -1)
456			return 0;
457
458		m_create(p_engine, IMSG_LPR_RMJOB, conn->id, 0, -1);
459		m_add_string(p_engine, conn->hostname);
460		m_add_string(p_engine, argv[0]);
461		m_add_string(p_engine, argv[1]);
462		m_add_int(p_engine, jf.njob);
463		for (i = 0; i < jf.njob; i++)
464			m_add_int(p_engine, jf.jobs[i]);
465		m_add_int(p_engine, jf.nuser);
466		for (i = 0; i < jf.nuser; i++)
467			m_add_string(p_engine, jf.users[i]);
468		m_close(p_engine);
469		return 0;
470
471	case SUBCMD('\1'):	/* ABORT */
472		m_compose(p_engine, IMSG_LPR_RECVJOB_CLEAR, conn->id, 0, -1,
473		    NULL, 0);
474		conn->recvcf = 0;
475		lpr_ack(conn, LPR_ACK);
476		return 0;
477
478	case SUBCMD('\2'):	/* CONTROL FILE <size> <filename> */
479	case SUBCMD('\3'):	/* DATA FILE    <size> <filename> */
480		if (argc != 2) {
481			log_debug("%08x invalid number of argument", conn->id);
482			lpr_ack(conn, LPR_NACK);
483			return 0;
484		}
485		errstr = NULL;
486		count = strtonum(argv[0], 1, LPR_MAXFILESIZE, &errstr);
487		if (errstr) {
488			log_debug("%08x invalid file size: %s", conn->id,
489			    strerror(errno));
490			lpr_ack(conn, LPR_NACK);
491			return 0;
492		}
493
494		if (cmd == SUBCMD('\2')) {
495			if (conn->recvcf) {
496				log_debug("%08x cf file already received",
497				    conn->id);
498				lpr_ack(conn, LPR_NACK);
499				return 0;
500			}
501			m_create(p_engine, IMSG_LPR_RECVJOB_CF, conn->id, 0,
502			    -1);
503		}
504		else
505			m_create(p_engine, IMSG_LPR_RECVJOB_DF, conn->id, 0,
506			    -1);
507		m_add_size(p_engine, count);
508		m_add_string(p_engine, argv[1]);
509		m_close(p_engine);
510		return 0;
511
512	default:
513		if (conn->recvjob)
514			lpr_reply(conn, "Protocol error");
515		else
516			lpr_reply(conn, "Illegal service request");
517		return 0;
518	}
519}
520
521static int
522lpr_readfile(struct lpr_conn *conn)
523{
524	size_t len, w;
525	char *data;
526
527	if (conn->expect) {
528		/* Read file content. */
529		data = io_data(conn->io);
530		len = io_datalen(conn->io);
531		if (len > conn->expect)
532			len = conn->expect;
533
534		log_debug("%08x %zu bytes received", conn->id, len);
535
536		w = fwrite(data, 1, len, conn->ofp);
537		if (w != len) {
538			log_warnx("%s: fwrite", __func__);
539			lpr_close(conn);
540			return -1;
541		}
542		io_drop(conn->io, w);
543		conn->expect -= w;
544		if (conn->expect)
545			return -1;
546
547		fclose(conn->ofp);
548		conn->ofp = NULL;
549
550		log_debug("%08x file received", conn->id);
551	}
552
553	/* Try to read '\0'. */
554	len = io_datalen(conn->io);
555	if (len == 0)
556		return -1;
557	data = io_data(conn->io);
558	io_drop(conn->io, 1);
559
560	log_debug("%08x eof %d", conn->id, (int)*data);
561
562	if (*data != '\0') {
563		lpr_close(conn);
564		return -1;
565	}
566
567	conn->state = STATE_READ_COMMAND;
568	lpr_ack(conn, LPR_ACK);
569	return 0;
570}
571
572static int
573lpr_parsejobfilter(struct lpr_conn *conn, struct lp_jobfilter *jf, int argc,
574    char **argv)
575{
576	const char *errstr;
577	char *arg;
578	int i, jobnum;
579
580	memset(jf, 0, sizeof(*jf));
581
582	for (i = 0; i < argc; i++) {
583		arg = argv[i];
584		if (isdigit((unsigned char)arg[0])) {
585			if (jf->njob == LP_MAXREQUESTS) {
586				lpr_reply(conn, "Too many requests");
587				return -1;
588			}
589			errstr = NULL;
590			jobnum = strtonum(arg, 0, INT_MAX, &errstr);
591			if (errstr) {
592				lpr_reply(conn, "Invalid job number");
593				return -1;
594			}
595			jf->jobs[jf->njob++] = jobnum;
596		}
597		else {
598			if (jf->nuser == LP_MAXUSERS) {
599				lpr_reply(conn, "Too many users");
600				return -1;
601			}
602			jf->users[jf->nuser++] = arg;
603		}
604	}
605
606	return 0;
607}
608
609static void
610lpr_free(struct lpr_conn *conn)
611{
612	if ((conn->flags & F_WAITADDRINFO) == 0)
613		free(conn);
614}
615
616static void
617lpr_close(struct lpr_conn *conn)
618{
619	uint32_t connid = conn->id;
620
621	SPLAY_REMOVE(lpr_conn_tree, &conns, conn);
622
623	if (conn->recvjob)
624		m_compose(p_engine, IMSG_LPR_RECVJOB_ROLLBACK, conn->id, 0, -1,
625		    NULL, 0);
626
627	io_free(conn->io);
628	free(conn->cmd);
629	if (conn->ofp)
630		fclose(conn->ofp);
631	if (conn->ifd != -1)
632		close(conn->ifd);
633	if (conn->ai)
634		freeaddrinfo(conn->ai);
635	if (conn->iofwd)
636		io_free(conn->iofwd);
637
638	conn->flags |= F_ZOMBIE;
639	lpr_free(conn);
640
641	frontend_conn_closed(connid);
642}
643
644static void
645lpr_ack(struct lpr_conn *conn, char c)
646{
647	if (c == 0)
648		log_debug("%08x ack", conn->id);
649	else
650		log_debug("%08x nack %d", conn->id, (int)c);
651
652	io_write(conn->io, &c, 1);
653}
654
655static void
656lpr_reply(struct lpr_conn *conn, const char *s)
657{
658	log_debug("%08x reply: %s", conn->id, s);
659
660	io_printf(conn->io, "%s\n", s);
661}
662
663/*
664 * Stream response file to the client.
665 */
666static void
667lpr_stream(struct lpr_conn *conn)
668{
669	char buf[BUFSIZ];
670	ssize_t r;
671
672	for (;;) {
673		if (io_queued(conn->io) > 65536)
674			return;
675
676		r = read(conn->ifd, buf, sizeof(buf));
677		if (r == -1) {
678			if (errno == EINTR)
679				continue;
680			log_warn("%s: read", __func__);
681			break;
682		}
683
684		if (r == 0) {
685			log_debug("%08x stream done", conn->id);
686			break;
687		}
688		log_debug("%08x stream %zu bytes", conn->id, r);
689
690		if (io_write(conn->io, buf, r) == -1) {
691			log_warn("%s: io_write", __func__);
692			break;
693		}
694	}
695
696	close(conn->ifd);
697	conn->ifd = -1;
698
699	if (conn->cmd)
700		lpr_forward(conn);
701
702	else if (io_queued(conn->io) == 0)
703		lpr_close(conn);
704}
705
706/*
707 * Forward request to the remote printer.
708 */
709static void
710lpr_forward(struct lpr_conn *conn)
711{
712	/*
713	 * Do not start forwarding the command if the address is not resolved
714	 * or if the local response is still being sent to the client.
715	 */
716	if (!conn->ai_done || conn->ifd == -1)
717		return;
718
719	if (conn->ai == NULL) {
720		if (io_queued(conn->io) == 0)
721			lpr_close(conn);
722		return;
723	}
724
725	log_debug("%08x forward start", conn->id);
726
727	conn->iofwd = io_new();
728	if (conn->iofwd == NULL) {
729		log_warn("%s: io_new", __func__);
730		if (io_queued(conn->io) == 0)
731			lpr_close(conn);
732		return;
733	}
734	io_set_callback(conn->iofwd, lpr_iofwd_dispatch, conn);
735	io_set_timeout(conn->io, SERVER_TIMEOUT);
736	io_connect(conn->iofwd, conn->ai);
737	conn->ai = NULL;
738}
739
740static void
741lpr_iofwd_dispatch(struct io *io, int evt, void *arg)
742{
743	struct lpr_conn *conn = arg;
744
745	switch (evt) {
746	case IO_CONNECTED:
747		log_debug("%08x forward connected", conn->id);
748		/* Send the request. */
749		io_print(io, conn->cmd);
750		io_print(io, "\n");
751		io_set_write(io);
752		return;
753
754	case IO_DATAIN:
755		/* Relay. */
756		io_write(conn->io, io_data(io), io_datalen(io));
757		io_drop(io, io_datalen(io));
758		return;
759
760	case IO_LOWAT:
761		/* Read response. */
762		io_set_read(io);
763		return;
764
765	case IO_CLOSED:
766		break;
767
768	case IO_DISCONNECTED:
769		log_debug("%08x forward disconnected", conn->id);
770		break;
771
772	case IO_TIMEOUT:
773		log_debug("%08x forward timeout", conn->id);
774		break;
775
776	case IO_ERROR:
777		log_debug("%08x forward io-error", conn->id);
778		break;
779
780	default:
781		fatalx("%s: unexpected event %d", __func__, evt);
782	}
783
784	log_debug("%08x forward done", conn->id);
785
786	io_free(io);
787	free(conn->cmd);
788	conn->cmd = NULL;
789	conn->iofwd = NULL;
790	if (io_queued(conn->io) == 0)
791		lpr_close(conn);
792}
793
794static int
795lpr_conn_cmp(struct lpr_conn *a, struct lpr_conn *b)
796{
797	if (a->id < b->id)
798		return -1;
799	if (a->id > b->id)
800		return 1;
801	return 0;
802}
803
804SPLAY_GENERATE(lpr_conn_tree, lpr_conn, entry, lpr_conn_cmp);
805