1 /*
2 AirSane Imaging Daemon
3 Copyright (C) 2018-2021 Simul Piscator
4 
5 This program is free software: you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation, either version 3 of the License, or
8 (at your option) any later version.
9 
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 GNU General Public License for more details.
14 
15 You should have received a copy of the GNU General Public License
16 along with this program.  If not, see <http://www.gnu.org/licenses/>.
17 */
18 
19 #include "mdnspublisher.h"
20 
21 #include <avahi-client/client.h>
22 #include <avahi-client/publish.h>
23 #include <avahi-common/alternative.h>
24 #include <avahi-common/error.h>
25 #include <avahi-common/malloc.h>
26 #include <avahi-common/thread-watch.h>
27 
28 #include <algorithm>
29 #include <cerrno>
30 #include <cstring>
31 #include <iostream>
32 #include <list>
33 
34 namespace {
35 
36 class AvahiThreadedPollGuard
37 {
38 public:
AvahiThreadedPollGuard(AvahiThreadedPoll * pThread)39   explicit AvahiThreadedPollGuard(AvahiThreadedPoll* pThread)
40     : mpThread(pThread)
41   {
42     ::avahi_threaded_poll_lock(mpThread);
43   }
~AvahiThreadedPollGuard()44   ~AvahiThreadedPollGuard() { ::avahi_threaded_poll_unlock(mpThread); }
45 
46 private:
47   AvahiThreadedPoll* mpThread;
48 };
49 
50 struct ServiceEntry
51 {
52   MdnsPublisher::Service* mpService;
53   AvahiEntryGroup* mpEntryGroup;
54 
ServiceEntry__anon96e916750111::ServiceEntry55   ServiceEntry(MdnsPublisher::Service* p)
56     : mpService(p)
57     , mpEntryGroup(nullptr)
58   {}
59 
~ServiceEntry__anon96e916750111::ServiceEntry60   ~ServiceEntry() { unannounce(); }
61 
announce__anon96e916750111::ServiceEntry62   bool announce(AvahiClient* pClient)
63   {
64     unannounce();
65     int err;
66     do {
67       err = doAnnounce(pClient);
68       if (err == AVAHI_ERR_COLLISION)
69         renameService();
70     } while (err == AVAHI_ERR_COLLISION);
71     if (err) {
72       std::cerr << "Avahi error when adding service: " << ::avahi_strerror(err)
73                 << " (" << err << ")" << std::endl;
74     }
75     return !err;
76   }
77 
doAnnounce__anon96e916750111::ServiceEntry78   int doAnnounce(AvahiClient* pClient)
79   {
80     if (!pClient) {
81       std::cerr << "No avahi client instance available, not registering service"
82                 << std::endl;
83       return AVAHI_ERR_FAILURE;
84     }
85     if (mpEntryGroup)
86       ::avahi_entry_group_free(mpEntryGroup);
87     mpEntryGroup = ::avahi_entry_group_new(pClient, &entryGroupCallback, this);
88     if (!mpEntryGroup)
89       return ::avahi_client_errno(pClient);
90     int err = ::avahi_entry_group_add_service(mpEntryGroup,
91                                               mpService->interfaceIndex(),
92                                               AVAHI_PROTO_UNSPEC,
93                                               AvahiPublishFlags(0),
94                                               mpService->name().c_str(),
95                                               mpService->type().c_str(),
96                                               nullptr,
97                                               nullptr,
98                                               mpService->port(),
99                                               nullptr);
100     if (!err)
101       err = ::avahi_entry_group_commit(mpEntryGroup);
102     if (!err) {
103       AvahiStringList* txt = nullptr;
104       for (const auto& entry : mpService->txtRecord())
105         txt = ::avahi_string_list_add_pair(
106           txt, entry.first.c_str(), entry.second.c_str());
107       err = ::avahi_entry_group_update_service_txt_strlst(
108         mpEntryGroup,
109         mpService->interfaceIndex(),
110         AVAHI_PROTO_UNSPEC,
111         AvahiPublishFlags(0),
112         mpService->name().c_str(),
113         mpService->type().c_str(),
114         nullptr,
115         txt);
116       ::avahi_string_list_free(txt);
117     }
118     return err;
119   }
120 
unannounce__anon96e916750111::ServiceEntry121   void unannounce()
122   {
123     if (mpEntryGroup) {
124       ::avahi_entry_group_free(mpEntryGroup);
125       mpEntryGroup = nullptr;
126     }
127   }
128 
renameService__anon96e916750111::ServiceEntry129   void renameService()
130   {
131     char* altname = ::avahi_alternative_service_name(mpService->name().c_str());
132     mpService->setName(altname);
133     ::avahi_free(altname);
134   }
135 
onCollision__anon96e916750111::ServiceEntry136   void onCollision()
137   {
138     renameService();
139     announce(::avahi_entry_group_get_client(mpEntryGroup));
140   }
141 
onError__anon96e916750111::ServiceEntry142   void onError() { unannounce(); }
143 
entryGroupCallback__anon96e916750111::ServiceEntry144   static void entryGroupCallback(AvahiEntryGroup*,
145                                  AvahiEntryGroupState state,
146                                  void* instance)
147   {
148     auto p = static_cast<ServiceEntry*>(instance);
149     switch (state) {
150       case AVAHI_ENTRY_GROUP_COLLISION:
151         p->onCollision();
152         break;
153       case AVAHI_ENTRY_GROUP_FAILURE:
154         p->onError();
155         break;
156       case AVAHI_ENTRY_GROUP_UNCOMMITED:
157       case AVAHI_ENTRY_GROUP_REGISTERING:
158       case AVAHI_ENTRY_GROUP_ESTABLISHED:
159         break;
160     }
161   }
162 };
163 
164 } // namespace
165 
166 struct MdnsPublisher::Private
167 {
168   AvahiThreadedPoll* mpThread;
169   AvahiClient* mpClient;
170   AvahiClientState mState;
171 
172   std::string mHostnameFqdn, mHostname;
173   std::list<ServiceEntry> mServices;
174 
PrivateMdnsPublisher::Private175   Private()
176     : mpThread(nullptr)
177     , mpClient(nullptr)
178     , mState(AVAHI_CLIENT_CONNECTING)
179   {
180     mpThread = ::avahi_threaded_poll_new();
181     if (mpThread) {
182       createClient();
183       if (::avahi_threaded_poll_start(mpThread)) {
184         int err = errno;
185         std::cerr << ::strerror(err) << std::endl;
186       }
187     }
188   }
~PrivateMdnsPublisher::Private189   ~Private()
190   {
191     if (mpThread) {
192       ::avahi_threaded_poll_stop(mpThread);
193       destroyClient();
194       ::avahi_threaded_poll_free(mpThread);
195     }
196   }
197 
clientCallbackMdnsPublisher::Private198   static void clientCallback(AvahiClient* client,
199                              AvahiClientState state,
200                              void* instance)
201   {
202     auto p = static_cast<Private*>(instance);
203     if (p->mState == AVAHI_CLIENT_CONNECTING)
204       switch (state) {
205         case AVAHI_CLIENT_S_COLLISION:
206         case AVAHI_CLIENT_S_REGISTERING:
207         case AVAHI_CLIENT_S_RUNNING:
208           p->onConnected();
209           break;
210         case AVAHI_CLIENT_FAILURE:
211         case AVAHI_CLIENT_CONNECTING:
212           break;
213       }
214     if (state == AVAHI_CLIENT_FAILURE) {
215       if (::avahi_client_errno(client) == AVAHI_ERR_DISCONNECTED)
216         p->onDisconnected();
217       else
218         p->onError(::avahi_client_errno(client));
219     }
220     p->mState = state;
221   }
222 
createClientMdnsPublisher::Private223   void createClient()
224   {
225     destroyClient();
226     const AvahiPoll* pPoll = ::avahi_threaded_poll_get(mpThread);
227     int err = 0;
228     mpClient = ::avahi_client_new(
229       pPoll, AVAHI_CLIENT_NO_FAIL, &clientCallback, this, &err);
230     if (!mpClient) {
231       std::cerr << "Failed to create avahi client: " << ::avahi_strerror(err)
232                 << " (" << err << ")" << std::endl;
233       return;
234     }
235     const char* name = ::avahi_client_get_host_name_fqdn(mpClient);
236     if (!name) {
237       int err = ::avahi_client_errno(mpClient);
238       std::cerr << "Failed to get host name: " << ::avahi_strerror(err) << " ("
239                 << err << ")" << std::endl;
240       return;
241     }
242     mHostnameFqdn = name;
243     size_t pos = mHostnameFqdn.find('.');
244     mHostname = mHostnameFqdn.substr(0, pos);
245   }
246 
destroyClientMdnsPublisher::Private247   void destroyClient()
248   {
249     if (mpClient) {
250       for (auto& entry : mServices)
251         entry.unannounce();
252       ::avahi_client_free(mpClient);
253     }
254     mpClient = nullptr;
255   }
256 
onConnectedMdnsPublisher::Private257   void onConnected()
258   {
259     for (auto& entry : mServices)
260       entry.announce(mpClient);
261   }
262 
onDisconnectedMdnsPublisher::Private263   void onDisconnected()
264   {
265     destroyClient();
266     createClient();
267   }
268 
onErrorMdnsPublisher::Private269   void onError(int err)
270   {
271     std::clog << ::avahi_strerror(err) << std::endl;
272     destroyClient();
273   }
274 
findServiceMdnsPublisher::Private275   std::list<ServiceEntry>::iterator findService(const Service* pService)
276   {
277     auto i = std::find_if(mServices.begin(),
278                           mServices.end(),
279                           [pService](const ServiceEntry& entry) {
280                             return entry.mpService == pService;
281                           });
282     return i;
283   }
284 };
285 
MdnsPublisher()286 MdnsPublisher::MdnsPublisher()
287   : p(new Private)
288 {}
289 
~MdnsPublisher()290 MdnsPublisher::~MdnsPublisher()
291 {
292   delete p;
293 }
294 
295 const std::string&
hostname() const296 MdnsPublisher::hostname() const
297 {
298   return p->mHostname;
299 }
300 
301 const std::string&
hostnameFqdn() const302 MdnsPublisher::hostnameFqdn() const
303 {
304   return p->mHostnameFqdn;
305 }
306 
307 bool
announce(MdnsPublisher::Service * pService)308 MdnsPublisher::announce(MdnsPublisher::Service* pService)
309 {
310   AvahiThreadedPollGuard guard(p->mpThread);
311   bool ok = false;
312   auto i = p->findService(pService);
313   if (i != p->mServices.end()) {
314     ok = true;
315   } else {
316     p->mServices.push_back(ServiceEntry(pService));
317     ok = p->mServices.back().announce(p->mpClient);
318   }
319   return ok;
320 }
321 
322 bool
unannounce(MdnsPublisher::Service * pService)323 MdnsPublisher::unannounce(MdnsPublisher::Service* pService)
324 {
325   AvahiThreadedPollGuard guard(p->mpThread);
326   bool ok = false;
327   auto i = p->findService(pService);
328   if (i != p->mServices.end()) {
329     ok = true;
330     i->unannounce();
331     p->mServices.erase(i);
332   }
333   return ok;
334 }
335