2 * ippdiscover command for CUPS.
4 * Copyright 2007-2013 by Apple Inc.
5 * Copyright 1997-2007 by Easy Software Products.
7 * These coded instructions, statements, and computer programs are the
8 * property of Apple Inc. and are protected by Federal copyright
9 * law. Distribution and use rights are outlined in the file "LICENSE.txt"
10 * which should have been included with this file. If this file is
11 * file is missing or damaged, see the license at "http://www.cups.org/".
13 * This file is subject to the Apple OS-Developed Software exception.
18 * Include necessary headers.
21 #include <cups/cups-private.h>
25 # pragma comment(lib, "dnssd.lib")
27 #endif /* HAVE_DNSSD */
29 # include <avahi-client/client.h>
30 # include <avahi-client/lookup.h>
31 # include <avahi-common/simple-watch.h>
32 # include <avahi-common/domain.h>
33 # include <avahi-common/error.h>
34 # include <avahi-common/malloc.h>
35 #define kDNSServiceMaxDomainName AVAHI_DOMAIN_NAME_MAX
36 #endif /* HAVE_AVAHI */
44 static int got_data
= 0; /* Got data from poll? */
45 static AvahiSimplePoll
*simple_poll
= NULL
;
46 /* Poll information */
47 #endif /* HAVE_AVAHI */
48 static const char *program
= NULL
;/* Program to run */
56 static void DNSSD_API
browse_callback(DNSServiceRef sdRef
,
57 DNSServiceFlags flags
,
58 uint32_t interfaceIndex
,
59 DNSServiceErrorType errorCode
,
60 const char *serviceName
,
62 const char *replyDomain
, void *context
)
63 __attribute__((nonnull(1,5,6,7,8)));
64 static void DNSSD_API
resolve_cb(DNSServiceRef sdRef
,
65 DNSServiceFlags flags
,
66 uint32_t interfaceIndex
,
67 DNSServiceErrorType errorCode
,
69 const char *hostTarget
,
70 uint16_t port
, uint16_t txtLen
,
71 const unsigned char *txtRecord
,
73 #endif /* HAVE_DNSSD */
76 static void browse_callback(AvahiServiceBrowser
*browser
,
77 AvahiIfIndex interface
,
78 AvahiProtocol protocol
,
79 AvahiBrowserEvent event
,
80 const char *serviceName
,
82 const char *replyDomain
,
83 AvahiLookupResultFlags flags
,
85 static void client_cb(AvahiClient
*client
, AvahiClientState state
,
87 static int poll_cb(struct pollfd
*pollfds
, unsigned int num_pollfds
,
88 int timeout
, void *context
);
89 static void resolve_cb(AvahiServiceResolver
*resolver
,
90 AvahiIfIndex interface
,
91 AvahiProtocol protocol
,
92 AvahiResolverEvent event
,
93 const char *name
, const char *type
,
94 const char *domain
, const char *host_name
,
95 const AvahiAddress
*address
, uint16_t port
,
97 AvahiLookupResultFlags flags
, void *context
);
98 #endif /* HAVE_AVAHI */
100 static void resolve_and_run(const char *name
, const char *type
,
102 static void unquote(char *dst
, const char *src
, size_t dstsize
);
103 static void usage(void) __attribute__((noreturn
));
107 * 'main()' - Browse for printers and run the specified command.
110 int /* O - Exit status */
111 main(int argc
, /* I - Number of command-line args */
112 char *argv
[]) /* I - Command-line arguments */
114 int i
; /* Looping var */
115 const char *opt
, /* Current option character */
116 *name
= NULL
, /* Service name */
117 *type
= "_ipp._tcp", /* Service type */
118 *domain
= "local."; /* Service domain */
120 DNSServiceRef ref
; /* Browsing service reference */
121 #endif /* HAVE_DNSSD */
123 AvahiClient
*client
; /* Client information */
124 int error
; /* Error code, if any */
125 #endif /* HAVE_AVAHI */
128 for (i
= 1; i
< argc
; i
++)
129 if (!strcmp(argv
[i
], "snmp"))
131 else if (!strcmp(argv
[i
], "ipp"))
135 puts("Usage: ./ipp-printers [{ipp | snmp}]");
140 * Create an array to track devices...
143 devices
= cupsArrayNew((cups_array_func_t
)compare_devices
, NULL
);
146 * Browse for different kinds of printers...
149 if (DNSServiceCreateConnection(&main_ref
) != kDNSServiceErr_NoError
)
151 perror("ERROR: Unable to create service connection");
155 fd
= DNSServiceRefSockFD(main_ref
);
158 DNSServiceBrowse(&ipp_ref
, kDNSServiceFlagsShareConnection
, 0,
159 "_ipp._tcp", NULL
, browse_callback
, devices
);
162 * Loop until we are killed...
173 timeout
.tv_usec
= 500000;
175 if (select(fd
+ 1, &input
, NULL
, NULL
, &timeout
) <= 0)
177 time_t curtime
= time(NULL
);
179 for (device
= (cups_device_t
*)cupsArrayFirst(devices
);
181 device
= (cups_device_t
*)cupsArrayNext(devices
))
182 if (!device
->got_resolve
)
187 if ((curtime
- device
->resolve_time
) > 10)
189 device
->got_resolve
= -1;
190 fprintf(stderr
, "\rUnable to resolve \"%s\": timeout\n",
202 if (FD_ISSET(fd
, &input
))
205 * Process results of our browsing...
209 DNSServiceProcessResult(main_ref
);
214 * Query any devices we've found...
217 DNSServiceErrorType status
; /* DNS query status */
218 int count
; /* Number of queries */
221 for (device
= (cups_device_t
*)cupsArrayFirst(devices
), count
= 0;
223 device
= (cups_device_t
*)cupsArrayNext(devices
))
225 if (!device
->ref
&& !device
->sent
)
228 * Found the device, now get the TXT record(s) for it...
233 device
->resolve_time
= time(NULL
);
234 device
->ref
= main_ref
;
236 status
= DNSServiceResolve(&(device
->ref
),
237 kDNSServiceFlagsShareConnection
,
238 0, device
->name
, device
->regtype
,
239 device
->domain
, resolve_callback
,
241 if (status
!= kDNSServiceErr_NoError
)
243 fprintf(stderr
, "\rUnable to resolve \"%s\": %d\n",
244 device
->name
, status
);
251 else if (!device
->sent
&& device
->got_resolve
)
254 * Got the TXT records, now report the device...
257 DNSServiceRefDeallocate(device
->ref
);
266 fprintf(stderr
, "\rFound %d printers. Now querying for capabilities...\n",
267 cupsArrayCount(devices
));
270 puts("#!/bin/sh -x");
271 puts("test -d results && rm -rf results");
272 puts("mkdir results");
273 puts("CUPS_DEBUG_LEVEL=6; export CUPS_DEBUG_LEVEL");
274 puts("CUPS_DEBUG_FILTER='^(ipp|http|_ipp|_http|cupsGetResponse|cupsSend|"
275 "cupsWrite|cupsDo).*'; export CUPS_DEBUG_FILTER");
277 for (device
= (cups_device_t
*)cupsArrayFirst(devices
);
279 device
= (cups_device_t
*)cupsArrayNext(devices
))
281 if (device
->got_resolve
<= 0 || device
->cups_shared
)
285 fprintf(stderr
, "Checking \"%s\" (got_resolve=%d, cups_shared=%d, uri=%s)\n",
286 device
->name
, device
->got_resolve
, device
->cups_shared
, device
->uri
);
288 fprintf(stderr
, "Checking \"%s\"...\n", device
->name
);
291 if ((http
= httpConnect(device
->host
, device
->port
)) == NULL
)
293 fprintf(stderr
, "Failed to connect to \"%s\": %s\n", device
->name
,
294 cupsLastErrorString());
298 request
= ippNewRequest(IPP_GET_PRINTER_ATTRIBUTES
);
299 ippAddString(request
, IPP_TAG_OPERATION
, IPP_TAG_URI
, "printer-uri", NULL
,
302 response
= cupsDoRequest(http
, request
, device
->rp
);
304 if (cupsLastError() > IPP_OK_SUBST
)
305 fprintf(stderr
, "Failed to query \"%s\": %s\n", device
->name
,
306 cupsLastErrorString());
309 if ((attr
= ippFindAttribute(response
, "ipp-versions-supported",
310 IPP_TAG_KEYWORD
)) != NULL
)
312 version
= attr
->values
[0].string
.text
;
314 for (i
= 1; i
< attr
->num_values
; i
++)
315 if (strcmp(attr
->values
[i
].string
.text
, version
) > 0)
316 version
= attr
->values
[i
].string
.text
;
323 if ((attr
= ippFindAttribute(response
, "document-format-supported",
324 IPP_TAG_MIMETYPE
)) != NULL
)
327 * Figure out the test file for printing, preferring PDF and PostScript
328 * over JPEG and plain text...
331 for (i
= 0; i
< attr
->num_values
; i
++)
333 if (!strcasecmp(attr
->values
[i
].string
.text
, "application/pdf"))
335 testfile
= "testfile.pdf";
338 else if (!strcasecmp(attr
->values
[i
].string
.text
,
339 "application/postscript"))
340 testfile
= "testfile.ps";
341 else if (!strcasecmp(attr
->values
[i
].string
.text
, "image/jpeg") &&
343 testfile
= "testfile.jpg";
344 else if (!strcasecmp(attr
->values
[i
].string
.text
, "text/plain") &&
346 testfile
= "testfile.txt";
347 else if (!strcasecmp(attr
->values
[i
].string
.text
,
348 "application/vnd.hp-PCL") && !testfile
)
349 testfile
= "testfile.pcl";
355 "Printer \"%s\" reports the following IPP file formats:\n",
357 for (i
= 0; i
< attr
->num_values
; i
++)
358 fprintf(stderr
, " \"%s\"\n", attr
->values
[i
].string
.text
);
362 if (!testfile
&& device
->pdl
)
364 char *pdl
, /* Copy of pdl string */
365 *start
, *end
; /* Pointers into pdl string */
368 pdl
= strdup(device
->pdl
);
369 for (start
= device
->pdl
; start
&& *start
; start
= end
)
371 if ((end
= strchr(start
, ',')) != NULL
)
374 if (!strcasecmp(start
, "application/pdf"))
376 testfile
= "testfile.pdf";
379 else if (!strcasecmp(start
, "application/postscript"))
380 testfile
= "testfile.ps";
381 else if (!strcasecmp(start
, "image/jpeg") && !testfile
)
382 testfile
= "testfile.jpg";
383 else if (!strcasecmp(start
, "text/plain") && !testfile
)
384 testfile
= "testfile.txt";
385 else if (!strcasecmp(start
, "application/vnd.hp-PCL") && !testfile
)
386 testfile
= "testfile.pcl";
393 "Using \"%s\" for printer \"%s\" based on TXT record pdl "
394 "info.\n", testfile
, device
->name
);
399 "Printer \"%s\" reports the following TXT file formats:\n",
401 fprintf(stderr
, " \"%s\"\n", device
->pdl
);
406 (attr
= ippFindAttribute(response
, "printer-make-and-model",
407 IPP_TAG_TEXT
)) != NULL
)
408 device
->ty
= strdup(attr
->values
[0].string
.text
);
410 if (strcmp(version
, "1.0") && testfile
&& device
->ty
)
412 char filename
[1024], /* Filename */
413 *fileptr
; /* Pointer into filename */
414 const char *typtr
; /* Pointer into ty */
416 if (!strncasecmp(device
->ty
, "DeskJet", 7) ||
417 !strncasecmp(device
->ty
, "DesignJet", 9) ||
418 !strncasecmp(device
->ty
, "OfficeJet", 9) ||
419 !strncasecmp(device
->ty
, "Photosmart", 10))
420 strlcpy(filename
, "HP_", sizeof(filename
));
424 fileptr
= filename
+ strlen(filename
);
426 if (!strncasecmp(device
->ty
, "Lexmark International Lexmark", 29))
427 typtr
= device
->ty
+ 22;
431 while (*typtr
&& fileptr
< (filename
+ sizeof(filename
) - 1))
433 if (isalnum(*typtr
& 255) || *typtr
== '-')
434 *fileptr
++ = *typtr
++;
444 printf("# %s\n", device
->name
);
445 printf("echo \"Testing %s...\"\n", device
->name
);
449 printf("echo \"snmpwalk -c public -v 1 -Cc %s 1.3.6.1.2.1.25 "
450 "1.3.6.1.2.1.43 1.3.6.1.4.1.2699.1\" > results/%s.snmpwalk\n",
451 device
->host
, filename
);
452 printf("snmpwalk -c public -v 1 -Cc %s 1.3.6.1.2.1.25 "
453 "1.3.6.1.2.1.43 1.3.6.1.4.1.2699.1 | "
454 "tee -a results/%s.snmpwalk\n",
455 device
->host
, filename
);
460 printf("echo \"./ipptool-static -tIf %s -T 30 -d NOPRINT=1 -V %s %s "
461 "ipp-%s.test\" > results/%s.log\n", testfile
, version
,
462 device
->uri
, version
, filename
);
463 printf("CUPS_DEBUG_LOG=results/%s.debug_log "
464 "./ipptool-static -tIf %s -T 30 -d NOPRINT=1 -V %s %s "
465 "ipp-%s.test | tee -a results/%s.log\n", filename
,
466 testfile
, version
, device
->uri
,
472 else if (!device
->ty
)
474 "Ignoring \"%s\" since it doesn't provide a make and model.\n",
478 "Ignoring \"%s\" since it does not support a common format.\n",
481 fprintf(stderr
, "Ignoring \"%s\" since it only supports IPP/1.0.\n",
494 * 'browse_callback()' - Browse devices.
499 DNSServiceRef sdRef
, /* I - Service reference */
500 DNSServiceFlags flags
, /* I - Option flags */
501 uint32_t interfaceIndex
, /* I - Interface number */
502 DNSServiceErrorType errorCode
, /* I - Error, if any */
503 const char *serviceName
, /* I - Name of service/device */
504 const char *regtype
, /* I - Type of service */
505 const char *replyDomain
, /* I - Service domain */
506 void *context
) /* I - Devices array */
509 fprintf(stderr
, "browse_callback(sdRef=%p, flags=%x, "
510 "interfaceIndex=%d, errorCode=%d, serviceName=\"%s\", "
511 "regtype=\"%s\", replyDomain=\"%s\", context=%p)\n",
512 sdRef
, flags
, interfaceIndex
, errorCode
,
513 serviceName
? serviceName
: "(null)",
514 regtype
? regtype
: "(null)",
515 replyDomain
? replyDomain
: "(null)",
520 * Only process "add" data...
523 if (errorCode
!= kDNSServiceErr_NoError
|| !(flags
& kDNSServiceFlagsAdd
))
530 get_device((cups_array_t
*)context
, serviceName
, regtype
, replyDomain
);
535 * 'compare_devices()' - Compare two devices.
538 static int /* O - Result of comparison */
539 compare_devices(cups_device_t
*a
, /* I - First device */
540 cups_device_t
*b
) /* I - Second device */
542 int retval
= strcmp(a
->name
, b
->name
);
547 return (-strcmp(a
->regtype
, b
->regtype
));
552 * 'get_device()' - Create or update a device.
555 static cups_device_t
* /* O - Device */
556 get_device(cups_array_t
*devices
, /* I - Device array */
557 const char *serviceName
, /* I - Name of service/device */
558 const char *regtype
, /* I - Type of service */
559 const char *replyDomain
) /* I - Service domain */
561 cups_device_t key
, /* Search key */
562 *device
; /* Device */
563 char fullName
[kDNSServiceMaxDomainName
];
564 /* Full name for query */
568 * See if this is a new device...
571 key
.name
= (char *)serviceName
;
572 key
.regtype
= (char *)regtype
;
574 for (device
= cupsArrayFind(devices
, &key
);
576 device
= cupsArrayNext(devices
))
577 if (strcasecmp(device
->name
, key
.name
))
581 if (!strcasecmp(device
->domain
, "local.") &&
582 strcasecmp(device
->domain
, replyDomain
))
585 * Update the .local listing to use the "global" domain name instead.
586 * The backend will try local lookups first, then the global domain name.
589 free(device
->domain
);
590 device
->domain
= strdup(replyDomain
);
592 DNSServiceConstructFullName(fullName
, device
->name
, regtype
,
594 free(device
->fullName
);
595 device
->fullName
= strdup(fullName
);
602 * Yes, add the device...
605 device
= calloc(sizeof(cups_device_t
), 1);
606 device
->name
= strdup(serviceName
);
607 device
->domain
= strdup(replyDomain
);
608 device
->regtype
= strdup(regtype
);
610 cupsArrayAdd(devices
, device
);
613 * Set the "full name" of this service, which is used for queries...
616 DNSServiceConstructFullName(fullName
, serviceName
, regtype
, replyDomain
);
617 device
->fullName
= strdup(fullName
);
620 fprintf(stderr
, "get_device: fullName=\"%s\"...\n", fullName
);
628 * 'progress()' - Show query progress.
635 const char *chars
= "|/-\\";
636 static int count
= 0;
639 fprintf(stderr
, "\rLooking for printers %c", chars
[count
]);
641 count
= (count
+ 1) & 3;
647 * 'resolve_callback()' - Process resolve data.
652 DNSServiceRef sdRef
, /* I - Service reference */
653 DNSServiceFlags flags
, /* I - Data flags */
654 uint32_t interfaceIndex
, /* I - Interface */
655 DNSServiceErrorType errorCode
, /* I - Error, if any */
656 const char *fullName
, /* I - Full service name */
657 const char *hostTarget
, /* I - Hostname */
658 uint16_t port
, /* I - Port number (network byte order) */
659 uint16_t txtLen
, /* I - Length of TXT record data */
660 const unsigned char *txtRecord
, /* I - TXT record data */
661 void *context
) /* I - Device */
663 char temp
[257], /* TXT key value */
664 uri
[1024]; /* Printer URI */
665 const void *value
; /* Value from TXT record */
666 uint8_t valueLen
; /* Length of value */
667 cups_device_t
*device
= (cups_device_t
*)context
;
672 fprintf(stderr
, "\rresolve_callback(sdRef=%p, flags=%x, "
673 "interfaceIndex=%d, errorCode=%d, fullName=\"%s\", "
674 "hostTarget=\"%s\", port=%d, txtLen=%u, txtRecord=%p, "
676 sdRef
, flags
, interfaceIndex
, errorCode
,
677 fullName
? fullName
: "(null)", hostTarget
? hostTarget
: "(null)",
678 ntohs(port
), txtLen
, txtRecord
, context
);
682 * Only process "add" data...
685 if (errorCode
!= kDNSServiceErr_NoError
)
688 device
->got_resolve
= 1;
689 device
->host
= strdup(hostTarget
);
690 device
->port
= ntohs(port
);
693 * Extract the "remote printer" key from the TXT record and save the URI...
696 if ((value
= TXTRecordGetValuePtr(txtLen
, txtRecord
, "rp",
699 if (((char *)value
)[0] == '/')
702 * "rp" value (incorrectly) has a leading slash already...
705 memcpy(temp
, value
, valueLen
);
706 temp
[valueLen
] = '\0';
711 * Convert to resource by concatenating with a leading "/"...
715 memcpy(temp
+ 1, value
, valueLen
);
716 temp
[valueLen
+ 1] = '\0';
722 * Default "rp" value is blank, mapping to a path of "/"...
729 if (!strncmp(temp
, "/printers/", 10))
730 device
->cups_shared
= -1;
732 httpAssembleURI(HTTP_URI_CODING_ALL
, uri
, sizeof(uri
), "ipp",
733 NULL
, hostTarget
, ntohs(port
), temp
);
734 device
->uri
= strdup(uri
);
735 device
->rp
= strdup(temp
);
737 if ((value
= TXTRecordGetValuePtr(txtLen
, txtRecord
, "ty",
740 memcpy(temp
, value
, valueLen
);
741 temp
[valueLen
] = '\0';
743 device
->ty
= strdup(temp
);
746 if ((value
= TXTRecordGetValuePtr(txtLen
, txtRecord
, "pdl",
749 memcpy(temp
, value
, valueLen
);
750 temp
[valueLen
] = '\0';
752 device
->pdl
= strdup(temp
);
755 if ((value
= TXTRecordGetValuePtr(txtLen
, txtRecord
, "printer-type",
757 device
->cups_shared
= 1;
759 if (device
->cups_shared
)
760 fprintf(stderr
, "\rIgnoring CUPS printer %s\n", uri
);
762 fprintf(stderr
, "\rFound IPP printer %s\n", uri
);
769 * 'unquote()' - Unquote a name string.
773 unquote(char *dst
, /* I - Destination buffer */
774 const char *src
, /* I - Source string */
775 size_t dstsize
) /* I - Size of destination buffer */
777 char *dstend
= dst
+ dstsize
- 1; /* End of destination buffer */
780 while (*src
&& dst
< dstend
)
785 if (isdigit(src
[0] & 255) && isdigit(src
[1] & 255) &&
786 isdigit(src
[2] & 255))
788 *dst
++ = ((((src
[0] - '0') * 10) + src
[1] - '0') * 10) + src
[2] - '0';
803 * 'usage()' - Show program usage and exit.
809 _cupsLangPuts(stdout
, _("Usage: ippdiscover [options] -a\n"
810 " ippdiscover [options] \"service name\"\n"
813 _cupsLangPuts(stdout
, _(" -a Browse for all services."));
814 _cupsLangPuts(stdout
, _(" -d domain Browse/resolve in specified domain."));
815 _cupsLangPuts(stdout
, _(" -p program Run specified program for each service."));
816 _cupsLangPuts(stdout
, _(" -t type Browse/resolve with specified type."));