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"
12#include "http/Http.h"
16
17namespace tiny_dlna {
18
19// forward declaration for friend
20class DLNAService;
21template <typename ClientType>
22class DLNADevice;
23
35 Str callback_url; // as provided in CALLBACK header
36 uint32_t timeout_sec = 1800;
37 uint32_t seq = 0;
38 uint64_t expires_at = 0;
40};
53 nullptr;
54 std::function<size_t(Print&, void*)> writer;
56 void* ref = nullptr;
57 int error_count = 0;
58 int seq = 0;
59};
60
89template <typename ClientType>
91 template <typename>
92 friend class DLNADevice;
93
94 public:
102
111
129 Str subscribe(DLNAServiceInfo& service, const char* callbackUrl,
130 const char* sid = nullptr,
131 uint32_t timeoutSec = 1800) override {
132 // simple SID generation
133 DlnaLogger.log(DlnaLogLevel::Info, "subscribe: %s %s",
134 StrView(service.service_id).c_str(),
135 StrView(callbackUrl).c_str());
136
137 bool hasSid = !StrView(sid).isEmpty();
138
139 // NEW subscription without a callback is invalid (GENA requirement)
140 if (!hasSid) {
141 if (StrView(callbackUrl).isEmpty()) {
142 DlnaLogger.log(
144 "subscribe: missing CALLBACK header for new subscription");
145 return Str();
146 }
147 }
148
149 // If sid provided, attempt to renew existing subscription for service
150 if (hasSid) {
151 Str renewed = renewSubscription(service, sid, callbackUrl, timeoutSec);
152 if (!renewed.isEmpty()) return renewed;
153 // not found: fall through and create new subscription
154 }
155
156 // generate uuid-based SID
157 char buffer[64];
158 snprintf(buffer, sizeof(buffer), "uuid:%lu", millis());
159
160 // fill subscription
161 Subscription* s = new Subscription();
162 s->sid = buffer;
163 s->callback_url = callbackUrl;
164 s->timeout_sec = timeoutSec;
165 s->seq = 0;
166 s->expires_at = millis() + (uint64_t)timeoutSec * 1000ULL;
167 s->service = &service;
168
169 for (auto& existing_sub : subscriptions) {
170 if (existing_sub->service == &service &&
171 StrView(existing_sub->callback_url).equals(callbackUrl)) {
172 // Found existing subscription for same service and callback URL
173 DlnaLogger.log(DlnaLogLevel::Info,
174 "subscribe: found existing subscription for service "
175 "'%s' and callback '%s', renewing SID '%s'",
176 StrView(service.service_id).c_str(),
177 StrView(callbackUrl).c_str(),
178 existing_sub->sid.c_str());
179 // Renew existing subscription
180 existing_sub->timeout_sec = timeoutSec;
181 existing_sub->expires_at = millis() + (uint64_t)timeoutSec * 1000ULL;
182 return existing_sub->sid;
183 }
184 }
185
186 // add subscription
187 subscriptions.push_back(s);
188 return s->sid;
189 }
190
203 bool unsubscribe(DLNAServiceInfo& service, const char* sid) override {
204 for (size_t i = 0; i < subscriptions.size(); ++i) {
206 if (s->service == &service && StrView(s->sid).equals(sid)) {
207 // remove pending notifications that reference this subscription
208 for (int j = 0; j < pending_list.size();) {
209 if (pending_list[j].p_subscription == s) {
210 pending_list.erase(j);
211 } else {
212 ++j;
213 }
214 }
215 delete s;
216 subscriptions.erase(i);
217 return true;
218 }
219 }
220 return false;
221 }
222
241 std::function<size_t(Print&, void*)> changeWriter,
242 void* ref) override {
243 bool any = false;
244 // do not enqueue if subscriptions are inactive
245 if (!is_active) return;
246
247 // enqueue notifications to be posted later by post()
248 for (int i = 0; i < subscriptions.size(); ++i) {
249 Subscription* sub = subscriptions[i];
250 if (sub->service != &service) continue;
251 any = true;
252 NullPrint np;
254 // store pointer to the subscription entry (no snapshot)
256 pn.ref = ref;
257 pn.writer = changeWriter;
258 pn.seq = sub->seq;
259 pending_list.push_back(pn);
260 // increment seq AFTER queuing (so first notification uses SEQ=0)
261 sub->seq++;
262 }
263 if (!any) {
264 DlnaLogger.log(DlnaLogLevel::Info, "service '%s' has no subscriptions",
265 service.service_id.c_str());
266 }
267 }
268
278 int publish() override {
279 if (!is_active) {
280 // clear any queued notifications when subscriptions are turned off
281 clear();
282 return 0;
283 }
284 // First remove expired subscriptions so we don't deliver to them.
286
287 if (pending_list.empty()) return 0;
288 // Attempt to process each pending notification once. If processing
289 // fails (non-200), leave the entry in the queue for a later retry.
290 int processed = 0;
291 for (int i = 0; i < pending_list.size();) {
292 // copy entry to avoid referencing vector storage while potentially
293 // modifying the vector (erase)
295
296 // Ensure the subscription pointer is valid; if not, drop the entry.
297 if (pn.p_subscription == nullptr) {
298 DlnaLogger.log(DlnaLogLevel::Warning,
299 "pending notification dropped: missing subscription");
300 pending_list.erase(i);
301 continue;
302 }
303 // determine subscription details
304 Subscription& sub = *pn.p_subscription;
305
306 // convert the sequence number to string
307 char seqBuf[32];
308 snprintf(seqBuf, sizeof(seqBuf), "%d", pn.seq);
309
310 // Build and send HTTP notify as in previous implementation
311 Url cbUrl(sub.callback_url.c_str());
314 http.setHost(cbUrl.host());
315 http.setAgent("tiny-dlna-notify");
316 http.request().put("NT", "upnp:event");
317 http.request().put("NTS", "upnp:propchange");
318 http.request().put("SEQ", seqBuf);
319 http.request().put("SID", sub.sid.c_str());
320 DlnaLogger.log(DlnaLogLevel::Info, "Notify %s", cbUrl.url());
321 DlnaLogger.log(DlnaLogLevel::Info, "- SEQ: %s", seqBuf);
322 DlnaLogger.log(DlnaLogLevel::Info, "- SID: %s", sub.sid.c_str());
323 int rc = http.notify(cbUrl, createXML, "text/xml", &pn);
324
325 // log result
326 DlnaLogger.log(DlnaLogLevel::Info, "Notify %s -> %d", cbUrl.url(), rc);
327
328 // remove processed notification on success; otherwise keep it for retry
329 if (rc == 200) {
330 pending_list.erase(i);
331 processed++;
332 // do not increment i: elements shifted down into current index
333 } else {
334 // Increment error count on the stored entry (not the local copy)
335 pending_list[i].error_count++;
336 if (pending_list[i].error_count > MAX_NOTIFY_RETIES) {
337 // give up and drop the entry after too many failed attempts
338 DlnaLogger.log(DlnaLogLevel::Warning,
339 "dropping notify to %s after %d errors with rc=%d %s",
340 cbUrl.url(), pending_list[i].error_count, rc,
341 http.reply().statusMessage());
342 pending_list.erase(i);
343 } else {
344 ++i;
345 }
346 }
347 }
348
349 DlnaLogger.log(
351 "Published: %d notifications, %d remaining (for %d subscriptions)",
352 processed, pendingCount(), subscriptionsCount());
353 return processed;
354 }
355
356
361 size_t subscriptionsCount() override { return subscriptions.size(); }
362
367 size_t pendingCount() override { return pending_list.size(); }
368
377 void setSubscriptionsActive(bool flag) override {
378 is_active = flag;
379 }
380
382 void end() override { setSubscriptionsActive(false); }
383
388 bool isSubscriptionsActive() override { return is_active; }
389
390
391 protected:
392 // store pointers to heap-allocated Subscription objects. This keeps
393 // references stable (we manage lifetimes explicitly) and avoids
394 // large copies when enqueuing notifications.
397 bool is_active = true;
398
407 uint64_t now = millis();
408 for (int i = 0; i < subscriptions.size();) {
410 if (s->expires_at != 0 && s->expires_at <= now) {
411 DlnaLogger.log(DlnaLogLevel::Info, "removing expired subscription %s",
412 s->sid.c_str());
413 // Use unsubscribe() which will remove pending notifications and
414 // free the subscription object. Do not increment `i` because the
415 // erase shifts the vector content down into the same index.
416 unsubscribe(*s->service, s->sid.c_str());
417 } else {
418 ++i;
419 }
420 }
421 }
422
429 Str renewSubscription(DLNAServiceInfo& service, const char* sid,
430 const char* callbackUrl, uint32_t timeoutSec) {
431 if (StrView(sid).isEmpty()) return Str();
432 for (int i = 0; i < subscriptions.size(); ++i) {
434 if (ex->service == &service && StrView(ex->sid).equals(sid)) {
435 // renew: update timeout and expiry, do not change SID
436 ex->timeout_sec = timeoutSec;
437 ex->expires_at = millis() + (uint64_t)timeoutSec * 1000ULL;
438 // if a new callback URL was provided, update it
439 if (callbackUrl != nullptr && !StrView(callbackUrl).isEmpty()) {
440 ex->callback_url = callbackUrl;
441 }
442 DlnaLogger.log(DlnaLogLevel::Info, "renewed subscription %s",
443 ex->sid.c_str());
444 return ex->sid;
445 }
446 }
447 return Str();
448 }
449
451 void clear() {
452 // free any remaining heap-allocated subscriptions
453 for (int i = 0; i < subscriptions.size(); ++i) {
454 delete subscriptions[i];
455 }
456 subscriptions.clear();
457 // pending_list entries do not own subscription pointers
458 pending_list.clear();
459 }
460
471 static size_t createXML(Print& out, void* ref) {
472 EscapingPrint out_esc{out};
474 const char* service_abbrev =
476 int instance_id = pn.p_subscription->service->instance_id;
477
478 size_t written = 0;
479 written += out.println("<?xml version=\"1.0\"?>");
480 written += out.println(
481 "<e:propertyset "
482 "xmlns:e=\"urn:schemas-upnp-org:metadata-1-0/events\">");
483 written += out.println("<e:property>");
484 written += out.println("<LastChange>");
485 // below should be escaped
486 written +=
487 out_esc.print("<Event xmlns=\"urn:schemas-upnp-org:metadata-1-0/");
488 written += out_esc.print(service_abbrev);
489 written += out_esc.print("/\">");
490 written += out_esc.print("<InstanceID val=\"");
491 written += out_esc.print(instance_id);
492 written += out_esc.print("\">");
493 if (pn.writer) {
494 written += pn.writer(out_esc, pn.ref);
495 }
496 written += out_esc.print("</InstanceID>");
497 written += out_esc.print("</Event>");
498 // end of escaped
499 written += out.println();
500 written += out.println("</LastChange>");
501 written += out.println("</e:property>");
502 written += out.println("</e:propertyset>");
503 return written;
504 }
505
518 DLNAServiceInfo& service) override {
519 // Get headers from request
520 const char* callbackHeader = server.requestHeader().get("CALLBACK");
521 const char* timeoutHeader = server.requestHeader().get("TIMEOUT");
522 const char* sidHeader = server.requestHeader().get("SID");
523
524 // Log the incoming headers
525 DlnaLogger.log(DlnaLogLevel::Info, "- SUBSCRIBE CALLBACK: %s",
526 callbackHeader ? callbackHeader : "null");
527 DlnaLogger.log(DlnaLogLevel::Info, "- SUBSCRIBE TIMEOUT: %s",
528 timeoutHeader ? timeoutHeader : "null");
529 DlnaLogger.log(DlnaLogLevel::Info, "- SUBSCRIBE SID: %s",
530 sidHeader ? sidHeader : "null");
531
532 // Parse and clean callback URL (remove angle brackets)
533 Str cbStr;
534 if (callbackHeader) {
535 cbStr = callbackHeader;
536 cbStr.replace("<", "");
537 cbStr.replace(">", "");
538 cbStr.trim();
539 }
540
541 // Sanitize SID (remove optional angle brackets)
542 Str sidStr;
543 if (sidHeader) {
544 sidStr = sidHeader;
545 sidStr.replace("<", "");
546 sidStr.replace(">", "");
547 sidStr.trim();
548 }
549
550 // Parse timeout (extract seconds from "Second-1800" format)
551 uint32_t tsec = 1800;
552 if (timeoutHeader && StrView(timeoutHeader).startsWith("Second-")) {
553 tsec = atoi(timeoutHeader + 7);
554 }
555
556 // Create or renew subscription
557 const char* callbackPtr = cbStr.isEmpty() ? nullptr : cbStr.c_str();
558 const char* sidPtr = sidStr.isEmpty() ? nullptr : sidStr.c_str();
559
560 Str sid = subscribe(service, callbackPtr, sidPtr, tsec);
561 if (sid.isEmpty()) {
562 DlnaLogger.log(DlnaLogLevel::Warning,
563 "subscribe request rejected (missing data)");
564 server.replyHeader().setValues(412, "Precondition Failed");
565 server.replyHeader().put("Content-Length", 0);
566 server.replyHeader().write(server.client());
567 server.endClient();
568 return false;
569 }
570 DlnaLogger.log(DlnaLogLevel::Info, "- SID: %s", sid.c_str());
571
572 // Send SUBSCRIBE response
573 server.replyHeader().setValues(200, "OK");
574 server.replyHeader().put("SID", sid.c_str());
575 server.replyHeader().put("TIMEOUT", "Second-1800");
576 server.replyHeader().put("Content-Length", 0);
577 server.replyHeader().write(server.client());
578 server.endClient();
579
580 return true;
581 }
582
595 DLNAServiceInfo& service) override {
596 const char* sid = server.requestHeader().get("SID");
597
598 DlnaLogger.log(DlnaLogLevel::Info, "- UNSUBSCRIBE SID: %s",
599 sid ? sid : "null");
600
601 if (sid) {
602 bool ok = unsubscribe(service, sid);
603 if (ok) {
604 DlnaLogger.log(DlnaLogLevel::Info, "Unsubscribed: %s", sid);
605 server.replyOK();
606 return true;
607 }
608 }
609
610 // SID not found or invalid
611 DlnaLogger.log(DlnaLogLevel::Warning,
612 "handleUnsubscribeRequest: failed for SID %s",
613 sid ? sid : "(null)");
614 server.replyNotFound();
615 return false;
616 }
617};
618
619} // 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:39
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:415
Simple API to process get, put, post, del http requests I tried to use Arduino HttpClient,...
Definition: HttpRequest.h:27
HttpRequestHeader & request() override
Returns the request header.
Definition: HttpRequest.h:165
void setHost(const char *host) override
Sets the host name for the requests.
Definition: HttpRequest.h:48
HttpReplyHeader & reply() override
Returns the reply header.
Definition: HttpRequest.h:162
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:91
void setTimeout(int ms) override
Sets the timeout.
Definition: HttpRequest.h:184
void setAgent(const char *agent) override
Sets the user agent.
Definition: HttpRequest.h:168
Abstract interface for HTTP server functionality.
Definition: IHttpServer.h:30
virtual void replyNotFound()=0
Send 404 Not Found response.
virtual void replyOK()=0
Send 200 OK response.
virtual HttpReplyHeader & replyHeader()=0
Get reference to reply header.
virtual Client & client()=0
Get reference to current client.
virtual void endClient()=0
Close the client connection.
virtual HttpRequestHeader & requestHeader()=0
Get reference to request header.
Abstract interface for UPnP event subscription management.
Definition: ISubscriptionMgrDevice.h:27
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:90
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:129
void addChange(DLNAServiceInfo &service, std::function< size_t(Print &, void *)> changeWriter, void *ref) override
Enqueue a state-variable change for delivery.
Definition: SubscriptionMgrDevice.h:240
int publish() override
Deliver queued notifications.
Definition: SubscriptionMgrDevice.h:278
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:429
bool unsubscribe(DLNAServiceInfo &service, const char *sid) override
Remove a subscription identified by SID.
Definition: SubscriptionMgrDevice.h:203
size_t pendingCount() override
Number of queued pending notifications.
Definition: SubscriptionMgrDevice.h:367
Vector< Subscription * > subscriptions
Definition: SubscriptionMgrDevice.h:395
void end() override
Convenience method to disable subscriptions at the end of the lifecycle.
Definition: SubscriptionMgrDevice.h:382
void setSubscriptionsActive(bool flag) override
Enable or disable subscription delivery.
Definition: SubscriptionMgrDevice.h:377
SubscriptionMgrDevice()=default
Construct a new Subscription Manager.
void clear()
Clear all subscriptions and pending notifications.
Definition: SubscriptionMgrDevice.h:451
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:471
~SubscriptionMgrDevice()
Destroy the Subscription Manager.
Definition: SubscriptionMgrDevice.h:110
bool processUnsubscribeRequest(IHttpServer &server, DLNAServiceInfo &service) override
Handle UNSUBSCRIBE request including HTTP response.
Definition: SubscriptionMgrDevice.h:594
bool isSubscriptionsActive() override
Query whether subscription delivery is active.
Definition: SubscriptionMgrDevice.h:388
bool processSubscribeRequest(IHttpServer &server, DLNAServiceInfo &service) override
Handle SUBSCRIBE request including HTTP response.
Definition: SubscriptionMgrDevice.h:517
bool is_active
Definition: SubscriptionMgrDevice.h:397
size_t subscriptionsCount() override
Number of active subscriptions.
Definition: SubscriptionMgrDevice.h:361
void removeExpired()
Remove expired subscriptions.
Definition: SubscriptionMgrDevice.h:406
Vector< PendingNotification > pending_list
Definition: SubscriptionMgrDevice.h:396
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
Lightweight wrapper around std::vector with Arduino-friendly helpers and a pluggable allocator.
Definition: Vector.h:39
#define DLNA_HTTP_REQUEST_TIMEOUT_MS
Define the default http request timeout.
Definition: dlna_config.h:20
#define MAX_NOTIFY_RETIES
Define maximum number of notify retries.
Definition: dlna_config.h:163
Definition: Allocator.h:13
Print wrapper that escapes & < > " ' while forwarding to an underlying Print. Returns the expanded ou...
Definition: EscapingPrint.h:8
Representation of a queued notification to be delivered later.
Definition: SubscriptionMgrDevice.h:51
int error_count
Definition: SubscriptionMgrDevice.h:57
int seq
Definition: SubscriptionMgrDevice.h:58
void * ref
Definition: SubscriptionMgrDevice.h:56
Subscription * p_subscription
Definition: SubscriptionMgrDevice.h:52
std::function< size_t(Print &, void *)> writer
Definition: SubscriptionMgrDevice.h:54
Represents a single event subscription for a service.
Definition: SubscriptionMgrDevice.h:33
uint32_t timeout_sec
Definition: SubscriptionMgrDevice.h:36
Str sid
Definition: SubscriptionMgrDevice.h:34
uint32_t seq
Definition: SubscriptionMgrDevice.h:37
DLNAServiceInfo * service
Definition: SubscriptionMgrDevice.h:39
Str callback_url
Definition: SubscriptionMgrDevice.h:35
uint64_t expires_at
Definition: SubscriptionMgrDevice.h:38