/* * Failover Backend for the Common UNIX Printing System (CUPS). * * Copyright (c) 2014, Red Hat, Inc. * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Red Hat, Inc. nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL RED HAT, * INC. BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH * DAMAGE. * * Original version by Clark Hale, Red Hat, Inc. * * This backend presents a fake printer that will choose the first * available printer from a list of IPP URIs. * * Option failover contains a comma separated list of IPP URIs. The * URIs are attempted in-order. * * Option failover-retries contains an integer that indicates how many * times to iterate through the failover list before completely * failing. * * Contents: * main() - Checks each printer in a failover list, and * sends job data to the first available printer * move_job() - Sends and IPP Move-Job request * check_printer() - Checks a printer's attributes to see * if it's enabled and accepting jobs * read_config() - Read the backends configuration from * options * get_printer_attributes() - Sends an IPP Get-Attributes request to * a URI * sigterm_handler() - Handle SIGTERM that cancels the job * password_cb() - Password call back used to disable password * prompt */ #include #include #include #include #include #include #include "backend-private.h" /* * Return Values */ typedef enum fo_state_e { FO_PRINTER_GOOD = 0, FO_PRINTER_BAD, FO_PRINTER_BUSY, FO_AUTH_REQUIRED } fo_state_t; /* * Constants */ #define FAILOVER_DEFAULT_RETRIES (3) #define FAILOVER_PASSWORD_RETRIES_MAX (3) /* * Local Functions */ static int check_printer(const char *device_uri); static int read_config(cups_array_t *printer_array, int *retries, const char *options); static int get_printer_attributes(const char *device_uri, ipp_t **attributes); static int move_job(int jobid, const char *dest); static void sigterm_handler(int sig); static const char *password_cb(const char *); /* * Global Variables */ static int job_canceled = 0; /* Job canceled */ static char *password = NULL; /* password for device */ static int password_retries = 0; static const char *auth_info_required = "none"; /* * 'main()' - Checks each printer in a failover list, and * sends job data to the first available printer * Usage: * printer-uri job-id user title copies options [file] * * The printer-uri option is not used, but it still required to fit * to the backend(7) standards. */ int main(int argc, char *argv[]) { const char *selected_uri = NULL; /* URI of selected printer */ const char *tmp_device_uri; /* Device URI to check */ cups_array_t *printer_array; /* Array of available printers */ int printer_count = 0; /* current printer array index */ int retry_max = 1; /* maximum retries before exit */ int retry_count = 0; /* current retry number */ int auth_failed_count = 0; /* auth failures per loop */ int rc = CUPS_BACKEND_OK; #if defined(HAVE_SIGACTION) && !defined(HAVE_SIGSET) struct sigaction action; /* Actions for POSIX signals */ #endif /* HAVE_SIGACTION && !HAVE_SIGSET */ /* * Check args */ if (argc == 1) { /* * print out discovery data */ char *backendName; if ((backendName = strrchr(argv[0], '/')) != NULL) backendName++; else backendName = argv[0]; _cupsLangPrintf(stderr,"network %s \"Unknown\" \"%s (%s)\"\n", backendName, _cupsLangString(cupsLangDefault(), _("Failover Printer")), backendName); return (CUPS_BACKEND_OK); } else if (argc < 6) { _cupsLangPrintf(stderr, _("Usage: %s job-id user title copies options [file]"), argv[0]); return (CUPS_BACKEND_STOP); } fprintf(stderr, "DEBUG: Failover backend starting up.\n"); /* * Don't buffer status messages */ setbuf(stderr, NULL); /* * Ignore SIGPIPE and catch SIGTERM signals... */ #ifdef HAVE_SIGSET sigset(SIGPIPE, SIG_IGN); sigset(SIGTERM, sigterm_handler); #elif defined(HAVE_SIGACTION) memset(&action, 0, sizeof(action)); action.sa_handler = SIG_IGN; sigaction(SIGPIPE, &action, NULL); sigemptyset(&action.sa_mask); sigaddset(&action.sa_mask, SIGTERM); action.sa_handler = sigterm_handler; sigaction(SIGTERM, &action, NULL); #else signal(SIGPIPE, SIG_IGN); signal(SIGTERM, sigterm_handler); #endif /* HAVE_SIGSET */ printer_array = cupsArrayNew(NULL, NULL); /* * Read Configuration */ if ((rc = read_config(printer_array, &retry_max, argv[5])) != CUPS_BACKEND_OK) { fprintf(stderr, "ERROR: Failed to read configuration options!\n"); goto cleanup; } /* * Main Retry Loop */ for (retry_count = 0; retry_count < retry_max; retry_count++) { fprintf(stderr, "DEBUG: Retry loop #%d\n", retry_count + 1); /* * Reset Counters */ printer_count = 0; auth_failed_count = 0; tmp_device_uri = (char *)cupsArrayFirst(printer_array); do { if (job_canceled) { fprintf(stderr, "DEBUG: Job Canceled\n"); goto cleanup; } fprintf(stderr,"DEBUG: Checking printer #%d: %s\n", printer_count+1, tmp_device_uri); rc = check_printer(tmp_device_uri); // Printer is available and not busy. if ( rc == FO_PRINTER_GOOD ) { selected_uri = tmp_device_uri; break; } // Printer is busy else if (rc == FO_PRINTER_BUSY) { fprintf(stderr, "DEBUG: Waiting for job to complete.\n"); sleep(2); continue; } // Authorization is required to access the printer. else if (rc == FO_AUTH_REQUIRED) { auth_failed_count++; fprintf(stderr, "DEBUG: auth_failed_count = %d\n", auth_failed_count); } // Printer is stopped or not accepting jobs else { if (!printer_count) fprintf(stderr, "INFO: Primary Printer, %s, not available. " "Attempting Failovers...\n", tmp_device_uri); else fprintf(stderr, "INFO: Failover Printer, %s, not available. " "Attempting Failovers..\n", tmp_device_uri); printer_count++; tmp_device_uri = (char *)cupsArrayNext(printer_array); } } while (tmp_device_uri != NULL); if (selected_uri && !printer_count) fprintf(stderr, "STATE: -primary-printer-failed\n"); else fprintf(stderr, "STATE: +primary-printer-failed\n"); if (job_canceled) { fprintf(stderr, "DEBUG: Job Canceled\n"); goto cleanup; } if (!selected_uri && auth_failed_count == printer_count) { fprintf(stderr, "ERROR: All failover printers failed with " "authorization issues.\n"); rc = CUPS_BACKEND_AUTH_REQUIRED; fprintf(stderr, "ATTR: auth-info-required=%s\n", auth_info_required); goto cleanup; } else if (!selected_uri && retry_count + 1 < retry_max) { fprintf(stderr, "INFO: No suitable printer found...retrying...\n"); sleep(2); continue; } else if (selected_uri) { fprintf(stderr, "DEBUG: Using printer, %s.\n", selected_uri); break; } } if (!selected_uri) { fprintf(stderr, "ERROR: No suitable printer found. Aborting print\n"); rc = CUPS_BACKEND_FAILED; goto cleanup; } rc = move_job(atoi(argv[1]), selected_uri); if (job_canceled) rc = CUPS_BACKEND_OK; cleanup : if (job_canceled) rc = CUPS_BACKEND_OK; tmp_device_uri = (char *)cupsArrayFirst(printer_array); do { free((void *)tmp_device_uri); } while ((tmp_device_uri = (char *)cupsArrayNext(printer_array)) != NULL); cupsArrayDelete(printer_array); sleep(2); return (rc); } /* * 'check_printer()' - Checks the status of a remote printer and returns * back a good/bad/busy status. */ int check_printer(const char *device_uri) { ipp_t *attributes = NULL; /* attributes for device_uri */ ipp_attribute_t *tmp_attribute; /* for examining attribs */ int rc = FO_PRINTER_GOOD; /* return code */ char *reason; /* printer state reason */ int i; fprintf(stderr, "DEBUG: Checking printer %s\n",device_uri); rc = get_printer_attributes(device_uri, &attributes); if ( rc != CUPS_BACKEND_OK ) { fprintf(stderr, "DEBUG: Failed to get attributes from printer: %s\n", device_uri); if ( rc == CUPS_BACKEND_AUTH_REQUIRED ) return (FO_AUTH_REQUIRED); else return (FO_PRINTER_BAD); } /* * Check if printer is accepting jobs */ if ((tmp_attribute = ippFindAttribute(attributes, "printer-is-accepting-jobs", IPP_TAG_BOOLEAN)) != NULL && !tmp_attribute->values[0].boolean) { fprintf(stderr, "DEBUG: Printer, %s, is not accepting jobs.\n", device_uri); rc = FO_PRINTER_BAD; } /* * Check if printer is stopped or busy processing */ if ((tmp_attribute = ippFindAttribute(attributes, "printer-state", IPP_TAG_ENUM)) != NULL) { // Printer Stopped if ( tmp_attribute->values[0].integer == IPP_PRINTER_STOPPED ) { fprintf(stderr, "DEBUG: Printer, %s, stopped.\n", device_uri); rc = FO_PRINTER_BAD; } // Printer Busy else if ( tmp_attribute->values[0].integer == IPP_PRINTER_PROCESSING ) { fprintf(stderr, "DEBUG: Printer %s is busy.\n", device_uri); rc = FO_PRINTER_BUSY; } } /* * Parse through the printer-state-reasons */ if ((tmp_attribute = ippFindAttribute(attributes, "printer-state-reasons", IPP_TAG_KEYWORD)) != NULL) { for (i = 0; i < tmp_attribute->num_values; i++) { reason = tmp_attribute->values[i].string.text; int len = strlen(reason); if (len > 8 && !strcmp(reason + len - 8, "-warning")) { fprintf(stderr, "DEBUG: Printer Supply Warning, %s\n", reason); rc = FO_PRINTER_BAD; } else if (len > 6 && !strcmp(reason + len - 6, "-error")) { fprintf(stderr, "DEBUG: Printer Supply Error, %s\n", reason); rc = FO_PRINTER_BAD; } } } return (rc); } /* * 'read_config()' - Parses the failover and failover-retries options * */ static int read_config(cups_array_t *printer_array, int *retries, const char *options) { const char *tmp; /* temporary ptr */ char *tok_tmp; /* temporary ptr for option parsing */ int jobopts_count = 0; /* number of options */ cups_option_t *jobopts = NULL; /* job options */ fprintf(stderr, "DEBUG: Reading Configuration.\n"); jobopts_count = cupsParseOptions(options, 0, &jobopts); if (!jobopts_count) { fprintf(stderr, "ERROR: No job options! Cannot find failover options!\n"); return (CUPS_BACKEND_STOP); } /* * Get attributes from the primary printer */ fprintf(stderr, "DEBUG: Searching for failover option.\n"); if ((tmp = cupsGetOption("failover", jobopts_count, jobopts)) != NULL) { fprintf(stderr, "DEBUG: Failover option contents: %s.\n", tmp); tok_tmp = strdup(tmp); tmp = strtok(tok_tmp, ","); do { cupsArrayAdd(printer_array, strdup(tmp)); } while ((tmp = strtok(NULL,",")) != NULL); free(tok_tmp); } else { /* * The queue is misconfigured, so return back CUPS_BACKEND_STOP */ fprintf(stderr, "ERROR: failover option not specified!\n"); return (CUPS_BACKEND_STOP); } /* * Get the failover-retries value, if it exists. */ fprintf(stderr, "DEBUG: Searching for failover-retries option.\n"); if ((tmp = cupsGetOption("failover-retries", jobopts_count, jobopts)) != NULL) { fprintf(stderr, "DEBUG: failover-retries option contents: %s.\n", tmp); *retries = atoi(tmp); } else { *retries = FAILOVER_DEFAULT_RETRIES; fprintf(stderr, "DEBUG: Failed to get failover-retries option\n"); fprintf(stderr, "DEBUG: Defaulted to %d retries\n", *retries); } return (CUPS_BACKEND_OK); } /* * 'get_printer_attributes()' - Sends an IPP Get-Attributes request to * a URI */ int get_printer_attributes(const char *device_uri, ipp_t **attributes) { char uri[HTTP_MAX_URI]; /* Updated URI without login */ int version; /* IPP version */ char scheme[256]; /* Scheme in URI */ ipp_status_t ipp_status; /* Status of IPP request */ char hostname[1024]; /* Hostname */ char resource[1024]; /* Resource infoo */ char addrname[256]; /* Address name */ int port; /* IPP Port number */ char portname[255]; /* Port as string */ http_t *http; /* HTTP connection */ ipp_t *request; /* IPP request */ int rc = CUPS_BACKEND_OK; /* Return Code */ char username[256]; /* Username for device URI */ char *option_ptr; /* for parsing resource opts */ const char * const pattrs[] = /* Printer attributes wanted */ { "printer-is-accepting-jobs", "printer-state", "printer-state-reasons" }; if (job_canceled) return (CUPS_BACKEND_OK); fprintf(stderr, "DEBUG: Getting Printer Attributes.\n"); fprintf(stderr, "DEBUG: Device URL %s.\n", device_uri); /* * Parse device_uri */ if (httpSeparateURI(HTTP_URI_CODING_ALL, device_uri, scheme, sizeof(scheme), username, sizeof(username), hostname, sizeof(hostname), &port, resource, sizeof(resource)) != HTTP_URI_OK) { fprintf(stderr, "ERROR: Problem parsing device_uri, %s\n", device_uri); return (CUPS_BACKEND_STOP); } if (!port) port = IPP_PORT; sprintf(portname, "%d", port); fprintf(stderr, "DEBUG: Getting Printer Attributes.\n"); /* * Configure password */ cupsSetPasswordCB(password_cb); /* * reset, in case a previous attempt for * another printer left residue */ cupsSetUser(NULL); password = NULL; password_retries = 0; if (*username) { if ((password = strchr(username, ':')) != NULL) { *password = '\0'; password++; } cupsSetUser(username); } else if (!getuid()) { const char *username_env; if ((username_env = getenv("AUTH_USERNAME")) != NULL) { cupsSetUser(username_env); password = getenv("AUTH_PASSWORD"); } } /* * Try connecting to the remote server... */ fprintf(stderr, "DEBUG: Connecting to %s:%d\n", hostname, port); _cupsLangPuts(stderr, _("INFO: Connecting to printer...\n")); http = httpConnectEncrypt(hostname, port, cupsEncryption()); /* * Deal the socket not being open. */ if (!http) { int error = errno; /* Connection error */ switch (error) { case EHOSTDOWN : _cupsLangPuts(stderr, _("WARNING: " "The printer may not exist or " "is unavailable at this time.\n")); break; case EHOSTUNREACH : _cupsLangPuts(stderr, _("WARNING: " "The printer is unreachable at this " "time.\n")); break; case ECONNREFUSED : _cupsLangPuts(stderr, _("WARNING: " "Connection Refused.\n")); break; default : fprintf(stderr, "DEBUG: Connection error: %s\n", strerror(errno)); break; } rc = CUPS_BACKEND_FAILED; sleep(5); goto prt_available_cleanup; } #ifdef AF_INET6 if (http->hostaddr->addr.sa_family == AF_INET6) fprintf(stderr, "DEBUG: Connected to [%s]:%d (IPv6)...\n", httpAddrString(http->hostaddr, addrname, sizeof(addrname)), ntohs(http->hostaddr->ipv6.sin6_port)); else #endif /* AF_INET6 */ if (http->hostaddr->addr.sa_family == AF_INET) fprintf(stderr, "DEBUG: Connected to %s:%d (IPv4)...\n", httpAddrString(http->hostaddr, addrname, sizeof(addrname)), ntohs(http->hostaddr->ipv4.sin_port)); /* * Search the resource string for options. * We only care about version, for the moment. */ version = 11; if ((option_ptr = strchr(resource, '?')) != NULL) { *option_ptr++ = '\0'; if ((option_ptr = strstr(option_ptr, "version="))!=NULL) { int minor; /* minor version from URI */ int major; /* major version from URI */ char *version_str; /* ipp version */ option_ptr += 8; version_str = option_ptr; while (*option_ptr && *option_ptr != '&' && *option_ptr != '+') option_ptr++; if (*option_ptr) *option_ptr = '\0'; sscanf(version_str, "%d.%d", &major, &minor); version = (major * 10) + minor; switch(version) { case 10 : case 11 : case 20 : case 21 : fprintf(stderr, "DEBUG: Set version to %d from URI\n", version); break; default : _cupsLangPrintf(stderr, _("DEBUG: Invalid version, %d, from URI. " "Using default of 1.1 \n"), version); version = 11; } } } /* * Build a URI for the printer. We can't use the URI in argv[0] * because it might contain username:password information... */ if (httpAssembleURI(HTTP_URI_CODING_ALL, uri, sizeof(uri), scheme, NULL, hostname, port, resource) != HTTP_URI_OK) { fprintf(stderr, "ERROR: Problem assembling printer URI from host %s, " "port %d, resource %s\n", hostname, port, resource); return (CUPS_BACKEND_STOP); } /* * Build the IPP request... */ request = ippNewRequest(IPP_GET_PRINTER_ATTRIBUTES); request->request.op.version[0] = version / 10; request->request.op.version[1] = version % 10; ippAddString(request, IPP_TAG_OPERATION, IPP_TAG_URI, "printer-uri", NULL, uri); ippAddStrings(request, IPP_TAG_OPERATION, IPP_TAG_KEYWORD, "requested-attributes", sizeof(pattrs) / sizeof(pattrs[0]), NULL, pattrs); /* * Do the request... */ fputs("DEBUG: Getting supported attributes...\n", stderr); fprintf(stderr, "DEBUG: IPP Request Structure Built.\n"); *attributes = cupsDoRequest(http, request, resource); ipp_status = cupsLastError(); fprintf(stderr, "DEBUG: Get-Printer-Attributes: %s (%s)\n", ippErrorString(ipp_status), cupsLastErrorString()); if (ipp_status > IPP_OK_CONFLICT) { fprintf(stderr, "DEBUG: Get-Printer-Attributes returned %s.\n", ippErrorString(ipp_status)); switch(ipp_status) { case IPP_FORBIDDEN : case IPP_NOT_AUTHORIZED : _cupsLangPuts(stderr, _("ERROR: Not Authorized.\n")); rc = CUPS_BACKEND_AUTH_REQUIRED; break; case IPP_PRINTER_BUSY : case IPP_SERVICE_UNAVAILABLE : _cupsLangPuts(stderr, _("ERROR: " "The printer is not responding.\n")); rc = CUPS_BACKEND_FAILED; break; case IPP_BAD_REQUEST : case IPP_VERSION_NOT_SUPPORTED : fprintf(stderr, "ERROR: Destination does not support IPP version %d\n", version); case IPP_NOT_FOUND : _cupsLangPuts(stderr, _("ERROR: " "The printer configuration is incorrect or the " "printer no longer exists.\n")); rc = CUPS_BACKEND_STOP; break; default : rc = CUPS_BACKEND_FAILED; } goto prt_available_cleanup; } prt_available_cleanup : httpClose(http); return (rc); } static int move_job(int jobid, /* Job ID */ const char *dest) /* Destination ipp address */ { ipp_t *request; /* IPP Request */ char job_uri[HTTP_MAX_URI]; /* job-uri */ http_t* http = httpConnectEncrypt(cupsServer(), ippPort(), cupsEncryption()); if (!http) { _cupsLangPrintf(stderr, _("failover: Unable to connect to server: %s\n"), strerror(errno)); return (CUPS_BACKEND_FAILED); } /* * Build a CUPS_MOVE_JOB request, which requires the following * attributes: * * job-uri/printer-uri * job-printer-uri * requesting-user-name */ request = ippNewRequest(CUPS_MOVE_JOB); snprintf(job_uri, sizeof(job_uri), "ipp://localhost/jobs/%d", jobid); ippAddString(request, IPP_TAG_OPERATION, IPP_TAG_URI, "job-uri", NULL, job_uri); ippAddString(request, IPP_TAG_OPERATION, IPP_TAG_NAME, "requesting-user-name", NULL, cupsUser()); ippAddString(request, IPP_TAG_JOB, IPP_TAG_URI, "job-printer-uri", NULL, dest); /* * Do the request and get back a response... */ ippDelete(cupsDoRequest(http, request, "/jobs")); httpClose(http); if (cupsLastError() > IPP_OK_CONFLICT) { _cupsLangPrintf(stderr, "failover: %s\n", cupsLastErrorString()); return (CUPS_BACKEND_FAILED); } else return (CUPS_BACKEND_OK); } /* * 'sigterm_handler()' - handles a sigterm, i.e. job canceled */ static void sigterm_handler(int sig) { if (!job_canceled) { write(2, "DEBUG: Got SIGTERM.\n", 20); job_canceled = 1; } else { /* * Job has already been canceled, so just exit */ exit(1); } } /* * 'password_cb()' - Disable the password prompt for cupsDoFileRequest(). */ static const char * /* O - Password */ password_cb(const char *prompt) /* I - Prompt (not used) */ { auth_info_required = "username,password"; password_retries++; if(password_retries < FAILOVER_PASSWORD_RETRIES_MAX) return (password); else return (NULL); }