1 /*
2 * RSS notifier for CUPS.
3 *
4 * Copyright 2007-2015 by Apple Inc.
5 * Copyright 2007 by Easy Software Products.
6 *
7 * Licensed under Apache License v2.0. See the file "LICENSE" for more information.
8 */
9
10 /*
11 * Include necessary headers...
12 */
13
14 #include <cups/cups.h>
15 #include <sys/stat.h>
16 #include <cups/language.h>
17 #include <cups/string-private.h>
18 #include <cups/array.h>
19 #include <sys/select.h>
20 #include <cups/ipp-private.h> /* TODO: Update so we don't need this */
21
22
23 /*
24 * Structures...
25 */
26
27 typedef struct _cups_rss_s /**** RSS message data ****/
28 {
29 int sequence_number; /* notify-sequence-number */
30 char *subject, /* Message subject/summary */
31 *text, /* Message text */
32 *link_url; /* Link to printer */
33 time_t event_time; /* When the event occurred */
34 } _cups_rss_t;
35
36
37 /*
38 * Local globals...
39 */
40
41 static char *rss_password; /* Password for remote RSS */
42
43
44 /*
45 * Local functions...
46 */
47
48 static int compare_rss(_cups_rss_t *a, _cups_rss_t *b);
49 static void delete_message(_cups_rss_t *rss);
50 static void load_rss(cups_array_t *rss, const char *filename);
51 static _cups_rss_t *new_message(int sequence_number, char *subject,
52 char *text, char *link_url,
53 time_t event_time);
54 static const char *password_cb(const char *prompt);
55 static int save_rss(cups_array_t *rss, const char *filename,
56 const char *baseurl);
57 static char *xml_escape(const char *s);
58
59
60 /*
61 * 'main()' - Main entry for the test notifier.
62 */
63
64 int /* O - Exit status */
main(int argc,char * argv[])65 main(int argc, /* I - Number of command-line arguments */
66 char *argv[]) /* I - Command-line arguments */
67 {
68 int i; /* Looping var */
69 ipp_t *event; /* Event from scheduler */
70 ipp_state_t state; /* IPP event state */
71 char scheme[32], /* URI scheme ("rss") */
72 username[256], /* Username for remote RSS */
73 host[1024], /* Hostname for remote RSS */
74 resource[1024], /* RSS file */
75 *options; /* Options */
76 int port, /* Port number for remote RSS */
77 max_events; /* Maximum number of events */
78 http_t *http; /* Connection to remote server */
79 http_status_t status; /* HTTP GET/PUT status code */
80 char filename[1024], /* Local filename */
81 newname[1024]; /* filename.N */
82 cups_lang_t *language; /* Language information */
83 ipp_attribute_t *printer_up_time, /* Timestamp on event */
84 *notify_sequence_number,/* Sequence number */
85 *notify_printer_uri; /* Printer URI */
86 char *subject, /* Subject for notification message */
87 *text, /* Text for notification message */
88 link_url[1024], /* Link to printer */
89 link_scheme[32], /* Scheme for link */
90 link_username[256], /* Username for link */
91 link_host[1024], /* Host for link */
92 link_resource[1024]; /* Resource for link */
93 int link_port; /* Link port */
94 cups_array_t *rss; /* RSS message array */
95 _cups_rss_t *msg; /* RSS message */
96 char baseurl[1024]; /* Base URL */
97 fd_set input; /* Input set for select() */
98 struct timeval timeout; /* Timeout for select() */
99 int changed; /* Has the RSS data changed? */
100 int exit_status; /* Exit status */
101
102
103 fprintf(stderr, "DEBUG: argc=%d\n", argc);
104 for (i = 0; i < argc; i ++)
105 fprintf(stderr, "DEBUG: argv[%d]=\"%s\"\n", i, argv[i]);
106
107 /*
108 * See whether we are publishing this RSS feed locally or remotely...
109 */
110
111 if (httpSeparateURI(HTTP_URI_CODING_ALL, argv[1], scheme, sizeof(scheme),
112 username, sizeof(username), host, sizeof(host), &port,
113 resource, sizeof(resource)) < HTTP_URI_OK)
114 {
115 fprintf(stderr, "ERROR: Bad RSS URI \"%s\"!\n", argv[1]);
116 return (1);
117 }
118
119 max_events = 20;
120
121 if ((options = strchr(resource, '?')) != NULL)
122 {
123 *options++ = '\0';
124
125 if (!strncmp(options, "max_events=", 11))
126 {
127 max_events = atoi(options + 11);
128
129 if (max_events <= 0)
130 max_events = 20;
131 }
132 }
133
134 rss = cupsArrayNew((cups_array_func_t)compare_rss, NULL);
135
136 if (host[0])
137 {
138 /*
139 * Remote feed, see if we can get the current file...
140 */
141
142 int fd; /* Temporary file */
143
144
145 if ((rss_password = strchr(username, ':')) != NULL)
146 *rss_password++ = '\0';
147
148 cupsSetPasswordCB(password_cb);
149 cupsSetUser(username);
150
151 if ((fd = cupsTempFd(filename, sizeof(filename))) < 0)
152 {
153 fprintf(stderr, "ERROR: Unable to create temporary file: %s\n",
154 strerror(errno));
155
156 return (1);
157 }
158
159 if ((http = httpConnect(host, port)) == NULL)
160 {
161 fprintf(stderr, "ERROR: Unable to connect to %s on port %d: %s\n",
162 host, port, strerror(errno));
163
164 close(fd);
165 unlink(filename);
166
167 return (1);
168 }
169
170 status = cupsGetFd(http, resource, fd);
171
172 close(fd);
173
174 if (status != HTTP_OK && status != HTTP_NOT_FOUND)
175 {
176 fprintf(stderr, "ERROR: Unable to GET %s from %s on port %d: %d %s\n",
177 resource, host, port, status, httpStatus(status));
178
179 httpClose(http);
180 unlink(filename);
181
182 return (1);
183 }
184
185 strlcpy(newname, filename, sizeof(newname));
186
187 httpAssembleURI(HTTP_URI_CODING_ALL, baseurl, sizeof(baseurl), "http",
188 NULL, host, port, resource);
189 }
190 else
191 {
192 const char *cachedir, /* CUPS_CACHEDIR */
193 *server_name, /* SERVER_NAME */
194 *server_port; /* SERVER_PORT */
195
196
197 http = NULL;
198
199 if ((cachedir = getenv("CUPS_CACHEDIR")) == NULL)
200 cachedir = CUPS_CACHEDIR;
201
202 if ((server_name = getenv("SERVER_NAME")) == NULL)
203 server_name = "localhost";
204
205 if ((server_port = getenv("SERVER_PORT")) == NULL)
206 server_port = "631";
207
208 snprintf(filename, sizeof(filename), "%s/rss%s", cachedir, resource);
209 snprintf(newname, sizeof(newname), "%s.N", filename);
210
211 httpAssembleURIf(HTTP_URI_CODING_ALL, baseurl, sizeof(baseurl), "http",
212 NULL, server_name, atoi(server_port), "/rss%s", resource);
213 }
214
215 /*
216 * Load the previous RSS file, if any...
217 */
218
219 load_rss(rss, filename);
220
221 changed = cupsArrayCount(rss) == 0;
222
223 /*
224 * Localize for the user's chosen language...
225 */
226
227 language = cupsLangDefault();
228
229 /*
230 * Read events and update the RSS file until we are out of events.
231 */
232
233 for (exit_status = 0, event = NULL;;)
234 {
235 if (changed)
236 {
237 /*
238 * Save the messages to the file again, uploading as needed...
239 */
240
241 if (save_rss(rss, newname, baseurl))
242 {
243 if (http)
244 {
245 /*
246 * Upload the RSS file...
247 */
248
249 if ((status = cupsPutFile(http, resource, filename)) != HTTP_CREATED)
250 fprintf(stderr, "ERROR: Unable to PUT %s from %s on port %d: %d %s\n",
251 resource, host, port, status, httpStatus(status));
252 }
253 else
254 {
255 /*
256 * Move the new RSS file over top the old one...
257 */
258
259 if (rename(newname, filename))
260 fprintf(stderr, "ERROR: Unable to rename %s to %s: %s\n",
261 newname, filename, strerror(errno));
262 }
263
264 changed = 0;
265 }
266 }
267
268 /*
269 * Wait up to 30 seconds for an event...
270 */
271
272 timeout.tv_sec = 30;
273 timeout.tv_usec = 0;
274
275 FD_ZERO(&input);
276 FD_SET(0, &input);
277
278 if (select(1, &input, NULL, NULL, &timeout) < 0)
279 continue;
280 else if (!FD_ISSET(0, &input))
281 {
282 fprintf(stderr, "DEBUG: %s is bored, exiting...\n", argv[1]);
283 break;
284 }
285
286 /*
287 * Read the next event...
288 */
289
290 event = ippNew();
291 while ((state = ippReadFile(0, event)) != IPP_DATA)
292 {
293 if (state <= IPP_IDLE)
294 break;
295 }
296
297 if (state == IPP_ERROR)
298 fputs("DEBUG: ippReadFile() returned IPP_ERROR!\n", stderr);
299
300 if (state <= IPP_IDLE)
301 break;
302
303 /*
304 * Collect the info from the event...
305 */
306
307 printer_up_time = ippFindAttribute(event, "printer-up-time",
308 IPP_TAG_INTEGER);
309 notify_sequence_number = ippFindAttribute(event, "notify-sequence-number",
310 IPP_TAG_INTEGER);
311 notify_printer_uri = ippFindAttribute(event, "notify-printer-uri",
312 IPP_TAG_URI);
313 subject = cupsNotifySubject(language, event);
314 text = cupsNotifyText(language, event);
315
316 if (printer_up_time && notify_sequence_number && subject && text)
317 {
318 /*
319 * Create a new RSS message...
320 */
321
322 if (notify_printer_uri)
323 {
324 httpSeparateURI(HTTP_URI_CODING_ALL,
325 notify_printer_uri->values[0].string.text,
326 link_scheme, sizeof(link_scheme),
327 link_username, sizeof(link_username),
328 link_host, sizeof(link_host), &link_port,
329 link_resource, sizeof(link_resource));
330 httpAssembleURI(HTTP_URI_CODING_ALL, link_url, sizeof(link_url),
331 "http", link_username, link_host, link_port,
332 link_resource);
333 }
334
335 msg = new_message(notify_sequence_number->values[0].integer,
336 xml_escape(subject), xml_escape(text),
337 notify_printer_uri ? xml_escape(link_url) : NULL,
338 printer_up_time->values[0].integer);
339
340 if (!msg)
341 {
342 fprintf(stderr, "ERROR: Unable to create message: %s\n",
343 strerror(errno));
344 exit_status = 1;
345 break;
346 }
347
348 /*
349 * Add it to the array...
350 */
351
352 cupsArrayAdd(rss, msg);
353
354 changed = 1;
355
356 /*
357 * Trim the array as needed...
358 */
359
360 while (cupsArrayCount(rss) > max_events)
361 {
362 msg = cupsArrayFirst(rss);
363
364 cupsArrayRemove(rss, msg);
365
366 delete_message(msg);
367 }
368 }
369
370 if (subject)
371 free(subject);
372
373 if (text)
374 free(text);
375
376 ippDelete(event);
377 event = NULL;
378 }
379
380 /*
381 * We only get here when idle or error...
382 */
383
384 ippDelete(event);
385
386 if (http)
387 {
388 unlink(filename);
389 httpClose(http);
390 }
391
392 return (exit_status);
393 }
394
395
396 /*
397 * 'compare_rss()' - Compare two messages.
398 */
399
400 static int /* O - Result of comparison */
compare_rss(_cups_rss_t * a,_cups_rss_t * b)401 compare_rss(_cups_rss_t *a, /* I - First message */
402 _cups_rss_t *b) /* I - Second message */
403 {
404 return (a->sequence_number - b->sequence_number);
405 }
406
407
408 /*
409 * 'delete_message()' - Free all memory used by a message.
410 */
411
412 static void
delete_message(_cups_rss_t * msg)413 delete_message(_cups_rss_t *msg) /* I - RSS message */
414 {
415 if (msg->subject)
416 free(msg->subject);
417
418 if (msg->text)
419 free(msg->text);
420
421 if (msg->link_url)
422 free(msg->link_url);
423
424 free(msg);
425 }
426
427
428 /*
429 * 'load_rss()' - Load an existing RSS feed file.
430 */
431
432 static void
load_rss(cups_array_t * rss,const char * filename)433 load_rss(cups_array_t *rss, /* I - RSS messages */
434 const char *filename) /* I - File to load */
435 {
436 FILE *fp; /* File pointer */
437 char line[4096], /* Line from file */
438 *subject, /* Subject */
439 *text, /* Text */
440 *link_url, /* Link URL */
441 *start, /* Start of element */
442 *end; /* End of element */
443 time_t event_time; /* Event time */
444 int sequence_number; /* Sequence number */
445 int in_item; /* In an item */
446 _cups_rss_t *msg; /* New message */
447
448
449 if ((fp = fopen(filename, "r")) == NULL)
450 {
451 if (errno != ENOENT)
452 fprintf(stderr, "ERROR: Unable to open %s: %s\n", filename,
453 strerror(errno));
454
455 return;
456 }
457
458 subject = NULL;
459 text = NULL;
460 link_url = NULL;
461 event_time = 0;
462 sequence_number = 0;
463 in_item = 0;
464
465 while (fgets(line, sizeof(line), fp))
466 {
467 if (strstr(line, "<item>"))
468 in_item = 1;
469 else if (strstr(line, "</item>") && in_item)
470 {
471 if (subject && text)
472 {
473 msg = new_message(sequence_number, subject, text, link_url,
474 event_time);
475
476 if (msg)
477 cupsArrayAdd(rss, msg);
478
479 }
480 else
481 {
482 if (subject)
483 free(subject);
484
485 if (text)
486 free(text);
487
488 if (link_url)
489 free(link_url);
490 }
491
492 subject = NULL;
493 text = NULL;
494 link_url = NULL;
495 event_time = 0;
496 sequence_number = 0;
497 in_item = 0;
498 }
499 else if (!in_item)
500 continue;
501 else if ((start = strstr(line, "<title>")) != NULL)
502 {
503 start += 7;
504 if ((end = strstr(start, "</title>")) != NULL)
505 {
506 *end = '\0';
507 subject = strdup(start);
508 }
509 }
510 else if ((start = strstr(line, "<description>")) != NULL)
511 {
512 start += 13;
513 if ((end = strstr(start, "</description>")) != NULL)
514 {
515 *end = '\0';
516 text = strdup(start);
517 }
518 }
519 else if ((start = strstr(line, "<link>")) != NULL)
520 {
521 start += 6;
522 if ((end = strstr(start, "</link>")) != NULL)
523 {
524 *end = '\0';
525 link_url = strdup(start);
526 }
527 }
528 else if ((start = strstr(line, "<pubDate>")) != NULL)
529 {
530 start += 9;
531 if ((end = strstr(start, "</pubDate>")) != NULL)
532 {
533 *end = '\0';
534 event_time = httpGetDateTime(start);
535 }
536 }
537 else if ((start = strstr(line, "<guid>")) != NULL)
538 sequence_number = atoi(start + 6);
539 }
540
541 if (subject)
542 free(subject);
543
544 if (text)
545 free(text);
546
547 if (link_url)
548 free(link_url);
549
550 fclose(fp);
551 }
552
553
554 /*
555 * 'new_message()' - Create a new RSS message.
556 */
557
558 static _cups_rss_t * /* O - New message */
new_message(int sequence_number,char * subject,char * text,char * link_url,time_t event_time)559 new_message(int sequence_number, /* I - notify-sequence-number */
560 char *subject, /* I - Subject/summary */
561 char *text, /* I - Text */
562 char *link_url, /* I - Link to printer */
563 time_t event_time) /* I - Date/time of event */
564 {
565 _cups_rss_t *msg; /* New message */
566
567
568 if ((msg = calloc(1, sizeof(_cups_rss_t))) == NULL)
569 return (NULL);
570
571 msg->sequence_number = sequence_number;
572 msg->subject = subject;
573 msg->text = text;
574 msg->link_url = link_url;
575 msg->event_time = event_time;
576
577 return (msg);
578 }
579
580
581 /*
582 * 'password_cb()' - Return the cached password.
583 */
584
585 static const char * /* O - Cached password */
password_cb(const char * prompt)586 password_cb(const char *prompt) /* I - Prompt string, unused */
587 {
588 (void)prompt;
589
590 return (rss_password);
591 }
592
593
594 /*
595 * 'save_rss()' - Save messages to a RSS file.
596 */
597
598 static int /* O - 1 on success, 0 on failure */
save_rss(cups_array_t * rss,const char * filename,const char * baseurl)599 save_rss(cups_array_t *rss, /* I - RSS messages */
600 const char *filename, /* I - File to save to */
601 const char *baseurl) /* I - Base URL */
602 {
603 FILE *fp; /* File pointer */
604 _cups_rss_t *msg; /* Current message */
605 char date[1024]; /* Current date */
606 char *href; /* Escaped base URL */
607
608
609 if ((fp = fopen(filename, "w")) == NULL)
610 {
611 fprintf(stderr, "ERROR: Unable to create %s: %s\n", filename,
612 strerror(errno));
613 return (0);
614 }
615
616 fchmod(fileno(fp), 0644);
617
618 fputs("<?xml version=\"1.0\"?>\n", fp);
619 fputs("<rss version=\"2.0\">\n", fp);
620 fputs(" <channel>\n", fp);
621 fputs(" <title>CUPS RSS Feed</title>\n", fp);
622
623 href = xml_escape(baseurl);
624 fprintf(fp, " <link>%s</link>\n", href);
625 free(href);
626
627 fputs(" <description>CUPS RSS Feed</description>\n", fp);
628 fputs(" <generator>" CUPS_SVERSION "</generator>\n", fp);
629 fputs(" <ttl>1</ttl>\n", fp);
630
631 fprintf(fp, " <pubDate>%s</pubDate>\n",
632 httpGetDateString2(time(NULL), date, sizeof(date)));
633
634 for (msg = (_cups_rss_t *)cupsArrayLast(rss);
635 msg;
636 msg = (_cups_rss_t *)cupsArrayPrev(rss))
637 {
638 char *subject = xml_escape(msg->subject);
639 char *text = xml_escape(msg->text);
640
641 fputs(" <item>\n", fp);
642 fprintf(fp, " <title>%s</title>\n", subject);
643 fprintf(fp, " <description>%s</description>\n", text);
644 if (msg->link_url)
645 fprintf(fp, " <link>%s</link>\n", msg->link_url);
646 fprintf(fp, " <pubDate>%s</pubDate>\n",
647 httpGetDateString2(msg->event_time, date, sizeof(date)));
648 fprintf(fp, " <guid>%d</guid>\n", msg->sequence_number);
649 fputs(" </item>\n", fp);
650
651 free(subject);
652 free(text);
653 }
654
655 fputs(" </channel>\n", fp);
656 fputs("</rss>\n", fp);
657
658 return (!fclose(fp));
659 }
660
661
662 /*
663 * 'xml_escape()' - Copy a string, escaping &, <, and > as needed.
664 */
665
666 static char * /* O - Escaped string */
xml_escape(const char * s)667 xml_escape(const char *s) /* I - String to escape */
668 {
669 char *e, /* Escaped string */
670 *eptr; /* Pointer into escaped string */
671 const char *sptr; /* Pointer into string */
672 size_t bytes; /* Bytes needed for string */
673
674
675 /*
676 * First figure out how many extra bytes we need...
677 */
678
679 for (bytes = 0, sptr = s; *sptr; sptr ++)
680 if (*sptr == '&')
681 bytes += 4; /* & */
682 else if (*sptr == '<' || *sptr == '>')
683 bytes += 3; /* < and > */
684
685 /*
686 * If there is nothing to escape, just strdup() it...
687 */
688
689 if (bytes == 0)
690 return (strdup(s));
691
692 /*
693 * Otherwise allocate memory and copy...
694 */
695
696 if ((e = malloc(bytes + 1 + strlen(s))) == NULL)
697 return (NULL);
698
699 for (eptr = e, sptr = s; *sptr; sptr ++)
700 if (*sptr == '&')
701 {
702 *eptr++ = '&';
703 *eptr++ = 'a';
704 *eptr++ = 'm';
705 *eptr++ = 'p';
706 *eptr++ = ';';
707 }
708 else if (*sptr == '<')
709 {
710 *eptr++ = '&';
711 *eptr++ = 'l';
712 *eptr++ = 't';
713 *eptr++ = ';';
714 }
715 else if (*sptr == '>')
716 {
717 *eptr++ = '&';
718 *eptr++ = 'g';
719 *eptr++ = 't';
720 *eptr++ = ';';
721 }
722 else
723 *eptr++ = *sptr;
724
725 *eptr = '\0';
726
727 return (e);
728 }
729