]> git.ipfire.org Git - thirdparty/cups.git/blob - scheduler/cups-lpd.c
Import CUPS 1.4svn-r7226.
[thirdparty/cups.git] / scheduler / cups-lpd.c
1 /*
2 * "$Id: cups-lpd.c 6781 2007-08-08 21:09:31Z mike $"
3 *
4 * Line Printer Daemon interface for the Common UNIX Printing System (CUPS).
5 *
6 * Copyright 2007-2008 by Apple Inc.
7 * Copyright 1997-2006 by Easy Software Products, all rights reserved.
8 *
9 * These coded instructions, statements, and computer programs are the
10 * property of Apple Inc. and are protected by Federal copyright
11 * law. Distribution and use rights are outlined in the file "LICENSE.txt"
12 * which should have been included with this file. If this file is
13 * file is missing or damaged, see the license at "http://www.cups.org/".
14 *
15 * Contents:
16 *
17 * main() - Process an incoming LPD request...
18 * create_job() - Create a new print job.
19 * get_printer() - Get the named printer and its options.
20 * print_file() - Add a file to the current job.
21 * recv_print_job() - Receive a print job from the client.
22 * remove_jobs() - Cancel one or more jobs.
23 * send_state() - Send the queue state.
24 * smart_gets() - Get a line of text, removing the trailing CR and/or LF.
25 */
26
27 /*
28 * Include necessary headers...
29 */
30
31 #include <cups/http-private.h>
32 #include <cups/cups.h>
33 #include <cups/string.h>
34 #include <cups/language.h>
35 #include <stdlib.h>
36 #include <errno.h>
37 #include <syslog.h>
38 #include <ctype.h>
39 #include <unistd.h>
40 #include <fcntl.h>
41
42 #include <sys/types.h>
43 #include <sys/socket.h>
44 #include <netinet/in.h>
45 #include <netdb.h>
46
47 #ifdef HAVE_INTTYPES_H
48 # include <inttypes.h>
49 #endif /* HAVE_INTTYPES_H */
50
51 #ifdef HAVE_COREFOUNDATION_H
52 # include <CoreFoundation/CoreFoundation.h>
53 #endif /* HAVE_COREFOUNDATION_H */
54 #ifdef HAVE_CFPRIV_H
55 # include <CoreFoundation/CFPriv.h>
56 #endif /* HAVE_CFPRIV_H */
57
58
59 /*
60 * LPD "mini-daemon" for CUPS. This program must be used in conjunction
61 * with inetd or another similar program that monitors ports and starts
62 * daemons for each client connection. A typical configuration is:
63 *
64 * printer stream tcp nowait lp /usr/lib/cups/daemon/cups-lpd cups-lpd
65 *
66 * This daemon implements most of RFC 1179 (the unofficial LPD specification)
67 * except for:
68 *
69 * - This daemon does not check to make sure that the source port is
70 * between 721 and 731, since it isn't necessary for proper
71 * functioning and port-based security is no security at all!
72 *
73 * - The "Print any waiting jobs" command is a no-op.
74 *
75 * The LPD-to-IPP mapping is as defined in RFC 2569. The report formats
76 * currently match the Solaris LPD mini-daemon.
77 */
78
79 /*
80 * Prototypes...
81 */
82
83 static int create_job(http_t *http, const char *dest, const char *title,
84 const char *user, int num_options,
85 cups_option_t *options);
86 static int get_printer(http_t *http, const char *name, char *dest,
87 int destsize, cups_option_t **options,
88 int *accepting, int *shared, ipp_pstate_t *state);
89 static int print_file(http_t *http, int id, const char *filename,
90 const char *docname, const char *user,
91 const char *format, int last);
92 static int recv_print_job(const char *name, int num_defaults,
93 cups_option_t *defaults);
94 static int remove_jobs(const char *name, const char *agent,
95 const char *list);
96 static int send_state(const char *name, const char *list,
97 int longstatus);
98 static char *smart_gets(char *s, int len, FILE *fp);
99
100
101 /*
102 * 'main()' - Process an incoming LPD request...
103 */
104
105 int /* O - Exit status */
106 main(int argc, /* I - Number of command-line arguments */
107 char *argv[]) /* I - Command-line arguments */
108 {
109 int i; /* Looping var */
110 int num_defaults; /* Number of default options */
111 cups_option_t *defaults; /* Default options */
112 char line[256], /* Command string */
113 command, /* Command code */
114 *dest, /* Pointer to destination */
115 *list, /* Pointer to list */
116 *agent, /* Pointer to user */
117 status; /* Status for client */
118 socklen_t hostlen; /* Size of client address */
119 http_addr_t hostaddr; /* Address of client */
120 char hostname[256], /* Name of client */
121 hostip[256], /* IP address */
122 *hostfamily; /* Address family */
123 int hostlookups; /* Do hostname lookups? */
124
125
126 /*
127 * Don't buffer the output...
128 */
129
130 setbuf(stdout, NULL);
131
132 /*
133 * Log things using the "cups-lpd" name...
134 */
135
136 openlog("cups-lpd", LOG_PID, LOG_LPR);
137
138 /*
139 * Scan the command-line for options...
140 */
141
142 num_defaults = 0;
143 defaults = NULL;
144 hostlookups = 1;
145
146 for (i = 1; i < argc; i ++)
147 if (argv[i][0] == '-')
148 {
149 switch (argv[i][1])
150 {
151 case 'o' : /* Option */
152 if (argv[i][2])
153 num_defaults = cupsParseOptions(argv[i] + 2, num_defaults,
154 &defaults);
155 else
156 {
157 i ++;
158 if (i < argc)
159 num_defaults = cupsParseOptions(argv[i], num_defaults,
160 &defaults);
161 else
162 syslog(LOG_WARNING, "Expected option string after -o option!");
163 }
164 break;
165
166 case 'n' : /* Don't do hostname lookups */
167 hostlookups = 0;
168 break;
169
170 default :
171 syslog(LOG_WARNING, "Unknown option \"%c\" ignored!", argv[i][1]);
172 break;
173 }
174 }
175 else
176 syslog(LOG_WARNING, "Unknown command-line option \"%s\" ignored!",
177 argv[i]);
178
179 /*
180 * Get the address of the client...
181 */
182
183 hostlen = sizeof(hostaddr);
184
185 if (getpeername(0, (struct sockaddr *)&hostaddr, &hostlen))
186 {
187 syslog(LOG_WARNING, "Unable to get client address - %s", strerror(errno));
188 strcpy(hostname, "unknown");
189 }
190 else
191 {
192 httpAddrString(&hostaddr, hostip, sizeof(hostip));
193
194 if (hostlookups)
195 httpAddrLookup(&hostaddr, hostname, sizeof(hostname));
196 else
197 strlcpy(hostname, hostip, sizeof(hostname));
198
199 #ifdef AF_INET6
200 if (hostaddr.addr.sa_family == AF_INET6)
201 hostfamily = "IPv6";
202 else
203 #endif /* AF_INET6 */
204 hostfamily = "IPv4";
205
206 syslog(LOG_INFO, "Connection from %s (%s %s)", hostname, hostfamily,
207 hostip);
208 }
209
210 num_defaults = cupsAddOption("job-originating-host-name", hostname,
211 num_defaults, &defaults);
212
213 /*
214 * RFC1179 specifies that only 1 daemon command can be received for
215 * every connection.
216 */
217
218 if (smart_gets(line, sizeof(line), stdin) == NULL)
219 {
220 /*
221 * Unable to get command from client! Send an error status and return.
222 */
223
224 syslog(LOG_ERR, "Unable to get command line from client!");
225 putchar(1);
226 return (1);
227 }
228
229 /*
230 * The first byte is the command byte. After that will be the queue name,
231 * resource list, and/or user name.
232 */
233
234 command = line[0];
235 dest = line + 1;
236
237 if (command == 0x02)
238 list = NULL;
239 else
240 {
241 for (list = dest + 1; *list && !isspace(*list & 255); list ++);
242
243 while (isspace(*list & 255))
244 *list++ = '\0';
245 }
246
247 /*
248 * Do the command...
249 */
250
251 switch (command)
252 {
253 default : /* Unknown command */
254 syslog(LOG_ERR, "Unknown LPD command 0x%02X!", command);
255 syslog(LOG_ERR, "Command line = %s", line + 1);
256 putchar(1);
257
258 status = 1;
259 break;
260
261 case 0x01 : /* Print any waiting jobs */
262 syslog(LOG_INFO, "Print waiting jobs (no-op)");
263 putchar(0);
264
265 status = 0;
266 break;
267
268 case 0x02 : /* Receive a printer job */
269 syslog(LOG_INFO, "Receive print job for %s", dest);
270 /* recv_print_job() sends initial status byte */
271
272 status = recv_print_job(dest, num_defaults, defaults);
273 break;
274
275 case 0x03 : /* Send queue state (short) */
276 syslog(LOG_INFO, "Send queue state (short) for %s %s", dest, list);
277 /* no status byte for this command */
278
279 status = send_state(dest, list, 0);
280 break;
281
282 case 0x04 : /* Send queue state (long) */
283 syslog(LOG_INFO, "Send queue state (long) for %s %s", dest, list);
284 /* no status byte for this command */
285
286 status = send_state(dest, list, 1);
287 break;
288
289 case 0x05 : /* Remove jobs */
290 /*
291 * Grab the agent and skip to the list of users and/or jobs.
292 */
293
294 agent = list;
295
296 for (; *list && !isspace(*list & 255); list ++);
297 while (isspace(*list & 255))
298 *list++ = '\0';
299
300 syslog(LOG_INFO, "Remove jobs %s on %s by %s", list, dest, agent);
301
302 status = remove_jobs(dest, agent, list);
303
304 putchar(status);
305 break;
306 }
307
308 syslog(LOG_INFO, "Closing connection");
309 closelog();
310
311 return (status);
312 }
313
314
315 /*
316 * 'create_job()' - Create a new print job.
317 */
318
319 static int /* O - Job ID or -1 on error */
320 create_job(http_t *http, /* I - HTTP connection */
321 const char *dest, /* I - Destination name */
322 const char *title, /* I - job-name */
323 const char *user, /* I - requesting-user-name */
324 int num_options, /* I - Number of options for job */
325 cups_option_t *options) /* I - Options for job */
326 {
327 ipp_t *request; /* IPP request */
328 ipp_t *response; /* IPP response */
329 ipp_attribute_t *attr; /* IPP attribute */
330 char uri[HTTP_MAX_URI]; /* Printer URI */
331 int id; /* Job ID */
332
333
334 /*
335 * Setup the Create-Job request...
336 */
337
338 request = ippNewRequest(IPP_CREATE_JOB);
339
340 httpAssembleURIf(HTTP_URI_CODING_ALL, uri, sizeof(uri), "ipp", NULL,
341 "localhost", 0, "/printers/%s", dest);
342
343 ippAddString(request, IPP_TAG_OPERATION, IPP_TAG_URI, "printer-uri",
344 NULL, uri);
345
346 ippAddString(request, IPP_TAG_OPERATION, IPP_TAG_NAME,
347 "requesting-user-name", NULL, user);
348
349 if (title)
350 ippAddString(request, IPP_TAG_OPERATION, IPP_TAG_NAME, "job-name",
351 NULL, title);
352
353 cupsEncodeOptions(request, num_options, options);
354
355 /*
356 * Do the request...
357 */
358
359 snprintf(uri, sizeof(uri), "/printers/%s", dest);
360
361 response = cupsDoRequest(http, request, uri);
362
363 if (!response || cupsLastError() > IPP_OK_CONFLICT)
364 {
365 syslog(LOG_ERR, "Unable to create job - %s", cupsLastErrorString());
366
367 ippDelete(response);
368
369 return (-1);
370 }
371
372 /*
373 * Get the job-id value from the response and return it...
374 */
375
376 if ((attr = ippFindAttribute(response, "job-id", IPP_TAG_INTEGER)) == NULL)
377 {
378 id = -1;
379
380 syslog(LOG_ERR, "No job-id attribute found in response from server!");
381 }
382 else
383 {
384 id = attr->values[0].integer;
385
386 syslog(LOG_INFO, "Print file - job ID = %d", id);
387 }
388
389 ippDelete(response);
390
391 return (id);
392 }
393
394
395 /*
396 * 'get_printer()' - Get the named printer and its options.
397 */
398
399 static int /* O - Number of options or -1 on error */
400 get_printer(http_t *http, /* I - HTTP connection */
401 const char *name, /* I - Printer name from request */
402 char *dest, /* I - Destination buffer */
403 int destsize, /* I - Size of destination buffer */
404 cups_option_t **options, /* O - Printer options */
405 int *accepting, /* O - printer-is-accepting-jobs value */
406 int *shared, /* O - printer-is-shared value */
407 ipp_pstate_t *state) /* O - printer-state value */
408 {
409 int num_options; /* Number of options */
410 cups_file_t *fp; /* lpoptions file */
411 char line[1024], /* Line from lpoptions file */
412 *value, /* Pointer to value on line */
413 *optptr; /* Pointer to options on line */
414 int linenum; /* Line number in file */
415 const char *cups_serverroot; /* CUPS_SERVERROOT env var */
416 ipp_t *request; /* IPP request */
417 ipp_t *response; /* IPP response */
418 ipp_attribute_t *attr; /* IPP attribute */
419 char uri[HTTP_MAX_URI]; /* Printer URI */
420 static const char * const requested[] =
421 { /* Requested attributes */
422 "printer-info",
423 "printer-is-accepting-jobs",
424 "printer-is-shared",
425 "printer-name",
426 "printer-state"
427 };
428
429
430 /*
431 * Initialize everything...
432 */
433
434 if (accepting)
435 *accepting = 0;
436 if (shared)
437 *shared = 0;
438 if (state)
439 *state = IPP_PRINTER_STOPPED;
440 if (options)
441 *options = NULL;
442
443 /*
444 * See if the name is a queue name optionally with an instance name.
445 */
446
447 strlcpy(dest, name, destsize);
448 if ((value = strchr(dest, '/')) != NULL)
449 *value = '\0';
450
451 /*
452 * Setup the Get-Printer-Attributes request...
453 */
454
455 request = ippNewRequest(IPP_GET_PRINTER_ATTRIBUTES);
456
457 httpAssembleURIf(HTTP_URI_CODING_ALL, uri, sizeof(uri), "ipp", NULL,
458 "localhost", 0, "/printers/%s", dest);
459
460 ippAddString(request, IPP_TAG_OPERATION, IPP_TAG_URI, "printer-uri",
461 NULL, uri);
462
463 ippAddStrings(request, IPP_TAG_OPERATION, IPP_TAG_KEYWORD,
464 "requested-attributes",
465 (int)(sizeof(requested) / sizeof(requested[0])),
466 NULL, requested);
467
468 /*
469 * Do the request...
470 */
471
472 response = cupsDoRequest(http, request, "/");
473
474 if (!response || cupsLastError() > IPP_OK_CONFLICT)
475 {
476 /*
477 * If we can't find the printer by name, look up the printer-name
478 * using the printer-info values...
479 */
480
481 ipp_attribute_t *accepting_attr,/* printer-is-accepting-jobs */
482 *info_attr, /* printer-info */
483 *name_attr, /* printer-name */
484 *shared_attr, /* printer-is-shared */
485 *state_attr; /* printer-state */
486
487
488 ippDelete(response);
489
490 /*
491 * Setup the CUPS-Get-Printers request...
492 */
493
494 request = ippNewRequest(CUPS_GET_PRINTERS);
495
496 ippAddStrings(request, IPP_TAG_OPERATION, IPP_TAG_KEYWORD,
497 "requested-attributes",
498 (int)(sizeof(requested) / sizeof(requested[0])),
499 NULL, requested);
500
501 /*
502 * Do the request...
503 */
504
505 response = cupsDoRequest(http, request, "/");
506
507 if (!response || cupsLastError() > IPP_OK_CONFLICT)
508 {
509 syslog(LOG_ERR, "Unable to get list of printers - %s",
510 cupsLastErrorString());
511
512 ippDelete(response);
513
514 return (-1);
515 }
516
517 /*
518 * Scan the response for printers...
519 */
520
521 *dest = '\0';
522 attr = response->attrs;
523
524 while (attr)
525 {
526 /*
527 * Skip to the next printer...
528 */
529
530 while (attr && attr->group_tag != IPP_TAG_PRINTER)
531 attr = attr->next;
532
533 if (!attr)
534 break;
535
536 /*
537 * Get all of the attributes for the current printer...
538 */
539
540 accepting_attr = NULL;
541 info_attr = NULL;
542 name_attr = NULL;
543 shared_attr = NULL;
544 state_attr = NULL;
545
546 while (attr && attr->group_tag == IPP_TAG_PRINTER)
547 {
548 if (!strcmp(attr->name, "printer-is-accepting-jobs") &&
549 attr->value_tag == IPP_TAG_BOOLEAN)
550 accepting_attr = attr;
551 else if (!strcmp(attr->name, "printer-info") &&
552 attr->value_tag == IPP_TAG_TEXT)
553 info_attr = attr;
554 else if (!strcmp(attr->name, "printer-name") &&
555 attr->value_tag == IPP_TAG_NAME)
556 name_attr = attr;
557 else if (!strcmp(attr->name, "printer-is-shared") &&
558 attr->value_tag == IPP_TAG_BOOLEAN)
559 shared_attr = attr;
560 else if (!strcmp(attr->name, "printer-state") &&
561 attr->value_tag == IPP_TAG_ENUM)
562 state_attr = attr;
563
564 attr = attr->next;
565 }
566
567 if (info_attr && name_attr &&
568 !strcasecmp(name, info_attr->values[0].string.text))
569 {
570 /*
571 * Found a match, use this one!
572 */
573
574 strlcpy(dest, name_attr->values[0].string.text, destsize);
575
576 if (accepting && accepting_attr)
577 *accepting = accepting_attr->values[0].boolean;
578
579 if (shared && shared_attr)
580 *shared = shared_attr->values[0].boolean;
581
582 if (state && state_attr)
583 *state = (ipp_pstate_t)state_attr->values[0].integer;
584
585 break;
586 }
587 }
588
589 ippDelete(response);
590
591 if (!*dest)
592 {
593 syslog(LOG_ERR, "Unable to find \"%s\" in list of printers!", name);
594
595 return (-1);
596 }
597
598 name = dest;
599 }
600 else
601 {
602 /*
603 * Get values from the response...
604 */
605
606 if (accepting)
607 {
608 if ((attr = ippFindAttribute(response, "printer-is-accepting-jobs",
609 IPP_TAG_BOOLEAN)) == NULL)
610 syslog(LOG_ERR, "No printer-is-accepting-jobs attribute found in "
611 "response from server!");
612 else
613 *accepting = attr->values[0].boolean;
614 }
615
616 if (shared)
617 {
618 if ((attr = ippFindAttribute(response, "printer-is-shared",
619 IPP_TAG_BOOLEAN)) == NULL)
620 {
621 syslog(LOG_ERR, "No printer-is-shared attribute found in "
622 "response from server!");
623 *shared = 1;
624 }
625 else
626 *shared = attr->values[0].boolean;
627 }
628
629 if (state)
630 {
631 if ((attr = ippFindAttribute(response, "printer-state",
632 IPP_TAG_ENUM)) == NULL)
633 syslog(LOG_ERR, "No printer-state attribute found in "
634 "response from server!");
635 else
636 *state = (ipp_pstate_t)attr->values[0].integer;
637 }
638
639 ippDelete(response);
640 }
641
642 /*
643 * Override shared value for LPD using system-specific APIs...
644 */
645
646 #ifdef HAVE_CFPRIV_H /* MacOS X */
647 if (shared && *shared)
648 {
649 CFURLRef prefsurl; /* URL for preferences file */
650 CFDataRef xmldata; /* XML data from preferences file */
651 CFPropertyListRef plist; /* Property list from XML data */
652 CFStringRef queueid; /* CFString of destination name */
653 CFArrayRef lprqarray; /* Array of shared "LPR" printers */
654 CFBooleanRef serverflag; /* State of the print service */
655 static const char printerprefsfile[] =
656 "/Library/Preferences/com.apple.printservice.plist";
657 /* Preferences file */
658
659
660 /*
661 * See if we are running on MacOS X Server...
662 */
663
664 CFDictionaryRef versdict = _CFCopyServerVersionDictionary();
665
666 if (versdict)
667 {
668 /*
669 * Yes, use the LPR sharing preference...
670 */
671
672 CFRelease(versdict);
673
674 *shared = 0;
675
676 prefsurl = CFURLCreateFromFileSystemRepresentation(
677 kCFAllocatorDefault,
678 (const UInt8 *)printerprefsfile,
679 (CFIndex)strlen(printerprefsfile),
680 false);
681 if (prefsurl)
682 {
683 if (CFURLCreateDataAndPropertiesFromResource(kCFAllocatorDefault,
684 prefsurl, &xmldata, NULL,
685 NULL, NULL))
686 {
687 plist = CFPropertyListCreateFromXMLData(kCFAllocatorDefault, xmldata,
688 kCFPropertyListImmutable,
689 NULL);
690 if (plist)
691 {
692 serverflag = (CFBooleanRef)CFDictionaryGetValue(
693 (CFDictionaryRef)plist,
694 CFSTR("serviceState"));
695
696 if (serverflag && CFBooleanGetValue(serverflag))
697 {
698 lprqarray = (CFArrayRef)CFDictionaryGetValue(
699 (CFDictionaryRef)plist,
700 CFSTR("lprSharedQueues"));
701
702 if (lprqarray)
703 {
704 queueid = CFStringCreateWithCString(CFAllocatorGetDefault(),
705 dest,
706 kCFStringEncodingUTF8);
707
708 if (queueid)
709 {
710 *shared = CFArrayContainsValue(lprqarray,
711 CFRangeMake(0,
712 CFArrayGetCount(lprqarray)),
713 queueid);
714
715 CFRelease(queueid);
716 }
717 }
718 }
719
720 CFRelease(plist);
721 }
722
723 CFRelease(xmldata);
724 }
725
726 CFRelease(prefsurl);
727 }
728
729 if (!shared)
730 syslog(LOG_ERR, "Warning - Print Service sharing disabled for LPD "
731 "on queue: %s", name);
732 }
733 }
734 #endif /* HAVE_CFPRIV_H */
735
736 /*
737 * Next look for the printer in the lpoptions file...
738 */
739
740 num_options = 0;
741
742 if (options && shared && accepting)
743 {
744 if ((cups_serverroot = getenv("CUPS_SERVERROOT")) == NULL)
745 cups_serverroot = CUPS_SERVERROOT;
746
747 snprintf(line, sizeof(line), "%s/lpoptions", cups_serverroot);
748 if ((fp = cupsFileOpen(line, "r")) != NULL)
749 {
750 linenum = 0;
751 while (cupsFileGetConf(fp, line, sizeof(line), &value, &linenum))
752 {
753 /*
754 * Make sure we have "Dest name options" or "Default name options"...
755 */
756
757 if ((strcasecmp(line, "Dest") && strcasecmp(line, "Default")) || !value)
758 continue;
759
760 /*
761 * Separate destination name from options...
762 */
763
764 for (optptr = value; *optptr && !isspace(*optptr & 255); optptr ++);
765
766 while (*optptr == ' ')
767 *optptr++ = '\0';
768
769 /*
770 * If this is our destination, parse the options and break out of
771 * the loop - we're done!
772 */
773
774 if (!strcasecmp(value, name))
775 {
776 num_options = cupsParseOptions(optptr, num_options, options);
777 break;
778 }
779 }
780
781 cupsFileClose(fp);
782 }
783 }
784 else if (options)
785 *options = NULL;
786
787 /*
788 * Return the number of options for this destination...
789 */
790
791 return (num_options);
792 }
793
794
795 /*
796 * 'print_file()' - Add a file to the current job.
797 */
798
799 static int /* O - 0 on success, -1 on failure */
800 print_file(http_t *http, /* I - HTTP connection */
801 int id, /* I - Job ID */
802 const char *filename, /* I - File to print */
803 const char *docname, /* I - document-name */
804 const char *user, /* I - requesting-user-name */
805 const char *format, /* I - document-format */
806 int last) /* I - 1 = last file in job */
807 {
808 ipp_t *request; /* IPP request */
809 char uri[HTTP_MAX_URI]; /* Printer URI */
810
811
812 /*
813 * Setup the Send-Document request...
814 */
815
816 request = ippNewRequest(IPP_SEND_DOCUMENT);
817
818 snprintf(uri, sizeof(uri), "ipp://localhost/jobs/%d", id);
819 ippAddString(request, IPP_TAG_OPERATION, IPP_TAG_URI, "job-uri", NULL, uri);
820
821 ippAddString(request, IPP_TAG_OPERATION, IPP_TAG_NAME,
822 "requesting-user-name", NULL, user);
823
824 if (docname)
825 ippAddString(request, IPP_TAG_OPERATION, IPP_TAG_NAME,
826 "document-name", NULL, docname);
827
828 if (format)
829 ippAddString(request, IPP_TAG_OPERATION, IPP_TAG_MIMETYPE,
830 "document-format", NULL, format);
831
832 if (last)
833 ippAddBoolean(request, IPP_TAG_OPERATION, "last-document", 1);
834
835 /*
836 * Do the request...
837 */
838
839 snprintf(uri, sizeof(uri), "/jobs/%d", id);
840
841 ippDelete(cupsDoFileRequest(http, request, uri, filename));
842
843 if (cupsLastError() > IPP_OK_CONFLICT)
844 {
845 syslog(LOG_ERR, "Unable to send document - %s", cupsLastErrorString());
846
847 return (-1);
848 }
849
850 return (0);
851 }
852
853
854 /*
855 * 'recv_print_job()' - Receive a print job from the client.
856 */
857
858 static int /* O - Command status */
859 recv_print_job(
860 const char *queue, /* I - Printer name */
861 int num_defaults, /* I - Number of default options */
862 cups_option_t *defaults) /* I - Default options */
863 {
864 http_t *http; /* HTTP connection */
865 int i; /* Looping var */
866 int status; /* Command status */
867 int fd; /* Temporary file */
868 FILE *fp; /* File pointer */
869 char filename[1024]; /* Temporary filename */
870 int bytes; /* Bytes received */
871 char line[256], /* Line from file/stdin */
872 command, /* Command from line */
873 *count, /* Number of bytes */
874 *name; /* Name of file */
875 const char *job_sheets; /* Job sheets */
876 int num_data; /* Number of data files */
877 char control[1024], /* Control filename */
878 data[100][256], /* Data files */
879 temp[100][1024]; /* Temporary files */
880 char user[1024], /* User name */
881 title[1024], /* Job title */
882 docname[1024], /* Document name */
883 dest[256]; /* Printer/class queue */
884 int accepting, /* printer-is-accepting */
885 shared, /* printer-is-shared */
886 num_options; /* Number of options */
887 cups_option_t *options; /* Options */
888 int id; /* Job ID */
889 int docnumber, /* Current document number */
890 doccount; /* Count of documents */
891
892
893 /*
894 * Connect to the server...
895 */
896
897 http = httpConnectEncrypt(cupsServer(), ippPort(), cupsEncryption());
898 if (!http)
899 {
900 syslog(LOG_ERR, "Unable to connect to server: %s", strerror(errno));
901
902 putchar(1);
903
904 return (1);
905 }
906
907 /*
908 * See if the printer is available...
909 */
910
911 num_options = get_printer(http, queue, dest, sizeof(dest), &options,
912 &accepting, &shared, NULL);
913
914 if (num_options < 0 || !accepting || !shared)
915 {
916 if (dest[0])
917 syslog(LOG_INFO, "Rejecting job because \"%s\" is not %s", dest,
918 !accepting ? "accepting jobs" : "shared");
919 else
920 syslog(LOG_ERR, "Unable to get printer information for \"%s\"", queue);
921
922 httpClose(http);
923
924 putchar(1);
925
926 return (1);
927 }
928
929 putchar(0); /* OK so far... */
930
931 /*
932 * Read the request...
933 */
934
935 status = 0;
936 num_data = 0;
937 fd = -1;
938
939 control[0] = '\0';
940
941 while (smart_gets(line, sizeof(line), stdin) != NULL)
942 {
943 if (strlen(line) < 2)
944 {
945 status = 1;
946 break;
947 }
948
949 command = line[0];
950 count = line + 1;
951
952 for (name = count + 1; *name && !isspace(*name & 255); name ++);
953 while (isspace(*name & 255))
954 *name++ = '\0';
955
956 switch (command)
957 {
958 default :
959 case 0x01 : /* Abort */
960 status = 1;
961 break;
962
963 case 0x02 : /* Receive control file */
964 if (strlen(name) < 2)
965 {
966 syslog(LOG_ERR, "Bad control file name \"%s\"", name);
967 putchar(1);
968 status = 1;
969 break;
970 }
971
972 if (control[0])
973 {
974 /*
975 * Append to the existing control file - the LPD spec is
976 * not entirely clear, but at least the OS/2 LPD code sends
977 * multiple control files per connection...
978 */
979
980 if ((fd = open(control, O_WRONLY)) < 0)
981 {
982 syslog(LOG_ERR,
983 "Unable to append to temporary control file \"%s\" - %s",
984 control, strerror(errno));
985 putchar(1);
986 status = 1;
987 break;
988 }
989
990 lseek(fd, 0, SEEK_END);
991 }
992 else
993 {
994 if ((fd = cupsTempFd(control, sizeof(control))) < 0)
995 {
996 syslog(LOG_ERR, "Unable to open temporary control file \"%s\" - %s",
997 control, strerror(errno));
998 putchar(1);
999 status = 1;
1000 break;
1001 }
1002
1003 strcpy(filename, control);
1004 }
1005 break;
1006
1007 case 0x03 : /* Receive data file */
1008 if (strlen(name) < 2)
1009 {
1010 syslog(LOG_ERR, "Bad data file name \"%s\"", name);
1011 putchar(1);
1012 status = 1;
1013 break;
1014 }
1015
1016 if (num_data >= (int)(sizeof(data) / sizeof(data[0])))
1017 {
1018 /*
1019 * Too many data files...
1020 */
1021
1022 syslog(LOG_ERR, "Too many data files (%d)", num_data);
1023 putchar(1);
1024 status = 1;
1025 break;
1026 }
1027
1028 strlcpy(data[num_data], name, sizeof(data[0]));
1029
1030 if ((fd = cupsTempFd(temp[num_data], sizeof(temp[0]))) < 0)
1031 {
1032 syslog(LOG_ERR, "Unable to open temporary data file \"%s\" - %s",
1033 temp[num_data], strerror(errno));
1034 putchar(1);
1035 status = 1;
1036 break;
1037 }
1038
1039 strcpy(filename, temp[num_data]);
1040
1041 num_data ++;
1042 break;
1043 }
1044
1045 putchar(status);
1046
1047 if (status)
1048 break;
1049
1050 /*
1051 * Copy the data or control file from the client...
1052 */
1053
1054 for (i = atoi(count); i > 0; i -= bytes)
1055 {
1056 if (i > sizeof(line))
1057 bytes = sizeof(line);
1058 else
1059 bytes = i;
1060
1061 if ((bytes = fread(line, 1, bytes, stdin)) > 0)
1062 bytes = write(fd, line, bytes);
1063
1064 if (bytes < 1)
1065 {
1066 syslog(LOG_ERR, "Error while reading file - %s",
1067 strerror(errno));
1068 status = 1;
1069 break;
1070 }
1071 }
1072
1073 /*
1074 * Read trailing nul...
1075 */
1076
1077 if (!status)
1078 {
1079 if (fread(line, 1, 1, stdin) < 1)
1080 {
1081 status = 1;
1082 syslog(LOG_ERR, "Error while reading trailing nul - %s",
1083 strerror(errno));
1084 }
1085 else if (line[0])
1086 {
1087 status = 1;
1088 syslog(LOG_ERR, "Trailing character after file is not nul (%02X)!",
1089 line[0]);
1090 }
1091 }
1092
1093 /*
1094 * Close the file and send an acknowledgement...
1095 */
1096
1097 close(fd);
1098
1099 putchar(status);
1100
1101 if (status)
1102 break;
1103 }
1104
1105 if (!status)
1106 {
1107 /*
1108 * Process the control file and print stuff...
1109 */
1110
1111 if ((fp = fopen(control, "rb")) == NULL)
1112 status = 1;
1113 else
1114 {
1115 /*
1116 * Copy the default options...
1117 */
1118
1119 for (i = 0; i < num_defaults; i ++)
1120 num_options = cupsAddOption(defaults[i].name,
1121 defaults[i].value,
1122 num_options, &options);
1123
1124 /*
1125 * Grab the job information...
1126 */
1127
1128 title[0] = '\0';
1129 user[0] = '\0';
1130 doccount = 0;
1131
1132 while (smart_gets(line, sizeof(line), fp) != NULL)
1133 {
1134 /*
1135 * Process control lines...
1136 */
1137
1138 switch (line[0])
1139 {
1140 case 'J' : /* Job name */
1141 strlcpy(title, line + 1, sizeof(title));
1142 break;
1143
1144 case 'P' : /* User identification */
1145 strlcpy(user, line + 1, sizeof(user));
1146 break;
1147
1148 case 'L' : /* Print banner page */
1149 /*
1150 * If a banner was requested and it's not overridden by a
1151 * command line option and the destination's default is none
1152 * then add the standard banner...
1153 */
1154
1155 if (cupsGetOption("job-sheets", num_defaults, defaults) == NULL &&
1156 ((job_sheets = cupsGetOption("job-sheets", num_options,
1157 options)) == NULL ||
1158 !strcmp(job_sheets, "none,none")))
1159 {
1160 num_options = cupsAddOption("job-sheets", "standard",
1161 num_options, &options);
1162 }
1163 break;
1164
1165 case 'c' : /* Plot CIF file */
1166 case 'd' : /* Print DVI file */
1167 case 'f' : /* Print formatted file */
1168 case 'g' : /* Plot file */
1169 case 'l' : /* Print file leaving control characters (raw) */
1170 case 'n' : /* Print ditroff output file */
1171 case 'o' : /* Print PostScript output file */
1172 case 'p' : /* Print file with 'pr' format (prettyprint) */
1173 case 'r' : /* File to print with FORTRAN carriage control */
1174 case 't' : /* Print troff output file */
1175 case 'v' : /* Print raster file */
1176 doccount ++;
1177
1178 if (line[0] == 'l' &&
1179 !cupsGetOption("document-format", num_options, options))
1180 num_options = cupsAddOption("raw", "", num_options, &options);
1181
1182 if (line[0] == 'p')
1183 num_options = cupsAddOption("prettyprint", "", num_options,
1184 &options);
1185 break;
1186 }
1187
1188 if (status)
1189 break;
1190 }
1191
1192 /*
1193 * Check that we have a username...
1194 */
1195
1196 if (!user[0])
1197 {
1198 syslog(LOG_WARNING, "No username specified by client! "
1199 "Using \"anonymous\"...");
1200 strcpy(user, "anonymous");
1201 }
1202
1203 /*
1204 * Create the job...
1205 */
1206
1207 if ((id = create_job(http, dest, title, user, num_options, options)) < 0)
1208 status = 1;
1209 else
1210 {
1211 /*
1212 * Then print the job files...
1213 */
1214
1215 rewind(fp);
1216
1217 docname[0] = '\0';
1218 docnumber = 0;
1219
1220 while (smart_gets(line, sizeof(line), fp) != NULL)
1221 {
1222 /*
1223 * Process control lines...
1224 */
1225
1226 switch (line[0])
1227 {
1228 case 'N' : /* Document name */
1229 strlcpy(docname, line + 1, sizeof(docname));
1230 break;
1231
1232 case 'c' : /* Plot CIF file */
1233 case 'd' : /* Print DVI file */
1234 case 'f' : /* Print formatted file */
1235 case 'g' : /* Plot file */
1236 case 'l' : /* Print file leaving control characters (raw) */
1237 case 'n' : /* Print ditroff output file */
1238 case 'o' : /* Print PostScript output file */
1239 case 'p' : /* Print file with 'pr' format (prettyprint) */
1240 case 'r' : /* File to print with FORTRAN carriage control */
1241 case 't' : /* Print troff output file */
1242 case 'v' : /* Print raster file */
1243 /*
1244 * Figure out which file we are printing...
1245 */
1246
1247 for (i = 0; i < num_data; i ++)
1248 if (!strcmp(data[i], line + 1))
1249 break;
1250
1251 if (i >= num_data)
1252 {
1253 status = 1;
1254 break;
1255 }
1256
1257 /*
1258 * Send the print file...
1259 */
1260
1261 docnumber ++;
1262
1263 if (print_file(http, id, temp[i], docname, user,
1264 cupsGetOption("document-format", num_options,
1265 options),
1266 docnumber == doccount))
1267 status = 1;
1268 else
1269 status = 0;
1270
1271 break;
1272 }
1273
1274 if (status)
1275 break;
1276 }
1277 }
1278
1279 fclose(fp);
1280 }
1281 }
1282
1283 cupsFreeOptions(num_options, options);
1284
1285 httpClose(http);
1286
1287 /*
1288 * Clean up all temporary files and return...
1289 */
1290
1291 unlink(control);
1292
1293 for (i = 0; i < num_data; i ++)
1294 unlink(temp[i]);
1295
1296 return (status);
1297 }
1298
1299
1300 /*
1301 * 'remove_jobs()' - Cancel one or more jobs.
1302 */
1303
1304 static int /* O - Command status */
1305 remove_jobs(const char *dest, /* I - Destination */
1306 const char *agent, /* I - User agent */
1307 const char *list) /* I - List of jobs or users */
1308 {
1309 int id; /* Job ID */
1310 http_t *http; /* HTTP server connection */
1311 ipp_t *request; /* IPP Request */
1312 char uri[HTTP_MAX_URI]; /* Job URI */
1313
1314
1315 (void)dest; /* Suppress compiler warnings... */
1316
1317 /*
1318 * Try connecting to the local server...
1319 */
1320
1321 if ((http = httpConnectEncrypt(cupsServer(), ippPort(),
1322 cupsEncryption())) == NULL)
1323 {
1324 syslog(LOG_ERR, "Unable to connect to server %s: %s", cupsServer(),
1325 strerror(errno));
1326 return (1);
1327 }
1328
1329 /*
1330 * Loop for each job...
1331 */
1332
1333 while ((id = atoi(list)) > 0)
1334 {
1335 /*
1336 * Skip job ID in list...
1337 */
1338
1339 while (isdigit(*list & 255))
1340 list ++;
1341 while (isspace(*list & 255))
1342 list ++;
1343
1344 /*
1345 * Build an IPP_CANCEL_JOB request, which requires the following
1346 * attributes:
1347 *
1348 * attributes-charset
1349 * attributes-natural-language
1350 * job-uri
1351 * requesting-user-name
1352 */
1353
1354 request = ippNewRequest(IPP_CANCEL_JOB);
1355
1356 sprintf(uri, "ipp://localhost/jobs/%d", id);
1357 ippAddString(request, IPP_TAG_OPERATION, IPP_TAG_URI, "job-uri", NULL, uri);
1358
1359 ippAddString(request, IPP_TAG_OPERATION, IPP_TAG_NAME,
1360 "requesting-user-name", NULL, agent);
1361
1362 /*
1363 * Do the request and get back a response...
1364 */
1365
1366 ippDelete(cupsDoRequest(http, request, "/jobs"));
1367
1368 if (cupsLastError() > IPP_OK_CONFLICT)
1369 {
1370 syslog(LOG_WARNING, "Cancel of job ID %d failed: %s\n", id,
1371 cupsLastErrorString());
1372 httpClose(http);
1373 return (1);
1374 }
1375 else
1376 syslog(LOG_INFO, "Job ID %d canceled", id);
1377 }
1378
1379 httpClose(http);
1380
1381 return (0);
1382 }
1383
1384
1385 /*
1386 * 'send_state()' - Send the queue state.
1387 */
1388
1389 static int /* O - Command status */
1390 send_state(const char *queue, /* I - Destination */
1391 const char *list, /* I - Job or user */
1392 int longstatus) /* I - List of jobs or users */
1393 {
1394 int id; /* Job ID from list */
1395 http_t *http; /* HTTP server connection */
1396 ipp_t *request, /* IPP Request */
1397 *response; /* IPP Response */
1398 ipp_attribute_t *attr; /* Current attribute */
1399 ipp_pstate_t state; /* Printer state */
1400 const char *jobdest, /* Pointer into job-printer-uri */
1401 *jobuser, /* Pointer to job-originating-user-name */
1402 *jobname; /* Pointer to job-name */
1403 ipp_jstate_t jobstate; /* job-state */
1404 int jobid, /* job-id */
1405 jobsize, /* job-k-octets */
1406 jobcount, /* Number of jobs */
1407 jobcopies, /* Number of copies */
1408 rank; /* Rank of job */
1409 char rankstr[255]; /* Rank string */
1410 char namestr[1024]; /* Job name string */
1411 char uri[HTTP_MAX_URI]; /* Printer URI */
1412 char dest[256]; /* Printer/class queue */
1413 static const char * const ranks[10] = /* Ranking strings */
1414 {
1415 "th",
1416 "st",
1417 "nd",
1418 "rd",
1419 "th",
1420 "th",
1421 "th",
1422 "th",
1423 "th",
1424 "th"
1425 };
1426 static const char * const requested[] =
1427 { /* Requested attributes */
1428 "job-id",
1429 "job-k-octets",
1430 "job-state",
1431 "job-printer-uri",
1432 "job-originating-user-name",
1433 "job-name",
1434 "copies"
1435 };
1436
1437
1438 /*
1439 * Try connecting to the local server...
1440 */
1441
1442 if ((http = httpConnectEncrypt(cupsServer(), ippPort(),
1443 cupsEncryption())) == NULL)
1444 {
1445 syslog(LOG_ERR, "Unable to connect to server %s: %s", cupsServer(),
1446 strerror(errno));
1447 printf("Unable to connect to server %s: %s", cupsServer(), strerror(errno));
1448 return (1);
1449 }
1450
1451 /*
1452 * Get the actual destination name and printer state...
1453 */
1454
1455 if (get_printer(http, queue, dest, sizeof(dest), NULL, NULL, NULL, &state))
1456 {
1457 syslog(LOG_ERR, "Unable to get printer %s: %s", queue,
1458 cupsLastErrorString());
1459 printf("Unable to get printer %s: %s", queue, cupsLastErrorString());
1460 return (1);
1461 }
1462
1463 /*
1464 * Show the queue state...
1465 */
1466
1467 switch (state)
1468 {
1469 case IPP_PRINTER_IDLE :
1470 printf("%s is ready\n", dest);
1471 break;
1472 case IPP_PRINTER_PROCESSING :
1473 printf("%s is ready and printing\n", dest);
1474 break;
1475 case IPP_PRINTER_STOPPED :
1476 printf("%s is not ready\n", dest);
1477 break;
1478 }
1479
1480 /*
1481 * Build an IPP_GET_JOBS or IPP_GET_JOB_ATTRIBUTES request, which requires
1482 * the following attributes:
1483 *
1484 * attributes-charset
1485 * attributes-natural-language
1486 * job-uri or printer-uri
1487 */
1488
1489 id = atoi(list);
1490
1491 request = ippNewRequest(id ? IPP_GET_JOB_ATTRIBUTES : IPP_GET_JOBS);
1492
1493 httpAssembleURIf(HTTP_URI_CODING_ALL, uri, sizeof(uri), "ipp", NULL,
1494 "localhost", 0, "/printers/%s", dest);
1495
1496 ippAddString(request, IPP_TAG_OPERATION, IPP_TAG_URI, "printer-uri",
1497 NULL, uri);
1498
1499 if (id)
1500 ippAddInteger(request, IPP_TAG_OPERATION, IPP_TAG_INTEGER, "job-id", id);
1501 else
1502 {
1503 ippAddString(request, IPP_TAG_OPERATION, IPP_TAG_NAME,
1504 "requesting-user-name", NULL, list);
1505 ippAddBoolean(request, IPP_TAG_OPERATION, "my-jobs", 1);
1506 }
1507
1508 ippAddStrings(request, IPP_TAG_OPERATION, IPP_TAG_KEYWORD,
1509 "requested-attributes",
1510 sizeof(requested) / sizeof(requested[0]),
1511 NULL, requested);
1512
1513 /*
1514 * Do the request and get back a response...
1515 */
1516
1517 jobcount = 0;
1518 response = cupsDoRequest(http, request, "/");
1519
1520 if (cupsLastError() > IPP_OK_CONFLICT)
1521 {
1522 printf("get-jobs failed: %s\n", cupsLastErrorString());
1523 ippDelete(response);
1524 return (1);
1525 }
1526
1527 /*
1528 * Loop through the job list and display them...
1529 */
1530
1531 for (attr = response->attrs, rank = 1; attr; attr = attr->next)
1532 {
1533 /*
1534 * Skip leading attributes until we hit a job...
1535 */
1536
1537 while (attr && (attr->group_tag != IPP_TAG_JOB || !attr->name))
1538 attr = attr->next;
1539
1540 if (!attr)
1541 break;
1542
1543 /*
1544 * Pull the needed attributes from this job...
1545 */
1546
1547 jobid = 0;
1548 jobsize = 0;
1549 jobstate = IPP_JOB_PENDING;
1550 jobname = "untitled";
1551 jobuser = NULL;
1552 jobdest = NULL;
1553 jobcopies = 1;
1554
1555 while (attr && attr->group_tag == IPP_TAG_JOB)
1556 {
1557 if (!strcmp(attr->name, "job-id") &&
1558 attr->value_tag == IPP_TAG_INTEGER)
1559 jobid = attr->values[0].integer;
1560
1561 if (!strcmp(attr->name, "job-k-octets") &&
1562 attr->value_tag == IPP_TAG_INTEGER)
1563 jobsize = attr->values[0].integer;
1564
1565 if (!strcmp(attr->name, "job-state") &&
1566 attr->value_tag == IPP_TAG_ENUM)
1567 jobstate = (ipp_jstate_t)attr->values[0].integer;
1568
1569 if (!strcmp(attr->name, "job-printer-uri") &&
1570 attr->value_tag == IPP_TAG_URI)
1571 if ((jobdest = strrchr(attr->values[0].string.text, '/')) != NULL)
1572 jobdest ++;
1573
1574 if (!strcmp(attr->name, "job-originating-user-name") &&
1575 attr->value_tag == IPP_TAG_NAME)
1576 jobuser = attr->values[0].string.text;
1577
1578 if (!strcmp(attr->name, "job-name") &&
1579 attr->value_tag == IPP_TAG_NAME)
1580 jobname = attr->values[0].string.text;
1581
1582 if (!strcmp(attr->name, "copies") &&
1583 attr->value_tag == IPP_TAG_INTEGER)
1584 jobcopies = attr->values[0].integer;
1585
1586 attr = attr->next;
1587 }
1588
1589 /*
1590 * See if we have everything needed...
1591 */
1592
1593 if (!jobdest || !jobid)
1594 {
1595 if (!attr)
1596 break;
1597 else
1598 continue;
1599 }
1600
1601 if (!longstatus && jobcount == 0)
1602 puts("Rank Owner Job File(s) Total Size");
1603
1604 jobcount ++;
1605
1606 /*
1607 * Display the job...
1608 */
1609
1610 if (jobstate == IPP_JOB_PROCESSING)
1611 strcpy(rankstr, "active");
1612 else
1613 {
1614 snprintf(rankstr, sizeof(rankstr), "%d%s", rank, ranks[rank % 10]);
1615 rank ++;
1616 }
1617
1618 if (longstatus)
1619 {
1620 puts("");
1621
1622 if (jobcopies > 1)
1623 snprintf(namestr, sizeof(namestr), "%d copies of %s", jobcopies,
1624 jobname);
1625 else
1626 strlcpy(namestr, jobname, sizeof(namestr));
1627
1628 printf("%s: %-33.33s [job %d localhost]\n", jobuser, rankstr, jobid);
1629 printf(" %-39.39s %.0f bytes\n", namestr, 1024.0 * jobsize);
1630 }
1631 else
1632 printf("%-7s %-7.7s %-7d %-31.31s %.0f bytes\n", rankstr, jobuser,
1633 jobid, jobname, 1024.0 * jobsize);
1634
1635 if (!attr)
1636 break;
1637 }
1638
1639 ippDelete(response);
1640
1641 if (jobcount == 0)
1642 puts("no entries");
1643
1644 httpClose(http);
1645
1646 return (0);
1647 }
1648
1649
1650 /*
1651 * 'smart_gets()' - Get a line of text, removing the trailing CR and/or LF.
1652 */
1653
1654 static char * /* O - Line read or NULL */
1655 smart_gets(char *s, /* I - Pointer to line buffer */
1656 int len, /* I - Size of line buffer */
1657 FILE *fp) /* I - File to read from */
1658 {
1659 char *ptr, /* Pointer into line */
1660 *end; /* End of line */
1661 int ch; /* Character from file */
1662
1663
1664 /*
1665 * Read the line; unlike fgets(), we read the entire line but dump
1666 * characters that go past the end of the buffer. Also, we accept
1667 * CR, LF, or CR LF for the line endings to be "safe", although
1668 * RFC 1179 specifically says "just use LF".
1669 */
1670
1671 ptr = s;
1672 end = s + len - 1;
1673
1674 while ((ch = getc(fp)) != EOF)
1675 {
1676 if (ch == '\n')
1677 break;
1678 else if (ch == '\r')
1679 {
1680 /*
1681 * See if a LF follows...
1682 */
1683
1684 ch = getc(fp);
1685
1686 if (ch != '\n')
1687 ungetc(ch, fp);
1688
1689 break;
1690 }
1691 else if (ptr < end)
1692 *ptr++ = ch;
1693 }
1694
1695 *ptr = '\0';
1696
1697 if (ch == EOF && ptr == s)
1698 return (NULL);
1699 else
1700 return (s);
1701 }
1702
1703
1704 /*
1705 * End of "$Id: cups-lpd.c 6781 2007-08-08 21:09:31Z mike $".
1706 */