blob: 4bfca0366cba010c3415d75c72824e5570f00f43 [file] [log] [blame]
// Copyright 2014 The Chromium OS 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 "peerd/avahi_service_publisher.h"
#include <vector>
#include <avahi-client/publish.h>
#include <avahi-common/address.h>
#include <base/message_loop/message_loop.h>
#include <base/callback.h>
#include <brillo/bind_lambda.h>
#include <brillo/dbus/dbus_method_invoker.h>
#include <brillo/dbus/dbus_signal_handler.h>
#include <brillo/strings/string_utils.h>
#include <dbus/bus.h>
#include "peerd/avahi_client.h"
#include "peerd/constants.h"
#include "peerd/dbus_constants.h"
#include "peerd/service.h"
using brillo::Error;
using brillo::ErrorPtr;
using brillo::dbus_utils::CallMethodAndBlock;
using brillo::dbus_utils::ExtractMethodCallResults;
using brillo::string_utils::Join;
using peerd::constants::kSerbusServiceId;
using peerd::constants::mdns::kSerbusServiceDelimiter;
using std::vector;
namespace peerd {
namespace errors {
namespace avahi {
const char kRemovedUnknownService[] = "avahi.unknown_service";
const char kInvalidServiceId[] = "avahi.invalid_service_id";
} // namespace avahi
} // namespace errors
AvahiServicePublisher::AvahiServicePublisher(
const std::string& uuid,
const std::string& unique_prefix,
const scoped_refptr<dbus::Bus>& bus,
dbus::ObjectProxy* avahi_proxy,
const base::Closure& on_publish_failure)
: uuid_{uuid},
unique_prefix_{unique_prefix},
bus_{bus},
avahi_proxy_{avahi_proxy},
on_publish_failure_{on_publish_failure} {
VLOG(1) << "mDNS prefix is " << unique_prefix << ".";
}
AvahiServicePublisher::~AvahiServicePublisher() {
for (const auto& group : outstanding_groups_) {
FreeGroup(nullptr, group.second);
}
}
base::WeakPtr<AvahiServicePublisher> AvahiServicePublisher::GetWeakPtr() {
return weak_ptr_factory_.GetWeakPtr();
}
bool AvahiServicePublisher::OnServiceUpdated(ErrorPtr* error,
const Service& service) {
if (service.GetServiceId() == kSerbusServiceId) {
Error::AddToPrintf(error, FROM_HERE,
kPeerdErrorDomain,
errors::avahi::kInvalidServiceId,
"Service name is reserved: %s.",
kSerbusServiceId);
return false;
}
const bool updated_group = UpdateGroup(
error, service.GetServiceId(),
service.GetServiceInfo(), service.GetMDnsOptions());
// Always update the master record.
return UpdateRootService(error) && updated_group;
}
bool AvahiServicePublisher::UpdateGroup(
ErrorPtr* error,
const std::string& service_id,
const Service::ServiceInfo& service_info,
const Service::MDnsOptions& mdns_options) {
VLOG(1) << "Modifying group for service_id=" << service_id;
auto it = outstanding_groups_.find(service_id);
dbus::ObjectProxy* group_proxy = nullptr;
if (it == outstanding_groups_.end()) {
// Create a new entry group for this service.
auto resp = CallMethodAndBlock(
avahi_proxy_, dbus_constants::avahi::kServerInterface,
dbus_constants::avahi::kServerMethodEntryGroupNew,
error);
dbus::ObjectPath group_path;
if (!resp || !ExtractMethodCallResults(resp.get(), error, &group_path)) {
return false;
}
group_proxy = bus_->GetObjectProxy(dbus_constants::avahi::kServiceName,
group_path);
// If we fail to connect to the the StateChange signal for this group, just
// report that the whole thing has failed.
auto on_failure_to_connect_cb = [](const base::Closure& on_failure,
const std::string& interface_name,
const std::string& signal_name,
bool success) {
if (success) { return; }
LOG(WARNING) << "Failed to connect to StateChange signal "
"from EntryGroup.";
base::MessageLoop::current()->PostTask(FROM_HERE, on_failure);
};
brillo::dbus_utils::ConnectToSignal(
group_proxy,
dbus_constants::avahi::kGroupInterface,
dbus_constants::avahi::kGroupSignalStateChanged,
base::Bind(&AvahiServicePublisher::HandleGroupStateChanged,
base::Unretained(this)),
base::Bind(on_failure_to_connect_cb, on_publish_failure_));
outstanding_groups_[service_id] = group_proxy;
} else {
// Reset the entry group for this service.
group_proxy = it->second;
auto resp = CallMethodAndBlock(
group_proxy, dbus_constants::avahi::kGroupInterface,
dbus_constants::avahi::kGroupMethodReset, error);
if (!resp || !ExtractMethodCallResults(resp.get(), error)) {
// Failed to reset the group. Remove the entry entirely from our DNS
// record, and forget about that service.
FreeGroup(error, group_proxy);
outstanding_groups_.erase(it);
return false;
}
}
// Now add records for this service/entry group.
if (!AddServiceToGroup(error, service_id, service_info,
mdns_options, group_proxy)) {
FreeGroup(error, group_proxy);
outstanding_groups_.erase(service_id); // |it| might be invalid
return false;
}
return true;
}
bool AvahiServicePublisher::OnServiceRemoved(ErrorPtr* error,
const std::string& service_id) {
auto it = outstanding_groups_.find(service_id);
if (it == outstanding_groups_.end()) {
Error::AddToPrintf(error, FROM_HERE,
kPeerdErrorDomain,
errors::avahi::kRemovedUnknownService,
"Attempted to remove unknown service: %s.",
service_id.c_str());
return false;
}
const bool remove_successful = FreeGroup(error, it->second);
outstanding_groups_.erase(it);
// Always update the master record.
return UpdateRootService(error) && remove_successful;
}
AvahiServicePublisher::TxtRecord AvahiServicePublisher::GetTxtRecord(
const Service::ServiceInfo& info) {
TxtRecord result;
result.reserve(info.size());
for (const auto& kv : info) {
result.emplace_back();
vector<uint8_t>& record = result.back();
record.reserve(kv.first.length() + kv.second.length() + 1);
record.insert(record.end(), kv.first.begin(), kv.first.end());
record.push_back('=');
record.insert(record.end(), kv.second.begin(), kv.second.end());
}
return result;
}
bool AvahiServicePublisher::AddServiceToGroup(
ErrorPtr* error,
const std::string& service_id,
const Service::ServiceInfo& service_info,
const Service::MDnsOptions& mdns_options,
dbus::ObjectProxy* group_proxy) {
auto resp = CallMethodAndBlock(
group_proxy,
dbus_constants::avahi::kGroupInterface,
dbus_constants::avahi::kGroupMethodAddService,
error,
int32_t{AVAHI_IF_UNSPEC},
int32_t{AVAHI_PROTO_UNSPEC},
uint32_t{0}, // No flags.
unique_prefix_,
AvahiClient::GetServiceType(service_id),
std::string{}, // domain.
std::string{}, // hostname
mdns_options.port,
GetTxtRecord(service_info));
if (!resp || !ExtractMethodCallResults(resp.get(), error)) { return false; }
resp = CallMethodAndBlock(group_proxy,
dbus_constants::avahi::kGroupInterface,
dbus_constants::avahi::kGroupMethodCommit,
error);
if (!resp || !ExtractMethodCallResults(resp.get(), error)) {
return false;
}
return true;
}
bool AvahiServicePublisher::FreeGroup(ErrorPtr* error,
dbus::ObjectProxy* group_proxy) {
auto resp = CallMethodAndBlock(group_proxy,
dbus_constants::avahi::kGroupInterface,
dbus_constants::avahi::kGroupMethodFree,
error);
// Extract and log relevant errors.
bool success = resp && ExtractMethodCallResults(resp.get(), error);
// Ignore any signals we may have registered for from this proxy.
group_proxy->Detach();
return success;
}
bool AvahiServicePublisher::UpdateRootService(ErrorPtr* error) {
std::vector<std::string> services;
services.reserve(outstanding_groups_.size());
for (const auto& pair : outstanding_groups_) {
if (pair.first != kSerbusServiceId) {
services.push_back(pair.first);
}
}
if (services.empty()) {
// If we have no services to advertise, don't even publish the root
// record.
bool success = true;
auto it = outstanding_groups_.find(kSerbusServiceId);
if (it != outstanding_groups_.end()) {
success = FreeGroup(error, it->second);
outstanding_groups_.erase(it);
}
return success;
}
Service::ServiceInfo service_info{
{constants::mdns::kSerbusVersion, "1.0"},
{constants::mdns::kSerbusPeerId, uuid_},
{constants::mdns::kSerbusServiceList, Join(kSerbusServiceDelimiter,
services)},
};
return UpdateGroup(
error, kSerbusServiceId, service_info, Service::MDnsOptions{});
}
void AvahiServicePublisher::HandleGroupStateChanged(
int32_t state,
const std::string& error_message) {
if (state == AVAHI_ENTRY_GROUP_COLLISION ||
state == AVAHI_ENTRY_GROUP_FAILURE) {
base::MessageLoop::current()->PostTask(FROM_HERE, on_publish_failure_);
}
}
} // namespace peerd