Arduino DLNA Server
Loading...
Searching...
No Matches
SubscriptionMgrDevice.h
Go to the documentation of this file.
1#pragma once
2
3#include <functional>
4
6#include "basic/Logger.h"
7#include "basic/NullPrint.h"
8#include "basic/Str.h"
9#include "basic/StrView.h"
10#include "basic/Url.h"
11#include "basic/Vector.h"
14#include "http/Http.h"
17
18namespace tiny_dlna {
19
20// forward declaration for friend
21class DLNAService;
22template <typename ClientType>
23class DLNADevice;
24
36 Str callback_url; // as provided in CALLBACK header
37 uint32_t timeout_sec = 1800;
38 uint32_t seq = 0;
39 uint64_t expires_at = 0;
41};
54 nullptr;
55 std::function<size_t(Print&, void*)> writer;
57 void* ref = nullptr;
58 int error_count = 0;
59 int seq = 0;
60};
61
90template <typename ClientType>
92 template <typename>
93 friend class DLNADevice;
94
95 public:
103
112
130 Str subscribe(DLNAServiceInfo& service, const char* callbackUrl,
131 const char* sid = nullptr,
132 uint32_t timeoutSec = 1800) override {
133 // simple SID generation
134 DlnaLogger.log(DlnaLogLevel::Info, "subscribe: %s %s",
135 StrView(service.service_id).c_str(),
136 StrView(callbackUrl).c_str());
137
138 bool hasSid = !StrView(sid).isEmpty();
139
140 // NEW subscription without a callback is invalid (GENA requirement)
141 if (!hasSid) {
142 if (StrView(callbackUrl).isEmpty()) {
143 DlnaLogger.log(
145 "subscribe: missing CALLBACK header for new subscription");
146 return Str();
147 }
148 }
149
150 // If sid provided, attempt to renew existing subscription for service
151 if (hasSid) {
152 Str renewed = renewSubscription(service, sid, callbackUrl, timeoutSec);
153 if (!renewed.isEmpty()) return renewed;
154 // not found: fall through and create new subscription
155 }
156
157 // Make sure that we have an url
158 if (StrView(callbackUrl).isEmpty()) {
159 DlnaLogger.log(DlnaLogLevel::Warning,
160 "subscribe: missing CALLBACK header for new subscription");
161 return Str();
162 }
163
164 // generate uuid-based SID
165 char buffer[64];
166 snprintf(buffer, sizeof(buffer), "uuid:%lu", millis());
167
168 // fill subscription
169 Subscription* s = new Subscription();
170 s->sid = buffer;
171 s->callback_url = callbackUrl;
172 s->timeout_sec = timeoutSec;
173 s->seq = 0;
174 s->expires_at = millis() + (uint64_t)timeoutSec * 1000ULL;
175 s->service = &service;
176
177 for (auto it = subscriptions.begin(); it != subscriptions.end(); it++) {
178 auto existing_sub = *it;
179 if (existing_sub->service == &service &&
180 StrView(existing_sub->callback_url).equals(callbackUrl)) {
181 // Found existing subscription for same service and callback URL
182 DlnaLogger.log(DlnaLogLevel::Info,
183 "subscribe: found existing subscription for service "
184 "'%s' and callback '%s', renewing SID '%s'",
185 StrView(service.service_id).c_str(),
186 StrView(callbackUrl).c_str(), existing_sub->sid.c_str());
187 // Renew existing subscription
188 existing_sub->timeout_sec = timeoutSec;
189 existing_sub->expires_at = millis() + (uint64_t)timeoutSec * 1000ULL;
190 return existing_sub->sid;
191 }
192 }
193
194 // add subscription
195 subscriptions.push_back(s);
196 return s->sid;
197 }
198
211 bool unsubscribe(DLNAServiceInfo& service, const char* sid) override {
212 for (auto it = subscriptions.begin(); it != subscriptions.end(); ++it) {
213 Subscription* s = *it;
214 if (s->service == &service && StrView(s->sid).equals(sid)) {
215 // remove pending notifications that reference this subscription
216 for (auto pit = pending_list.begin(); pit != pending_list.end();) {
217 if (pit->p_subscription == s) {
218 pending_list.erase(pit);
219 pit = pending_list
220 .begin(); // restart after erase (iterator invalidated)
221 } else {
222 ++pit;
223 }
224 }
225 delete s;
226 subscriptions.erase(it);
227 return true;
228 }
229 }
230 return false;
231 }
232
251 std::function<size_t(Print&, void*)> changeWriter,
252 void* ref) override {
253 bool any = false;
254 // do not enqueue if subscriptions are inactive
255 if (!is_active) return;
256
257 // enqueue notifications to be posted later by post()
258 for (auto it = subscriptions.begin(); it != subscriptions.end(); ++it) {
259 Subscription* sub = *it;
260 if (sub->service != &service) continue;
261 any = true;
262 NullPrint np;
264 // store pointer to the subscription entry (no snapshot)
265 pn.p_subscription = sub;
266 pn.ref = ref;
267 pn.writer = changeWriter;
268 pn.seq = sub->seq;
269 pending_list.push_back(pn);
270 // increment seq AFTER queuing (so first notification uses SEQ=0)
271 sub->seq++;
272 }
273 if (!any) {
274 DlnaLogger.log(DlnaLogLevel::Info, "service '%s' has no subscriptions",
275 service.service_id.c_str());
276 }
277 }
278
288 int publish() override {
289 if (!is_active) {
290 // clear any queued notifications when subscriptions are turned off
291 clear();
292 return 0;
293 }
294 // First remove expired subscriptions so we don't deliver to them.
296
297 if (pending_list.empty()) return 0;
298 // Attempt to process each pending notification once. If processing
299 // fails (non-200), leave the entry in the queue for a later retry.
300 int processed = 0;
301 for (auto it = pending_list.begin(); it != pending_list.end();) {
302 PendingNotification pn = *it;
303 // Ensure the subscription pointer is valid; if not, drop the entry.
304 if (pn.p_subscription == nullptr) {
305 DlnaLogger.log(DlnaLogLevel::Warning,
306 "pending notification dropped: missing subscription");
307 pending_list.erase(it);
308 it = pending_list.begin(); // restart after erase
309 continue;
310 }
311 Subscription& sub = *pn.p_subscription;
312 char seqBuf[32];
313 snprintf(seqBuf, sizeof(seqBuf), "%d", pn.seq);
314 Url cbUrl(sub.callback_url.c_str());
317 http.setHost(cbUrl.host());
318 http.setAgent("tiny-dlna-notify");
319 http.request().put("NT", "upnp:event");
320 http.request().put("NTS", "upnp:propchange");
321 http.request().put("SEQ", seqBuf);
322 http.request().put("SID", sub.sid.c_str());
323 DlnaLogger.log(DlnaLogLevel::Info, "Notify %s", cbUrl.url());
324 DlnaLogger.log(DlnaLogLevel::Info, "- SEQ: %s", seqBuf);
325 DlnaLogger.log(DlnaLogLevel::Info, "- SID: %s", sub.sid.c_str());
326 int rc = http.notify(cbUrl, createXML, "text/xml", &pn);
327 DlnaLogger.log(DlnaLogLevel::Info, "Notify %s -> %d", cbUrl.url(), rc);
328 if (rc == 200) {
329 pending_list.erase(it);
330 it = pending_list.begin();
331 processed++;
332 } else {
333 // Increment error count on the stored entry (not the local copy)
334 it->error_count++;
335 if (it->error_count > MAX_NOTIFY_RETIES) {
336 DlnaLogger.log(DlnaLogLevel::Warning,
337 "dropping notify to %s after %d errors with rc=%d %s",
338 cbUrl.url(), it->error_count, rc,
339 http.reply().statusMessage());
340 pending_list.erase(it);
341 it = pending_list.begin();
342 } else {
343 ++it;
344 }
345 }
346 }
347
348 DlnaLogger.log(
350 "Published: %d notifications, %d remaining (for %d subscriptions)",
351 processed, pendingCount(), subscriptionsCount());
352 return processed;
353 }
354
359 size_t subscriptionsCount() override { return subscriptions.size(); }
360
365 size_t pendingCount() override { return pending_list.size(); }
366
375 void setSubscriptionsActive(bool flag) override { is_active = flag; }
376
378 void end() override { setSubscriptionsActive(false); }
379
384 bool isSubscriptionsActive() override { return is_active; }
385
386 protected:
387 // store pointers to heap-allocated Subscription objects. This keeps
388 // references stable (we manage lifetimes explicitly) and avoids
389 // large copies when enqueuing notifications.
390 // Vector<Subscription*> subscriptions;
391 // Vector<PendingNotification> pending_list;
394 bool is_active = true;
395
404 uint64_t now = millis();
405 auto it = subscriptions.begin();
406 while (it != subscriptions.end()) {
407 Subscription* s = *it;
408 if (s->expires_at != 0 && s->expires_at <= now) {
409 DlnaLogger.log(DlnaLogLevel::Info, "removing expired subscription %s",
410 s->sid.c_str());
411 unsubscribe(*s->service, s->sid.c_str());
412 it = subscriptions.begin(); // restart after erase
413 } else {
414 ++it;
415 }
416 }
417 }
418
425 Str renewSubscription(DLNAServiceInfo& service, const char* sid,
426 const char* callbackUrl, uint32_t timeoutSec) {
427 if (StrView(sid).isEmpty()) return Str();
428 for (auto it = subscriptions.begin(); it != subscriptions.end(); ++it) {
429 Subscription* ex = *it;
430 if (ex->service == &service && StrView(ex->sid).equals(sid)) {
431 // renew: update timeout and expiry, do not change SID
432 ex->timeout_sec = timeoutSec;
433 ex->expires_at = millis() + (uint64_t)timeoutSec * 1000ULL;
434 // if a new callback URL was provided, update it
435 if (callbackUrl != nullptr && !StrView(callbackUrl).isEmpty()) {
436 ex->callback_url = callbackUrl;
437 }
438 DlnaLogger.log(DlnaLogLevel::Info, "renewed subscription %s",
439 ex->sid.c_str());
440 return ex->sid;
441 }
442 }
443 return Str();
444 }
445
447 void clear() {
448 // free any remaining heap-allocated subscriptions
449 for (int i = 0; i < subscriptions.size(); ++i) {
450 delete subscriptions[i];
451 }
452 subscriptions.clear();
453 // pending_list entries do not own subscription pointers
454 pending_list.clear();
455 }
456
467 static size_t createXML(Print& out, void* ref) {
468 EscapingPrint out_esc{out};
470 const char* service_abbrev =
472 int instance_id = pn.p_subscription->service->instance_id;
473
474 size_t written = 0;
475 written += out.println("<?xml version=\"1.0\"?>");
476 written += out.println(
477 "<e:propertyset "
478 "xmlns:e=\"urn:schemas-upnp-org:metadata-1-0/events\">");
479 written += out.println("<e:property>");
480 written += out.println("<LastChange>");
481 // below should be escaped
482 written +=
483 out_esc.print("<Event xmlns=\"urn:schemas-upnp-org:metadata-1-0/");
484 written += out_esc.print(service_abbrev);
485 written += out_esc.print("/\">");
486 written += out_esc.print("<InstanceID val=\"");
487 written += out_esc.print(instance_id);
488 written += out_esc.print("\">");
489 if (pn.writer) {
490 written += pn.writer(out_esc, pn.ref);
491 }
492 written += out_esc.print("</InstanceID>");
493 written += out_esc.print("</Event>");
494 // end of escaped
495 written += out.println();
496 written += out.println("</LastChange>");
497 written += out.println("</e:property>");
498 written += out.println("</e:propertyset>");
499 return written;
500 }
501
514 IClientHandler* client) {
515 // Get headers from request
516 const char* callbackHeader =
517 client ? client->requestHeader().get("CALLBACK") : nullptr;
518 const char* timeoutHeader =
519 client ? client->requestHeader().get("TIMEOUT") : nullptr;
520 const char* sidHeader =
521 client ? client->requestHeader().get("SID") : nullptr;
522
523 // Log the incoming headers
524 DlnaLogger.log(DlnaLogLevel::Info, "- SUBSCRIBE CALLBACK: %s",
525 callbackHeader ? callbackHeader : "null");
526 DlnaLogger.log(DlnaLogLevel::Info, "- SUBSCRIBE TIMEOUT: %s",
527 timeoutHeader ? timeoutHeader : "null");
528 DlnaLogger.log(DlnaLogLevel::Info, "- SUBSCRIBE SID: %s",
529 sidHeader ? sidHeader : "null");
530
531 // Parse and clean callback URL (remove angle brackets)
532 Str cbStr;
533 if (callbackHeader) {
534 cbStr = callbackHeader;
535 cbStr.replace("<", "");
536 cbStr.replace(">", "");
537 cbStr.trim();
538 }
539
540 // Sanitize SID (remove optional angle brackets)
541 Str sidStr;
542 if (sidHeader) {
543 sidStr = sidHeader;
544 sidStr.replace("<", "");
545 sidStr.replace(">", "");
546 sidStr.trim();
547 }
548
549 // Parse timeout (extract seconds from "Second-1800" format)
550 uint32_t tsec = 1800;
551 if (timeoutHeader && StrView(timeoutHeader).startsWith("Second-")) {
552 tsec = atoi(timeoutHeader + 7);
553 }
554
555 // Create or renew subscription
556 const char* callbackPtr = cbStr.c_str();
557 const char* sidPtr = sidStr.c_str();
558
559 Str sid = subscribe(service, callbackPtr, sidPtr, tsec);
560 if (sid.isEmpty()) {
561 DlnaLogger.log(DlnaLogLevel::Warning,
562 "subscribe request rejected (missing data)");
563 if (client) {
564 client->replyHeader().setValues(412, "Precondition Failed");
565 client->replyHeader().put("Content-Length", 0);
566 if (client->client()) client->replyHeader().write(*client->client());
567 client->endClient();
568 }
569 return false;
570 }
571 DlnaLogger.log(DlnaLogLevel::Info, "- SID: %s", sid.c_str());
572
573 // Send SUBSCRIBE response
574 if (client) {
575 client->replyHeader().setValues(200, "OK");
576 client->replyHeader().put("SID", sid.c_str());
577 client->replyHeader().put("TIMEOUT", "Second-1800");
578 client->replyHeader().put("Content-Length", 0);
579 if (client->client()) client->replyHeader().write(*client->client());
580 client->endClient();
581 }
582
583 return true;
584 }
585
598 IClientHandler* client) {
599 const char* sid = client ? client->requestHeader().get("SID") : nullptr;
600
601 DlnaLogger.log(DlnaLogLevel::Info, "- UNSUBSCRIBE SID: %s",
602 sid ? sid : "null");
603
604 if (sid) {
605 bool ok = unsubscribe(service, sid);
606 if (ok) {
607 DlnaLogger.log(DlnaLogLevel::Info, "Unsubscribed: %s", sid);
608 if (client) client->replyOK();
609 return true;
610 }
611 }
612
613 // SID not found or invalid
614 DlnaLogger.log(DlnaLogLevel::Warning,
615 "handleUnsubscribeRequest: failed for SID %s",
616 sid ? sid : "(null)");
617 if (client) client->replyOK();
618 return false;
619 }
620};
621
622} // namespace tiny_dlna
Setup of a Basic DLNA Device service. The device registers itself to the network and answers to the D...
Definition: DLNADevice.h:46
Attributes needed for the DLNA Service Definition.
Definition: DLNAServiceInfo.h:18
Str service_id
Definition: DLNAServiceInfo.h:38
int instance_id
Definition: DLNAServiceInfo.h:56
const char * subscription_namespace_abbrev
Definition: DLNAServiceInfo.h:53
const char * statusMessage()
Definition: HttpHeader.h:225
HttpHeader & put(const char *key, const char *value)
Definition: HttpHeader.h:105
void write(Client &out)
Definition: HttpHeader.h:267
const char * get(const char *key)
Definition: HttpHeader.h:161
void setValues(int statusCode, const char *msg="", const char *protocol=nullptr)
Definition: HttpHeader.h:412
Simple API to process get, put, post, del http requests I tried to use Arduino HttpClient,...
Definition: HttpRequest.h:31
HttpRequestHeader & request() override
Returns the request header.
Definition: HttpRequest.h:170
void setHost(const char *host) override
Sets the host name for the requests.
Definition: HttpRequest.h:52
HttpReplyHeader & reply() override
Returns the reply header.
Definition: HttpRequest.h:167
int notify(Url &url, std::function< size_t(Print &, void *)> writer, const char *mime=nullptr, void *ref=nullptr) override
Sends a NOTIFY request with streaming body.
Definition: HttpRequest.h:96
void setTimeout(int ms) override
Sets the timeout.
Definition: HttpRequest.h:189
void setAgent(const char *agent) override
Sets the user agent.
Definition: HttpRequest.h:173
Definition: IHttpServer.h:19
virtual HttpRequestHeader & requestHeader()=0
virtual void replyOK()=0
virtual Client * client()=0
virtual HttpReplyHeader & replyHeader()=0
virtual void endClient()=0
Abstract interface for HTTP server functionality.
Definition: IHttpServer.h:50
Abstract interface for UPnP event subscription management.
Definition: ISubscriptionMgrDevice.h:27
Lock-free double linked list using atomic operations.
Definition: ListLockFree.h:28
Class with does not do any output: it can be used to determine the length of the output.
Definition: NullPrint.h:12
A simple wrapper to provide string functions on char*. If the underlying char* is a const we do not a...
Definition: StrView.h:18
virtual bool isEmpty()
checks if the string is empty
Definition: StrView.h:383
virtual const char * c_str()
provides the string value as const char*
Definition: StrView.h:376
virtual bool equals(const char *str)
checks if the string equals indicated parameter string
Definition: StrView.h:177
Heap-backed string utility used throughout tiny_dlna.
Definition: Str.h:27
bool isEmpty() const
True if empty.
Definition: Str.h:54
void trim()
Trim spaces on both ends.
Definition: Str.h:235
const char * c_str() const
C-string pointer to internal buffer.
Definition: Str.h:88
bool replace(const char *toReplace, const char *replaced, int startPos=0)
Replace first occurrence of toReplace with replaced starting at startPos.
Definition: Str.h:269
Manages UPnP event subscriptions and queued notifications for a DLNA device.
Definition: SubscriptionMgrDevice.h:91
Str subscribe(DLNAServiceInfo &service, const char *callbackUrl, const char *sid=nullptr, uint32_t timeoutSec=1800) override
Add or renew a subscription for a service.
Definition: SubscriptionMgrDevice.h:130
void addChange(DLNAServiceInfo &service, std::function< size_t(Print &, void *)> changeWriter, void *ref) override
Enqueue a state-variable change for delivery.
Definition: SubscriptionMgrDevice.h:250
int publish() override
Deliver queued notifications.
Definition: SubscriptionMgrDevice.h:288
Str renewSubscription(DLNAServiceInfo &service, const char *sid, const char *callbackUrl, uint32_t timeoutSec)
Try to renew an existing subscription identified by SID for the given service. If successful the subs...
Definition: SubscriptionMgrDevice.h:425
bool unsubscribe(DLNAServiceInfo &service, const char *sid) override
Remove a subscription identified by SID.
Definition: SubscriptionMgrDevice.h:211
size_t pendingCount() override
Number of queued pending notifications.
Definition: SubscriptionMgrDevice.h:365
void end() override
Convenience method to disable subscriptions at the end of the lifecycle.
Definition: SubscriptionMgrDevice.h:378
void setSubscriptionsActive(bool flag) override
Enable or disable subscription delivery.
Definition: SubscriptionMgrDevice.h:375
ListLockFree< Subscription * > subscriptions
Definition: SubscriptionMgrDevice.h:392
SubscriptionMgrDevice()=default
Construct a new Subscription Manager.
void clear()
Clear all subscriptions and pending notifications.
Definition: SubscriptionMgrDevice.h:447
bool processUnsubscribeRequest(IHttpServer &server, DLNAServiceInfo &service, IClientHandler *client)
Handle UNSUBSCRIBE request including HTTP response.
Definition: SubscriptionMgrDevice.h:597
static size_t createXML(Print &out, void *ref)
Generate the propertyset XML for a single variable and write it to the provided Print instance....
Definition: SubscriptionMgrDevice.h:467
~SubscriptionMgrDevice()
Destroy the Subscription Manager.
Definition: SubscriptionMgrDevice.h:111
bool isSubscriptionsActive() override
Query whether subscription delivery is active.
Definition: SubscriptionMgrDevice.h:384
bool processSubscribeRequest(IHttpServer &server, DLNAServiceInfo &service, IClientHandler *client)
Handle SUBSCRIBE request including HTTP response.
Definition: SubscriptionMgrDevice.h:513
bool is_active
Definition: SubscriptionMgrDevice.h:394
size_t subscriptionsCount() override
Number of active subscriptions.
Definition: SubscriptionMgrDevice.h:359
void removeExpired()
Remove expired subscriptions.
Definition: SubscriptionMgrDevice.h:403
ListLockFree< PendingNotification > pending_list
Definition: SubscriptionMgrDevice.h:393
URL parser which breaks a full url string up into its individual parts.
Definition: Url.h:18
const char * host()
Definition: Url.h:41
const char * url()
Definition: Url.h:39
#define DLNA_HTTP_REQUEST_TIMEOUT_MS
Define the default http request timeout.
Definition: dlna_config.h:25
#define MAX_NOTIFY_RETIES
Define maximum number of notify retries.
Definition: dlna_config.h:173
Definition: Allocator.h:13
Print wrapper that escapes & < > " ' while forwarding to an underlying Print. Returns the expanded ou...
Definition: EscapingPrint.h:9
Representation of a queued notification to be delivered later.
Definition: SubscriptionMgrDevice.h:52
int error_count
Definition: SubscriptionMgrDevice.h:58
int seq
Definition: SubscriptionMgrDevice.h:59
void * ref
Definition: SubscriptionMgrDevice.h:57
Subscription * p_subscription
Definition: SubscriptionMgrDevice.h:53
std::function< size_t(Print &, void *)> writer
Definition: SubscriptionMgrDevice.h:55
Represents a single event subscription for a service.
Definition: SubscriptionMgrDevice.h:34
uint32_t timeout_sec
Definition: SubscriptionMgrDevice.h:37
Str sid
Definition: SubscriptionMgrDevice.h:35
uint32_t seq
Definition: SubscriptionMgrDevice.h:38
DLNAServiceInfo * service
Definition: SubscriptionMgrDevice.h:40
Str callback_url
Definition: SubscriptionMgrDevice.h:36
uint64_t expires_at
Definition: SubscriptionMgrDevice.h:39