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