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"
9#include "dlna/Schedule.h"
10#include "dlna/Scheduler.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, const char* path="") override {
91 char buffer[200];
92 snprintf(buffer, sizeof(buffer), "http://%s:%d%s", url.toString().c_str(), port,
93 path);
94 local_url.setUrl(buffer);
95 }
96
98 void setSearchRepeatMs(int repeatMs) override {
99 msearch_repeat_ms = repeatMs;
100 }
101
103 void setReference(void* ref) override { reference = ref; }
104
106 void setDeviceIndex(int idx) override { default_device_idx = 0; }
107
110 std::function<void(const char* sid, const char* varName,
111 const char* newValue, void* reference)>
112 cb,
113 void* ref = nullptr) override {
115 }
116
118 void setHttpServer(IHttpServer& server) override {
119 DlnaLogger.log(DlnaLogLevel::Debug, "DLNAControlPointMgr::setHttpServer");
120 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 if (p_http_server) {
165 // handle server requests
166 if (!p_http_server->begin()) {
167 DlnaLogger.log(DlnaLogLevel::Error, "HttpServer begin failed");
168 return false;
169 }
170 }
171
172 // setup multicast UDP
173 if (!(p_udp->begin(DLNABroadcastAddress))) {
174 DlnaLogger.log(DlnaLogLevel::Error, "UDP begin failed");
175 return false;
176 }
177
178 // Send MSearch request via UDP. Use maxWaitMs as the emission window.
179 MSearchSchedule* search =
180 new MSearchSchedule(DLNABroadcastAddress, searchTarget);
181
182 // ensure min <= max
183 if (minWaitMs > maxWaitMs) minWaitMs = maxWaitMs;
184 search->end_time = millis() + maxWaitMs;
186 search->active = true;
187 scheduler.add(search);
188
189 // if maxWaitMs > 0 we will block here and process events. We guarantee
190 // we wait at least minWaitMs before returning; we stop waiting after
191 // maxWaitMs or earlier if stopWhenFound is true AND minWaitMs has elapsed
192 uint64_t start = millis();
193 uint64_t minEnd = start + minWaitMs;
194 uint64_t maxEnd = start + maxWaitMs;
195 while (millis() < maxEnd) {
196 // if a device is found and we've satisfied the minimum wait, we can
197 // return early (we always allow early return after minWaitMs when a
198 // device has been discovered)
199 if (devices.size() > 0 && millis() >= minEnd) break;
200 loop();
201 }
202
203 // instantiate subscription manager
205
206 // If we exited early because a device was found, deactivate the MSearch
207 // schedule so it will stop repeating. The scheduler will clean up
208 // inactive schedules on its next pass.
209 if (devices.size() > 0 && search != nullptr) {
210 search->active = false;
211 }
212
213 DlnaLogger.log(DlnaLogLevel::Info,
214 "Control Point started with %d devices found",
215 devices.size());
216
217 return devices.size() > 0;
218 }
219
221 p_http = &http;
222 p_udp = &udp;
223 }
224
226 void end() override {
227 DlnaLogger.log(DlnaLogLevel::Debug, "DLNAControlPointMgr::end");
228 // stop active processing
229 is_active = false;
230
231 // drain scheduler (delete schedules). Execute will call cleanup which
232 // deletes inactive schedules — run it until queue is empty. Only run if
233 // we have a udp instance to avoid calling execute with a null reference.
234 scheduler.setActive(false);
235 if (p_udp) {
236 while (scheduler.size() > 0) {
238 }
239 }
240 // p_server->end();
241 // clear device contents and drop containers
242 for (auto& device : devices) device.clear();
243 devices.clear();
244
245 // clear pending actions
246 actions.clear();
247
248 // stop http server if attached
249 if (p_http_server) {
251 p_http_server = nullptr;
252 }
253
254 // reset xml printer state
256
257 // clear last reply
258 reply.clear();
260
261 // reset transport/client refs
262 p_http = nullptr;
263 p_udp = nullptr;
264
265 // reset indices and references
267 }
268
271 DlnaLogger.log(DlnaLogLevel::Debug, "DLNAControlPointMgr::addAction");
272 actions.push_back(act);
273 return actions[actions.size() - 1];
274 }
275
284 ActionReply& executeActions(XMLCallback xmlProcessor = nullptr) override {
285 DlnaLogger.log(DlnaLogLevel::Debug, "DLNAControlPointMgr::executeActions");
286 reply.clear();
287 postAllActions(xmlProcessor);
288
289 // Default behaviour: log and dump collected reply arguments
290 DlnaLogger.log(DlnaLogLevel::Info, "Collected reply arguments: %d",
291 (int)reply.size());
293 return reply;
294 }
295
298 bool loop() override {
299 if (!is_active) return false;
301
302 // process subscriptions
304
305 // process UDP requests
306 RequestData req = p_udp->receive();
307 if (req && scheduler.isMSearchActive()) {
308 Schedule* schedule = parser.parse(req);
309 if (schedule != nullptr) {
310 // handle NotifyReplyCP
311 if (StrView(schedule->name()).equals("NotifyReplyCP")) {
312 NotifyReplyCP& notify_schedule = *(NotifyReplyCP*)schedule;
313 // notify_schedule.callback = processDevice;
314 processDevice(notify_schedule);
315 }
316 // handle MSearchReplyCP (HTTP/1.1 200 OK responses to M-SEARCH)
317 if (StrView(schedule->name()).equals("MSearchReplyCP")) {
318 MSearchReplyCP& ms_schedule = *(MSearchReplyCP*)schedule;
319 // Process M-SEARCH replies immediately: they contain LOCATION URLs
320 // pointing to device descriptions. Add the device and free the
321 // temporary schedule object.
322 processMSearchReply(ms_schedule);
323 }
324 }
325 }
326
327 // execute scheduled udp replys
329
331 // handle server requests
332 bool rc = p_http_server->doLoop();
333 }
334
335 // be nice, if we have other tasks
336 delay(5);
337 return true;
338 }
339
341 DLNAServiceInfo& getService(const char* id) override {
342 DlnaLogger.log(DlnaLogLevel::Debug, "DLNAControlPointMgr::getService");
343 static DLNAServiceInfo no_service(false);
344 for (auto& dev : devices) {
345 DLNAServiceInfo& result = dev.getService(id);
346 if (result) return result;
347 }
348 return no_service;
349 }
350
353 DlnaLogger.log(DlnaLogLevel::Debug, "DLNAControlPointMgr::getDevice");
355 }
356
358 DLNADeviceInfo& getDevice(int idx) override {
359 DlnaLogger.log(DlnaLogLevel::Debug, "DLNAControlPointMgr::getDevice");
360 if (idx < 0 || idx >= devices.size()) {
361 DlnaLogger.log(DlnaLogLevel::Error, "Device index %d out of range", idx);
362 return NO_DEVICE;
363 }
364 return devices[idx];
365 }
366
369 DlnaLogger.log(DlnaLogLevel::Debug, "DLNAControlPointMgr::getDevice");
370 for (auto& dev : devices) {
371 for (auto& srv : dev.getServices()) {
372 if (srv == service) return dev;
373 }
374 }
375 return NO_DEVICE;
376 }
377
379 DLNADeviceInfo& getDevice(Url location) override {
380 DlnaLogger.log(DlnaLogLevel::Debug, "DLNAControlPointMgr::getDevice");
381 for (auto& dev : devices) {
382 if (dev.getDeviceURL() == location) {
383 DlnaLogger.log(DlnaLogLevel::Debug,
384 "DLNAControlPointMgr::getDevice: Found device %s",
385 location.url());
386 return dev;
387 }
388 }
389 return NO_DEVICE;
390 }
391
393
400 const char* getUrl(DLNADeviceInfo& device, const char* suffix, char* buffer,
401 int len) override {
402 return getUrlImpl(device, suffix, buffer, len);
403 }
404
406 bool addDevice(DLNADeviceInfo dev) override {
407 DlnaLogger.log(DlnaLogLevel::Debug, "DLNAControlPointMgr::addDevice");
408 for (auto& existing_device : devices) {
409 if (dev.getUDN() == existing_device.getUDN()) {
410 DlnaLogger.log(DlnaLogLevel::Debug, "Device '%s' already exists",
411 dev.getUDN());
412 return false;
413 }
414 }
415 DlnaLogger.log(DlnaLogLevel::Info, "Device '%s' has been added",
416 dev.getUDN());
417 devices.push_back(dev);
418 return true;
419 }
420
422 bool addDevice(Url url) override {
423 DlnaLogger.log(DlnaLogLevel::Debug, "DLNAControlPointMgr::addDevice");
424 DLNADeviceInfo& device = getDevice(url);
425 if (device != NO_DEVICE) {
426 // device already exists
427 device.setActive(true);
428 return true;
429 }
430 // http get - incremental parse using XMLParserPrint so we don't hold
431 // the whole device.xml in memory
432 int rc = p_http->get(url, "text/xml");
433
434 if (rc != 200) {
435 DlnaLogger.log(DlnaLogLevel::Error, "Http get to '%s' failed with %d",
436 url.url(), rc);
437 return false;
438 }
439
440 DLNADeviceInfo new_device;
441 new_device.base_url.clear();
442 new_device.device_url = url;
443
444 XMLDeviceParser device_parser{new_device};
445 uint8_t buffer[XML_PARSER_BUFFER_SIZE];
446
447 // Read and incrementally parse into new_device
448 device_parser.begin();
449 while (true) {
450 int len = p_http->read(buffer, sizeof(buffer));
451 if (len <= 0) break;
452 device_parser.parse(buffer, len);
453 }
454 device_parser.end(new_device);
455
456 // if base_url is empty, derive it from the device description URL root
457 if (new_device.base_url.isEmpty()) {
458 new_device.base_url = url.urlRoot();
459 }
460
461 // Avoid adding the same device multiple times: check UDN uniqueness
462 for (auto& existing_device : devices) {
463 if (existing_device.getUDN() == new_device.getUDN()) {
464 DlnaLogger.log(DlnaLogLevel::Debug,
465 "Device '%s' already exists (skipping add)",
466 new_device.getUDN());
467 // Make sure the existing device is marked active and keep the URL
468 existing_device.setActive(true);
469 return true;
470 }
471 }
472
473 DlnaLogger.log(DlnaLogLevel::Info, "Device '%s' has been added",
474 new_device.getUDN());
475 devices.push_back(new_device);
476 return true;
477 }
478
480 void setActive(bool flag) override { is_active = flag; }
481
483 bool isActive() override { return is_active; }
484
486 void setAllowLocalhost(bool flag) override { allow_localhost = flag; }
487
489 ActionReply& getLastReply() override { return reply; }
490
493 return &subscription_mgr;
494 }
495
497 void setSubscribeNotificationsActive(bool flag) override {
499 }
500
502 const char* registerString(const char* s) override {
503 return s; // registry removed
504 }
505
506 protected:
510 IUDPService* p_udp = nullptr;
517 int msearch_repeat_ms = 10000;
518 bool is_active = false;
519 bool is_parse_device = false;
520 const char* search_target;
522 bool allow_localhost = false;
524 void* reference = nullptr;
525 std::function<void(const char* nodeName, const char* text,
526 const char* attributes)>
528
531 static bool handleNotifyByebye(Str& usn) {
532 // delegate to existing bye handling
533 return selfDLNAControlPoint->processBye(usn);
534 }
535
538 static bool isUdnKnown(const char* usn_c, DLNADeviceInfo*& outDev) {
539 outDev = nullptr;
540 if (!usn_c || *usn_c == '\0') return false;
541 const char* sep = strstr(usn_c, "::");
542 int udn_len = sep ? (int)(sep - usn_c) : (int)strlen(usn_c);
543 for (auto& dev : selfDLNAControlPoint->devices) {
544 const char* known_udn = dev.getUDN();
545 if (known_udn && strncmp(known_udn, usn_c, udn_len) == 0 &&
546 (int)strlen(known_udn) == udn_len) {
547 outDev = &dev;
548 return true;
549 }
550 }
551 return false;
552 }
553
554 static bool handleNotifyAlive(NotifyReplyCP& data) {
555 bool select = selfDLNAControlPoint->matches(data.usn.c_str());
556 DlnaLogger.log(DlnaLogLevel::Debug, "addDevice: %s -> %s", data.usn.c_str(),
557 select ? "added" : "filtered");
558 if (!select) return false;
559
560 DLNADeviceInfo* existing = nullptr;
561 if (isUdnKnown(data.usn.c_str(), existing)) {
562 DlnaLogger.log(DlnaLogLevel::Debug,
563 "Device '%s' already known (skip GET)",
564 existing ? existing->getUDN() : "<unknown>");
565 if (existing) existing->setActive(true);
566 return true;
567 }
568
569 // Apply discovery netmask filtering before adding the device
570 Url url{data.location.c_str()};
572 DlnaLogger.log(DlnaLogLevel::Info,
573 "Device '%s' filtered by netmask (LOCATION %s)",
574 data.usn.c_str(), url.host());
575 return false;
576 }
577
578 // Not known and subnet accepted -> fetch and add device description
580 return true;
581 }
582
583 // Returns true if the LOCATION URL's host is in the same subnet as the
584 // local_url host according to DLNA_DISCOVERY_NETMASK. If either side is
585 // not a numeric IP literal or information is missing, the function
586 // permits discovery (returns true).
588 IPAddress netmask = DLNA_DISCOVERY_NETMASK;
589
590 const char* peerHost = url.host();
591 const char* localHost = local_url.host();
592
593 IPAddress peerIP;
594 IPAddress localIP;
595 bool peerOK = (peerHost && *peerHost) ? peerIP.fromString(peerHost) : false;
596 bool localOK =
597 (localHost && *localHost) ? localIP.fromString(localHost) : false;
598
599 // If we cannot parse both addresses, allow by default.
600 if (!peerOK || !localOK) return true;
601
602 for (int i = 0; i < 4; i++) {
603 if ((localIP[i] & netmask[i]) != (peerIP[i] & netmask[i])) {
604 DlnaLogger.log(DlnaLogLevel::Info,
605 "Discovery filtered: local=%d.%d.%d.%d peer=%d.%d.%d.%d "
606 "mask=%d.%d.%d.%d",
607 localIP[0], localIP[1], localIP[2], localIP[3],
608 peerIP[0], peerIP[1], peerIP[2], peerIP[3], netmask[0],
609 netmask[1], netmask[2], netmask[3]);
610 return false;
611 }
612 }
613
614 DlnaLogger.log(DlnaLogLevel::Debug,
615 "Discovery accepted: local=%d.%d.%d.%d peer=%d.%d.%d.%d",
616 localIP[0], localIP[1], localIP[2], localIP[3], peerIP[0],
617 peerIP[1], peerIP[2], peerIP[3]);
618 return true;
619 }
620
621 static bool processDevice(NotifyReplyCP& data) {
622 DlnaLogger.log(DlnaLogLevel::Debug, "DLNAControlPointMgr::processDevice");
623 Str& nts = data.nts;
624 if (nts.equals("ssdp:byebye")) {
625 return handleNotifyByebye(nts);
626 }
627 if (nts.equals("ssdp:alive")) {
628 return handleNotifyAlive(data);
629 }
630 return false;
631 }
632
635 DlnaLogger.log(DlnaLogLevel::Debug,
636 "DLNAControlPointMgr::processMSearchReply");
637 // data.location contains the device description URL
638 if (data.location.isEmpty()) return false;
639
640 const char* usn_c = data.usn.c_str();
641 if (usn_c && *usn_c) {
642 const char* sep = strstr(usn_c, "::");
643 int udn_len = sep ? (int)(sep - usn_c) : (int)strlen(usn_c);
644 for (auto& dev : selfDLNAControlPoint->devices) {
645 const char* known_udn = dev.getUDN();
646 if (known_udn && strncmp(known_udn, usn_c, udn_len) == 0 &&
647 (int)strlen(known_udn) == udn_len) {
648 DlnaLogger.log(DlnaLogLevel::Debug,
649 "MSearchReply: device '%s' already known (skip GET)",
650 known_udn);
651 dev.setActive(true);
652 return true;
653 }
654 }
655 }
656
657 Url url{data.location.c_str()};
659 return true;
660 }
661
663 bool matches(const char* usn) {
664 if (StrView(search_target).equals("ssdp:all")) return true;
665 return StrView(usn).contains(search_target);
666 }
667
669 bool processBye(Str& usn) {
670 DlnaLogger.log(DlnaLogLevel::Debug, "DLNAControlPointMgr::processBye");
671 for (auto& dev : devices) {
672 if (usn.startsWith(dev.getUDN())) {
673 for (auto& srv : dev.getServices()) {
674 srv.is_active = false;
675 if (usn.endsWith(srv.service_type)) {
676 if (srv.is_active) {
677 DlnaLogger.log(DlnaLogLevel::Info, "removeDevice: %s", usn);
678 srv.is_active = false;
679 }
680 }
681 }
682 }
683 }
684 return false;
685 }
686
702 size_t createXML(ActionRequest& action) {
703 DlnaLogger.log(DlnaLogLevel::Debug, "DLNAControlPointMgr::createXML");
704 size_t result = xml_printer.printXMLHeader();
705
706 result += xml_printer.printNodeBegin(
707 "Envelope",
708 "xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" "
709 "s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\"",
710 "s");
711 result += xml_printer.printNodeBegin("Body", nullptr, "s");
712
713 char ns[200];
714 StrView namespace_str(ns, 200);
715 namespace_str = "xmlns:u=\"%1\"";
716 bool ok = namespace_str.replace("%1", action.getService()->service_type);
717 DlnaLogger.log(DlnaLogLevel::Debug, "ns = '%s'", namespace_str.c_str());
718
719 // assert(ok);
720 result += xml_printer.printNodeBegin(action.getAction(),
721 namespace_str.c_str(), "u");
722 for (auto arg : action.getArguments()) {
723 const char* n = arg.name.c_str();
724 const char* v = arg.value.c_str();
725 if (n && *n) {
726 result += xml_printer.printNode(n, v);
727 }
728 }
729 result += xml_printer.printNodeEnd(action.getAction(), "u");
730
731 result += xml_printer.printNodeEnd("Body", "s");
732 result += xml_printer.printNodeEnd("Envelope", "s");
733 return result;
734 }
735
736 ActionReply& postAllActions(XMLCallback xmlProcessor = nullptr) {
737 reply.clear();
738 DlnaLogger.log(DlnaLogLevel::Debug, "DLNAControlPointMgr::postAllActions");
739 for (auto& action : actions) {
740 if (action) postAction(action, xmlProcessor);
741 }
742 return reply;
743 }
744
746 XMLCallback xmlProcessor = nullptr) {
747 DlnaLogger.log(DlnaLogLevel::Debug, "DLNAControlPointMgr::postAction: %s",
748 action.getAction());
749 reply.clear();
750 DLNAServiceInfo& service = *(action.getService());
751 DLNADeviceInfo& device = getDevice(service);
752
753 // create SOAPACTION header value
754 char act[200];
755 StrView action_str{act, 200};
756 action_str = "\"";
757 action_str.add(action.getService()->service_type);
758 action_str.add("#");
759 action_str.add(action.getAction());
760 action_str.add("\"");
761
762 // crate control url
763 char url_buffer[DLNA_MAX_URL_LEN] = {0};
764 // Log service and base to help debug malformed control URLs
765 DlnaLogger.log(DlnaLogLevel::Info,
766 "Service control_url: %s, device base: %s",
767 StrView(service.control_url).c_str(),
768 StrView(device.getBaseURL()).c_str());
769 Url post_url = getUrl(device, service.control_url, url_buffer, DLNA_MAX_URL_LEN);
770 DlnaLogger.log(DlnaLogLevel::Info, "POST URL computed: %s", post_url.url());
771
772 // send HTTP POST and collect/handle response. If the caller provided an
773 // httpProcessor we forward it so they can process the raw HTTP reply
774 // (and avoid the library's default XML parsing) before consumption.
775 return processActionHttpPost(action, post_url, action_str.c_str(),
776 xmlProcessor);
777 }
778
780 size_t createSoapXML(ActionRequest& action, Print& out) {
781 DlnaLogger.log(DlnaLogLevel::Debug, "DLNAControlPointMgr::createSoapXML");
783 return createXML(action);
784 }
785
788 const char* soapAction,
789 XMLCallback xmlProcessor = nullptr) {
790 DlnaLogger.log(DlnaLogLevel::Debug,
791 "DLNAControlPointMgr::processActionHttpPost");
792
793 NullPrint np;
794 size_t xmlLen = createSoapXML(action, np);
795 // dynamically create and write xml
796 auto printXML = [this, &action, &xmlLen](Print& out, void* ref) -> size_t {
797 (void)ref;
798 return createSoapXML(action, out);
799 };
800 p_http->stop();
801 p_http->request().put("SOAPACTION", soapAction);
802 int rc = p_http->post(post_url, xmlLen, printXML, "text/xml");
803
804 // abort on HTTP error
805 if (rc != 200) {
806 p_http->stop();
807 reply.setValid(false);
808 DlnaLogger.log(DlnaLogLevel::Error, "Action '%s' failed with HTTP rc %d",
809 StrView(soapAction).c_str(), rc);
810 return reply;
811 }
812
813 reply.setValid(true);
814 reply.clear();
815
816 // If caller provided a custom XML processor, let it handle the live
817 // client/response. This allows callers to avoid the default XML parsing
818 // which would otherwise consume the response stream.
819 if (xmlProcessor) {
820 Client* c = p_http->client();
821 if (c) xmlProcessor(*c, reply);
822 return reply;
823 }
824
825 // Default: parse response incrementally and populate reply arguments
826 parseResult();
827 return reply;
828 }
829
837 void parseResult() {
838 XMLParserPrint xml_parser;
839 xml_parser.setExpandEncoded(true);
840 Str outNodeName;
841 Vector<Str> outPath;
842 Str outText;
843 Str outAttributes;
844 uint8_t buffer[XML_PARSER_BUFFER_SIZE];
845 while (p_http->client()->available()) {
846 int len = p_http->client()->read(buffer, XML_PARSER_BUFFER_SIZE);
847 if (len > 0) {
848 xml_parser.write(buffer, len);
849 while (xml_parser.parse(outNodeName, outPath, outText, outAttributes)) {
850 Argument arg;
851 // For most nodes we only add arguments when they contain text or
852 // attributes. The special "Result" node contains the DIDL-Lite
853 // payload (raw XML) and must be preserved so callers (e.g. the
854 // MediaServer helper) can parse returned media items.
855 if (!(outText.isEmpty() && outAttributes.isEmpty()) ||
856 outNodeName.equals("Result")) {
857 unEscapeStr(outAttributes);
858 unEscapeStr(outText);
859
860 if (!outText.isEmpty()) {
861 arg.name = outNodeName.c_str();
862 arg.value = outText;
863 reply.addArgument(arg);
864 }
865
866 DlnaLogger.log(DlnaLogLevel::Info, "Callback: '%s': %s (%s)",
867 StrView(outNodeName).c_str(),
868 StrView(outText).c_str(),
869 StrView(outAttributes).c_str());
870 if (result_callback) {
872 StrView(outNodeName ? outNodeName : "").c_str(),
873 StrView(outText ? outText : "").c_str(),
874 StrView(outAttributes ? outAttributes : "").c_str());
875 }
876 }
877 }
878 }
879 }
880 xml_parser.end();
881 }
882
883 void unEscapeStr(Str& str) {
884 str.replaceAll("&quot;", "\"");
885 str.replaceAll("&amp;", "&");
886 str.replaceAll("&apos;", "'");
887 }
888
889 const char* getUrlImpl(DLNADeviceInfo& device, const char* suffix,
890 const char* buffer, int len) {
891 DlnaLogger.log(DlnaLogLevel::Debug, "DLNAControlPointMgr::getUrl");
892 StrView url_str{(char*)buffer, len};
893 // start with base url if available, otherwise construct from device URL
894 const char* base = device.getBaseURL();
895 if (base != nullptr && *base != '\0') {
896 url_str = base;
897 } else {
898 url_str.add(device.getDeviceURL().protocol());
899 url_str.add("://");
900 url_str.add(device.getDeviceURL().host());
901 url_str.add(":");
902 url_str.add(device.getDeviceURL().port());
903 }
904
905 // Normalize slashes when appending suffix to avoid double '//'
906 if (suffix != nullptr && suffix[0] != '\0') {
907 int base_len = url_str.length();
908 const char* tmpc = url_str.c_str();
909 bool base_ends_slash =
910 tmpc != nullptr && base_len > 0 && tmpc[base_len - 1] == '/';
911 bool suf_starts_slash = suffix[0] == '/';
912 if (base_ends_slash && suf_starts_slash) {
913 // skip leading slash of suffix
914 url_str.add(suffix + 1);
915 } else if (!base_ends_slash && !suf_starts_slash) {
916 // ensure single slash between base and suffix
917 url_str.add('/');
918 url_str.add(suffix);
919 } else {
920 // exactly one slash present
921 url_str.add(suffix);
922 }
923 }
924 return buffer;
925 }
926};
927
928} // 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:531
void * reference
Definition: DLNAControlPoint.h:524
void setSubscribeNotificationsActive(bool flag) override
Activate/deactivate subscription notifications.
Definition: DLNAControlPoint.h:497
void setHttpServer(IHttpServer &server) override
Set HttpServer instance and register the notify handler.
Definition: DLNAControlPoint.h:118
void end() override
Stops the processing and releases the resources.
Definition: DLNAControlPoint.h:226
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:780
bool matches(const char *usn)
checks if the usn contains the search target
Definition: DLNAControlPoint.h:663
size_t createXML(ActionRequest &action)
Definition: DLNAControlPoint.h:702
static bool processMSearchReply(MSearchReplyCP &data)
Processes an M-SEARCH HTTP 200 reply and attempts to add the device.
Definition: DLNAControlPoint.h:634
int default_device_idx
Definition: DLNAControlPoint.h:516
static bool handleNotifyAlive(NotifyReplyCP &data)
Definition: DLNAControlPoint.h:554
bool addDevice(Url url) override
Adds the device from the device xml url if it does not already exist.
Definition: DLNAControlPoint.h:422
bool is_parse_device
Definition: DLNAControlPoint.h:519
Vector< DLNADeviceInfo > devices
Definition: DLNAControlPoint.h:512
const char * getUrlImpl(DLNADeviceInfo &device, const char *suffix, const char *buffer, int len)
Definition: DLNAControlPoint.h:889
Url local_url
Definition: DLNAControlPoint.h:521
bool addDevice(DLNADeviceInfo dev) override
Adds a new device.
Definition: DLNAControlPoint.h:406
bool allow_localhost
Definition: DLNAControlPoint.h:522
IHttpServer * p_http_server
Definition: DLNAControlPoint.h:523
Scheduler scheduler
Definition: DLNAControlPoint.h:508
~DLNAControlPoint() override=default
bool is_active
Definition: DLNAControlPoint.h:518
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:220
const char * registerString(const char *s) override
Register a string in the shared registry and return the stored pointer.
Definition: DLNAControlPoint.h:502
SubscriptionMgrControlPoint * getSubscriptionMgr() override
Provides the subscription manager.
Definition: DLNAControlPoint.h:492
DLNADeviceInfo NO_DEVICE
Definition: DLNAControlPoint.h:507
void parseResult()
Parses the XML response from the HTTP client and populates reply arguments.
Definition: DLNAControlPoint.h:837
const char * getUrl(DLNADeviceInfo &device, const char *suffix, char *buffer, int len) override
Definition: DLNAControlPoint.h:400
DLNAServiceInfo & getService(const char *id) override
Provide addess to the service information.
Definition: DLNAControlPoint.h:341
void setDeviceIndex(int idx) override
Selects the default device by index.
Definition: DLNAControlPoint.h:106
void setSearchRepeatMs(int repeatMs) override
Sets the repeat interval for M-SEARCH requests (define before begin)
Definition: DLNAControlPoint.h:98
int msearch_repeat_ms
Definition: DLNAControlPoint.h:517
void setActive(bool flag) override
We can activate/deactivate the scheduler.
Definition: DLNAControlPoint.h:480
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:109
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:787
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:513
bool isActive() override
Checks if the scheduler is active.
Definition: DLNAControlPoint.h:483
bool loop() override
Definition: DLNAControlPoint.h:298
static bool isUdnKnown(const char *usn_c, DLNADeviceInfo *&outDev)
Definition: DLNAControlPoint.h:538
ActionReply & getLastReply() override
Provides the last reply.
Definition: DLNAControlPoint.h:489
const char * search_target
Definition: DLNAControlPoint.h:520
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:352
static bool processDevice(NotifyReplyCP &data)
Definition: DLNAControlPoint.h:621
std::function< void(const char *nodeName, const char *text, const char *attributes)> result_callback
Definition: DLNAControlPoint.h:527
DLNADeviceInfo & getDevice(int idx) override
Provides the device information by index.
Definition: DLNAControlPoint.h:358
bool processBye(Str &usn)
processes a bye-bye message
Definition: DLNAControlPoint.h:669
DLNADeviceInfo & getDevice(Url location) override
Get a device for a Url.
Definition: DLNAControlPoint.h:379
ActionReply & postAction(ActionRequest &action, XMLCallback xmlProcessor=nullptr)
Definition: DLNAControlPoint.h:745
ActionReply reply
Definition: DLNAControlPoint.h:514
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:392
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
IHttpRequest * p_http
Definition: DLNAControlPoint.h:509
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:368
ActionReply & postAllActions(XMLCallback xmlProcessor=nullptr)
Definition: DLNAControlPoint.h:736
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:284
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:270
XMLPrinter xml_printer
Definition: DLNAControlPoint.h:515
bool isDiscoveryAllowed(Url &url)
Definition: DLNAControlPoint.h:587
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:103
SubscriptionMgrControlPoint subscription_mgr
Definition: DLNAControlPoint.h:511
void unEscapeStr(Str &str)
Definition: DLNAControlPoint.h:883
void setAllowLocalhost(bool flag) override
Defines if localhost devices should be allowed.
Definition: DLNAControlPoint.h:486
IUDPService * p_udp
Definition: DLNAControlPoint.h:510
Device Attributes and generation of XML using urn:schemas-upnp-org:device-1-0. We could just return a...
Definition: DLNADeviceInfo.h:25
Url device_url
Definition: DLNADeviceInfo.h:254
Str base_url
Definition: DLNADeviceInfo.h:258
const char * getUDN()
Provide the udn uuid.
Definition: DLNADeviceInfo.h:88
const char * getBaseURL()
Provides the base url.
Definition: DLNADeviceInfo.h:118
virtual bool begin()
Override to initialize the device.
Definition: DLNADeviceInfo.h:59
Url & getDeviceURL()
This method returns base url/device.xml.
Definition: DLNADeviceInfo.h:129
void setActive(bool flag)
Sets the server to inactive.
Definition: DLNADeviceInfo.h:237
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.
Abstract interface for HTTP server functionality.
Definition: IHttpServer.h:30
virtual bool begin()=0
Start the HTTP server.
virtual bool doLoop()=0
Process server loop.
virtual void end()=0
Stop the HTTP server.
virtual bool isActive()=0
Check if server is active.
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:148
Str usn
Definition: Schedule.h:152
Str location
Definition: Schedule.h:151
Send MSearch request.
Definition: Schedule.h:46
Represents a notification/notify reply scheduled for control-point processing.
Definition: Schedule.h:189
Str nts
Definition: Schedule.h:192
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:84
void setup(IHttpRequest &http, IUDPService &udp, Url &localCallbackUrl, DLNADeviceInfo &device)
Inject required dependencies and initialize the manager.
Definition: SubscriptionMgrControlPoint.h:62
void setEventSubscriptionActive(bool active)
Turn automatic event subscription on or off.
Definition: SubscriptionMgrControlPoint.h:172
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_MAX_URL_LEN
app-wide max URL length
Definition: dlna_config.h:45
#define XML_PARSER_BUFFER_SIZE
Define XML parse buffer size.
Definition: dlna_config.h:25
#define DLNA_HTTP_REQUEST_TIMEOUT_MS
Define the default http request timeout.
Definition: dlna_config.h:20
#define DLNA_DISCOVERY_NETMASK
Define the netmask for discovery filtering.
Definition: dlna_config.h:187
Definition: Allocator.h:13
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