Files
Alexander Pevzner 25d6ee1e82 eSCL quirks for Xerox B205/B215
These devices has a lot of bugs:
  - HTTP statuses 404/410 may indicate temporary unavailability
    (normally, HTTP 503 used for this purpose)
  - Host name part broken in the Location: header when using IPv6
    (Location: http://[fe80/eSCL/ScanJobs/4060, notice unbalanced
    square bracket)
  - Very long HTTP DELETE timeout after the job completion over
    the IPP-USB

Appropriate quirks were added, so now these devices seem to work
correctly when used both via the network and via IPP over USB.
2025-04-15 17:08:36 +03:00

1345 lines
42 KiB
C

/* AirScan (a.k.a. eSCL) backend for SANE
*
* Copyright (C) 2019 and up by Alexander Pevzner (pzz@apevzner.com)
* See LICENSE for license terms and conditions
*
* ESCL protocol handler
*/
#include "airscan.h"
/******************** Protocol constants ********************/
/* If HTTP 503 reply is received, how many retry attempts
* to perform before giving up
*
* ESCL_RETRY_ATTEMPTS_LOAD - for NextDocument request
* ESCL_RETRY_ATTEMPTS - for other requests
*
* Note, some printers (namely, HP LaserJet MFP M28w) require
* a lot of retry attempts when loading next page at high res
*/
#define ESCL_RETRY_ATTEMPTS_LOAD 30
#define ESCL_RETRY_ATTEMPTS 10
/* And pause between retries, in milliseconds
*/
#define ESCL_RETRY_PAUSE 1000
/* Some devices (namely, Brother MFC-L2710DW) erroneously returns
* HTTP 404 Not Found when scanning from ADF, if next LOAD request
* send immediately after completion the previous one, and ScannerStatus
* returns ScannerAdfEmpty at this case, which leads to premature
* scan job termination with SANE_STATUS_NO_DOCS status
*
* Introducing a small delay between subsequent LOAD requests solves
* this problem.
*
* To avoid performance regression on a very fast scanners, this
* delay is limited to some fraction of the preceding LOAD
* query time
*
* ESCL_NEXT_LOAD_DELAY - delay between LOAD requests, milliseconds
* ESCL_NEXT_LOAD_DELAY_MAX - upper limit of this delay, as a fraction
* of a previous LOAD time
*/
#define ESCL_NEXT_LOAD_DELAY 1000
#define ESCL_NEXT_LOAD_DELAY_MAX 0.5
/* proto_handler_escl represents eSCL protocol handler
*/
typedef struct {
proto_handler proto; /* Base class */
/* Miscellaneous flags */
bool quirk_localhost; /* Set Host: localhost in ScanJobs rq */
bool quirk_canon_mf410_series; /* Canon MF410 Series */
bool quirk_port_in_host; /* Always set port in Host: header */
bool quirk_next_load_delay; /* Use ESCL_NEXT_LOAD_DELAY */
bool quirk_retry_on_404; /* Retry GET NextDocunemt on HTTP 404 */
bool quirk_retry_on_410; /* Retry GET NextDocunemt on HTTP 410 */
bool quirk_broken_ipv6_location; /* Invalid hostname in IPv6 Location: */
bool quirk_skip_cleanup; /* Don't cleanup after normal operations*/
} proto_handler_escl;
/* XML namespace for XML writer
*/
static const xml_ns escl_xml_wr_ns[] = {
{"pwg", "http://www.pwg.org/schemas/2010/12/sm"},
{"scan", "http://schemas.hp.com/imaging/escl/2011/05/03"},
{NULL, NULL}
};
/******************** Miscellaneous types ********************/
/* escl_scanner_status represents decoded ScannerStatus response
*/
typedef struct {
SANE_Status device_status; /* <pwg:State>XXX</pwg:State> */
SANE_Status adf_status; /* <scan:AdfState>YYY</scan:AdfState> */
} escl_scanner_status;
/******************** Forward declarations ********************/
static error
escl_parse_scanner_status (const proto_ctx *ctx,
const char *xml_text, size_t xml_len, escl_scanner_status *out);
/******************** HTTP utility functions ********************/
/* Create HTTP query
*/
static http_query*
escl_http_query (const proto_ctx *ctx, const char *path,
const char *method, char *body)
{
proto_handler_escl *escl = (proto_handler_escl*) ctx->proto;
http_query *query = http_query_new_relative(ctx->http, ctx->base_uri, path,
method, body, "text/xml");
if (escl->quirk_port_in_host) {
http_query_force_port(query, true);
}
return query;
}
/* Create HTTP get query
*/
static http_query*
escl_http_get (const proto_ctx *ctx, const char *path)
{
return escl_http_query(ctx, path, "GET", NULL);
}
/******************** Device Capabilities ********************/
/* Parse color modes
*/
static error
escl_devcaps_source_parse_color_modes (xml_rd *xml, devcaps_source *src)
{
src->colormodes = 0;
xml_rd_enter(xml);
for (; !xml_rd_end(xml); xml_rd_next(xml)) {
if(xml_rd_node_name_match(xml, "scan:ColorMode")) {
const char *v = xml_rd_node_value(xml);
if (!strcmp(v, "BlackAndWhite1")) {
src->colormodes |= 1 << ID_COLORMODE_BW1;
} else if (!strcmp(v, "Grayscale8")) {
src->colormodes |= 1 << ID_COLORMODE_GRAYSCALE;
} else if (!strcmp(v, "RGB24")) {
src->colormodes |= 1 << ID_COLORMODE_COLOR;
}
}
}
xml_rd_leave(xml);
return NULL;
}
/* Parse document formats
*/
static error
escl_devcaps_source_parse_document_formats (xml_rd *xml, devcaps_source *src)
{
xml_rd_enter(xml);
for (; !xml_rd_end(xml); xml_rd_next(xml)) {
unsigned int flags = 0;
if(xml_rd_node_name_match(xml, "pwg:DocumentFormat")) {
flags |= DEVCAPS_SOURCE_PWG_DOCFMT;
}
if(xml_rd_node_name_match(xml, "scan:DocumentFormatExt")) {
flags |= DEVCAPS_SOURCE_SCAN_DOCFMT_EXT;
}
if (flags != 0) {
const char *v = xml_rd_node_value(xml);
ID_FORMAT fmt = id_format_by_mime_name(v);
if (fmt != ID_FORMAT_UNKNOWN) {
src->formats |= 1 << fmt;
src->flags |= flags;
}
}
}
xml_rd_leave(xml);
return NULL;
}
/* Parse discrete resolutions.
*/
static error
escl_devcaps_source_parse_discrete_resolutions (xml_rd *xml,
devcaps_source *src)
{
error err = NULL;
sane_word_array_reset(&src->resolutions);
xml_rd_enter(xml);
for (; err == NULL && !xml_rd_end(xml); xml_rd_next(xml)) {
if (xml_rd_node_name_match(xml, "scan:DiscreteResolution")) {
SANE_Word x = 0, y = 0;
xml_rd_enter(xml);
for (; err == NULL && !xml_rd_end(xml); xml_rd_next(xml)) {
if (xml_rd_node_name_match(xml, "scan:XResolution")) {
err = xml_rd_node_value_uint(xml, &x);
} else if (xml_rd_node_name_match(xml,
"scan:YResolution")) {
err = xml_rd_node_value_uint(xml, &y);
}
}
xml_rd_leave(xml);
if (x && y && x == y) {
src->resolutions = sane_word_array_append(src->resolutions, x);
}
}
}
xml_rd_leave(xml);
if (sane_word_array_len(src->resolutions) > 0) {
src->flags |= DEVCAPS_SOURCE_RES_DISCRETE;
sane_word_array_sort(src->resolutions);
}
return err;
}
/* Parse resolutions range
*/
static error
escl_devcaps_source_parse_resolutions_range (xml_rd *xml, devcaps_source *src)
{
error err = NULL;
SANE_Range range_x = {0, 0, 0}, range_y = {0, 0, 0};
xml_rd_enter(xml);
for (; err == NULL && !xml_rd_end(xml); xml_rd_next(xml)) {
SANE_Range *range = NULL;
if (xml_rd_node_name_match(xml, "scan:XResolution")) {
range = &range_x;
} else if (xml_rd_node_name_match(xml, "scan:XResolution")) {
range = &range_y;
}
if (range != NULL) {
xml_rd_enter(xml);
for (; err == NULL && !xml_rd_end(xml); xml_rd_next(xml)) {
if (xml_rd_node_name_match(xml, "scan:Min")) {
err = xml_rd_node_value_uint(xml, &range->min);
} else if (xml_rd_node_name_match(xml, "scan:Max")) {
err = xml_rd_node_value_uint(xml, &range->max);
} else if (xml_rd_node_name_match(xml, "scan:Step")) {
err = xml_rd_node_value_uint(xml, &range->quant);
}
}
xml_rd_leave(xml);
}
}
xml_rd_leave(xml);
if (range_x.min > range_x.max) {
err = ERROR("Invalid scan:XResolution range");
goto DONE;
}
if (range_y.min > range_y.max) {
err = ERROR("Invalid scan:YResolution range");
goto DONE;
}
/* If no quantization value, SANE uses 0, not 1
*/
if (range_x.quant == 1) {
range_x.quant = 0;
}
if (range_y.quant == 1) {
range_y.quant = 0;
}
/* Try to merge x/y ranges */
if (!math_range_merge(&src->res_range, &range_x, &range_y)) {
err = ERROR("Incompatible scan:XResolution and "
"scan:YResolution ranges");
goto DONE;
}
src->flags |= DEVCAPS_SOURCE_RES_RANGE;
DONE:
return err;
}
/* Parse supported resolutions.
*/
static error
escl_devcaps_source_parse_resolutions (xml_rd *xml, devcaps_source *src)
{
error err = NULL;
xml_rd_enter(xml);
for (; err == NULL && !xml_rd_end(xml); xml_rd_next(xml)) {
if (xml_rd_node_name_match(xml, "scan:DiscreteResolutions")) {
err = escl_devcaps_source_parse_discrete_resolutions(xml, src);
} else if (xml_rd_node_name_match(xml, "scan:ResolutionRange")) {
err = escl_devcaps_source_parse_resolutions_range(xml, src);
}
}
xml_rd_leave(xml);
/* Prefer discrete resolution, if both are provided */
if (src->flags & DEVCAPS_SOURCE_RES_DISCRETE) {
src->flags &= ~DEVCAPS_SOURCE_RES_RANGE;
}
return err;
}
/* Parse setting profiles (color modes, document formats etc).
*/
static error
escl_devcaps_source_parse_setting_profiles (xml_rd *xml, devcaps_source *src)
{
error err = NULL;
/* Parse setting profiles */
xml_rd_enter(xml);
for (; err == NULL && !xml_rd_end(xml); xml_rd_next(xml)) {
if (xml_rd_node_name_match(xml, "scan:SettingProfile")) {
xml_rd_enter(xml);
for (; err == NULL && !xml_rd_end(xml); xml_rd_next(xml)) {
if (xml_rd_node_name_match(xml, "scan:ColorModes")) {
err = escl_devcaps_source_parse_color_modes(xml, src);
} else if (xml_rd_node_name_match(xml,
"scan:DocumentFormats")) {
err = escl_devcaps_source_parse_document_formats(xml, src);
} else if (xml_rd_node_name_match(xml,
"scan:SupportedResolutions")) {
err = escl_devcaps_source_parse_resolutions(xml, src);
}
}
xml_rd_leave(xml);
}
}
xml_rd_leave(xml);
/* Validate results */
if (err == NULL) {
src->colormodes &= DEVCAPS_COLORMODES_SUPPORTED;
if (src->colormodes == 0) {
return ERROR("no color modes detected");
}
src->formats &= DEVCAPS_FORMATS_SUPPORTED;
if (src->formats == 0) {
return ERROR("no image formats detected");
}
if (!(src->flags & (DEVCAPS_SOURCE_RES_DISCRETE|
DEVCAPS_SOURCE_RES_RANGE))){
return ERROR("scan resolutions are not defined");
}
}
return err;
}
/* Parse supported intents (photo/document etc).
*/
static error
escl_devcaps_source_parse_supported_intents (xml_rd *xml, devcaps_source *src)
{
error err = NULL;
xml_rd_enter(xml);
for (; !xml_rd_end(xml); xml_rd_next(xml)) {
if(xml_rd_node_name_match(xml, "scan:SupportedIntent") ||
xml_rd_node_name_match(xml, "scan:Intent")) {
const char *v = xml_rd_node_value(xml);
if (!strcmp(v, "Document")) {
src->scanintents |= 1 << ID_SCANINTENT_DOCUMENT;
} else if (!strcmp(v, "TextAndGraphic")) {
src->scanintents |= 1 << ID_SCANINTENT_TEXTANDGRAPHIC;
} else if (!strcmp(v, "Photo")) {
src->scanintents |= 1 << ID_SCANINTENT_PHOTO;
} else if (!strcmp(v, "Preview")) {
src->scanintents |= 1 << ID_SCANINTENT_PREVIEW;
} else if (!strcmp(v, "Object")) {
src->scanintents |= 1 << ID_SCANINTENT_OBJECT;
} else if (!strcmp(v, "BusinessCard")) {
src->scanintents |= 1 << ID_SCANINTENT_BUSINESSCARD;
} else {
log_debug(NULL, "unknown intent: %s", v);
}
}
}
xml_rd_leave(xml);
return err;
}
/* Parse ADF justification
*/
static void
escl_devcaps_parse_justification (xml_rd *xml,
ID_JUSTIFICATION *x, ID_JUSTIFICATION *y)
{
xml_rd_enter(xml);
*x = *y = ID_JUSTIFICATION_UNKNOWN;
for (; !xml_rd_end(xml); xml_rd_next(xml)) {
if(xml_rd_node_name_match(xml, "pwg:XImagePosition")){
const char *v = xml_rd_node_value(xml);
if (!strcmp(v, "Right")){
*x = ID_JUSTIFICATION_RIGHT;
} else if (!strcmp(v, "Center")) {
*x = ID_JUSTIFICATION_CENTER;
} else if (!strcmp(v, "Left")) {
*x = ID_JUSTIFICATION_LEFT;
}
} else if(xml_rd_node_name_match(xml, "pwg:YImagePosition")){
const char *v = xml_rd_node_value(xml);
if (!strcmp(v, "Top")){
*y = ID_JUSTIFICATION_TOP;
} else if (!strcmp(v, "Center")) {
*y = ID_JUSTIFICATION_CENTER;
} else if (!strcmp(v, "Bottom")) {
*y = ID_JUSTIFICATION_BOTTOM;
}
}
}
xml_rd_leave(xml);
}
/* Parse source capabilities. Returns NULL on success, error string otherwise
*/
static error
escl_devcaps_source_parse (xml_rd *xml, devcaps_source **out)
{
devcaps_source *src = devcaps_source_new();
error err = NULL;
xml_rd_enter(xml);
for (; err == NULL && !xml_rd_end(xml); xml_rd_next(xml)) {
if(xml_rd_node_name_match(xml, "scan:MinWidth")) {
err = xml_rd_node_value_uint(xml, &src->min_wid_px);
} else if (xml_rd_node_name_match(xml, "scan:MaxWidth")) {
err = xml_rd_node_value_uint(xml, &src->max_wid_px);
} else if (xml_rd_node_name_match(xml, "scan:MinHeight")) {
err = xml_rd_node_value_uint(xml, &src->min_hei_px);
} else if (xml_rd_node_name_match(xml, "scan:MaxHeight")) {
err = xml_rd_node_value_uint(xml, &src->max_hei_px);
} else if (xml_rd_node_name_match(xml, "scan:SettingProfiles")) {
err = escl_devcaps_source_parse_setting_profiles(xml, src);
} else if (xml_rd_node_name_match(xml, "scan:SupportedIntents")) {
err = escl_devcaps_source_parse_supported_intents(xml, src);
}
}
xml_rd_leave(xml);
if (err != NULL) {
goto DONE;
}
if (src->max_wid_px != 0 && src->max_hei_px != 0)
{
/* Validate window size */
if (src->min_wid_px > src->max_wid_px)
{
err = ERROR("Invalid scan:MinWidth or scan:MaxWidth");
goto DONE;
}
if (src->min_hei_px > src->max_hei_px)
{
err = ERROR("Invalid scan:MinHeight or scan:MaxHeight");
goto DONE;
}
src->flags |= DEVCAPS_SOURCE_HAS_SIZE;
/* Set window ranges */
src->win_x_range_mm.min = src->win_y_range_mm.min = 0;
src->win_x_range_mm.max = math_px2mm_res(src->max_wid_px, 300);
src->win_y_range_mm.max = math_px2mm_res(src->max_hei_px, 300);
}
DONE:
if (err != NULL) {
devcaps_source_free(src);
} else {
if (*out == NULL) {
*out = src;
} else {
/* Duplicate detected. Ignored for now */
devcaps_source_free(src);
}
}
return err;
}
/* Parse compression factor parameters
*/
static error
escl_devcaps_compression_parse (xml_rd *xml, devcaps *caps)
{
for (; !xml_rd_end(xml); xml_rd_next(xml)) {
error err = NULL;
if (xml_rd_node_name_match(xml, "scan:Min")) {
err = xml_rd_node_value_uint(xml, &caps->compression_range.min);
} else if (xml_rd_node_name_match(xml, "scan:Max")) {
err = xml_rd_node_value_uint(xml, &caps->compression_range.max);
} else if (xml_rd_node_name_match(xml, "scan:Step")) {
err = xml_rd_node_value_uint(xml, &caps->compression_range.quant);
} else if (xml_rd_node_name_match(xml, "scan:Normal")) {
err = xml_rd_node_value_uint(xml, &caps->compression_norm);
}
if (err != NULL) {
return err;
}
}
/* Validate obtained parameters.
*
* Note, errors are silently ignored starting from this point
*/
if (caps->compression_range.min > caps->compression_range.max) {
return NULL;
}
if (caps->compression_norm < caps->compression_range.min ||
caps->compression_norm > caps->compression_range.max) {
return NULL;
}
caps->compression_ok = true;
return NULL;
}
/* Parse device capabilities. devcaps structure must be initialized
* before calling this function.
*/
static error
escl_devcaps_parse (proto_handler_escl *escl,
devcaps *caps, const char *xml_text, size_t xml_len)
{
error err = NULL;
xml_rd *xml;
bool quirk_canon_iR2625_2630 = false;
ID_SOURCE id_src;
bool src_ok = false;
/* Fill "constant" part of device capabilities */
caps->units = 300;
caps->protocol = escl->proto.name;
caps->justification_x = caps->justification_y = ID_JUSTIFICATION_UNKNOWN;
/* Parse capabilities XML */
err = xml_rd_begin(&xml, xml_text, xml_len, NULL);
if (err != NULL) {
goto DONE;
}
if (!xml_rd_node_name_match(xml, "scan:ScannerCapabilities")) {
err = ERROR("XML: missed scan:ScannerCapabilities");
goto DONE;
}
xml_rd_enter(xml);
for (; !xml_rd_end(xml); xml_rd_next(xml)) {
if (xml_rd_node_name_match(xml, "pwg:MakeAndModel")) {
const char *m = xml_rd_node_value(xml);
if (!strcmp(m, "Canon iR2625/2630")) {
quirk_canon_iR2625_2630 = true;
} else if (!strcmp(m, "HP LaserJet MFP M630")) {
escl->quirk_localhost = true;
} else if (!strcmp(m, "HP Color LaserJet FlowMFP M578")) {
escl->quirk_localhost = true;
} else if (!strcmp(m, "MF410 Series")) {
escl->quirk_canon_mf410_series = true;
} else if (!strncasecmp(m, "EPSON ", 6)) {
escl->quirk_port_in_host = true;
} else if (!strncasecmp(m, "Brother ", 8)) {
escl->quirk_next_load_delay = true;
} else if (!strcmp(m, "B205") || !strcmp(m, "B215")) {
/* Xerox B205/B215 machines may indicate temporary
* unavailability of the scanned document (the need
* for retry) using HTTP 404/410 codes. Normally,
* HTTP 503 used for this purpose...
*/
escl->quirk_retry_on_404 = true;
escl->quirk_retry_on_410 = true;
escl->quirk_broken_ipv6_location = true;
escl->quirk_skip_cleanup = true;
}
} else if (xml_rd_node_name_match(xml, "scan:Manufacturer")) {
const char *m = xml_rd_node_value(xml);
if (!strcasecmp(m, "EPSON")) {
escl->quirk_port_in_host = true;
}
} else if (xml_rd_node_name_match(xml, "scan:Platen")) {
xml_rd_enter(xml);
if (xml_rd_node_name_match(xml, "scan:PlatenInputCaps")) {
err = escl_devcaps_source_parse(xml,
&caps->src[ID_SOURCE_PLATEN]);
}
xml_rd_leave(xml);
} else if (xml_rd_node_name_match(xml, "scan:Adf")) {
xml_rd_enter(xml);
while (!xml_rd_end(xml)) {
if (xml_rd_node_name_match(xml, "scan:AdfSimplexInputCaps")) {
err = escl_devcaps_source_parse(xml,
&caps->src[ID_SOURCE_ADF_SIMPLEX]);
} else if (xml_rd_node_name_match(xml,
"scan:AdfDuplexInputCaps")) {
err = escl_devcaps_source_parse(xml,
&caps->src[ID_SOURCE_ADF_DUPLEX]);
}
else if (xml_rd_node_name_match(xml, "scan:Justification")) {
escl_devcaps_parse_justification(xml,
&caps->justification_x, &caps->justification_y);
}
xml_rd_next(xml);
}
xml_rd_leave(xml);
} else if (xml_rd_node_name_match(xml, "scan:CompressionFactorSupport")) {
xml_rd_enter(xml);
err = escl_devcaps_compression_parse(xml, caps);
xml_rd_leave(xml);
}
if (err != NULL) {
goto DONE;
}
}
/* Check that we have at least one source */
for (id_src = (ID_SOURCE) 0; id_src < NUM_ID_SOURCE; id_src ++) {
if (caps->src[id_src] != NULL) {
src_ok = true;
}
}
if (!src_ok) {
err = ERROR("XML: neither platen nor ADF sources detected");
goto DONE;
}
/* Apply quirks, if any */
if (quirk_canon_iR2625_2630) {
/* This device announces resolutions up to 600 DPI,
* but actually doesn't support more that 300
*
* https://oip.manual.canon/USRMA-4209-zz-CSL-2600-enUV/contents/devu-apdx-sys_spec-send.html?search=600
*
* See #57 for details
*/
for (id_src = (ID_SOURCE) 0; id_src < NUM_ID_SOURCE; id_src ++) {
devcaps_source *src = caps->src[id_src];
if (src != NULL &&
/* paranoia: array won't be empty after quirk applied */
sane_word_array_len(src->resolutions) > 0 &&
src->resolutions[1] <= 300) {
sane_word_array_bound(src->resolutions, 0, 300);
}
}
}
DONE:
if (err != NULL) {
devcaps_reset(caps);
}
xml_rd_finish(&xml);
return err;
}
/* Query device capabilities
*/
static http_query*
escl_devcaps_query (const proto_ctx *ctx)
{
return escl_http_get(ctx, "ScannerCapabilities");
}
/* Decode device capabilities
*/
static error
escl_devcaps_decode (const proto_ctx *ctx, devcaps *caps)
{
proto_handler_escl *escl = (proto_handler_escl*) ctx->proto;
http_data *data = http_query_get_response_data(ctx->query);
const char *s;
/* Most of devices that have Server: HP_Compact_Server
* in their HTTP response header, require this quirk
* (see #116)
*/
s = http_query_get_response_header(ctx->query, "server");
if (s != NULL && !strcmp(s, "HP_Compact_Server")) {
escl->quirk_localhost = true;
}
return escl_devcaps_parse(escl, caps, data->bytes, data->size);
}
/* Create pre-scan check query
*/
static http_query*
escl_precheck_query (const proto_ctx *ctx)
{
return escl_http_get(ctx, "ScannerStatus");
}
/* Decode pre-scan check query results
*/
static proto_result
escl_precheck_decode (const proto_ctx *ctx)
{
proto_handler_escl *escl = (proto_handler_escl*) ctx->proto;
proto_result result = {0};
error err = NULL;
escl_scanner_status sts;
bool adf = ctx->params.src == ID_SOURCE_ADF_SIMPLEX ||
ctx->params.src == ID_SOURCE_ADF_DUPLEX;
/* Initialize result to something optimistic :-) */
result.status = SANE_STATUS_GOOD;
result.next = PROTO_OP_SCAN;
/* Decode status */
err = http_query_error(ctx->query);
if (err == NULL) {
http_data *data = http_query_get_response_data(ctx->query);
err = escl_parse_scanner_status(ctx, data->bytes, data->size, &sts);
}
if (err != NULL) {
result.err = err;
result.status = SANE_STATUS_IO_ERROR;
result.next = PROTO_OP_FINISH;
return result;
}
/* Note, the pre-check status is not always reliable, so normally
* we ignore it. However, with Canon MF410 Series attempt to
* scan from empty ADF causes ADF jam error (really, physical!),
* so we must take care
*/
if (escl->quirk_canon_mf410_series) {
if (adf) {
switch (sts.adf_status) {
case SANE_STATUS_JAMMED:
case SANE_STATUS_NO_DOCS:
result.status = sts.adf_status;
result.next = PROTO_OP_FINISH;
default:
break;
}
}
}
return result;
}
/* Fix Location: URL
*
* It replaces host part of uri with the host part from `orig_uri` and
* used for two purposes:
* 1) To fix URL in the Location: header. We don't trust the device
* and only allow it to specify path, not host (host is preserved
* from the `orig_uri` -- the URL used to initiate scanning
* 2) As redirection callback, together with the quirk_localhost.
* At this case, Host: is purposely invalid, which makes
* redirection unreliable (most likely, redirection will
* use "localhost" from the Host: header as the host part
* of URL).
*
* Can be directly used as http_query_onredir() callback
*/
static void
escl_scan_fix_location (void *p, http_uri *uri, const http_uri *orig_uri)
{
(void) p;
http_uri_fix_host(uri, orig_uri, "localhost");
}
/* Initiate scanning
*/
static http_query*
escl_scan_query (const proto_ctx *ctx)
{
proto_handler_escl *escl = (proto_handler_escl*) ctx->proto;
const proto_scan_params *params = &ctx->params;
const char *source = NULL;
const char *colormode = NULL;
const char *scanintent = NULL;
const char *mime = id_format_mime_name(ctx->params.format);
const devcaps_source *src = ctx->devcaps->src[params->src];
bool duplex = false;
http_query *query;
/* Prepare parameters */
switch (params->src) {
case ID_SOURCE_PLATEN: source = "Platen"; duplex = false; break;
case ID_SOURCE_ADF_SIMPLEX: source = "Feeder"; duplex = false; break;
case ID_SOURCE_ADF_DUPLEX: source = "Feeder"; duplex = true; break;
default:
log_internal_error(ctx->log);
}
switch (params->colormode) {
case ID_COLORMODE_COLOR: colormode = "RGB24"; break;
case ID_COLORMODE_GRAYSCALE: colormode = "Grayscale8"; break;
case ID_COLORMODE_BW1: colormode = "BlackAndWhite1"; break;
default:
log_internal_error(ctx->log);
}
switch (params->scanintent) {
case ID_SCANINTENT_UNSET: break;
case ID_SCANINTENT_DOCUMENT: scanintent = "Document"; break;
case ID_SCANINTENT_TEXTANDGRAPHIC: scanintent = "TextAndGraphic"; break;
case ID_SCANINTENT_PHOTO: scanintent = "Photo"; break;
case ID_SCANINTENT_PREVIEW: scanintent = "Preview"; break;
case ID_SCANINTENT_OBJECT: scanintent = "Object"; break;
case ID_SCANINTENT_BUSINESSCARD: scanintent = "BusinessCard"; break;
default:
log_internal_error(ctx->log);
}
/* Build scan request */
xml_wr *xml = xml_wr_begin("scan:ScanSettings", escl_xml_wr_ns);
xml_wr_add_text(xml, "pwg:Version", "2.0");
if (scanintent) {
xml_wr_add_text(xml, "scan:Intent", scanintent);
}
xml_wr_enter(xml, "pwg:ScanRegions");
xml_wr_enter(xml, "pwg:ScanRegion");
xml_wr_add_text(xml, "pwg:ContentRegionUnits",
"escl:ThreeHundredthsOfInches");
xml_wr_add_uint(xml, "pwg:XOffset", params->x_off);
xml_wr_add_uint(xml, "pwg:YOffset", params->y_off);
xml_wr_add_uint(xml, "pwg:Width", params->wid);
xml_wr_add_uint(xml, "pwg:Height", params->hei);
xml_wr_leave(xml); /* pwg:ScanRegion */
xml_wr_leave(xml); /* pwg:ScanRegions */
//xml_wr_add_text(xml, "scan:InputSource", source);
xml_wr_add_text(xml, "pwg:InputSource", source);
if (ctx->devcaps->compression_ok) {
xml_wr_add_uint(xml, "scan:CompressionFactor",
ctx->devcaps->compression_norm);
}
xml_wr_add_text(xml, "scan:ColorMode", colormode);
xml_wr_add_text(xml, "pwg:DocumentFormat", mime);
if ((src->flags & DEVCAPS_SOURCE_SCAN_DOCFMT_EXT) != 0) {
xml_wr_add_text(xml, "scan:DocumentFormatExt", mime);
}
xml_wr_add_uint(xml, "scan:XResolution", params->x_res);
xml_wr_add_uint(xml, "scan:YResolution", params->y_res);
if (params->src != ID_SOURCE_PLATEN) {
xml_wr_add_bool(xml, "scan:Duplex", duplex);
}
/* Send request to device */
query = escl_http_query(ctx, "ScanJobs", "POST",
xml_wr_finish_compact(xml));
/* Kyocera ECOSYS M6526cdn drops TLS connection after sending
* response HTTP headers, but before the body transfer is completed.
*
* As for this request we are only interested in the response
* headers, we can ignore this kind of error
*
* See here for details:
* https://github.com/alexpevzner/sane-airscan/issues/163
*/
http_query_no_need_response_body(query);
/* It's a dirty hack
*
* HP LaserJet MFP M630, HP Color LaserJet FlowMFP M578 and
* probably some other HP devices don't allow eSCL scan, unless
* Host is set to "localhost". It is probably bad and naive attempt
* to enforce some access security.
*
* So here we forcibly set Host to "localhost".
*
* Note, this hack doesn't work with some other printers
* see #92, #98 for details
*/
if (escl->quirk_localhost && !http_uri_is_loopback(ctx->base_uri)) {
http_query_set_request_header(query, "Host", "localhost");
http_query_onredir(query, escl_scan_fix_location);
}
return query;
}
/* Decode result of scan request
*/
static proto_result
escl_scan_decode (const proto_ctx *ctx)
{
proto_handler_escl *escl = (proto_handler_escl*) ctx->proto;
proto_result result = {0};
error err = NULL;
const char *location;
http_uri *uri;
/* Check HTTP status */
if (http_query_status(ctx->query) != HTTP_STATUS_CREATED) {
err = eloop_eprintf("ScanJobs request: unexpected HTTP status %d",
http_query_status(ctx->query));
result.next = PROTO_OP_CHECK;
result.err = err;
return result;
}
/* Obtain location */
location = http_query_get_response_header(ctx->query, "Location");
if (location == NULL || *location == '\0') {
err = eloop_eprintf("ScanJobs request: empty location received");
goto ERROR;
}
/* Fix broken IPv6 location */
if (escl->quirk_broken_ipv6_location) {
/* Xerox B205/B215 return Location that looks like this:
*
* Location: http://[fe80/eSCL/ScanJobs/4060
*
* Note unbalanced square bracket and truncated IPv6 address.
* As we anyway ignore Location: hostname, it is easy to fix...
*/
if (str_has_prefix(location, "http://[") ||
str_has_prefix(location, "https://[")) {
if (strchr(location, ']') == NULL) {
const char *s = strstr(location, "://");
s += 3;
while (*s && *s != '/') {
s ++;
}
if (*s == '/') {
/* Skip URL until /path/...
*/
log_debug(ctx->log, "Broken IPv6 Location: %s", location);
location = s;
log_debug(ctx->log, "Fixed Location: %s", location);
}
}
}
}
/* Validate and save location */
uri = http_uri_new_relative(ctx->base_uri, location, true, false);
if (uri == NULL) {
err = eloop_eprintf("ScanJobs request: invalid location received");
goto ERROR;
}
/* Don't trust hostname in Location, replace it with hostname
* from the ctx->query (which represents scan request)
*
* The initial idea behind this approach is that scanner may
* not have a strong knowledge of its own host name so hostname
* supplied by device may be inaccurate.
*
* At least one device, HP Deskjet 3520 series, has demonstrated
* this behavior when scanning via IPP over USB. Instead of (expected)
* localhost host name, it returns CN26F178X605SZ.03f011b0.hpLedmUSB
*/
http_uri_fix_host(uri, http_query_uri(ctx->query), NULL);
result.data.location = str_dup(http_uri_str(uri));
http_uri_free(uri);
result.next = PROTO_OP_LOAD;
return result;
ERROR:
result.next = PROTO_OP_FINISH;
result.status = SANE_STATUS_IO_ERROR;
result.err = err;
return result;
}
/* Initiate image downloading
*/
static http_query*
escl_load_query (const proto_ctx *ctx)
{
char *url, *sep;
http_query *q;
sep = str_has_suffix(ctx->location, "/") ? "" : "/";
url = str_concat(ctx->location, sep, "NextDocument", NULL);
q = escl_http_get(ctx, url);
mem_free(url);
return q;
}
/* Decode result of image request
*/
static proto_result
escl_load_decode (const proto_ctx *ctx)
{
proto_handler_escl *escl = (proto_handler_escl*) ctx->proto;
proto_result result = {0};
error err = NULL;
timestamp t = 0;
/* Check HTTP status */
err = http_query_error(ctx->query);
if (err != NULL) {
if (ctx->params.src == ID_SOURCE_PLATEN && ctx->images_received > 0) {
result.next = PROTO_OP_CLEANUP;
if (escl->quirk_skip_cleanup) {
result.next = PROTO_OP_FINISH;
}
} else {
result.next = PROTO_OP_CHECK;
result.err = eloop_eprintf("HTTP: %s", ESTRING(err));
}
return result;
}
/* Compute delay until next load */
if (escl->quirk_next_load_delay && ctx->params.src != ID_SOURCE_PLATEN) {
t = timestamp_now() - http_query_timestamp(ctx->query);
t *= ESCL_NEXT_LOAD_DELAY_MAX;
if (t > ESCL_NEXT_LOAD_DELAY) {
t = ESCL_NEXT_LOAD_DELAY;
}
}
/* Fill proto_result */
result.next = PROTO_OP_LOAD;
result.delay = (int) t;
result.data.image = http_data_ref(http_query_get_response_data(ctx->query));
return result;
}
/* Request device status
*/
static http_query*
escl_status_query (const proto_ctx *ctx)
{
return escl_http_get(ctx, "ScannerStatus");
}
/* Parse ScannerStatus response.
*
* Returned SANE_STATUS_UNSUPPORTED means status not understood
*/
static error
escl_parse_scanner_status (const proto_ctx *ctx,
const char *xml_text, size_t xml_len, escl_scanner_status *out)
{
error err = NULL;
xml_rd *xml;
const char *opname = proto_op_name(ctx->op);
escl_scanner_status sts = {SANE_STATUS_UNSUPPORTED,
SANE_STATUS_UNSUPPORTED};
/* Decode XML */
err = xml_rd_begin(&xml, xml_text, xml_len, NULL);
if (err != NULL) {
goto DONE;
}
if (!xml_rd_node_name_match(xml, "scan:ScannerStatus")) {
err = ERROR("XML: missed scan:ScannerStatus");
goto DONE;
}
xml_rd_enter(xml);
for (; !xml_rd_end(xml); xml_rd_next(xml)) {
if (xml_rd_node_name_match(xml, "pwg:State")) {
const char *state = xml_rd_node_value(xml);
if (!strcmp(state, "Idle")) {
sts.device_status = SANE_STATUS_GOOD;
} else if (!strcmp(state, "Processing")) {
sts.device_status = SANE_STATUS_DEVICE_BUSY;
} else if (!strcmp(state, "Testing")) {
/* HP LaserJet MFP M630 warm up */
sts.device_status = SANE_STATUS_DEVICE_BUSY;
} else {
sts.device_status = SANE_STATUS_UNSUPPORTED;
}
} else if (xml_rd_node_name_match(xml, "scan:AdfState")) {
const char *state = xml_rd_node_value(xml);
if (!strcmp(state, "ScannerAdfLoaded")) {
sts.adf_status = SANE_STATUS_GOOD;
} else if (!strcmp(state, "ScannerAdfJam")) {
sts.adf_status = SANE_STATUS_JAMMED;
} else if (!strcmp(state, "ScannerAdfDoorOpen")) {
sts.adf_status = SANE_STATUS_COVER_OPEN;
} else if (!strcmp(state, "ScannerAdfProcessing")) {
/* Kyocera version */
sts.adf_status = SANE_STATUS_NO_DOCS;
} else if (!strcmp(state, "ScannerAdfEmpty")) {
/* Cannon TR4500, EPSON XP-7100 */
sts.adf_status = SANE_STATUS_NO_DOCS;
} else {
sts.adf_status = SANE_STATUS_UNSUPPORTED;
}
}
}
DONE:
xml_rd_finish(&xml);
if (err != NULL) {
log_debug(ctx->log, "%s: %s", opname, ESTRING(err));
} else {
log_debug(ctx->log, "%s: device status: %s",
opname, sane_strstatus(sts.device_status));
log_debug(ctx->log, "%s: ADF status: %s",
opname, sane_strstatus(sts.adf_status));
}
*out = sts;
return err;
}
/* Parse ScannerStatus response.
*
* Returned SANE_STATUS_UNSUPPORTED means status not understood
*/
static SANE_Status
escl_decode_scanner_status (const proto_ctx *ctx,
const char *xml_text, size_t xml_len)
{
escl_scanner_status sts;
error err;
SANE_Status status;
const char *opname = proto_op_name(ctx->op);
err = escl_parse_scanner_status(ctx, xml_text, xml_len, &sts);
if (err != NULL) {
return SANE_STATUS_IO_ERROR;
}
/* Decode Job status */
if (ctx->params.src != ID_SOURCE_PLATEN &&
sts.adf_status != SANE_STATUS_GOOD &&
sts.adf_status != SANE_STATUS_UNSUPPORTED) {
status = sts.adf_status;
} else {
status = sts.device_status;
}
log_debug(ctx->log, "%s: job status: %s", opname, sane_strstatus(status));
return status;
}
/* Decode result of device status request
*/
static proto_result
escl_status_decode (const proto_ctx *ctx)
{
proto_handler_escl *escl = (proto_handler_escl*) ctx->proto;
proto_result result = {0};
error err = NULL;
SANE_Status status;
int max_attempts;
bool temporary = false;
/* Decode status */
err = http_query_error(ctx->query);
if (err != NULL) {
status = SANE_STATUS_IO_ERROR;
goto FAIL;
} else {
http_data *data = http_query_get_response_data(ctx->query);
status = escl_decode_scanner_status(ctx, data->bytes, data->size);
}
/* Now it's time to make a decision */
max_attempts = ESCL_RETRY_ATTEMPTS;
if (ctx->failed_op == PROTO_OP_LOAD) {
max_attempts = ESCL_RETRY_ATTEMPTS_LOAD;
}
switch (ctx->failed_http_status) {
case HTTP_STATUS_SERVICE_UNAVAILABLE:
temporary = true;
break;
case HTTP_STATUS_NOT_FOUND:
if (escl->quirk_retry_on_404) {
temporary = true;
}
break;
case HTTP_STATUS_GONE:
if (escl->quirk_retry_on_410) {
temporary = true;
}
break;
}
if (temporary && ctx->failed_attempt < max_attempts) {
/* Note, some devices may return HTTP 503 error core, meaning
* that it makes sense to come back after small delay
*
* So if status doesn't cleanly indicate any error, lets retry
* several times
*/
bool retry = false;
switch (status) {
case SANE_STATUS_GOOD:
case SANE_STATUS_UNSUPPORTED:
case SANE_STATUS_DEVICE_BUSY:
retry = true;
break;
case SANE_STATUS_NO_DOCS:
/* For some devices SANE_STATUS_NO_DOCS is not
* reliable, if we have reached SANE_STATUS_NO_DOCS
* operation: HTTP 503 may mean "I'm temporary not
* ready, please try again", while ADF sensor
* raises "ADF empty" signal.
*
* So retry at this case
*/
if (ctx->failed_op == PROTO_OP_LOAD) {
retry = true;
}
break;
default:
break;
}
if (retry) {
result.next = ctx->failed_op;
result.delay = ESCL_RETRY_PAUSE;
return result;
}
}
/* If status cannot be cleanly decoded, look to HTTP status */
if (status == SANE_STATUS_GOOD || status == SANE_STATUS_UNSUPPORTED) {
status = SANE_STATUS_IO_ERROR;
switch (ctx->failed_http_status) {
case HTTP_STATUS_SERVICE_UNAVAILABLE:
status = SANE_STATUS_DEVICE_BUSY;
break;
case HTTP_STATUS_NOT_FOUND:
if (ctx->params.src != ID_SOURCE_PLATEN &&
ctx->failed_op == PROTO_OP_LOAD) {
status = SANE_STATUS_NO_DOCS;
}
break;
}
}
/* Fill the result */
FAIL:
result.next = ctx->location ? PROTO_OP_CLEANUP : PROTO_OP_FINISH;
if (escl->quirk_skip_cleanup) {
result.next = PROTO_OP_FINISH;
}
result.status = status;
result.err = err;
return result;
}
/* Cancel scan in progress
*/
static http_query*
escl_cancel_query (const proto_ctx *ctx)
{
return escl_http_query(ctx, ctx->location, "DELETE", NULL);
}
/******************** Test interfaces ********************/
/* Test interface: decode device capabilities
*/
static error
escl_test_decode_devcaps (proto_handler *proto,
const void *xml_text, size_t xms_size,
devcaps *caps)
{
proto_handler_escl *escl = (proto_handler_escl*) proto;
return escl_devcaps_parse(escl, caps, xml_text, xms_size);
}
/******************** Constructor/destructor ********************/
/* Free ESCL protocol handler
*/
static void
escl_free (proto_handler *proto)
{
mem_free(proto);
}
/* proto_handler_escl_new creates new eSCL protocol handler
*/
proto_handler*
proto_handler_escl_new (void)
{
proto_handler_escl *escl = mem_new(proto_handler_escl, 1);
escl->proto.name = "eSCL";
escl->proto.free = escl_free;
escl->proto.devcaps_query = escl_devcaps_query;
escl->proto.devcaps_decode = escl_devcaps_decode;
escl->proto.precheck_query = escl_precheck_query;
escl->proto.precheck_decode = escl_precheck_decode;
escl->proto.scan_query = escl_scan_query;
escl->proto.scan_decode = escl_scan_decode;
escl->proto.load_query = escl_load_query;
escl->proto.load_decode = escl_load_decode;
escl->proto.status_query = escl_status_query;
escl->proto.status_decode = escl_status_decode;
escl->proto.cleanup_query = escl_cancel_query;
escl->proto.cancel_query = escl_cancel_query;
escl->proto.test_decode_devcaps = escl_test_decode_devcaps;
return &escl->proto;
}
/* vim:ts=8:sw=4:et
*/