| // Copyright (c) 2013 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 "p2p/server/service_publisher.h" |
| |
| #include <avahi-client/client.h> |
| #include <avahi-client/publish.h> |
| #include <avahi-common/error.h> |
| #include <avahi-glib/glib-watch.h> |
| #include <glib.h> |
| |
| #include <map> |
| |
| #include <base/logging.h> |
| #include <base/macros.h> |
| #include <base/strings/stringprintf.h> |
| |
| #include "p2p/common/util.h" |
| |
| using std::map; |
| using std::string; |
| |
| namespace p2p { |
| |
| namespace server { |
| |
| // File sizes can change very quickly and very often so rate-limit |
| // these kind of changes to once every ten seconds. Otherwise we |
| // may end up generate a lot of unnecessary traffic. |
| const int kFileChangedDelayMSec = 10000; |
| |
| class ServicePublisherAvahi : public ServicePublisher { |
| public: |
| explicit ServicePublisherAvahi(uint16_t http_port); |
| |
| ~ServicePublisherAvahi() override; |
| |
| void AddFile(const string& file, size_t file_size) override; |
| |
| void RemoveFile(const string& file) override; |
| |
| void UpdateFileSize(const string& file, size_t file_size) override; |
| |
| void SetNumConnections(int num_connections) override; |
| |
| map<string, size_t> files() override; |
| |
| bool Init(); |
| |
| private: |
| // Callback used for timeout management - see kFileChangedDelayMSec. |
| static gboolean OnDelayTimeoutExpired(gpointer user_data); |
| |
| // Callback used for when Avahi changes state. |
| static void OnAvahiChanged(AvahiClient* client, |
| AvahiClientState state, |
| void* user_data); |
| |
| // Helper for calculating the TXT records to publish. |
| AvahiStringList* CalculateTXTRecords(); |
| |
| // Method used to publish the information in files_ to Avahi. |
| void Publish(bool may_delay); |
| |
| // The TCP port of the HTTP server. |
| uint16_t http_port_; |
| |
| // The LAN name currently used by Avahi. This is used as the |
| // identifier of the DNS-SD service being exported via mDNS. |
| string lan_name_; |
| |
| // Object used for integrating Avahi with the GLib mainloop. |
| AvahiGLibPoll* poll_; |
| |
| // The Avahi object. |
| AvahiClient* client_; |
| |
| // Object used to publish DNS-SD records. |
| AvahiEntryGroup* group_; |
| |
| // The files (and their sizes) to export. These are exported in TXT |
| // records of the DNS-SD service (prefixed with id_). |
| map<string, size_t> files_; |
| |
| // The current number of HTTP connections. This is exported as a |
| // decimal number in the "num-connections" TXT record. |
| int num_connections_; |
| |
| // GLib source id used for timeout management - see kFileChangedDelayMSec. |
| guint delay_timeout_id_; |
| |
| DISALLOW_COPY_AND_ASSIGN(ServicePublisherAvahi); |
| }; |
| |
| ServicePublisherAvahi::ServicePublisherAvahi(uint16_t http_port) |
| : http_port_(http_port), |
| poll_(NULL), |
| client_(NULL), |
| group_(NULL), |
| num_connections_(0), |
| delay_timeout_id_(0) {} |
| |
| ServicePublisherAvahi::~ServicePublisherAvahi() { |
| if (delay_timeout_id_ != 0) |
| g_source_remove(delay_timeout_id_); |
| if (group_ != NULL) |
| avahi_entry_group_free(group_); |
| if (client_ != NULL) |
| avahi_client_free(client_); |
| if (poll_ != NULL) |
| avahi_glib_poll_free(poll_); |
| } |
| |
| AvahiStringList* ServicePublisherAvahi::CalculateTXTRecords() { |
| AvahiStringList* list; |
| string str = base::StringPrintf("num_connections=%d", num_connections_); |
| list = avahi_string_list_new(str.c_str(), NULL); |
| for (auto& item : files_) { |
| string key = string("id_") + item.first; |
| string value = std::to_string(item.second); |
| // TODO(zeuthen): ensure that len(key+"="+value) <= 255 |
| list = avahi_string_list_add_pair(list, key.c_str(), value.c_str()); |
| } |
| return list; |
| } |
| |
| gboolean ServicePublisherAvahi::OnDelayTimeoutExpired(gpointer user_data) { |
| ServicePublisherAvahi* publisher = |
| reinterpret_cast<ServicePublisherAvahi*>(user_data); |
| VLOG(1) << "Publishing timeout expired"; |
| publisher->delay_timeout_id_ = 0; |
| publisher->Publish(false); |
| return FALSE; // Remove timeout source |
| } |
| |
| void ServicePublisherAvahi::Publish(bool may_delay) { |
| int rc; |
| AvahiStringList* txt; |
| |
| if (may_delay) { |
| if (delay_timeout_id_ != 0) { |
| // Already have a timeout, no need to schedule a new one |
| return; |
| } |
| delay_timeout_id_ = |
| g_timeout_add(kFileChangedDelayMSec, |
| static_cast<GSourceFunc>(OnDelayTimeoutExpired), this); |
| VLOG(1) << "Scheduling publishing to happen in " << kFileChangedDelayMSec |
| << " msec"; |
| return; |
| } else { |
| // Not allowed to delay, have to publish immediately .. so if we have |
| // a timeout cancel it |
| if (delay_timeout_id_ != 0) { |
| g_source_remove(delay_timeout_id_); |
| delay_timeout_id_ = 0; |
| VLOG(1) << "Cancelling already scheduled publishing event"; |
| } |
| } |
| |
| VLOG(1) << "Publishing records"; |
| |
| txt = CalculateTXTRecords(); |
| if (group_ == NULL) { |
| group_ = avahi_entry_group_new(client_, NULL, NULL); /* user_data */ |
| if (group_ == NULL) { |
| LOG(ERROR) << "Error creating AvahiEntryGroup: " |
| << avahi_strerror(avahi_client_errno(client_)); |
| avahi_string_list_free(txt); |
| return; |
| } |
| rc = avahi_entry_group_add_service_strlst( |
| group_, AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC, (AvahiPublishFlags)0, |
| lan_name_.c_str(), "_cros_p2p._tcp", |
| /* service type */ |
| NULL, /* domain */ |
| NULL, /* host */ |
| http_port_, /* IP port */ |
| txt); |
| if (rc != AVAHI_OK) { |
| LOG(ERROR) << "Error adding service to AvahiEntryGroup: " |
| << avahi_strerror(avahi_client_errno(client_)); |
| avahi_string_list_free(txt); |
| return; |
| } |
| |
| rc = avahi_entry_group_commit(group_); |
| if (rc != AVAHI_OK) { |
| LOG(ERROR) << "Error committing AvahiEntryGroup: " |
| << avahi_strerror(avahi_client_errno(client_)); |
| } |
| } else { |
| avahi_entry_group_update_service_txt_strlst( |
| group_, AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC, (AvahiPublishFlags)0, |
| lan_name_.c_str(), "_cros_p2p._tcp", |
| /* service type */ |
| NULL, /* domain */ |
| txt); |
| } |
| |
| avahi_string_list_free(txt); |
| } |
| |
| void ServicePublisherAvahi::OnAvahiChanged(AvahiClient* client, |
| AvahiClientState state, |
| void* user_data) { |
| ServicePublisherAvahi* publisher = |
| reinterpret_cast<ServicePublisherAvahi*>(user_data); |
| |
| // So, we're called directly by avahi_client_new() - meaning |
| // client_ member isn't set yet - thanks :-/ |
| if (publisher->client_ == NULL) |
| publisher->client_ = client; |
| |
| VLOG(1) << "OnAvahiChanged, state=" << state; |
| if (state == AVAHI_CLIENT_S_RUNNING) { |
| // Free the existing group, if there is one. This can happen if |
| // e.g. the LAN name used by Avahi changes. |
| if (publisher->group_ != NULL) { |
| avahi_entry_group_free(publisher->group_); |
| publisher->group_ = NULL; |
| } |
| publisher->lan_name_ = string(avahi_client_get_host_name(client)); |
| VLOG(1) << "Server running, publishing services using LAN name '" |
| << publisher->lan_name_ << "'"; |
| publisher->Publish(false); |
| } |
| } |
| |
| bool ServicePublisherAvahi::Init() { |
| int error; |
| |
| poll_ = avahi_glib_poll_new(NULL, G_PRIORITY_DEFAULT); |
| client_ = avahi_client_new(avahi_glib_poll_get(poll_), (AvahiClientFlags)0, |
| OnAvahiChanged, this, &error); |
| if (client_ == NULL) { |
| LOG(ERROR) << "Error constructing AvahiClient: " << error; |
| return false; |
| } |
| return true; |
| } |
| |
| void ServicePublisherAvahi::AddFile(const string& file, size_t file_size) { |
| files_[file] = file_size; |
| Publish(false); |
| } |
| |
| void ServicePublisherAvahi::RemoveFile(const string& file) { |
| if (files_.erase(file) != 1) { |
| LOG(WARNING) << "Removing file " << file << " not in map"; |
| } |
| Publish(false); |
| } |
| |
| void ServicePublisherAvahi::UpdateFileSize(const string& file, |
| size_t file_size) { |
| auto it = files_.find(file); |
| if (it == files_.end()) { |
| LOG(WARNING) << "Trying to set size for file " << file << " not in map"; |
| return; |
| } |
| it->second = file_size; |
| Publish(true); |
| } |
| |
| void ServicePublisherAvahi::SetNumConnections(int num_connections) { |
| if (num_connections_ == num_connections) |
| return; |
| num_connections_ = num_connections; |
| Publish(false); |
| } |
| |
| map<string, size_t> ServicePublisherAvahi::files() { |
| return files_; |
| } |
| |
| // ----------------------------------------------------------------------------- |
| |
| ServicePublisher* ServicePublisher::Construct(uint16_t http_port) { |
| ServicePublisherAvahi* instance = new ServicePublisherAvahi(http_port); |
| if (!instance->Init()) { |
| delete instance; |
| return NULL; |
| } else { |
| return instance; |
| } |
| } |
| |
| } // namespace server |
| |
| } // namespace p2p |