| // Copyright 2020 The Chromium OS Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "lorgnette/ippusb_device.h" |
| |
| #include <memory> |
| |
| #include <libusb.h> |
| #include <sys/socket.h> |
| #include <sys/time.h> |
| #include <sys/types.h> |
| #include <sys/un.h> |
| |
| #include <base/files/file_path.h> |
| #include <base/files/file_util.h> |
| #include <base/files/scoped_file.h> |
| #include <base/logging.h> |
| #include <base/strings/stringprintf.h> |
| #include <base/strings/string_util.h> |
| #include <base/threading/platform_thread.h> |
| #include <base/time/time.h> |
| #include <base/timer/elapsed_timer.h> |
| #include <re2/re2.h> |
| |
| namespace lorgnette { |
| |
| namespace { |
| |
| const char kIppUsbSocketDir[] = "/run/ippusb"; |
| const char kIppUsbManagerSocket[] = "ippusb_manager.sock"; |
| const base::TimeDelta kSocketCreationTimeout = base::TimeDelta::FromSeconds(3); |
| const char kScannerTypeMFP[] = "multi-function peripheral"; // Matches SANE. |
| const uint8_t kIppUsbInterfaceProtocol = 0x04; |
| |
| // Get a file descriptor connected to the ippusb_manager socket. Upstart will |
| // auto-start ippusb_manager if needed, so this should be ready to send messages |
| // as soon as this function is done. Returns an invalid fd if the connection |
| // fails. |
| base::ScopedFD ConnectIppusbManager() { |
| LOG(INFO) << "Creating socket for ippusb_manager"; |
| int raw_fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0); |
| if (raw_fd < 0) { |
| PLOG(ERROR) << "Unable to create AF_UNIX socket"; |
| return base::ScopedFD(); |
| } |
| base::ScopedFD fd(raw_fd); |
| |
| struct sockaddr_un addr; |
| memset(&addr, 0, sizeof(addr)); |
| addr.sun_family = AF_UNIX; |
| snprintf(addr.sun_path, sizeof(addr.sun_path), "%s/%s", kIppUsbSocketDir, |
| kIppUsbManagerSocket); |
| |
| LOG(INFO) << "Connecting ippusb_manager socket to " << addr.sun_path; |
| if (connect(fd.get(), reinterpret_cast<const sockaddr*>(&addr), |
| sizeof(addr)) < 0) { |
| PLOG(ERROR) << "Unable to connect to " << kIppUsbManagerSocket; |
| return base::ScopedFD(); |
| } |
| |
| LOG(INFO) << "Connected to ippusb_manager on fd " << fd.get(); |
| return fd; |
| } |
| |
| // Send a message through |fd| to ippusb_manager requesting the socket name for |
| // the |vid|:|pid| device. ippusb_manager will check and start ippusb_bridge as |
| // needed. |
| // The expected message format is 1 byte of length followed by <vid>_<pid> and a |
| // null byte. |vid| and |pid| should each be 4 hex characters. The length byte |
| // is not included in the length, but the trailing null byte is. |
| bool SendDeviceRequest(int fd, const std::string& vid, const std::string& pid) { |
| std::string payload = base::JoinString({vid, pid}, "_"); |
| LOG(INFO) << "Sending " << payload << " to ippusb_manager on fd " << fd; |
| size_t payload_len = payload.length() + 1; // +1 to include NULL byte. |
| if (payload_len > UINT8_MAX) { |
| LOG(ERROR) << "Message '" << payload << "' is too long for ippusb_manager"; |
| return false; |
| } |
| |
| auto msg = std::vector<char>(payload_len + 1); // +1 for size byte. |
| msg[0] = payload_len; |
| memcpy(msg.data() + 1, payload.c_str(), payload_len); |
| |
| size_t sent = 0; |
| size_t remaining = msg.size(); |
| while (remaining > 0) { |
| ssize_t bytes = |
| HANDLE_EINTR(send(fd, msg.data() + sent, remaining, MSG_NOSIGNAL)); |
| if (bytes < 0) { |
| PLOG(ERROR) << "Failed to send message body"; |
| return false; |
| } |
| sent += bytes; |
| remaining -= bytes; |
| LOG(INFO) << "Sent " << bytes << " bytes"; |
| } |
| LOG(INFO) << "Sent " << sent << " total payload bytes"; |
| |
| return true; |
| } |
| |
| // Read an ippusb_manager response from |fd|. Only the socket name is |
| // returned; the full path can be constructed by looking in kIppUsbSocketDir. |
| // Returns an empty string if the response is not valid. The socket may not yet |
| // exist when the response arrives. |
| // The expected message format is one byte of length followed by the name of an |
| // AF_UNIX socket that can be used to connect to the previously requested |
| // device. The length byte is not included in the length. |
| std::string ReadDeviceResponse(int fd) { |
| // Set a timeout so we don't wait indefinitely if ippusb_manager has crashed |
| // before writing its response. |
| LOG(INFO) << "Reading ippusb_manager response from fd " << fd; |
| struct timeval timeout; |
| memset(&timeout, 0, sizeof(timeout)); |
| timeout.tv_sec = kSocketCreationTimeout.InSeconds(); |
| if (setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)) < 0) { |
| PLOG(ERROR) << "Failed to set socket timeout"; |
| return ""; |
| } |
| |
| uint8_t msg_len; |
| if (HANDLE_EINTR(recv(fd, &msg_len, sizeof(uint8_t), 0)) != sizeof(msg_len)) { |
| PLOG(ERROR) << "Failed to read response length"; |
| return ""; |
| } |
| LOG(INFO) << "Message length is " << msg_len; |
| |
| // It's not clear if ippusb_manager will always include a trailing null in the |
| // response, so append an extra one just in case. |
| auto response = std::vector<char>(msg_len + 1); |
| if (HANDLE_EINTR(recv(fd, response.data(), msg_len, MSG_WAITALL)) != |
| msg_len) { |
| PLOG(ERROR) << "Failed to read response body"; |
| return ""; |
| } |
| response[msg_len] = '\0'; |
| LOG(INFO) << "Response body: " << response.data(); |
| |
| // ippusb_manager will return a socket name of "Device not found" if it can't |
| // find the requested USB device. Validate the path to make sure we don't try |
| // to connect to a string like this. |
| if (!RE2::FullMatch(response.data(), "^[0-9A-Fa-f_-]+\\.sock$")) { |
| LOG(ERROR) << "Socket response (" << response.data() << ") is not valid."; |
| return ""; |
| } |
| |
| return std::string(response.data()); |
| } |
| |
| // ippusb_manager returns a socket path without waiting for ippusb_bridge to |
| // finish starting. This function waits for the expected socket file to appear |
| // in the filesystem. It returns true if that happens, or false if the socket |
| // doesn't appear within |timeout|. |
| bool WaitForSocket(const std::string& sock_name, base::TimeDelta timeout) { |
| base::FilePath socket_path(kIppUsbSocketDir); |
| socket_path = socket_path.Append(sock_name); |
| LOG(INFO) << "Waiting for socket " << socket_path; |
| |
| base::ElapsedTimer timer; |
| while (!base::PathExists(socket_path)) { |
| if (timer.Elapsed() > timeout) { |
| LOG(ERROR) << "Timed out waiting for socket " << socket_path; |
| return false; |
| } |
| |
| base::PlatformThread::Sleep(base::TimeDelta::FromMilliseconds(10)); |
| } |
| |
| return true; |
| } |
| |
| std::string VidPid(const libusb_device_descriptor& descriptor) { |
| return base::StringPrintf("%04x:%04x", descriptor.idVendor, |
| descriptor.idProduct); |
| } |
| |
| // Loop through all altsettings for all interfaces in |config| and return true |
| // if any is a printer interface class that implements the IPP-USB protocol. |
| // Also sets |isPrinter| to true if any interface has the printer class |
| // regardless of whether it supports IPP-USB. |
| bool ContainsIppUsbInterface(const libusb_config_descriptor* config, |
| bool* isPrinter) { |
| for (uint8_t i = 0; i < config->bNumInterfaces; i++) { |
| for (uint8_t j = 0; j < config->interface[i].num_altsetting; j++) { |
| const libusb_interface_descriptor* interface = |
| &config->interface[i].altsetting[j]; |
| |
| if (interface->bInterfaceClass != LIBUSB_CLASS_PRINTER) { |
| continue; |
| } |
| |
| *isPrinter = true; |
| if (interface->bInterfaceProtocol == kIppUsbInterfaceProtocol) { |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| |
| // Create a ScannerInfo protobuf describing |device|, which is presumed to be an |
| // IPP-USB capable printer. The resulting |device_name| member will claim escl |
| // support through the ippusb backend, but this function will not check for |
| // proper support. The caller must connect to the device and probe it before |
| // attempting to scan. |
| base::Optional<ScannerInfo> ScannerInfoForDevice( |
| libusb_device* device, const libusb_device_descriptor& descriptor) { |
| const std::string vid_pid = VidPid(descriptor); |
| |
| libusb_device_handle* h; |
| int status = libusb_open(device, &h); |
| if (status < 0) { |
| LOG(ERROR) << "Failed to open device " << vid_pid << ": " |
| << libusb_error_name(status); |
| return base::nullopt; |
| } |
| auto handle = std::unique_ptr<libusb_device_handle, decltype(&libusb_close)>( |
| h, libusb_close); |
| |
| std::vector<uint8_t> buf(256); |
| int bytes = libusb_get_string_descriptor_ascii( |
| handle.get(), descriptor.iManufacturer, buf.data(), buf.size()); |
| if (bytes < 0) { |
| LOG(ERROR) << "Failed to read manufacturer from device " << vid_pid << ": " |
| << libusb_error_name(bytes); |
| return base::nullopt; |
| } |
| std::string mfgr_name((const char*)buf.data(), bytes); |
| |
| bytes = libusb_get_string_descriptor_ascii(handle.get(), descriptor.iProduct, |
| buf.data(), buf.size()); |
| if (bytes < 0) { |
| LOG(ERROR) << "Failed to read product name from device " << vid_pid << ": " |
| << libusb_error_name(bytes); |
| return base::nullopt; |
| } |
| std::string model_name((const char*)buf.data(), bytes); |
| |
| std::string printer_name; |
| if (base::StartsWith(model_name, mfgr_name, |
| base::CompareCase::INSENSITIVE_ASCII)) { |
| printer_name = model_name; |
| } else { |
| printer_name = mfgr_name + " " + model_name; |
| } |
| |
| std::string device_name = |
| base::StringPrintf("ippusb:escl:%s:%04x_%04x/eSCL/", printer_name.c_str(), |
| descriptor.idVendor, descriptor.idProduct); |
| LOG(INFO) << "Adding " << device_name << " to possible IPP-USB scanners."; |
| ScannerInfo info; |
| info.set_name(device_name); |
| info.set_manufacturer(mfgr_name); |
| info.set_model(model_name); |
| info.set_type(kScannerTypeMFP); // Printer that can scan == MFP. |
| return info; |
| } |
| |
| // Check if |device| is a printer that supports IPP-USB and return a ScannerInfo |
| // proto if it is. |
| base::Optional<ScannerInfo> CheckUsbDevice(libusb_device* device) { |
| libusb_device_descriptor descriptor; |
| int status = libusb_get_device_descriptor(device, &descriptor); |
| if (status < 0) { |
| LOG(WARNING) << "Failed to get device descriptor: " |
| << libusb_error_name(status); |
| return base::nullopt; |
| } |
| const std::string vid_pid = VidPid(descriptor); |
| |
| // Printers always have a printer class interface defined. They don't define |
| // a top-level device class. |
| if (descriptor.bDeviceClass != LIBUSB_CLASS_PER_INTERFACE) { |
| return base::nullopt; |
| } |
| |
| bool isPrinter = false; |
| bool isIppUsb = false; |
| for (uint8_t c = 0; c < descriptor.bNumConfigurations; c++) { |
| libusb_config_descriptor* config; |
| status = libusb_get_config_descriptor(device, c, &config); |
| if (status < 0) { |
| LOG(ERROR) << "Failed to get config descriptor " << c << " for device " |
| << vid_pid << ": " << libusb_error_name(status); |
| continue; |
| } |
| |
| isIppUsb = ContainsIppUsbInterface(config, &isPrinter); |
| |
| libusb_free_config_descriptor(config); |
| if (isIppUsb) { |
| break; |
| } |
| } |
| if (isPrinter && !isIppUsb) { |
| LOG(INFO) << "Device " << vid_pid << " is a printer without IPP-USB"; |
| } |
| if (!isIppUsb) { |
| return base::nullopt; |
| } |
| |
| return ScannerInfoForDevice(device, descriptor); |
| } |
| |
| } // namespace |
| |
| base::Optional<std::string> BackendForDevice(const std::string& device_name) { |
| LOG(INFO) << "Finding real backend for device: " << device_name; |
| std::string protocol, name, vid, pid, path; |
| if (!RE2::FullMatch( |
| device_name, |
| "ippusb:([^:]+):([^:]+):([0-9A-Fa-f]{4})_([0-9A-Fa-f]{4})(/.*)", |
| &protocol, &name, &vid, &pid, &path)) { |
| return base::nullopt; |
| } |
| |
| base::ScopedFD fd = ConnectIppusbManager(); |
| if (!fd.is_valid()) { |
| return base::nullopt; |
| } |
| if (!SendDeviceRequest(fd.get(), vid, pid)) { |
| return base::nullopt; |
| } |
| std::string socket = ReadDeviceResponse(fd.get()); |
| if (socket.empty()) { |
| return base::nullopt; |
| } |
| if (!WaitForSocket(socket, kSocketCreationTimeout)) { |
| return base::nullopt; |
| } |
| |
| std::string real_device = |
| base::StringPrintf("airscan:%s:%s:unix://%s%s", protocol.c_str(), |
| name.c_str(), socket.c_str(), path.c_str()); |
| return real_device; |
| } |
| |
| std::vector<ScannerInfo> FindIppUsbDevices() { |
| libusb_context* ctx; |
| int status = libusb_init(&ctx); |
| if (status != 0) { |
| LOG(ERROR) << "Failed to initialize libusb: " << libusb_error_name(status); |
| return {}; |
| } |
| auto context = |
| std::unique_ptr<libusb_context, decltype(&libusb_exit)>(ctx, libusb_exit); |
| |
| libusb_device** dev_list; |
| ssize_t num_devices = libusb_get_device_list(context.get(), &dev_list); |
| if (num_devices < 0) { |
| LOG(ERROR) << "Failed to enumerate USB devices: " |
| << libusb_error_name(num_devices); |
| return {}; |
| } |
| |
| std::vector<ScannerInfo> scanners; |
| for (ssize_t i = 0; i < num_devices; i++) { |
| base::Optional<ScannerInfo> info = CheckUsbDevice(dev_list[i]); |
| if (info.has_value()) { |
| scanners.push_back(info.value()); |
| } |
| } |
| |
| libusb_free_device_list(dev_list, 1); |
| return scanners; |
| } |
| |
| } // namespace lorgnette |