blob: 9c8621ef121e876157542f35caf82290c1e14223 [file] [log] [blame]
// 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