| // Copyright 2013 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/local_discovery/service_discovery_client_mac.h" |
| |
| #import <arpa/inet.h> |
| #import <Foundation/Foundation.h> |
| #import <net/if_dl.h> |
| #include <stddef.h> |
| #include <stdint.h> |
| |
| #include <memory> |
| |
| #include "base/apple/foundation_util.h" |
| #include "base/functional/bind.h" |
| #include "base/message_loop/message_pump_type.h" |
| #include "base/strings/sys_string_conversions.h" |
| #import "base/task/single_thread_task_runner.h" |
| #include "base/task/single_thread_task_runner.h" |
| #include "base/threading/thread.h" |
| #include "net/base/ip_address.h" |
| #include "net/base/ip_endpoint.h" |
| |
| using local_discovery::ServiceWatcher; |
| using local_discovery::ServiceResolver; |
| using local_discovery::ServiceDescription; |
| |
| @interface NetServiceBrowser |
| : NSObject <NSNetServiceBrowserDelegate, NSNetServiceDelegate> |
| // Creates a new Browser instance for |serviceType|, which will call |
| // |callback| on |callbackRunner| when changes are detected. This does NOT |
| // start listening, as that must be done on the discovery thread via |
| // -discoverServices. |
| - (instancetype)initWithServiceType:(const std::string&)serviceType |
| callback:(ServiceWatcher::UpdatedCallback)callback |
| callbackRunner: |
| (scoped_refptr<base::SingleThreadTaskRunner>) |
| callbackRunner; |
| |
| // Creates a new NSNetServiceBrowser and starts listening for discovery |
| // notifications. |
| - (void)discoverServices; |
| |
| // Stops listening for discovery notifications. |
| - (void)stop; |
| @end |
| |
| @interface NetServiceResolver : NSObject <NSNetServiceDelegate> |
| // Creates a new resolver instance for service named |name|. Calls the |
| // |callback| on the |callbackRunner| when done or an error occurs. |
| - (instancetype) |
| initWithServiceName:(const std::string&)name |
| resolvedCallback:(ServiceResolver::ResolveCompleteCallback)callback |
| callbackRunner: |
| (scoped_refptr<base::SingleThreadTaskRunner>)callbackRunner; |
| |
| // Begins a resolve request for the service. |
| - (void)resolveService; |
| |
| // Stops any in-flight resolve operation. |
| - (void)stop; |
| @end |
| |
| namespace local_discovery { |
| |
| namespace { |
| |
| const char kServiceDiscoveryThreadName[] = "Service Discovery Thread"; |
| |
| const NSTimeInterval kResolveTimeout = 10.0; |
| |
| // These functions are used to PostTask with ObjC objects, without needing to |
| // manage the lifetime of a C++ pointer for either the Watcher or Resolver. |
| // Clients of those classes can delete the C++ object while operations on the |
| // ObjC objects are still in flight. Because the ObjC objects are reference |
| // counted, the strong references passed to these functions ensure the object |
| // remains alive until for the duration of the operation. |
| |
| void StartServiceBrowser(NetServiceBrowser* browser) { |
| [browser discoverServices]; |
| } |
| |
| void StopServiceBrowser(NetServiceBrowser* browser) { |
| [browser stop]; |
| } |
| |
| void StartServiceResolver(NetServiceResolver* resolver) { |
| [resolver resolveService]; |
| } |
| |
| void StopServiceResolver(NetServiceResolver* resolver) { |
| [resolver stop]; |
| } |
| |
| // Extracts the instance name, name type and domain from a full service name or |
| // the service type and domain from a service type. Returns true if successful. |
| // TODO(justinlin): This current only handles service names with format |
| // <name>._<protocol2>._<protocol1>.<domain>. Service names with |
| // subtypes will not parse correctly: |
| // <name>._<type>._<sub>._<protocol2>._<protocol1>.<domain>. |
| bool ExtractServiceInfo(const std::string& service, |
| bool is_service_name, |
| NSString** instance, |
| NSString** type, |
| NSString** domain) { |
| if (service.empty()) |
| return false; |
| |
| const size_t last_period = service.find_last_of('.'); |
| if (last_period == std::string::npos || service.length() <= last_period) |
| return false; |
| |
| if (!is_service_name) { |
| *type = base::SysUTF8ToNSString(service.substr(0, last_period) + "."); |
| } else { |
| // Find third last period that delimits type and instance name. |
| size_t type_period = last_period; |
| for (int i = 0; i < 2; ++i) { |
| type_period = service.find_last_of('.', type_period - 1); |
| if (type_period == std::string::npos) { |
| return false; |
| } |
| } |
| |
| *instance = base::SysUTF8ToNSString(service.substr(0, type_period)); |
| *type = base::SysUTF8ToNSString( |
| service.substr(type_period + 1, last_period - type_period)); |
| } |
| *domain = base::SysUTF8ToNSString(service.substr(last_period + 1) + "."); |
| |
| return [*domain length] > 0 && |
| [*type length] > 0 && |
| (!is_service_name || [*instance length] > 0); |
| } |
| |
| void ParseTxtRecord(NSData* record, std::vector<std::string>* output) { |
| if (record.length <= 1) { |
| return; |
| } |
| |
| VLOG(1) << "ParseTxtRecord: " << record.length; |
| |
| const uint8_t* bytes = reinterpret_cast<const uint8_t*>(record.bytes); |
| size_t size = record.length; |
| size_t offset = 0; |
| while (offset < size) { |
| uint8_t record_size = bytes[offset++]; |
| if (offset > size - record_size) |
| break; |
| |
| NSString* txt_record = |
| [[NSString alloc] initWithBytes:&bytes[offset] |
| length:record_size |
| encoding:NSUTF8StringEncoding]; |
| if (txt_record) { |
| std::string txt_record_string = base::SysNSStringToUTF8(txt_record); |
| VLOG(1) << "TxtRecord: " << txt_record_string; |
| output->push_back(std::move(txt_record_string)); |
| } else { |
| VLOG(1) << "TxtRecord corrupted at offset " << offset; |
| } |
| |
| offset += record_size; |
| } |
| } |
| |
| } // namespace |
| |
| ServiceDiscoveryClientMac::ServiceDiscoveryClientMac() = default; |
| ServiceDiscoveryClientMac::~ServiceDiscoveryClientMac() = default; |
| |
| std::unique_ptr<ServiceWatcher> ServiceDiscoveryClientMac::CreateServiceWatcher( |
| const std::string& service_type, |
| ServiceWatcher::UpdatedCallback callback) { |
| StartThreadIfNotStarted(); |
| VLOG(1) << "CreateServiceWatcher: " << service_type; |
| return std::make_unique<ServiceWatcherImplMac>( |
| service_type, std::move(callback), |
| service_discovery_thread_->task_runner()); |
| } |
| |
| std::unique_ptr<ServiceResolver> |
| ServiceDiscoveryClientMac::CreateServiceResolver( |
| const std::string& service_name, |
| ServiceResolver::ResolveCompleteCallback callback) { |
| StartThreadIfNotStarted(); |
| VLOG(1) << "CreateServiceResolver: " << service_name; |
| return std::make_unique<ServiceResolverImplMac>( |
| service_name, std::move(callback), |
| service_discovery_thread_->task_runner()); |
| } |
| |
| std::unique_ptr<LocalDomainResolver> |
| ServiceDiscoveryClientMac::CreateLocalDomainResolver( |
| const std::string& domain, |
| net::AddressFamily address_family, |
| LocalDomainResolver::IPAddressCallback callback) { |
| NOTIMPLEMENTED(); // TODO(noamsml): Implement. |
| VLOG(1) << "CreateLocalDomainResolver: " << domain; |
| return nullptr; |
| } |
| |
| void ServiceDiscoveryClientMac::StartThreadIfNotStarted() { |
| if (!service_discovery_thread_) { |
| service_discovery_thread_ = |
| std::make_unique<base::Thread>(kServiceDiscoveryThreadName); |
| // Only TYPE_UI uses an NSRunLoop. |
| base::Thread::Options options(base::MessagePumpType::UI, 0); |
| service_discovery_thread_->StartWithOptions(std::move(options)); |
| } |
| } |
| |
| // Service Watcher ///////////////////////////////////////////////////////////// |
| |
| ServiceWatcherImplMac::ServiceWatcherImplMac( |
| const std::string& service_type, |
| ServiceWatcher::UpdatedCallback callback, |
| scoped_refptr<base::SingleThreadTaskRunner> service_discovery_runner) |
| : service_type_(service_type), |
| callback_(std::move(callback)), |
| service_discovery_runner_(service_discovery_runner) {} |
| |
| ServiceWatcherImplMac::~ServiceWatcherImplMac() { |
| service_discovery_runner_->PostTask( |
| FROM_HERE, base::BindOnce(&StopServiceBrowser, std::move(browser_))); |
| } |
| |
| void ServiceWatcherImplMac::Start() { |
| DCHECK(!started_); |
| VLOG(1) << "ServiceWatcherImplMac::Start"; |
| |
| browser_ = [[NetServiceBrowser alloc] |
| initWithServiceType:service_type_ |
| callback:base::BindRepeating( |
| &ServiceWatcherImplMac::OnServicesUpdate, |
| weak_factory_.GetWeakPtr()) |
| callbackRunner:base::SingleThreadTaskRunner::GetCurrentDefault()]; |
| started_ = true; |
| } |
| |
| void ServiceWatcherImplMac::DiscoverNewServices() { |
| DCHECK(started_); |
| VLOG(1) << "ServiceWatcherImplMac::DiscoverNewServices"; |
| service_discovery_runner_->PostTask( |
| FROM_HERE, base::BindOnce(&StartServiceBrowser, browser_)); |
| } |
| |
| void ServiceWatcherImplMac::SetActivelyRefreshServices( |
| bool actively_refresh_services) { |
| DCHECK(started_); |
| VLOG(1) << "ServiceWatcherImplMac::SetActivelyRefreshServices"; |
| } |
| |
| std::string ServiceWatcherImplMac::GetServiceType() const { |
| return service_type_; |
| } |
| |
| void ServiceWatcherImplMac::OnServicesUpdate(ServiceWatcher::UpdateType update, |
| const std::string& service) { |
| VLOG(1) << "ServiceWatcherImplMac::OnServicesUpdate: " |
| << service + "." + service_type_; |
| callback_.Run(update, service + "." + service_type_); |
| } |
| |
| // Service Resolver //////////////////////////////////////////////////////////// |
| |
| ServiceResolverImplMac::ServiceResolverImplMac( |
| const std::string& service_name, |
| ServiceResolver::ResolveCompleteCallback callback, |
| scoped_refptr<base::SingleThreadTaskRunner> service_discovery_runner) |
| : service_name_(service_name), |
| callback_(std::move(callback)), |
| service_discovery_runner_(service_discovery_runner) {} |
| |
| ServiceResolverImplMac::~ServiceResolverImplMac() { |
| StopResolving(); |
| } |
| |
| void ServiceResolverImplMac::StartResolving() { |
| VLOG(1) << "Resolving service " << service_name_; |
| resolver_ = [[NetServiceResolver alloc] |
| initWithServiceName:service_name_ |
| resolvedCallback:base::BindOnce( |
| &ServiceResolverImplMac::OnResolveComplete, |
| weak_factory_.GetWeakPtr()) |
| callbackRunner:base::SingleThreadTaskRunner::GetCurrentDefault()]; |
| service_discovery_runner_->PostTask( |
| FROM_HERE, base::BindOnce(&StartServiceResolver, resolver_)); |
| } |
| |
| std::string ServiceResolverImplMac::GetName() const { |
| return service_name_; |
| } |
| |
| void ServiceResolverImplMac::OnResolveComplete( |
| RequestStatus status, |
| const ServiceDescription& description) { |
| VLOG(1) << "ServiceResolverImplMac::OnResolveComplete: " << service_name_ |
| << ", " << status; |
| |
| has_resolved_ = true; |
| |
| StopResolving(); |
| |
| // The |callback_| can delete this. |
| if (!callback_.is_null()) |
| std::move(callback_).Run(status, description); |
| } |
| |
| void ServiceResolverImplMac::StopResolving() { |
| service_discovery_runner_->PostTask( |
| FROM_HERE, base::BindOnce(&StopServiceResolver, std::move(resolver_))); |
| } |
| |
| void ParseNetService(NSNetService* service, ServiceDescription& description) { |
| for (NSData* address in [service addresses]) { |
| const void* bytes = [address bytes]; |
| int length = [address length]; |
| const sockaddr* socket = static_cast<const sockaddr*>(bytes); |
| net::IPEndPoint end_point; |
| if (end_point.FromSockAddr(socket, length)) { |
| description.address = net::HostPortPair::FromIPEndPoint(end_point); |
| description.ip_address = end_point.address(); |
| break; |
| } |
| } |
| |
| ParseTxtRecord([service TXTRecordData], &description.metadata); |
| } |
| |
| } // namespace local_discovery |
| |
| // Service Watcher ///////////////////////////////////////////////////////////// |
| |
| @implementation NetServiceBrowser { |
| std::string _serviceType; |
| |
| ServiceWatcher::UpdatedCallback _callback; |
| scoped_refptr<base::SingleThreadTaskRunner> _callbackRunner; |
| |
| NSNetServiceBrowser* __strong _browser; |
| NSMutableArray<NSNetService*>* __strong _services; |
| } |
| |
| - (instancetype)initWithServiceType:(const std::string&)serviceType |
| callback:(ServiceWatcher::UpdatedCallback)callback |
| callbackRunner: |
| (scoped_refptr<base::SingleThreadTaskRunner>) |
| callbackRunner { |
| if ((self = [super init])) { |
| _serviceType = serviceType; |
| |
| _callback = std::move(callback); |
| _callbackRunner = callbackRunner; |
| |
| _services = [[NSMutableArray alloc] initWithCapacity:1]; |
| } |
| return self; |
| } |
| |
| - (void)dealloc { |
| [self stop]; |
| } |
| |
| - (void)discoverServices { |
| if (!_browser) { |
| _browser = [[NSNetServiceBrowser alloc] init]; |
| [_browser setDelegate:self]; |
| } |
| |
| NSString* instance; |
| NSString* type; |
| NSString* domain; |
| if (!local_discovery::ExtractServiceInfo(_serviceType, false, &instance, |
| &type, &domain)) { |
| return; |
| } |
| |
| DCHECK(![instance length]); |
| DVLOG(1) << "Listening for service type '" << type << "' on domain '" |
| << domain << "'"; |
| |
| [_browser searchForServicesOfType:type inDomain:domain]; |
| } |
| |
| - (void)stop { |
| [_browser stop]; |
| |
| // Work around a 10.12 bug: NSNetServiceBrowser doesn't lose interest in its |
| // weak delegate during deallocation, so a subsequently-deallocated delegate |
| // attempts to clear the pointer to itself in an NSNetServiceBrowser that's |
| // already gone. |
| // https://crbug.com/657495, https://openradar.appspot.com/28943305 |
| _browser.delegate = nil; |
| |
| // Ensure the delegate clears all references to itself, which it had added as |
| // discovered services were reported to it. |
| for (NSNetService* netService in _services) { |
| [netService stopMonitoring]; |
| [netService setDelegate:nil]; |
| } |
| [_services removeAllObjects]; |
| |
| _browser = nil; |
| } |
| |
| - (void)netServiceBrowser:(NSNetServiceBrowser*)netServiceBrowser |
| didFindService:(NSNetService*)netService |
| moreComing:(BOOL)moreServicesComing { |
| [netService setDelegate:self]; |
| [netService startMonitoring]; |
| [_services addObject:netService]; |
| |
| _callbackRunner->PostTask( |
| FROM_HERE, base::BindOnce(_callback, ServiceWatcher::UPDATE_ADDED, |
| base::SysNSStringToUTF8([netService name]))); |
| } |
| |
| - (void)netServiceBrowser:(NSNetServiceBrowser*)netServiceBrowser |
| didRemoveService:(NSNetService*)netService |
| moreComing:(BOOL)moreServicesComing { |
| NSUInteger index = [_services indexOfObject:netService]; |
| if (index != NSNotFound) { |
| _callbackRunner->PostTask( |
| FROM_HERE, base::BindOnce(_callback, ServiceWatcher::UPDATE_REMOVED, |
| base::SysNSStringToUTF8([netService name]))); |
| |
| // Stop monitoring this service for updates. The |netService| object may be |
| // different than the one stored in |_services|, even though they represent |
| // the same service. Stop monitoring and clear the delegate on both. |
| [netService stopMonitoring]; |
| [netService setDelegate:nil]; |
| |
| netService = _services[index]; |
| [netService stopMonitoring]; |
| [netService setDelegate:nil]; |
| |
| [_services removeObjectAtIndex:index]; |
| } |
| } |
| |
| - (void)netService:(NSNetService*)sender |
| didUpdateTXTRecordData:(NSData*)data { |
| _callbackRunner->PostTask( |
| FROM_HERE, base::BindOnce(_callback, ServiceWatcher::UPDATE_CHANGED, |
| base::SysNSStringToUTF8([sender name]))); |
| } |
| |
| @end |
| |
| // Service Resolver //////////////////////////////////////////////////////////// |
| |
| @implementation NetServiceResolver { |
| std::string _serviceName; |
| |
| ServiceResolver::ResolveCompleteCallback _callback; |
| scoped_refptr<base::SingleThreadTaskRunner> _callbackRunner; |
| |
| ServiceDescription _serviceDescription; |
| NSNetService* __strong _service; |
| } |
| |
| - (instancetype) |
| initWithServiceName:(const std::string&)serviceName |
| resolvedCallback:(ServiceResolver::ResolveCompleteCallback)callback |
| callbackRunner: |
| (scoped_refptr<base::SingleThreadTaskRunner>)callbackRunner { |
| if ((self = [super init])) { |
| _serviceName = serviceName; |
| _callback = std::move(callback); |
| _callbackRunner = callbackRunner; |
| } |
| return self; |
| } |
| |
| - (void)dealloc { |
| [self stop]; |
| } |
| |
| - (void)resolveService { |
| NSString* instance; |
| NSString* type; |
| NSString* domain; |
| if (!local_discovery::ExtractServiceInfo(_serviceName, true, &instance, &type, |
| &domain)) { |
| [self updateServiceDescription:ServiceResolver::STATUS_KNOWN_NONEXISTENT]; |
| return; |
| } |
| |
| VLOG(1) << "-[ServiceResolver resolveService] " << _serviceName |
| << ", instance: " << instance << ", type: " << type |
| << ", domain: " << domain; |
| |
| _service = [[NSNetService alloc] initWithDomain:domain |
| type:type |
| name:instance]; |
| [_service setDelegate:self]; |
| [_service resolveWithTimeout:local_discovery::kResolveTimeout]; |
| } |
| |
| - (void)stop { |
| [_service stop]; |
| |
| // Work around a 10.12 bug: NSNetService doesn't lose interest in its weak |
| // delegate during deallocation, so a subsequently-deallocated delegate |
| // attempts to clear the pointer to itself in an NSNetService that's already |
| // gone. |
| // https://crbug.com/657495, https://openradar.appspot.com/28943305 |
| _service.delegate = nil; |
| _service = nil; |
| } |
| |
| - (void)netServiceDidResolveAddress:(NSNetService*)sender { |
| [self updateServiceDescription:ServiceResolver::STATUS_SUCCESS]; |
| } |
| |
| - (void)netService:(NSNetService*)sender |
| didNotResolve:(NSDictionary*)errorDict { |
| [self updateServiceDescription:ServiceResolver::STATUS_REQUEST_TIMEOUT]; |
| } |
| |
| - (void)updateServiceDescription:(ServiceResolver::RequestStatus)status { |
| if (_callback.is_null()) |
| return; |
| |
| if (status != ServiceResolver::STATUS_SUCCESS) { |
| _callbackRunner->PostTask( |
| FROM_HERE, |
| base::BindOnce(std::move(_callback), status, ServiceDescription())); |
| return; |
| } |
| |
| _serviceDescription.service_name = _serviceName; |
| ParseNetService(_service, _serviceDescription); |
| |
| if (_serviceDescription.address.host().empty()) { |
| VLOG(1) << "Service IP is not resolved: " << _serviceName; |
| _callbackRunner->PostTask( |
| FROM_HERE, base::BindOnce(std::move(_callback), |
| ServiceResolver::STATUS_KNOWN_NONEXISTENT, |
| ServiceDescription())); |
| return; |
| } |
| |
| // TODO(justinlin): Implement last_seen. |
| _serviceDescription.last_seen = base::Time::Now(); |
| _callbackRunner->PostTask( |
| FROM_HERE, |
| base::BindOnce(std::move(_callback), status, _serviceDescription)); |
| } |
| |
| @end |