/* * RSS notifier for CUPS. * * Copyright 2007-2015 by Apple Inc. * Copyright 2007 by Easy Software Products. * * Licensed under Apache License v2.0. See the file "LICENSE" for more information. */ /* * Include necessary headers... */ #include #include #include #include #include #include #include /* TODO: Update so we don't need this */ /* * Structures... */ typedef struct _cups_rss_s /**** RSS message data ****/ { int sequence_number; /* notify-sequence-number */ char *subject, /* Message subject/summary */ *text, /* Message text */ *link_url; /* Link to printer */ time_t event_time; /* When the event occurred */ } _cups_rss_t; /* * Local globals... */ static char *rss_password; /* Password for remote RSS */ /* * Local functions... */ static int compare_rss(_cups_rss_t *a, _cups_rss_t *b); static void delete_message(_cups_rss_t *msg); static void load_rss(cups_array_t *rss, const char *filename); static _cups_rss_t *new_message(int sequence_number, char *subject, char *text, char *link_url, time_t event_time); static const char *password_cb(const char *prompt); static int save_rss(cups_array_t *rss, const char *filename, const char *baseurl); static char *xml_escape(const char *s); /* * 'main()' - Main entry for the test notifier. */ int /* O - Exit status */ main(int argc, /* I - Number of command-line arguments */ char *argv[]) /* I - Command-line arguments */ { int i; /* Looping var */ ipp_t *event; /* Event from scheduler */ ipp_state_t state; /* IPP event state */ char scheme[32], /* URI scheme ("rss") */ username[256], /* Username for remote RSS */ host[1024], /* Hostname for remote RSS */ resource[1024], /* RSS file */ *options; /* Options */ int port, /* Port number for remote RSS */ max_events; /* Maximum number of events */ http_t *http; /* Connection to remote server */ http_status_t status; /* HTTP GET/PUT status code */ char filename[1024], /* Local filename */ newname[1024]; /* filename.N */ cups_lang_t *language; /* Language information */ ipp_attribute_t *printer_up_time, /* Timestamp on event */ *notify_sequence_number,/* Sequence number */ *notify_printer_uri; /* Printer URI */ char *subject, /* Subject for notification message */ *text, /* Text for notification message */ link_url[1024], /* Link to printer */ link_scheme[32], /* Scheme for link */ link_username[256], /* Username for link */ link_host[1024], /* Host for link */ link_resource[1024]; /* Resource for link */ int link_port; /* Link port */ cups_array_t *rss; /* RSS message array */ _cups_rss_t *msg; /* RSS message */ char baseurl[1024]; /* Base URL */ fd_set input; /* Input set for select() */ struct timeval timeout; /* Timeout for select() */ int changed; /* Has the RSS data changed? */ int exit_status; /* Exit status */ fprintf(stderr, "DEBUG: argc=%d\n", argc); for (i = 0; i < argc; i ++) fprintf(stderr, "DEBUG: argv[%d]=\"%s\"\n", i, argv[i]); /* * See whether we are publishing this RSS feed locally or remotely... */ if (httpSeparateURI(HTTP_URI_CODING_ALL, argv[1], scheme, sizeof(scheme), username, sizeof(username), host, sizeof(host), &port, resource, sizeof(resource)) < HTTP_URI_OK) { fprintf(stderr, "ERROR: Bad RSS URI \"%s\"!\n", argv[1]); return (1); } max_events = 20; if ((options = strchr(resource, '?')) != NULL) { *options++ = '\0'; if (!strncmp(options, "max_events=", 11)) { max_events = atoi(options + 11); if (max_events <= 0) max_events = 20; } } rss = cupsArrayNew((cups_array_func_t)compare_rss, NULL); if (host[0]) { /* * Remote feed, see if we can get the current file... */ int fd; /* Temporary file */ if ((rss_password = strchr(username, ':')) != NULL) *rss_password++ = '\0'; cupsSetPasswordCB(password_cb); cupsSetUser(username); if ((fd = cupsTempFd(filename, sizeof(filename))) < 0) { fprintf(stderr, "ERROR: Unable to create temporary file: %s\n", strerror(errno)); return (1); } if ((http = httpConnect2(host, port, NULL, AF_UNSPEC, HTTP_ENCRYPTION_IF_REQUESTED, 1, 30000, NULL)) == NULL) { fprintf(stderr, "ERROR: Unable to connect to %s on port %d: %s\n", host, port, strerror(errno)); close(fd); unlink(filename); return (1); } status = cupsGetFd(http, resource, fd); close(fd); if (status != HTTP_OK && status != HTTP_NOT_FOUND) { fprintf(stderr, "ERROR: Unable to GET %s from %s on port %d: %d %s\n", resource, host, port, status, httpStatus(status)); httpClose(http); unlink(filename); return (1); } strlcpy(newname, filename, sizeof(newname)); httpAssembleURI(HTTP_URI_CODING_ALL, baseurl, sizeof(baseurl), "http", NULL, host, port, resource); } else { const char *cachedir, /* CUPS_CACHEDIR */ *server_name, /* SERVER_NAME */ *server_port; /* SERVER_PORT */ http = NULL; if ((cachedir = getenv("CUPS_CACHEDIR")) == NULL) cachedir = CUPS_CACHEDIR; if ((server_name = getenv("SERVER_NAME")) == NULL) server_name = "localhost"; if ((server_port = getenv("SERVER_PORT")) == NULL) server_port = "631"; snprintf(filename, sizeof(filename), "%s/rss%s", cachedir, resource); snprintf(newname, sizeof(newname), "%s.N", filename); httpAssembleURIf(HTTP_URI_CODING_ALL, baseurl, sizeof(baseurl), "http", NULL, server_name, atoi(server_port), "/rss%s", resource); } /* * Load the previous RSS file, if any... */ load_rss(rss, filename); changed = cupsArrayCount(rss) == 0; /* * Localize for the user's chosen language... */ language = cupsLangDefault(); /* * Read events and update the RSS file until we are out of events. */ for (exit_status = 0, event = NULL;;) { if (changed) { /* * Save the messages to the file again, uploading as needed... */ if (save_rss(rss, newname, baseurl)) { if (http) { /* * Upload the RSS file... */ if ((status = cupsPutFile(http, resource, filename)) != HTTP_CREATED) fprintf(stderr, "ERROR: Unable to PUT %s from %s on port %d: %d %s\n", resource, host, port, status, httpStatus(status)); } else { /* * Move the new RSS file over top the old one... */ if (rename(newname, filename)) fprintf(stderr, "ERROR: Unable to rename %s to %s: %s\n", newname, filename, strerror(errno)); } changed = 0; } } /* * Wait up to 30 seconds for an event... */ timeout.tv_sec = 30; timeout.tv_usec = 0; FD_ZERO(&input); FD_SET(0, &input); if (select(1, &input, NULL, NULL, &timeout) < 0) continue; else if (!FD_ISSET(0, &input)) { fprintf(stderr, "DEBUG: %s is bored, exiting...\n", argv[1]); break; } /* * Read the next event... */ event = ippNew(); while ((state = ippReadFile(0, event)) != IPP_DATA) { if (state <= IPP_IDLE) break; } if (state == IPP_ERROR) fputs("DEBUG: ippReadFile() returned IPP_ERROR!\n", stderr); if (state <= IPP_IDLE) break; /* * Collect the info from the event... */ printer_up_time = ippFindAttribute(event, "printer-up-time", IPP_TAG_INTEGER); notify_sequence_number = ippFindAttribute(event, "notify-sequence-number", IPP_TAG_INTEGER); notify_printer_uri = ippFindAttribute(event, "notify-printer-uri", IPP_TAG_URI); subject = cupsNotifySubject(language, event); text = cupsNotifyText(language, event); if (printer_up_time && notify_sequence_number && subject && text) { /* * Create a new RSS message... */ if (notify_printer_uri) { httpSeparateURI(HTTP_URI_CODING_ALL, notify_printer_uri->values[0].string.text, link_scheme, sizeof(link_scheme), link_username, sizeof(link_username), link_host, sizeof(link_host), &link_port, link_resource, sizeof(link_resource)); httpAssembleURI(HTTP_URI_CODING_ALL, link_url, sizeof(link_url), "http", link_username, link_host, link_port, link_resource); } msg = new_message(notify_sequence_number->values[0].integer, xml_escape(subject), xml_escape(text), notify_printer_uri ? xml_escape(link_url) : NULL, printer_up_time->values[0].integer); if (!msg) { fprintf(stderr, "ERROR: Unable to create message: %s\n", strerror(errno)); exit_status = 1; break; } /* * Add it to the array... */ cupsArrayAdd(rss, msg); changed = 1; /* * Trim the array as needed... */ while (cupsArrayCount(rss) > max_events) { msg = cupsArrayFirst(rss); cupsArrayRemove(rss, msg); delete_message(msg); } } if (subject) free(subject); if (text) free(text); ippDelete(event); event = NULL; } /* * We only get here when idle or error... */ ippDelete(event); if (http) { unlink(filename); httpClose(http); } return (exit_status); } /* * 'compare_rss()' - Compare two messages. */ static int /* O - Result of comparison */ compare_rss(_cups_rss_t *a, /* I - First message */ _cups_rss_t *b) /* I - Second message */ { return (a->sequence_number - b->sequence_number); } /* * 'delete_message()' - Free all memory used by a message. */ static void delete_message(_cups_rss_t *msg) /* I - RSS message */ { if (msg->subject) free(msg->subject); if (msg->text) free(msg->text); if (msg->link_url) free(msg->link_url); free(msg); } /* * 'load_rss()' - Load an existing RSS feed file. */ static void load_rss(cups_array_t *rss, /* I - RSS messages */ const char *filename) /* I - File to load */ { FILE *fp; /* File pointer */ char line[4096], /* Line from file */ *subject, /* Subject */ *text, /* Text */ *link_url, /* Link URL */ *start, /* Start of element */ *end; /* End of element */ time_t event_time; /* Event time */ int sequence_number; /* Sequence number */ int in_item; /* In an item */ _cups_rss_t *msg; /* New message */ if ((fp = fopen(filename, "r")) == NULL) { if (errno != ENOENT) fprintf(stderr, "ERROR: Unable to open %s: %s\n", filename, strerror(errno)); return; } subject = NULL; text = NULL; link_url = NULL; event_time = 0; sequence_number = 0; in_item = 0; while (fgets(line, sizeof(line), fp)) { if (strstr(line, "")) in_item = 1; else if (strstr(line, "") && in_item) { if (subject && text) { msg = new_message(sequence_number, subject, text, link_url, event_time); if (msg) cupsArrayAdd(rss, msg); } else { if (subject) free(subject); if (text) free(text); if (link_url) free(link_url); } subject = NULL; text = NULL; link_url = NULL; event_time = 0; sequence_number = 0; in_item = 0; } else if (!in_item) continue; else if ((start = strstr(line, "")) != NULL) { start += 7; if ((end = strstr(start, "")) != NULL) { *end = '\0'; subject = strdup(start); } } else if ((start = strstr(line, "")) != NULL) { start += 13; if ((end = strstr(start, "")) != NULL) { *end = '\0'; text = strdup(start); } } else if ((start = strstr(line, "")) != NULL) { start += 6; if ((end = strstr(start, "")) != NULL) { *end = '\0'; link_url = strdup(start); } } else if ((start = strstr(line, "")) != NULL) { start += 9; if ((end = strstr(start, "")) != NULL) { *end = '\0'; event_time = httpGetDateTime(start); } } else if ((start = strstr(line, "")) != NULL) sequence_number = atoi(start + 6); } if (subject) free(subject); if (text) free(text); if (link_url) free(link_url); fclose(fp); } /* * 'new_message()' - Create a new RSS message. */ static _cups_rss_t * /* O - New message */ new_message(int sequence_number, /* I - notify-sequence-number */ char *subject, /* I - Subject/summary */ char *text, /* I - Text */ char *link_url, /* I - Link to printer */ time_t event_time) /* I - Date/time of event */ { _cups_rss_t *msg; /* New message */ if ((msg = calloc(1, sizeof(_cups_rss_t))) == NULL) { #ifdef __clang_analyzer__ // These free calls are really unnecessary (a failure here ultimately causes // an exit, which frees all memory much faster) but it makes Clang happy... free(subject); free(text); free(link_url); #endif // __clang_analyzer__ return (NULL); } msg->sequence_number = sequence_number; msg->subject = subject; msg->text = text; msg->link_url = link_url; msg->event_time = event_time; return (msg); } /* * 'password_cb()' - Return the cached password. */ static const char * /* O - Cached password */ password_cb(const char *prompt) /* I - Prompt string, unused */ { (void)prompt; return (rss_password); } /* * 'save_rss()' - Save messages to a RSS file. */ static int /* O - 1 on success, 0 on failure */ save_rss(cups_array_t *rss, /* I - RSS messages */ const char *filename, /* I - File to save to */ const char *baseurl) /* I - Base URL */ { FILE *fp; /* File pointer */ _cups_rss_t *msg; /* Current message */ char date[1024]; /* Current date */ char *href; /* Escaped base URL */ if ((fp = fopen(filename, "w")) == NULL) { fprintf(stderr, "ERROR: Unable to create %s: %s\n", filename, strerror(errno)); return (0); } fchmod(fileno(fp), 0644); fputs("\n", fp); fputs("\n", fp); fputs(" \n", fp); fputs(" CUPS RSS Feed\n", fp); href = xml_escape(baseurl); fprintf(fp, " %s\n", href); free(href); fputs(" CUPS RSS Feed\n", fp); fputs(" " CUPS_SVERSION "\n", fp); fputs(" 1\n", fp); fprintf(fp, " %s\n", httpGetDateString2(time(NULL), date, sizeof(date))); for (msg = (_cups_rss_t *)cupsArrayLast(rss); msg; msg = (_cups_rss_t *)cupsArrayPrev(rss)) { char *subject = xml_escape(msg->subject); char *text = xml_escape(msg->text); fputs(" \n", fp); fprintf(fp, " %s\n", subject); fprintf(fp, " %s\n", text); if (msg->link_url) fprintf(fp, " %s\n", msg->link_url); fprintf(fp, " %s\n", httpGetDateString2(msg->event_time, date, sizeof(date))); fprintf(fp, " %d\n", msg->sequence_number); fputs(" \n", fp); free(subject); free(text); } fputs(" \n", fp); fputs("\n", fp); return (!fclose(fp)); } /* * 'xml_escape()' - Copy a string, escaping &, <, and > as needed. */ static char * /* O - Escaped string */ xml_escape(const char *s) /* I - String to escape */ { char *e, /* Escaped string */ *eptr; /* Pointer into escaped string */ const char *sptr; /* Pointer into string */ size_t bytes; /* Bytes needed for string */ /* * First figure out how many extra bytes we need... */ for (bytes = 0, sptr = s; *sptr; sptr ++) if (*sptr == '&') bytes += 4; /* & */ else if (*sptr == '<' || *sptr == '>') bytes += 3; /* < and > */ /* * If there is nothing to escape, just strdup() it... */ if (bytes == 0) return (strdup(s)); /* * Otherwise allocate memory and copy... */ if ((e = malloc(bytes + 1 + strlen(s))) == NULL) return (NULL); for (eptr = e, sptr = s; *sptr; sptr ++) if (*sptr == '&') { *eptr++ = '&'; *eptr++ = 'a'; *eptr++ = 'm'; *eptr++ = 'p'; *eptr++ = ';'; } else if (*sptr == '<') { *eptr++ = '&'; *eptr++ = 'l'; *eptr++ = 't'; *eptr++ = ';'; } else if (*sptr == '>') { *eptr++ = '&'; *eptr++ = 'g'; *eptr++ = 't'; *eptr++ = ';'; } else *eptr++ = *sptr; *eptr = '\0'; return (e); }