1 /*
2  * Copyright 2003-2021 The Music Player Daemon Project
3  * http://www.musicpd.org
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.	 See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License along
16  * with this program; if not, write to the Free Software Foundation, Inc.,
17  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18  */
19 
20 #include "UpnpDatabasePlugin.hxx"
21 #include "Directory.hxx"
22 #include "Tags.hxx"
23 #include "lib/upnp/ClientInit.hxx"
24 #include "lib/upnp/Discovery.hxx"
25 #include "lib/upnp/ContentDirectoryService.hxx"
26 #include "db/Interface.hxx"
27 #include "db/DatabasePlugin.hxx"
28 #include "db/Selection.hxx"
29 #include "db/VHelper.hxx"
30 #include "db/UniqueTags.hxx"
31 #include "db/DatabaseError.hxx"
32 #include "db/LightDirectory.hxx"
33 #include "song/LightSong.hxx"
34 #include "song/Filter.hxx"
35 #include "song/TagSongFilter.hxx"
36 #include "db/Stats.hxx"
37 #include "tag/Table.hxx"
38 #include "fs/Traits.hxx"
39 #include "util/ConstBuffer.hxx"
40 #include "util/RecursiveMap.hxx"
41 #include "util/SplitString.hxx"
42 #include "config/Block.hxx"
43 
44 #include <cassert>
45 #include <string>
46 #include <utility>
47 
48 #include <string.h>
49 
50 static const char *const rootid = "0";
51 
52 class UpnpSongData {
53 protected:
54 	std::string uri;
55 	Tag tag;
56 
57 	template<typename U, typename T>
UpnpSongData(U && _uri,T && _tag)58 	UpnpSongData(U &&_uri, T &&_tag) noexcept
59 		:uri(std::forward<U>(_uri)), tag(std::forward<T>(_tag)) {}
60 };
61 
62 class UpnpSong : UpnpSongData, public LightSong {
63 	std::string real_uri2;
64 
65 public:
66 	template<typename U>
UpnpSong(UPnPDirObject && object,U && _uri)67 	UpnpSong(UPnPDirObject &&object, U &&_uri) noexcept
68 		:UpnpSongData(std::forward<U>(_uri), std::move(object.tag)),
69 		 LightSong(UpnpSongData::uri.c_str(), UpnpSongData::tag),
70 		 real_uri2(std::move(object.url)) {
71 		real_uri = real_uri2.c_str();
72 	}
73 };
74 
75 class UpnpDatabase : public Database {
76 	EventLoop &event_loop;
77 	UpnpClient_Handle handle;
78 	UPnPDeviceDirectory *discovery;
79 
80 	const char* interface;
81 
82 public:
UpnpDatabase(EventLoop & _event_loop,const ConfigBlock & block)83 	explicit UpnpDatabase(EventLoop &_event_loop, const ConfigBlock &block) noexcept
84 		:Database(upnp_db_plugin),
85 		 event_loop(_event_loop),
86 		 interface(block.GetBlockValue("interface", nullptr)) {}
87 
88 	static DatabasePtr Create(EventLoop &main_event_loop,
89 				  EventLoop &io_event_loop,
90 				  DatabaseListener &listener,
91 				  const ConfigBlock &block) noexcept;
92 
93 	void Open() override;
94 	void Close() noexcept override;
95 	[[nodiscard]] const LightSong *GetSong(std::string_view uri_utf8) const override;
96 	void ReturnSong(const LightSong *song) const noexcept override;
97 
98 	void Visit(const DatabaseSelection &selection,
99 		   VisitDirectory visit_directory,
100 		   VisitSong visit_song,
101 		   VisitPlaylist visit_playlist) const override;
102 
103 	[[nodiscard]] RecursiveMap<std::string> CollectUniqueTags(const DatabaseSelection &selection,
104 						    ConstBuffer<TagType> tag_types) const override;
105 
106 	[[nodiscard]] DatabaseStats GetStats(const DatabaseSelection &selection) const override;
107 
GetUpdateStamp() const108 	[[nodiscard]] std::chrono::system_clock::time_point GetUpdateStamp() const noexcept override {
109 		return std::chrono::system_clock::time_point::min();
110 	}
111 
112 private:
113 	void VisitServer(const ContentDirectoryService &server,
114 			 std::forward_list<std::string_view> &&vpath,
115 			 const DatabaseSelection &selection,
116 			 const VisitDirectory& visit_directory,
117 			 const VisitSong& visit_song,
118 			 const VisitPlaylist& visit_playlist) const;
119 
120 	/**
121 	 * Run an UPnP search according to MPD parameters, and
122 	 * visit_song the results.
123 	 */
124 	void SearchSongs(const ContentDirectoryService &server,
125 			 const char *objid,
126 			 const DatabaseSelection &selection,
127 			 const VisitSong& visit_song) const;
128 
129 	UPnPDirContent SearchSongs(const ContentDirectoryService &server,
130 				   const char *objid,
131 				   const DatabaseSelection &selection) const;
132 
133 	UPnPDirObject Namei(const ContentDirectoryService &server,
134 			    std::forward_list<std::string_view> &&vpath) const;
135 
136 	/**
137 	 * Take server and objid, return metadata.
138 	 */
139 	UPnPDirObject ReadNode(const ContentDirectoryService &server,
140 			       const char *objid) const;
141 
142 	/**
143 	 * Get the path for an object Id. This works much like pwd,
144 	 * except easier cause our inodes have a parent id. Not used
145 	 * any more actually (see comments in SearchSongs).
146 	 */
147 	[[nodiscard]] std::string BuildPath(const ContentDirectoryService &server,
148 			      const UPnPDirObject& dirent) const;
149 };
150 
151 DatabasePtr
Create(EventLoop &,EventLoop & io_event_loop,DatabaseListener & listener,const ConfigBlock & block)152 UpnpDatabase::Create(EventLoop &, EventLoop &io_event_loop,
153 		     [[maybe_unused]] DatabaseListener &listener,
154 		     const ConfigBlock &block) noexcept
155 {
156 	return std::make_unique<UpnpDatabase>(io_event_loop, block);;
157 }
158 
159 void
Open()160 UpnpDatabase::Open()
161 {
162 	handle = UpnpClientGlobalInit(interface);
163 
164 	discovery = new UPnPDeviceDirectory(event_loop, handle);
165 	try {
166 		discovery->Start();
167 	} catch (...) {
168 		delete discovery;
169 		UpnpClientGlobalFinish();
170 		throw;
171 	}
172 }
173 
174 void
Close()175 UpnpDatabase::Close() noexcept
176 {
177 	delete discovery;
178 	UpnpClientGlobalFinish();
179 }
180 
181 void
ReturnSong(const LightSong * _song) const182 UpnpDatabase::ReturnSong(const LightSong *_song) const noexcept
183 {
184 	assert(_song != nullptr);
185 
186 	auto *song = (UpnpSong *)const_cast<LightSong *>(_song);
187 	delete song;
188 }
189 
190 // Get song info by path. We can receive either the id path, or the titles
191 // one
192 const LightSong *
GetSong(std::string_view uri) const193 UpnpDatabase::GetSong(std::string_view uri) const
194 {
195 	auto vpath = SplitString(uri, '/');
196 	if (vpath.empty())
197 		throw DatabaseError(DatabaseErrorCode::NOT_FOUND,
198 				    "No such song");
199 
200 	auto server = discovery->GetServer(vpath.front());
201 	vpath.pop_front();
202 
203 	if (vpath.empty())
204 		throw DatabaseError(DatabaseErrorCode::NOT_FOUND,
205 				    "No such song");
206 
207 	UPnPDirObject dirent;
208 	if (vpath.front() != rootid) {
209 		dirent = Namei(server, std::move(vpath));
210 	} else {
211 		vpath.pop_front();
212 		if (vpath.empty())
213 			throw DatabaseError(DatabaseErrorCode::NOT_FOUND,
214 					    "No such song");
215 
216 		dirent = ReadNode(server, std::string(vpath.front()).c_str());
217 	}
218 
219 	return new UpnpSong(std::move(dirent), uri);
220 }
221 
222 /**
223  * Double-quote a string, adding internal backslash escaping.
224  */
225 static void
dquote(std::string & out,const char * in)226 dquote(std::string &out, const char *in) noexcept
227 {
228 	out.push_back('"');
229 
230 	for (; *in != 0; ++in) {
231 		switch(*in) {
232 		case '\\':
233 		case '"':
234 			out.push_back('\\');
235 			break;
236 		}
237 
238 		out.push_back(*in);
239 	}
240 
241 	out.push_back('"');
242 }
243 
244 // Run an UPnP search, according to MPD parameters. Return results as
245 // UPnP items
246 UPnPDirContent
SearchSongs(const ContentDirectoryService & server,const char * objid,const DatabaseSelection & selection) const247 UpnpDatabase::SearchSongs(const ContentDirectoryService &server,
248 			  const char *objid,
249 			  const DatabaseSelection &selection) const
250 {
251 	const SongFilter *filter = selection.filter;
252 	if (selection.filter == nullptr)
253 		return {};
254 
255 	const auto searchcaps = server.getSearchCapabilities(handle);
256 	if (searchcaps.empty())
257 		return {};
258 
259 	std::string cond;
260 	for (const auto &item : filter->GetItems()) {
261 		if (auto t = dynamic_cast<const TagSongFilter *>(item.get())) {
262 			auto tag = t->GetTagType();
263 			if (tag == TAG_NUM_OF_ITEM_TYPES) {
264 				if (!cond.empty()) {
265 					cond += " and ";
266 				}
267 				cond += '(';
268 				bool first(true);
269 				for (const auto& cap : searchcaps) {
270 					if (first)
271 						first = false;
272 					else
273 						cond += " or ";
274 					cond += cap;
275 					if (t->GetFoldCase()) {
276 						cond += " contains ";
277 					} else {
278 						cond += " = ";
279 					}
280 					dquote(cond, t->GetValue().c_str());
281 				}
282 				cond += ')';
283 				continue;
284 			}
285 
286 			if (tag == TAG_ALBUM_ARTIST)
287 				tag = TAG_ARTIST;
288 
289 			const char *name = tag_table_lookup(upnp_tags, tag);
290 			if (name == nullptr)
291 				continue;
292 
293 			if (!cond.empty()) {
294 				cond += " and ";
295 			}
296 			cond += name;
297 
298 			/* FoldCase doubles up as contains/equal
299 			   switch. UpNP search is supposed to be
300 			   case-insensitive, but at least some servers
301 			   have the same convention as mpd (e.g.:
302 			   minidlna) */
303 			if (t->GetFoldCase()) {
304 				cond += " contains ";
305 			} else {
306 				cond += " = ";
307 			}
308 			dquote(cond, t->GetValue().c_str());
309 		}
310 
311 		// TODO: support other ISongFilter implementations
312 	}
313 
314 	return server.search(handle, objid, cond.c_str());
315 }
316 
317 static void
visitSong(const UPnPDirObject & meta,const char * path,const DatabaseSelection & selection,const VisitSong & visit_song)318 visitSong(const UPnPDirObject &meta, const char *path,
319 	  const DatabaseSelection &selection,
320 	  const VisitSong& visit_song)
321 {
322 	if (!visit_song)
323 		return;
324 
325 	LightSong song(path, meta.tag);
326 	song.real_uri = meta.url.c_str();
327 
328 	if (selection.Match(song))
329 		visit_song(song);
330 }
331 
332 /**
333  * Build synthetic path based on object id for search results. The use
334  * of "rootid" is arbitrary, any name that is not likely to be a top
335  * directory name would fit.
336  */
337 static std::string
songPath(const std::string & servername,const std::string & objid)338 songPath(const std::string &servername,
339 	 const std::string &objid) noexcept
340 {
341 	return servername + "/" + rootid + "/" + objid;
342 }
343 
344 void
SearchSongs(const ContentDirectoryService & server,const char * objid,const DatabaseSelection & selection,const VisitSong & visit_song) const345 UpnpDatabase::SearchSongs(const ContentDirectoryService &server,
346 			  const char *objid,
347 			  const DatabaseSelection &selection,
348 			  const VisitSong& visit_song) const
349 {
350 	if (!visit_song)
351 		return;
352 
353 	const auto content = SearchSongs(server, objid, selection);
354 	for (auto &dirent : content.objects) {
355 		if (dirent.type != UPnPDirObject::Type::ITEM ||
356 		    dirent.item_class != UPnPDirObject::ItemClass::MUSIC)
357 			continue;
358 
359 		// We get song ids as the result of the UPnP search. But our
360 		// client expects paths (e.g. we get 1$4$3788 from minidlna,
361 		// but we need to translate to /Music/All_Music/Satisfaction).
362 		// We can do this in two ways:
363 		//  - Rebuild a normal path using BuildPath() which is a kind of pwd
364 		//  - Build a bogus path based on the song id.
365 		// The first method is nice because the returned paths are pretty, but
366 		// it has two big problems:
367 		//  - The song paths are ambiguous: e.g. minidlna returns all search
368 		//    results as being from the "All Music" directory, which can
369 		//    contain several songs with the same title (but different objids)
370 		//  - The performance of BuildPath() is atrocious on very big
371 		//    directories, even causing timeouts in clients. And of
372 		//    course, 'All Music' is very big.
373 		// So we return synthetic and ugly paths based on the object id,
374 		// which we later have to detect.
375 		const std::string path = songPath(server.getFriendlyName(),
376 						  dirent.id);
377 		visitSong(dirent, path.c_str(),
378 			  selection, visit_song);
379 	}
380 }
381 
382 UPnPDirObject
ReadNode(const ContentDirectoryService & server,const char * objid) const383 UpnpDatabase::ReadNode(const ContentDirectoryService &server,
384 		       const char *objid) const
385 {
386 	auto dirbuf = server.getMetadata(handle, objid);
387 	if (dirbuf.objects.size() != 1)
388 		throw std::runtime_error("Bad resource");
389 
390 	return std::move(dirbuf.objects.front());
391 }
392 
393 std::string
BuildPath(const ContentDirectoryService & server,const UPnPDirObject & idirent) const394 UpnpDatabase::BuildPath(const ContentDirectoryService &server,
395 			const UPnPDirObject& idirent) const
396 {
397 	const char *pid = idirent.id.c_str();
398 	std::string path;
399 	while (strcmp(pid, rootid) != 0) {
400 		auto dirent = ReadNode(server, pid);
401 		pid = dirent.parent_id.c_str();
402 
403 		if (path.empty())
404 			path = dirent.name;
405 		else
406 			path = PathTraitsUTF8::Build(dirent.name, path);
407 	}
408 
409 	return PathTraitsUTF8::Build(server.getFriendlyName(),
410 				     path.c_str());
411 }
412 
413 // Take server and internal title pathname and return objid and metadata.
414 UPnPDirObject
Namei(const ContentDirectoryService & server,std::forward_list<std::string_view> && vpath) const415 UpnpDatabase::Namei(const ContentDirectoryService &server,
416 		    std::forward_list<std::string_view> &&vpath) const
417 {
418 	if (vpath.empty())
419 		// looking for root info
420 		return ReadNode(server, rootid);
421 
422 	std::string objid(rootid);
423 
424 	// Walk the path elements, read each directory and try to find the next one
425 	while (true) {
426 		auto dirbuf = server.readDir(handle, objid.c_str());
427 
428 		// Look for the name in the sub-container list
429 		UPnPDirObject *child = dirbuf.FindObject(vpath.front());
430 		if (child == nullptr)
431 			throw DatabaseError(DatabaseErrorCode::NOT_FOUND,
432 					    "No such object");
433 
434 		vpath.pop_front();
435 		if (vpath.empty())
436 			return std::move(*child);
437 
438 		if (child->type != UPnPDirObject::Type::CONTAINER)
439 			throw DatabaseError(DatabaseErrorCode::NOT_FOUND,
440 					    "Not a container");
441 
442 		objid = std::move(child->id);
443 	}
444 }
445 
446 static void
VisitItem(const UPnPDirObject & object,const char * uri,const DatabaseSelection & selection,const VisitSong & visit_song,const VisitPlaylist & visit_playlist)447 VisitItem(const UPnPDirObject &object, const char *uri,
448 	  const DatabaseSelection &selection,
449 	  const VisitSong& visit_song, const VisitPlaylist& visit_playlist)
450 {
451 	assert(object.type == UPnPDirObject::Type::ITEM);
452 
453 	switch (object.item_class) {
454 	case UPnPDirObject::ItemClass::MUSIC:
455 		visitSong(object, uri, selection, visit_song);
456 		break;
457 
458 	case UPnPDirObject::ItemClass::PLAYLIST:
459 		if (visit_playlist) {
460 			/* Note: I've yet to see a
461 			   playlist item (playlists
462 			   seem to be usually handled
463 			   as containers, so I'll
464 			   decide what to do when I
465 			   see one... */
466 		}
467 
468 		break;
469 
470 	case UPnPDirObject::ItemClass::UNKNOWN:
471 		break;
472 	}
473 }
474 
475 static void
VisitObject(const UPnPDirObject & object,const char * uri,const DatabaseSelection & selection,const VisitDirectory & visit_directory,const VisitSong & visit_song,const VisitPlaylist & visit_playlist)476 VisitObject(const UPnPDirObject &object, const char *uri,
477 	    const DatabaseSelection &selection,
478 	    const VisitDirectory& visit_directory,
479 	    const VisitSong& visit_song,
480 	    const VisitPlaylist& visit_playlist)
481 {
482 	switch (object.type) {
483 	case UPnPDirObject::Type::UNKNOWN:
484 		assert(false);
485 		gcc_unreachable();
486 
487 	case UPnPDirObject::Type::CONTAINER:
488 		if (visit_directory)
489 			visit_directory(LightDirectory(uri,
490 						       std::chrono::system_clock::time_point::min()));
491 		break;
492 
493 	case UPnPDirObject::Type::ITEM:
494 		VisitItem(object, uri, selection,
495 			  visit_song, visit_playlist);
496 		break;
497 	}
498 }
499 
500 // vpath is a parsed and writeable version of selection.uri. There is
501 // really just one path parameter.
502 void
VisitServer(const ContentDirectoryService & server,std::forward_list<std::string_view> && vpath,const DatabaseSelection & selection,const VisitDirectory & visit_directory,const VisitSong & visit_song,const VisitPlaylist & visit_playlist) const503 UpnpDatabase::VisitServer(const ContentDirectoryService &server,
504 			  std::forward_list<std::string_view> &&vpath,
505 			  const DatabaseSelection &selection,
506 			  const VisitDirectory& visit_directory,
507 			  const VisitSong& visit_song,
508 			  const VisitPlaylist& visit_playlist) const
509 {
510 	/* If the path begins with rootid, we know that this is a
511 	   song, not a directory (because that's how we set things
512 	   up). Just visit it. Note that the choice of rootid is
513 	   arbitrary, any value not likely to be the name of a top
514 	   directory would be ok. */
515 	/* !Note: this *can't* be handled by Namei further down,
516 	   because the path is not valid for traversal. Besides, it's
517 	   just faster to access the target node directly */
518 	if (!vpath.empty() && vpath.front() == rootid) {
519 		vpath.pop_front();
520 		if (vpath.empty())
521 			return;
522 
523 		const std::string objid(vpath.front());
524 		vpath.pop_front();
525 		if (!vpath.empty())
526 			throw DatabaseError(DatabaseErrorCode::NOT_FOUND,
527 					    "Not found");
528 
529 		if (visit_song) {
530 			auto dirent = ReadNode(server, objid.c_str());
531 
532 			if (dirent.type != UPnPDirObject::Type::ITEM ||
533 			    dirent.item_class != UPnPDirObject::ItemClass::MUSIC)
534 				throw DatabaseError(DatabaseErrorCode::NOT_FOUND,
535 						    "Not found");
536 
537 			std::string path = songPath(server.getFriendlyName(),
538 						    dirent.id);
539 			visitSong(dirent, path.c_str(),
540 				  selection, visit_song);
541 		}
542 
543 		return;
544 	}
545 
546 	// Translate the target path into an object id and the associated metadata.
547 	const auto tdirent = Namei(server, std::move(vpath));
548 
549 	/* If recursive is set, this is a search... No use sending it
550 	   if the filter is empty. In this case, we implement limited
551 	   recursion (1-deep) here, which will handle the "add dir"
552 	   case. */
553 	if (selection.recursive && selection.filter) {
554 		SearchSongs(server, tdirent.id.c_str(), selection, visit_song);
555 		return;
556 	}
557 
558 	const char *const base_uri = selection.uri.empty()
559 		? server.getFriendlyName()
560 		: selection.uri.c_str();
561 
562 	if (tdirent.type == UPnPDirObject::Type::ITEM) {
563 		VisitItem(tdirent, base_uri,
564 			  selection,
565 			  visit_song, visit_playlist);
566 		return;
567 	}
568 
569 	/* Target was a a container. Visit it. We could read slices
570 	   and loop here, but it's not useful as mpd will only return
571 	   data to the client when we're done anyway. */
572 	const auto contents = server.readDir(handle, tdirent.id.c_str());
573 	for (const auto &dirent : contents.objects) {
574 		const std::string uri = PathTraitsUTF8::Build(base_uri,
575 							      dirent.name.c_str());
576 		VisitObject(dirent, uri.c_str(),
577 			    selection,
578 			    visit_directory,
579 			    visit_song, visit_playlist);
580 	}
581 }
582 
583 gcc_const
584 static DatabaseSelection
CheckSelection(DatabaseSelection selection)585 CheckSelection(DatabaseSelection selection) noexcept
586 {
587 	selection.uri.clear();
588 	selection.filter = nullptr;
589 	return selection;
590 }
591 
592 // Deal with the possibly multiple servers, call VisitServer if needed.
593 void
Visit(const DatabaseSelection & selection,VisitDirectory visit_directory,VisitSong visit_song,VisitPlaylist visit_playlist) const594 UpnpDatabase::Visit(const DatabaseSelection &selection,
595 		    VisitDirectory visit_directory,
596 		    VisitSong visit_song,
597 		    VisitPlaylist visit_playlist) const
598 {
599 	DatabaseVisitorHelper helper(CheckSelection(selection), visit_song);
600 
601 	auto vpath = SplitString(selection.uri, '/');
602 	if (vpath.empty()) {
603 		for (const auto &server : discovery->GetDirectories()) {
604 			if (visit_directory) {
605 				const LightDirectory d(server.getFriendlyName(),
606 						       std::chrono::system_clock::time_point::min());
607 				visit_directory(d);
608 			}
609 
610 			if (selection.recursive)
611 				VisitServer(server, std::move(vpath), selection,
612 					    visit_directory, visit_song,
613 					    visit_playlist);
614 		}
615 
616 		helper.Commit();
617 		return;
618 	}
619 
620 	// We do have a path: the first element selects the server
621 	std::string servername(vpath.front());
622 	vpath.pop_front();
623 
624 	auto server = discovery->GetServer(servername.c_str());
625 	VisitServer(server, std::move(vpath), selection,
626 		    visit_directory, visit_song, visit_playlist);
627 	helper.Commit();
628 }
629 
630 RecursiveMap<std::string>
CollectUniqueTags(const DatabaseSelection & selection,ConstBuffer<TagType> tag_types) const631 UpnpDatabase::CollectUniqueTags(const DatabaseSelection &selection,
632 				ConstBuffer<TagType> tag_types) const
633 {
634 	return ::CollectUniqueTags(*this, selection, tag_types);
635 }
636 
637 DatabaseStats
GetStats(const DatabaseSelection &) const638 UpnpDatabase::GetStats(const DatabaseSelection &) const
639 {
640 	/* Note: this gets called before the daemonizing so we can't
641 	   reallyopen this would be a problem if we had real stats */
642 	DatabaseStats stats;
643 	stats.Clear();
644 	return stats;
645 }
646 
647 const DatabasePlugin upnp_db_plugin = {
648 	"upnp",
649 	0,
650 	UpnpDatabase::Create,
651 };
652