Commit f25dad99 authored by Jesse Schettler's avatar Jesse Schettler Committed by Commit Bot

scanning: Create ZeroconfScannerDetector

Create a ZeroconfScannerDetector to detect network scanners using mDNS
and DNS-SD. The detector detects eSCL and eSCLS scanners.

The detector stores information for each detected scanner, but clients
receive a deduplicated list of scanners (i.e. if the same scanner is
reported for more than one service type, the information from each
detection is merged before being sent to clients).

Bug: b:153541027
Change-Id: Ia69d98ca176f2d6d09f8c6db94ce30d8f96b9249
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2307619Reviewed-by: default avatarSean Kau <skau@chromium.org>
Reviewed-by: default avatarZentaro Kavanagh <zentaro@chromium.org>
Reviewed-by: default avatarMitsuru Oshima <oshima@chromium.org>
Commit-Queue: Jesse Schettler <jschettler@chromium.org>
Cr-Commit-Position: refs/heads/master@{#796135}
parent b2e1d317
...@@ -2392,6 +2392,11 @@ source_set("chromeos") { ...@@ -2392,6 +2392,11 @@ source_set("chromeos") {
"remote_apps/remote_apps_model.cc", "remote_apps/remote_apps_model.cc",
"remote_apps/remote_apps_model.h", "remote_apps/remote_apps_model.h",
"reset/metrics.h", "reset/metrics.h",
"scanning/scanner_detector.h",
"scanning/zeroconf_scanner_detector.cc",
"scanning/zeroconf_scanner_detector.h",
"scanning/zeroconf_scanner_detector_utils.cc",
"scanning/zeroconf_scanner_detector_utils.h",
"scheduler_configuration_manager.cc", "scheduler_configuration_manager.cc",
"scheduler_configuration_manager.h", "scheduler_configuration_manager.h",
"secure_channel/secure_channel_client_provider.cc", "secure_channel/secure_channel_client_provider.cc",
...@@ -3419,6 +3424,7 @@ source_set("unit_tests") { ...@@ -3419,6 +3424,7 @@ source_set("unit_tests") {
"release_notes/release_notes_notification_unittest.cc", "release_notes/release_notes_notification_unittest.cc",
"release_notes/release_notes_storage_unittest.cc", "release_notes/release_notes_storage_unittest.cc",
"remote_apps/remote_apps_model_unittest.cc", "remote_apps/remote_apps_model_unittest.cc",
"scanning/zeroconf_scanner_detector_unittest.cc",
"scheduler_configuration_manager_unittest.cc", "scheduler_configuration_manager_unittest.cc",
"session_length_limiter_unittest.cc", "session_length_limiter_unittest.cc",
"settings/cros_settings_unittest.cc", "settings/cros_settings_unittest.cc",
......
file://chromeos/scanning/OWNERS
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CHROME_BROWSER_CHROMEOS_SCANNING_SCANNER_DETECTOR_H_
#define CHROME_BROWSER_CHROMEOS_SCANNING_SCANNER_DETECTOR_H_
#include <vector>
#include "base/callback.h"
#include "chromeos/scanning/scanner.h"
namespace chromeos {
// Interface for automatic scanner detection. This API allows for incremental
// discovery of scanners and provides a notification when discovery is complete.
//
// All of the interface calls in this class must be called from the same
// sequence but do not have to be on any specific thread.
//
// The usual usage of this interface by a class that wants to maintain an
// up-to-date list of detected scanners is:
//
// auto detector_ = ScannerDetectorImplementation::Create();
// detector_->RegisterScannersDetectedCallback(callback);
// scanners_ = detector_->GetScanners();
//
class CHROMEOS_EXPORT ScannerDetector {
public:
virtual ~ScannerDetector() = default;
// Registers the callback used to provide notifications when scanners are
// detected.
using OnScannersDetectedCallback =
base::RepeatingCallback<void(const std::vector<Scanner>& scanners)>;
virtual void RegisterScannersDetectedCallback(
OnScannersDetectedCallback callback) = 0;
// Returns the detected scanners.
virtual std::vector<Scanner> GetScanners() = 0;
};
} // namespace chromeos
#endif // CHROME_BROWSER_CHROMEOS_SCANNING_SCANNER_DETECTOR_H_
// Copyright 2020 The Chromium 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 "chrome/browser/chromeos/scanning/zeroconf_scanner_detector.h"
#include <array>
#include <string>
#include <utility>
#include <vector>
#include "base/check.h"
#include "base/containers/flat_map.h"
#include "base/logging.h"
#include "base/optional.h"
#include "base/sequence_checker.h"
#include "base/strings/string_piece_forward.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "chrome/browser/chromeos/scanning/zeroconf_scanner_detector_utils.h"
#include "chrome/browser/local_discovery/service_discovery_shared_client.h"
#include "chromeos/scanning/scanner.h"
namespace chromeos {
// Supported service types for scanners.
const char ZeroconfScannerDetector::kEsclServiceType[] = "_uscan._tcp.local";
const char ZeroconfScannerDetector::kEsclsServiceType[] = "_uscans._tcp.local";
constexpr std::array<const char*, 2> kServiceTypes = {
ZeroconfScannerDetector::kEsclsServiceType,
ZeroconfScannerDetector::kEsclServiceType,
};
namespace {
using local_discovery::ServiceDescription;
using local_discovery::ServiceDiscoveryDeviceLister;
using local_discovery::ServiceDiscoverySharedClient;
// TODO(jschettler): Update this class once the eSCL specification is released.
// These fields (including the default values) come from the eSCL specification.
// Not all of these will necessarily be specified for a given scanner. Also,
// unused fields are excluded here.
class ParsedMetadata {
public:
explicit ParsedMetadata(const ServiceDescription& service_description) {
for (const std::string& entry : service_description.metadata) {
const base::StringPiece key_value(entry);
const size_t equal_pos = key_value.find("=");
if (equal_pos == base::StringPiece::npos)
continue;
const base::StringPiece key = key_value.substr(0, equal_pos);
const base::StringPiece value = key_value.substr(equal_pos + 1);
if (key == "rs")
rs_ = value.as_string();
}
}
ParsedMetadata(const ParsedMetadata&) = delete;
ParsedMetadata& operator=(const ParsedMetadata&) = delete;
~ParsedMetadata() = default;
const std::string& rs() const { return rs_; }
private:
// Used to construct the path for a device name URL.
std::string rs_ = "none";
};
// Attempts to create a Scanner using the information in |service_description|
// and |metadata|. Returns the Scanner on success, base::nullopt on failure.
base::Optional<Scanner> CreateScanner(
const std::string& service_type,
const ServiceDescription& service_description,
const ParsedMetadata& metadata) {
// If there isn't enough information available to interact with the scanner,
// fail. Also fail if the port number is 0, as this is used to indicate that
// the service doesn't *actually* exist, the device just wants to guard the
// name.
if (service_description.service_name.empty() ||
service_description.ip_address.empty() ||
service_description.address.port() == 0) {
return base::nullopt;
}
return CreateSaneAirscanScanner(
service_description.instance_name(), service_type, metadata.rs(),
service_description.ip_address, service_description.address.port());
}
class ZeroconfScannerDetectorImpl final : public ZeroconfScannerDetector {
public:
// Normal constructor that connects to service discovery.
ZeroconfScannerDetectorImpl()
: discovery_client_(ServiceDiscoverySharedClient::GetInstance()) {}
// Testing constructor that uses injected backends.
explicit ZeroconfScannerDetectorImpl(ListersMap&& device_listers) {
device_listers_ = std::move(device_listers);
for (auto& lister : device_listers_) {
lister.second->Start();
lister.second->DiscoverNewDevices();
}
}
ZeroconfScannerDetectorImpl(const ZeroconfScannerDetectorImpl&) = delete;
ZeroconfScannerDetectorImpl& operator=(const ZeroconfScannerDetectorImpl&) =
delete;
~ZeroconfScannerDetectorImpl() override = default;
// Initializes the detector by creating its device listers.
void Init() {
for (const char* service_type : kServiceTypes)
CreateDeviceLister(service_type);
}
// ScannerDetector:
void RegisterScannersDetectedCallback(
OnScannersDetectedCallback callback) override {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_);
DCHECK(!on_scanners_detected_callback_);
on_scanners_detected_callback_ = std::move(callback);
}
// ScannerDetector:
std::vector<Scanner> GetScanners() override {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_);
return GetDedupedScanners();
}
// ServiceDiscoveryDeviceLister::Delegate:
void OnDeviceChanged(const std::string& service_type,
bool added,
const ServiceDescription& service_description) override {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_);
// Generate an update whether the device was added or not.
ParsedMetadata metadata(service_description);
auto scanner = CreateScanner(service_type, service_description, metadata);
if (!scanner.has_value())
return;
scanners_[service_description.service_name] = scanner.value();
if (on_scanners_detected_callback_)
on_scanners_detected_callback_.Run(GetDedupedScanners());
}
// ServiceDiscoveryDeviceLister::Delegate:
void OnDeviceRemoved(const std::string& service_type,
const std::string& service_name) override {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_);
if (scanners_.erase(service_name)) {
if (on_scanners_detected_callback_)
on_scanners_detected_callback_.Run(GetDedupedScanners());
} else {
LOG(WARNING) << "Device removal requested for unknown service: "
<< service_name;
}
}
// ServiceDiscoveryDeviceLister::Delegate:
// Removes all devices that originated on all service types and requests a new
// round of discovery. Clears all scanners to avoid returning stale cached
// scanners.
void OnDeviceCacheFlushed(const std::string& service_type) override {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_);
if (!scanners_.empty()) {
scanners_.clear();
if (on_scanners_detected_callback_)
on_scanners_detected_callback_.Run(GetDedupedScanners());
}
// Request a new round of discovery from the lister.
auto lister_entry = device_listers_.find(service_type);
DCHECK(lister_entry != device_listers_.end());
lister_entry->second->DiscoverNewDevices();
}
private:
// Creates a new device lister for the given |service_type| and adds it to the
// ones managed by this object.
void CreateDeviceLister(const std::string& service_type) {
auto lister = ServiceDiscoveryDeviceLister::Create(
this, discovery_client_.get(), service_type);
lister->Start();
lister->DiscoverNewDevices();
DCHECK(!base::Contains(device_listers_, service_type));
device_listers_[service_type] = std::move(lister);
}
// Returns the detected scanners after merging duplicates.
std::vector<Scanner> GetDedupedScanners() {
// Use a map of display name to Scanner to deduplicate the detected
// scanners. If a Scanner has the same display name as one that's already
// been added to the map, merge the two by adding the new Scanner's
// information to the existing Scanner.
base::flat_map<std::string, Scanner> deduped_scanners;
for (const auto& entry : scanners_) {
const Scanner* scanner = &entry.second;
auto it = deduped_scanners.find(scanner->display_name);
if (it == deduped_scanners.end()) {
deduped_scanners.insert({scanner->display_name, *scanner});
} else {
// Each Scanner in scanners_ should have a single device name
// corresponding to a known protocol.
ScanProtocol protocol = ScanProtocol::kUnknown;
if (scanner->device_names.find(ScanProtocol::kEscls) !=
scanner->device_names.end()) {
protocol = ScanProtocol::kEscls;
} else if (scanner->device_names.find(ScanProtocol::kEscl) !=
scanner->device_names.end()) {
protocol = ScanProtocol::kEscl;
} else {
NOTREACHED() << "Zeroconf scanner with unknown protocol.";
}
it->second.device_names[protocol].insert(
scanner->device_names.at(protocol).begin(),
scanner->device_names.at(protocol).end());
it->second.ip_addresses.insert(scanner->ip_addresses.begin(),
scanner->ip_addresses.end());
}
}
std::vector<Scanner> scanners;
scanners.reserve(deduped_scanners.size());
for (const auto& entry : deduped_scanners)
scanners.push_back(entry.second);
return scanners;
}
SEQUENCE_CHECKER(sequence_);
// Map from service name to Scanner.
base::flat_map<std::string, Scanner> scanners_;
// Keep a reference to the shared device client around for the lifetime of
// this object.
scoped_refptr<ServiceDiscoverySharedClient> discovery_client_;
// Map from service_type to associated lister.
ListersMap device_listers_;
// Callback used to notify when scanners are detected.
OnScannersDetectedCallback on_scanners_detected_callback_;
};
} // namespace
// static
std::unique_ptr<ZeroconfScannerDetector> ZeroconfScannerDetector::Create() {
std::unique_ptr<ZeroconfScannerDetectorImpl> detector =
std::make_unique<ZeroconfScannerDetectorImpl>();
detector->Init();
return std::move(detector);
}
// static
std::unique_ptr<ZeroconfScannerDetector>
ZeroconfScannerDetector::CreateForTesting(ListersMap&& device_listers) {
return std::make_unique<ZeroconfScannerDetectorImpl>(
std::move(device_listers));
}
} // namespace chromeos
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CHROME_BROWSER_CHROMEOS_SCANNING_ZEROCONF_SCANNER_DETECTOR_H_
#define CHROME_BROWSER_CHROMEOS_SCANNING_ZEROCONF_SCANNER_DETECTOR_H_
#include <memory>
#include <string>
#include "base/containers/flat_map.h"
#include "chrome/browser/chromeos/scanning/scanner_detector.h"
#include "chrome/browser/local_discovery/service_discovery_device_lister.h"
namespace chromeos {
// Uses mDNS and DNS-SD to detect nearby networked scanners.
class ZeroconfScannerDetector
: public ScannerDetector,
public local_discovery::ServiceDiscoveryDeviceLister::Delegate {
public:
// Service types used by the detector.
static const char kEsclServiceType[];
static const char kEsclsServiceType[];
~ZeroconfScannerDetector() override = default;
static std::unique_ptr<ZeroconfScannerDetector> Create();
// Creates an instance that uses the passed device listers instead of creating
// its own. |device_listers| is a map from service type to lister and should
// supply a lister for each of the service types used by the detector.
// Ownership is taken of the listers map.
using ListersMap = base::flat_map<
std::string,
std::unique_ptr<local_discovery::ServiceDiscoveryDeviceLister>>;
static std::unique_ptr<ZeroconfScannerDetector> CreateForTesting(
ListersMap&& device_listers);
};
} // namespace chromeos
#endif // CHROME_BROWSER_CHROMEOS_SCANNING_ZEROCONF_SCANNER_DETECTOR_H_
// Copyright 2020 The Chromium 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 "chrome/browser/chromeos/scanning/zeroconf_scanner_detector_utils.h"
#include <string>
#include "base/logging.h"
#include "base/strings/stringprintf.h"
#include "chrome/browser/chromeos/scanning/zeroconf_scanner_detector.h"
#include "url/gurl.h"
namespace chromeos {
namespace {
// Sets the values of |scheme| and |protocol| based on |service_type|.
void SetSchemeAndProtocol(const std::string& service_type,
std::string& scheme_out,
ScanProtocol& protocol_out) {
if (service_type == ZeroconfScannerDetector::kEsclsServiceType) {
scheme_out = "https";
protocol_out = ScanProtocol::kEscls;
} else if (service_type == ZeroconfScannerDetector::kEsclServiceType) {
scheme_out = "http";
protocol_out = ScanProtocol::kEscl;
} else {
NOTREACHED() << "Zeroconf scanner with unknown service type: "
<< service_type;
}
}
// Creates a device name compatible with the sane-airscan backend. Returns the
// name on success and an empty string on failure.
std::string CreateDeviceName(const std::string& name,
const std::string& scheme,
const std::string& rs,
const net::IPAddress& ip_address,
int port) {
std::string path;
if (rs == "none")
path = "eSCL/";
else if (!rs.empty())
path = rs + "/";
const std::string ip_address_str =
ip_address.IsIPv6()
? base::StringPrintf("[%s]", ip_address.ToString().c_str())
: ip_address.ToString();
GURL url(base::StringPrintf("%s://%s:%d/%s", scheme.c_str(),
ip_address_str.c_str(), port, path.c_str()));
if (!url.is_valid()) {
LOG(ERROR) << "Cannot create device name with invalid URL: "
<< url.possibly_invalid_spec();
return "";
}
return base::StringPrintf("airscan:escl:%s:%s", name.c_str(),
url.spec().c_str());
}
} // namespace
base::Optional<Scanner> CreateSaneAirscanScanner(
const std::string& name,
const std::string& service_type,
const std::string& rs,
const net::IPAddress& ip_address,
int port) {
std::string scheme;
ScanProtocol protocol = ScanProtocol::kUnknown;
SetSchemeAndProtocol(service_type, scheme, protocol);
const std::string device_name =
CreateDeviceName(name, scheme, rs, ip_address, port);
if (device_name.empty())
return base::nullopt;
Scanner scanner;
scanner.display_name = name;
scanner.device_names[protocol].insert(ScannerDeviceName(device_name));
scanner.ip_addresses.insert(ip_address);
return scanner;
}
} // namespace chromeos
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CHROME_BROWSER_CHROMEOS_SCANNING_ZEROCONF_SCANNER_DETECTOR_UTILS_H_
#define CHROME_BROWSER_CHROMEOS_SCANNING_ZEROCONF_SCANNER_DETECTOR_UTILS_H_
#include <string>
#include "base/optional.h"
#include "chromeos/scanning/scanner.h"
#include "net/base/ip_address.h"
namespace chromeos {
// Creates a Scanner with a device name that can be used to interact with a
// scanner via the sane-airscan backend. If errors occur, base::nullopts is
// returned. The device name must be in the format "airscan:escl:name:url",
// where name is an arbitrary name. The IP address is used instead of the host
// name since sane-airscan may not be able to resolve host names it did not
// discover itself. See mdns_make_escl_endpoint() at
// https://github.com/alexpevzner/sane-airscan/blob/master/airscan-mdns.c for
// more details.
base::Optional<Scanner> CreateSaneAirscanScanner(
const std::string& name,
const std::string& service_type,
const std::string& rs,
const net::IPAddress& ip_address,
int port);
} // namespace chromeos
#endif // CHROME_BROWSER_CHROMEOS_SCANNING_ZEROCONF_SCANNER_DETECTOR_UTILS_H_
...@@ -85,6 +85,8 @@ component("chromeos") { ...@@ -85,6 +85,8 @@ component("chromeos") {
"process_proxy/process_proxy.h", "process_proxy/process_proxy.h",
"process_proxy/process_proxy_registry.cc", "process_proxy/process_proxy_registry.cc",
"process_proxy/process_proxy_registry.h", "process_proxy/process_proxy_registry.h",
"scanning/scanner.cc",
"scanning/scanner.h",
] ]
if (current_cpu == "arm" || current_cpu == "x86") { if (current_cpu == "arm" || current_cpu == "x86") {
defines = [ "BINDER_IPC_32BIT" ] defines = [ "BINDER_IPC_32BIT" ]
......
jschettler@chromium.org
skau@chromium.org
# Backup reviewers:
zentaro@chromium.org
// Copyright 2020 The Chromium 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 "chromeos/scanning/scanner.h"
namespace chromeos {
ScannerDeviceName::ScannerDeviceName(const std::string& device_name)
: device_name(device_name) {}
ScannerDeviceName::~ScannerDeviceName() = default;
ScannerDeviceName::ScannerDeviceName(const ScannerDeviceName& other) = default;
ScannerDeviceName& ScannerDeviceName::operator=(
const ScannerDeviceName& other) = default;
bool ScannerDeviceName::operator<(const ScannerDeviceName& other) const {
return device_name < other.device_name;
}
bool ScannerDeviceName::operator==(const ScannerDeviceName& other) const {
return device_name == other.device_name;
}
Scanner::Scanner() = default;
Scanner::~Scanner() = default;
Scanner::Scanner(const Scanner& other) = default;
Scanner& Scanner::operator=(const Scanner& other) = default;
} // namespace chromeos
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CHROMEOS_SCANNING_SCANNER_H_
#define CHROMEOS_SCANNING_SCANNER_H_
#include <string>
#include "base/containers/flat_map.h"
#include "base/containers/flat_set.h"
#include "chromeos/chromeos_export.h"
#include "net/base/ip_address.h"
namespace chromeos {
// The type of protocol used to communicate with a scanner.
enum class CHROMEOS_EXPORT ScanProtocol {
kUnknown,
kEscl, // eSCL protocol.
kEscls, // eSCLS protocol.
kLegacyNetwork, // Non-eSCL(S), legacy network protocol.
kLegacyUsb // Non-eSCL(S), legacy USB protocol.
};
struct CHROMEOS_EXPORT ScannerDeviceName {
explicit ScannerDeviceName(const std::string& device_name);
~ScannerDeviceName();
ScannerDeviceName(const ScannerDeviceName& scanner_device_name);
ScannerDeviceName& operator=(const ScannerDeviceName& scanner_device_name);
bool operator<(const ScannerDeviceName& other) const;
bool operator==(const ScannerDeviceName& other) const;
// The device name corresponding to the SANE backend, and therefore the
// protocol, used to interact with the scanner.
std::string device_name;
// Signifies whether the device name can be used to interact with the scanner.
// Device names are considered usable until proven otherwise (i.e. a device
// name is marked as unusable when attempting to use it fails).
bool usable = true;
};
struct CHROMEOS_EXPORT Scanner {
Scanner();
~Scanner();
Scanner(const Scanner& scanner);
Scanner& operator=(const Scanner& scanner);
// Name to display in a UI.
std::string display_name;
// Map of ScanProtocol to a set of corresponding ScannerDeviceNames that can
// be used with the lorgnette D-Bus service. Clients are responsible for
// selecting which device name to use.
base::flat_map<ScanProtocol, std::set<ScannerDeviceName>> device_names;
// Known IP addresses for this scanner. Used to deduplicate network scanners
// from multiple sources.
base::flat_set<net::IPAddress> ip_addresses;
};
} // namespace chromeos
#endif // CHROMEOS_SCANNING_SCANNER_H_
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment