| // 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 |