4 * ippdiscover command for CUPS.
6 * Copyright 2007-2013 by Apple Inc.
7 * Copyright 1997-2007 by Easy Software Products.
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/".
15 * This file is subject to the Apple OS-Developed Software exception.
23 * Include necessary headers.
26 #include <cups/cups-private.h>
30 # pragma comment(lib, "dnssd.lib")
32 #endif /* HAVE_DNSSD */
34 # include <avahi-client/client.h>
35 # include <avahi-client/lookup.h>
36 # include <avahi-common/simple-watch.h>
37 # include <avahi-common/domain.h>
38 # include <avahi-common/error.h>
39 # include <avahi-common/malloc.h>
40 #define kDNSServiceMaxDomainName AVAHI_DOMAIN_NAME_MAX
41 #endif /* HAVE_AVAHI */
49 static int got_data
= 0; /* Got data from poll? */
50 static AvahiSimplePoll
*simple_poll
= NULL
;
51 /* Poll information */
52 #endif /* HAVE_AVAHI */
53 static const char *program
= NULL
;/* Program to run */
61 static void DNSSD_API
browse_callback(DNSServiceRef sdRef
,
62 DNSServiceFlags flags
,
63 uint32_t interfaceIndex
,
64 DNSServiceErrorType errorCode
,
65 const char *serviceName
,
67 const char *replyDomain
, void *context
)
68 __attribute__((nonnull(1,5,6,7,8)));
69 static void DNSSD_API
resolve_cb(DNSServiceRef sdRef
,
70 DNSServiceFlags flags
,
71 uint32_t interfaceIndex
,
72 DNSServiceErrorType errorCode
,
74 const char *hostTarget
,
75 uint16_t port
, uint16_t txtLen
,
76 const unsigned char *txtRecord
,
78 #endif /* HAVE_DNSSD */
81 static void browse_callback(AvahiServiceBrowser
*browser
,
82 AvahiIfIndex interface
,
83 AvahiProtocol protocol
,
84 AvahiBrowserEvent event
,
85 const char *serviceName
,
87 const char *replyDomain
,
88 AvahiLookupResultFlags flags
,
90 static void client_cb(AvahiClient
*client
, AvahiClientState state
,
92 static int poll_cb(struct pollfd
*pollfds
, unsigned int num_pollfds
,
93 int timeout
, void *context
);
94 static void resolve_cb(AvahiServiceResolver
*resolver
,
95 AvahiIfIndex interface
,
96 AvahiProtocol protocol
,
97 AvahiResolverEvent event
,
98 const char *name
, const char *type
,
99 const char *domain
, const char *host_name
,
100 const AvahiAddress
*address
, uint16_t port
,
101 AvahiStringList
*txt
,
102 AvahiLookupResultFlags flags
, void *context
);
103 #endif /* HAVE_AVAHI */
105 static void resolve_and_run(const char *name
, const char *type
,
107 static void unquote(char *dst
, const char *src
, size_t dstsize
);
108 static void usage(void) __attribute__((noreturn
));
112 * 'main()' - Browse for printers and run the specified command.
115 int /* O - Exit status */
116 main(int argc
, /* I - Number of command-line args */
117 char *argv
[]) /* I - Command-line arguments */
119 int i
; /* Looping var */
120 const char *opt
, /* Current option character */
121 *name
= NULL
, /* Service name */
122 *type
= "_ipp._tcp", /* Service type */
123 *domain
= "local."; /* Service domain */
125 DNSServiceRef ref
; /* Browsing service reference */
126 #endif /* HAVE_DNSSD */
128 AvahiClient
*client
; /* Client information */
129 int error
; /* Error code, if any */
130 #endif /* HAVE_AVAHI */
133 for (i
= 1; i
< argc
; i
++)
134 if (!strcmp(argv
[i
], "snmp"))
136 else if (!strcmp(argv
[i
], "ipp"))
140 puts("Usage: ./ipp-printers [{ipp | snmp}]");
145 * Create an array to track devices...
148 devices
= cupsArrayNew((cups_array_func_t
)compare_devices
, NULL
);
151 * Browse for different kinds of printers...
154 if (DNSServiceCreateConnection(&main_ref
) != kDNSServiceErr_NoError
)
156 perror("ERROR: Unable to create service connection");
160 fd
= DNSServiceRefSockFD(main_ref
);
163 DNSServiceBrowse(&ipp_ref
, kDNSServiceFlagsShareConnection
, 0,
164 "_ipp._tcp", NULL
, browse_callback
, devices
);
167 * Loop until we are killed...
178 timeout
.tv_usec
= 500000;
180 if (select(fd
+ 1, &input
, NULL
, NULL
, &timeout
) <= 0)
182 time_t curtime
= time(NULL
);
184 for (device
= (cups_device_t
*)cupsArrayFirst(devices
);
186 device
= (cups_device_t
*)cupsArrayNext(devices
))
187 if (!device
->got_resolve
)
192 if ((curtime
- device
->resolve_time
) > 10)
194 device
->got_resolve
= -1;
195 fprintf(stderr
, "\rUnable to resolve \"%s\": timeout\n",
207 if (FD_ISSET(fd
, &input
))
210 * Process results of our browsing...
214 DNSServiceProcessResult(main_ref
);
219 * Query any devices we've found...
222 DNSServiceErrorType status
; /* DNS query status */
223 int count
; /* Number of queries */
226 for (device
= (cups_device_t
*)cupsArrayFirst(devices
), count
= 0;
228 device
= (cups_device_t
*)cupsArrayNext(devices
))
230 if (!device
->ref
&& !device
->sent
)
233 * Found the device, now get the TXT record(s) for it...
238 device
->resolve_time
= time(NULL
);
239 device
->ref
= main_ref
;
241 status
= DNSServiceResolve(&(device
->ref
),
242 kDNSServiceFlagsShareConnection
,
243 0, device
->name
, device
->regtype
,
244 device
->domain
, resolve_callback
,
246 if (status
!= kDNSServiceErr_NoError
)
248 fprintf(stderr
, "\rUnable to resolve \"%s\": %d\n",
249 device
->name
, status
);
256 else if (!device
->sent
&& device
->got_resolve
)
259 * Got the TXT records, now report the device...
262 DNSServiceRefDeallocate(device
->ref
);
271 fprintf(stderr
, "\rFound %d printers. Now querying for capabilities...\n",
272 cupsArrayCount(devices
));
275 puts("#!/bin/sh -x");
276 puts("test -d results && rm -rf results");
277 puts("mkdir results");
278 puts("CUPS_DEBUG_LEVEL=6; export CUPS_DEBUG_LEVEL");
279 puts("CUPS_DEBUG_FILTER='^(ipp|http|_ipp|_http|cupsGetResponse|cupsSend|"
280 "cupsWrite|cupsDo).*'; export CUPS_DEBUG_FILTER");
282 for (device
= (cups_device_t
*)cupsArrayFirst(devices
);
284 device
= (cups_device_t
*)cupsArrayNext(devices
))
286 if (device
->got_resolve
<= 0 || device
->cups_shared
)
290 fprintf(stderr
, "Checking \"%s\" (got_resolve=%d, cups_shared=%d, uri=%s)\n",
291 device
->name
, device
->got_resolve
, device
->cups_shared
, device
->uri
);
293 fprintf(stderr
, "Checking \"%s\"...\n", device
->name
);
296 if ((http
= httpConnect(device
->host
, device
->port
)) == NULL
)
298 fprintf(stderr
, "Failed to connect to \"%s\": %s\n", device
->name
,
299 cupsLastErrorString());
303 request
= ippNewRequest(IPP_GET_PRINTER_ATTRIBUTES
);
304 ippAddString(request
, IPP_TAG_OPERATION
, IPP_TAG_URI
, "printer-uri", NULL
,
307 response
= cupsDoRequest(http
, request
, device
->rp
);
309 if (cupsLastError() > IPP_OK_SUBST
)
310 fprintf(stderr
, "Failed to query \"%s\": %s\n", device
->name
,
311 cupsLastErrorString());
314 if ((attr
= ippFindAttribute(response
, "ipp-versions-supported",
315 IPP_TAG_KEYWORD
)) != NULL
)
317 version
= attr
->values
[0].string
.text
;
319 for (i
= 1; i
< attr
->num_values
; i
++)
320 if (strcmp(attr
->values
[i
].string
.text
, version
) > 0)
321 version
= attr
->values
[i
].string
.text
;
328 if ((attr
= ippFindAttribute(response
, "document-format-supported",
329 IPP_TAG_MIMETYPE
)) != NULL
)
332 * Figure out the test file for printing, preferring PDF and PostScript
333 * over JPEG and plain text...
336 for (i
= 0; i
< attr
->num_values
; i
++)
338 if (!strcasecmp(attr
->values
[i
].string
.text
, "application/pdf"))
340 testfile
= "testfile.pdf";
343 else if (!strcasecmp(attr
->values
[i
].string
.text
,
344 "application/postscript"))
345 testfile
= "testfile.ps";
346 else if (!strcasecmp(attr
->values
[i
].string
.text
, "image/jpeg") &&
348 testfile
= "testfile.jpg";
349 else if (!strcasecmp(attr
->values
[i
].string
.text
, "text/plain") &&
351 testfile
= "testfile.txt";
352 else if (!strcasecmp(attr
->values
[i
].string
.text
,
353 "application/vnd.hp-PCL") && !testfile
)
354 testfile
= "testfile.pcl";
360 "Printer \"%s\" reports the following IPP file formats:\n",
362 for (i
= 0; i
< attr
->num_values
; i
++)
363 fprintf(stderr
, " \"%s\"\n", attr
->values
[i
].string
.text
);
367 if (!testfile
&& device
->pdl
)
369 char *pdl
, /* Copy of pdl string */
370 *start
, *end
; /* Pointers into pdl string */
373 pdl
= strdup(device
->pdl
);
374 for (start
= device
->pdl
; start
&& *start
; start
= end
)
376 if ((end
= strchr(start
, ',')) != NULL
)
379 if (!strcasecmp(start
, "application/pdf"))
381 testfile
= "testfile.pdf";
384 else if (!strcasecmp(start
, "application/postscript"))
385 testfile
= "testfile.ps";
386 else if (!strcasecmp(start
, "image/jpeg") && !testfile
)
387 testfile
= "testfile.jpg";
388 else if (!strcasecmp(start
, "text/plain") && !testfile
)
389 testfile
= "testfile.txt";
390 else if (!strcasecmp(start
, "application/vnd.hp-PCL") && !testfile
)
391 testfile
= "testfile.pcl";
398 "Using \"%s\" for printer \"%s\" based on TXT record pdl "
399 "info.\n", testfile
, device
->name
);
404 "Printer \"%s\" reports the following TXT file formats:\n",
406 fprintf(stderr
, " \"%s\"\n", device
->pdl
);
411 (attr
= ippFindAttribute(response
, "printer-make-and-model",
412 IPP_TAG_TEXT
)) != NULL
)
413 device
->ty
= strdup(attr
->values
[0].string
.text
);
415 if (strcmp(version
, "1.0") && testfile
&& device
->ty
)
417 char filename
[1024], /* Filename */
418 *fileptr
; /* Pointer into filename */
419 const char *typtr
; /* Pointer into ty */
421 if (!strncasecmp(device
->ty
, "DeskJet", 7) ||
422 !strncasecmp(device
->ty
, "DesignJet", 9) ||
423 !strncasecmp(device
->ty
, "OfficeJet", 9) ||
424 !strncasecmp(device
->ty
, "Photosmart", 10))
425 strlcpy(filename
, "HP_", sizeof(filename
));
429 fileptr
= filename
+ strlen(filename
);
431 if (!strncasecmp(device
->ty
, "Lexmark International Lexmark", 29))
432 typtr
= device
->ty
+ 22;
436 while (*typtr
&& fileptr
< (filename
+ sizeof(filename
) - 1))
438 if (isalnum(*typtr
& 255) || *typtr
== '-')
439 *fileptr
++ = *typtr
++;
449 printf("# %s\n", device
->name
);
450 printf("echo \"Testing %s...\"\n", device
->name
);
454 printf("echo \"snmpwalk -c public -v 1 -Cc %s 1.3.6.1.2.1.25 "
455 "1.3.6.1.2.1.43 1.3.6.1.4.1.2699.1\" > results/%s.snmpwalk\n",
456 device
->host
, filename
);
457 printf("snmpwalk -c public -v 1 -Cc %s 1.3.6.1.2.1.25 "
458 "1.3.6.1.2.1.43 1.3.6.1.4.1.2699.1 | "
459 "tee -a results/%s.snmpwalk\n",
460 device
->host
, filename
);
465 printf("echo \"./ipptool-static -tIf %s -T 30 -d NOPRINT=1 -V %s %s "
466 "ipp-%s.test\" > results/%s.log\n", testfile
, version
,
467 device
->uri
, version
, filename
);
468 printf("CUPS_DEBUG_LOG=results/%s.debug_log "
469 "./ipptool-static -tIf %s -T 30 -d NOPRINT=1 -V %s %s "
470 "ipp-%s.test | tee -a results/%s.log\n", filename
,
471 testfile
, version
, device
->uri
,
477 else if (!device
->ty
)
479 "Ignoring \"%s\" since it doesn't provide a make and model.\n",
483 "Ignoring \"%s\" since it does not support a common format.\n",
486 fprintf(stderr
, "Ignoring \"%s\" since it only supports IPP/1.0.\n",
499 * 'browse_callback()' - Browse devices.
504 DNSServiceRef sdRef
, /* I - Service reference */
505 DNSServiceFlags flags
, /* I - Option flags */
506 uint32_t interfaceIndex
, /* I - Interface number */
507 DNSServiceErrorType errorCode
, /* I - Error, if any */
508 const char *serviceName
, /* I - Name of service/device */
509 const char *regtype
, /* I - Type of service */
510 const char *replyDomain
, /* I - Service domain */
511 void *context
) /* I - Devices array */
514 fprintf(stderr
, "browse_callback(sdRef=%p, flags=%x, "
515 "interfaceIndex=%d, errorCode=%d, serviceName=\"%s\", "
516 "regtype=\"%s\", replyDomain=\"%s\", context=%p)\n",
517 sdRef
, flags
, interfaceIndex
, errorCode
,
518 serviceName
? serviceName
: "(null)",
519 regtype
? regtype
: "(null)",
520 replyDomain
? replyDomain
: "(null)",
525 * Only process "add" data...
528 if (errorCode
!= kDNSServiceErr_NoError
|| !(flags
& kDNSServiceFlagsAdd
))
535 get_device((cups_array_t
*)context
, serviceName
, regtype
, replyDomain
);
540 * 'compare_devices()' - Compare two devices.
543 static int /* O - Result of comparison */
544 compare_devices(cups_device_t
*a
, /* I - First device */
545 cups_device_t
*b
) /* I - Second device */
547 int retval
= strcmp(a
->name
, b
->name
);
552 return (-strcmp(a
->regtype
, b
->regtype
));
557 * 'get_device()' - Create or update a device.
560 static cups_device_t
* /* O - Device */
561 get_device(cups_array_t
*devices
, /* I - Device array */
562 const char *serviceName
, /* I - Name of service/device */
563 const char *regtype
, /* I - Type of service */
564 const char *replyDomain
) /* I - Service domain */
566 cups_device_t key
, /* Search key */
567 *device
; /* Device */
568 char fullName
[kDNSServiceMaxDomainName
];
569 /* Full name for query */
573 * See if this is a new device...
576 key
.name
= (char *)serviceName
;
577 key
.regtype
= (char *)regtype
;
579 for (device
= cupsArrayFind(devices
, &key
);
581 device
= cupsArrayNext(devices
))
582 if (strcasecmp(device
->name
, key
.name
))
586 if (!strcasecmp(device
->domain
, "local.") &&
587 strcasecmp(device
->domain
, replyDomain
))
590 * Update the .local listing to use the "global" domain name instead.
591 * The backend will try local lookups first, then the global domain name.
594 free(device
->domain
);
595 device
->domain
= strdup(replyDomain
);
597 DNSServiceConstructFullName(fullName
, device
->name
, regtype
,
599 free(device
->fullName
);
600 device
->fullName
= strdup(fullName
);
607 * Yes, add the device...
610 device
= calloc(sizeof(cups_device_t
), 1);
611 device
->name
= strdup(serviceName
);
612 device
->domain
= strdup(replyDomain
);
613 device
->regtype
= strdup(regtype
);
615 cupsArrayAdd(devices
, device
);
618 * Set the "full name" of this service, which is used for queries...
621 DNSServiceConstructFullName(fullName
, serviceName
, regtype
, replyDomain
);
622 device
->fullName
= strdup(fullName
);
625 fprintf(stderr
, "get_device: fullName=\"%s\"...\n", fullName
);
633 * 'progress()' - Show query progress.
640 const char *chars
= "|/-\\";
641 static int count
= 0;
644 fprintf(stderr
, "\rLooking for printers %c", chars
[count
]);
646 count
= (count
+ 1) & 3;
652 * 'resolve_callback()' - Process resolve data.
657 DNSServiceRef sdRef
, /* I - Service reference */
658 DNSServiceFlags flags
, /* I - Data flags */
659 uint32_t interfaceIndex
, /* I - Interface */
660 DNSServiceErrorType errorCode
, /* I - Error, if any */
661 const char *fullName
, /* I - Full service name */
662 const char *hostTarget
, /* I - Hostname */
663 uint16_t port
, /* I - Port number (network byte order) */
664 uint16_t txtLen
, /* I - Length of TXT record data */
665 const unsigned char *txtRecord
, /* I - TXT record data */
666 void *context
) /* I - Device */
668 char temp
[257], /* TXT key value */
669 uri
[1024]; /* Printer URI */
670 const void *value
; /* Value from TXT record */
671 uint8_t valueLen
; /* Length of value */
672 cups_device_t
*device
= (cups_device_t
*)context
;
677 fprintf(stderr
, "\rresolve_callback(sdRef=%p, flags=%x, "
678 "interfaceIndex=%d, errorCode=%d, fullName=\"%s\", "
679 "hostTarget=\"%s\", port=%d, txtLen=%u, txtRecord=%p, "
681 sdRef
, flags
, interfaceIndex
, errorCode
,
682 fullName
? fullName
: "(null)", hostTarget
? hostTarget
: "(null)",
683 ntohs(port
), txtLen
, txtRecord
, context
);
687 * Only process "add" data...
690 if (errorCode
!= kDNSServiceErr_NoError
)
693 device
->got_resolve
= 1;
694 device
->host
= strdup(hostTarget
);
695 device
->port
= ntohs(port
);
698 * Extract the "remote printer" key from the TXT record and save the URI...
701 if ((value
= TXTRecordGetValuePtr(txtLen
, txtRecord
, "rp",
704 if (((char *)value
)[0] == '/')
707 * "rp" value (incorrectly) has a leading slash already...
710 memcpy(temp
, value
, valueLen
);
711 temp
[valueLen
] = '\0';
716 * Convert to resource by concatenating with a leading "/"...
720 memcpy(temp
+ 1, value
, valueLen
);
721 temp
[valueLen
+ 1] = '\0';
727 * Default "rp" value is blank, mapping to a path of "/"...
734 if (!strncmp(temp
, "/printers/", 10))
735 device
->cups_shared
= -1;
737 httpAssembleURI(HTTP_URI_CODING_ALL
, uri
, sizeof(uri
), "ipp",
738 NULL
, hostTarget
, ntohs(port
), temp
);
739 device
->uri
= strdup(uri
);
740 device
->rp
= strdup(temp
);
742 if ((value
= TXTRecordGetValuePtr(txtLen
, txtRecord
, "ty",
745 memcpy(temp
, value
, valueLen
);
746 temp
[valueLen
] = '\0';
748 device
->ty
= strdup(temp
);
751 if ((value
= TXTRecordGetValuePtr(txtLen
, txtRecord
, "pdl",
754 memcpy(temp
, value
, valueLen
);
755 temp
[valueLen
] = '\0';
757 device
->pdl
= strdup(temp
);
760 if ((value
= TXTRecordGetValuePtr(txtLen
, txtRecord
, "printer-type",
762 device
->cups_shared
= 1;
764 if (device
->cups_shared
)
765 fprintf(stderr
, "\rIgnoring CUPS printer %s\n", uri
);
767 fprintf(stderr
, "\rFound IPP printer %s\n", uri
);
774 * 'unquote()' - Unquote a name string.
778 unquote(char *dst
, /* I - Destination buffer */
779 const char *src
, /* I - Source string */
780 size_t dstsize
) /* I - Size of destination buffer */
782 char *dstend
= dst
+ dstsize
- 1; /* End of destination buffer */
785 while (*src
&& dst
< dstend
)
790 if (isdigit(src
[0] & 255) && isdigit(src
[1] & 255) &&
791 isdigit(src
[2] & 255))
793 *dst
++ = ((((src
[0] - '0') * 10) + src
[1] - '0') * 10) + src
[2] - '0';
808 * 'usage()' - Show program usage and exit.
814 _cupsLangPuts(stdout
, _("Usage: ippdiscover [options] -a\n"
815 " ippdiscover [options] \"service name\"\n"
818 _cupsLangPuts(stdout
, _(" -a Browse for all services."));
819 _cupsLangPuts(stdout
, _(" -d domain Browse/resolve in specified domain."));
820 _cupsLangPuts(stdout
, _(" -p program Run specified program for each service."));
821 _cupsLangPuts(stdout
, _(" -t type Browse/resolve with specified type."));