blob: d34080b657ff6ddd76d878ffb10e8e6405bdd3b8 [file] [log] [blame]
// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ash/printing/usb_printer_util.h"
#include <stdint.h>
#include <algorithm>
#include <optional>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#include "base/containers/contains.h"
#include "base/containers/fixed_flat_set.h"
#include "base/containers/span.h"
#include "base/functional/callback_helpers.h"
#include "base/numerics/byte_conversions.h"
#include "base/numerics/safe_conversions.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/grit/generated_resources.h"
#include "chromeos/printing/printer_configuration.h"
#include "chromeos/printing/usb_printer_id.h"
#include "components/device_event_log/device_event_log.h"
#include "crypto/obsolete/md5.h"
#include "services/device/public/cpp/usb/usb_utils.h"
#include "services/device/public/mojom/usb_device.mojom.h"
#include "services/device/public/mojom/usb_enumeration_options.mojom.h"
#include "services/device/public/mojom/usb_manager.mojom.h"
#include "ui/base/l10n/l10n_util.h"
namespace ash {
namespace printing {
crypto::obsolete::Md5 MakeMd5HasherForUsbPrinterUtil() {
return {};
}
} // namespace printing
namespace {
using device::mojom::UsbDeviceInfo;
// Base class used for printer USB interfaces
// (https://www.usb.org/developers/defined_class).
constexpr uint8_t kPrinterInterfaceClass = 7;
// Subclass used for printers
// (http://www.usb.org/developers/docs/devclass_docs/usbprint11a021811.pdf).
constexpr uint8_t kPrinterInterfaceSubclass = 1;
// Protocol for ippusb printing.
// (http://www.usb.org/developers/docs/devclass_docs/IPP.zip).
constexpr uint8_t kPrinterIppusbProtocol = 4;
// Configuration for a GET_DEVICE_ID Printer Class-Specific Request.
const int kGetDeviceIdRequest = 0;
// Generic USB make_and_model strings that are reused across device.
bool IsGenericUsbDescription(const std::string& make_and_model) {
static constexpr auto kGenericUsbModels =
base::MakeFixedFlatSet<std::string_view>({
"canon canon capt usb device",
"epson usb1.1 mfp(hi-speed)",
"epson usb2.0 mfp(hi-speed)",
"epson usb2.0 printer (hi-speed)",
"epson usb mfp",
"epson usb printer",
"seiko epson usb mfp",
"oki data corp usb device",
});
return base::Contains(kGenericUsbModels, make_and_model);
}
// Callback for device.mojom.UsbDevice.ControlTransferIn.
// Expects |data| to hold a newly queried Device ID.
void OnControlTransfer(mojo::Remote<device::mojom::UsbDevice> device,
uint8_t interface_num,
GetDeviceIdCallback cb,
device::mojom::UsbTransferStatus status,
base::span<const uint8_t> data) {
if (status != device::mojom::UsbTransferStatus::COMPLETED || data.empty()) {
return std::move(cb).Run({});
}
// Cleanup device.
device->ReleaseInterface(interface_num, base::DoNothing());
device->Close(base::DoNothing());
return std::move(cb).Run(chromeos::UsbPrinterId(data));
}
// Callback for device.mojom.UsbDevice.SetInterfaceAlternateSetting.
// If interface was set successfully, attempts to query printer for a
// Device ID.
void OnAlternateSelected(mojo::Remote<device::mojom::UsbDevice> device,
const internal::PrinterInterfaceTarget& target,
GetDeviceIdCallback cb,
bool result) {
if (!result) {
PRINTER_LOG(ERROR) << "USB printer " << target.vidpid
<< " failed to set interface "
<< target.interface << " to alternate "
<< target.alternate;
return std::move(cb).Run({});
}
auto params = device::mojom::UsbControlTransferParams::New();
params->type = device::mojom::UsbControlTransferType::CLASS;
params->recipient = device::mojom::UsbControlTransferRecipient::INTERFACE;
params->request = kGetDeviceIdRequest;
params->value = target.config;
params->index = (uint16_t(target.interface) << 8) | (target.alternate);
// Query for IEEE1284 string.
auto* device_raw = device.get();
device_raw->ControlTransferIn(
std::move(params), 255 /* max size */, 2000 /* 2 second timeout */,
base::BindOnce(OnControlTransfer, std::move(device), target.interface,
std::move(cb)));
}
// Callback for device.mojom.UsbDevice.ClaimInterface.
// If interface was claimed successfully, either sets the target alternate or
// continues directly to querying Device ID.
void OnClaimInterface(mojo::Remote<device::mojom::UsbDevice> device,
const internal::PrinterInterfaceTarget& target,
GetDeviceIdCallback cb,
device::mojom::UsbClaimInterfaceResult result) {
if (result != device::mojom::UsbClaimInterfaceResult::kSuccess) {
PRINTER_LOG(ERROR) << "USB printer " << target.vidpid
<< " failed to claim interface "
<< target.interface << ": " << result;
return std::move(cb).Run({});
}
// Set alternate or directly proceed to next step if the interface only has
// one alternate.
if (target.set_alternate) {
auto* device_raw = device.get();
device_raw->SetInterfaceAlternateSetting(
target.interface, target.alternate,
base::BindOnce(OnAlternateSelected, std::move(device),
std::move(target), std::move(cb)));
} else {
OnAlternateSelected(std::move(device), std::move(target), std::move(cb),
true);
}
}
// Callback for device.mojom.UsbDevice.Open.
// If device was opened successfully, attempts to claim printer's default
// interface.
void OnDeviceOpen(mojo::Remote<device::mojom::UsbDevice> device,
const internal::PrinterInterfaceTarget& target,
GetDeviceIdCallback cb,
device::mojom::UsbOpenDeviceResultPtr result) {
if (result->is_error() || !device) {
return std::move(cb).Run({});
}
// Claim target interface.
auto* device_raw = device.get();
device_raw->ClaimInterface(target.interface,
base::BindOnce(OnClaimInterface, std::move(device),
target, std::move(cb)));
}
// Incorporate the byte |val| into the incremental hash carried in |md5|.
void Md5UpdateU8(crypto::obsolete::Md5& md5, base::StrictNumeric<uint8_t> val) {
uint8_t tmp = val;
md5.Update(base::span_from_ref(tmp));
}
// Incorporate |val| into |md5| as a big-endian value.
void Md5UpdateU16BigEndian(crypto::obsolete::Md5& md5,
base::StrictNumeric<uint16_t> val) {
md5.Update(base::U16ToBigEndian(val));
}
// Update the hash with the contents of |str|.
//
// UTF-16 strings are a bit fraught for consistency in memory representation;
// endianness is an issue, but more importantly, there are *optional* prefix
// codepoints to identify the endianness of the string.
//
// This is a long way to say "UTF-16 is hard to hash, let's just convert
// to UTF-8 and hash that", which avoids all of these issues.
void Md5UpdateString16(crypto::obsolete::Md5& md5, const std::u16string& str) {
md5.Update(base::UTF16ToUTF8(str));
}
// Get the usb printer id for |device|. This is used both as the identifier for
// the printer in the user's PrintersManager and as the name of the printer in
// CUPS, so it has to satisfy the naming restrictions of both. CUPS in
// particular is intolerant of much more than [a-z0-9_-], so we use that
// character set. This needs to be stable for a given device, but as unique as
// possible for that device. So we basically toss every bit of stable
// information from the device into an MD5 hash, and then hexify the hash value
// as a suffix to "usb-" as the final printer id.
std::string CreateUsbPrinterId(const UsbDeviceInfo& device_info) {
// Paranoid checks; in the unlikely event someone messes with the USB device
// definition, our (supposedly stable) hashes will change.
static_assert(sizeof(device_info.class_code) == 1, "Class size changed");
static_assert(sizeof(device_info.subclass_code) == 1,
"Subclass size changed");
static_assert(sizeof(device_info.protocol_code) == 1,
"Protocol size changed");
static_assert(sizeof(device_info.vendor_id) == 2, "Vendor id size changed");
static_assert(sizeof(device_info.product_id) == 2, "Product id size changed");
static_assert(sizeof(device::GetDeviceVersion(device_info)) == 2,
"Version size changed");
auto md5 = ash::printing::MakeMd5HasherForUsbPrinterUtil();
Md5UpdateU8(md5, device_info.class_code);
Md5UpdateU8(md5, device_info.subclass_code);
Md5UpdateU8(md5, device_info.protocol_code);
Md5UpdateU16BigEndian(md5, device_info.vendor_id);
Md5UpdateU16BigEndian(md5, device_info.product_id);
Md5UpdateU16BigEndian(md5, device::GetDeviceVersion(device_info));
md5.Update(GetManufacturerName(device_info));
md5.Update(GetProductName(device_info));
Md5UpdateString16(md5, GetSerialNumber(device_info));
return base::StringPrintf("usb-%s",
base::ToLowerASCII(base::HexEncode(md5.Finish())));
}
// Creates a mojom filter which can be used to identify a basic USB printer.
mojo::StructPtr<device::mojom::UsbDeviceFilter> CreatePrinterFilter() {
auto printer_filter = device::mojom::UsbDeviceFilter::New();
printer_filter->has_class_code = true;
printer_filter->class_code = kPrinterInterfaceClass;
printer_filter->has_subclass_code = true;
printer_filter->subclass_code = kPrinterInterfaceSubclass;
return printer_filter;
}
bool UsbDeviceSupportsIppusb(const UsbDeviceInfo& device_info) {
auto printer_filter = CreatePrinterFilter();
printer_filter->has_protocol_code = true;
printer_filter->protocol_code = kPrinterIppusbProtocol;
return device::UsbDeviceFilterMatches(*printer_filter, device_info);
}
// Convert the interesting details of a device to a string, for
// logging/debugging.
std::string UsbPrinterDeviceDetailsAsString(const UsbDeviceInfo& device_info) {
return base::StringPrintf(
" guid: %s\n"
" usb version: %d\n"
" device class: %d\n"
" device subclass: %d\n"
" device protocol: %d\n"
" vendor id: %04x\n"
" product id: %04x\n"
" device version: %d\n"
" manufacturer string: %s\n"
" product string: %s\n"
" serial number: %s",
device_info.guid.c_str(), device::GetUsbVersion(device_info),
device_info.class_code, device_info.subclass_code,
device_info.protocol_code, device_info.vendor_id, device_info.product_id,
device::GetDeviceVersion(device_info),
GetManufacturerName(device_info).c_str(),
GetProductName(device_info).c_str(),
base::UTF16ToUTF8(GetSerialNumber(device_info)).c_str());
}
// Gets the URI CUPS would use to refer to this USB device. Assumes device
// is a printer.
chromeos::Uri UsbPrinterUri(const UsbDeviceInfo& device_info) {
// Note that serial may, for some devices, be empty or bogus (all zeros, non
// unique, or otherwise not really a serial number), but having a non-unique
// or empty serial field in the URI still lets us print, it just means we
// don't have a way to uniquely identify a printer if there are multiple ones
// plugged in with the same VID/PID, so we may print to the *wrong* printer.
// There doesn't seem to be a robust solution to this problem; if printers
// don't supply a serial number, we don't have any reliable way to do that
// differentiation.
std::string serial = base::UTF16ToUTF8(GetSerialNumber(device_info));
chromeos::Uri uri;
uri.SetScheme("usb");
uri.SetHost(base::StringPrintf("%04x", device_info.vendor_id));
uri.SetPath({base::StringPrintf("%04x", device_info.product_id)});
uri.SetQuery({{"serial", serial}});
return uri;
}
} // namespace
namespace internal {
// Find the best interface to send a printer-specific GET_DEVICE_ID request to.
// Not every printer has a printer class interface at alternate 0 of interface
// 0. First, try to find the first interface that does have printer class
// without any alternates. If that fails, select the first interface that has
// printer class on any of the alternates and mark that SET_INTERFACE will need
// to be called.
std::optional<PrinterInterfaceTarget> FindPrinterInterfaceTarget(
const device::mojom::UsbDeviceInfo& device_info) {
std::optional<PrinterInterfaceTarget> candidate;
for (uint8_t config_idx = 0;
const auto& config : device_info.configurations) {
for (uint8_t iface_idx = 0; const auto& iface : config->interfaces) {
for (uint8_t alt_idx = 0; const auto& alt : iface->alternates) {
if (alt->class_code == kPrinterInterfaceClass &&
alt->subclass_code == kPrinterInterfaceSubclass) {
std::string vidpid = base::StringPrintf(
"%04x:%04x", device_info.vendor_id, device_info.product_id);
if (iface->alternates.size() == 1) {
return PrinterInterfaceTarget{
std::move(vidpid), config_idx, iface_idx, alt_idx, false,
};
} else if (!candidate.has_value()) {
candidate.emplace(std::move(vidpid), config_idx, iface_idx, alt_idx,
true);
}
}
alt_idx++;
}
iface_idx++;
}
config_idx++;
}
if (candidate.has_value()) {
return candidate;
}
return std::nullopt;
}
} // namespace internal
std::string GuessEffectiveMakeAndModel(
const device::mojom::UsbDeviceInfo& device_info) {
return base::StrCat(
{GetManufacturerName(device_info), " ", GetProductName(device_info)});
}
std::string GetManufacturerName(const UsbDeviceInfo& device_info) {
return base::UTF16ToUTF8(
device_info.manufacturer_name.value_or(std::u16string()));
}
std::string GetProductName(const UsbDeviceInfo& device_info) {
const std::string manufacturer =
base::StrCat({GetManufacturerName(device_info), " "});
std::string model =
base::UTF16ToUTF8(device_info.product_name.value_or(std::u16string()));
// Some devices have the manufacturer duplicated in the product
// string. This needs to be removed to remain consistent with the make
// and model strings in the ppd index.
if (base::StartsWith(model, manufacturer,
base::CompareCase::INSENSITIVE_ASCII)) {
model.erase(0, manufacturer.size());
}
return model;
}
std::u16string GetSerialNumber(const UsbDeviceInfo& device_info) {
// If the device does not have a serial number or has an empty serial number,
// use '?' so this matches the convention that CUPS uses for 'no serial
// number'.
if (!device_info.serial_number.has_value() ||
device_info.serial_number.value().empty()) {
return u"?";
}
return device_info.serial_number.value();
}
bool UsbDeviceIsPrinter(const UsbDeviceInfo& device_info) {
auto printer_filter = CreatePrinterFilter();
return device::UsbDeviceFilterMatches(*printer_filter, device_info);
}
// Attempt to gather all the information we need to work with this printer by
// querying the USB device. This should only be called using devices we believe
// are printers, not arbitrary USB devices, as we may get weird partial results
// from arbitrary devices. The results are saved in the second parameter.
bool UsbDeviceToPrinter(const UsbDeviceInfo& device_info,
PrinterDetector::DetectedPrinter* entry) {
DCHECK(entry);
// Preflight all required fields and log errors if we find something wrong.
if (device_info.vendor_id == 0 || device_info.product_id == 0) {
LOG(ERROR) << "Failed to convert USB device to printer. Fields were:\n"
<< UsbPrinterDeviceDetailsAsString(device_info);
return false;
}
entry->ppd_search_data.usb_manufacturer = GetManufacturerName(device_info);
entry->ppd_search_data.usb_model = GetProductName(device_info);
const std::string& make = entry->ppd_search_data.usb_manufacturer;
const std::string& model = entry->ppd_search_data.usb_model;
// Synthesize make-and-model string for printer identification.
entry->printer.set_make_and_model(GuessEffectiveMakeAndModel(device_info));
entry->printer.set_display_name(MakeDisplayName(make, model));
entry->printer.set_description(entry->printer.display_name());
entry->printer.SetUri(UsbPrinterUri(device_info));
entry->printer.set_id(CreateUsbPrinterId(device_info));
entry->printer.set_supports_ippusb(UsbDeviceSupportsIppusb(device_info));
return true;
}
void GetDeviceId(const device::mojom::UsbDeviceInfo& device_info,
mojo::Remote<device::mojom::UsbDevice> device,
GetDeviceIdCallback cb) {
// Find printer class interface so the request can be directed to it.
auto target = internal::FindPrinterInterfaceTarget(device_info);
if (!target.has_value()) {
PRINTER_LOG(ERROR) << "No printer interface found for device: "
<< UsbPrinterDeviceDetailsAsString(device_info);
return std::move(cb).Run({});
}
// Open device.
auto* device_raw = device.get();
device_raw->Open(base::BindOnce(OnDeviceOpen, std::move(device),
std::move(target.value()), std::move(cb)));
}
std::string MakeDisplayName(const std::string& make, const std::string& model) {
// Construct the display name by however much of the manufacturer/model
// information that we have available.
if (make.empty() && model.empty()) {
return l10n_util::GetStringUTF8(IDS_USB_PRINTER_UNKNOWN_DISPLAY_NAME);
} else if (!make.empty() && !model.empty()) {
return l10n_util::GetStringFUTF8(IDS_USB_PRINTER_DISPLAY_NAME,
base::UTF8ToUTF16(make),
base::UTF8ToUTF16(model));
} else {
// Exactly one string is present.
DCHECK_NE(make.empty(), model.empty());
return l10n_util::GetStringFUTF8(IDS_USB_PRINTER_DISPLAY_NAME_MAKE_OR_MODEL,
base::UTF8ToUTF16(make + model));
}
}
void UpdateSearchDataFromDeviceId(const chromeos::UsbPrinterId& device_id,
PrinterDetector::DetectedPrinter* printer) {
// If the IEEE1284 device info looks complete and doesn't match the USB
// string descriptors, add an additional PPD search string. In addition, if
// the USB make_and_model is empty or matches known generic strings, replace
// the entire device description with the values from the IEEE1284 info.
const std::string& usb_make = device_id.make();
const std::string& usb_model = device_id.model();
if (usb_make.empty() || usb_model.empty()) {
return;
}
std::string usb_make_and_model = base::StrCat({usb_make, " ", usb_model});
if (base::CompareCaseInsensitiveASCII(printer->printer.make_and_model(),
usb_make_and_model) == 0) {
return;
}
if (base::TrimWhitespaceASCII(printer->printer.make_and_model(),
base::TRIM_ALL)
.empty() ||
IsGenericUsbDescription(
base::ToLowerASCII(printer->printer.make_and_model()))) {
PRINTER_LOG(EVENT) << printer->printer.make_and_model()
<< " replaced with USB device info: "
<< usb_make_and_model;
printer->printer.set_display_name(MakeDisplayName(usb_make, usb_model));
printer->printer.set_description(printer->printer.display_name());
printer->printer.set_make_and_model(usb_make_and_model);
printer->ppd_search_data.make_and_model.front() =
base::ToLowerASCII(usb_make_and_model);
} else {
// Not a generic string, but still add the IEEE 1284 ID as an additional
// possible PPD match.
printer->ppd_search_data.make_and_model.push_back(
base::ToLowerASCII(usb_make_and_model));
}
}
} // namespace ash