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