blob: fd6a02f213a67fcea873ee0d6e73fa16fc433376 [file] [log] [blame]
// 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 <vector>
#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);
// 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() {
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);
if (connect(fd.get(), reinterpret_cast<const sockaddr*>(&addr),
sizeof(addr)) < 0) {
PLOG(ERROR) << "Unable to connect to " << kIppUsbManagerSocket;
return base::ScopedFD();
}
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}, "_");
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;
}
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.
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 "";
}
// 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';
// 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);
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;
}
} // namespace
base::Optional<std::string> BackendForDevice(const std::string& 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;
}
} // namespace lorgnette