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;			/* &amp; */
682     else if (*sptr == '<' || *sptr == '>')
683       bytes += 3;			/* &lt; and &gt; */
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