Stroika Library 3.0d25
 
Loading...
Searching...
No Matches
Subscriber.cpp
1/*
2 * Copyright(c) Sophist Solutions, Inc. 1990-2026. All rights reserved
3 */
4#include "Stroika/Frameworks/StroikaPreComp.h"
5
6#include <atomic>
7#include <cmath>
8#include <mutex>
9#include <utility>
10
12#include "Stroika/Foundation/Characters/String2Int.h"
13#include "Stroika/Foundation/DataExchange/BadFormatException.h"
15#if qStroika_Foundation_DataExchange_XML_SupportDOM
16#include "Stroika/Foundation/DataExchange/XML/DOM.h"
17#endif
19#include "Stroika/Foundation/Execution/Exceptions.h"
22#include "Stroika/Foundation/IO/Network/ConnectionOrientedStreamSocket.h"
24#include "Stroika/Foundation/IO/Network/HTTP/Status.h"
25#include "Stroika/Foundation/IO/Network/Transfer/Connection.h"
26
27#include "Subscriber.h"
28
29using namespace Stroika::Foundation;
32using namespace Stroika::Foundation::Execution;
34using namespace Stroika::Frameworks;
35using namespace Stroika::Frameworks::UPnP;
36using namespace Stroika::Frameworks::UPnP::GENA;
37
38namespace {
39 // how often to try again, a renewal and a new subscription having failed
40 constexpr Time::DurationSeconds kRetryAfter_{30.0};
41
42 // a response header, by name - whatever its case
43 optional<String> Header_ (const Transfer::Response& r, const String& name)
44 {
45 for (const KeyValuePair<String, String>& h : r.GetHeaders ()) {
46 if (String::EqualsComparer{eCaseInsensitive}(h.fKey, name)) {
47 return h.fValue.Trim ();
48 }
49 }
50 return nullopt;
51 }
52
53 // a TIMEOUT header's duration - Second-, then a number of seconds (UPnP Device Architecture 1.1, section 4.1.1) - as Publisher's
54 optional<Time::DurationSeconds> Timeout_ (const optional<String>& timeout)
55 {
56 if (timeout and timeout->StartsWith ("Second-"sv, eCaseInsensitive)) {
57 if (int64_t seconds = String2Int<int64_t> (timeout->SubString (7)); seconds > 0) { // 0: not a number
58 return Time::DurationSeconds{static_cast<double> (seconds)};
59 }
60 }
61 return nullopt;
62 }
63
64 // a NOTIFY's body's variables: each e:property's one element, by name (UPnP Device Architecture 1.1, section 4.3.2) - their
65 // values plain text, as UPnP's types are. Read with the XML DOM: without one, Start throws, so none is read
66 Mapping<String, String> PropertySet_ ([[maybe_unused]] const Memory::BLOB& body, [[maybe_unused]] const DataExchange::XML::Options& xmlOptions)
67 {
69#if qStroika_Foundation_DataExchange_XML_SupportDOM
70 using namespace DataExchange::XML::DOM;
71 Document::Ptr doc = Document::New (body.As<Streams::InputStream::Ptr<std::byte>> (), nullptr, xmlOptions); // kept: its elements do not keep it
72 if (Element::Ptr propertySet = doc.GetRootElement (); propertySet != nullptr) {
73 for (const Element::Ptr& property : propertySet.GetChildElements ()) {
74 for (const Element::Ptr& variable : property.GetChildElements ()) {
75 result.Add (variable.GetName ().fName, variable.GetValue ());
76 }
77 }
78 }
79#endif
80 return result;
81 }
82}
83
84/*
85 ********************************************************************************
86 ************************* UPnP::GENA::Subscriber::Rep_ *************************
87 ********************************************************************************
88 */
89class Subscriber::Rep_ {
90public:
91 Rep_ (const URI& eventSubURL, const URI& callbackURL, const function<void (const Event&)>& onEvent, const Options& options)
92 : fEventSubURL_{eventSubURL}
93 , fCallbackURL_{callbackURL}
94 , fOnEvent_{onEvent}
95 , fOptions_{options}
96 {
97 }
98 ~Rep_ ()
99 {
100 Stop ();
101 }
102
103public:
104 void Start ()
105 {
106#if not qStroika_Foundation_DataExchange_XML_SupportDOM
107 Throw (Exception<runtime_error>{"GENA events cannot be read: this build has no XML parser"sv});
108#endif
109 lock_guard l{fLifecycleMutex_};
110 if (fActive_) {
111 return;
112 }
113 fActive_ = true; // before the SUBSCRIBE: its first event can come before its answer
114 try {
115 auto [sid, timeout] = Subscribe_ (nullopt);
116 fSID_.store (sid);
117 }
118 catch (...) {
119 fActive_ = false;
120 throw;
121 }
122 {
123 lock_guard r{fRenewalMutex_};
124 fStopped_ = false;
125 }
126 RenewIn_ (fGranted_ / 2);
127 }
128 void Stop ()
129 {
130 lock_guard l{fLifecycleMutex_};
131 if (not fActive_) {
132 return;
133 }
134 fActive_ = false; // no more events reported
135 optional<IntervalTimer::TimerID> renewal;
136 {
137 lock_guard r{fRenewalMutex_};
138 fStopped_ = true; // so a renewal under way schedules no other
139 renewal = exchange (fRenewal_, nullopt);
140 }
141 if (renewal) {
142 IntervalTimer::Manager::sThe.Remove (*renewal); // waits for it, if under way
143 }
144 if (optional<String> sid = fSID_.load ()) {
145 try {
146 Send_ ("UNSUBSCRIBE"sv, Mapping<String, String>{{"SID"sv, *sid}});
147 }
148 catch (...) {
149 DbgTrace ("GENA UNSUBSCRIBE {} failed (it lapses at its TIMEOUT): {}"_f, *sid, current_exception ());
150 }
151 }
152 fSID_.store (nullopt);
153 }
154
155public:
156 // a SUBSCRIBE or UNSUBSCRIBE at the event URL
157 Transfer::Response Send_ (const String& method, const Mapping<String, String>& headers) const
158 {
159 Transfer::Connection::Ptr c = fOptions_.fConnectionFactory (fEventSubURL_);
160 c.SetSchemeAndAuthority (fEventSubURL_.GetSchemeAndAuthority ());
162 r.fMethod = method;
163 r.fAuthorityRelativeURL = fEventSubURL_.GetAuthorityRelativeResource<URI> ();
164 r.fOverrideHeaders = headers;
165 return c.SendAndThrowOnFailure (r);
166 }
167 // a SUBSCRIBE - new, or the renewal of sid - and what it granted: the SID, and TIMEOUT (also kept in fGranted_)
168 pair<String, Time::DurationSeconds> Subscribe_ (const optional<String>& sid)
169 {
170 const String timeout = "Second-{}"_f(std::llround (fOptions_.fTimeout.count ()));
172 Send_ ("SUBSCRIBE"sv,
173 sid ? Mapping<String, String>{{"SID"sv, *sid}, {"TIMEOUT"sv, timeout}}
174 : Mapping<String, String>{{"CALLBACK"sv, "<{}>"_f(fCallbackURL_)}, {"NT"sv, "upnp:event"sv}, {"TIMEOUT"sv, timeout}});
175 optional<String> newSID = Header_ (r, "SID"sv);
176 if (not newSID) {
177 Throw (Exception<runtime_error>{"GENA SUBSCRIBE answered with no SID"sv});
178 }
179 fGranted_ = Timeout_ (Header_ (r, "TIMEOUT"sv)).value_or (fOptions_.fTimeout);
180 return {*newSID, fGranted_};
181 }
182 // renew in this long - unless stopping
183 void RenewIn_ (Time::DurationSeconds after)
184 {
185 lock_guard l{fRenewalMutex_};
186 if (not fStopped_) {
187 fRenewal_ = IntervalTimer::Manager::sThe.AddOneShot ([this] () { Renew_ (); }, after);
188 }
189 }
190 // on IntervalTimer's thread: renew - or, failing, subscribe again; failing that too, try again a little later
191 void Renew_ ()
192 {
193 try {
194 auto [sid, timeout] = Subscribe_ (fSID_.load ());
195 fSID_.store (sid);
196 RenewIn_ (timeout / 2);
197 return;
198 }
199 catch (...) {
200 DbgTrace ("GENA renewal of {} failed: {} - subscribing again"_f, fSID_.load (), current_exception ());
201 }
202 try {
203 fSID_.store (nullopt); // till the answer: its first event may come before it
204 auto [sid, timeout] = Subscribe_ (nullopt);
205 fSID_.store (sid);
206 RenewIn_ (timeout / 2);
207 }
208 catch (...) {
209 DbgTrace ("GENA SUBSCRIBE to {} failed: {} - trying again later"_f, fEventSubURL_, current_exception ());
210 RenewIn_ (kRetryAfter_);
211 }
212 }
213
214public:
215 const URI fEventSubURL_;
216 const URI fCallbackURL_;
217 const function<void (const Event&)> fOnEvent_;
218 const Options fOptions_;
219 mutex fLifecycleMutex_; // Start and Stop
220 atomic<bool> fActive_{false}; // started, and not stopped: events are reported
221 Synchronized<optional<String>> fSID_; // nullopt while subscribing, or not subscribed
222 Time::DurationSeconds fGranted_{0.0}; // the TIMEOUT last granted - only on the thread subscribing
223 mutex fRenewalMutex_;
224 bool fStopped_{true}; // (fRenewalMutex_) so no renewal is scheduled
225 optional<IntervalTimer::TimerID> fRenewal_; // (fRenewalMutex_) the one-shot timer to renew at
226};
227
228/*
229 ********************************************************************************
230 *************************** UPnP::GENA::Subscriber *****************************
231 ********************************************************************************
232 */
233Subscriber::Subscriber (const URI& eventSubURL, const URI& callbackURL, const function<void (const Event&)>& onEvent, const Options& options)
234 : fRep_{make_unique<Rep_> (eventSubURL, callbackURL, onEvent, options)}
235{
236}
237
238Subscriber::~Subscriber () = default;
239
241{
242 fRep_->Start ();
243}
244
246{
247 fRep_->Stop ();
248}
249
250optional<String> Subscriber::GetSID () const
251{
252 return fRep_->fSID_.load ();
253}
254
256{
257 const WebServer::Request& request = m.request ();
258 WebServer::Response& response = m.rwResponse ();
259 const optional<String> nt = request.headers ().LookupOne ("NT"sv);
260 const optional<String> nts = request.headers ().LookupOne ("NTS"sv);
261 if (not nt or not nts) {
262 response.status = HTTP::StatusCodes::kBadRequest;
263 return;
264 }
265 const optional<String> sid = request.headers ().LookupOne ("SID"sv);
266 const optional<String> current = fRep_->fSID_.load ();
267 if (not fRep_->fActive_ or nt != "upnp:event"sv or nts != "upnp:propchange"sv or (current and sid != current)) {
268 response.status = HTTP::StatusCodes::kPreconditionFailed;
269 return;
270 }
271 Event e{.fSEQ = String2Int<uint32_t> (request.headers ().LookupOne ("SEQ"sv).value_or (String{}))};
272 try {
273 e.fVariables = PropertySet_ (m.rwRequest ().GetBody (), fRep_->fOptions_.fXMLOptions);
274 }
275 catch (const DataExchange::BadFormatException&) {
276 response.status = HTTP::StatusCodes::kBadRequest; // not a property set
277 return;
278 }
279 fRep_->fOnEvent_ (e);
280}
281
282URI Subscriber::MakeCallbackURL (const URI& eventSubURL, PortType port, const String& path)
283{
284 optional<URI::Authority> a = eventSubURL.GetAuthority ();
285 Require (a and a->GetHost ());
287 if (optional<InternetAddress> ia = a->GetHost ()->AsInternetAddress ()) {
288 addresses += *ia;
289 }
290 else {
291 addresses = DNS::kThe.GetHostAddresses (a->GetHost ()->AsRegisteredName ().value_or (String{}));
292 }
293 exception_ptr lastFailure;
294 for (const InternetAddress& ia : addresses) {
295 try {
296 const SocketAddress service{ia, eventSubURL.GetPortValue ()};
297 ConnectionOrientedStreamSocket::Ptr s = ConnectionOrientedStreamSocket::New (service.GetAddressFamily (), Socket::STREAM);
298 s.Connect (service, 5s);
299 if (optional<SocketAddress> local = s.GetLocalAddress ()) {
300 return URI{URI::SchemeType{"http"sv}, URI::Authority{URI::Host{local->GetInternetAddress ()}, port}, path};
301 }
302 }
303 catch (...) {
304 lastFailure = current_exception ();
305 }
306 }
307 if (lastFailure) {
308 rethrow_exception (lastFailure);
309 }
310 Throw (Exception<runtime_error>{"no address for the GENA service"sv});
311}
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
A Mapping uniquely associates two elements: a key and a value (use Assocation to allow multiple value...
nonvirtual bool Add(ArgByValueType< key_type > key, ArgByValueType< mapped_type > newElt, AddReplaceMode addReplaceMode=AddReplaceMode::eAddReplaces)
Definition Mapping.inl:186
A generalization of a vector: a container whose elements are keyed by the natural numbers.
Exception<> is a replacement (subclass) for any std c++ exception class (e.g. the default 'std::excep...
Definition Exceptions.h:219
Wrap any object with Synchronized<> and it can be used similarly to the base type,...
nonvirtual void Connect(const SocketAddress &sockAddr) const
Connects to the argument sockAddr;.
nonvirtual Sequence< InternetAddress > GetHostAddresses(const String &hostNameOrAddress) const
simple wrapper on GetHostEntry - looking up the hostname/ip address and returning the list of associa...
Definition DNS.cpp:238
nonvirtual optional< IO::Network::SocketAddress > GetLocalAddress() const
Definition Socket.inl:57
nonvirtual PortType GetPortValue() const
Definition URI.inl:66
nonvirtual optional< Authority > GetAuthority() const
Definition URI.inl:55
nonvirtual RETURN_TYPE GetAuthorityRelativeResource() const
Return the (PCT etc encoded if a string) data AFTER the authority, but not including the fragment.
Authority is roughly the part of a URL where you say the hostname (and portnumber etc) - part just af...
InputStream<>::Ptr is Smart pointer (with abstract Rep) class defining the interface to reading from ...
Subscriber(const URI &eventSubURL, const URI &callbackURL, const function< void(const Event &)> &onEvent, const Options &options=kDefaultOptions)
nonvirtual optional< String > GetSID() const
nonvirtual void Start()
Subscribe - throwing if the service does not take it - and keep it renewed. Started already: does not...
nonvirtual void Stop()
Unsubscribe - failing quietly, as the service then drops it at its TIMEOUT - so no more events are re...
nonvirtual void HandleNotify(WebServer::Message &m)
A NOTIFY at the callback URL - answered as UPnP Device Architecture 1.1, section 4....
static URI MakeCallbackURL(const URI &eventSubURL, Foundation::IO::Network::PortType port, const String &path)
A callback URL for the service at eventSubURL: http, at this machine's address on the network the ser...
Common::ReadOnlyProperty< Response & > rwResponse
Definition Message.h:93
Common::ReadOnlyProperty< const Request & > request
Definition Message.h:72
Common::ReadOnlyProperty< Request & > rwRequest
Definition Message.h:80
this represents a HTTP request object for the WebServer module
CONTAINER::value_type * Start(CONTAINER &c)
For a contiguous container (such as a vector or basic_string) - find the pointer to the start of the ...
void Throw(T &&e2Throw)
like the builtin C++ 'throw', but stamps in the current Activity context and does helpful,...
Definition Throw.inl:54
How XML is read and written - for now, with which provider - taken last by each XML function (DOM::Do...