• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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;			/* &amp; */
693     else if (*sptr == '<' || *sptr == '>')
694       bytes += 3;			/* &lt; and &gt; */
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