Stroika Library 3.0d25
 
Loading...
Searching...
No Matches
Publisher.cpp
1/*
2 * Copyright(c) Sophist Solutions, Inc. 1990-2026. All rights reserved
3 */
4#include "Stroika/Frameworks/StroikaPreComp.h"
5
6#include <cmath>
7#include <limits>
8
10#include "Stroika/Foundation/Characters/String2Int.h"
12#include "Stroika/Foundation/Common/GUID.h"
13#include "Stroika/Foundation/Containers/Sequence.h"
14#include "Stroika/Foundation/DataExchange/XML/WriterUtils.h"
18#include "Stroika/Foundation/IO/Network/HTTP/Status.h"
19#include "Stroika/Foundation/IO/Network/Transfer/Connection.h"
21
22#include "Publisher.h"
23
24using namespace Stroika::Foundation;
27using namespace Stroika::Foundation::Execution;
29using namespace Stroika::Frameworks;
30using namespace Stroika::Frameworks::UPnP;
31using namespace Stroika::Frameworks::UPnP::GENA;
32
33namespace {
34 // a NOTIFY's body: each variable an e:property of an e:propertyset (UPnP Device Architecture 1.1, section 4.3.2)
35 string PropertySet_ (const Mapping<String, String>& variables)
36 {
38 sb << "<?xml version=\"1.0\"?>\r\n<e:propertyset xmlns:e=\"urn:schemas-upnp-org:event-1-0\">"sv;
39 for (const KeyValuePair<String, String>& v : variables) {
40 sb << "<e:property><"sv << v.fKey << ">"sv << DataExchange::XML::QuoteForXMLW (v.fValue) << "</"sv << v.fKey << "></e:property>"sv;
41 }
42 sb << "</e:propertyset>\r\n"sv;
43 return sb.str ().AsUTF8<string> ();
44 }
45
46 // a CALLBACK header's URLs: each in angle brackets, tried in order (UPnP Device Architecture 1.1, section 4.1.1) - http ones only
47 Sequence<URI> CallbackURLs_ (const String& callback)
48 {
49 Sequence<URI> result;
50 for (size_t at = 0; optional<size_t> start = callback.Find ('<', at);) {
51 optional<size_t> end = callback.Find ('>', *start);
52 if (not end) {
53 break;
54 }
55 try {
56 URI u = URI::Parse (callback.SubString (*start + 1, *end));
57 if (u.GetScheme () == URI::SchemeType{"http"sv} and u.GetAuthority ()) {
58 result += u;
59 }
60 }
61 catch (...) {
62 // not a URL: passed by
63 }
64 at = *end + 1;
65 }
66 return result;
67 }
68
69 // a TIMEOUT header's duration - Second-, then a number of seconds (UPnP Device Architecture 1.1, section 4.1.1) - or nullopt:
70 // none, Second-infinite (which UPnP 1.1 no longer allows), or not understood
71 optional<Time::DurationSeconds> Timeout_ (const optional<String>& timeout)
72 {
73 if (timeout and timeout->StartsWith ("Second-"sv, eCaseInsensitive)) {
74 if (int64_t seconds = String2Int<int64_t> (timeout->SubString (7)); seconds > 0) { // 0: not a number
75 return Time::DurationSeconds{static_cast<double> (seconds)};
76 }
77 }
78 return nullopt;
79 }
80
81 String TimeoutHeader_ (Time::DurationSeconds timeout)
82 {
83 return "Second-{}"_f(std::llround (timeout.count ()));
84 }
85}
86
87/*
88 ********************************************************************************
89 ************************* UPnP::GENA::Publisher::Rep_ **************************
90 ********************************************************************************
91 */
92class Publisher::Rep_ {
93public:
94 Rep_ (const function<Mapping<String, String> ()>& currentState, const Options& options)
95 : fCurrentState_{currentState}
96 , fOptions_{options}
97 {
98 }
99
100public:
101 struct Subscription_ {
102 Sequence<URI> fCallbacks;
103 Time::TimePointSeconds fExpiresAt;
104 uint32_t fNextSEQ{0};
105 bool fPending{true}; // till its SUBSCRIBE's answer is sent: Notify passes it by, as its first event is to come
106 };
107
108public:
109 // holding fSubscriptions_: drop what has expired
110 static void DropExpired_ (Mapping<String, Subscription_>* subscriptions)
111 {
112 Time::TimePointSeconds now = Time::GetTickCount ();
113 subscriptions->RemoveAll ([&] (const KeyValuePair<String, Subscription_>& s) { return s.fValue.fExpiresAt <= now; });
114 }
115 // the TIMEOUT a subscription gets for this SUBSCRIBE's
116 Time::DurationSeconds Grant_ (const optional<String>& asked) const
117 {
118 optional<Time::DurationSeconds> t = Timeout_ (asked);
119 return t and *t < fOptions_.fTimeout ? *t : fOptions_.fTimeout;
120 }
121 // holding fSubscriptions_: queue an event for this subscription, its SEQ taken now - so the events go in SEQ order. variables
122 // nullopt: its first event, all of them, as they are when it is sent
123 void Send_ (const String& sid, Subscription_* s, const optional<Mapping<String, String>>& variables)
124 {
125 uint32_t seq = s->fNextSEQ;
126 s->fNextSEQ = seq == numeric_limits<uint32_t>::max () ? 1 : seq + 1; // 0 only ever the first (section 4.3.2)
127 fSender_.AddTask ([sid, callbacks = s->fCallbacks, seq, variables, currentState = fCurrentState_,
128 connectionFactory = fOptions_.fConnectionFactory] () {
129 Deliver_ (connectionFactory, sid, callbacks, seq, PropertySet_ (variables ? *variables : currentState ()));
130 });
131 }
132 // on fSender_'s thread: a NOTIFY to the first of the callback URLs that takes it
133 static void Deliver_ (const function<Transfer::Connection::Ptr (const URI&)>& connectionFactory, const String& sid,
134 const Sequence<URI>& callbacks, uint32_t seq, const string& body)
135 {
136 for (const URI& u : callbacks) {
137 try {
138 Transfer::Connection::Ptr c = connectionFactory (u);
139 c.SetSchemeAndAuthority (u.GetSchemeAndAuthority ());
141 r.fMethod = "NOTIFY"sv;
142 r.fAuthorityRelativeURL = u.GetAuthorityRelativeResource<URI> ();
143 r.fOverrideHeaders = Mapping<String, String>{{"Content-Type"sv, "text/xml; charset=\"utf-8\""sv},
144 {"NT"sv, "upnp:event"sv},
145 {"NTS"sv, "upnp:propchange"sv},
146 {"SID"sv, sid},
147 {"SEQ"sv, "{}"_f(seq)}};
148 r.fData = Memory::BLOB{as_bytes (span{body})};
149 if (c.Send (r).GetSucceeded ()) {
150 return;
151 }
152 }
153 catch (...) {
154 DbgTrace ("GENA NOTIFY to {} failed: {}"_f, u, current_exception ());
155 }
156 }
157 DbgTrace ("GENA NOTIFY {} (SEQ {}) went to none of its callbacks"_f, sid, seq);
158 }
159
160public:
161 const function<Mapping<String, String> ()> fCurrentState_;
162 const Options fOptions_;
163 Synchronized<Mapping<String, Subscription_>> fSubscriptions_; // by SID
164 // last, so destroyed first: its tasks use nothing else of this
165 ThreadPool fSender_{ThreadPool::Options{.fThreadCount = 1}};
166};
167
168/*
169 ********************************************************************************
170 **************************** UPnP::GENA::Publisher *****************************
171 ********************************************************************************
172 */
173Publisher::Publisher (const function<Mapping<String, String> ()>& currentState, const Options& options)
174 : fRep_{make_unique<Rep_> (currentState, options)}
175{
176}
177
178Publisher::~Publisher () = default;
179
181{
182 using HTTP::StatusCodes::kBadRequest;
183 using HTTP::StatusCodes::kPreconditionFailed;
184 const WebServer::Request& request = m.request ();
185 WebServer::Response& response = m.rwResponse ();
186 const optional<String> sid = request.headers ().LookupOne ("SID"sv);
187 const optional<String> nt = request.headers ().LookupOne ("NT"sv);
188 const optional<String> callback = request.headers ().LookupOne ("CALLBACK"sv);
189 auto answer = [&] (const String& subscriptionID, Time::DurationSeconds timeout) {
190 response.rwHeaders ().Set ("SID"sv, subscriptionID);
191 response.rwHeaders ().Set ("TIMEOUT"sv, TimeoutHeader_ (timeout));
192 };
193 const String method = request.httpMethod ();
194 if (sid and (nt or callback)) {
195 response.status = kBadRequest; // a SID says which subscription; NT and CALLBACK make a new one
196 return;
197 }
198 if (method == "SUBSCRIBE"sv and not sid) {
199 Sequence<URI> callbacks = callback ? CallbackURLs_ (*callback) : Sequence<URI>{};
200 if (nt != "upnp:event"sv or callbacks.empty ()) {
201 response.status = kPreconditionFailed;
202 return;
203 }
204 const String newSID = "uuid:" + Common::GUID::GenerateNew ().As<String> ();
205 const Time::DurationSeconds timeout = fRep_->Grant_ (request.headers ().LookupOne ("TIMEOUT"sv));
206 {
207 auto subscriptions = fRep_->fSubscriptions_.rwget ();
208 Rep_::DropExpired_ (&subscriptions.rwref ());
209 subscriptions->Add (newSID, Rep_::Subscription_{.fCallbacks = callbacks, .fExpiresAt = Time::GetTickCount () + timeout});
210 }
211 answer (newSID, timeout);
212 // its first event, once this answer - with the SID it needs to know it by - is sent
213 bool answered = response.End ();
214 auto subscriptions = fRep_->fSubscriptions_.rwget ();
215 if (optional<Rep_::Subscription_> s = subscriptions->Lookup (newSID)) {
216 if (answered) {
217 s->fPending = false;
218 fRep_->Send_ (newSID, &*s, nullopt);
219 subscriptions->Add (newSID, *s);
220 }
221 else {
222 subscriptions->Remove (newSID); // the subscriber never heard of it
223 }
224 }
225 }
226 else if (method == "SUBSCRIBE"sv) {
227 auto subscriptions = fRep_->fSubscriptions_.rwget ();
228 Rep_::DropExpired_ (&subscriptions.rwref ());
229 optional<Rep_::Subscription_> s = subscriptions->Lookup (*sid);
230 if (not s) {
231 response.status = kPreconditionFailed;
232 return;
233 }
234 const Time::DurationSeconds timeout = fRep_->Grant_ (request.headers ().LookupOne ("TIMEOUT"sv));
235 s->fExpiresAt = Time::GetTickCount () + timeout;
236 subscriptions->Add (*sid, *s);
237 answer (*sid, timeout);
238 }
239 else if (method == "UNSUBSCRIBE"sv) {
240 auto subscriptions = fRep_->fSubscriptions_.rwget ();
241 Rep_::DropExpired_ (&subscriptions.rwref ());
242 if (not sid or not subscriptions->RemoveIf (*sid)) {
243 response.status = kPreconditionFailed;
244 }
245 }
246 else {
247 response.status = HTTP::StatusCodes::kMethodNotAllowed;
248 }
249}
250
252{
253 auto subscriptions = fRep_->fSubscriptions_.rwget ();
254 Rep_::DropExpired_ (&subscriptions.rwref ());
255 for (const KeyValuePair<String, Rep_::Subscription_>& i : Mapping<String, Rep_::Subscription_>{subscriptions.rwref ()}) { // a copy: changed as it goes
256 if (not i.fValue.fPending) {
257 Rep_::Subscription_ s = i.fValue;
258 fRep_->Send_ (i.fKey, &s, changed);
259 subscriptions->Add (i.fKey, s);
260 }
261 }
262}
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
Similar to String, but intended to more efficiently construct a String. Mutable type (String is large...
String is like std::u32string, except it is much easier to use, often much more space efficient,...
Definition String.h:201
nonvirtual String SubString(SZ from) const
nonvirtual optional< size_t > Find(Character c, CompareOptions co=eWithCase) const
Definition String.inl:687
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
nonvirtual optional< mapped_type > Lookup(ArgByValueType< key_type > key) const
Definition Mapping.inl:142
nonvirtual bool RemoveIf(ArgByValueType< key_type > key)
Remove the given item, if it exists. Return true if found and removed.
Definition Mapping.inl:233
nonvirtual void RemoveAll()
RemoveAll removes all, or all matching (predicate, iterator range, equals comparer or whatever) items...
Definition Mapping.inl:238
nonvirtual void Remove(ArgByValueType< key_type > key)
Remove the given item (which must exist).
Definition Mapping.inl:221
A generalization of a vector: a container whose elements are keyed by the natural numbers.
Wrap any object with Synchronized<> and it can be used similarly to the base type,...
Common::Property< String > httpMethod
typically HTTP::Methods::kGet
static URI Parse(const String &rawURL)
Definition URI.cpp:103
nonvirtual optional< SchemeType > GetScheme() const
Definition URI.inl:36
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.
nonvirtual bool empty() const
Returns true iff size() == 0.
Definition Iterable.inl:317
nonvirtual void Notify(const Mapping< String, String > &changed)
Send every subscriber these state variables' new values, by name.
nonvirtual void HandleRequest(WebServer::Message &m)
A SUBSCRIBE - a new subscription, or the renewal of one by its SID - or UNSUBSCRIBE,...
Publisher(const function< Mapping< String, String >()> &currentState, const Options &options=kDefaultOptions)
Common::ReadOnlyProperty< Response & > rwResponse
Definition Message.h:93
Common::ReadOnlyProperty< const Request & > request
Definition Message.h:72
this represents a HTTP request object for the WebServer module
static GUID GenerateNew() noexcept
Definition GUID.cpp:76