File: zeroconf.cpp

package info (click to toggle)
aseba 1.6.0-5
  • links: PTS, VCS
  • area: main
  • in suites: buster
  • size: 18,300 kB
  • sloc: cpp: 44,647; ansic: 5,686; python: 1,455; java: 1,136; sh: 393; xml: 202; makefile: 10
file content (340 lines) | stat: -rw-r--r-- 12,097 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
/*
	Aseba - an event-based framework for distributed robot control
	Copyright (C) 2007--2016:
		Stephane Magnenat <stephane at magnenat dot net>
		(http://stephane.magnenat.net)
		and other contributors, see authors.txt for details

	This program is free software: you can redistribute it and/or modify
	it under the terms of the GNU Lesser General Public License as published
	by the Free Software Foundation, version 3 of the License.

	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU Lesser General Public License for more details.

	You should have received a copy of the GNU Lesser General Public License
	along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

#ifndef WIN32
#include <netinet/in.h>
#else
#include <winsock2.h>
#endif

#include "../utils/FormatableString.h"
#include <dashel/dashel.h>
#include "zeroconf.h"
#include "dns_sd.h"

using namespace std;

namespace Aseba
{
	using namespace Dashel;

	//! Callbacks for the DNS Service Discovery API, in a class to make it friend with Zeroconf
	//! while not exposing the #include "dns_sd.h" in zeroconf.h
	struct ZeroconfCallbacks
	{
		static void DNSSD_API registerReply(DNSServiceRef sdRef, DNSServiceFlags flags, DNSServiceErrorType errorCode, const char *name, const char *regtype, const char *domain, void *context);
		static void DNSSD_API browseReply(DNSServiceRef sdRef, DNSServiceFlags flags, uint32_t interfaceIndex, DNSServiceErrorType errorCode, const char *serviceName, const char *regtype, const char *replyDomain, void *context);
		static void DNSSD_API resolveReply(DNSServiceRef sdRef, DNSServiceFlags flags, uint32_t interfaceIndex, DNSServiceErrorType errorCode, const char *fullname, const char *hosttarget, uint16_t port, /* In network byte order */ uint16_t txtLen, const unsigned char *txtRecord, void *context);
	};

	const Zeroconf::Target::StateNameMap Zeroconf::Target::stateToString {
		{ State::UNINITIALIZED, "uninitialized" },
		{ State::REGISTRATING, "registering" },
		{ State::REGISTERED, "registered" },
		{ State::RESOLVING, "resolving" }
	};

	//! Advertise a target with a given name and port, with associated txtrec.
	//! If the target already exists, its txtrec is updated.
	void Zeroconf::advertise(const std::string & name, const int port, const TxtRecord & txtrec)
	{
		auto targetIt(getTarget(name, port));
		if (targetIt == targets.end())
		{
			// Target does not exist, create
			targets.emplace_back(name, port, *this);
			registerTarget(targets.back(), txtrec);
		}
		else
		{
			// Target does exist, update
			updateTarget(*targetIt, txtrec);
		}
	}

	//! Advertise a target for a given Dashel stream, with associated txtrec.
	//! If the target already exists, its txtrec is updated.
	void Zeroconf::advertise(const std::string & name, const Dashel::Stream * stream, const TxtRecord & txtrec)
	{
		auto targetIt(getTarget(name, stream));
		if (targetIt == targets.end())
		{
			// Target does not exist, create
			targets.emplace_back(name, stream, *this);
			registerTarget(targets.back(), txtrec);
		}
		else
		{
			// Target does exist, update
			updateTarget(*targetIt, txtrec);
		}
	}

	//! Forget a target with a given name and port, if the target is unknown, do nothing.
	void Zeroconf::forget(const std::string & name, const int port)
	{
		forget(getTarget(name, port));
	}

	//! Forget a target for a given Dashel stream, if the target is unknown, do nothing.
	void Zeroconf::forget(const std::string & name, const Dashel::Stream * stream)
	{
		forget(getTarget(name, stream));
	}

	//! Forget a target referenced by its iterator
	void Zeroconf::forget(Targets::iterator targetIt)
	{
		if (targetIt != targets.end())
		{
			const Target& target(*targetIt);
			if (target.state != Target::State::REGISTERED && target.state != Target::State::REGISTRATING)
				throw Zeroconf::Error(FormatableString("forget: target in invalid state: %0").arg(target.stateToString.at(target.state)));
			if (target.state == Target::State::REGISTRATING)
				targetsToRemoveUponRegistration.insert(target.serviceRef);
			else
				targets.erase(targetIt);
		}
	}

	//! A target can ask Zeroconf to register it in DNS, with additional information in a TXT record
	void Zeroconf::registerTarget(Zeroconf::Target & target, const TxtRecord& txtrec)
	{
		string txt{txtrec.record()};
		uint16_t len = txt.size();
		const char* record = txt.c_str();
		auto err = DNSServiceRegister(&(target.serviceRef),
					      0, // no flags
					      0, // default all interfaces
					      target.name.c_str(),
					      "_aseba._tcp",
					      nullptr, // use default domain, usually "local."
					      nullptr, // use this host name
					      htons(target.port),
					      len, // TXT length
					      record, // TXT record
					      ZeroconfCallbacks::registerReply,
					      this); // context pointer is Zeroconf
		target.state = Zeroconf::Target::State::REGISTRATING;
		if (err != kDNSServiceErr_NoError)
		{
			// register failed, remove target and throw exception
			auto targetIt(getTarget(target.serviceRef));
			assert(targetIt != targets.end());
			targets.erase(targetIt);
			throw Zeroconf::Error(FormatableString("DNSServiceRegister: error %0").arg(err));
		}
		else
			processServiceRef(target.serviceRef);
	}

	//! DNSSD callback for registerTarget, update Zeroconf::Target record with results of registration
	void DNSSD_API ZeroconfCallbacks::registerReply(DNSServiceRef sdRef,
					     DNSServiceFlags flags,
					     DNSServiceErrorType errorCode,
					     const char *name,
					     const char *regtype,
					     const char *domain,
					     void *context)
	{
		// retrieve target
		auto zeroconf(static_cast<Zeroconf *>(context));
		auto targetIt(zeroconf->getTarget(sdRef));
		if (targetIt == zeroconf->targets.end())
			return;
		if (errorCode != kDNSServiceErr_NoError)
		{
			zeroconf->targets.erase(targetIt);
			throw Zeroconf::Error(FormatableString("DNSServiceRegisterReply: error %0").arg(errorCode));
		}
		else
		{
			Zeroconf::Target& target(*targetIt);
			// fill informations
			target.name = name;
			target.domain = domain;
			target.regtype = regtype;
			target.state = Zeroconf::Target::State::REGISTERED;
			target.registerCompleted();
			// find if this target should be directly deleted
			auto targetRemoveIt(zeroconf->targetsToRemoveUponRegistration.find(target.serviceRef));
			if (targetRemoveIt != zeroconf->targetsToRemoveUponRegistration.end())
			{
				zeroconf->targetsToRemoveUponRegistration.erase(targetRemoveIt);
				zeroconf->targets.erase(zeroconf->getTarget(*targetRemoveIt));
			}
		}
	}

	//! A target can ask Zeroconf to update its TXT record
	void Zeroconf::updateTarget(Zeroconf::Target & target, const TxtRecord& txtrec)
	{
		assert(target.serviceRef);
		if (target.state != Target::State::REGISTERED)
			throw Zeroconf::Error(FormatableString("updateTarget: target in invalid state: %0").arg(target.stateToString.at(target.state)));
		const string rawdata{txtrec.record()};
		DNSServiceErrorType err = DNSServiceUpdateRecord(target.serviceRef,
								 nullptr, // update primary TXT record
								 0, // no flags
								 rawdata.length(),
								 rawdata.c_str(),
								 0);
		if (err != kDNSServiceErr_NoError)
			throw Zeroconf::Error(FormatableString("DNSServiceUpdateRecord: error %0").arg(err));
		target.updateCompleted();
	}

	//! Update the set of known targets with remote ones found by browsing DNS.
	void Zeroconf::browse()
	{
		// browse() does nothing if called more then once
		if (browseServiceRef)
			return;
		auto err = DNSServiceBrowse(&browseServiceRef,
					    0, // no flags
					    0, // default all interfaces
					    "_aseba._tcp",
					    nullptr, // use default domain, usually "local."
					    ZeroconfCallbacks::browseReply,
					    this
		);
		if (err != kDNSServiceErr_NoError)
			throw Zeroconf::Error(FormatableString("DNSServiceBrowse: error %0").arg(err));
		else
			processServiceRef(browseServiceRef);
	}

	//! DNSSD callback for browse, update DiscoveryRequest record with results of browse
	void DNSSD_API ZeroconfCallbacks::browseReply(DNSServiceRef sdRef,
					   DNSServiceFlags flags,
					   uint32_t interfaceIndex,
					   DNSServiceErrorType errorCode,
					   const char *name,
					   const char *regtype,
					   const char *domain,
					   void *context)
	{
		if (errorCode != kDNSServiceErr_NoError)
		{
			throw Zeroconf::Error(FormatableString("DNSServiceBrowseReply: error %0").arg(errorCode));
		}
		else
		{
			auto zeroconf(static_cast<Zeroconf *>(context));
			if (flags & kDNSServiceFlagsAdd)
			{
				zeroconf->resolveTarget(name, regtype, domain);
			}
			else
			{
				Zeroconf::TargetInformation targetInformationKey(name, regtype, domain);
				auto targetInformationIt(zeroconf->detectedTargets.find(targetInformationKey));
				if (targetInformationIt != zeroconf->detectedTargets.end())
				{
					zeroconf->targetRemoved(*targetInformationIt);
					zeroconf->detectedTargets.erase(targetInformationIt);
				}
			}
		}
	}

	//! A remote target can ask Zeroconf to resolve its host name and port
	void Zeroconf::resolveTarget(const std::string & name, const std::string & regtype, const std::string & domain)
	{
		targets.emplace_back(name, regtype, domain, *this);
		Target& target(targets.back());
		auto err = DNSServiceResolve(&target.serviceRef,
						 0, // no flags
						 0, // default all interfaces
						 name.c_str(),
						 regtype.c_str(),
						 domain.c_str(),
						 ZeroconfCallbacks::resolveReply,
						 this);
		target.state = Zeroconf::Target::State::RESOLVING;
		if (err != kDNSServiceErr_NoError)
		{
			targets.pop_back();
			throw Zeroconf::Error(FormatableString("DNSServiceResolve: error %0").arg(err));
		}
		else
			processServiceRef(target.serviceRef);
	}

	//! DNSSD callback for resolveTarget, update Zeroconf::Target record with results of lookup
	void DNSSD_API ZeroconfCallbacks::resolveReply(DNSServiceRef sdRef,
					    DNSServiceFlags flags,
					    uint32_t interfaceIndex,
					    DNSServiceErrorType errorCode,
					    const char * fullname,
					    const char * hosttarget,
					    uint16_t port,
					    uint16_t txtLen,
					    const unsigned char * txtRecord,
					    void *context)
	{
		auto zeroconf(static_cast<Zeroconf *>(context));
		auto targetIt(zeroconf->getTarget(sdRef));
		if (targetIt == zeroconf->targets.end())
			return;
		if (errorCode != kDNSServiceErr_NoError)
		{
			zeroconf->targets.erase(targetIt);
			throw Zeroconf::Error(FormatableString("DNSServiceResolveReply: error %0").arg(errorCode));
		}
		else
		{
			Zeroconf::Target& target(*targetIt);
			target.host = hosttarget;
			target.port = ntohs(port);
			Aseba::Zeroconf::TxtRecord tnew{ txtRecord,txtLen };
			target.properties.clear();
			for (auto const & field: tnew)
				target.properties[field.first] = field.second;
			target.properties["fullname"] = string(fullname);
			zeroconf->detectedTargets.insert(target);
			target.targetFound();
			zeroconf->targets.erase(targetIt);
		}
	}

	//! Return an iterator to the target holding a given service reference, return targets.end() if not found.
	Zeroconf::Targets::iterator Zeroconf::getTarget(DNSServiceRef serviceRef)
	{
		return find_if(targets.begin(), targets.end(),
			[=] (const Target& target) { return target.serviceRef == serviceRef; }
		);
	}

	//! Return an iterator to the target with a name and port, return targets.end() if not found.
	Zeroconf::Targets::iterator Zeroconf::getTarget(const std::string & name, const int port)
	{
		return find_if(targets.begin(), targets.end(),
			[=] (const Target& target) { return target.name == name && target.port == port; }
		);
	}

	//! Return an iterator to the target for a given Dashel stream, return targets.end() if not found.
	Zeroconf::Targets::iterator Zeroconf::getTarget(const std::string & name, const Dashel::Stream * stream)
	{
		return getTarget(name, atoi(stream->getTargetParameter("port").c_str()));
	}

} // namespace Aseba