1 /*
2  * Cantata
3  *
4  * Copyright (c) 2011-2020 Craig Drummond <craig.p.drummond@gmail.com>
5  *
6  * ----
7  *
8  * This program is free software; you can redistribute it and/or modify
9  * it under the terms of the GNU General Public License as published by
10  * the Free Software Foundation; either version 2 of the License, or
11  * (at your option) any later version.
12  *
13  * This program is distributed in the hope that it will be useful,
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
16  * General Public License for more details.
17  *
18  * You should have received a copy of the GNU General Public License
19  * along with this program; see the file COPYING.  If not, write to
20  * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
21  * Boston, MA 02110-1301, USA.
22  */
23 
24 #include "httpserver.h"
25 #include "httpsocket.h"
26 #ifdef TAGLIB_FOUND
27 #include "tags/tags.h"
28 #endif
29 #include "gui/settings.h"
30 #include "support/thread.h"
31 #include "support/globalstatic.h"
32 #include "mpd-interface/mpdconnection.h"
33 #include <QFile>
34 #include <QUrl>
35 #include <QTimer>
36 #include <QUrlQuery>
37 
38 #include <QDebug>
39 static bool debugIsEnabled=false;
40 #define DBUG if (debugIsEnabled) qWarning() << "HttpServer" << __FUNCTION__
41 
42 #ifdef Q_OS_WIN
fixWindowsPath(const QString & f)43 static inline QString fixWindowsPath(const QString &f)
44 {
45     return f.length()>3 && f.startsWith(QLatin1Char('/')) && QLatin1Char(':')==f.at(2) ? f.mid(1) : f;
46 }
47 #endif
48 
enableDebug()49 void HttpServer::enableDebug()
50 {
51     debugIsEnabled=true;
52 }
53 
debugEnabled()54 bool HttpServer::debugEnabled()
55 {
56     return debugIsEnabled;
57 }
58 
GLOBAL_STATIC(HttpServer,instance)59 GLOBAL_STATIC(HttpServer, instance)
60 
61 #ifdef ENABLE_HTTP_SERVER
62 HttpServer::HttpServer()
63     : QObject(nullptr)
64     , thread(nullptr)
65     , socket(nullptr)
66     , closeTimer(nullptr)
67 {
68     connect(MPDConnection::self(), SIGNAL(cantataStreams(QList<Song>,bool)), this, SLOT(cantataStreams(QList<Song>,bool)));
69     connect(MPDConnection::self(), SIGNAL(cantataStreams(QStringList)), this, SLOT(cantataStreams(QStringList)));
70     connect(MPDConnection::self(), SIGNAL(removedIds(QSet<qint32>)), this, SLOT(removedIds(QSet<qint32>)));
71     connect(MPDConnection::self(), SIGNAL(ifaceIp(QString)), this, SLOT(ifaceIp(QString)));
72 }
73 
isAlive() const74 bool HttpServer::isAlive() const
75 {
76     // started on demand, but only start if allowed
77     return MPDConnection::self()->getDetails().allowLocalStreaming;
78 }
79 
start()80 bool HttpServer::start()
81 {
82     if (closeTimer) {
83         DBUG << "stop close timer";
84         closeTimer->stop();
85     }
86 
87     if (socket) {
88         DBUG << "already open";
89         return true;
90     }
91 
92     DBUG << "open new socket";
93     quint16 prevPort=Settings::self()->httpAllocatedPort();
94     bool newThread=nullptr==thread;
95     if (newThread) {
96         thread=new Thread("HttpServer");
97     }
98     socket=new HttpSocket(Settings::self()->httpInterface(), prevPort);
99     socket->mpdAddress(MPDConnection::self()->ipAddress());
100     connect(this, SIGNAL(terminateSocket()), socket, SLOT(terminate()), Qt::QueuedConnection);
101     if (socket->serverPort()!=prevPort) {
102         Settings::self()->saveHttpAllocatedPort(socket->serverPort());
103     }
104     socket->moveToThread(thread);
105     bool started=socket->isListening();
106     if (newThread) {
107         thread->start();
108     }
109     return started;
110 }
111 
stop()112 void HttpServer::stop()
113 {
114     if (socket) {
115         DBUG;
116         emit terminateSocket();
117         socket=nullptr;
118     }
119 }
120 
readConfig()121 void HttpServer::readConfig()
122 {
123     QString iface=Settings::self()->httpInterface();
124 
125     if (socket && socket->isListening() && iface==socket->configuredInterface()) {
126         return;
127     }
128 
129     bool wasStarted=nullptr!=socket;
130     stop();
131     if (wasStarted) {
132         start();
133     }
134 }
135 
serverUrl(const QString & ip,quint16 port)136 static inline QString serverUrl(const QString &ip, quint16 port)
137 {
138     return QLatin1String("http://")+ip+QLatin1Char(':')+QString::number(port);
139 }
140 
address() const141 QString HttpServer::address() const
142 {
143     return socket ? serverUrl(currentIfaceIp, socket->serverPort()) : QLatin1String("http://127.0.0.1:*");
144 }
145 
isOurs(const QString & url) const146 bool HttpServer::isOurs(const QString &url) const
147 {
148     if (nullptr==socket || !url.startsWith(QLatin1String("http://"))) {
149         return false;
150     }
151 
152     for (const QString &ip: ipAddresses) {
153         if (url.startsWith(serverUrl(ip, socket->serverPort()))) {
154             return true;
155         }
156     }
157 
158     return false;
159 }
160 
encodeUrl(const Song & s)161 QByteArray HttpServer::encodeUrl(const Song &s)
162 {
163     DBUG << "song" << s.file << isAlive();
164     if (!start()) {
165         return QByteArray();
166     }
167     QUrl url;
168     QUrlQuery query;
169     url.setScheme("http");
170     url.setHost(currentIfaceIp);
171     url.setPort(socket->serverPort());
172     #ifdef Q_OS_WIN
173     // Use a query item, as s.file might have a driver specifier
174     query.addQueryItem("file", s.file);
175     url.setPath("/"+Utils::getFile(s.file));
176     #else
177     url.setPath(s.file);
178     #endif
179     if (!s.album.isEmpty()) {
180         query.addQueryItem("album", s.album);
181     }
182     if (!s.artist.isEmpty()) {
183         query.addQueryItem("artist", s.artist);
184     }
185     if (!s.albumartist.isEmpty()) {
186         query.addQueryItem("albumartist", s.albumartist);
187     }
188     if (!s.composer().isEmpty()) {
189         query.addQueryItem("composer", s.composer());
190     }
191     if (!s.title.isEmpty()) {
192         query.addQueryItem("title", s.title);
193     }
194     if (!s.genres[0].isEmpty()) {
195         query.addQueryItem("genre", s.firstGenre());
196     }
197     if (s.disc) {
198         query.addQueryItem("disc", QString::number(s.disc));
199     }
200     if (s.year) {
201         query.addQueryItem("year", QString::number(s.year));
202     }
203     if (s.time) {
204         query.addQueryItem("time", QString::number(s.time));
205     }
206     if (s.track) {
207         query.addQueryItem("track", QString::number(s.track));
208     }
209     if (s.isFromOnlineService()) {
210         query.addQueryItem("onlineservice", s.onlineService());
211     }
212     query.addQueryItem("id", QString::number(s.id));
213     query.addQueryItem("cantata", "song");
214     url.setQuery(query);
215     DBUG << "encoded as" << url.toString();
216     return url.toEncoded();
217 }
218 
encodeUrl(const QString & file)219 QByteArray HttpServer::encodeUrl(const QString &file)
220 {
221     Song s;
222     #ifdef Q_OS_WIN
223     QString f=fixWindowsPath(file);
224     DBUG << "file" << f << "orig" << file;
225     // For some reason, drag'n' drop of \\share\path\file.mp3 is changed to share/path/file.mp3!
226     if (!f.startsWith(QLatin1String("//")) && !QFile::exists(f)) {
227         QString share=f.startsWith(QLatin1Char('/')) ? (QLatin1Char('/')+f) : (QLatin1String("//")+f);
228         if (QFile::exists(share)) {
229             f=share;
230             DBUG << "converted to share-path" << f;
231         }
232     }
233     #ifdef TAGLIB_FOUND
234     s=Tags::read(f);
235     #endif
236     s.file=f;
237     #else
238     DBUG << "file" << file;
239     #ifdef TAGLIB_FOUND
240     s=Tags::read(file);
241     #endif
242     s.file=file;
243     #endif
244     return /*s.isEmpty() ? QByteArray() :*/ encodeUrl(s);
245 }
246 
decodeUrl(const QString & url) const247 Song HttpServer::decodeUrl(const QString &url) const
248 {
249     return decodeUrl(QUrl(url));
250 }
251 
decodeUrl(const QUrl & url) const252 Song HttpServer::decodeUrl(const QUrl &url) const
253 {
254     Song s;
255     QUrlQuery q(url);
256 
257     if (q.hasQueryItem("cantata") && q.queryItemValue("cantata")=="song") {
258         if (q.hasQueryItem("album")) {
259             s.album=q.queryItemValue("album");
260         }
261         if (q.hasQueryItem("artist")) {
262             s.artist=q.queryItemValue("artist");
263         }
264         if (q.hasQueryItem("albumartist")) {
265             s.albumartist=q.queryItemValue("albumartist");
266         }
267         if (q.hasQueryItem("composer")) {
268             s.setComposer(q.queryItemValue("composer"));
269         }
270         if (q.hasQueryItem("title")) {
271             s.title=q.queryItemValue("title");
272         }
273         if (q.hasQueryItem("genre")) {
274             s.addGenre(q.queryItemValue("genre"));
275         }
276         if (q.hasQueryItem("disc")) {
277             s.disc=q.queryItemValue("disc").toInt();
278         }
279         if (q.hasQueryItem("year")) {
280             s.year=q.queryItemValue("year").toInt();
281         }
282         if (q.hasQueryItem("time")) {
283             s.time=q.queryItemValue("time").toInt();
284         }
285         if (q.hasQueryItem("track")) {
286             s.track=q.queryItemValue("track").toInt();
287         }
288         if (q.hasQueryItem("id")) {
289             s.id=q.queryItemValue("id").toInt();
290         }
291         if (q.hasQueryItem("onlineservice")) {
292             s.setIsFromOnlineService(q.queryItemValue("onlineservice"));
293         }
294         #ifdef Q_OS_WIN
295         s.file=fixWindowsPath(q.queryItemValue("file"));
296         #else
297         s.file=url.path();
298         #endif
299         s.type=Song::CantataStream;
300         #if defined CDDB_FOUND || defined MUSICBRAINZ5_FOUND
301         if (s.file.startsWith(Song::constCddaProtocol)) {
302             s.type=Song::Cdda;
303         }
304         #endif
305         DBUG << s.file << s.albumArtist() << s.album << s.title;
306     }
307 
308     return s;
309 }
310 
startCloseTimer()311 void HttpServer::startCloseTimer()
312 {
313     if (!closeTimer) {
314         closeTimer=new QTimer(this);
315         closeTimer->setSingleShot(true);
316         connect(closeTimer, SIGNAL(timeout()), this, SLOT(stop()));
317     }
318     DBUG;
319     closeTimer->start(1000);
320 }
321 
cantataStreams(const QStringList & files)322 void HttpServer::cantataStreams(const QStringList &files)
323 {
324     DBUG << files;
325     for (const QString &f: files) {
326         Song s=HttpServer::self()->decodeUrl(f);
327         if (s.isCantataStream() || s.isCdda()) {
328             start();
329             break;
330         }
331     }
332 }
333 
cantataStreams(const QList<Song> & songs,bool isUpdate)334 void HttpServer::cantataStreams(const QList<Song> &songs, bool isUpdate)
335 {
336     DBUG << isUpdate << songs.count();
337     if (!isUpdate) {
338         streamIds.clear();
339     }
340 
341     for (const Song &s: songs) {
342         streamIds.insert(s.id);
343     }
344 
345     if (streamIds.isEmpty()) {
346         startCloseTimer();
347     } else {
348         start();
349     }
350 }
351 
removedIds(const QSet<qint32> & ids)352 void HttpServer::removedIds(const QSet<qint32> &ids)
353 {
354     streamIds+=ids;
355     if (streamIds.isEmpty()) {
356         startCloseTimer();
357     }
358 }
359 
ifaceIp(const QString & ip)360 void HttpServer::ifaceIp(const QString &ip)
361 {
362     DBUG << "MPD interface ip" << ip;
363     if (ip.isEmpty()) {
364         return;
365     }
366     currentIfaceIp=ip;
367     ipAddresses.insert(ip);
368 }
369 
370 #endif
371 
372 #include "moc_httpserver.cpp"
373