/*
* 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 <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/wait.h>
#include <cups/http-private.h>
#include <cups/http.h>
#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);
}