Stroika Library 3.0d25
 
Loading...
Searching...
No Matches
SearchResponder.cpp
1/*
2 * Copyright(c) Sophist Solutions, Inc. 1990-2026. All rights reserved
3 */
4#include "Stroika/Frameworks/StroikaPreComp.h"
5
7#include "Stroika/Foundation/Characters/String2Int.h"
8#include "Stroika/Foundation/Containers/Sequence.h"
11#include "Stroika/Foundation/Execution/Throw.h"
13#include "Stroika/Foundation/IO/Network/ConnectionlessSocket.h"
18#include "Stroika/Foundation/Streams/MemoryStream.h"
20
23
24#include "SearchResponder.h"
25
26using std::byte;
27
28using namespace Stroika::Foundation;
31using namespace Stroika::Foundation::IO;
33using namespace Stroika::Foundation::Execution;
34
35using namespace Stroika::Frameworks;
36using namespace Stroika::Frameworks::UPnP;
37using namespace Stroika::Frameworks::UPnP::SSDP;
38using namespace Stroika::Frameworks::UPnP::SSDP::Server;
39
40// Comment this in to turn on tracing in this module
41//#define USE_NOISY_TRACE_IN_THIS_MODULE_ 1
42
43/*
44********************************************************************************
45******************************** SearchResponder *******************************
46********************************************************************************
47*/
48namespace {
49 // the LOCATION to answer asker with: location's, given this machine's address as asker reaches it - from where the search
50 // arrived (arrived: the address it was sent to, and the interface it came in on) and the asker's subnet:
51 // o the address it was sent to, if this machine's own (a unicast M-SEARCH)
52 // o else asker's own address, if asker is this machine (on any of listeningOn) - whichever interface it came in on: with
53 // two interfaces on one subnet, a search sent out of one comes in on the other too
54 // o else this machine's address on asker's subnet - on the interface it came in on, if there (two interfaces can share a
55 // subnet); else on another of listeningOn (the interfaces it can have come in on): two networks may be bridged, so it
56 // came in on the other, whose address asker may not reach
57 // o else, asker further off, an address of the interface it came in on
58 // o else, not known where it came in, the routing table's. Not the routing table's first: a VPN's route covering a LAN
59 // makes that the VPN's address, which the LAN cannot reach.
60 optional<URI> LocationFor_ (const LocationProvider& location, const SocketAddress& asker,
61 const optional<ConnectionlessSocket::PacketInfo>& arrived, const InterfacesByID& listeningOn)
62 {
63 optional<InternetAddress> local;
64 optional<Interface> arrivedOn;
65 if (arrived) {
66 if (not arrived->fDestination.IsMulticastAddress ()) {
67 local = arrived->fDestination;
68 }
69 arrivedOn = listeningOn.First ([&] (const Interface& i) { return i.fIndex == arrived->fInterfaceIndex; });
70 }
71 if (asker.IsInternetAddress ()) {
72 if (not local and
73 listeningOn.Any ([&] (const Interface& i) { return i.fBindings.fAddresses.Contains (asker.GetInternetAddress ()); })) {
74 local = asker.GetInternetAddress ();
75 }
76 if (not local and arrivedOn) {
77 local = SSDP::Server::Private_::LocalAddressOnAskersNetwork (asker.GetInternetAddress (), Iterable<Interface>{*arrivedOn});
78 }
79 if (not local) {
80 local = SSDP::Server::Private_::LocalAddressOnAskersNetwork (asker.GetInternetAddress (), listeningOn);
81 }
82 if (not local and arrivedOn) {
83 local = SSDP::Server::Private_::AdvertisableAddress (*arrivedOn, asker.GetInternetAddress ().GetAddressFamily ());
84 }
85 }
86 if (not local) {
87 local = GetLocalAddressToReach (asker);
88 if (not local) {
89 return nullopt; // no route back
90 }
91 if (local->GetAddressFamily () == InternetAddress::AddressFamily::V6 and local->IsLinkLocalAddress ()) {
92 // asked from a link-local address: offer the interface's other IPv6 address, if it has one
93 if (optional<Interface> i = SystemInterfacesMgr{}.GetAll ().First (
94 [&] (const Interface& ii) { return ii.fBindings.fAddresses.Contains (*local); })) {
95 local = SSDP::Server::Private_::AdvertisableAddress (*i, InternetAddress::AddressFamily::V6);
96 }
97 }
98 }
99 return location (LocationContext{*local, asker});
100 }
101
102 // an answer to a search, to send at fDueAt
103 struct Answer_ {
106 SocketAddress fTo;
107 Memory::BLOB fPacket;
108 };
109
110 // most answers waiting to be sent at once - so a flood of M-SEARCHes cannot make them pile up without bound
111 constexpr size_t kMaxWaitingAnswers_{1000};
112
113 // a header's value, by its name in any case
114 optional<String> Header_ (const SSDP::Advertisement& a, const String& name)
115 {
116 for (const KeyValuePair<String, String>& h : a.fRawHeaders) {
117 if (String::EqualsComparer{eCaseInsensitive}(h.fKey, name)) {
118 return h.fValue;
119 }
120 }
121 return nullopt;
122 }
123
124 // how long an M-SEARCH lets its answers wait, if it says: its MX - 5 seconds at most, as the UPnP Device Architecture
125 // (1.1, section 1.3.3) has a device assume for a larger one
126 optional<Time::DurationSeconds> MX_ (const SSDP::Advertisement& search)
127 {
128 optional<String> mx = Header_ (search, "MX"sv);
129 if (not mx or mx->empty () or not mx->All ([] (Character c) { return c.IsDigit (); })) {
130 return nullopt;
131 }
132 return Time::DurationSeconds{mx->size () > 9 ? 5 : min (Characters::String2Int<int> (*mx), 5)};
133 }
134
135 // whether an M-SEARCH was multicast: as where it arrived says (arrived); else, not known, unless its HOST names a unicast address
136 // - which some control points get wrong, naming the multicast group in a unicast M-SEARCH
137 bool IsMulticast_ (const SSDP::Advertisement& search, const optional<ConnectionlessSocket::PacketInfo>& arrived)
138 {
139 if (arrived) {
140 return arrived->fDestination.IsMulticastAddress ();
141 }
142 if (optional<String> host = Header_ (search, "HOST"sv)) {
143 if (optional<UniformResourceIdentification::Authority> a = UniformResourceIdentification::Authority::Parse (*host)) {
144 if (optional<InternetAddress> ia = a->GetHost () ? a->GetHost ()->AsInternetAddress () : nullopt) {
145 return ia->IsMulticastAddress ();
146 }
147 }
148 }
149 return true;
150 }
151
152 // a device or service type - urn:domain:device|service:type:version - as the type, and its version
153 optional<pair<String, unsigned int>> TypeAndVersion_ (const String& target)
154 {
155 optional<size_t> lastColon = target.RFind (':');
156 if (not target.StartsWith ("urn:"sv, eCaseInsensitive) or not lastColon) {
157 return nullopt;
158 }
159 String version = target.SubString (*lastColon + 1);
160 if (version.empty () or version.size () > 9 or not version.All ([] (Character c) { return c.IsDigit (); })) {
161 return nullopt;
162 }
163 return make_pair (target.SubString (0, *lastColon), Characters::String2Int<unsigned int> (version));
164 }
165
166 // advertisement a, as it answers a search for searchTarget - or nullopt if it does not. Every advertisement answers ssdp:all;
167 // and one for a device or service type answers a search for an older version of it too, as that version, in its ST and its USN
168 // - each version being compatible with those before it (UPnP Device Architecture 1.1, sections 1.3.2 and 1.3.3)
169 optional<Advertisement> AnswerTo_ (const String& searchTarget, const Advertisement& a)
170 {
171 String::EqualsComparer equals{eCaseInsensitive};
172 if (equals (searchTarget, kTarget_SSDPAll) or equals (searchTarget, a.fTarget)) {
173 return a;
174 }
175 optional<pair<String, unsigned int>> searchedFor = TypeAndVersion_ (searchTarget);
176 optional<pair<String, unsigned int>> advertised = TypeAndVersion_ (a.fTarget);
177 if (searchedFor and advertised and equals (searchedFor->first, advertised->first) and 1 <= searchedFor->second and
178 searchedFor->second < advertised->second) {
179 Advertisement answer = a;
180 answer.fTarget = searchTarget;
181 if (a.fUSN.EndsWith ("::"sv + a.fTarget, eCaseInsensitive)) {
182 answer.fUSN = a.fUSN.SubString (0, a.fUSN.size () - a.fTarget.size ()) + searchTarget;
183 }
184 return answer;
185 }
186 return nullopt;
187 }
188
189 // the answers to packet, if an M-SEARCH for something advertised. Each of a multicast M-SEARCH's is due after a random wait of
190 // up to its MX, so devices, and a device's several answers, do not all come at once - and one with no MX is ignored (1.1,
191 // section 1.3.3); a unicast one's, at once
192 Sequence<Answer_> Answers_ (span<const byte> packet, const Iterable<Advertisement>& advertisements, const LocationProvider& location,
194 const optional<ConnectionlessSocket::PacketInfo>& arrived, const InterfacesByID& listeningOn)
195 {
196 String headLine;
197 SSDP::Advertisement da;
198 SSDP::DeSerialize (Memory::BLOB{packet}, &headLine, &da);
199#if USE_NOISY_TRACE_IN_THIS_MODULE_
200 Debug::TraceContextBumper ctx{"Read SSDP Packet"};
201 DbgTrace ("headLine: {}"_f, headLine);
202#endif
203 Sequence<Answer_> result;
204 if (headLine.StartsWith ("M-SEARCH "sv)) {
205 optional<Time::DurationSeconds> waitAtMost; // nullopt: answer at once
206 if (IsMulticast_ (da, arrived)) {
207 waitAtMost = MX_ (da);
208 if (not waitAtMost) {
209 return result;
210 }
211 }
212 Sequence<Advertisement> answering; // each advertisement that answers it, as it does
213 for (const Advertisement& a : advertisements) {
214 if (optional<Advertisement> answer = AnswerTo_ (da.fTarget, a)) {
215 answering += *answer;
216 }
217 }
218 optional<URI> url = answering.empty () ? nullopt : LocationFor_ (location, sendTo, arrived, listeningOn);
219 if (url) {
220#if USE_NOISY_TRACE_IN_THIS_MODULE_
221 DbgTrace ("sending search responder advertisements..."_f);
222#endif
223 for (Advertisement a : answering) {
224 a.fAlive = nullopt; // in responder we don't set alive flag
225 a.fLocation = *url;
226
227 Time::TimePointSeconds dueAt = Time::GetTickCount ();
228 if (waitAtMost) {
229 dueAt += SSDP::Private_::RandomDuration (*waitAtMost);
230 }
231 result += Answer_{dueAt, useSocket, sendTo, SSDP::Serialize ("HTTP/1.1 200 OK"sv, SearchOrNotify::SearchResponse, a)};
232#if USE_NOISY_TRACE_IN_THIS_MODULE_
233 DbgTrace ("(location={},TARGET(ST/NT)={},USN={})"_f, a.fLocation, a.fTarget, a.fUSN);
234#endif
235 }
236 }
237 }
238 return result;
239 }
240}
241
242SearchResponder::SearchResponder (const Iterable<Advertisement>& advertisements, const LocationProvider& location, const Options& options)
243{
245 advertisements.Apply ([] ([[maybe_unused]] const auto& a) { Require (not a.fTarget.empty ()); });
246 }
247 // Construction of search responder will fail if we cannot bind - instead of failing quietly inside the loop
248 Sequence<pair<ConnectionlessSocket::Ptr, InternetAddress>> socketsAndGroups; // each socket, and the SSDP group it joins
249 {
250 static constexpr Activity kActivity_{"SSDP Binding in SearchResponder"sv};
251 DeclareActivity da{&kActivity_};
252 // (no multicast TTL to set: these sockets send only answers, which are unicast and need no TTL limit - UPnP Device
253 // Architecture 1.1, section 1.3.3)
254 if (InternetProtocol::IP::SupportIPV4 (options.fIPVersion)) {
255 ConnectionlessSocket::Ptr s = ConnectionlessSocket::New (SocketAddress::INET, Socket::DGRAM);
256 s.SetReceivePacketInfo (true); // where each search arrived (@see LocationFor_)
257 s.Bind (SocketAddress{Network::V4::kAddrAny, UPnP::SSDP::V4::kSocketAddress.GetPort ()}, Socket::BindFlags{.fSO_REUSEADDR = true});
258 s.SetMulticastLoopMode (true); // probably should make this configurable
259 socketsAndGroups += make_pair (s, UPnP::SSDP::V4::kSocketAddress.GetInternetAddress ());
260 }
261 if (InternetProtocol::IP::SupportIPV6 (options.fIPVersion)) {
262 ConnectionlessSocket::Ptr s = ConnectionlessSocket::New (SocketAddress::INET6, Socket::DGRAM);
263 s.SetReceivePacketInfo (true); // where each search arrived (@see LocationFor_)
264 s.Bind (SocketAddress{Network::V6::kAddrAny, UPnP::SSDP::V6::kSocketAddress.GetPort ()}, Socket::BindFlags{.fSO_REUSEADDR = true});
265 s.SetMulticastLoopMode (true); // probably should make this configurable
266 socketsAndGroups += make_pair (s, UPnP::SSDP::V6::kSocketAddress.GetInternetAddress ());
267 }
268 }
269
270 // Use a thread to wait on a set of sockets we are listening for requests on
271 static const String kThreadName_{"SSDP Search Responder"sv};
272 fListenThread_ = Thread::New (
273 [this, advertisements = Sequence<Advertisement>{advertisements}, location, interfaceFilter = options.fInterfaces, socketsAndGroups] () {
274 Debug::TraceContextBumper ctx{"SSDP SearchResponder thread loop"};
275 auto inUseSockets = socketsAndGroups.Map<Iterable<ConnectionlessSocket::Ptr>> ([] (auto i) { return i.first; });
277 fJoinAgain_->GetWaitInfo ()};
278 // the group is joined on every interface (running, not loopback) with an address of its family - so searches arriving
279 // on any of them are heard: at first, again when a network appears (on these sockets, so the searches waiting in them,
280 // and the answers waiting to go, are kept), and - until at least one join works (e.g. started before there was a
281 // network) - every so often
282 optional<Time::TimePointSeconds> joinAt = Time::GetTickCount ();
283 Time::DurationSeconds retryJoinAfter{1s};
284 InterfacesByID listeningOn; // as joined last (fListeningOn_, for this thread)
285 // only stopped by thread abort - which drops the answers still waiting
286 Sequence<Answer_> waiting; // not yet due: this thread goes on answering other searches meanwhile, as 1.3.3 requires
287 while (true) {
288 try {
289 if (fJoinAgain_->IsSet () or (joinAt and *joinAt <= Time::GetTickCount ())) {
290 fJoinAgain_->Clear (); // first: a network appearing as this joins wakes the wait below at once
291 listeningOn = SSDP::Private_::JoinOnEveryInterface (socketsAndGroups, interfaceFilter);
292 fListeningOn_.store (listeningOn);
293 joinAt = nullopt;
294 if (listeningOn.empty ()) {
295 // SEE https://github.com/SophistSolutions/Stroika/issues/1094 (STK-962)
296 DbgTrace ("SSDP SearchResponder: could join on no interface, so try again in {}"_f, retryJoinAfter);
297 joinAt = Time::GetTickCount () + retryJoinAfter;
298 retryJoinAfter = min<Time::DurationSeconds> (retryJoinAfter * 2, 60s);
299 }
300 }
301 Time::TimePointSeconds nextDue = joinAt.value_or (Time::TimePointSeconds{Time::kInfinity});
302 for (const Answer_& a : waiting) {
303 nextDue = min (nextDue, a.fDueAt);
304 }
305 for (ConnectionlessSocket::Ptr s : readyChecker.WaitQuietlyUntil (nextDue)) {
306 SocketAddress from;
307 optional<ConnectionlessSocket::PacketInfo> arrived;
308 byte buf[4 * 1024]; // not sure of max packet size
309 size_t nBytesRead = s.ReceiveFrom (buf, 0, &from, &arrived).size ();
310 Assert (nBytesRead <= std::size (buf));
311 for (const Answer_& a : Answers_ (span{buf, nBytesRead}, advertisements, location, s, from, arrived, listeningOn)) {
312 if (waiting.size () < kMaxWaitingAnswers_) {
313 waiting += a;
314 }
315 }
316 }
317 Time::TimePointSeconds now = Time::GetTickCount ();
318 Sequence<Answer_> notYet;
319 for (const Answer_& a : waiting) {
320 if (a.fDueAt <= now) {
321 IgnoreExceptionsExceptThreadAbortForCall (a.fSocket.SendTo (a.fPacket, a.fTo));
322 }
323 else {
324 notYet += a;
325 }
326 }
327 waiting = notYet;
328 }
329 catch (const Thread::AbortException&) {
330 ReThrow ();
331 }
332 catch (...) {
333 // ignore errors - and keep on trucking
334 // but avoid wasting too much time if we get into an error storm
335 Sleep (1.0s);
336 }
337 }
338 },
339 Thread::eAutoStart, kThreadName_);
340 if (options.fFollowNetworkChanges) {
341 // a network appeared: the listening thread joins it there too - woken to, on the sockets it has (each used by one thread
342 // at a time)
343 fNetworkChanges_ = SSDP::Private_::FollowNetworkChanges ([this] () { fJoinAgain_->Set (); });
344 }
345}
346
348{
349 return fListeningOn_.load ();
350}
#define qStroika_Foundation_Debug_AssertionsChecked
The qStroika_Foundation_Debug_AssertionsChecked flag determines if assertions are checked and validat...
Definition Assertions.h:49
time_point< RealtimeClock, DurationSeconds > TimePointSeconds
TimePointSeconds is a simpler approach to chrono::time_point, which doesn't require using templates e...
Definition Realtime.h:82
chrono::duration< double > DurationSeconds
chrono::duration<double> - a time span (length of time) measured in seconds, but high precision.
Definition Realtime.h:57
#define DbgTrace
Definition Trace.h:327
String is like std::u32string, except it is much easier to use, often much more space efficient,...
Definition String.h:201
nonvirtual size_t size() const noexcept
Definition String.inl:536
nonvirtual String SubString(SZ from) const
nonvirtual bool StartsWith(const Character &c, CompareOptions co=eWithCase) const
Definition String.cpp:1118
nonvirtual optional< size_t > RFind(Character c) const noexcept
Definition String.cpp:1070
a cross between Mapping<KEY, T> and Collection<T> and Set<T>
A generalization of a vector: a container whose elements are keyed by the natural numbers.
nonvirtual RESULT_CONTAINER Map(ELEMENT_MAPPER &&elementMapper) const
'override' Iterable<>::Map () function so RESULT_CONTAINER defaults to Sequence, and improve that cas...
Definition Sequence.inl:296
nonvirtual void SetReceivePacketInfo(bool on) const
Have ReceiveFrom say where each datagram arrived (.
nonvirtual constexpr AddressFamily GetAddressFamily() const
nonvirtual void Bind(const SocketAddress &sockAddr, BindFlags bindFlags=BindFlags{})
Definition Socket.cpp:212
nonvirtual InternetAddress GetInternetAddress() const
static optional< Authority > Parse(const String &rawURLAuthorityText)
Iterable<T> is a base class for containers which easily produce an Iterator<T> to traverse them.
Definition Iterable.h:234
nonvirtual bool Any() const
Any() same as not empty (); Any (includeIfTrue) returns true iff includeIfTrue returns true on any va...
nonvirtual optional< T > First() const
return first element in iterable, or if 'that' specified, first where 'that' is true,...
Definition Iterable.inl:996
nonvirtual bool All(const function< bool(ArgByValueType< T >)> &testEachElt) const
return true iff argument predicate returns true for each element of the iterable
nonvirtual size_t size() const
Returns the number of items contained.
Definition Iterable.inl:311
nonvirtual void Apply(const function< void(ArgByValueType< T > item)> &doToElement) const
Run the argument function (or lambda) on each element of the container.
nonvirtual bool empty() const
Returns true iff size() == 0.
Definition Iterable.inl:317
SearchResponder(const Iterable< Advertisement > &advertisements, const LocationProvider &location, const Options &options=kDefaultOptions)
nonvirtual IO::Network::InterfacesByID GetNetworkInterfaces() const
The network interfaces it is listening for searches on now - as they were when it joined them (so wit...
Ptr New(const function< void()> &fun2CallOnce, const optional< Characters::String > &name, const optional< Configuration > &configuration)
Definition Thread.cpp:887
void Sleep(Time::Duration seconds2Wait)
Definition Sleep.inl:101
ConnectionlessSocket::Ptr New(SocketAddress::FamilyType family, Type socketKind, const optional< IPPROTO > &protocol=nullopt)
bool SupportIPV4(IPVersionSupport flag)
Definition IP.inl:8
optional< InternetAddress > GetLocalAddressToReach(const SocketAddress &peer)
This machine's address for reaching peer - the local address a datagram sent to peer would carry,...