Arduino DLNA Server
Loading...
Searching...
No Matches
DLNAControlPoint.h
Go to the documentation of this file.
1#pragma once
2
3#include <functional>
4
5#include "Client.h"
7#include "basic/Url.h"
15#include "dlna/xml/XMLParser.h"
17#include "dlna_config.h"
18#include "http/Http.h"
19
20namespace tiny_dlna {
21
22class DLNAControlPoint;
24
62 public:
65
68 setTransports(http, udp);
69 }
70
74 setTransports(http, udp);
75 setHttpServer(server);
76 }
77
80 setHttpServer(server);
81 }
82
83 ~DLNAControlPoint() override = default;
84
86 void setParseDevice(bool flag) override { is_parse_device = flag; }
87
89 void setLocalURL(Url url) override { local_url = url; }
90 void setLocalURL(IPAddress url, int port = 9001,
91 const char* path = "") override {
92 char buffer[200];
93 snprintf(buffer, sizeof(buffer), "http://%s:%d%s", url.toString().c_str(),
94 port, path);
95 local_url.setUrl(buffer);
96 }
97
99 void setSearchRepeatMs(int repeatMs) override {
100 msearch_repeat_ms = repeatMs;
101 }
102
104 void setReference(void* ref) override { reference = ref; }
105
107 void setDeviceIndex(int idx) override { default_device_idx = 0; }
108
111 std::function<void(const char* sid, const char* varName,
112 const char* newValue, void* reference)>
113 cb,
114 void* ref = nullptr) override {
116 }
117
119 void setHttpServer(IHttpServer& server) override {
120 DlnaLogger.log(DlnaLogLevel::Debug, "DLNAControlPointMgr::setHttpServer");
121 p_http_server = &server;
122 }
123
127 void onResultNode(std::function<void(const char* nodeName, const char* text,
128 const char* attributes)>
129 cb) override {
130 result_callback = cb;
131 }
132
133 bool begin(const char* searchTarget = "ssdp:all", uint32_t minWaitMs = 3000,
134 uint32_t maxWaitMs = 60000) override {
135 if (!p_http || !p_udp) {
136 DlnaLogger.log(DlnaLogLevel::Error,
137 "DLNAControlPoint::begin: transports not configured");
138 return false;
139 }
140 return begin(*p_http, *p_udp, searchTarget, minWaitMs, maxWaitMs);
141 }
142
156 const char* searchTarget = "ssdp:all", uint32_t minWaitMs = 3000,
157 uint32_t maxWaitMs = 60000) override {
158 DlnaLogger.log(DlnaLogLevel::Info, "DLNADevice::begin");
160 search_target = searchTarget;
161 is_active = true;
162 setTransports(http, udp);
163
164 // setup multicast UDP
165 if (!(p_udp->begin(DLNABroadcastAddress))) {
166 DlnaLogger.log(DlnaLogLevel::Error, "UDP begin failed");
167 return false;
168 }
169
170 // Send MSearch request via UDP. Use maxWaitMs as the emission window.
171 MSearchSchedule* search =
172 new MSearchSchedule(DLNABroadcastAddress, searchTarget);
173
174 // ensure min <= max
175 if (minWaitMs > maxWaitMs) minWaitMs = maxWaitMs;
176 search->end_time = millis() + maxWaitMs;
178 search->active = true;
179 scheduler.add(search);
180
181 // if maxWaitMs > 0 we will block here and process events. We guarantee
182 // we wait at least minWaitMs before returning; we stop waiting after
183 // maxWaitMs or earlier if stopWhenFound is true AND minWaitMs has elapsed
184 uint64_t start = millis();
185 uint64_t minEnd = start + minWaitMs;
186 uint64_t maxEnd = start + maxWaitMs;
187 while (millis() < maxEnd) {
188 // if a device is found and we've satisfied the minimum wait, we can
189 // return early (we always allow early return after minWaitMs when a
190 // device has been discovered)
191 if (devices.size() > 0 && millis() >= minEnd) break;
192 loop();
193 }
194
195 // If we exited early because a device was found, deactivate the MSearch
196 // schedule so it will stop repeating. The scheduler will clean up
197 // inactive schedules on its next pass.
198 if (devices.size() > 0 && search != nullptr) {
199 search->active = false;
200 }
201
202 DlnaLogger.log(DlnaLogLevel::Info,
203 "Control Point started with %d devices found",
204 devices.size());
205
206 return devices.size() > 0;
207 }
208
210 p_http = &http;
212 p_udp = &udp;
213 }
214
216 bool subscribe() {
217 if (devices.size() == 0) return false;
218 if (!getDevice()) return false;
219
220 // setup subscription manager
221 if (local_url && p_http_server) {
222 if (p_http_server) {
223 // handle server requests
224 if (!p_http_server->begin()) {
225 DlnaLogger.log(DlnaLogLevel::Error, "HttpServer begin failed");
226 return false;
227 }
228 }
231 return true;
232 }
233
234 if (!local_url && !p_http_server) {
235 DlnaLogger.log(DlnaLogLevel::Info,
236 "No local URL and no HttpServer for subscriptions");
237 } else if (!local_url) {
238 DlnaLogger.log(DlnaLogLevel::Warning, "No local URL for subscriptions");
239 } else {
240 DlnaLogger.log(DlnaLogLevel::Warning, "No HttpServer for subscriptions");
241 }
242 return false;
243 }
244
246 void end() override {
247 DlnaLogger.log(DlnaLogLevel::Debug, "DLNAControlPointMgr::end");
248 // stop active processing
249 is_active = false;
250
251 // drain scheduler (delete schedules). Execute will call cleanup which
252 // deletes inactive schedules — run it until queue is empty. Only run if
253 // we have a udp instance to avoid calling execute with a null reference.
254 scheduler.setActive(false);
255 if (p_udp) {
256 while (scheduler.size() > 0) {
258 }
259 }
260 // p_server->end();
261 // clear device contents and drop containers
262 for (auto& device : devices) device.clear();
263 devices.clear();
264
265 // clear pending actions
266 actions.clear();
267
268 // stop http server if attached
269 if (p_http_server) {
271 p_http_server = nullptr;
272 }
273
274 // reset xml printer state
276
277 // clear last reply
278 reply.clear();
280
281 // reset transport/client refs
282 p_http = nullptr;
283 p_udp = nullptr;
284
285 // reset indices and references
287 }
288
291 DlnaLogger.log(DlnaLogLevel::Debug, "DLNAControlPointMgr::addAction");
292 actions.push_back(act);
293 return actions[actions.size() - 1];
294 }
295
304 ActionReply& executeActions(XMLCallback xmlProcessor = nullptr) override {
305 DlnaLogger.log(DlnaLogLevel::Debug, "DLNAControlPointMgr::executeActions");
306 reply.clear();
307 postAllActions(xmlProcessor);
308
309 // Default behaviour: log and dump collected reply arguments
310 DlnaLogger.log(DlnaLogLevel::Info, "Collected reply arguments: %d",
311 (int)reply.size());
313 return reply;
314 }
315
318 bool loop() override {
319 if (!is_active) return false;
321
322 // process subscriptions
324
325 // process UDP requests
326 RequestData req = p_udp->receive();
327 if (req && scheduler.isMSearchActive()) {
328 Schedule* schedule = parser.parse(req);
329 if (schedule != nullptr) {
330 // handle NotifyReplyCP
331 if (StrView(schedule->name()).equals("NotifyReplyCP")) {
332 NotifyReplyCP& notify_schedule = *(NotifyReplyCP*)schedule;
333 // notify_schedule.callback = processDevice;
334 processDevice(notify_schedule);
335 }
336 // handle MSearchReplyCP (HTTP/1.1 200 OK responses to M-SEARCH)
337 if (StrView(schedule->name()).equals("MSearchReplyCP")) {
338 MSearchReplyCP& ms_schedule = *(MSearchReplyCP*)schedule;
339 // Process M-SEARCH replies immediately: they contain LOCATION URLs
340 // pointing to device descriptions. Add the device and free the
341 // temporary schedule object.
342 processMSearchReply(ms_schedule);
343 }
344 }
345 }
346
347 // execute scheduled udp replys
349
351 // handle server requests
352 bool rc = p_http_server->doLoop();
353 }
354
355 // be nice, if we have other tasks
356 delay(5);
357 return true;
358 }
359
361 DLNAServiceInfo& getService(const char* id) override {
362 DlnaLogger.log(DlnaLogLevel::Debug, "DLNAControlPointMgr::getService");
363 static DLNAServiceInfo no_service(false);
364 for (auto& dev : devices) {
365 DLNAServiceInfo& result = dev.getService(id);
366 if (result) return result;
367 }
368 return no_service;
369 }
370
373 DlnaLogger.log(DlnaLogLevel::Debug, "DLNAControlPointMgr::getDevice");
374 if (default_device_idx < 0 || default_device_idx >= devices.size())
375 return NO_DEVICE;
377 }
378
380 DLNADeviceInfo& getDevice(int idx) override {
381 DlnaLogger.log(DlnaLogLevel::Debug, "DLNAControlPointMgr::getDevice");
382 if (idx < 0 || idx >= devices.size()) {
383 DlnaLogger.log(DlnaLogLevel::Error, "Device index %d out of range", idx);
384 return NO_DEVICE;
385 }
386 return devices[idx];
387 }
388
391 DlnaLogger.log(DlnaLogLevel::Debug, "DLNAControlPointMgr::getDevice");
392 for (auto& dev : devices) {
393 for (auto& srv : dev.getServices()) {
394 if (srv == service) return dev;
395 }
396 }
397 return NO_DEVICE;
398 }
399
401 DLNADeviceInfo& getDevice(Url location) override {
402 DlnaLogger.log(DlnaLogLevel::Debug, "DLNAControlPointMgr::getDevice");
403 for (auto& dev : devices) {
404 if (dev.getDeviceURL() == location) {
405 DlnaLogger.log(DlnaLogLevel::Debug,
406 "DLNAControlPointMgr::getDevice: Found device %s",
407 location.url());
408 return dev;
409 }
410 }
411 return NO_DEVICE;
412 }
413
415
422 const char* getUrl(DLNADeviceInfo& device, const char* suffix, char* buffer,
423 int len) override {
424 return getUrlImpl(device, suffix, buffer, len);
425 }
426
428 bool addDevice(DLNADeviceInfo dev) override {
429 DlnaLogger.log(DlnaLogLevel::Debug, "DLNAControlPointMgr::addDevice");
430 for (auto& existing_device : devices) {
431 if (dev.getUDN() == existing_device.getUDN()) {
432 DlnaLogger.log(DlnaLogLevel::Debug, "Device '%s' already exists",
433 dev.getUDN());
434 return false;
435 }
436 }
437 DlnaLogger.log(DlnaLogLevel::Info, "Device '%s' has been added",
438 dev.getUDN());
439 devices.push_back(dev);
440 return true;
441 }
442
444 bool addDevice(Url url) override {
445 DlnaLogger.log(DlnaLogLevel::Debug, "DLNAControlPointMgr::addDevice");
446 DLNADeviceInfo& device = getDevice(url);
447 if (device != NO_DEVICE) {
448 // device already exists
449 device.setActive(true);
450 return true;
451 }
452 // http get - incremental parse using XMLParserPrint so we don't hold
453 // the whole device.xml in memory
454 int rc = p_http->get(url, "text/xml");
455
456 if (rc != 200) {
457 DlnaLogger.log(DlnaLogLevel::Error, "Http get to '%s' failed with %d",
458 url.url(), rc);
459 return false;
460 }
461
462 DLNADeviceInfo new_device;
463 new_device.base_url.clear();
464 new_device.device_url = url;
465
466 XMLDeviceParser device_parser{new_device};
467 uint8_t buffer[XML_PARSER_BUFFER_SIZE];
468
469 // Read and incrementally parse into new_device
470 device_parser.begin();
471 while (true) {
472 int len = p_http->read(buffer, sizeof(buffer));
473 if (len <= 0) break;
474 device_parser.parse(buffer, len);
475 }
476 device_parser.end(new_device);
477
478 // if base_url is empty, derive it from the device description URL root
479 if (new_device.base_url.isEmpty()) {
480 new_device.base_url = url.urlRoot();
481 }
482
483 // Avoid adding the same device multiple times: check UDN uniqueness
484 for (auto& existing_device : devices) {
485 if (existing_device.getUDN() == new_device.getUDN()) {
486 DlnaLogger.log(DlnaLogLevel::Debug,
487 "Device '%s' already exists (skipping add)",
488 new_device.getUDN());
489 // Make sure the existing device is marked active and keep the URL
490 existing_device.setActive(true);
491 return true;
492 }
493 }
494
495 DlnaLogger.log(DlnaLogLevel::Info, "Device '%s' has been added",
496 new_device.getUDN());
497 devices.push_back(new_device);
498 return true;
499 }
500
502 void setActive(bool flag) override { is_active = flag; }
503
505 bool isActive() override { return is_active; }
506
508 void setAllowLocalhost(bool flag) override { allow_localhost = flag; }
509
511 ActionReply& getLastReply() override { return reply; }
512
515 return &subscription_mgr;
516 }
517
519 void setNotificationsActive(bool flag) override {
521 }
522
523 protected:
527 IUDPService* p_udp = nullptr;
534 int msearch_repeat_ms = 10000;
535 bool is_active = false;
536 bool is_parse_device = false;
537 const char* search_target;
539 bool allow_localhost = false;
541 void* reference = nullptr;
542 std::function<void(const char* nodeName, const char* text,
543 const char* attributes)>
545
548 static bool handleNotifyByebye(Str& usn) {
549 // delegate to existing bye handling
550 return selfDLNAControlPoint->processBye(usn);
551 }
552
555 static bool isUdnKnown(const char* usn_c, DLNADeviceInfo*& outDev) {
556 outDev = nullptr;
557 if (!usn_c || *usn_c == '\0') return false;
558 const char* sep = strstr(usn_c, "::");
559 int udn_len = sep ? (int)(sep - usn_c) : (int)strlen(usn_c);
560 for (auto& dev : selfDLNAControlPoint->devices) {
561 const char* known_udn = dev.getUDN();
562 if (known_udn && strncmp(known_udn, usn_c, udn_len) == 0 &&
563 (int)strlen(known_udn) == udn_len) {
564 outDev = &dev;
565 return true;
566 }
567 }
568 return false;
569 }
570
571 static bool handleNotifyAlive(NotifyReplyCP& data) {
572 bool select = selfDLNAControlPoint->matches(data.usn.c_str());
573 DlnaLogger.log(DlnaLogLevel::Debug, "addDevice: %s -> %s", data.usn.c_str(),
574 select ? "added" : "filtered");
575 if (!select) return false;
576
577 DLNADeviceInfo* existing = nullptr;
578 if (isUdnKnown(data.usn.c_str(), existing)) {
579 DlnaLogger.log(DlnaLogLevel::Debug,
580 "Device '%s' already known (skip GET)",
581 existing ? existing->getUDN() : "<unknown>");
582 if (existing) existing->setActive(true);
583 return true;
584 }
585
586 // Apply discovery netmask filtering before adding the device
587 Url url{data.location.c_str()};
589 DlnaLogger.log(DlnaLogLevel::Info,
590 "Device '%s' filtered by netmask (LOCATION %s)",
591 data.usn.c_str(), url.host());
592 return false;
593 }
594
595 // Not known and subnet accepted -> fetch and add device description
597 return true;
598 }
599
600 // Returns true if the LOCATION URL's host is in the same subnet as the
601 // local_url host according to DLNA_DISCOVERY_NETMASK. If either side is
602 // not a numeric IP literal or information is missing, the function
603 // permits discovery (returns true).
605 IPAddress netmask = DLNA_DISCOVERY_NETMASK;
606
607 const char* peerHost = url.host();
608 const char* localHost = local_url.host();
609
610 IPAddress peerIP;
611 IPAddress localIP;
612 bool peerOK = (peerHost && *peerHost) ? peerIP.fromString(peerHost) : false;
613 bool localOK =
614 (localHost && *localHost) ? localIP.fromString(localHost) : false;
615
616 // If we cannot parse both addresses, allow by default.
617 if (!peerOK || !localOK) return true;
618
619 for (int i = 0; i < 4; i++) {
620 if ((localIP[i] & netmask[i]) != (peerIP[i] & netmask[i])) {
621 DlnaLogger.log(DlnaLogLevel::Info,
622 "Discovery filtered: local=%d.%d.%d.%d peer=%d.%d.%d.%d "
623 "mask=%d.%d.%d.%d",
624 localIP[0], localIP[1], localIP[2], localIP[3],
625 peerIP[0], peerIP[1], peerIP[2], peerIP[3], netmask[0],
626 netmask[1], netmask[2], netmask[3]);
627 return false;
628 }
629 }
630
631 DlnaLogger.log(DlnaLogLevel::Debug,
632 "Discovery accepted: local=%d.%d.%d.%d peer=%d.%d.%d.%d",
633 localIP[0], localIP[1], localIP[2], localIP[3], peerIP[0],
634 peerIP[1], peerIP[2], peerIP[3]);
635 return true;
636 }
637
638 static bool processDevice(NotifyReplyCP& data) {
639 DlnaLogger.log(DlnaLogLevel::Debug, "DLNAControlPointMgr::processDevice");
640 Str& nts = data.nts;
641 if (nts.equals("ssdp:byebye")) {
642 return handleNotifyByebye(nts);
643 }
644 if (nts.equals("ssdp:alive")) {
645 return handleNotifyAlive(data);
646 }
647 return false;
648 }
649
652 DlnaLogger.log(DlnaLogLevel::Debug,
653 "DLNAControlPointMgr::processMSearchReply");
654 // data.location contains the device description URL
655 if (data.location.isEmpty()) return false;
656
657 const char* usn_c = data.usn.c_str();
658 if (usn_c && *usn_c) {
659 const char* sep = strstr(usn_c, "::");
660 int udn_len = sep ? (int)(sep - usn_c) : (int)strlen(usn_c);
661 for (auto& dev : selfDLNAControlPoint->devices) {
662 const char* known_udn = dev.getUDN();
663 if (known_udn && strncmp(known_udn, usn_c, udn_len) == 0 &&
664 (int)strlen(known_udn) == udn_len) {
665 DlnaLogger.log(DlnaLogLevel::Debug,
666 "MSearchReply: device '%s' already known (skip GET)",
667 known_udn);
668 dev.setActive(true);
669 return true;
670 }
671 }
672 }
673
674 Url url{data.location.c_str()};
676 return true;
677 }
678
680 bool matches(const char* usn) {
681 if (search_target == nullptr || *search_target == '\0') return true;
682 if (StrView(search_target).equals("ssdp:all")) return true;
683 return StrView(usn).contains(search_target) ||
685 }
686
688 bool processBye(Str& usn) {
689 DlnaLogger.log(DlnaLogLevel::Debug, "DLNAControlPointMgr::processBye");
690 for (auto& dev : devices) {
691 if (usn.startsWith(dev.getUDN())) {
692 for (auto& srv : dev.getServices()) {
693 srv.is_active = false;
694 if (usn.endsWith(srv.service_type)) {
695 if (srv.is_active) {
696 DlnaLogger.log(DlnaLogLevel::Info, "removeDevice: %s", usn);
697 srv.is_active = false;
698 }
699 }
700 }
701 }
702 }
703 return false;
704 }
705
721 size_t createXML(ActionRequest& action) {
722 DlnaLogger.log(DlnaLogLevel::Debug, "DLNAControlPointMgr::createXML");
723 size_t result = xml_printer.printXMLHeader();
724
725 result += xml_printer.printNodeBegin(
726 "Envelope",
727 "xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" "
728 "s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\"",
729 "s");
730 result += xml_printer.printNodeBegin("Body", nullptr, "s");
731
732 char ns[200];
733 StrView namespace_str(ns, 200);
734 namespace_str = "xmlns:u=\"%1\"";
735 bool ok = namespace_str.replace("%1", action.getService()->service_type);
736 DlnaLogger.log(DlnaLogLevel::Debug, "ns = '%s'", namespace_str.c_str());
737
738 // assert(ok);
739 result += xml_printer.printNodeBegin(action.getAction(),
740 namespace_str.c_str(), "u");
741 for (auto arg : action.getArguments()) {
742 const char* n = arg.name.c_str();
743 const char* v = arg.value.c_str();
744 if (n && *n) {
745 result += xml_printer.printNode(n, v);
746 }
747 }
748 result += xml_printer.printNodeEnd(action.getAction(), "u");
749
750 result += xml_printer.printNodeEnd("Body", "s");
751 result += xml_printer.printNodeEnd("Envelope", "s");
752 return result;
753 }
754
755 ActionReply& postAllActions(XMLCallback xmlProcessor = nullptr) {
756 reply.clear();
757 DlnaLogger.log(DlnaLogLevel::Debug, "DLNAControlPointMgr::postAllActions");
758 for (auto& action : actions) {
759 if (action) postAction(action, xmlProcessor);
760 }
761 return reply;
762 }
763
765 XMLCallback xmlProcessor = nullptr) {
766 DlnaLogger.log(DlnaLogLevel::Debug, "DLNAControlPointMgr::postAction: %s",
767 action.getAction());
768 reply.clear();
769 DLNAServiceInfo& service = *(action.getService());
770 DLNADeviceInfo& device = getDevice(service);
771
772 // create SOAPACTION header value
773 char act[200];
774 StrView action_str{act, 200};
775 action_str = "\"";
776 action_str.add(action.getService()->service_type);
777 action_str.add("#");
778 action_str.add(action.getAction());
779 action_str.add("\"");
780
781 // crate control url
782 char url_buffer[DLNA_MAX_URL_LEN] = {0};
783 // Log service and base to help debug malformed control URLs
784 DlnaLogger.log(DlnaLogLevel::Info,
785 "Service control_url: %s, device base: %s",
786 StrView(service.control_url).c_str(),
787 StrView(device.getBaseURL()).c_str());
788 Url post_url =
789 getUrl(device, service.control_url, url_buffer, DLNA_MAX_URL_LEN);
790 DlnaLogger.log(DlnaLogLevel::Info, "POST URL computed: %s", post_url.url());
791
792 // send HTTP POST and collect/handle response. If the caller provided an
793 // httpProcessor we forward it so they can process the raw HTTP reply
794 // (and avoid the library's default XML parsing) before consumption.
795 return processActionHttpPost(action, post_url, action_str.c_str(),
796 xmlProcessor);
797 }
798
800 size_t createSoapXML(ActionRequest& action, Print& out) {
801 DlnaLogger.log(DlnaLogLevel::Debug, "DLNAControlPointMgr::createSoapXML");
803 return createXML(action);
804 }
805
808 const char* soapAction,
809 XMLCallback xmlProcessor = nullptr) {
810 DlnaLogger.log(DlnaLogLevel::Debug,
811 "DLNAControlPointMgr::processActionHttpPost");
812
813 NullPrint np;
814 size_t xmlLen = createSoapXML(action, np);
815 // dynamically create and write xml
816 auto printXML = [this, &action, &xmlLen](Print& out, void* ref) -> size_t {
817 (void)ref;
818 return createSoapXML(action, out);
819 };
820 if (!p_http->isKeepAlive()) {
821 p_http->stop();
822 }
823 p_http->request().put("SOAPACTION", soapAction);
824 int rc = p_http->post(post_url, xmlLen, printXML, "text/xml");
825
826 // abort on HTTP error
827 if (rc != 200) {
828 p_http->stop();
829 reply.setValid(false);
830 DlnaLogger.log(DlnaLogLevel::Error, "Action '%s' failed with HTTP rc %d",
831 StrView(soapAction).c_str(), rc);
832 return reply;
833 }
834
835 reply.setValid(true);
836 reply.clear();
837
838 // If caller provided a custom XML processor, let it handle the live
839 // client/response. This allows callers to avoid the default XML parsing
840 // which would otherwise consume the response stream.
841 if (xmlProcessor) {
842 Client* c = p_http->client();
843 if (c) xmlProcessor(*c, reply);
844 return reply;
845 }
846
847 // Default: parse response incrementally and populate reply arguments
848 parseResult();
849 return reply;
850 }
851
859 void parseResult() {
860 XMLParserPrint xml_parser;
861 xml_parser.setExpandEncoded(true);
862 Str outNodeName;
863 Vector<Str> outPath;
864 Str outText;
865 Str outAttributes;
866 uint8_t buffer[XML_PARSER_BUFFER_SIZE];
867 while (p_http->client()->available()) {
868 int len = p_http->client()->read(buffer, XML_PARSER_BUFFER_SIZE);
869 if (len > 0) {
870 xml_parser.write(buffer, len);
871 while (xml_parser.parse(outNodeName, outPath, outText, outAttributes)) {
872 Argument arg;
873 // For most nodes we only add arguments when they contain text or
874 // attributes. The special "Result" node contains the DIDL-Lite
875 // payload (raw XML) and must be preserved so callers (e.g. the
876 // MediaServer helper) can parse returned media items.
877 if (!(outText.isEmpty() && outAttributes.isEmpty()) ||
878 outNodeName.equals("Result")) {
879 unEscapeStr(outAttributes);
880 unEscapeStr(outText);
881
882 if (!outText.isEmpty()) {
883 arg.name = outNodeName.c_str();
884 arg.value = outText;
885 reply.addArgument(arg);
886 }
887
888 DlnaLogger.log(DlnaLogLevel::Info, "Callback: '%s': %s (%s)",
889 StrView(outNodeName).c_str(),
890 StrView(outText).c_str(),
891 StrView(outAttributes).c_str());
892 if (result_callback) {
894 StrView(outNodeName ? outNodeName : "").c_str(),
895 StrView(outText ? outText : "").c_str(),
896 StrView(outAttributes ? outAttributes : "").c_str());
897 }
898 }
899 }
900 }
901 }
902 xml_parser.end();
903 }
904
905 void unEscapeStr(Str& str) {
906 str.replaceAll("&quot;", "\"");
907 str.replaceAll("&amp;", "&");
908 str.replaceAll("&apos;", "'");
909 }
910
911 const char* getUrlImpl(DLNADeviceInfo& device, const char* suffix,
912 const char* buffer, int len) {
913 DlnaLogger.log(DlnaLogLevel::Debug, "DLNAControlPointMgr::getUrl");
914 StrView url_str{(char*)buffer, len};
915 // start with base url if available, otherwise construct from device URL
916 const char* base = device.getBaseURL();
917 if (base != nullptr && *base != '\0') {
918 url_str = base;
919 } else {
920 url_str.add(device.getDeviceURL().protocol());
921 url_str.add("://");
922 url_str.add(device.getDeviceURL().host());
923 url_str.add(":");
924 url_str.add(device.getDeviceURL().port());
925 }
926
927 // Normalize slashes when appending suffix to avoid double '//'
928 if (suffix != nullptr && suffix[0] != '\0') {
929 int base_len = url_str.length();
930 const char* tmpc = url_str.c_str();
931 bool base_ends_slash =
932 tmpc != nullptr && base_len > 0 && tmpc[base_len - 1] == '/';
933 bool suf_starts_slash = suffix[0] == '/';
934 if (base_ends_slash && suf_starts_slash) {
935 // skip leading slash of suffix
936 url_str.add(suffix + 1);
937 } else if (!base_ends_slash && !suf_starts_slash) {
938 // ensure single slash between base and suffix
939 url_str.add('/');
940 url_str.add(suffix);
941 } else {
942 // exactly one slash present
943 url_str.add(suffix);
944 }
945 }
946 return buffer;
947 }
948};
949
950} // namespace tiny_dlna
Represents the result of invoking a DLNA service Action.
Definition: Action.h:48
int size()
Definition: Action.h:79
void clear()
Definition: Action.h:81
void setValid(bool flag)
Definition: Action.h:52
void addArgument(Argument arg)
Definition: Action.h:59
void logArguments()
Definition: Action.h:83
Represents a request to invoke a remote DLNA service action.
Definition: Action.h:104
DLNAServiceInfo * getService()
Definition: Action.h:163
const char * getAction()
Definition: Action.h:157
Vector< Argument > & getArguments()
Definition: Action.h:165
DLNA Service: Action Argument.
Definition: Action.h:17
Str value
Definition: Action.h:25
Str name
Definition: Action.h:24
Translates DLNA UDP Requests to Schedule so that we can schedule a reply.
Definition: DLNAControlPointRequestParser.h:15
Schedule * parse(RequestData &req)
Definition: DLNAControlPointRequestParser.h:17
Lightweight DLNA control point manager.
Definition: DLNAControlPoint.h:61
static bool handleNotifyByebye(Str &usn)
Definition: DLNAControlPoint.h:548
void * reference
Definition: DLNAControlPoint.h:541
void setHttpServer(IHttpServer &server) override
Set HttpServer instance and register the notify handler.
Definition: DLNAControlPoint.h:119
void end() override
Stops the processing and releases the resources.
Definition: DLNAControlPoint.h:246
DLNAControlPoint(IHttpServer &server)
Constructor supporting Notifications.
Definition: DLNAControlPoint.h:79
size_t createSoapXML(ActionRequest &action, Print &out)
Build the SOAP XML request body for the action into out
Definition: DLNAControlPoint.h:800
bool matches(const char *usn)
checks if the usn contains the search target
Definition: DLNAControlPoint.h:680
size_t createXML(ActionRequest &action)
Definition: DLNAControlPoint.h:721
static bool processMSearchReply(MSearchReplyCP &data)
Processes an M-SEARCH HTTP 200 reply and attempts to add the device.
Definition: DLNAControlPoint.h:651
int default_device_idx
Definition: DLNAControlPoint.h:533
static bool handleNotifyAlive(NotifyReplyCP &data)
Definition: DLNAControlPoint.h:571
bool addDevice(Url url) override
Adds the device from the device xml url if it does not already exist.
Definition: DLNAControlPoint.h:444
bool is_parse_device
Definition: DLNAControlPoint.h:536
Vector< DLNADeviceInfo > devices
Definition: DLNAControlPoint.h:529
const char * getUrlImpl(DLNADeviceInfo &device, const char *suffix, const char *buffer, int len)
Definition: DLNAControlPoint.h:911
Url local_url
Definition: DLNAControlPoint.h:538
bool addDevice(DLNADeviceInfo dev) override
Adds a new device.
Definition: DLNAControlPoint.h:428
bool allow_localhost
Definition: DLNAControlPoint.h:539
IHttpServer * p_http_server
Definition: DLNAControlPoint.h:540
Scheduler scheduler
Definition: DLNAControlPoint.h:525
~DLNAControlPoint() override=default
bool is_active
Definition: DLNAControlPoint.h:535
void setLocalURL(Url url) override
Defines the local url (needed for subscriptions)
Definition: DLNAControlPoint.h:89
void setTransports(IHttpRequest &http, IUDPService &udp)
Definition: DLNAControlPoint.h:209
SubscriptionMgrControlPoint * getSubscriptionMgr() override
Provides the subscription manager.
Definition: DLNAControlPoint.h:514
DLNADeviceInfo NO_DEVICE
Definition: DLNAControlPoint.h:524
void parseResult()
Parses the XML response from the HTTP client and populates reply arguments.
Definition: DLNAControlPoint.h:859
const char * getUrl(DLNADeviceInfo &device, const char *suffix, char *buffer, int len) override
Definition: DLNAControlPoint.h:422
DLNAServiceInfo & getService(const char *id) override
Provide addess to the service information.
Definition: DLNAControlPoint.h:361
void setDeviceIndex(int idx) override
Selects the default device by index.
Definition: DLNAControlPoint.h:107
void setSearchRepeatMs(int repeatMs) override
Sets the repeat interval for M-SEARCH requests (define before begin)
Definition: DLNAControlPoint.h:99
int msearch_repeat_ms
Definition: DLNAControlPoint.h:534
void setActive(bool flag) override
We can activate/deactivate the scheduler.
Definition: DLNAControlPoint.h:502
void setEventSubscriptionCallback(std::function< void(const char *sid, const char *varName, const char *newValue, void *reference)> cb, void *ref=nullptr) override
Register a callback that will be invoked for incoming event notification.
Definition: DLNAControlPoint.h:110
ActionReply & processActionHttpPost(ActionRequest &action, Url &post_url, const char *soapAction, XMLCallback xmlProcessor=nullptr)
Processes an HTTP POST request for the given action and URL.
Definition: DLNAControlPoint.h:807
void setLocalURL(IPAddress url, int port=9001, const char *path="") override
Set the local callback URL for event subscriptions.
Definition: DLNAControlPoint.h:90
Vector< ActionRequest > actions
Definition: DLNAControlPoint.h:530
bool isActive() override
Checks if the scheduler is active.
Definition: DLNAControlPoint.h:505
bool loop() override
Definition: DLNAControlPoint.h:318
static bool isUdnKnown(const char *usn_c, DLNADeviceInfo *&outDev)
Definition: DLNAControlPoint.h:555
ActionReply & getLastReply() override
Provides the last reply.
Definition: DLNAControlPoint.h:511
const char * search_target
Definition: DLNAControlPoint.h:537
void onResultNode(std::function< void(const char *nodeName, const char *text, const char *attributes)> cb) override
Definition: DLNAControlPoint.h:127
DLNADeviceInfo & getDevice() override
Provides the device information of the actually selected device.
Definition: DLNAControlPoint.h:372
static bool processDevice(NotifyReplyCP &data)
Definition: DLNAControlPoint.h:638
std::function< void(const char *nodeName, const char *text, const char *attributes)> result_callback
Definition: DLNAControlPoint.h:544
DLNADeviceInfo & getDevice(int idx) override
Provides the device information by index.
Definition: DLNAControlPoint.h:380
bool processBye(Str &usn)
processes a bye-bye message
Definition: DLNAControlPoint.h:688
DLNADeviceInfo & getDevice(Url location) override
Get a device for a Url.
Definition: DLNAControlPoint.h:401
ActionReply & postAction(ActionRequest &action, XMLCallback xmlProcessor=nullptr)
Definition: DLNAControlPoint.h:764
ActionReply reply
Definition: DLNAControlPoint.h:531
void setParseDevice(bool flag) override
Requests the parsing of the device information.
Definition: DLNAControlPoint.h:86
Vector< DLNADeviceInfo > & getDevices() override
Get list of all discovered devices.
Definition: DLNAControlPoint.h:414
bool begin(IHttpRequest &http, IUDPService &udp, const char *searchTarget="ssdp:all", uint32_t minWaitMs=3000, uint32_t maxWaitMs=60000) override
Start discovery by sending M-SEARCH requests and process replies.
Definition: DLNAControlPoint.h:155
void setNotificationsActive(bool flag) override
Activate/deactivate subscription notifications.
Definition: DLNAControlPoint.h:519
IHttpRequest * p_http
Definition: DLNAControlPoint.h:526
bool begin(const char *searchTarget="ssdp:all", uint32_t minWaitMs=3000, uint32_t maxWaitMs=60000) override
Start discovery with default HTTP/UDP services.
Definition: DLNAControlPoint.h:133
DLNADeviceInfo & getDevice(DLNAServiceInfo &service) override
Provides the device for a service.
Definition: DLNAControlPoint.h:390
ActionReply & postAllActions(XMLCallback xmlProcessor=nullptr)
Definition: DLNAControlPoint.h:755
ActionReply & executeActions(XMLCallback xmlProcessor=nullptr) override
Executes action and parses the reply xml to collect the reply entries. If an XML processor is provide...
Definition: DLNAControlPoint.h:304
DLNAControlPoint(IHttpRequest &http, IUDPService &udp, IHttpServer &server)
Constructor wiring HTTP and UDP dependencies; notifications disabled.
Definition: DLNAControlPoint.h:72
ActionRequest & addAction(ActionRequest act) override
Registers a method that will be called.
Definition: DLNAControlPoint.h:290
bool subscribe()
Subscribes to event notifications for all services of the selected device.
Definition: DLNAControlPoint.h:216
XMLPrinter xml_printer
Definition: DLNAControlPoint.h:532
bool isDiscoveryAllowed(Url &url)
Definition: DLNAControlPoint.h:604
DLNAControlPoint(IHttpRequest &http, IUDPService &udp)
Constructor wiring HTTP and UDP dependencies; notifications disabled.
Definition: DLNAControlPoint.h:67
DLNAControlPoint()
Default constructor w/o Notifications.
Definition: DLNAControlPoint.h:64
void setReference(void *ref) override
Attach an opaque reference pointer (optional, for caller context)
Definition: DLNAControlPoint.h:104
SubscriptionMgrControlPoint subscription_mgr
Definition: DLNAControlPoint.h:528
void unEscapeStr(Str &str)
Definition: DLNAControlPoint.h:905
void setAllowLocalhost(bool flag) override
Defines if localhost devices should be allowed.
Definition: DLNAControlPoint.h:508
IUDPService * p_udp
Definition: DLNAControlPoint.h:527
Device Attributes and generation of XML using urn:schemas-upnp-org:device-1-0. We could just return a...
Definition: DLNADeviceInfo.h:28
Url device_url
Definition: DLNADeviceInfo.h:284
Str base_url
Definition: DLNADeviceInfo.h:288
const char * getUDN()
Provide the udn uuid.
Definition: DLNADeviceInfo.h:90
const char * getBaseURL()
Provides the base url.
Definition: DLNADeviceInfo.h:120
virtual bool begin()
Override to initialize the device.
Definition: DLNADeviceInfo.h:61
Url & getDeviceURL()
This method returns base url/device.xml.
Definition: DLNADeviceInfo.h:131
void setActive(bool flag)
Sets the server to inactive.
Definition: DLNADeviceInfo.h:267
Attributes needed for the DLNA Service Definition.
Definition: DLNAServiceInfo.h:18
Str service_type
Definition: DLNAServiceInfo.h:37
Str control_url
Definition: DLNAServiceInfo.h:40
HttpHeader & put(const char *key, const char *value)
Definition: HttpHeader.h:105
Abstract interface for DLNA Control Point functionality.
Definition: IControlPoint.h:43
Abstract interface for HTTP client request functionality.
Definition: IHttpRequest.h:21
virtual void setTimeout(int ms)=0
Set request timeout in milliseconds.
virtual Client * client()=0
Get pointer to client.
virtual int read(uint8_t *str, int len)=0
Read data from response.
virtual HttpRequestHeader & request()=0
Get reference to request header.
virtual int post(Url &url, const char *mime, const char *data, int len=-1)=0
Send POST request with string data.
virtual void stop()=0
Stop the connection.
virtual int get(Url &url, const char *acceptMime=nullptr, const char *data=nullptr, int len=-1)=0
Send GET request.
virtual void setConnection(const char *connection)=0
Set Connection header.
virtual bool isKeepAlive()=0
Do not close the connection after request.
Abstract interface for HTTP server functionality.
Definition: IHttpServer.h:50
virtual bool begin()=0
virtual bool doLoop()=0
Process server loop.
virtual void end()=0
virtual bool isActive()=0
Abstract Interface for UDP API.
Definition: IUDPService.h:33
virtual RequestData receive()=0
Receive incoming UDP data and peer information.
virtual bool begin(int port)=0
Initialize UDP service on specified port.
Processing at control point to handle a MSearchReply from the device.
Definition: Schedule.h:207
Str usn
Definition: Schedule.h:211
Str location
Definition: Schedule.h:210
Send MSearch request.
Definition: Schedule.h:46
Represents a notification/notify reply scheduled for control-point processing.
Definition: Schedule.h:248
Str nts
Definition: Schedule.h:251
Class with does not do any output: it can be used to determine the length of the output.
Definition: NullPrint.h:12
Scheduler which processes all due Schedules (to send out UDP replies)
Definition: Scheduler.h:15
int execute(IUDPService &udp)
Execute all due schedules.
Definition: Scheduler.h:30
void setActive(bool flag)
Definition: Scheduler.h:81
int size()
Number of queued schedules.
Definition: Scheduler.h:79
bool isMSearchActive()
Returns true if there is any active schedule with name "MSearch".
Definition: Scheduler.h:69
void add(Schedule *schedule)
Add a schedule to the scheduler.
Definition: Scheduler.h:18
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 void add(int value)
adds a int value
Definition: StrView.h:128
virtual const char * c_str()
provides the string value as const char*
Definition: StrView.h:376
virtual bool replace(const char *toReplace, const int replaced)
Replaces the first instance of toReplace with replaced.
Definition: StrView.h:395
virtual bool equals(const char *str)
checks if the string equals indicated parameter string
Definition: StrView.h:177
virtual bool contains(const char *str)
checks if the string contains a substring
Definition: StrView.h:284
Heap-backed string utility used throughout tiny_dlna.
Definition: Str.h:27
bool equals(const char *other) const
Exact string equality with C-string.
Definition: Str.h:167
bool isEmpty() const
True if empty.
Definition: Str.h:54
bool endsWith(const char *suffix) const
True if ends with suffix (case-sensitive)
Definition: Str.h:175
bool startsWith(const char *prefix) const
True if starts with prefix (case-sensitive)
Definition: Str.h:206
int replaceAll(const char *toReplace, const char *replaced)
Replace all occurrences of toReplace with replaced; returns count.
Definition: Str.h:279
void clear()
Clear contents (size -> 0)
Definition: Str.h:93
const char * c_str() const
C-string pointer to internal buffer.
Definition: Str.h:88
Standalone manager for UPnP/DLNA event subscriptions used by control points and devices.
Definition: SubscriptionMgrControlPoint.h:44
void setEventSubscriptionCallback(std::function< void(const char *sid, const char *varName, const char *newValue, void *reference)> callback, void *ref=nullptr)
Register a callback invoked when event properties change.
Definition: SubscriptionMgrControlPoint.h:108
bool loop()
Periodic work to manage subscription renewals and retries.
Definition: SubscriptionMgrControlPoint.h:198
void setHttpServer(IHttpServer &server)
Attach an HttpServer instance and register the internal NOTIFY handler.
Definition: SubscriptionMgrControlPoint.h:85
void setNotificationsActive(bool active)
Turn automatic event subscription on or off.
Definition: SubscriptionMgrControlPoint.h:172
void setup(IHttpRequest &http, IUDPService &udp, Url &localCallbackUrl, DLNADeviceInfo &device)
Inject required dependencies and initialize the manager.
Definition: SubscriptionMgrControlPoint.h:62
URL parser which breaks a full url string up into its individual parts.
Definition: Url.h:18
int port()
Definition: Url.h:46
const char * host()
Definition: Url.h:41
const char * urlRoot()
Definition: Url.h:43
const char * protocol()
Definition: Url.h:42
void setUrl(const char *url)
Definition: Url.h:48
const char * url()
Definition: Url.h:39
void clear()
Definition: Url.h:63
Lightweight wrapper around std::vector with Arduino-friendly helpers and a pluggable allocator.
Definition: Vector.h:39
Incremental XML device parser using XMLParserPrint.
Definition: XMLDeviceParser.h:20
Helper class that implements a Print interface to accumulate XML data and then parse it using XMLPars...
Definition: XMLParserPrint.h:16
size_t write(uint8_t ch) override
Writes a single byte to the buffer (Print interface)
Definition: XMLParserPrint.h:32
void setExpandEncoded(bool flag)
Forwards expand-entities setting to the underlying XMLParser.
Definition: XMLParserPrint.h:48
void end()
Resets the internal buffer.
Definition: XMLParserPrint.h:87
bool parse(Str &outNodeName, Vector< Str > &outPath, Str &outText, Str &outAttributes)
Parses the accumulated XML data and returns results via output parameters.
Definition: XMLParserPrint.h:59
#define DLNA_REQUEST_KEEP_ALIVE
Keep the session open for multiple requests.
Definition: dlna_config.h:30
#define DLNA_MAX_URL_LEN
app-wide max URL length
Definition: dlna_config.h:55
#define XML_PARSER_BUFFER_SIZE
Define XML parse buffer size.
Definition: dlna_config.h:35
#define DLNA_HTTP_REQUEST_TIMEOUT_MS
Define the default http request timeout.
Definition: dlna_config.h:25
#define DLNA_DISCOVERY_NETMASK
Define the netmask for discovery filtering.
Definition: dlna_config.h:197
Definition: Allocator.h:13
const char * CON_KEEP_ALIVE
Definition: HttpHeader.h:20
const char * CON_CLOSE
Definition: HttpHeader.h:19
DLNAControlPoint * selfDLNAControlPoint
Definition: DLNAControlPoint.h:23
std::function< void(Client &client, ActionReply &reply)> XMLCallback
Definition: IControlPoint.h:25
Provides information of the received UDP which consists of the (xml) data and the peer address and po...
Definition: IUDPService.h:22
An individual Schedule (to send out UDP messages)
Definition: Schedule.h:18
uint64_t end_time
Definition: Schedule.h:25
bool active
Definition: Schedule.h:27
virtual const char * name()
Definition: Schedule.h:35
uint32_t repeat_ms
Definition: Schedule.h:23
Functions to efficiently output XML. XML data contains a lot of redundancy so it is more memory effic...
Definition: XMLPrinter.h:56
size_t printNodeBegin(const char *node, const char *attributes=nullptr, const char *ns=nullptr)
Prints the beginning of an XML node.
Definition: XMLPrinter.h:314
size_t printNode(XMLNode node)
Prints an XML node from XMLNode struct.
Definition: XMLPrinter.h:89
size_t printNodeEnd(const char *node, const char *ns=nullptr)
Prints the end of an XML node.
Definition: XMLPrinter.h:352
void setOutput(Print &output)
Defines the output Print object.
Definition: XMLPrinter.h:73
size_t printXMLHeader()
Prints the XML header.
Definition: XMLPrinter.h:79
void clear()
Definition: XMLPrinter.h:363