diff --git a/src/zeroconf/ZeroconfAvahi.cxx b/src/zeroconf/ZeroconfAvahi.cxx index 2d6047381..30f39697f 100644 --- a/src/zeroconf/ZeroconfAvahi.cxx +++ b/src/zeroconf/ZeroconfAvahi.cxx @@ -21,41 +21,30 @@ #include "avahi/Client.hxx" #include "avahi/ConnectionListener.hxx" #include "avahi/ErrorHandler.hxx" +#include "avahi/Publisher.hxx" +#include "avahi/Service.hxx" #include "ZeroconfInternal.hxx" #include "Listen.hxx" #include "util/Domain.hxx" #include "util/RuntimeError.hxx" #include "Log.hxx" -#include -#include - -#include #include -#include -#include static constexpr Domain avahi_domain("avahi"); -class AvahiGlue final : Avahi::ErrorHandler, Avahi::ConnectionListener { +class AvahiGlue final : Avahi::ErrorHandler { public: Avahi::Client client; + Avahi::Publisher publisher; - explicit AvahiGlue(EventLoop &event_loop) - :client(event_loop, *this) + AvahiGlue(EventLoop &event_loop, + const char *name, std::forward_list services) + :client(event_loop, *this), + publisher(client, name, std::move(services), *this) { - client.AddListener(*this); } - ~AvahiGlue() noexcept { - client.RemoveListener(*this); - } - - /* virtual methods from class AvahiConnectionListener */ - void OnAvahiConnect(AvahiClient *client) noexcept override; - void OnAvahiDisconnect() noexcept override; - void OnAvahiChanged() noexcept override; - /* virtual methods from class Avahi::ErrorHandler */ bool OnAvahiError(std::exception_ptr e) noexcept override { LogError(e); @@ -63,141 +52,7 @@ public: } }; -static char *avahi_name; static AvahiGlue *avahi_glue; -static AvahiEntryGroup *avahi_group; - -static void -AvahiRegisterService(AvahiClient *c); - -/** - * Callback when the EntryGroup changes state. - */ -static void -AvahiGroupCallback(AvahiEntryGroup *g, - AvahiEntryGroupState state, - [[maybe_unused]] void *userdata) -{ - assert(g != nullptr); - - FormatDebug(avahi_domain, - "Service group changed to state %d", state); - - switch (state) { - case AVAHI_ENTRY_GROUP_ESTABLISHED: - /* The entry group has been established successfully */ - FormatNotice(avahi_domain, - "Service '%s' successfully established.", - avahi_name); - break; - - case AVAHI_ENTRY_GROUP_COLLISION: - /* A service name collision happened. Let's pick a new name */ - { - char *n = avahi_alternative_service_name(avahi_name); - avahi_free(avahi_name); - avahi_name = n; - } - - FormatNotice(avahi_domain, - "Service name collision, renaming service to '%s'", - avahi_name); - - /* And recreate the services */ - AvahiRegisterService(avahi_entry_group_get_client(g)); - break; - - case AVAHI_ENTRY_GROUP_FAILURE: - FormatError(avahi_domain, - "Entry group failure: %s", - avahi_strerror(avahi_client_errno(avahi_entry_group_get_client(g)))); - /* Some kind of failure happened while we were - registering our services */ - break; - - case AVAHI_ENTRY_GROUP_UNCOMMITED: - LogDebug(avahi_domain, "Service group is UNCOMMITED"); - break; - - case AVAHI_ENTRY_GROUP_REGISTERING: - LogDebug(avahi_domain, "Service group is REGISTERING"); - } -} - -/** - * Registers a new service with avahi. - */ -static void -AvahiRegisterService(AvahiClient *c) -{ - assert(c != nullptr); - - FormatDebug(avahi_domain, "Registering service %s/%s", - SERVICE_TYPE, avahi_name); - - /* If this is the first time we're called, - * let's create a new entry group */ - if (!avahi_group) { - avahi_group = avahi_entry_group_new(c, AvahiGroupCallback, nullptr); - if (!avahi_group) { - FormatError(avahi_domain, - "Failed to create avahi EntryGroup: %s", - avahi_strerror(avahi_client_errno(c))); - return; - } - } - - /* Add the service */ - /* TODO: This currently binds to ALL interfaces. - * We could maybe add a service per actual bound interface, - * if that's better. */ - int result = avahi_entry_group_add_service(avahi_group, - AVAHI_IF_UNSPEC, - AVAHI_PROTO_UNSPEC, - AvahiPublishFlags(0), - avahi_name, SERVICE_TYPE, - nullptr, nullptr, - listen_port, nullptr); - if (result < 0) { - FormatError(avahi_domain, "Failed to add service %s: %s", - SERVICE_TYPE, avahi_strerror(result)); - return; - } - - /* Tell the server to register the service group */ - result = avahi_entry_group_commit(avahi_group); - if (result < 0) { - FormatError(avahi_domain, "Failed to commit service group: %s", - avahi_strerror(result)); - return; - } -} - -void -AvahiGlue::OnAvahiConnect(AvahiClient *c) noexcept -{ - if (avahi_group == nullptr) - AvahiRegisterService(c); -} - -void -AvahiGlue::OnAvahiDisconnect() noexcept -{ - if (avahi_group != nullptr) { - avahi_entry_group_free(avahi_group); - avahi_group = nullptr; - } -} - -void -AvahiGlue::OnAvahiChanged() noexcept -{ - if (avahi_group != nullptr) { - LogDebug(avahi_domain, "Resetting group"); - avahi_entry_group_reset(avahi_group); - } -} - void AvahiInit(EventLoop &loop, const char *serviceName) @@ -207,9 +62,12 @@ AvahiInit(EventLoop &loop, const char *serviceName) if (!avahi_is_valid_service_name(serviceName)) throw FormatRuntimeError("Invalid zeroconf_name \"%s\"", serviceName); - avahi_name = avahi_strdup(serviceName); + std::forward_list services; + services.emplace_front(AVAHI_IF_UNSPEC, + AVAHI_PROTO_UNSPEC, + SERVICE_TYPE, listen_port); - avahi_glue = new AvahiGlue(loop); + avahi_glue = new AvahiGlue(loop, serviceName, std::move(services)); } void @@ -217,13 +75,5 @@ AvahiDeinit() { LogDebug(avahi_domain, "Shutting down interface"); - if (avahi_group != nullptr) { - avahi_entry_group_free(avahi_group); - avahi_group = nullptr; - } - delete avahi_glue; - - avahi_free(avahi_name); - avahi_name = nullptr; } diff --git a/src/zeroconf/avahi/EntryGroup.hxx b/src/zeroconf/avahi/EntryGroup.hxx new file mode 100644 index 000000000..deaa2c3f5 --- /dev/null +++ b/src/zeroconf/avahi/EntryGroup.hxx @@ -0,0 +1,49 @@ +/* + * Copyright 2007-2021 CM4all GmbH + * All rights reserved. + * + * author: Max Kellermann + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the + * distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * FOUNDATION OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include + +#include + +namespace Avahi { + +struct EntryGroupDeleter { + void operator()(AvahiEntryGroup *g) noexcept { + avahi_entry_group_free(g); + } +}; + +using EntryGroupPtr = std::unique_ptr; + +} // namespace Avahi diff --git a/src/zeroconf/avahi/Publisher.cxx b/src/zeroconf/avahi/Publisher.cxx new file mode 100644 index 000000000..ab4d1198d --- /dev/null +++ b/src/zeroconf/avahi/Publisher.cxx @@ -0,0 +1,231 @@ +/* + * Copyright 2007-2021 CM4all GmbH + * All rights reserved. + * + * author: Max Kellermann + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the + * distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * FOUNDATION OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "Publisher.hxx" +#include "Service.hxx" +#include "Client.hxx" +#include "Error.hxx" +#include "ErrorHandler.hxx" +#include "net/SocketAddress.hxx" + +#include +#include +#include + +#include + +#include +#include + +namespace Avahi { + +/** + * Append the process id to the given prefix string. This is used as + * a workaround for an avahi-daemon bug/problem: when a service gets + * restarted, and then binds to a new port number (e.g. beng-proxy + * with automatic port assignment), we don't get notified, and so we + * never query the new port. By appending the process id to the + * client name, we ensure that the exiting old process broadcasts + * AVAHI_BROWSER_REMOVE, and hte new process broadcasts + * AVAHI_BROWSER_NEW. + */ +static std::string +MakePidName(const char *prefix) +{ + char buffer[256]; + snprintf(buffer, sizeof(buffer), "%s[%u]", prefix, (unsigned)getpid()); + return buffer; +} + +Publisher::Publisher(Client &_client, const char *_name, + std::forward_list _services, + ErrorHandler &_error_handler) noexcept + :error_handler(_error_handler), + name(MakePidName(_name)), + client(_client), services(std::move(_services)) +{ + assert(!services.empty()); + + client.AddListener(*this); + + auto *c = client.GetClient(); + if (c != nullptr) + RegisterServices(c); +} + +Publisher::~Publisher() noexcept +{ + client.RemoveListener(*this); +} + +inline void +Publisher::GroupCallback(AvahiEntryGroup *g, + AvahiEntryGroupState state) noexcept +{ + switch (state) { + case AVAHI_ENTRY_GROUP_ESTABLISHED: + break; + + case AVAHI_ENTRY_GROUP_COLLISION: + if (!visible) + /* meanwhile, HideServices() has been called */ + return; + + /* pick a new name */ + + { + char *new_name = avahi_alternative_service_name(name.c_str()); + name = new_name; + avahi_free(new_name); + } + + /* And recreate the services */ + RegisterServices(avahi_entry_group_get_client(g)); + break; + + case AVAHI_ENTRY_GROUP_FAILURE: + error_handler.OnAvahiError(std::make_exception_ptr(MakeError(*avahi_entry_group_get_client(g), + "Avahi service group failure"))); + break; + + case AVAHI_ENTRY_GROUP_UNCOMMITED: + case AVAHI_ENTRY_GROUP_REGISTERING: + break; + } +} + +void +Publisher::GroupCallback(AvahiEntryGroup *g, + AvahiEntryGroupState state, + void *userdata) noexcept +{ + auto &publisher = *(Publisher *)userdata; + publisher.GroupCallback(g, state); +} + +static void +AddService(AvahiEntryGroup &group, const Service &service, + const char *name) +{ + int error = avahi_entry_group_add_service(&group, + service.interface, + service.protocol, + AvahiPublishFlags(0), + name, service.type.c_str(), + nullptr, nullptr, + service.port, nullptr); + if (error != AVAHI_OK) + throw MakeError(error, "Failed to add Avahi service"); +} + +static void +AddServices(AvahiEntryGroup &group, + const std::forward_list &services, const char *name) +{ + for (const auto &i : services) + AddService(group, i, name); +} + +static EntryGroupPtr +MakeEntryGroup(AvahiClient &c, + const std::forward_list &services, const char *name, + AvahiEntryGroupCallback callback, void *userdata) +{ + EntryGroupPtr group(avahi_entry_group_new(&c, callback, userdata)); + if (!group) + throw MakeError(c, "Failed to create Avahi service group"); + + AddServices(*group, services, name); + + int error = avahi_entry_group_commit(group.get()); + if (error != AVAHI_OK) + throw MakeError(error, "Failed to commit Avahi service group"); + + return group; +} + +void +Publisher::RegisterServices(AvahiClient *c) noexcept +{ + assert(visible); + + try { + group = MakeEntryGroup(*c, services, name.c_str(), + GroupCallback, this); + } catch (...) { + error_handler.OnAvahiError(std::current_exception()); + } +} + +void +Publisher::HideServices() noexcept +{ + if (!visible) + return; + + visible = false; + group.reset(); +} + +void +Publisher::ShowServices() noexcept +{ + if (visible) + return; + + visible = true; + + auto *c = client.GetClient(); + if (c != nullptr) + RegisterServices(c); +} + +void +Publisher::OnAvahiConnect(AvahiClient *c) noexcept +{ + if (group == nullptr && visible) + RegisterServices(c); +} + +void +Publisher::OnAvahiDisconnect() noexcept +{ + group.reset(); +} + +void +Publisher::OnAvahiChanged() noexcept +{ + group.reset(); +} + +} // namespace Avahi diff --git a/src/zeroconf/avahi/Publisher.hxx b/src/zeroconf/avahi/Publisher.hxx new file mode 100644 index 000000000..a12b56de4 --- /dev/null +++ b/src/zeroconf/avahi/Publisher.hxx @@ -0,0 +1,107 @@ +/* + * Copyright 2007-2021 CM4all GmbH + * All rights reserved. + * + * author: Max Kellermann + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the + * distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * FOUNDATION OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include "EntryGroup.hxx" +#include "ConnectionListener.hxx" + +#include + +#include +#include + +class SocketAddress; + +namespace Avahi { + +struct Service; +class ErrorHandler; +class Client; + +/** + * A helper class which manages a list of services to be published via + * Avahi/Zeroconf. + */ +class Publisher final : ConnectionListener { + ErrorHandler &error_handler; + + std::string name; + + Client &client; + + EntryGroupPtr group; + + const std::forward_list services; + + /** + * Shall the published services be visible? This is controlled by + * HideServices() and ShowServices(). + */ + bool visible = true; + +public: + Publisher(Client &client, const char *_name, + std::forward_list _services, + ErrorHandler &_error_handler) noexcept; + ~Publisher() noexcept; + + Publisher(const Publisher &) = delete; + Publisher &operator=(const Publisher &) = delete; + + /** + * Temporarily hide all registered services. You can undo this + * with ShowServices(). + */ + void HideServices() noexcept; + + /** + * Undo HideServices(). + */ + void ShowServices() noexcept; + +private: + void GroupCallback(AvahiEntryGroup *g, + AvahiEntryGroupState state) noexcept; + static void GroupCallback(AvahiEntryGroup *g, + AvahiEntryGroupState state, + void *userdata) noexcept; + + void RegisterServices(AvahiClient *c) noexcept; + + /* virtual methods from class AvahiConnectionListener */ + void OnAvahiConnect(AvahiClient *client) noexcept override; + void OnAvahiDisconnect() noexcept override; + void OnAvahiChanged() noexcept override; +}; + +} // namespace Avahi diff --git a/src/zeroconf/avahi/Service.hxx b/src/zeroconf/avahi/Service.hxx new file mode 100644 index 000000000..4798c3eb1 --- /dev/null +++ b/src/zeroconf/avahi/Service.hxx @@ -0,0 +1,54 @@ +/* + * Copyright 2007-2021 CM4all GmbH + * All rights reserved. + * + * author: Max Kellermann + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the + * distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * FOUNDATION OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include + +#include +#include + +namespace Avahi { + +struct Service { + AvahiIfIndex interface = AVAHI_IF_UNSPEC; + AvahiProtocol protocol = AVAHI_PROTO_UNSPEC; + std::string type; + uint16_t port; + + Service(AvahiIfIndex _interface, AvahiProtocol _protocol, + const char *_type, uint16_t _port) noexcept + :interface(_interface), protocol(_protocol), + type(_type), port(_port) {} +}; + +} // namespace Avahi diff --git a/src/zeroconf/avahi/meson.build b/src/zeroconf/avahi/meson.build index 0d47f5988..d13b55201 100644 --- a/src/zeroconf/avahi/meson.build +++ b/src/zeroconf/avahi/meson.build @@ -9,6 +9,7 @@ avahi = static_library( 'Client.cxx', 'Error.cxx', 'Poll.cxx', + 'Publisher.cxx', include_directories: inc, dependencies: [ libavahi_client,