Alex Vakulenko | f0f5534 | 2015-08-18 15:51:40 -0700 | [diff] [blame] | 1 | /* |
| 2 | * Copyright 2015 The Android Open Source Project |
| 3 | * |
| 4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | * you may not use this file except in compliance with the License. |
| 6 | * You may obtain a copy of the License at |
| 7 | * |
| 8 | * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | * |
| 10 | * Unless required by applicable law or agreed to in writing, software |
| 11 | * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | * See the License for the specific language governing permissions and |
| 14 | * limitations under the License. |
| 15 | */ |
| 16 | |
Robert Ginda | cf92c66 | 2015-08-20 09:30:11 -0700 | [diff] [blame] | 17 | #include <vector> |
Alex Vakulenko | f0f5534 | 2015-08-18 15:51:40 -0700 | [diff] [blame] | 18 | |
Robert Ginda | cf92c66 | 2015-08-20 09:30:11 -0700 | [diff] [blame] | 19 | #include "buffet/avahi_mdns_client.h" |
| 20 | #include "buffet/dbus_constants.h" |
| 21 | |
| 22 | #include <avahi-common/defs.h> |
| 23 | #include <avahi-common/address.h> |
Alex Vakulenko | f0f5534 | 2015-08-18 15:51:40 -0700 | [diff] [blame] | 24 | #include <base/guid.h> |
Alex Vakulenko | 4170585 | 2015-10-13 10:12:06 -0700 | [diff] [blame] | 25 | #include <brillo/dbus/async_event_sequencer.h> |
| 26 | #include <brillo/dbus/dbus_signal_handler.h> |
| 27 | #include <brillo/dbus/dbus_method_invoker.h> |
| 28 | #include <brillo/errors/error.h> |
Robert Ginda | cf92c66 | 2015-08-20 09:30:11 -0700 | [diff] [blame] | 29 | #include <dbus/object_path.h> |
| 30 | #include <dbus/object_proxy.h> |
| 31 | |
Alex Vakulenko | 4170585 | 2015-10-13 10:12:06 -0700 | [diff] [blame] | 32 | using brillo::ErrorPtr; |
| 33 | using brillo::dbus_utils::AsyncEventSequencer; |
| 34 | using brillo::dbus_utils::CallMethodAndBlock; |
| 35 | using brillo::dbus_utils::ExtractMethodCallResults; |
Robert Ginda | cf92c66 | 2015-08-20 09:30:11 -0700 | [diff] [blame] | 36 | using CompletionAction = |
Alex Vakulenko | 4170585 | 2015-10-13 10:12:06 -0700 | [diff] [blame] | 37 | brillo::dbus_utils::AsyncEventSequencer::CompletionAction; |
Alex Vakulenko | f0f5534 | 2015-08-18 15:51:40 -0700 | [diff] [blame] | 38 | |
| 39 | namespace buffet { |
| 40 | |
Alex Vakulenko | 7f019f8 | 2015-08-21 19:30:36 -0700 | [diff] [blame] | 41 | AvahiMdnsClient::AvahiMdnsClient(const scoped_refptr<dbus::Bus> &bus) |
Alex Vakulenko | 0022b75 | 2015-10-02 11:09:59 -0700 | [diff] [blame] | 42 | : bus_(bus), service_name_(base::GenerateGUID()) { |
Alex Vakulenko | f0f5534 | 2015-08-18 15:51:40 -0700 | [diff] [blame] | 43 | } |
| 44 | |
| 45 | AvahiMdnsClient::~AvahiMdnsClient() { |
| 46 | } |
| 47 | |
Robert Ginda | cf92c66 | 2015-08-20 09:30:11 -0700 | [diff] [blame] | 48 | // NB: This should be the one-and-only definition of this MdnsClient static |
| 49 | // method. |
| 50 | std::unique_ptr<MdnsClient> MdnsClient::CreateInstance( |
| 51 | const scoped_refptr<dbus::Bus> &bus) { |
| 52 | return std::unique_ptr<MdnsClient>{new AvahiMdnsClient(bus)}; |
| 53 | } |
| 54 | |
| 55 | // TODO(rginda): Report errors back to the caller. |
| 56 | // TODO(rginda): Support publishing more than one service. |
Alex Vakulenko | f0f5534 | 2015-08-18 15:51:40 -0700 | [diff] [blame] | 57 | void AvahiMdnsClient::PublishService( |
Alex Vakulenko | 35d119a | 2015-09-10 16:16:27 -0700 | [diff] [blame] | 58 | const std::string& service_type, uint16_t port, |
| 59 | const std::vector<std::string>& txt) { |
Robert Ginda | cf92c66 | 2015-08-20 09:30:11 -0700 | [diff] [blame] | 60 | |
Alex Vakulenko | 35d119a | 2015-09-10 16:16:27 -0700 | [diff] [blame] | 61 | CHECK_EQ("_privet._tcp", service_type); |
Robert Ginda | b50bf54 | 2015-08-24 15:27:26 -0700 | [diff] [blame] | 62 | |
Robert Ginda | cf92c66 | 2015-08-20 09:30:11 -0700 | [diff] [blame] | 63 | if (service_state_ == READY) { |
Robert Ginda | b50bf54 | 2015-08-24 15:27:26 -0700 | [diff] [blame] | 64 | if (service_type_ != service_type || port_ != port) { |
| 65 | // If the type or port of a service changes we have to re-publish |
| 66 | // rather than just update the txt record. |
| 67 | StopPublishing(service_type_); |
| 68 | if (service_state_ != UNDEF) { |
| 69 | LOG(ERROR) << "Failed to disable existing service."; |
| 70 | return; |
| 71 | } |
Robert Ginda | cf92c66 | 2015-08-20 09:30:11 -0700 | [diff] [blame] | 72 | } |
| 73 | } |
| 74 | |
Robert Ginda | cf92c66 | 2015-08-20 09:30:11 -0700 | [diff] [blame] | 75 | service_type_ = service_type; |
| 76 | port_ = port; |
| 77 | txt_ = GetTxtRecord(txt); |
| 78 | |
| 79 | if (avahi_state_ == UNDEF || avahi_state_ == ERROR) { |
| 80 | ConnectToAvahi(); |
Robert Ginda | b50bf54 | 2015-08-24 15:27:26 -0700 | [diff] [blame] | 81 | } else if (service_state_ == READY) { |
| 82 | UpdateServiceTxt(); |
Robert Ginda | cf92c66 | 2015-08-20 09:30:11 -0700 | [diff] [blame] | 83 | } else if (avahi_state_ == READY) { |
Alex Vakulenko | 7f019f8 | 2015-08-21 19:30:36 -0700 | [diff] [blame] | 84 | CreateEntryGroup(); |
Robert Ginda | cf92c66 | 2015-08-20 09:30:11 -0700 | [diff] [blame] | 85 | } else { |
| 86 | CHECK(avahi_state_ == PENDING); |
| 87 | } |
Alex Vakulenko | f0f5534 | 2015-08-18 15:51:40 -0700 | [diff] [blame] | 88 | } |
| 89 | |
Robert Ginda | cf92c66 | 2015-08-20 09:30:11 -0700 | [diff] [blame] | 90 | // TODO(rginda): If we support publishing more than one service then we |
| 91 | // may need a less ambiguous way of unpublishing them. |
| 92 | void AvahiMdnsClient::StopPublishing(const std::string& service_type) { |
Robert Ginda | b50bf54 | 2015-08-24 15:27:26 -0700 | [diff] [blame] | 93 | if (service_type_ != service_type) { |
Robert Ginda | cf92c66 | 2015-08-20 09:30:11 -0700 | [diff] [blame] | 94 | LOG(ERROR) << "Unknown service type: " << service_type; |
| 95 | return; |
| 96 | } |
| 97 | |
| 98 | if (service_state_ != READY) { |
| 99 | LOG(ERROR) << "Service is not published."; |
| 100 | } |
| 101 | |
Robert Ginda | cf92c66 | 2015-08-20 09:30:11 -0700 | [diff] [blame] | 102 | service_type_.clear(); |
| 103 | port_ = 0; |
| 104 | |
| 105 | FreeEntryGroup(); |
Alex Vakulenko | f0f5534 | 2015-08-18 15:51:40 -0700 | [diff] [blame] | 106 | } |
| 107 | |
Robert Ginda | cf92c66 | 2015-08-20 09:30:11 -0700 | [diff] [blame] | 108 | // Transform a service_info to a mDNS compatible TXT record value. |
| 109 | // Concretely, a TXT record consists of a list of strings in the format |
| 110 | // "key=value". Each string must be less than 256 bytes long, since they are |
| 111 | // length/value encoded. Keys may not contain '=' characters, but are |
| 112 | // otherwise unconstrained. |
| 113 | // |
| 114 | // We need a DBus type of "aay", which is a vector<vector<uint8_t>> in our |
| 115 | // bindings. |
| 116 | AvahiMdnsClient::TxtRecord AvahiMdnsClient::GetTxtRecord( |
Alex Vakulenko | 35d119a | 2015-09-10 16:16:27 -0700 | [diff] [blame] | 117 | const std::vector<std::string>& txt) { |
Robert Ginda | cf92c66 | 2015-08-20 09:30:11 -0700 | [diff] [blame] | 118 | TxtRecord result; |
| 119 | result.reserve(txt.size()); |
Alex Vakulenko | 35d119a | 2015-09-10 16:16:27 -0700 | [diff] [blame] | 120 | for (const std::string& s : txt) { |
Robert Ginda | cf92c66 | 2015-08-20 09:30:11 -0700 | [diff] [blame] | 121 | result.emplace_back(); |
| 122 | std::vector<uint8_t>& record = result.back(); |
Alex Vakulenko | 35d119a | 2015-09-10 16:16:27 -0700 | [diff] [blame] | 123 | record.insert(record.end(), s.begin(), s.end()); |
Robert Ginda | cf92c66 | 2015-08-20 09:30:11 -0700 | [diff] [blame] | 124 | } |
| 125 | return result; |
| 126 | } |
| 127 | |
| 128 | void AvahiMdnsClient::ConnectToAvahi() { |
| 129 | avahi_state_ = PENDING; |
| 130 | |
| 131 | avahi_ = bus_->GetObjectProxy( |
| 132 | dbus_constants::avahi::kServiceName, |
| 133 | dbus::ObjectPath(dbus_constants::avahi::kServerPath)); |
| 134 | |
| 135 | // This callback lives for the lifetime of the ObjectProxy. |
| 136 | avahi_->SetNameOwnerChangedCallback( |
| 137 | base::Bind(&AvahiMdnsClient::OnAvahiOwnerChanged, |
| 138 | weak_ptr_factory_.GetWeakPtr())); |
| 139 | |
| 140 | // Reconnect to our signals on a new Avahi instance. |
| 141 | scoped_refptr<AsyncEventSequencer> sequencer(new AsyncEventSequencer()); |
Alex Vakulenko | 4170585 | 2015-10-13 10:12:06 -0700 | [diff] [blame] | 142 | brillo::dbus_utils::ConnectToSignal( |
Robert Ginda | cf92c66 | 2015-08-20 09:30:11 -0700 | [diff] [blame] | 143 | avahi_, |
| 144 | dbus_constants::avahi::kServerInterface, |
| 145 | dbus_constants::avahi::kServerSignalStateChanged, |
| 146 | base::Bind(&AvahiMdnsClient::OnAvahiStateChanged, |
| 147 | weak_ptr_factory_.GetWeakPtr()), |
| 148 | sequencer->GetExportHandler( |
| 149 | dbus_constants::avahi::kServerInterface, |
| 150 | dbus_constants::avahi::kServerSignalStateChanged, |
| 151 | "Failed to subscribe to Avahi state change.", |
| 152 | true)); |
| 153 | sequencer->OnAllTasksCompletedCall( |
| 154 | {// Get a onetime callback with the initial state of Avahi. |
| 155 | AsyncEventSequencer::WrapCompletionTask( |
| 156 | base::Bind(&dbus::ObjectProxy::WaitForServiceToBeAvailable, |
| 157 | avahi_, |
| 158 | base::Bind(&AvahiMdnsClient::OnAvahiAvailable, |
| 159 | weak_ptr_factory_.GetWeakPtr()))), |
| 160 | }); |
| 161 | } |
| 162 | |
| 163 | void AvahiMdnsClient::CreateEntryGroup() { |
| 164 | ErrorPtr error; |
| 165 | |
| 166 | service_state_ = PENDING; |
| 167 | |
| 168 | auto resp = CallMethodAndBlock( |
| 169 | avahi_, dbus_constants::avahi::kServerInterface, |
| 170 | dbus_constants::avahi::kServerMethodEntryGroupNew, |
| 171 | &error); |
| 172 | |
| 173 | dbus::ObjectPath group_path; |
| 174 | if (!resp || !ExtractMethodCallResults(resp.get(), &error, &group_path)) { |
| 175 | service_state_ = ERROR; |
| 176 | LOG(ERROR) << "Error creating group."; |
| 177 | return; |
| 178 | } |
| 179 | entry_group_ = bus_->GetObjectProxy(dbus_constants::avahi::kServiceName, |
| 180 | group_path); |
| 181 | |
Alex Vakulenko | 7f019f8 | 2015-08-21 19:30:36 -0700 | [diff] [blame] | 182 | // If we fail to connect to the StateChange signal for this group, just |
Robert Ginda | cf92c66 | 2015-08-20 09:30:11 -0700 | [diff] [blame] | 183 | // report that the whole thing has failed. |
| 184 | auto on_connect_cb = [](const std::string& interface_name, |
| 185 | const std::string& signal_name, |
| 186 | bool success) { |
| 187 | if (!success) { |
| 188 | LOG(ERROR) << "Failed to connect to StateChange signal " |
| 189 | "from EntryGroup."; |
| 190 | return; |
| 191 | } |
| 192 | }; |
| 193 | |
Alex Vakulenko | 4170585 | 2015-10-13 10:12:06 -0700 | [diff] [blame] | 194 | brillo::dbus_utils::ConnectToSignal( |
Robert Ginda | cf92c66 | 2015-08-20 09:30:11 -0700 | [diff] [blame] | 195 | entry_group_, |
| 196 | dbus_constants::avahi::kGroupInterface, |
| 197 | dbus_constants::avahi::kGroupSignalStateChanged, |
| 198 | base::Bind(&AvahiMdnsClient::HandleGroupStateChanged, |
| 199 | weak_ptr_factory_.GetWeakPtr()), |
| 200 | base::Bind(on_connect_cb)); |
| 201 | |
Alex Vakulenko | 7f019f8 | 2015-08-21 19:30:36 -0700 | [diff] [blame] | 202 | CreateService(); |
Robert Ginda | cf92c66 | 2015-08-20 09:30:11 -0700 | [diff] [blame] | 203 | } |
| 204 | |
| 205 | void AvahiMdnsClient::FreeEntryGroup() { |
| 206 | if (!entry_group_) { |
| 207 | LOG(ERROR) << "No group to free."; |
| 208 | return; |
| 209 | } |
| 210 | |
| 211 | ErrorPtr error; |
| 212 | auto resp = CallMethodAndBlock(entry_group_, |
| 213 | dbus_constants::avahi::kGroupInterface, |
| 214 | dbus_constants::avahi::kGroupMethodFree, |
| 215 | &error); |
| 216 | // Extract and log relevant errors. |
| 217 | bool success = resp && ExtractMethodCallResults(resp.get(), &error); |
| 218 | if (!success) { |
| 219 | LOG(ERROR) << "Error freeing service group"; |
| 220 | } |
| 221 | |
| 222 | // Ignore any signals we may have registered for from this proxy. |
| 223 | entry_group_->Detach(); |
| 224 | entry_group_ = nullptr; |
| 225 | |
| 226 | service_state_ = UNDEF; |
| 227 | } |
| 228 | |
| 229 | void AvahiMdnsClient::CreateService() { |
| 230 | ErrorPtr error; |
| 231 | |
Alex Vakulenko | 0022b75 | 2015-10-02 11:09:59 -0700 | [diff] [blame] | 232 | VLOG(1) << "CreateService: name: " << service_name_ << ", type: " << |
Robert Ginda | cf92c66 | 2015-08-20 09:30:11 -0700 | [diff] [blame] | 233 | service_type_ << ", port: " << port_; |
Robert Ginda | cf92c66 | 2015-08-20 09:30:11 -0700 | [diff] [blame] | 234 | auto resp = CallMethodAndBlock( |
| 235 | entry_group_, |
| 236 | dbus_constants::avahi::kGroupInterface, |
| 237 | dbus_constants::avahi::kGroupMethodAddService, |
| 238 | &error, |
| 239 | int32_t{AVAHI_IF_UNSPEC}, |
| 240 | int32_t{AVAHI_PROTO_UNSPEC}, |
| 241 | uint32_t{0}, // No flags. |
Alex Vakulenko | 0022b75 | 2015-10-02 11:09:59 -0700 | [diff] [blame] | 242 | service_name_, |
Robert Ginda | b50bf54 | 2015-08-24 15:27:26 -0700 | [diff] [blame] | 243 | std::string{"_privet._tcp"}, |
Robert Ginda | cf92c66 | 2015-08-20 09:30:11 -0700 | [diff] [blame] | 244 | std::string{}, // domain. |
| 245 | std::string{}, // hostname |
| 246 | port_, |
| 247 | txt_); |
| 248 | if (!resp || !ExtractMethodCallResults(resp.get(), &error)) { |
| 249 | LOG(ERROR) << "Error creating service"; |
| 250 | service_state_ = ERROR; |
| 251 | return; |
| 252 | } |
| 253 | |
| 254 | resp = CallMethodAndBlock(entry_group_, |
| 255 | dbus_constants::avahi::kGroupInterface, |
| 256 | dbus_constants::avahi::kGroupMethodCommit, |
| 257 | &error); |
| 258 | if (!resp || !ExtractMethodCallResults(resp.get(), &error)) { |
| 259 | LOG(ERROR) << "Error committing service."; |
| 260 | service_state_ = ERROR; |
| 261 | return; |
| 262 | } |
| 263 | |
| 264 | service_state_ = READY; |
| 265 | } |
| 266 | |
Robert Ginda | b50bf54 | 2015-08-24 15:27:26 -0700 | [diff] [blame] | 267 | void AvahiMdnsClient::UpdateServiceTxt() { |
| 268 | ErrorPtr error; |
| 269 | |
| 270 | CHECK_EQ(READY, service_state_); |
| 271 | |
Alex Vakulenko | 0022b75 | 2015-10-02 11:09:59 -0700 | [diff] [blame] | 272 | VLOG(1) << "UpdateServiceTxt: name " << service_name_ << ", type: " << |
Robert Ginda | b50bf54 | 2015-08-24 15:27:26 -0700 | [diff] [blame] | 273 | service_type_ << ", port: " << port_; |
| 274 | auto resp = CallMethodAndBlock( |
| 275 | entry_group_, |
| 276 | dbus_constants::avahi::kGroupInterface, |
| 277 | dbus_constants::avahi::kGroupMethodUpdateServiceTxt, |
| 278 | &error, |
| 279 | int32_t{AVAHI_IF_UNSPEC}, |
| 280 | int32_t{AVAHI_PROTO_UNSPEC}, |
| 281 | uint32_t{0}, // No flags. |
Alex Vakulenko | 0022b75 | 2015-10-02 11:09:59 -0700 | [diff] [blame] | 282 | service_name_, |
Robert Ginda | b50bf54 | 2015-08-24 15:27:26 -0700 | [diff] [blame] | 283 | std::string{"_privet._tcp"}, |
| 284 | std::string{}, // domain. |
| 285 | txt_); |
| 286 | if (!resp || !ExtractMethodCallResults(resp.get(), &error)) { |
| 287 | LOG(ERROR) << "Error creating service"; |
| 288 | service_state_ = ERROR; |
| 289 | return; |
| 290 | } |
| 291 | }; |
| 292 | |
Robert Ginda | cf92c66 | 2015-08-20 09:30:11 -0700 | [diff] [blame] | 293 | void AvahiMdnsClient::OnAvahiOwnerChanged(const std::string& old_owner, |
| 294 | const std::string& new_owner) { |
| 295 | if (new_owner.empty()) { |
| 296 | OnAvahiAvailable(false); |
| 297 | return; |
| 298 | } |
| 299 | OnAvahiAvailable(true); |
| 300 | } |
| 301 | |
| 302 | void AvahiMdnsClient::OnAvahiStateChanged(int32_t state, |
| 303 | const std::string& error) { |
| 304 | HandleAvahiStateChange(state); |
| 305 | } |
| 306 | |
| 307 | void AvahiMdnsClient::OnAvahiAvailable(bool avahi_is_on_dbus) { |
| 308 | VLOG(1) << "Avahi is " << (avahi_is_on_dbus ? "up." : "down."); |
| 309 | int32_t state = AVAHI_SERVER_FAILURE; |
| 310 | if (avahi_is_on_dbus) { |
| 311 | auto resp = CallMethodAndBlock( |
| 312 | avahi_, dbus_constants::avahi::kServerInterface, |
| 313 | dbus_constants::avahi::kServerMethodGetState, |
| 314 | nullptr); |
| 315 | if (!resp || !ExtractMethodCallResults(resp.get(), nullptr, &state)) { |
| 316 | LOG(WARNING) << "Failed to get avahi initial state. Relying on signal."; |
| 317 | } |
| 318 | } |
| 319 | VLOG(1) << "Initial Avahi state=" << state << "."; |
| 320 | HandleAvahiStateChange(state); |
| 321 | } |
| 322 | |
| 323 | void AvahiMdnsClient::HandleAvahiStateChange(int32_t state) { |
| 324 | switch (state) { |
| 325 | case AVAHI_SERVER_RUNNING: { |
| 326 | // All host RRs have been established. |
| 327 | VLOG(1) << "Avahi ready for action."; |
| 328 | if (avahi_state_ == READY) { |
| 329 | LOG(INFO) << "Ignoring redundant Avahi up event."; |
| 330 | return; |
| 331 | } |
| 332 | avahi_state_ = READY; |
Alex Vakulenko | 7f019f8 | 2015-08-21 19:30:36 -0700 | [diff] [blame] | 333 | CreateEntryGroup(); |
Robert Ginda | cf92c66 | 2015-08-20 09:30:11 -0700 | [diff] [blame] | 334 | } break; |
| 335 | case AVAHI_SERVER_INVALID: |
| 336 | // Invalid state (initial). |
| 337 | case AVAHI_SERVER_REGISTERING: |
| 338 | // Host RRs are being registered. |
| 339 | case AVAHI_SERVER_COLLISION: |
| 340 | // There is a collision with a host RR. All host RRs have been withdrawn, |
| 341 | // the user should set a new host name via avahi_server_set_host_name(). |
| 342 | case AVAHI_SERVER_FAILURE: |
| 343 | // Some fatal failure happened, the server is unable to proceed. |
| 344 | avahi_state_ = ERROR; |
Robert Ginda | b50bf54 | 2015-08-24 15:27:26 -0700 | [diff] [blame] | 345 | if (service_state_ != UNDEF) |
| 346 | service_state_ = ERROR; |
| 347 | |
Robert Ginda | cf92c66 | 2015-08-20 09:30:11 -0700 | [diff] [blame] | 348 | LOG(ERROR) << "Avahi changed to error state: " << state; |
| 349 | break; |
| 350 | default: |
| 351 | LOG(ERROR) << "Unknown Avahi server state change to " << state; |
| 352 | break; |
| 353 | } |
| 354 | } |
| 355 | |
| 356 | void AvahiMdnsClient::HandleGroupStateChanged( |
| 357 | int32_t state, |
| 358 | const std::string& error_message) { |
| 359 | if (state == AVAHI_ENTRY_GROUP_COLLISION || |
| 360 | state == AVAHI_ENTRY_GROUP_FAILURE) { |
| 361 | LOG(ERROR) << "Avahi service group error: " << state; |
| 362 | } |
Alex Vakulenko | f0f5534 | 2015-08-18 15:51:40 -0700 | [diff] [blame] | 363 | } |
| 364 | |
| 365 | } // namespace buffet |